[CLAUDE] Contract: K1 SP-002 danh muc loai HD - ContractCatalogEntries (Mig 70) + seed 86 + CQRS + FE kind-5 x2 app + 8 test
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 6m31s

Gate: reviewer PASS-WITH-FLAGS 9f (sub-reviewer-gate-k1.md). Xu ly: F4
IncludeInactive mac-dinh-loai (spec K1.c) + test T5b; F7 comment 5-kind;
F1 OG-6 doi nhan soat-SAU @PAUSE-2 (seeder skip-if-exists giu sua-tay CRUD
qua restart - update-branch co y KHONG co); F3 authz mirror khuon Catalogs,
gioi-han khai: 4 policy ContractCatalog.* sinh ra nhung chua endpoint nao
tieu thu (display-layer khac API-layer, #82); F6 ten index IX_ giu EF-default
(NIT). Suite 598/0 (45D+553I). Mig 70 ap SQL Server that OK. Drift DELTA:
MenuKeys.All 55->56, policies 220->224, bang 96->97.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-07-31 15:45:50 +07:00
parent c0a96e54eb
commit 50e6d8c382
20 changed files with 8566 additions and 80 deletions

View File

@ -0,0 +1,62 @@
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using SolutionErp.Application.Master.Catalogs;
namespace SolutionErp.Api.Controllers;
// [K1 — S164, Mig 70] API "Danh mục Hợp đồng" (SOL-CCM-SP-002, 86 dòng × 8 nhóm duyệt).
//
// Route nằm DƯỚI `api/catalogs/` để đồng bộ với 4 catalog cũ (FE dùng chung trang
// KIND_CONFIG theo `:kind` → kind mới = `contract-catalog`), nhưng tách CONTROLLER
// riêng cho gọn (CatalogsController đã 4 kind × 4 verb).
//
// AUTHZ (QUYẾT-1 S164 — mirror y CatalogsController.cs:12-14, KHÔNG policy key mới):
// - class `[Authorize]` = mọi user đăng nhập ĐỌC được → picker chọn hạng mục ở
// màn tạo phiếu KHKK (K2) không bị 403 vì thiếu grant leaf (gotcha #44/#85).
// - mọi action GHI `[Authorize(Roles = "Admin")]` — sửa danh mục là việc quản trị,
// KHÔNG phải quyền của drafter phiếu.
[ApiController]
[Route("api/catalogs/contract-catalog")]
[Authorize]
public class ContractCatalogController(IMediator mediator) : ControllerBase
{
[HttpGet]
public async Task<List<ContractCatalogEntryDto>> List(
[FromQuery] string? groupCode,
[FromQuery] int? approvalGroup,
[FromQuery] bool includeInactive,
[FromQuery] string? q,
CancellationToken ct)
=> await mediator.Send(new ListContractCatalogQuery(groupCode, approvalGroup, includeInactive, q), ct);
[HttpGet("{id:guid}")]
public async Task<ContractCatalogEntryDto> Get(Guid id, CancellationToken ct)
=> await mediator.Send(new GetContractCatalogEntryQuery(id), ct);
[HttpPost]
[Authorize(Roles = "Admin")]
public async Task<IActionResult> Create([FromBody] CreateContractCatalogEntryCommand body, CancellationToken ct)
{
var id = await mediator.Send(body, ct);
return CreatedAtAction(nameof(Get), new { id }, new { id });
}
[HttpPut("{id:guid}")]
[Authorize(Roles = "Admin")]
public async Task<IActionResult> Update(Guid id, [FromBody] UpdateContractCatalogEntryCommand body, CancellationToken ct)
{
if (id != body.Id) return BadRequest("Id mismatch");
await mediator.Send(body, ct);
return NoContent();
}
[HttpDelete("{id:guid}")]
[Authorize(Roles = "Admin")]
public async Task<IActionResult> Delete(Guid id, CancellationToken ct)
{
await mediator.Send(new DeleteContractCatalogEntryCommand(id), ct);
return NoContent();
}
}