[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
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:
@ -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);
|
||||
|
||||
@ -280,11 +280,13 @@ public record UpdateEmployeeProfileCommand(
|
||||
decimal? SeniorityLeaveDays,
|
||||
DateOnly? SocialInsuranceStartDate,
|
||||
string? MedicalRegistrationPlace,
|
||||
bool IsCommunistParty,
|
||||
// Phase 1.5 S34 — bool → bool? safe partial update. FE chỉ gửi field admin
|
||||
// muốn đổi → null = KHÔNG đổi (giữ giá trị cũ). Reviewer minor #(b) S33.
|
||||
bool? IsCommunistParty,
|
||||
DateOnly? CommunistPartyJoinDate,
|
||||
bool IsYouthUnion,
|
||||
bool? IsYouthUnion,
|
||||
DateOnly? YouthUnionJoinDate,
|
||||
bool IsTradeUnion,
|
||||
bool? IsTradeUnion,
|
||||
DateOnly? TradeUnionJoinDate,
|
||||
string? PhotoUrl,
|
||||
string? Notes) : IRequest;
|
||||
@ -403,11 +405,13 @@ public class UpdateEmployeeProfileCommandHandler(
|
||||
entity.SeniorityLeaveDays = request.SeniorityLeaveDays;
|
||||
entity.SocialInsuranceStartDate = request.SocialInsuranceStartDate;
|
||||
entity.MedicalRegistrationPlace = request.MedicalRegistrationPlace;
|
||||
entity.IsCommunistParty = request.IsCommunistParty;
|
||||
// Phase 1.5 S34 — bool? partial update: null = KHÔNG đổi (giữ giá trị cũ).
|
||||
// FE chỉ gửi field admin muốn đổi tránh accidental reset (Reviewer minor S33).
|
||||
if (request.IsCommunistParty.HasValue) entity.IsCommunistParty = request.IsCommunistParty.Value;
|
||||
entity.CommunistPartyJoinDate = request.CommunistPartyJoinDate;
|
||||
entity.IsYouthUnion = request.IsYouthUnion;
|
||||
if (request.IsYouthUnion.HasValue) entity.IsYouthUnion = request.IsYouthUnion.Value;
|
||||
entity.YouthUnionJoinDate = request.YouthUnionJoinDate;
|
||||
entity.IsTradeUnion = request.IsTradeUnion;
|
||||
if (request.IsTradeUnion.HasValue) entity.IsTradeUnion = request.IsTradeUnion.Value;
|
||||
entity.TradeUnionJoinDate = request.TradeUnionJoinDate;
|
||||
entity.PhotoUrl = request.PhotoUrl;
|
||||
entity.Notes = request.Notes;
|
||||
|
||||
Reference in New Issue
Block a user