[CLAUDE] Supplier: Excel-import Phase B (upload NCC preview/confirm) + Mig 63
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 5m25s

Import Excel 'Database NCC': upload→preview classify (New/Update/Skip/Error)→confirm all-or-nothing upsert-by-Code (OrdinalIgnoreCase dedup, fill-nulls-safe). Mig 63 +SourceUpdatedAt/By (2 nullable). Parser layout-locked header-fingerprint NFC-normalized + absolute-cell-index + #REF!-null + NAS-backslash-raw. Type-lạ→NhaCungCap. FE dialog fe-admin+fe-user byte-identical. 10 test SupplierExcelImportServiceTests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-07-12 16:13:46 +07:00
parent a829d0df99
commit e100ef065b
20 changed files with 8268 additions and 14 deletions

View File

@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Mvc;
using SolutionErp.Application.Common.Models;
using SolutionErp.Application.Master.Suppliers.Commands.CreateSupplier;
using SolutionErp.Application.Master.Suppliers.Commands.DeleteSupplier;
using SolutionErp.Application.Master.Suppliers.Commands.ImportSuppliers;
using SolutionErp.Application.Master.Suppliers.Commands.UpdateSupplier;
using SolutionErp.Application.Master.Suppliers.Dtos;
using SolutionErp.Application.Master.Suppliers.Queries.GetSupplier;
@ -57,4 +58,25 @@ public class SuppliersController(IMediator mediator) : ControllerBase
await mediator.Send(new DeleteSupplierCommand(id), ct);
return NoContent();
}
// ========== Import Excel "Database NCC" (Supplier Phase B, Approach A — layout-locked) ==========
// Preview: parse + phân loại từng hàng (KHÔNG ghi DB). Header sai layout → LayoutValid=false.
[Authorize(Roles = "Admin,CatalogManager")]
[HttpPost("import/preview")]
[RequestSizeLimit(25_000_000)]
public async Task<ActionResult<SupplierImportPreviewDto>> ImportPreview(IFormFile file, CancellationToken ct)
{
if (file is null || file.Length == 0)
return BadRequest(new { detail = "Chưa chọn file." });
await using var stream = file.OpenReadStream();
return Ok(await mediator.Send(new SupplierImportPreviewCommand(stream), ct));
}
// Confirm: ALL-OR-NOTHING upsert (New → insert; existing case-insensitive-Code → fill-nulls).
// Body JSON = { "rows": [ ... ] } (mảng SupplierImportRowDto round-trip từ preview).
[Authorize(Roles = "Admin,CatalogManager")]
[HttpPost("import/confirm")]
public async Task<ActionResult<SupplierImportResultDto>> ImportConfirm(
[FromBody] SupplierImportConfirmCommand cmd, CancellationToken ct)
=> Ok(await mediator.Send(cmd, ct));
}