Files
solution-erp/.claude/workflows/runs/2026-07-12-S112-supplier-excel-import/sub-invest-codebase-C-edge.md
2026-07-12 13:38:40 +07:00

8.9 KiB
Raw Blame History

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.