[CLAUDE] App+Api: S34 Plan 3 Item 3 BE 5 satellite CRUD scaffold (15 endpoint)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m36s

Phase 1.5 Item 3 BE — Implementer Case 2 cookie-cutter 5-entity scaffold
mirror parent EmployeeProfile CRUD pattern (Pattern 12-ter NEW saved).

New file: Application/Hrm/EmployeeSatelliteFeatures.cs (621 LOC):
- 5 region (#region {EntityName}) — WorkHistory/Education/FamilyRelation/Skill/Document
- Each region: Create/Update/Delete Command + Validator + Handler
- Verify parent EmployeeProfile.Exists pattern AnyAsync(!IsDeleted)
- Soft delete IsDeleted=true + DeletedAt=UtcNow + DeletedBy=currentUser
- Validator nullable enum IsInEnum().When(HasValue) pattern

Modified file: Api/Controllers/EmployeesController.cs (70 → 234 LOC, +15 endpoint):
- 5 region × 3 verb (POST/PUT/DELETE) = 15 satellite endpoint
- Path per satellite:
  - /api/employees/{id}/work-history
  - /api/employees/{id}/education
  - /api/employees/{id}/family-relations
  - /api/employees/{id}/skills
  - /api/employees/{id}/documents
- Per-action policy: Hrm_HoSo.Create/Update/Delete (override class-level Read)
- BadRequest guard: id != cmd.{EmployeeProfileId|Id} → "ID không khớp"

Document satellite metadata-only — file upload IFileStorage body wire defer S35.

Verify:
- dotnet build PASS (2 warn DocxRenderer baseline, 0 error)
- dotnet test 130/130 PASS (58 Domain + 72 Infra baseline preserve, no test add)
- Endpoints count: ~154 → 169 (+15)
- LOC delta: +785 BE (621 features + 164 controller)

Pattern 12-ter NEW (Implementer MEMORY): 5× satellite CRUD scaffold cookie-cutter
within-module (different from Pattern 12-bis cross-module mirror). Reusable
cho future N-satellite parent (vd Contract attachments, PE quotes, Budget details).

Defer S35:
- FE inline edit forms 5 satellite section (~1.5h em main solo)
- Test bundle satellite CRUD (~30 phút Implementer Case 3)
- IFileStorage Document body upload wire (multipart form-data)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-05-27 14:56:14 +07:00
parent 57099c56d7
commit e506cd8135
3 changed files with 806 additions and 0 deletions

View File

@ -67,4 +67,168 @@ public class EmployeesController(IMediator mediator) : ControllerBase
await mediator.Send(new DeleteEmployeeProfileCommand(id), ct);
return NoContent();
}
// Phase 1.5 Item 3 (S34) — 5 satellite CRUD endpoint scaffold.
// FE inline form satellite DEFER S35. File upload IFileStorage wire body DEFER.
// Per-action policy inherit class-level Read + override Create/Update/Delete.
#region WorkHistory satellite
[HttpPost("{id:guid}/work-history")]
[Authorize(Policy = "Hrm_HoSo.Create")]
public async Task<ActionResult<object>> CreateWorkHistory(
Guid id, [FromBody] CreateEmployeeWorkHistoryCommand cmd, CancellationToken ct)
{
if (id != cmd.EmployeeProfileId) return BadRequest(new { detail = "Employee ID không khớp" });
var newId = await mediator.Send(cmd, ct);
return Ok(new { id = newId });
}
[HttpPut("{id:guid}/work-history/{satId:guid}")]
[Authorize(Policy = "Hrm_HoSo.Update")]
public async Task<IActionResult> UpdateWorkHistory(
Guid id, Guid satId, [FromBody] UpdateEmployeeWorkHistoryCommand cmd, CancellationToken ct)
{
if (satId != cmd.Id) return BadRequest(new { detail = "ID không khớp" });
await mediator.Send(cmd, ct);
return NoContent();
}
[HttpDelete("{id:guid}/work-history/{satId:guid}")]
[Authorize(Policy = "Hrm_HoSo.Delete")]
public async Task<IActionResult> DeleteWorkHistory(Guid id, Guid satId, CancellationToken ct)
{
await mediator.Send(new DeleteEmployeeWorkHistoryCommand(satId), ct);
return NoContent();
}
#endregion
#region Education satellite
[HttpPost("{id:guid}/education")]
[Authorize(Policy = "Hrm_HoSo.Create")]
public async Task<ActionResult<object>> CreateEducation(
Guid id, [FromBody] CreateEmployeeEducationCommand cmd, CancellationToken ct)
{
if (id != cmd.EmployeeProfileId) return BadRequest(new { detail = "Employee ID không khớp" });
var newId = await mediator.Send(cmd, ct);
return Ok(new { id = newId });
}
[HttpPut("{id:guid}/education/{satId:guid}")]
[Authorize(Policy = "Hrm_HoSo.Update")]
public async Task<IActionResult> UpdateEducation(
Guid id, Guid satId, [FromBody] UpdateEmployeeEducationCommand cmd, CancellationToken ct)
{
if (satId != cmd.Id) return BadRequest(new { detail = "ID không khớp" });
await mediator.Send(cmd, ct);
return NoContent();
}
[HttpDelete("{id:guid}/education/{satId:guid}")]
[Authorize(Policy = "Hrm_HoSo.Delete")]
public async Task<IActionResult> DeleteEducation(Guid id, Guid satId, CancellationToken ct)
{
await mediator.Send(new DeleteEmployeeEducationCommand(satId), ct);
return NoContent();
}
#endregion
#region FamilyRelation satellite
[HttpPost("{id:guid}/family-relations")]
[Authorize(Policy = "Hrm_HoSo.Create")]
public async Task<ActionResult<object>> CreateFamilyRelation(
Guid id, [FromBody] CreateEmployeeFamilyRelationCommand cmd, CancellationToken ct)
{
if (id != cmd.EmployeeProfileId) return BadRequest(new { detail = "Employee ID không khớp" });
var newId = await mediator.Send(cmd, ct);
return Ok(new { id = newId });
}
[HttpPut("{id:guid}/family-relations/{satId:guid}")]
[Authorize(Policy = "Hrm_HoSo.Update")]
public async Task<IActionResult> UpdateFamilyRelation(
Guid id, Guid satId, [FromBody] UpdateEmployeeFamilyRelationCommand cmd, CancellationToken ct)
{
if (satId != cmd.Id) return BadRequest(new { detail = "ID không khớp" });
await mediator.Send(cmd, ct);
return NoContent();
}
[HttpDelete("{id:guid}/family-relations/{satId:guid}")]
[Authorize(Policy = "Hrm_HoSo.Delete")]
public async Task<IActionResult> DeleteFamilyRelation(Guid id, Guid satId, CancellationToken ct)
{
await mediator.Send(new DeleteEmployeeFamilyRelationCommand(satId), ct);
return NoContent();
}
#endregion
#region Skill satellite
[HttpPost("{id:guid}/skills")]
[Authorize(Policy = "Hrm_HoSo.Create")]
public async Task<ActionResult<object>> CreateSkill(
Guid id, [FromBody] CreateEmployeeSkillCommand cmd, CancellationToken ct)
{
if (id != cmd.EmployeeProfileId) return BadRequest(new { detail = "Employee ID không khớp" });
var newId = await mediator.Send(cmd, ct);
return Ok(new { id = newId });
}
[HttpPut("{id:guid}/skills/{satId:guid}")]
[Authorize(Policy = "Hrm_HoSo.Update")]
public async Task<IActionResult> UpdateSkill(
Guid id, Guid satId, [FromBody] UpdateEmployeeSkillCommand cmd, CancellationToken ct)
{
if (satId != cmd.Id) return BadRequest(new { detail = "ID không khớp" });
await mediator.Send(cmd, ct);
return NoContent();
}
[HttpDelete("{id:guid}/skills/{satId:guid}")]
[Authorize(Policy = "Hrm_HoSo.Delete")]
public async Task<IActionResult> DeleteSkill(Guid id, Guid satId, CancellationToken ct)
{
await mediator.Send(new DeleteEmployeeSkillCommand(satId), ct);
return NoContent();
}
#endregion
#region Document satellite
[HttpPost("{id:guid}/documents")]
[Authorize(Policy = "Hrm_HoSo.Create")]
public async Task<ActionResult<object>> CreateDocument(
Guid id, [FromBody] CreateEmployeeDocumentCommand cmd, CancellationToken ct)
{
if (id != cmd.EmployeeProfileId) return BadRequest(new { detail = "Employee ID không khớp" });
var newId = await mediator.Send(cmd, ct);
return Ok(new { id = newId });
}
[HttpPut("{id:guid}/documents/{satId:guid}")]
[Authorize(Policy = "Hrm_HoSo.Update")]
public async Task<IActionResult> UpdateDocument(
Guid id, Guid satId, [FromBody] UpdateEmployeeDocumentCommand cmd, CancellationToken ct)
{
if (satId != cmd.Id) return BadRequest(new { detail = "ID không khớp" });
await mediator.Send(cmd, ct);
return NoContent();
}
[HttpDelete("{id:guid}/documents/{satId:guid}")]
[Authorize(Policy = "Hrm_HoSo.Delete")]
public async Task<IActionResult> DeleteDocument(Guid id, Guid satId, CancellationToken ct)
{
await mediator.Send(new DeleteEmployeeDocumentCommand(satId), ct);
return NoContent();
}
#endregion
}