wal: flush 20260712T1757
This commit is contained in:
@ -27,8 +27,12 @@ Sheet1 · 31 col · **header ROW 4** · 4 data row (R5-R8 = 4 NCC đã seed prod
|
||||
5. `invest:dedup-validation-rules` — dedup-by-MST edge (null/dup), R6 publish-guard, reconcile 22 NCC prod, test cases
|
||||
|
||||
## Status
|
||||
- [!] invest ensemble **RUNNING** (`wf_e7896ca6-901`)
|
||||
- next: em-main (single-writer) synthesize 5 lane RETURN → `spec-supplier-import-v2-12-07-2026.md` (3-mục: ①đề-bài ②cách-implement ③checklist) → present open-questions cho anh Kiệt.
|
||||
- [x] invest ensemble **DONE** (`wf_e7896ca6-901`) — 4/5 lane structured (schema · import-service · fe-ux · dedup-validation) + 1 lane `public-visibility-consumers` **#53-garble** → em-main-solo recover (grep BE 16 + FE 11 consumer).
|
||||
- [x] spec-file **WRITTEN** → `spec-supplier-import-v2-12-07-2026.md`
|
||||
- [!] **CHỜ anh Kiệt chốt** D1 (Mã NCC identity) · D2 (publish gate R4 vs R6) · D3 (authz) → rồi HMW Opus implement.
|
||||
|
||||
## Synthesis
|
||||
_(pending — điền sau khi 5 lane về; em-main refute + synthesize P3/P4)_
|
||||
## Synthesis (em-main refute + P3/P4)
|
||||
**Hội tụ 4 lane (auto-decide):** IsPublic bool (KHÔNG đụng SupplierStatus) · dedup MST-primary+Code-backstop (chống 500) · backfill 22 prod→public · PublishSupplierCommand riêng (né #73) · template BE-gen ClosedXML · LayoutValid re-bake 30-token-thật (đã extract byte-exact).
|
||||
**Chẻ đôi (→ anh chốt):** D1 Mã-NCC = Code[schema-lens] vs field-mới[import-lens]. D2 R4-strict vs R6-literal (mâu thuẫn thật). D3 authz FE⧣BE mismatch.
|
||||
**Recover visibility:** consumers = PE picker (PeWorkspaceCreateView/PeDetailTabs) · ContractCreatePage · CreateContractFromEvaluationFeatures · dashboard — đều qua ListSuppliersQuery → filter `Published`. HMW grep-all trước implement.
|
||||
→ Chi tiết đầy đủ ở `spec-supplier-import-v2-12-07-2026.md`.
|
||||
|
||||
@ -0,0 +1,115 @@
|
||||
# SPEC — Supplier import v2 (12-07-2026, S113)
|
||||
|
||||
- **run:** `wf_e7896ca6-901` (fable-clone ensemble 5 lane investigator-codebase; 4 structured + 1 garble#53 [visibility] recover em-main-solo)
|
||||
- **engine:** `fable-clone-ensemble · Opus-S113` · **owner:** anh Kiệt · **lead:** Opus 4.8 (1M) Max
|
||||
- **source file:** `C:\Users\pqhuy\Downloads\Data base - 1_7_2027.xlsx` (Sheet1, header ROW 4, 30 cột, 4 data row)
|
||||
- **downstream:** HMW Opus 4.8 MAX implement THEO spec này (sau khi anh Kiệt chốt §Open-Decisions).
|
||||
|
||||
---
|
||||
|
||||
## ① TÍNH NĂNG / ĐỀ BÀI
|
||||
|
||||
Nâng cấp Supplier Excel-import (S112 Phase B) thành v2:
|
||||
- **R1** Import đọc ĐÚNG cấu trúc file thật (header ROW 4, 30 cột).
|
||||
- **R2** Nút "Tải file mẫu" — download template xlsx trống đúng layout.
|
||||
- **R3** Sau upload: bước nhập **Mã NCC** thủ công per-dòng (file KHÔNG có cột này).
|
||||
- **R4** Thiếu Mã NCC → **draft/ẩn**, không public (nháp trước khi public).
|
||||
- **R5** Dedup theo **MST** (Mã số thuế, cột 12).
|
||||
- **R6** Không MST **VÀ** không Mã NCC → không được public.
|
||||
- **R7** List NCC hiện trạng thái **Public / đang-ẩn(draft)**.
|
||||
|
||||
**PHÁT HIỆN GỐC (mọi lane đồng thuận):** `Supplier.Code` (comment "Mã NCC") nhưng S112 import NHỒI c4 "TÊN VIẾT TẮT" vào Code. Field mang nhãn "Mã NCC" đang chứa viết-tắt → đây chính là chỗ R3 muốn tách.
|
||||
|
||||
---
|
||||
|
||||
## ★ DECISIONS — anh Kiệt ĐÃ CHỐT (S113)
|
||||
|
||||
> ✅ **D1 = A** (Mã NCC = `Code`) · ✅ **D2** (Code = cổng public; MST = cảnh-báo-mềm; existing-public giữ nguyên; logic import-scope) · ✅ **D3** (nới quyền: mọi user có quyền cập-nhật danh-mục, KHÔNG admin-only). Chi tiết áp + API-contract ở cuối mục. Phần option gốc bên dưới giữ để tra cứu.
|
||||
|
||||
### CHỐT áp dụng:
|
||||
- **D1=A:** Mã NCC = `Supplier.Code`. KHÔNG field mới, KHÔNG ShortName. Import pre-fill Code từ c4 (editable grid). Draft-row Code="" → filtered-unique đổi `WHERE IsDeleted=0 AND Code<>''`. Code non-empty = publish-able, "" = draft.
|
||||
- **D2:** `IsPublic` tại confirm = `!IsNullOrWhiteSpace(Code)` [R4]. MST thiếu → KHÔNG chặn, WARNING preview "⚠️ Chưa có MST — không kiểm tra được trùng, có thể bị trùng" [R6→mềm]. 22 prod public giữ (backfill=1). List toggle "Công bố/Ẩn" (PublishSupplierCommand, gate Code-non-empty).
|
||||
- **D3:** BE import preview/confirm + template + publish: `[Authorize(Roles="Admin,CatalogManager")]` → policy quyền **Suppliers Update/Create** (danh mục). FE guard = PermissionGuard Suppliers Update/Create (khớp BE, hết 403).
|
||||
|
||||
### API CONTRACT (chốt cho BE⟂FE parallel):
|
||||
- `SupplierDto` +`bool IsPublic` (append CUỐI positional record → sửa 2 site projection khít: `ListSuppliersQuery:48-54` + `GetSupplierQuery:21-26`).
|
||||
- `GET /api/suppliers?published={true|false}` — ListSuppliersQuery +`bool? Published`; null=all (admin quản lý).
|
||||
- `POST /api/suppliers/{id}/publish` body `{ maNcc?: string, publish: boolean }` → PublishSupplierCommand (nếu maNcc gửi: set Code + unique-CI-check; publish=true gate Code-non-empty; KHÔNG dùng UpdateSupplier).
|
||||
- `GET /api/suppliers/import/template` → .xlsx (ClosedXML, 30 token row-4, sheet "Sheet1", freeze row-4).
|
||||
- Import confirm rows: Code editable per-row; confirm set `IsPublic=has-Code`; preview row DTO +`bool MstMissing` (FE render warning).
|
||||
|
||||
---
|
||||
|
||||
## (option gốc — tra cứu) OPEN-DECISIONS ban đầu
|
||||
|
||||
**D1 (GỐC — chi phối toàn bộ) — "Mã NCC" là field nào?**
|
||||
- **Opt A** — Mã NCC = `Supplier.Code` hiện có; c4 "TÊN VIẾT TẮT" chuyển sang field MỚI `ShortName`. Import thôi bắt buộc Code; draft-row Code="". Đổi filtered-unique `WHERE IsDeleted=0 AND Code<>''`. Contract-number `SOL&{Code}` giữ đúng nghĩa. *(schema-lens đề xuất; churn cao hơn + cần guard "contract phải có Code")*
|
||||
- **Opt B** — Mã NCC = field MỚI `PublicCode`; `Code` giữ = c4 viết-tắt (required/unique/dedup-backstop/codegen giữ nguyên). *(import-service-lens đề xuất — go-live rủi-ro-thấp nhất; nhược: Code mang nhãn "Mã NCC" thành sai)*
|
||||
- **Lead lean:** **Opt B** cho go-live an toàn (codegen + dedup-backstop nguyên vẹn), NHƯNG cách anh diễn đạt nghiêng A → **cần anh chốt rõ**.
|
||||
|
||||
**D2 — Cổng public (R4 vs R6 mâu thuẫn thật):** R4="không Mã NCC→ẩn" (Mã NCC bắt buộc) vs R6="không MST VÀ không Mã NCC→ẩn" (MST thay được).
|
||||
- **Opt 1 (R6-literal):** public ⟺ có Mã NCC **HOẶC** có MST.
|
||||
- **Opt 2 (R4-strict):** public ⟺ **bắt buộc** có Mã NCC (MST không mở khóa).
|
||||
- **Lead lean:** **Opt 2** (khớp R3/R4 "user tự nhập Mã NCC mới hiện"); import luôn tạo draft, publish là bước người làm tay. Enforce server-side trong `PublishSupplierCommand`.
|
||||
|
||||
**D3 — Authz nút Import/Template:** FE gate `Suppliers/Create` (mở mọi user từ S59) ⧣ BE import `[Authorize(Roles="Admin,CatalogManager")]` → non-admin bấm bị 403.
|
||||
- **Lead lean:** siết FE nút Import+Template về role `Admin/CatalogManager` cho khớp BE (đây cũng là follow-up S112 "FE-Import role-gate"). *(Hoặc nới BE — anh chọn.)*
|
||||
|
||||
**D-auto (lead tự quyết, đồng thuận 4 lane — anh phủ quyết nếu muốn):**
|
||||
- Trạng thái = **`bool IsPublic` default false** (KHÔNG đụng enum `SupplierStatus` = business-status c27).
|
||||
- Dedup = **MST-primary + Code-backstop** (chống 500 unique-violation), normalize MST (Trim+bỏ space nội, CI); MST rỗng → KHÔNG coi trùng nhau.
|
||||
- Backfill **22 NCC prod → IsPublic=true** (đang live, PE/Contract tham chiếu — để draft sẽ ẩn NCC đang dùng + vỡ picker).
|
||||
- Publish qua **`PublishSupplierCommand` riêng** (chỉ set Mã NCC + IsPublic), KHÔNG dùng `UpdateSupplierCommand` (blind absolute-set → #73 clobber data draft).
|
||||
- Template R2 = **BE endpoint** `GET /suppliers/import/template` (ClosedXML từ token single-source, 0 drift).
|
||||
|
||||
---
|
||||
|
||||
## ② CÁCH IMPLEMENT
|
||||
|
||||
### BE — Migration 64 `AddSupplierPublishState` (3-file rule)
|
||||
- `Supplier.cs` +`bool IsPublic` (default false). [D1=A] thêm `string? ShortName` + đổi comment Code. [D1=B] thêm `string? PublicCode`.
|
||||
- `SupplierConfiguration.cs` +`Property(IsPublic)`; [D1=A] đổi unique-filter `[Code]<>''` (DropIndex→CreateIndex); [D1=B] index giữ nguyên + (option) config PublicCode.
|
||||
- Migration Up: `AddColumn<bool>("IsPublic", nullable:false, defaultValue:false)` + `Sql("UPDATE Suppliers SET IsPublic=1")` backfill (idempotent). Down: DropColumn. **No new table (89 giữ).**
|
||||
- `DbInitializer.cs` seed IsPublic + [D1] ShortName/PublicCode cho 4 real + 16 sample (4 real: IsPublic=true).
|
||||
|
||||
### BE — `SupplierExcelImportService.cs`
|
||||
- **R1 LayoutValid (CRITICAL):** re-bake `ExpectedHeaderTokens` BYTE-EXACT theo 30 token THẬT dưới đây; re-run test Case 9. Sửa test drift `NormalizeJoin.Norm` thiếu `.Normalize(FormC)` (khớp service :313).
|
||||
- **R5 dedup→MST:** build `existingByTaxCode` (bỏ blank) + GIỮ `existingByCode` backstop. `NormalizeMst`=Trim+strip inner ws, CI. Precedence: MST-match→Update · else Code-match→Update · else New. In-batch: `batchByMst`+`batchByCode`. MST rỗng → rơi Code-path, KHÔNG merge 2 blank-MST. Update = FillNulls (#73-safe).
|
||||
- **R3 Mã NCC per-row:** [D1=A] preview để Code editable, confirm round-trip `row.code`. [D1=B] +`string? ManualCode` vào `SupplierImportRowDto` round-trip → NewSupplier ghi vào `PublicCode`.
|
||||
- **R4 draft:** confirm set `IsPublic=false` cho MỌI new-row (file không có Mã NCC → tất cả nháp). Update-path KHÔNG demote NCC đang public. +`DraftCount` vào ResultDto.
|
||||
- Endpoint preview/confirm giữ chữ ký (rows mang thêm field). **NEW** `GET /suppliers/import/template` (ClosedXML, sheet "Sheet1", 30 token vào ROW 4, freeze row 4) via `BuildTemplate()` trên `ISupplierExcelImportService`.
|
||||
|
||||
### BE — Publish + List filter
|
||||
- **NEW** `PublishSupplierCommand(Guid Id, string MaNcc, bool publish)`: load entity → nếu publish: enforce D2-gate (Opt2: Mã NCC non-empty; Opt1: Mã NCC OR MST) + unique-CI → set Mã-NCC-field + IsPublic → save. Endpoint `POST /suppliers/{id}/publish`.
|
||||
- `ListSuppliersQuery` +`bool? Published` → `Where(x=>x.IsPublic==published)` khi có. `SupplierDto` +`IsPublic` (sửa CẢ 2 site projection: `ListSuppliersQuery:48-54` + `GetSupplierQuery:21-26` — positional record khít thứ tự).
|
||||
- **R4/R6 VISIBILITY (recover lane):** consumer pass `published=true` → ẩn draft. Consumers cần lọc: PE picker (`PeWorkspaceCreateView.tsx`, `PeDetailTabs.tsx`), `ContractCreatePage.tsx`, `CreateContractFromEvaluationFeatures.cs`, `PurchaseEvaluationSupplierFeatures.cs`, dashboard. **HMW PHẢI grep MỌI consumer** (bài học cardinality S87/S88) + quyết nơi nào lọc public-only, nơi admin-quản-lý thấy cả draft.
|
||||
|
||||
### FE — 2 app SHA-mirror (Pattern 16-bis)
|
||||
- `types/master.ts` (2 app): `Supplier` +`isPublic: boolean` (+[D1=B] `publicCode`).
|
||||
- `SuppliersPage.tsx` (2 app): +cột "Trạng thái" badge Public(emerald)/Nháp-ẩn(slate) [Pattern 14 full-class-literal] + nút "Công bố" (guard Suppliers/Update, mở dialog nhập Mã NCC→POST publish) + filter select + [D3] siết guard nút Import.
|
||||
- `SupplierImportDialog.tsx` (2 app): +nút "Tải file mẫu" (blob-download mẫu `PeDetailTabs.tsx:3273-3279`) + cột "Mã" editable (`updateRow` patch state) + helper "NCC chưa nhập Mã sẽ lưu nháp (ẩn)" + nới `disabled` chỉ chặn lỗi THẬT (layout/dup), KHÔNG chặn vì thiếu Mã NCC.
|
||||
|
||||
### 30 TOKEN HEADER THẬT (row 4 — bake byte-exact vào ExpectedHeaderTokens)
|
||||
```
|
||||
c1 STT · c2 GÓI THẦU · c3 "PHÂN LOẠI\n(NTP/NCC/Cả hai)" · c4 "TÊN VIẾT TẮT\n(Dùng trong HĐ)" · c5 "TÊN CÔNG TY\n(Đầy đủ, đúng pháp lý)" · c6 "ĐỊA CHỈ XUẤT HÓA ĐƠN\n(Địa chỉ đăng ký kinh doanh)" · c7 "ĐỊA CHỈ VĂN PHÒNG \n(nếu có)" · c8 "SỐ ĐIỆN THOẠI\nCÔNG TY" · c9 FAX · c10 "SỐ TÀI KHOẢN+ TÊN+CN. NGÂN HÀNG\n(Đầy đủ, đúng pháp lý)" · c11 "SỐ TK PHỤ\n(nếu có)" · c12 MÃ SỐ THUẾ · c13 "NGƯỜI ĐẠI DIỆN\nPHÁP LUẬT" · c14 "CHỨC VỤ\nĐẠI DIỆN" · c15 "GIẤY ỦY QUYỀN\n(số, ngày, người ủy quyền)" · c16 Link GUQ · c17 Link GPKD · c18 Link HSNL · c19 "NGƯỜI LIÊN HỆ\nCHÍNH" · c20 "CHỨC VỤ\nNGƯỜI LH" · c21 SĐT CHÍNH · c22 EMAIL · c23 ĐỊA CHỈ GỬI THƯ · c24 "NGƯỜI NHẬN THƯ/\nSDT" · c25 "NGUỒN\nGIỚI THIỆU" · c26 "NGƯỜI PHỤ TRÁCH\n(PMH)" · c27 "TÌNH TRẠNG\nHIỆN TẠI" · c28 "GHI CHÚ / LÝ DO\nBLACKLIST" · c29 "NGÀY CẬP NHẬT\nCUỐI" · c30 NGƯỜI CẬP NHẬT
|
||||
```
|
||||
(NormalizeHeader = upper + collapse-ws + FormC → `\n`/space bị chuẩn hóa, nhưng text phải khớp. c21 "SĐT" + c24 "SDT" = shorthand cần khớp đúng.)
|
||||
|
||||
---
|
||||
|
||||
## ③ CHECKLIST (vai + deliverable + acceptance ĐO ĐƯỢC)
|
||||
|
||||
| Vai | Deliverable | Acceptance đo được |
|
||||
|---|---|---|
|
||||
| 🟨 implementer-backend | Mig 64 `AddSupplierPublishState` (3-file) + Supplier/config/DbInit + re-bake tokens + dedup-MST + PublishSupplierCommand + import/template endpoint + ListSuppliersQuery filter + SupplierDto | `dotnet build slnx` 0/0; Mig Up/Down reversible; sys.tables=**89** (no new table); backfill Sql idempotent |
|
||||
| 🟧 implementer-frontend | 2-app mirror: types +isPublic · SuppliersPage badge+nút Công bố+filter+guard · ImportDialog nút mẫu+cột Mã editable+helper | `npm run build` ×2 PASS; SHA-identical 2 dialog + 2 page (Pattern 16-bis); badge full-class-literal |
|
||||
| 🟪 test-specialist | dedup T1-T7 (MST/Code/blank/conflict) · publish T8-T11 (draft-on-import/gate/unpublish) · list T12-T13 filter · reconcile T14 backfill · LayoutValid Case9 real-token · all-or-nothing T15 | ≥15 test mới; 0 fail/skip; test-before cho dedup+publish-guard (critical/security) |
|
||||
| 🟥 reviewer | Adversarial pre-commit: dedup edge (null/dup MST), publish-gate D2, visibility grep-all-consumer, #73 clobber check | GO/GO-WITH-ADJ verdict; 0 must-fix |
|
||||
| 🟩 cicd-monitor | post-deploy verify Run # | bundle rotate ×2 + Mig 64 prod applied + smoke 200 + byte-verify nút "Tải file mẫu"/badge |
|
||||
|
||||
**Ràng buộc:** UI 100% tiếng Việt · commit `[CLAUDE] Supplier: ...` · KHÔNG đụng SupplierStatus enum · publish KHÔNG qua UpdateSupplierCommand (#73).
|
||||
|
||||
---
|
||||
|
||||
## 🔻 #53-garble note
|
||||
Lane 3 `public-visibility-consumers` (agentId af249d48) completed without StructuredOutput (#53 tái diễn — ×4 phiên gần đây). Recover: em-main-solo grep consumers (BE 16 file + FE fe-user 11 file) → consumers chính = PE picker + Contract create + dashboard, đều qua `ListSuppliersQuery` → filter `Published` giải quyết. KHÔNG re-spawn.
|
||||
Reference in New Issue
Block a user