Stakeholder decided: createAssignment() blocks if AssetItem.status isn't in_stock, and update routes use PATCH /api/<entity>/[id] (applies to both Assignment and PurchaseOrder). Co-Authored-By: Claude Sonnet 5 <[email protected]>
36 KiB
Asset & License Management — Dev Notes
Web app nội bộ quản lý tài sản IT + license cho Ubisoft VN local IT. Luồng chính: Request → Approval → Procurement → Receiving → Delivery → Tracking → Thu hồi/Retire.
Business context, data model, luồng trạng thái, case đặc biệt: xem docs/asset-license-management.md — đó là nguồn brainstorm chính, đừng duplicate nội dung đó ở đây, chỉ link tới.
Stack (đã chốt)
- Next.js (TypeScript) full-stack
- DB: Postgres (đã chốt 2026-07-29, xem lý do trong
docs/asset-license-management.md) - DB access: Prisma (ORM + migrations, đã chốt 2026-07-30 — lý do trong
docs/asset-license-management.md). Không viết raw SQL cho app logic nữa — ngoại lệ duy nhất là hạ tầng test (tests/setup.tsdùng rawpgClientđể tạo/xoá throwaway database, đó là test infra chứ không phải data access layer). - Validation: Zod (đã chốt 2026-07-30, đi cùng quyết định Prisma)
- UI/CSS: Tailwind CSS v4 + shadcn/ui (CLI
@latest, hiện tại v4.16.0) — nâng cấp từ Tailwind v3 +[email protected]ngày 2026-07-30, xem lý do quyết định trongdocs/asset-license-management.md. Không còntailwind.config.ts— theme khai báo thẳng trong CSS qua@theme inline { ... }(src/app/globals.css),postcss.config.jschỉ còn plugin@tailwindcss/postcss(không cầnautoprefixerriêng nữa). shadcn config: stylenew-york-v4, base colorneutral, CSS variables bật,tailwind.config: ""(components.json) — đây là style/preset cổ điển (Radix), không phải 8 preset thương hiệu hoá mới của CLI hiện tại (nova/vega/maia/lyra/mira/luma/sera/rhea, chọn qua-p), vốn ép dùng theme "Nova/Vega..." + tuỳ chọn thư viện primitive khác (--base base= Base UI,--base aria= React Aria) — không dùng các preset đó cho project này. Component dùngradix-ui(package hợp nhất, không phải@radix-ui/react-slotriêng lẻ nữa) vàtw-animate-css(thaytailwindcss-animate).globals.csscòn@import "shadcn/tailwind.css"— 1 file CSS runtime do chính packageshadcn(npm) publish kèm nhiều utility (data-state variants, scroll-fade, shimmer...), là dependency thật (không phải chỉ dev tool) vì cần resolve lúc build. Khi thêm component mới:npx shadcn@latest add <component>(đã test non-interactive OK vớicomponents.jsonnày qua flag-y --overwrite). - Self-hosted (theo nguyên tắc chung của workspace — ưu tiên self-host hơn cloud, xem
../processes/new-office-fitout.md)
Coding conventions
Rút ra từ pattern thật đã dùng cho Request (xem src/lib/requests.ts, src/app/api/requests/route.ts, prisma/schema.prisma) — theo đúng pattern này cho các entity tiếp theo, đừng bịa pattern mới:
- 1 file schema Prisma (
prisma/schema.prisma) cho tất cả model — không tách nhiều file. Field trong model dùng camelCase (employeeId), map sang cột DB snake_case qua@map("employee_id"); tên bảng map qua@@map("requests")(snake_case, số nhiều). - 1 file
src/lib/<entity>.tsmỗi entity, chứa cả 3 thứ trong cùng file: Zod schema + hàm validate trả vềValidationResult({ok:true,value}|{ok:false,error}, không throw), các hàm CRUD gọi Prisma Client, và 1 hàmtoRecord()map từ shape Prisma (camelCase) sang shape public API (snake_case — vdemployee_id,created_at). Ranh giới cố ý: nội bộ TS/Prisma dùng camelCase, API trả ra ngoài dùng snake_case. - API route (
src/app/api/<entity>/route.ts) giữ mỏng: parse JSON trong try/catch (400 nếu invalid JSON), checktypeof body === "object" && body !== null(400 nếu không), gọi hàm validate của lib, gọi hàm CRUD, trảNextResponse.json(...)với status code đúng (400 validation lỗi, 201 tạo mới, 200 list). Không viết business logic trong route handler. - Prisma Client: dùng singleton ở
src/lib/prisma.ts(global-guard chống hot-reload spawn nhiều instance) — importprismatừ đó, không tựnew PrismaClient()ở nơi khác. - Enum có tập giá trị cố định gắn với domain (vd
type: asset|license) → Prismaenum, không phảiString+ check tay. - Màu/theme Tailwind: từ khi lên Tailwind v4 (2026-07-30), theme khai báo thẳng trong
src/app/globals.cssqua@theme inline { --color-*: var(--*); ... }— không còntailwind.config.ts. Biến CSS ở:root/.darkđã là giá trịoklch(...)đầy đủ,@theme inlinechỉ alias thẳng quavar(--x), không bọc thêmhsl(...)/rgb(...)(bài học từ bughsl(oklch(...))hồi còn Tailwind v3 +tailwind.config.ts, đã fix rồi bỏ luôn file đó khi lên v4). - Test: 1 file
tests/<entity>.test.tsmirror theo lib file, chạy trên Postgres thật qua DB tạm per-test-file (xemtests/setup.ts) — không mock DB.beforeEachdọn bằngprisma.<model>.deleteMany(). - Schema mới/đổi schema → tạo migration Prisma thật (
prisma migrate dev --name <mô_tả>), không sửa tay SQL.
Git / commit convention
- Message theo Conventional Commits:
feat:,fix:,docs:,chore:,test:,refactor:+ mô tả ngắn gọn ở thì hiện tại (vdfeat: add Employee Prisma model + migration). - Chia nhỏ commit theo lớp kỹ thuật khi build 1 entity/feature mới — không gộp hết vào 1 commit lớn khó review. Thứ tự chuẩn cho 1 entity mới:
feat: add <Entity> Prisma model + migrationfeat: add <Entity> validation + CRUD (src/lib/<entity>.ts)feat: add <Entity> API routestest: add <Entity> API tests
- Nếu 1 lớp cần sửa lại (bug do lớp sau phát hiện), thêm commit
fix:riêng, không gộp ngược vào commit cũ (đã push thì không amend).
- Vẫn chạy xuyên suốt cả entity trong 1 lượt (feature-implementer → test-runner) rồi mới hỏi/báo cáo — không dừng lại xin review sau từng commit nhỏ. User sẽ tự xem lại qua
git log/diff từng commit sau khi xong. - Lưu ý PowerShell trên máy này: đừng để dấu
"(ngoặc kép) lồng bên trong message nhiều dòng khi truyền quagit commit -m— dễ bị PowerShell tách nhầm argument, khiến git báo lỗi pathspec (đã gặp thật, xem cách né ở lịch sử commit). Cách an toàn: gán message vào biến trước ($msg = @'...'@; git commit -m $msg), tránh dấu"trong nội dung, hoặc dùnggit commit -F <file>nếu bắt buộc phải có".
Môi trường
Project này được dùng từ nhiều máy khác nhau (máy Windows công ty, VM Linux ở nhà qua Proxmox) — luôn kiểm tra host hiện tại trước khi giả định cách chạy npm, đừng áp cứng 1 cách cho mọi nơi.
- Máy Windows công ty (
C:\Users\thoangminh\Desktop\ubisoft\asset-license-mgmt): không có Node.js/npm cài native. Mọinpm install/npm test/tscphải chạy qua Docker (node:20container, xem cú pháp bên dưới). Docker Desktop đã confirm hoạt động được trên máy này (dùng để chạy Xibo containers, xem../xibo/). - VM Linux ở nhà (Proxmox): nếu VM có Node.js cài native (kiểm tra bằng
node -v/which nodetrước), chạynpm install/npm test/tsctrực tiếp, không cần wrap qua Docker — chỉ Postgres mới cần Docker (docker compose up -d db), vì đó là quyết định kiến trúc (Postgres qua container), không phải vì thiếu Node. Nếu VM đó cũng không có Node, dùng lại cách wrap Docker như máy Windows. - DB luôn là Postgres qua
docker-compose.yml(servicedb, networkasset-license-mgmt-net, imagepostgres:16-alpine) — cái này giống nhau ở mọi máy, không phụ thuộc host có Node native hay không. Trước khinpm test, phải start Postgres:docker compose up -d db(từ thư mục project).- Nếu phải wrap qua Docker (máy không có Node native): chạy npm trong container gắn cùng network để resolve được hostname
db:docker run --rm --network asset-license-mgmt-net -v "<project dir>:/app" -w /app -e DATABASE_URL="postgres://asset_license_mgmt:change-me-local-dev@db:5432/asset_license_mgmt" node:20 sh -c "npm install && npm test" - Nếu Node native: set
DATABASE_URL(xem.env/.env.example) rồi chạy thẳngnpm install && npm test— Postgres vẫn phải resolve quadbnếu chạy trong Docker network, hoặclocalhost/port map nếu compose có publish port (hiệndocker-compose.ymlkhông publish port ra host, cần thêm port mapping hoặc chạy npm trong cùng Docker network nếu muốn gọi từ ngoài container). - Test tự tạo/xoá một database Postgres tạm (
test_<uuid>) riêng cho mỗi lần chạy file test (xemtests/setup.ts), không đụng vào database dev chính. DATABASE_URLmẫu nằm ở.env.example— copy thành.env(đã gitignore) nếu cần chỉnh local, riêng cho từng máy.
- Nếu phải wrap qua Docker (máy không có Node native): chạy npm trong container gắn cùng network để resolve được hostname
npm installtự chạyprisma generatequa hookpostinstall(xempackage.json) — không cần gọi tay trừ khi thay đổischema.prismamà không chạynpm installlại.pgvẫn còn trongdevDependencies— chỉ dùng cho hạ tầng test (tests/setup.tstạo/xoá throwaway DB), không phải data access layer của app (đó là Prisma). Đừng nhầm là app vẫn dùng raw SQL.
Lưu ý về việc gọi custom agent (ba/pm/feature-implementer/test-runner/code-reviewer)
Các agent định nghĩa ở .claude/agents/ trong thư mục này (asset-license-mgmt/) chỉ được Claude Code nhận diện làm subagent_type nếu phiên Claude Code được mở với working directory là chính thư mục này (hoặc thư mục con của nó) — Claude Code quét .claude/agents/ từ cwd đi lên tới repo root, không quét xuống thư mục con. Nếu mở Claude Code từ ubisoft/ (thư mục cha, gộp nhiều project), các agent này sẽ báo "not found". Cách đúng: cd vào asset-license-mgmt/ rồi mới chạy claude khi làm việc trên project này.
Multi-agent dev workflow
Mục tiêu: stakeholder (user) chỉ cần đưa ra mục tiêu/ưu tiên ở mức nghiệp vụ — không cần tự chia nhỏ task hay chọn giữa các phương án kỹ thuật. Đồng thời không để một agent vừa viết code vừa tự chấm bài mình — tách vai trò để bug bị bắt lại thay vì được tự "cho qua".
Với mỗi mục tiêu nghiệp vụ stakeholder đưa ra (vd "thêm tracking seat cho License", "cho phép reject Request"):
ba— chuyển yêu cầu ở mức business thành spec cụ thể (field, rule, edge case, acceptance criteria), bám theodocs/asset-license-management.md, và note lại spec/quyết định đã chốt vào chính doc đó. Nếu có quyết định nghiệp vụ chưa rõ (ai được approve, xử lýAssignmentkhiLicensebị retire...) → dừng lại hỏi thẳng stakeholder, không tự đoán. Bỏ qua bước này nếu yêu cầu đã đủ nhỏ/rõ (vd 1 bug fix cụ thể).pm— nhận spec từba(hoặc thẳng backlog đã rõ), chia thành danh sách task cụ thể theo đúng thứ tự lớp kỹ thuật chuẩn (model+migration → lib → API → test), sắp theo phụ thuộc, map mỗi task tới đúng 1 commit theo convention ở "Git / commit convention" — tách càng cụ thể càng tốt. Nếu spec thiếu chi tiết kỹ thuật mà việc tự quyết có thể đổi hành vi nghiệp vụ,pmhỏi ngược lạibađể bổ sung spec trước khi chốt task, thay vì tự đoán rồi chia task luôn. Không tự code, không tự quyết nghiệp vụ.feature-implementer— nhận đúng 1 task từpm, viết code + test đi kèm trong cùng lượt. Không tự tuyên bố "đã pass" — chỉ báo cáo đã đổi gì, giả định gì, và cái gì cố tình để ngoài scope.test-runner— spawn riêng, không phải cùng agent vừa code. Chạy toàn bộ test suite + lint + type-check, báo lỗi cụ thể kèm file:line. Không tự sửa code.- Nếu fail: đưa report của test-runner làm input cho một lượt
feature-implementermới để fix. Lặp lại bước 2–3 tới khi xanh hết. code-reviewer(optional, dùng cho thay đổi lớn/rủi ro — vd đổi state machine của Request, logic seat-count license) — review diff cuối cùng trước khi commit, tìm bug logic/security, không tự sửa.
Quy tắc cứng
ba/pmluôn chạy trước khi có task cụ thể chofeature-implementer, trừ khi task đã đủ nhỏ/rõ ràng (bug fix cụ thể, không phải feature mới) — trường hợp đó có thể giao thẳng chofeature-implementer.- Implementer và test-runner luôn là 2 lượt spawn riêng biệt cho cùng 1 task — không gộp.
- test-runner không được tự sửa file, kể cả khi lỗi trông "hiển nhiên".
- Nếu implementer thấy task mơ hồ, chọn giả định nhỏ nhất hợp lý và ghi rõ trong report thay vì dừng lại hỏi — nhưng nếu ảnh hưởng luồng trạng thái Request/quyền approval thì phải hỏi trước khi code.
ba/pmkhông tự quyết nghiệp vụ mơ hồ — luôn đẩy ngược lại stakeholder thay vì đoán.pmgặp gap kỹ thuật ảnh hưởng nghiệp vụ (field/rule/quan hệ chưa nêu trong spec) → hỏi ngược lạiba(không tự bịa),batrả lời bằng cách tradocs/asset-license-management.mdhoặc escalate tiếp lên stakeholder nếu doc cũng chưa có câu trả lời.
Trạng thái hiện tại
-
Task 1 xong: đã bootstrap Next.js (TS, App Router) project thật, entity
Request(chỉ stateDraft) vớiPOST/GET /api/requests. -
Migrate SQLite → Postgres xong: CONFIRMED pass qua test-runner độc lập.
-
ESLint setup xong: CONFIRMED pass sạch qua test-runner độc lập.
-
Retrofit
Requestsang Prisma + Zod xong (2026-07-30):prisma/schema.prisma(modelRequest, enumRequestType),src/lib/prisma.ts(singleton client),src/lib/requests.ts(Zod schema thay hàm validate tay, Prisma Client thay raw SQL),scripts/migrate.ts(gọiprisma migrate deploy),tests/setup.ts/tests/requests.test.ts(schema áp quaprisma migrate deploy, dọn dữ liệu quaprisma.request.deleteMany()).src/lib/db.ts(raw pg Pool cũ) đã xoá. CONFIRMED qua test-runner độc lập: 6/6 test pass trên Postgres thật, type-check sạch, lint sạch. Migration SQL đối chiếu khớp 1:1 với schema cũ (chỉ kháctypetừTEXT+CHECKsang Postgres enum — tương đương semantics). -
npm audit: 1 "critical" nằm ởvitest(devDependency, CVE cầnvitest --uiđang chạy — project không dùng UI mode nên không khai thác được trong cách dùng hiện tại). Rủi ro thực tế thấp, chưa cần fix gấp — xem "Việc còn dở". -
Tailwind + shadcn/ui setup xong (2026-07-30, ban đầu trên v3):
postcss.config.js,src/app/globals.css,components.json,src/lib/utils.ts(cn()),src/components/ui/button.tsx(component mẫu để xác nhận pipeline chạy được). Có 1 bug phát hiện qua verify độc lập (màu token bị bọchsl()chồng lên giá trịoklch()sẵn có → CSS không hợp lệ, silent fail lúc render dù build/lint/tsc đều xanh) — đã fix và CONFIRMED qua verify độc lập lần 2. -
Nâng cấp lên Tailwind v4 + shadcn CLI
@latestxong (2026-07-30): bỏtailwind.config.ts(theme giờ khai trongglobals.cssqua@theme inline), đổipostcss.config.jssang@tailwindcss/postcss, đổi dependency (radix-uithay@radix-ui/react-slot,tw-animate-cssthaytailwindcss-animate, thêmshadcnlàm runtime dependency vìglobals.cssimportshadcn/tailwind.css),button.tsxviết lại theo registrynew-york-v4hiện tại. CONFIRMED qua test-runner độc lập: install/build/tsc/lint sạch, 6/6 test Request vẫn pass (không liên quan tới UI), CSS đã compile xác nhận màu sạch (không còn kiểu bughsl(oklch(...))), không còn gói v3 sót lại.- Rủi ro tồn đọng (dormant, chưa gãy):
button.tsxmới bỏReact.forwardRef(convention của registry hiện tại là cho React 19), nhưng project đang pin React 18.3.1 — nếu sau này có chỗ truyềnrefvào<Button>, ref sẽ không được forward đúng cách. Hiện tại không chỗ nào trong code truyềnrefnên chưa phát sinh lỗi — nhưng cần nhớ khi build UI thật, tránh<Button ref={...}>hoặc phải tự thêm lạiforwardRefnếu cần.
- Rủi ro tồn đọng (dormant, chưa gãy):
-
Git repo khởi tạo + push lên GitHub xong (2026-07-30):
https://github.com/toandev96/ubi-dan-itam.git(private), remoteorigin/branchmaster. Business context (docs/asset-license-management.md) đã move từubisoft/processes/vào trong repo này cùng ngày, để travel theo code khi clone ở máy khác (vd VM Linux ở nhà qua Proxmox) — không còn phụ thuộc thư mụcprocesses/ở workspace ngoài repo nữa cho riêng phần business context của app này. -
Auth (login nội bộ, không phải SSO công ty) xong (2026-07-31):
UserPrisma model (prisma/schema.prisma),src/lib/users.ts(Zod +createUser()+verifyUserCredentials(), bcryptjs hash),scripts/create-user.ts(tạo account bằng CLI, không có self-registration), Auth.js v5 (next-auth@^5.0.0-beta.32, Credentials provider, JWT session, không dùng@auth/prisma-adaptervì không cần OAuth/DB session) ởsrc/auth.ts+src/app/api/auth/[...nextauth]/route.ts, trang loginsrc/app/login/page.tsx. Permission model flat (bất kỳ User nào login cũng làm mọi thứ được — quyết định đã chốt, không córole). CONFIRMED qua verify độc lập (test-runner + security review): 16/16 test pass (6 cũ + 10 mới) trên Postgres thật, tsc/lint/build sạch, không lộ password, bcrypt compare đúng chuẩn constant-time,AUTH_SECRETfail-closed thật khi thiếu (đã test thực tế, không fallback giá trị đoán được).- Finding nhỏ (chưa block, cần fix sau): timing side-channel trong
verifyUserCredentials— trả về sớm khi email không tồn tại (bỏ qua bcrypt compare), nên request với email sai vs password sai có thời gian phản hồi khác nhau chút, có thể dò được email nào tồn tại. Fix: luôn chạybcrypt.compare()kể cả khi không tìm thấy user (so với 1 hash giả cố định). - Finding nhỏ (chấp nhận được, cần track):
scripts/create-user.ts --password <giá trị>lộ password vào shell history/process list — chấp nhận vì chỉ IT admin tự chạy tay, không phải self-registration công khai, nhưng nên cải thiện sau (đọc từ prompt/stdin thay vì argv). - Note triển khai: chạy
next start(production) cần thêmAUTH_TRUST_HOST=truehoặcAUTH_URLtrong env, nếu không sẽ gặp lỗiUntrustedHost— đã thêm vào.env.example/.envkhi deploy thật (xem bullet deploy bên dưới).
- Finding nhỏ (chưa block, cần fix sau): timing side-channel trong
-
Route-gating xong (2026-07-31):
src/middleware.ts(protect mọi page route theo default-deny, redirect/loginnếu chưa đăng nhập, trừ/loginchính nó — tránh vòng lặp redirect),src/auth.config.ts(config Auth.js edge-safe, tách ra khỏisrc/auth.ts— không có Credentials provider vìauthorize()gọiverifyUserCredentials()kéo theobcryptjs, là Node API không chạy được trên Edge Runtime màmiddleware.tsdùng;src/middleware.tstự tạo instanceNextAuth(authConfig)riêng, nhẹ, chỉ để decode JWT session cookie — xác nhận qua build thật: trước khi tách,next buildbáo warning bcryptjs/jose bundle vào Edge Runtime (middleware 115kB); sau khi tách, build sạch, middleware còn 78.9kB).src/auth.tsgiờ spreadauthConfig+ thêm Credentials provider thật, dùng cho API route/Server Component (chạy Node runtime).config.matchercủa middleware loại trừ toàn bộ/api(không chỉ/api/auth) — API route tự checkauth()lấy session ngay trong handler (không dựa hoàn toàn vào middleware, theo đúng khuyến nghị chính thức của Auth.js: verify session càng gần chỗ fetch data càng tốt)./api/requests(GET/POST) đã thêm checkauth(), trả401 {"error":"Unauthorized"}nếu chưa login — xemsrc/app/api/requests/route.ts./(home page placeholder) có yêu cầu login — chọn theo nguyên tắc "mặc định chặn hết, allowlist rõ ràng cho cái không cần" thay vì opt-in dễ quên route mới.- Test:
tests/requests.test.tsmock@/auth(vi.mock) thay vì gọiauth()thật, vìauth()không tham số đọc cookie quanext/headers— chỉ hoạt động khi chạy trong request-handling thật của Next.js, không phải khi test import gọi thẳngGET()/POST()(bypass hoàn toàn Next). Test mặc định mock session hợp lệ (beforeEach) để không phá test nghiệp vụ cũ, có riêng 2 test set mock vềnullđể xác nhận 401.middleware.tsbản thân không có test tự động (chạy Edge Runtime, không test được qua vitest theo cách hiện tại) — đã verify tay quanext build+next startthật (curl xác nhận:GET /→ 307 →/login,GET /login→ 200 không loop,GET/POST /api/requestschưa login → 401 JSON,GET /api/auth/session→ 200 không bị chặn). CONFIRMED:npx tsc --noEmit,npm run lint,npm test(18/18, gồm 2 test 401 mới),npm run buildđều sạch.
-
Employeeentity xong +Request.employee_idretrofit sang FK thật (2026-07-31):EmployeePrisma model (prisma/schema.prisma—name,department(string đơn giản, chưa tách entity riêng),managerIdself-relation nullable),src/lib/employees.ts(Zod + CRUD, theo đúng pattern chuẩn ở "Coding conventions"),src/app/api/employees/route.ts(POST/GET, gateauth()như/api/requests),tests/employees.test.ts(11 test, Postgres thật).Request.employeeId— trước đây làStringtự do, không có ràng buộc — giờ là FK thật tớiEmployee(@relation, migration20260731075828_relate_request_to_employee). Đây là thay đổi hành vi có chủ ý tới APIRequestđã ship trước đó:employee_idtrong bodyPOST /api/requestsgiờ bắt buộc là UUID hợp lệ (Zod.uuid(), không còn nhận string tuỳ ý như"emp-1"), và phải trỏ tới mộtEmployeecó thật — nếu không,createRequest()bắt lỗi FK violation của Prisma (P2003) và trả400 {"error": "employee_id does not reference an existing employee"}thay vì lỗi 500 thô.tests/requests.test.tsđã cập nhật: mọi test tạoRequestgiờ tạoEmployeethật trước qua helpercreateTestEmployee()rồi dùng.idthật, có thêm 2 test mới cho đúng 2 case bị FK chặn (UUID sai format, UUID đúng format nhưng không tồn tại).- Bug hạ tầng test phát hiện + fix trong lượt này:
tests/setup.tstạo DB Postgres tạm riêng cho mỗi file test bằng cách ghi đèprocess.env.DATABASE_URLtrongbeforeAll— cách này không an toàn khi Vitest chạy nhiều file test song song (mặc định): timing giữabeforeAll/query của 2 file có thể chồng nhau khiến cả 2 cùng thao tác nhầm lên 1 DB tạm. Trước đây không lộ ra vì chưa có 2 file test nào cùng đụng bảng có FK với nhau; lộ ra ngay khiRequestcó FK thật tớiEmployee(tests/employees.test.tsvàtests/requests.test.tsxoá/đọc nhầm dữ liệu của nhau, lặp lại 100% mỗi lần chạy). Fix:vitest.config.tsthêmfileParallelism: false(chạy tuần tự từng file) — CONFIRMED: 31/31 test pass ổn định quanpm test(11 employees + 10 requests + 10 auth),npx tsc --noEmit/npm run lint/npm run buildđều sạch.
- Bug hạ tầng test phát hiện + fix trong lượt này:
-
AssetItementity xong (2026-07-31):AssetItemPrisma model + enumAssetItemStatus(in_stock/assigned/repair/retired, defaultin_stock) —serialNumber(unique),model,purchaseDate,warrantyEndDate(nullable — không phải asset nào cũng biết hạn bảo hành lúc nhập), migration20260731090347_add_asset_item.src/lib/assets.ts(Zod + CRUD, theo đúng pattern chuẩn ở "Coding conventions", bắt lỗi PrismaP2002khiserialNumbertrùng và trả400 {"error": "serialNumber already exists"}thay vì lỗi 500 thô),src/app/api/assets/route.ts(POST/GET, gateauth()như/api/employees),tests/assets.test.ts(11 test, Postgres thật). Asset mới luôn tạo ở statusin_stock(không nhận status từ client lúc tạo, nhưRequestluôn tạoDraft) — các state khác (assigned/repair/retired) sẽ set qua thao tác sau, chưa có endpoint riêng. Chưa có quan hệ tớiVendor/Assignment(2 entity đó chưa build). CONFIRMED qua test-runner độc lập: 42/42 test pass (11 assets + 11 employees + 10 requests + 10 auth) trên Postgres thật,npx tsc --noEmit/npm run lint/npm run buildđều sạch. -
Deploy lên homelab xong (2026-08-02):
Dockerfile(single-stagenode:20-alpine,npm ci+npm run build+npm start) và serviceappmới trongdocker-compose.yml(port3002:3000, cùng networkasset-license-mgmtvớidbđể resolve hostnamedb). Gotcha đã gặp + fix:node:20-alpinekhông cólibsslsẵn → Prisma không detect được OpenSSL lúcnpm ci(postinstall chạyprisma generate), tự chọn nhầm engine binary (openssl-1.1.x) — build vẫn xanh (route API là dynamic, không chạm Prisma lúc build) nhưng crash lúc runtime khi query thật. Fix:RUN apk add --no-cache openssltrongDockerfiletrướcnpm ci. Đã verify bằng cách gọiprisma.employee.count()thật bên trong container sau khi rebuild — pass. Expose ra internet quaubi.toanhoang.worktheo đúng flowadd-serviceskill ở/opt/homelab/CLAUDE.md: subdomain quacloudflare-ddns-ubi, NPM proxy host (10.8.2.3:3002, SSL forced) + Let's Encrypt, Unbound Host Override cho LAN.AUTH_TRUST_HOST=truethêm vào.env/.env.example(đóng TODO cũ). Chưa có user nào trong DB — chạydocker compose exec app npm run user:createđể tạo account đầu tiên trước khi ai có thể login thật. -
Licenseentity xong (2026-08-02):LicensePrisma model + enumLicenseType(perpetual/subscription) —name,vendor(string đơn giản, chưa tách entityVendorriêng, theo đúng precedentEmployee.department),seatsTotal(bắt buộc> 0),seatsUsed(default0, client không set được lúc tạo — giốngAssetItem.statusluôn tạoin_stock),expiryDate(nullable — perpetual không hết hạn), migration20260802123955_add_license.src/lib/licenses.ts(Zod + CRUD),src/app/api/licenses/route.ts(POST/GET, gateauth()như các entity khác),tests/licenses.test.ts(27 test, cả lib-level lẫn route-level, có thêm test malformed JSON/non-object body — đóng luôn 1 phần gap coverage cũ, nhưng chỉ cholicenses, chưa retrofit lại choassets/employees/requests). CONFIRMED qua test-runner độc lập: 69/69 test pass, tsc/lint sạch. Review quacode-reviewer(trigger: seat-count logic): không có bug chặn, nhưng có finding cần nhớ —createLicense()trảPromise<LicenseRecord>trần (không theo pattern{ok,value}|{ok,error}nhưcreateAssetItem()); vô hại bây giờ vìLicensechưa có unique constraint nào, nhưng nếu sau này thêm@@unique(vd trênname+vendor) mà không đồng thời thêm xử lý lỗiP2002ởcreateLicense(), request trùng sẽ trả 500 thô thay vì400sạch như các entity khác. Chưa fix vì hiện tại xử lý lỗi cho constraint không tồn tại là dư thừa — cần nhớ làm cùng lúc nếu/khi thêm unique constraint.
Việc còn dở
fileParallelism: false(fix race condition test-infra) là fix đúng nhưng "cùn" — chặn song song toàn bộ suite thay vì chỉ fix gốc rễ (tests/setup.tsghi đèprocess.env.DATABASE_URLtoàn cục thay vì truyền connection string riêng cho từng Prisma Client instance). Test suite sẽ chạy chậm dần tuyến tính khi thêm entity mới (AssetItem,License...). Cân nhắc refactortests/setup.tsđể mỗi file tự có Prisma Client riêng trỏ đúng DB tạm của mình, rồi bật lại parallelism — chưa gấp ở quy mô hiện tại (3 file, ~15s).- Fix timing side-channel ở
verifyUserCredentials(xem note ở "Trạng thái hiện tại"). - Cải thiện
scripts/create-user.tsđể password không lộ qua CLI arg (đọc từ stdin/prompt thay vì--password). - Bump
vitestlên 4.x (fix advisory critical GHSA-5xrq-8626-4rwp — hiện không khai thác được vì project không chạyvitest --ui, nhưng nên dọn khi có breaking-change window) — không khẩn cấp. - Rà lại advisory riêng của
next(prod dependency, có vài high/moderate — vd SSRF qua WebSocket upgrade, middleware bypass) khi có dịp, tách biệt khỏi đợt retrofit Prisma/Zod này. - Test coverage gaps (chưa block gì, chỉ là follow-up): chưa test malformed JSON body, chưa test JSON body không phải object (array/string).
Button(shadcn) khôngforwardRef, React đang pin 18 — dormant risk, xem note ở "Trạng thái hiện tại".Vendor/PurchaseOrder(gộpReceipt)/Assignment/AssetCategoryAttributeDef(2026-08-02): spec đầy đủ ởdocs/asset-license-management.md, đã có task list phụ thuộc-đúng dopm-pass sequencing (2026-08-02), sẵn sàng giaofeature-implementertheo đúng thứ tự dưới đây (3 lane độc lập, có thể xen kẽ):- Lane A (Vendor → License.vendorId → PurchaseOrder → purchaseOrderId x3): (1-4)
Vendorentity chuẩn 4 commit (model+migration/lib/API/test), không phụ thuộc gì. (5) RetrofitLicense.vendor(string) →License.vendorId(FK) — 1 commit gộp (schema+migration+lib+test), theo đúng tiền lệ retrofitRequest.employeeId(commit49d96b7, không tách lớp như entity mới vì tách ra sẽ để lại state nửa vời không chạy được) — tiện thể fix luôn finding cũ củacode-reviewer:createLicense()chưa theo pattern{ok,value}|{ok,error}, giờ bắt buộc phải thêm vìvendorIdlà FK mới (cần bắtP2003). Phụ thuộc (1). (6-9)PurchaseOrderentity chuẩn 4 commit (vendorIdFK,receivedByFK→Employee, Zod.refine()cho ràng buộcreceivedAt/receivedBycùng null hoặc cùng có giá trị, bắtP2003cho cả 2 FK, theo đúng pattern{ok,error}ngay từ đầu) — phụ thuộc (1)Vendor+Employee(đã có). (10)AssetItem.purchaseOrderId— 1 commit gộp (nullable FK). (11)License.purchaseOrderId— 1 commit gộp riêng với task (5) dù cùng file, vì trigger phụ thuộc khác nhau (VendorvsPurchaseOrder) — gộp chung sẽ vi phạm nguyên tắc 1-commit-1-thay-đổi-nghiệp-vụ. (12)Request.purchaseOrderId— 1 commit gộp. (10)(11)(12) đều chỉ phụ thuộc (6), độc lập lẫn nhau, thứ tự giữa 3 task này không quan trọng. - Lane B (AssetCategory → AssetCategoryAttributeDef + AssetModel → wire dynamic validation → AssetItem.modelId): gap phát hiện khi sequencing —
AssetCategory/AssetModel/ConsumableItemđã có spec từ round BA trước (2026-08-02, xem "Phân loại tài sản" trong doc) nhưng CHƯA build (không có trongprisma/schema.prismahiện tại,AssetItem.modelvẫn là string tự do) — phải làm trước thìAssetCategoryAttributeDefmới có ý nghĩa. (13-16)AssetCategoryentity chuẩn 4 commit (name,valueGroupenumhigh_value/low_value), không phụ thuộc gì. (17-20)AssetCategoryAttributeDefentity chuẩn 4 commit (categoryIdFK,key,label,dataTypeenum,required,sortOrder, unique(categoryId,key)→ bắtP2002) — phụ thuộc (13), độc lập vớiAssetModel. (21-24)AssetModelentity chuẩn 4 commit (categoryIdFK + business rule "category phảivalueGroup=high_value" — cần lookup DB, không phải Zod thuần;manufacturer,name,specsJSONB — ở task nàyspecschỉ validate lỏng là object hợp lệ, CHƯA khớp động theoAssetCategoryAttributeDef) — phụ thuộc (13), độc lập với (17-20). (25) Wire dynamic specs validation vàoAssetModelcreate/update — 1 commit riêng, không gộp vào (22), vì đây là logic mới/rủi ro (build Zod schema runtime từAssetCategoryAttributeDef, mapdataType→z.string()/number()/boolean()/coerce.date(), từ chối key lạ, bắt buộc keyrequired) — phụ thuộc (18) và (22). (26) RetrofitAssetItem.model(string) →AssetItem.modelId(FK) — 1 commit gộp, phụ thuộc (21) (không cần đợi (25)). - Lane C (
Assignment, độc lập hoàn toàn với Lane A/B — chỉ cầnEmployee+AssetItem+Licenseđã có sẵn): (27) model+migration (employeeIdFK,assetItemId/licenseIdnullable FK,assignedAt,returnedAtnullable,signedAtnullable). (28) lib validation+CRUD — rủi ro nhất trong cả 4 entity: Zod.refine()XORassetItemId/licenseId, chặn >1Assignmentactive/AssetItem, transaction tăng/giảmLicense.seatsUsedkèm chặn overbook (seatsUsed >= seatsTotal), đồng bộAssetItem.status(assigned/in_stock), helper transfer (đóng cũ+mở mới cùng 1 transaction). (29) API routes — cần quyết định shape mới: đây là entity đầu tiên cần thao tác "update" (return/transfer) trong khi codebase hiện tại mọi entity đều chỉ có POST(create)+GET(list), chưa có route update nào — đề xuất mặc địnhPOST /api/assignments(assign/transfer) +PATCH /api/assignments/[id](return, nhận{returnedAt}}), cần xác nhận lại khi giao task chopm/feature-implementervì không có tiền lệ. (30) tests. (31) Bắt buộc code-reviewer pass riêng cho Assignment (diff 27-30) trước khi coi là xong — đúng rule ở "Multi-agent dev workflow" (state-mutation-với-invariant, seat-count) — không phải 1 commit, là 1 gate. - 2 câu hỏi mở đã được stakeholder trả lời (2026-08-02, xem "Assignment" trong
docs/asset-license-management.md) — Lane C hết bị chặn: (a)createAssignment()chặn nếuAssetItem.statuskhácin_stock(400). (b) Route update dùngPATCH /api/<entity>/[id](áp dụng cho cảAssignmentvàPurchaseOrder), không dùng action-based sub-route. ConsumableItem(phần catalog, KHÔNG phải luồng "xin+nhận" — luồng đó vẫn BỊ CHẶN chờ 7 câu hỏi mở, xem doc): độc lập với toàn bộ 3 lane trên (chỉ cầnAssetCategory), sẵn sàng chopmchia task riêng nếu stakeholder muốn gộp vào cùng đợt — không đưa vào sequencing này vì nằm ngoài phạm vi 4 việc được giao lần này.
- Lane A (Vendor → License.vendorId → PurchaseOrder → purchaseOrderId x3): (1-4)
- Các state tiếp theo của Request: Pending Approval → Approved → Ordered → Received → Delivered → Closed, và nhánh Rejected/Cancelled
- UI (hiện chỉ có placeholder home page)
- Auth/phân quyền (ai được approve request, ai được xác nhận nhận hàng/bàn giao)
Cập nhật lần cuối: 2026-08-02 (deploy lên homelab ubi.toanhoang.work, License entity, task sequencing cho Vendor/PurchaseOrder/Assignment/AssetCategoryAttributeDef)