[CLAUDE] Supplier: import v2 — Mig 64 publish/draft + dedup-MST + template + file mẫu
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 5m14s

- Mig 64 AddSupplierPublishState: +IsPublic (draft/public); backfill 22 prod->public; filtered-unique doi [Code]<>'' (cho phep nhieu nhap Code=''); no new table (89).
- Import v2: dedup MST-primary + Code-backstop (chong 500 unique-violation); re-bake 30 token header THAT byte-exact (LayoutValid khop file that); nhap Ma NCC per-row (=Code); thieu Ma NCC->nhap (IsPublic=false); MST thieu->canh bao mem (MstMissing).
- PublishSupplierCommand rieng (ne #73 clobber); GET /suppliers/import/template (BE-gen xlsx 30-col).
- FE 2-app SHA-mirror: badge Public/Nhap; nut Cong bo; filter; nut Tai file mau; cot Ma editable; canh bao MST. Picker PE + tao HD loc published=true (an nhap -> ma HD khong dinh Code rong). CreateSupplier set IsPublic=true.
- authz D3: import/preview/confirm/template/publish = Policy Suppliers.Update (khop FE PermissionGuard, het 403).
- Tests +19 (477 PASS): dedup T1-T7, publish-guard, list-filter, all-or-nothing, LayoutValid. Fix null-Code NRE (path R4 loi).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-07-12 19:03:37 +07:00
parent 41f29acf68
commit 5fa11b588a
28 changed files with 7659 additions and 105 deletions

View File

@ -5,9 +5,11 @@ 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.PublishSupplier;
using SolutionErp.Application.Master.Suppliers.Commands.UpdateSupplier;
using SolutionErp.Application.Master.Suppliers.Dtos;
using SolutionErp.Application.Master.Suppliers.Queries.GetSupplier;
using SolutionErp.Application.Master.Suppliers.Queries.GetSupplierImportTemplate;
using SolutionErp.Application.Master.Suppliers.Queries.ListSuppliers;
using SolutionErp.Domain.Master;
@ -18,13 +20,14 @@ namespace SolutionErp.Api.Controllers;
[Authorize]
public class SuppliersController(IMediator mediator) : ControllerBase
{
// published: null = tất cả (admin quản lý thấy cả nháp) · true = chỉ NCC đã công bố (picker/consumer) · false = chỉ nháp.
[HttpGet]
public async Task<ActionResult<PagedResult<SupplierDto>>> List(
[FromQuery] int page = 1, [FromQuery] int pageSize = 20,
[FromQuery] string? search = null, [FromQuery] string? sortBy = null, [FromQuery] bool sortDesc = true,
[FromQuery] SupplierType? type = null,
[FromQuery] SupplierType? type = null, [FromQuery] bool? published = null,
CancellationToken ct = default)
=> Ok(await mediator.Send(new ListSuppliersQuery(type) { Page = page, PageSize = pageSize, Search = search, SortBy = sortBy, SortDesc = sortDesc }, ct));
=> Ok(await mediator.Send(new ListSuppliersQuery(type, published) { Page = page, PageSize = pageSize, Search = search, SortBy = sortBy, SortDesc = sortDesc }, ct));
[HttpGet("{id:guid}")]
public async Task<ActionResult<SupplierDto>> Get(Guid id, CancellationToken ct)
@ -59,9 +62,28 @@ public class SuppliersController(IMediator mediator) : ControllerBase
return NoContent();
}
// ========== Import Excel "Database NCC" (Supplier Phase B, Approach A — layout-locked) ==========
// [S113 D3] Công bố / ẩn 1 NCC (import v2). NHÁP (thiếu Mã NCC) → publish tay ở đây.
// KHÔNG dùng UpdateSupplierCommand (blind absolute-set → clobber #73). Body { maNcc?, publish }.
[Authorize(Policy = "Suppliers.Update")]
[HttpPost("{id:guid}/publish")]
public async Task<IActionResult> Publish(Guid id, [FromBody] PublishSupplierBody body, CancellationToken ct)
{
await mediator.Send(new PublishSupplierCommand(id, body.MaNcc, body.Publish), ct);
return NoContent();
}
// ========== Import Excel "Database NCC" (Supplier import v2 — layout-locked ROW 4, 30 cột) ==========
// [S113 D3] authz: policy quyền Suppliers Update (danh mục), khớp FE PermissionGuard (hết 403 non-admin).
// Tải file mẫu (.xlsx trống, 30 token header ROW 4). Single-source ExpectedHeaderTokens (0 drift).
[Authorize(Policy = "Suppliers.Update")]
[HttpGet("import/template")]
public async Task<IActionResult> ImportTemplate(CancellationToken ct)
=> File(await mediator.Send(new GetSupplierImportTemplateQuery(), ct),
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"Mau-Database-NCC.xlsx");
// Preview: parse + phân loại từng hàng (KHÔNG ghi DB). Header sai layout → LayoutValid=false.
[Authorize(Roles = "Admin,CatalogManager")]
[Authorize(Policy = "Suppliers.Update")]
[HttpPost("import/preview")]
[RequestSizeLimit(25_000_000)]
public async Task<ActionResult<SupplierImportPreviewDto>> ImportPreview(IFormFile file, CancellationToken ct)
@ -72,9 +94,9 @@ public class SuppliersController(IMediator mediator) : ControllerBase
return Ok(await mediator.Send(new SupplierImportPreviewCommand(stream), ct));
}
// Confirm: ALL-OR-NOTHING upsert (New → insert; existing case-insensitive-Code → fill-nulls).
// Confirm: ALL-OR-NOTHING upsert (dedup MST-primary + Code-backstop; blank Code = draft ẩn).
// Body JSON = { "rows": [ ... ] } (mảng SupplierImportRowDto round-trip từ preview).
[Authorize(Roles = "Admin,CatalogManager")]
[Authorize(Policy = "Suppliers.Update")]
[HttpPost("import/confirm")]
public async Task<ActionResult<SupplierImportResultDto>> ImportConfirm(
[FromBody] SupplierImportConfirmCommand cmd, CancellationToken ct)