[CLAUDE] Domain+App+Api+Tests+FE-Admin+FE-User: S34 Plan 3 Phase 1.5 batch 4 item
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m48s

Phase 1.5 backlog G-H1 EmployeeProfile hardening batch (Items 6+2+1+4 of 6).

Item 6 — menuKeys FE drift sync × 2 app:
- fe-admin add: Catalogs + 4 Catalog leaves + Workflows + Budgets + Bg_List/Create/Pending (10 key)
- fe-user add: Budgets + Bg_List/Create/Pending + ApprovalWorkflowsV2 + 2 AwV2 leaf + MenuVisibility + Workflows (8 key)
- Cả 2 file giờ identical mirror BE MenuKeys.cs (28 key cumulative)

Item 2 — UpdateEmployeeProfileCommand bool→bool? safe partial update:
- 3 field IsCommunistParty/IsYouthUnion/IsTradeUnion → bool?
- Handler: HasValue check, null = giữ giá trị cũ (Reviewer minor #(b) S33 fixed)
- FE không bắt buộc gửi 3 field every PUT — tránh accidental reset

Item 1 — EmployeesController per-action policy (gotcha #44 mitigation):
- Class-level [Authorize(Policy = "Hrm_HoSo.Read")] — non-admin thiếu Read → 403
- POST [Authorize(Policy = "Hrm_HoSo.Create")]
- PUT  [Authorize(Policy = "Hrm_HoSo.Update")]
- DELETE [Authorize(Policy = "Hrm_HoSo.Delete")]

Item 4 — Test bundle Phase 1.5 (+10 [Fact], baseline 120 → 130/130 PASS):
- EmployeeCodeGeneratorTests (3 [Fact]) — atomic SERIALIZABLE NV/YYYY/NNNN
  + first call + sequential increment + year boundary preserve old year
- CreateEmployeeProfileCommandTests (4 [Fact]) — Create handler edge case
  + first profile + duplicate UserId Conflict + soft-deleted Conflict-restore
  + UserNotFound NotFoundException
- ListEmployeesQueryTests (3 [Fact]) — filter + paging logic
  + status filter + departmentId filter + search by EmployeeCode partial

Implementer Case 3 test gen caught spec mismatch (allow new after soft-delete
vs throws Conflict-restore) — chose CODE source of truth + renamed test
documenting discriminator message branch. Em main verify behavior correct
(admin UX khôi phục thay vì tạo mới — explicit flow defer Phase 1.5+).

Verify:
- dotnet build PASS (2 warn DocxRenderer baseline, 0 error)
- dotnet test 130/130 PASS (58 Domain + 72 Infra = +10)
- 4 endpoint /api/employees policy wired (gotcha #44 active mitigation)
- 4 MEMORY agent updated post-spawn (CICD Run #238 + Implementer test bundle)

Deferred Phase 1.5 next batch:
- Item 3 Satellite CRUD endpoints (WorkHistory/Education/FamilyRelation/Skill/
  Document) + FE inline edit forms — heavy ~2-3h
- Item 5 UAT smoke non-admin role verify silent 403 catch — defer post-deploy

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-05-27 13:57:08 +07:00
parent ea440da990
commit 61e9ce5b3b
9 changed files with 521 additions and 9 deletions

View File

@ -13,11 +13,16 @@ namespace SolutionErp.Api.Controllers;
// Satellite endpoint (WorkHistory/Education/FamilyRelation/Skill/Document
// CRUD) DEFER Phase 1.5.
//
// Class-level [Authorize] only — em main Task 6 wire per-action policy
// "Hrm_HoSo_View/Create/Edit/Delete" sau khi seed MenuKeys.
// Phase 1.5 S34 — per-action policy wired (Reviewer recommend gotcha #44 mitigation):
// GET → "Hrm_HoSo.Read"
// POST → "Hrm_HoSo.Create"
// PUT → "Hrm_HoSo.Update"
// DELETE → "Hrm_HoSo.Delete"
// Class-level Read policy default — non-admin role thiếu Read sẽ 403 silent
// (cross-ref gotcha #44 — FE PermissionGuard wrap để tránh silent UX).
[ApiController]
[Route("api/employees")]
[Authorize]
[Authorize(Policy = "Hrm_HoSo.Read")]
public class EmployeesController(IMediator mediator) : ControllerBase
{
[HttpGet]
@ -37,6 +42,7 @@ public class EmployeesController(IMediator mediator) : ControllerBase
=> Ok(await mediator.Send(new GetEmployeeProfileQuery(id), ct));
[HttpPost]
[Authorize(Policy = "Hrm_HoSo.Create")]
public async Task<ActionResult<object>> Create(
[FromBody] CreateEmployeeProfileCommand cmd, CancellationToken ct)
{
@ -45,6 +51,7 @@ public class EmployeesController(IMediator mediator) : ControllerBase
}
[HttpPut("{id:guid}")]
[Authorize(Policy = "Hrm_HoSo.Update")]
public async Task<IActionResult> Update(
Guid id, [FromBody] UpdateEmployeeProfileCommand cmd, CancellationToken ct)
{
@ -54,6 +61,7 @@ public class EmployeesController(IMediator mediator) : ControllerBase
}
[HttpDelete("{id:guid}")]
[Authorize(Policy = "Hrm_HoSo.Delete")]
public async Task<IActionResult> Delete(Guid id, CancellationToken ct)
{
await mediator.Send(new DeleteEmployeeProfileCommand(id), ct);