# 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.
