wal: flush 20260712T1338

This commit is contained in:
pqhuy1987
2026-07-12 13:38:40 +07:00
parent 286450f67b
commit cf11faf1ec
7 changed files with 338 additions and 7 deletions

View File

@ -4,8 +4,11 @@ goal: Điều tra + thiết kế Supplier Excel-import (Phase B unpark) — fabl
chain:
[x] scout Excel (header 4-dòng, data row-5, 30 cột, 4 NCC, #REF! cols) + Supplier schema (27 field Mig 62, 0 import endpoint) + spec 3-mục ghi
[!] fable-clone ensemble: 3 lane investigator-codebase (mapping/approach/edge) + reviewer verify — Opus, folder runs/2026-07-12-S112-supplier-excel-import
[ ] em-main synthesize → design-doc + option cho anh quyết + agent-memory (invest + reviewer stamp [engine: fable-clone-ensemble · Opus-S112])
[x] fable-clone ensemble RAN 4/4 clean (wf_e2f23a84-933, Opus): 3 lane invest + reviewer PASS_WITH_CONCERNS; sub-files + run.md §synthesis written
[x] synthesize → design-doc trình anh: mapping 27/27 bijective · Approach B ĐÃ SHIP file này (seed Mig 62 Run#488) · MAJOR case-collation dedup (reviewer bắt, cả 3 lane sót) · 5 owner-decision
[!] anh QUYẾT approach (A tool reusable — port NamGroup HrEmployeeImportService, fix Code-case trước / B đã-xong / C) + 5 decision → rồi mới code
[ ] em harvest agent-memory @closeout (invest + reviewer, stamp [engine: fable-clone-ensemble · Opus-S112])
next: launch Workflow → chờ return → synthesize + ghi run.md §synthesis + sub-files từ return; KHÔNG code (chờ anh quyết approach)
verify: runs/2026-07-12-S112-supplier-excel-import/ có spec + synthesis non-zero; mapping đủ 30 cột; reviewer verdict present
next: chờ anh chọn approach + 5 decision; nếu A → fix MAJOR normalize-Code-case là precondition; KHÔNG code tới khi anh chốt
verify: runs/2026-07-12-S112-supplier-excel-import/ có spec + 4 sub-file + synthesis non-zero (DONE); harvest pending @closeout
side: harness_floor 30K→100K (anh-directed) + measured re-measure 21K→55K (S112, sister-parity ~60-71K)

View File

@ -86,7 +86,8 @@
},
"harness_floor": {
"_note": "Harness-15 A1/A3 (S81, 2026-06-20): SAN-harness = fixed per-spawn cost (NOT tunable) = tool-schema + framing + own persona/role file + lead-pasted base-doc slice + task prompt. SEPARATE HOUSE (A3 anti-double-count): persona + lead-pasted-docs belong HERE (floor), NOT counted in token_governor.l1_always (which = own agent-memory + archive index + work-state block only). MEASURED-ESTIMATE not exact (H15 honest-note b): persona = directly measured bytes (.claude/agents/<name>.md 4.3KB-13.3KB => ~1.3K-4.0K tok via /3.3); tool-schema + framing = harness-injected (cannot byte-count locally), estimated comparable to AI_INFRA same-toolset-family (Read/Write/Edit/Bash/Grep/Glob/Skill/RAG).",
"measured_token_estimate": 21000,
"measured_token_estimate": 55000,
"_s112_remeasure_note": "S112 (2026-07-12) RE-MEASURE — supersedes the S81 ~21K figure, which was a UNDER-COUNT (owner flagged 'why is SE so little vs sisters 60K+'). Byte-measured session-start DISK-inject floor = 57,595B (root CLAUDE.md 13,638 + docs/CLAUDE.md 10,222 + commands/session-start.md 23,669 + user-memory MEMORY.md 10,066) = ~14.4-19.2K tok ALONE. PLUS harness-INJECTED tool-schema + framing + MCP-instructions + deferred-tools + skills-catalog (~50 tool defs + 4 MCP blocks + ~90 deferred + ~60 skills), NOT byte-countable locally, ~30-50K tok. => FULL session-start floor ~45-70K tok, matching the fleet's measured ~60-71K (sister harness_floor_est @S103=71,364). The old 21K was the anomaly: S81 GUESSED the harness block at only ~5K(tool)+2K(framing) AND counted a small 'lead-pasted slice ~5-9K' instead of the full dual-CLAUDE.md + 23.7KB session-start body + user-memory inject. 55000 = honest mid estimate (~19K byte-measured + ~36K harness-injected-est). measured_token_estimate stays an ESTIMATE (harness block not locally byte-countable), not a false-precise number.",
"rounded_up_tokens": 100000,
"_owner_override_note": "S112 (2026-07-12) owner-directive: rounded_up_tokens raised 30000->100000 = OWNER-SET RESERVE (project-owner authority per token_governor.role_boundary_note + mark RC-pqhuy1987-21-06-2026-01-58-01), NOT a re-measurement -- measured_token_estimate stays 21000 (the honest S81 estimate). Rationale: reserve headroom for the grown fixed per-spawn overhead (roster 11->12 agents, +office-document, +2 monitor, more MCP tool-schemas, richer lead-pasted slices at the 380K read-set). CAVEAT (honest): harness_floor is an ACCOUNTING/AWARENESS figure (separate house, A3 anti-double-count) documented here; it is NOT actively subtracted by the current scripts (mfe-eval.ps1 / crystallized-backfill.ps1 govern via token_governor caps, not this floor). If owner wants 100K wired as an ACTIVE reserve subtracted from headroom, that is a separate change. Re-measure available on request to check the true current floor.",
"components_note": "persona(measured 1.3K-4.0K, DIRECTLY via Get-ChildItem byte-count of .claude/agents/*.md, NOT via measure-agent-memory.ps1 which only sizes agent-memory tiers) + tool-schema(est ~5K) + framing(est ~2K) + lead-pasted base-slice(est ~5-9K; em-main injects CLAUDE.md/README-slice + task ctx) + prompt(est ~1K). README decision-tree corpus = 32704B but em-main pastes a SLICE, not whole. The ~21K sum is SE's OWN reasoning over SE's OWN agents (persona byte-measured + each harness-injected sub-component estimated independently); it lands NEAR AI_INFRA's ~21K because the toolset family is identical (Read/Write/Edit/Bash/Grep/Glob/Skill/RAG), NOT because borrowed. SE's governing cap = 100K (owner-set reserve S112 2026-07-12; was 30K round-up), independent of AI_INFRA's figure."

View File

@ -22,5 +22,31 @@
## deliverable
Design-doc: mapping ĐỦ 30 cột → 27 field + 3 approach tradeoff + reviewer verdict → **anh quyết approach + scope**. KHÔNG code lượt này.
## synthesis
_(pending — em-main ghi sau khi Workflow return)_
## synthesis (em-main single-writer — wf_e2f23a84-933, 4/4 clean, ~416K tok Opus)
> ⚠️ Sub-file naming: journal completion-order ≠ LANES order → `sub-invest-codebase-B-approach.md` HOLDS Lane C (dedup/edge) content · `sub-invest-codebase-C-edge.md` HOLDS Lane B (approach) content. Content correct, filenames swapped (harvest reads content).
**REVIEWER VERDICT: PASS_WITH_CONCERNS.** Design sound; every in-repo file:line verified KHỚP (0 fabrication); 27/27 field bijective, 0 silent loss. 1 MAJOR + 4 MINOR refine it before build.
### Load-bearing findings
1. **Schema 100% ready** — 30 Excel col → 27 field bijective (1 ignore STT, 27 direct col2-28, 2 audit-provenance col29-30 = owner-call). Mig 62's 18-field expansion purpose-built for this Excel. 0 invented field.
2. **🎁 Approach B ĐÃ SHIP** — spec "seed fills partial" is WRONG: `DbInitializer.cs:2775-2782` fills all 30 fields for the 4 NCC, prod-verified (Mig 62 `778fc98` Run #488). **This exact file's 4 NCC are already in prod.**
3. **ClosedXML 0.105 already in `SolutionErp.Infrastructure.csproj:8`** (0 new dep); read-idiom `XlsxRenderer.cs:18-25`; IFormFile plumbing in 3 controllers. NamGroup `HrEmployeeImportService` = complete upload→preview→confirm pattern to port (⚠️ MINOR: unverifiable from this env — repo ERP_MINI_FINAL absent).
4. **🔴 MAJOR (reviewer caught, all 3 lanes missed):** dedup Code = `Ordinal` (case-sensitive) vs DB unique-index default = case-INSENSITIVE collation → Approach A with case-variant Code → dict-miss → INSERT → hard unique-violation 500. **Precondition:** normalize Code (Trim+UPPER or OrdinalIgnoreCase) before dedup+store+preview.
5. **Must-knows:** import writes entity directly with fill-where-non-empty, NOT via `UpdateSupplierCommand` (blind absolute-set → NULL-clobber #73); read by absolute cell index NOT `CellsUsed()` (skips empties → drift); parser anchors r4 + fingerprint-guard; 2-pass all-or-nothing (~4-20 rows).
### 3 approach
| | Effort | Reusable | Verdict |
|---|---|---|---|
| **A upload endpoint** | MED (port NamGroup) | ✅ | best if NCC keep coming; needs MAJOR fix first |
| **B extend seed** | ~0 (ĐÃ SHIP file này) | ❌ | done for these 4 NCC; no update-sync |
| **C import-command** | MED | ~ | bỏ — cắt preview = cắt lá chắn cho Excel bẩn |
### 5 owner decisions
1. Col 29-30 (NGÀY/NGƯỜI CẬP NHẬT) → ignore / Note / +2 field Mig 63?
2. Code = col4 "TÊN VIẾT TẮT" confirmed (= mã HĐ key RG-001)?
3. Reusable tool needed? → A ; chỉ file này → B đã xong.
4. Re-import existing Code → fill-nulls (safe) vs overwrite-non-empty (updates, destructive)?
5. PHÂN LOẠI unknown value → reject-row vs default NhaCungCap?
**Harvest:** pending @closeout — em-main append 1 entry investigator-codebase + 1 reviewer, stamp `[engine: fable-clone-ensemble · Opus-S112]`.

View File

@ -0,0 +1,66 @@
# sub-invest-codebase-A-mapping
**HEADLINE:** 30 cột Excel map GỌN vào 27 field Supplier: 1 ignore (STT) + 27 direct-field (cột 228, mỗi field dùng đúng 1 lần) + 2 cột audit-provenance (NGÀY/NGƯỜI CẬP NHẬT) KHÔNG có field khớp trực-tiếp → cần owner quyết. Seed 4 NCC (DbInitializer.cs:2775-2782) đã là precedent transform sống cho 27 mapping.
## LANE A — COLUMN→FIELD MAPPING (verified vs source thật)
**Nguồn verify:** `src/Backend/SolutionErp.Domain/Master/Supplier.cs:5-36` (27 field), `SupplierType.cs:3-11`, `SupplierStatus.cs:3-8`, precedent transform `DbInitializer.cs:2775-2782` (seed 4 NCC thật đã map 30→27), audit `AuditingInterceptor.cs:44-49` + `BaseEntity.cs:6-9`.
### Bảng mapping đủ 30 dòng
| # | Excel col (r4) | → target field | kind | transform / note |
|---|---|---|---|---|
| 1 | STT | — | **ignore** | Số dòng, không persist |
| 2 | GÓI THẦU | `PackageCategory` | direct-field | Free-text. Seed vd `"10-Nhôm kính+Face Mặt dựng"` |
| 3 | PHÂN LOẠI (NTP/NCC/Cả hai) | `Type` (`SupplierType`, **non-null**) | **enum-map** | Trim+ci: `NCC`→NhaCungCap(1) · `NTP`→NhaThauPhu(2) · `Cả hai`→CaHai(6). (Enum còn TĐ=3/ĐVDV=4/CĐT=5 nếu source có). Non-nullable → giá trị lạ phải reject-row hoặc default (owner) |
| 4 | TÊN VIẾT TẮT | `Code` (**required**) | direct-field | = Mã NCC dùng trong mã HĐ. Trim+UPPER khuyến nghị (seed: `TRUONGGIANG`). Là **khóa idempotent/dedup** |
| 5 | TÊN CÔNG TY | `Name` (**required**) | direct-field | Tên đầy đủ. KHÔNG tạo cột `companyName` mới — reuse `Name` gốc |
| 6 | ĐỊA CHỈ XUẤT HÓA ĐƠN | `Address` | direct-field | = "địa chỉ xuất HĐ/đăng ký" (comment field khớp). Reuse cột gốc, KHÔNG tạo `invoiceAddress` |
| 7 | ĐỊA CHỈ VĂN PHÒNG | `OfficeAddress` | direct-field | 4 NCC seed để trống → field vẫn là target đúng |
| 8 | SỐ ĐT CÔNG TY | `Phone` | direct-field | Phone công-ty (phân biệt cột 21) |
| 9 | FAX | `Fax` | direct-field | Chỉ TGN có (seed `"0251.2814663"`) |
| 10 | SỐ TÀI KHOẢN+TÊN+CN.NH | `BankAccount` | direct-field · **COMPOSITE** | **Giữ RAW 1 ô** (precedent: `"100212127 Ngân hàng Eximbank - Chi Nhánh Hòa Bình"`). Entity KHÔNG có AccountName/BankBranch → không tách |
| 11 | SỐ TK PHỤ | `SecondaryBankAccount` | direct-field | 4 NCC seed để trống, field đúng target |
| 12 | MÃ SỐ THUẾ | `TaxCode` | direct-field | Giữ raw kể cả có space (`"0312 251 859"`). Có thể null (DONGDUONG) |
| 13 | NGƯỜI ĐẠI DIỆN PHÁP LUẬT | `LegalRepresentative` | direct-field | Giữ prefix `"Ông/Bà"` như seed |
| 14 | CHỨC VỤ ĐẠI DIỆN | `LegalRepTitle` | direct-field | vd `"GIÁM ĐỐC ĐIỀU HÀNH"` |
| 15 | GIẤY ỦY QUYỀN | `AuthorizationNote` | direct-field | Free-text dài (số GUQ+ngày+người ủy quyền) |
| 16 | Link GUQ | `LinkGuq` | direct-field · **NAS path** | **Giữ RAW backslash** `@"01. GPKD_HSNL_GUQ\02.Trường giang\...pdf"` — KHÔNG normalize `\``/` |
| 17 | Link GPKD | `LinkGpkd` | direct-field · NAS path | như trên |
| 18 | Link HSNL | `LinkHsnl` | direct-field · NAS path | như trên |
| 19 | NGƯỜI LH CHÍNH | `ContactPerson` | direct-field | Người liên hệ chính (seed `"Mr. Việt"`) |
| 20 | CHỨC VỤ NGƯỜI LH | `ContactTitle` | direct-field | vd `"Kinh doanh"` |
| 21 | SĐT CHÍNH | `ContactPhone` | direct-field | Phone người-LH (KHÁC cột 8 phone công-ty) |
| 22 | EMAIL | `Email` | direct-field | |
| 23 | ĐỊA CHỈ GỬI THƯ | `MailingAddress` | direct-field | Địa chỉ thứ-3 (khác cột 6 & 7) |
| 24 | NGƯỜI NHẬN THƯ/SĐT | `MailRecipient` | direct-field · **COMPOSITE** | **Giữ RAW** `"MS Thủy / 036 405 0016"` (name+phone gộp). Entity không có field tách |
| 25 | NGUỒN GIỚI THIỆU | `ReferralSource` | direct-field | vd `"Tái sử dụng DA trước"` |
| 26 | NGƯỜI PHỤ TRÁCH(PMH) | `OwnerPmh` | direct-field | vd `"Bình"` |
| 27 | TÌNH TRẠNG HIỆN TẠI | `Status` (`SupplierStatus?`, **nullable**) | **enum-map** | **Strip emoji + trim**: `✅ Đang hoạt động`→DangHoatDong(1) · `Blacklist`/`đen`→Blacklist(2) · `Ngừng hợp tác`→NgungHopTac(3). Nullable → giá trị lạ = null OK |
| 28 | GHI CHÚ/LÝ DO BLACKLIST | `Note` | direct-field | Target ĐÚNG cho Note. (Seed để `Note=null` vì đã un-cram legacy — import thì col 28 mới là Note thật; coherence: khi Status=Blacklist thì lý-do nằm ở đây) |
| 29 | NGÀY CẬP NHẬT CUỐI | **(audit)** `UpdatedAt`? | **KHÔNG có field khớp** | ⚠️ Map `UpdatedAt` = **futile**: `AuditingInterceptor.cs:48` GHI ĐÈ `UpdatedAt=now` mỗi save. → ignore / gộp-Note / đề-xuất-field-mới (xem openQuestions) |
| 30 | NGƯỜI CẬP NHẬT | **(audit)** `UpdatedBy`? | **KHÔNG có field khớp** | ⚠️ `UpdatedBy``Guid?` (userId), Excel là **tên người** (text) → type mismatch + bị `AuditingInterceptor.cs:49` ghi đè. Không direct-map được |
### Kiểm chứng độ-phủ (0 sót)
- **27/27 field entity** được map đúng 1 lần (9 core + 18 expansion) — cột 228 (27 cột).
- **Cột 1** STT = ignore.
- **Cột 2930** = audit-provenance, KHÔNG field khớp trực tiếp → quyết định owner.
- Tổng: 1 ignore + 27 direct + 2 no-field = **30 ✓**.
### 3 nhóm cần transform đặc-biệt (đã đánh dấu trong bảng)
- **COMPOSITE (2):** cột 10 BankAccount + cột 24 MailRecipient → **giữ RAW** (precedent seed đã giữ raw, entity thiếu sub-field để tách).
- **ENUM-MAP (2):** cột 3 Type (trim+ci, non-null → cần rule giá-trị-lạ) + cột 27 Status (strip emoji, nullable → lạ=null).
- **NAS path (3):** cột 16/17/18 → giữ raw backslash, KHÔNG đổi `\``/`.
- **Chung mọi cột:** normalize `\n`/whitespace trong header r4 (multi-line) + trim cell; skip `#REF!` ở r2 (chỉ parse tên r4).
### Lưu ý reuse-cột (tránh orphan)
Cột 5/6 map thẳng `Name`/`Address` gốc — KHÔNG tạo `companyName`/`invoiceAddress` mới (sẽ orphan cột seed live). 18 expansion field vốn purpose-built đúng cho Excel này (Mig 62), khớp 1-1 cột 2,7,9,10,11,13-18,20,21,23-27.
## Open Questions
**1. Cột 29-30 (NGÀY/NGƯỜI CẬP NHẬT) xử lý sao?** — KHÔNG có field khớp (audit fields `UpdatedAt`/`UpdatedBy` bị `AuditingInterceptor` ghi đè + `UpdatedBy` là Guid≠tên). 3 lựa chọn: (a) **ignore** — audit tự stamp import-time (mất provenance nguồn); (b) **gộp-Note** — nối `"Cập nhật cuối 01/07/2027 bởi Bình"` vào `Note` (giữ provenance, không thêm schema); (c) **đề-xuất-field-mới** `SourceUpdatedAt DateTime?` + `SourceUpdatedBy string?` (+2 cột Mig 63, sạch nhất nhưng thêm migration).
**2. PHÂN LOẠI (col 3, non-nullable Type) khi gặp giá-trị-lạ** ngoài NTP/NCC/Cả hai → reject-row hay default về NhaCungCap? (Status nullable nên không vướng — lạ=null).
**3. Composite cột 10 & 24 — xác nhận giữ RAW** (theo precedent seed) hay owner muốn tách sub-field (cần thêm cột AccountName/BankBranch/RecipientPhone → Mig mới)?
**4. Code (col 4) là khóa idempotent** — import gặp Code đã tồn tại thì skip / fill-nulls / overwrite? (Ảnh hưởng Lane B robustness).

View File

@ -0,0 +1,95 @@
# sub-invest-codebase-B-approach
**HEADLINE:** LANE C: Dedup = Code (DB-unique filtered, always-present, in-mã-HĐ) KHÔNG TaxCode (nullable+bẩn); parser = anchor r4 positional + fingerprint-guard, đọc `cell.GetString()`; import PHẢI fill-where-non-empty (KHÔNG route UpdateSupplierCommand = clobber #73); 12 edge-case có cách xử.
## LANE C — DATA-INTEGRITY / EDGE-CASE (ground-truth verified)
### 0. Nền tảng đã verify (file:line)
- **Excel read primitive CÓ SẴN:** `XlsxRenderer.cs:18` `new XLWorkbook(path)` + `:22` `ws.CellsUsed()` + `:24` `cell.DataType` + `:25` `cell.GetString()`. ClosedXML `0.105.0` (`SolutionErp.Infrastructure.csproj:8`). `GetString()` auto-decode shared-string → KHÔNG cần OpenXml raw. **Đây là read-idiom chuẩn để mirror.**
- **Code = DB natural key:** `SupplierConfiguration.cs:44` `HasIndex(x=>x.Code).IsUnique().HasFilter("[IsDeleted]=0")` (Mig 47). **TaxCode KHÔNG có unique index** (`:17` chỉ `HasMaxLength(20)`).
- **Seed per-code idempotent** `DbInitializer.cs:2786-2819`: dedup bằng `existingByCode` (`StringComparer.Ordinal`, `:2787`), FILL-NULLS-if-exists (`:2796-2813` chỉ set cột null, KHÔNG đè non-null).
- **Ground-truth 4 NCC:** `TRUONGGIANG` TaxCode=`"0312 251 859"` (CÓ SPACE, `:2775`) · `DONGDUONG` TaxCode=**null** (`:2781`) → chứng cả 2 vấn đề dedup+format.
- **Validation hiện tại** `CreateSupplierCommand.cs:45-71`: Code `NotEmpty.MaxLength(50)` · Name `NotEmpty.MaxLength(200)` · Type `IsInEnum()` · TaxCode `MaxLength(20)` **KHÔNG regex format** · 18 field mới `MaxLength`-only. Enum `SupplierType{NhaCungCap=1,NhaThauPhu=2,ToDoi=3,DonViDichVu=4,ChuDauTu=5,CaHai=6}` · `SupplierStatus?{DangHoatDong=1,Blacklist=2,NgungHopTac=3}` (nullable).
---
### 1. PARSER-STRATEGY (messy 4-dòng header)
**Nguyên tắc: anchor r4 + đọc positional, KHÔNG name-lookup-tự-do, KHÔNG dùng r2.**
1. `IFormFile.OpenReadStream()``new XLWorkbook(stream)``wb.Worksheet(1)` (single sheet).
2. **BỎ HẲN r1(title)/r2(số-cột+#REF!)/r3(nhóm).** Chỉ r4 = tên cột thật, data từ r5.
3. **Header-fingerprint guard (r4):** normalize mỗi cell r4 = `GetString().Replace("\n"," ").Trim().ToUpperInvariant().Collapse-space`. Ghép chuỗi 30 header → so với FINGERPRINT kỳ vọng hard-code. **Khớp → map positional theo thứ tự r4 (col1→STT... col30→NGƯỜI CẬP NHẬT). LỆCH → reject cả file** ("layout Excel đã đổi") thay vì mismap câm. → xử "cột đổi vị trí giữa các lần export".
4. **`#REF!` / lỗi ô:** coi literal `#REF!|#N/A|#VALUE!|#DIV/0!|#NAME?` = null (bỏ ô). #REF! nằm ở vùng header-merge r2 (đã bỏ) nhưng defensive-guard mọi ô data r5+.
5. **NAS backslash path** (`LinkGuq/Gpkd/Hsnl`): `GetString()` trả literal single-`\`. **Chỉ `.Trim()` ngoài — KHÔNG escape, KHÔNG normalize slash, KHÔNG `Path.GetFullPath`.** Seed lưu đúng verbatim (`:2776 @"01. GPKD_HSNL_GUQ\02.Trường giang\..."`).
6. **Composite cell** (SỐ TÀI KHOẢN = "số TK + tên + CN.NH" 1 ô): đổ raw nguyên văn vào `BankAccount` (MaxLength 500 đủ). KHÔNG tách sub-field. Tương tự NGƯỜI NHẬN THƯ/SĐT → `MailRecipient` raw.
7. **Đọc ô:** `cell.GetString()` cho mọi cột text (auto shared-string). Số/ngày (NGÀY CẬP NHẬT CUỐI) — hiện entity KHÔNG có field ngày → drop hoặc gom Note (mapping-lane quyết).
8. Cột **21-31 #REF!** trong r2-numbering: vì map theo r4-order + fingerprint nên padding cols này KHÔNG ảnh hưởng; nếu r4 có ô rỗng/#REF! giữa 30 tên → fingerprint tự bắt.
---
### 2. 🔴 DEDUP KEY = **Code** (quyết định, 5 chứng cứ)
| Tiêu chí | Code (TÊN VIẾT TẮT) | TaxCode (MST) |
|---|---|---|
| DB constraint | ✅ UNIQUE filtered `SupplierConfiguration.cs:44` | ❌ không unique (`:17`) |
| Luôn có mặt | ✅ `.IsRequired()` `:14` + cả 4 seed | ❌ null (DONGDUONG `:2781`) |
| Business identity | ✅ dùng trong mã HĐ `SOL&{SupplierCode}` (RG-001) | — |
| Format sạch | ✅ token viết-tắt | ❌ "0312 251 859" có space vs "0305497393" |
| Seed đã dedup bằng | ✅ `existingByCode` Ordinal `:2787` | — |
**Chốt: dedup = `Code.Trim()` (Ordinal, khớp `StringComparer.Ordinal` seed + filter DB).** TaxCode chỉ làm **SECONDARY soft-warn**: 2 dòng cùng normalized-TaxCode khác Code → cảnh báo "nghi trùng NCC", KHÔNG block.
**Upsert idempotent:**
- Missing Code → INSERT (Code lấy TỪ Excel, KHÔNG codegen — khác Contract/PE sequence).
- Exist Code → UPDATE **fill-where-non-empty** (ô Excel rỗng → GIỮ giá trị DB cũ).
- ⚠️ **TUYỆT ĐỐI KHÔNG route qua `UpdateSupplierCommand`**: handler `:86-112` blind absolute-set CẢ 27 field (incl `entity.Note=request.Note :94`, `entity.TaxCode :89`) → ô Excel thiếu = **NULL-hóa data tốt (latent #73 clobber)**. Import handler tự viết entity trực tiếp, per-field `if(!string.IsNullOrWhiteSpace(cell)) entity.X = cell`.
- Reuse guard conflict-check pattern `UpdateSupplierCommand.cs:82-84` (Code trùng khác Id → Conflict) cho case trùng trong-batch.
---
### 3. VALIDATION (mirror `CreateSupplierCommandValidator`)
| Field | Rule | Fail → |
|---|---|---|
| Code | NotEmpty + Trim + MaxLength(50) | **REJECT row** (required + PK) |
| Name | NotEmpty + MaxLength(200) | **REJECT row** |
| Type | map "NTP"→2 / "NCC"→1 / "Cả hai"/"NTP/NCC"→CaHai=6; IsInEnum | **REJECT row** (required) |
| TaxCode | normalize strip space/dot → soft `^\d{10}(\d{3})?$` | **WARN-not-block** (giữ "0312 251 859", null OK) |
| Status | strip emoji+trim → map text; unmapped | **→ NULL** (nullable, đừng reject) |
| Email | `EmailAddress().When(non-empty)` (`:50`) | WARN, giữ raw |
| Mọi field text | MaxLength (Bank/AuthNote 500/1000...) | **truncate+WARN** (import-tolerance) |
**Lưu ý MST:** validator hiện KHÔNG có regex → nếu thêm format-check phải **SOFT** (warn), vì strict `^\d{10,13}$` sẽ reject chính data thật "0312 251 859" + block DONGDUONG(null). Normalize (strip space/dot) TRƯỚC, rồi mới check, để "0312 251 859"→"0312251859" pass 10-digit.
---
### 4. BẢNG EDGE-CASE (12 case × cách xử)
| # | Edge case | Cách xử |
|---|---|---|
| 1 | MST null (DONGDUONG `:2781`) | Cho phép, TaxCode nullable; dedup=Code nên không cần MST |
| 2 | MST có space "0312 251 859" | Normalize strip space/dot trước store + soft-check; giữ nếu fail |
| 3 | Dòng trống giữa/cuối data | Skip nếu Code&Name đều rỗng (sentinel); KHÔNG tạo NCC rỗng |
| 4 | Emoji Status "✅ Đang hoạt động" | Strip ký-tự-đầu-không-phải-chữ + trim → map text; unmapped→null |
| 5 | Composite bank 1 ô (TK+tên+CN.NH) | Raw verbatim → BankAccount (MaxLen 500); KHÔNG tách |
| 6 | NAS backslash `\...\..pdf` | Verbatim single-`\`, chỉ Trim ngoài; KHÔNG escape/normalize |
| 7 | Cột đổi vị trí giữa export | Header-fingerprint r4 lệch → REJECT cả file, không mismap câm |
| 8 | #REF! lan sang ô data | Coi error-literal (#REF!/#N/A/#VALUE!/#DIV/0!/#NAME?) = null/skip ô |
| 9 | Trùng Code trong CÙNG file | Report cả 2 dòng; last-wins + WARN (KHÔNG câm) |
| 10 | Code đã có trong DB (re-import) | Upsert fill-where-non-empty; KHÔNG qua UpdateCommand (clobber #73) |
| 11 | Multi-line `\n` trong ô (Name/Address) | Collapse `\n`→space cho field 1-dòng; Address giữ (owner quyết) |
| 12 | Value > MaxLength (Bank/AuthNote) | Truncate + WARN (import-tolerance) hoặc reject-row (owner quyết) |
**Atomicity (data-integrity):** ~4-20 dòng thật → khuyến nghị **2-pass all-or-nothing**: pass-1 parse+validate TẤT, gom `List<RowError>`; có hard-error (Code/Name/Type) → trả report KHÔNG commit; sạch → 1 `SaveChanges` atomic. Trả về `{inserted, updated, skipped, warnings[], errors[]}` cho FE hiển thị.
---
*Token cost est: ~19K (6 tool calls). Verified file:line, no fabrication. Lane scope = parser+dedup+validation+edge; mapping-30-cột + approach-tradeoff = lane khác.*
## Open Questions
1. **Upsert khi re-import Code đã tồn tại:** overwrite-with-non-empty (last-write-wins, đúng kỳ vọng "cập nhật NCC") HAY fill-nulls-only như seed (`:2796`, không bao giờ đè)? Đề xuất overwrite-non-empty cho import (khác seed) — cần anh chốt.
2. **Hard-error → all-or-nothing hay commit-valid-skip-bad?** Đề xuất all-or-nothing (chỉ ~4-20 dòng).
3. **MST format:** thêm soft-regex `^\d{10}(\d{3})?$` (warn) hay bỏ hẳn format-check giữ MaxLength(20) như hiện tại? (strict sẽ reject data thật có space + null DONGDUONG).
4. **NGÀY CẬP NHẬT CUỐI / NGƯỜI CẬP NHẬT (col 29-30):** entity KHÔNG có field ngày-cập-nhật-nghiệp-vụ → drop, gom Note, hay thêm cột mới? (mapping-lane + anh quyết).
5. **Value quá dài (edge #12):** truncate+warn hay reject-row?
6. **Trùng Code trong file (edge #9):** last-wins hay reject-toàn-file?

View File

@ -0,0 +1,59 @@
# sub-invest-codebase-C-edge
**HEADLINE:** ClosedXML 0.105 đã sẵn (csproj:8, 0 dep mới); import PHẢI ghi entity trực tiếp / fill-nulls, KHÔNG route qua UpdateSupplierCommand (blind absolute-set 27 field = clobber); Approach B (seed 4 NCC 30-field) THỰC RA ĐÃ SHIP prod → chỉ chọn A nếu cần tool reusable, và NamGroup có sẵn pattern port thẳng.
## LANE B — Existing code + Approach (đã verify source thật)
### 1. Shape hiện tại (Phase A đã ship — Create/Update/DTO đều 27 field)
| File:line | Shape | Validation |
|---|---|---|
| `CreateSupplierCommand.cs:10-39` | record 27 field (9 core + 18 expansion trailing-optional `=null`) → handler ghi **entity trực tiếp** (`:86-115`) | `:41-72` MaximumLength cho mọi field; core: Code NotEmpty(50)/Name NotEmpty(200)/Type IsInEnum/Email EmailAddress-when-nonblank. Expansion **CHỈ MaximumLength, KHÔNG NotEmpty** (payload tối thiểu hợp lệ). Handler `:83` chặn trùng Code → ConflictException |
| `UpdateSupplierCommand.cs:10-39` | record 27 field | handler `:86-112` **BLIND ABSOLUTE-SET cả 27 field** incl Note/Status |
| `SupplierDto.cs:5-35` | 27 field + CreatedAt/UpdatedAt | hand-projected **2 site**: `GetSupplierQuery.cs:21-25` (positional) + `ListSuppliersQuery.cs:48-53` (positional). Không AutoMapper → thêm field = sửa 4 file (entity/cmd×2/dto/2 query) |
| `SuppliersController.cs` | `POST` Create = **open-auth** (mọi user login, note S59 `:34-36`) · `PUT`/`DELETE` = `Admin,CatalogManager` (`:44,:53`) · **KHÔNG có import/upload endpoint** (verified) |
### 2. 🔴 Gotcha số-1 cho mọi approach: KHÔNG route import qua UpdateSupplierCommand
`UpdateSupplierCommandHandler:86-112` gán tuyệt-đối cả 27 field từ request. Excel-row thiếu field nào → field đó bị **NULL-out** (clobber, class #73). Import PHẢI:
- **Upsert-by-Code**: Code mới → INSERT (như `CreateSupplierCommandHandler`); Code có → **fill-where-non-empty / fill-nulls**, KHÔNG absolute-set.
- Seed đã có sẵn công thức chuẩn: `DbInitializer.cs:2792-2815` fill-nulls-if-exists (chỉ set cột đang null, không đè non-null).
### 3. Lib đọc xlsx: ĐÃ CÓ — ClosedXML 0.105.0, 0 dependency mới
- `SolutionErp.Infrastructure.csproj:8` `ClosedXML 0.105.0` (+`:9` DocumentFormat.OpenXml 3.5.1 nhưng **chỉ dùng cho DOCX**, không đọc xlsx).
- **Read-pattern sẵn**: `XlsxRenderer.cs:18` `new XLWorkbook(path)` + `:20-25` iterate `ws.CellsUsed()` + `cell.GetString()` (auto-decode shared-string → messy `\n`/composite cell xử được).
- Write-pattern: `ContractExcelExporter.cs:56`, `AttendanceReportExcelExporter.cs:14`.
- **Multipart IFormFile plumbing sẵn** để mirror cho endpoint: `FormsController.cs:44-45`, `ContractsController.cs:75-77`, `PurchaseEvaluationsController.cs:244-246`.
### 4. ⭐ NamGroup CÓ pattern Excel-import HOÀN CHỈNH — port thẳng (cùng lib ClosedXML)
`…ERP_MINI_FINAL\ERP.Server\Features\Hrm\Employees\Services\HrEmployeeImportService.cs` (`using ClosedXML.Excel` `:1`) = interface 3-method chuẩn **upload→preview→confirm**:
- `BuildTemplate():12` → xuất Excel mẫu styled header + 2 dòng ví dụ (byte[])
- `PreviewAsync(Stream):15` → parse + phát hiện trùng (CCCD) + per-row error
- `ImportAsync(rows,actor):18` → commit CHỈ dòng client chọn
- Column-order map `(Header,Field)[]:24-44`; helper an-toàn `GetCell:317`/`GetDateCell:325`; `ValidateRow:256`. Controller `ImportTTNVController.cs` tồn tại.
→ Đây là template Approach A gần-hoàn-chỉnh, cùng lib → hạ effort A đáng kể.
### 5. ⚠️ Correction vs spec: Approach B THỰC RA ĐÃ SHIP (không "partial")
Spec nói seed "fill PARTIAL — không đụng 18 expansion field" → **SAI với code hiện tại**. `DbInitializer.cs:2775-2782` fill đủ 30-field (PackageCategory/BankAccount/LegalRepresentative/LegalRepTitle/AuthorizationNote/LinkGuq/LinkGpkd/LinkHsnl/ContactTitle/ContactPhone/MailingAddress/MailRecipient/ReferralSource/OwnerPmh/Status/Fax/…) cho **đúng 4 NCC** TRUONGGIANG/TANPHU/TGN/DONGDUONG, mỗi field set nơi Excel có data. Idempotent insert-if-missing + fill-nulls (`:2790-2818`), ungated → tới prod (memory: Mig 62 commit `778fc98`, Run #488 PASS). **Nghĩa là: đưa đúng file Excel này (4 NCC × 30 field) vào DB = ĐÃ XONG.** Quyết định còn lại thuần: có cần tool reusable cho batch NCC tương lai không.
### 6. Bảng 3 APPROACH × tradeoff
| | Effort | Reusable | Risk | UX |
|---|---|---|---|---|
| **(A) Upload endpoint** (FE .xlsx → BE ClosedXML parse → preview → confirm upsert) | **MED** (giảm nhờ port NamGroup; ~1 service + preview DTO + `[Authorize(Admin,CatalogManager)]` endpoint + FE upload/preview UI). FE preview là phần nặng nhất | **CAO** — self-service mọi batch sau, không cần deploy | **MED** — Excel messy: 4-dòng header gộp (data từ row 5), composite cell BankAccount, `#REF!` col21-31, emoji Status, NAS `\` path. Preview + per-row error hạ risk | **TỐT NHẤT** — xem trước, sửa, chọn dòng commit |
| **(B) Extend SeedRealMasterDataAsync** (hardcode NCC, 30-field) | **~0 cho file này** (đã ship); thêm NCC = thêm row array + recompile+redeploy | **KHÔNG** — hardcode, 0 UI, dev-only | **THẤP** — compile-time, idempotent, đã prod-verified | **KHÔNG** UI |
| **(C) Admin import-command/button** (command đọc file server-path / trigger seed-lại) | **MED** (parse như A nhưng bỏ preview UI) | **MED** — lặp được, kém linh hoạt | **MED-CAO** — commit thẳng KHÔNG preview trên Excel bẩn = nguy hiểm nhất | **TRUNG BÌNH** — 1 nút, không xem trước |
### 7. MVP recommendation (solo-dev)
- **Nếu mục tiêu = đưa đúng file "Database NCC" hiện tại vào prod → KHÔNG build gì thêm** (Approach B đã ship cho đúng 4 NCC này, prod-verified). Chỉ cần confirm anh muốn thêm/sửa số liệu nào thì patch array `:2775-2782`.
- **Nếu anh muốn tool tự-phục-vụ cho NCC phát sinh liên tục (nghiệp vụ đi thầu — note S59) → Approach A**, port `HrEmployeeImportService` từ NamGroup (cùng ClosedXML). Semantic upsert = **fill-nulls-if-exists** (bê từ seed `:2796-2813`), key = Code. Auth `Admin,CatalogManager` (khớp Update/Delete). Bỏ Approach C: tiết kiệm giả — cắt preview = cắt đúng lá chắn giá-trị-nhất cho Excel bẩn.
### 8. Mapping 30 Excel-col → 27 entity field (load-bearing; verify với seed row TRUONGGIANG `:2775-2776`)
STT→(bỏ, row-index) · GÓI THẦU→`PackageCategory` · PHÂN LOẠI→`Type`(NTP=NhaThauPhu/NCC=NhaCungCap/Cả hai=CaHai=6) · TÊN VIẾT TẮT→`Code`⚠️(xem Q) · TÊN CÔNG TY→`Name` · ĐC XUẤT HĐ→`Address` · ĐC VĂN PHÒNG→`OfficeAddress` · SỐ ĐT CTY→`Phone` · FAX→`Fax` · SỐ TK+TÊN+CN.NH→`BankAccount`(composite 1 ô, 500) · SỐ TK PHỤ→`SecondaryBankAccount` · MST→`TaxCode` · NG.ĐẠI DIỆN PL→`LegalRepresentative` · CHỨC VỤ ĐD→`LegalRepTitle` · GIẤY ỦY QUYỀN→`AuthorizationNote` · Link GUQ→`LinkGuq` · Link GPKD→`LinkGpkd` · Link HSNL→`LinkHsnl` · NG.LH CHÍNH→`ContactPerson` · CHỨC VỤ LH→`ContactTitle` · SĐT CHÍNH→`ContactPhone` · EMAIL→`Email` · ĐC GỬI THƯ→`MailingAddress` · NG.NHẬN THƯ/SĐT→`MailRecipient` · NGUỒN GIỚI THIỆU→`ReferralSource` · NG.PHỤ TRÁCH(PMH)→`OwnerPmh` · TÌNH TRẠNG→`Status`(keyword-map, strip emoji: "hoạt động"→DangHoatDong/"blacklist"→Blacklist/"ngừng"→NgungHopTac) · GHI CHÚ/BLACKLIST→`Note` · NGÀY CẬP NHẬT CUỐI + NGƯỜI CẬP NHẬT→(bỏ / hoặc audit).
→ 30 col STT 2 audit col = **27 khớp chính-xác entity**. Sample 4-row THẬT đã có sẵn trong seed `DbInitializer.cs:2775-2782` (dùng làm golden fixture cho test parser Approach A).
**Token cost ~21K.** (Honor RETURN-ONLY: không ghi MEMORY/file spawn này — em-main harvest.)
## Open Questions
1. **Code = cột "TÊN VIẾT TẮT" (col4)?** Seed dùng Code=TRUONGGIANG/TANPHU/TGN/DONGDUONG (viết tắt tên cty) → suy col4→Code. Nhưng Code là mã dùng trong mã HĐ (RG-001) — anh xác nhận Code lấy từ TÊN VIẾT TẮT hay gán riêng? (ảnh hưởng upsert-key của Approach A).
2. **Có cần tool reusable không?** Nếu chỉ file 4-NCC này → đã xong (Approach B shipped), khỏi build. Approach A chỉ đáng nếu NCC phát sinh liên tục.
3. **2 cột "NGÀY/NGƯỜI CẬP NHẬT CUỐI"** (col29-30) không có field entity → bỏ, nhét Note, hay map audit UpdatedAt/UpdatedBy (hiện system-set)?
4. **Import auth** = `Admin,CatalogManager` (khớp Update/Delete)? Create hiện open-auth nhưng bulk-import nên privileged.
5. **Upsert clash**: Code đã tồn tại + Excel có data mới KHÁC (non-null cả 2) → giữ DB (fill-nulls, an toàn) hay cho Excel đè (destructive)? Fill-nulls là mặc định an toàn nhưng không cập nhật được data đổi.

View File

@ -0,0 +1,81 @@
# sub-reviewer-0
**HEADLINE:** PASS_WITH_CONCERNS — 3 lane mapping/dedup/approach chính xác (mọi file:line in-repo verify KHỚP, 27/27 field bijective, 0 field bịa, 0 silent data-loss), NHƯNG 1 edge chưa lane nào chốt: dedup Code dùng Ordinal (case-sensitive) trong khi DB unique-index = default CI collation → hard unique-violation cho Approach A; + citation NamGroup không verify được.
## Reviewer độc lập — Supplier Excel-Import design (đọc source thật, không tin mù 3 lane)
**Method:** đọc trực tiếp 14 file in-repo mà 3 lane cite; cross-validate mapping với living seed 4 NCC; grep độc lập "no import endpoint"; grep collation config; verify Mig 62 tồn tại; search NamGroup repo trên disk.
**Kết luận nhanh:** 3 lane làm việc chất lượng cao — **mọi file:line in-repo đều verify KHỚP CHÍNH XÁC** (không phịa). 5 điểm kiểm chứng đều đứng vững ở tầng thiết-kế. Tôi bắt được **1 edge MAJOR** (case-collation dedup) mà cả 3 lane bỏ sót + vài minor. Không có lỗi nào làm hỏng design; chúng REFINE nó trước khi owner chọn approach.
---
### (1) MAPPING đủ 30 cột, 0 silent data-loss — ✅ PASS
Verify từng cột vs `Supplier.cs:5-36`:
- **28/30 cột có nhà rõ ràng** (cột 1 STT = ignore-có-lý-do; cột 228 = 27 direct-field).
- **Cột 29-30 (NGÀY/NGƯỜI CẬP NHẬT)** = KHÔNG bị nuốt câm — được **flag tường minh** với 3 option owner (ignore / gộp-Note / +2 field Mig 63). Đây là surface-to-owner đúng cách, KHÔNG phải data-loss.
- Cross-check với seed thật: `DbInitializer.cs:2776` TRUONGGIANG có `Phone="08 2253 1381"` (cột 8) TÁCH BIỆT `ContactPhone="0966 801 456"` (cột 21), và `Address` (cột 6) tách `MailingAddress` (cột 23) — xác nhận 3 address-field + 2 phone-field phân biệt đúng, không đè lên nhau. Mapping là **bijective** trên 27 field.
### (2) DEDUP KEY — ✅ PASS (key choice + null-safe) NHƯNG ⚠️ MAJOR gap ở tầng implementation
**Key choice đúng + null-safe (claim literal PASS):**
- Code UNIQUE filtered `SupplierConfiguration.cs:44` ✓ verified · TaxCode KHÔNG unique `:17` ✓ · Code `IsRequired` `:14` ✓ · TaxCode null DONGDUONG `DbInitializer.cs:2781` ✓. Dedup=Code nên TaxCode-null KHÔNG vỡ — **null-safe xác nhận**.
**🔴 MAJOR — edge cả 3 lane bỏ sót (Lane C 12 edge-case MISS case này):**
Lane C chốt dedup = `Code.Trim()` **Ordinal (case-SENSITIVE)** để khớp `StringComparer.Ordinal` của seed (`:2787`). NHƯNG:
- Grep collation trong toàn `SolutionErp.Infrastructure` = **0 hit** (không `UseCollation`, không explicit column collation) → Code inherit **default SQL Server collation = CASE-INSENSITIVE** (CI) trên đa số install.
- Hệ quả cho **Approach A** (import Excel ngoài): Excel có Code case-variant (vd `"truonggiang"` vs DB `"TRUONGGIANG"`) → dict Ordinal miss → quyết INSERT → DB unique-index (CI) THROW `DbUpdateException` unique-violation → **hard 500, cả batch all-or-nothing vỡ**.
- Seed hiện tại KHÔNG dính vì nó own 4 code uppercase cố định; nhưng import ăn Excel tùy ý thì dính.
- Lane A CÓ gợi ý "Trim+UPPER khuyến nghị" cho Code (đúng hướng fix) nhưng Lane C dedup-section lại viết "Ordinal" — **2 lane mâu thuẫn nội bộ**, và bản viết cuối (Ordinal) chính là cái tạo bug.
- **Acceptance để đóng:** normalize Code (Trim + case nhất-quán, vd UPPER) TRƯỚC cả dedup-check LẪN store, HOẶC dùng `StringComparer.OrdinalIgnoreCase` cho dict — để khớp collation DB. Preview cũng phải dùng cùng comparer, nếu không preview "sạch" rồi commit mới nổ.
*(Soft-delete slot: Excel Code trùng row IsDeleted=1 → dict [HasQueryFilter !IsDeleted] không thấy → INSERT → index filter `[IsDeleted]=0` cho phép → tạo row active mới. Đây là intended "slot reusable" `:44`, KHÔNG phải bug.)*
### (3) APPROACH cho solo-dev + 4-row — ✅ SOUND (không over-engineer, không mất data) + 2 MINOR
- Correction của Lane B **verified ĐÚNG**: spec framing "seed fill PARTIAL — không đụng 18 expansion field" là **SAI**. `DbInitializer.cs:2776,2778,2780,2782` set đủ expansion field (PackageCategory/BankAccount/LegalRepresentative/LinkGuq/Gpkd/Hsnl/ContactTitle/ContactPhone/MailingAddress/MailRecipient/ReferralSource/OwnerPmh/Status/Fax) cho 4 NCC. Approach B **THỰC ĐÃ SHIP** 30-field. Rec "file này đã xong, khỏi build" là anti-over-engineer đúng cho solo-dev.
- Fill-nulls `:2796-2813` verified: mỗi dòng `if (e.X is null && s.X is not null)`**KHÔNG bao giờ đè non-null**. Idempotent an toàn.
- **⚠️ MINOR-1 (external citation không verify được):** `find` toàn `D:/Dropbox/CONG_VIEC` cho `HrEmployeeImportService.cs` = **0 hit**; repo `ERP_MINI_FINAL` KHÔNG có mặt (chỉ FORM/QUY_TRINH/SOLUTION_ERP). Lane B dựng luận điểm "hạ effort A đáng kể nhờ port NamGroup" trên file:line (`:12/:15/:18/:24-44/:256/:317/:325`) **không thể verify từ env này** — có thể ở checkout khác, nhưng effort-estimate A đang tựa artifact chưa xác nhận. Design vẫn sound (preview→confirm là pattern chuẩn), chỉ con-số-effort là unbacked.
- **⚠️ MINOR-2 (Approach B là ONE-TIME):** fill-nulls không đè non-null → nếu owner SỬA Excel (đổi phone TRUONGGIANG) rồi re-seed, thay đổi **KHÔNG propagate**. Approach B = load-một-lần, KHÔNG phải cơ chế update. Lane B ngụ ý qua "thêm NCC = thêm row+redeploy" nhưng không nói sắc điểm "corrections không sync".
### (4) FIDELITY 27-field — ✅ PASS (0 bịa, 0 sót)
- Đếm `Supplier.cs:7-35`: đúng **27 business field** (9 core + 18 expansion). Tất cả 27 target mà 3 lane map ĐỀU tồn tại — **0 field ngoài Supplier.cs**.
- 27/27 field được map **đúng 1 lần** (audit đầy đủ ở đầu review). **0 field khớp bị sót.**
- Field "đề xuất mới" (`SourceUpdatedAt`/`SourceUpdatedBy` trong Lane A option-c) được label rõ là PROPOSED — không claim đã tồn tại → không vi phạm fidelity.
- Audit-field xác nhận không map được: `BaseEntity.cs:9` `UpdatedBy`**`Guid?`** (Excel = tên text → type mismatch) + `AuditingInterceptor.cs:48-49` ghi đè `UpdatedAt`/`UpdatedBy` mỗi Modified → map cột 29-30 vào audit = futile. **Cả 3 lane claim verified ĐÚNG.**
### (5) PARSER-STRATEGY — ✅ PASS (header-4-dòng + #REF! + backslash) + 1 MINOR
- Read primitive verified: `XlsxRenderer.cs:18` `new XLWorkbook` + `:22` `CellsUsed()` + `:25` `cell.GetString()` (auto-decode shared-string). ClosedXML `0.105.0` `csproj:8` ✓ — **0 dependency mới**.
- Anchor r4 + bỏ r1/r2/r3 + data từ r5 + header-fingerprint reject-file-nếu-lệch: xử đúng header-gộp + cột-đổi-vị-trí.
- #REF! → null-literal skip; NAS backslash → raw verbatim (khớp seed `:2776` `@"01. GPKD_HSNL_GUQ\02.Trường giang\...pdf"`); composite → raw (BankAccount MaxLen 500 `SupplierConfiguration.cs:29` đủ). Tất cả đúng.
- **⚠️ MINOR-3 (implementation caveat):** `CellsUsed()` (idiom được cite) **SKIP ô trống**. Cho POSITIONAL 30-cột, implementer PHẢI đọc header VÀ data theo **cùng absolute column index** (`row.Cell(i)`), KHÔNG iterate CellsUsed — nếu không, 1 ô rỗng (vd FAX trống) làm **lệch toàn bộ cột sau**. Fingerprint bắt lệch-gross nhưng đọc-bằng-CellsUsed có thể mismap câm. Intent (positional) của Lane C đúng; chỉ cần chốt primitive đọc-theo-index.
---
### Bảng severity
| # | Severity | Điểm | Gap | Đóng bằng |
|---|---|---|---|---|
| 1 | **MAJOR** | (2) | Dedup Code Ordinal vs DB default CI collation → hard unique-violation Approach A | Normalize Code case (hoặc OrdinalIgnoreCase dict) trước dedup+store, cả preview |
| 2 | MINOR | (3) | NamGroup `HrEmployeeImportService` cite không verify được (repo vắng) | Xác nhận artifact tồn tại trước khi tin effort-A-thấp |
| 3 | MINOR | (3) | Approach B = one-time fill-nulls, corrections không re-sync | Ghi rõ giới hạn; nếu cần update → Approach A |
| 4 | MINOR | (5) | `CellsUsed()` skip ô trống → lệch positional | Đọc header+data theo absolute cell index |
### Điểm resisted scrutiny (positive validation)
- Mapping bijective 27/27 + cross-validate living seed = solid, không phịa file:line.
- Dedup KEY = Code (không TaxCode) đúng + 5 chứng cứ đều verify khớp.
- "No import endpoint" premise verify độc lập: grep Suppliers Application + controller = **0 hit** import/IFormFile/upload.
- Mig 62 `ExpandSupplierFields` (2026-07-06) tồn tại → 18 expansion column có backing migration (schema integrity OK).
- Controller auth = `[Authorize(Roles=...)]` role-based (KHÔNG Policy) → import rec `Admin,CatalogManager` khớp convention, **không dính gotcha #44** (read mở, write role-gated).
**Smart-Friend guard:** không hạ bar. Design THẬT tốt → PASS phần lớn; nhưng MAJOR case-collation là bug hard-fail thật sự cho Approach A mà không lane nào chốt → nêu rõ, không rubber-stamp.
**Token cost ~34K.** RETURN-ONLY honored (không ghi MEMORY/file — em-main harvest).
## Open Questions
**Owner (anh) quyết:**
1. **Cột 29-30 (NGÀY/NGƯỜI CẬP NHẬT):** ignore (audit tự stamp, mất provenance) / gộp-Note / +2 field `SourceUpdatedAt`+`SourceUpdatedBy` (Mig 63)? — 3 lane đồng thuận đây là owner-call.
2. **Code source = cột "TÊN VIẾT TẮT"?** Code dùng trong mã HĐ (RG-001 `SOL&{SupplierCode}`) → cần anh xác nhận lấy từ col4 hay gán riêng (ảnh hưởng upsert-key Approach A).
3. **Có cần tool reusable?** Chỉ file 4-NCC → Approach B đã xong, khỏi build. NCC phát sinh liên tục → Approach A.
4. **Re-import Code đã tồn tại:** fill-nulls (an toàn, không sync data đổi) hay overwrite-non-empty (cập nhật được nhưng destructive)? — Lane C đề xuất overwrite cho import (khác seed), cần anh chốt.
5. **PHÂN LOẠI (Type non-nullable) gặp giá-trị-lạ:** reject-row hay default NhaCungCap?
**Implementer PHẢI fix trước khi build Approach A (không phải owner-call, là kỹ thuật):**
6. **Chuẩn hóa Code case** để khớp DB CI collation (MAJOR #1) — nếu bỏ qua, import case-variant Code sẽ nổ unique-violation hard-fail. Đây là điều kiện tiên quyết cho robustness của A, không được để dạng "Ordinal" như Lane C viết.