[CLAUDE] Workflow: LeaveBalance business logic — trừ phép khi duyệt + số dư (Phase 11 P11-B)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 4m8s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 4m8s
Số dư phép theo (User × LeaveType × Year) + trừ tự động khi đơn nghỉ duyệt cuối. Policy: cho phép vượt số dư (âm) + cảnh báo (anh main chốt), tích hợp vào trang đơn nghỉ. Schema (Mig 42 AddLeaveBalances — pure additive, 1 bảng): - LeaveBalance: UserId + LeaveTypeId + Year + EntitledDays + UsedDays + AdjustmentDays. UNIQUE (UserId,LeaveTypeId,Year), FK LeaveType Restrict, decimal(5,2). Remaining = Entitled + Adjustment − Used (computed, không store). Deduction hook (ApproveLeaveRequestHandler nhánh terminal DaDuyet — exactly-once): - Upsert LeaveBalance(RequesterUserId, LeaveTypeId, StartDate.Year), auto-create từ LeaveType.DaysPerYear, UsedDays += NumDays. Guard Status!=DaGuiDuyet chặn re-approve. FK invariant guard (em main thêm sau test reveal FK risk): - Create + UpdateDraft validate LeaveTypeId tồn tại (AnyAsync) → ConflictException. Đóng cửa vào — bogus type không thể tới deduction FK insert (tránh 500 kẹt đơn). CQRS LeaveBalanceFeatures.cs: GetMy (self, lazy merge active LeaveType) + GetUser (admin) + AdjustLeaveBalance (admin upsert carry-over). Controller [Authorize] + admin Roles=Admin. Embed: GetLeaveRequestByIdHandler trả balance NGƯỜI TẠO (approver xem thấy đúng). FE: WorkflowAppDetailPage ×2 — block "Số dư phép" + cảnh báo vượt khi kind=leave (SHA256 identical). Tests (+11, 130→154 PASS): deduction single/multi-level/accumulate/negative-allowed/ reject-return-no-deduct + lazy-merge + adjust upsert + Create guard bogus→Conflict. Cũng repair 2 test S42 terminal FK-fail (template BuildLeave +seed LeaveType). Verify: build 0 error · 154 test · FE ×2 · reviewer Max PASS (deduction exactly-once + FK invariant fully closed, 2 minor concurrency/comment defer). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@ -0,0 +1,42 @@
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using SolutionErp.Application.Common.Interfaces;
|
||||
using SolutionErp.Application.Hrm;
|
||||
|
||||
namespace SolutionErp.Api.Controllers;
|
||||
|
||||
// Phase 11 P11-B Wave 1 (Mig 42 — S43) — Số dư phép theo năm.
|
||||
// /my = mọi user đăng nhập (xem phép của chính mình). Admin endpoint (xem user khác +
|
||||
// điều chỉnh) dùng [Authorize(Roles = "Admin")] — mirror HrmConfigsController convention
|
||||
// (HRM write/admin = Roles "Admin", KHÔNG menu policy).
|
||||
[ApiController]
|
||||
[Route("api/leave-balances")]
|
||||
[Authorize]
|
||||
public class LeaveBalancesController(IMediator mediator, IDateTime clock) : ControllerBase
|
||||
{
|
||||
[HttpGet("my")]
|
||||
public async Task<IActionResult> GetMy([FromQuery] int? year)
|
||||
=> Ok(await mediator.Send(new GetMyLeaveBalancesQuery(year ?? clock.Now.Year)));
|
||||
|
||||
[HttpGet]
|
||||
[Authorize(Roles = "Admin")]
|
||||
public async Task<IActionResult> GetForUser([FromQuery] Guid userId, [FromQuery] int? year)
|
||||
=> Ok(await mediator.Send(new GetUserLeaveBalancesQuery(userId, year ?? clock.Now.Year)));
|
||||
|
||||
[HttpPut("adjust")]
|
||||
[Authorize(Roles = "Admin")]
|
||||
public async Task<IActionResult> Adjust([FromBody] AdjustLeaveBalanceBody body)
|
||||
{
|
||||
await mediator.Send(new AdjustLeaveBalanceCommand(
|
||||
body.UserId, body.LeaveTypeId, body.Year, body.EntitledDays, body.AdjustmentDays));
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
public record AdjustLeaveBalanceBody(
|
||||
Guid UserId,
|
||||
Guid LeaveTypeId,
|
||||
int Year,
|
||||
decimal? EntitledDays,
|
||||
decimal? AdjustmentDays);
|
||||
}
|
||||
Reference in New Issue
Block a user