[CLAUDE] App+Infra+Api+FE-User+FE-Admin+Tests: W3 KHKK — duyet 3 tram OR-of-N + finalize chot gia (test-before 6 RED->GREEN)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 5m54s

- ContractSigningPlanWorkflowService: copy khuon ApproveV2Async (con-tro doi StepIndex+LevelOrder, GroupBy Order, OR-of-N, LevelOpinions UPSERT "(duyet — khong y kien)") — CAM Proposal-flatten; KHONG port 2 duong ket-thuc-som (owner phan PHA VO S155)
- Choke-point finalize ApplyApprovedValuesOnFinalize (Line.ApprovedAmount ??= Proposed) 1-SITE-BY-DESIGN: hop-dong transitions khong co targetPhase => admin cung xuyen choke-point (triet lop #81 tu goc); RULE-comment khuon :356-360
- Transitions endpoint: submit/approve/return/reject (hop-dong LEAD-dinh-nghia literal — reviewer Truc-4 KHOP 5/5, truc W2 tung dut 6 diem); guard trinh CreatedBy|Drafter|DeptManager-cung-phong|Admin; changelog/approvals bang KHKK rieng (CAM IChangelogService — FK-547 rao bang vang-mat-dep); notify Cap-ke dich danh + exclusion actor/drafter (S86)
- Save-diem: 1 SaveChanges cuoi = 1 transition 1 unit-of-work (service la diem vao, khac nguon :537)
- FE KhkkWorkflowPanel x2 app SHA-pair: so-do Buoc/Cap + banner den-luot + Gui/Duyet/Tra/Tu-choi + y kien; panel GATE nut theo actor (khong de an 403)
- Test-before: 6 test san PIN RED-evidence 6/6 (5 NotImplemented + 1 assert) -> GREEN 6/6; sua-test dung 1 seed FAULT-INJECT sot (0 assert doi); suite 574 -> 580/0
- Reviewer 7/7 truc DAT 0 chan (4 truc vai + 3 truc lead on-behalf; Q1-Q6 duyet)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-07-29 22:33:11 +07:00
parent d7bdb45312
commit df52fa0cd0
11 changed files with 2370 additions and 6 deletions

View File

@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using SolutionErp.Application.Common.Models;
using SolutionErp.Application.ContractSigningPlans;
using SolutionErp.Application.ContractSigningPlans.Services;
using SolutionErp.Domain.ContractSigningPlans;
namespace SolutionErp.Api.Controllers;
@ -18,12 +19,16 @@ namespace SolutionErp.Api.Controllers;
// (`MenuKeys.cs:42,180`) × `MenuKeys.Actions` trong `Api/Program.cs`. Key KHÔNG nằm
// trong `All` ⇒ 500 lúc chạy (policy không tồn tại), không phải 403.
//
// 🔴 KHÔNG có endpoint transition (submit/approve/reject/return) — đó là W3.
// [W3 đợt-1 — S161] ĐÃ CÓ endpoint transition (đính chính dòng cũ "KHÔNG có ... đó là W3"):
// `POST {id}/transitions` mở ở cuối file. Đợt-1 mới là SKELETON — service ném
// NotImplementedException ⇒ endpoint trả 500 cho tới khi đợt-3 điền logic. CỐ Ý: test-before.
// GlobalExceptionMiddleware map exception → ProblemDetails ⇒ TUYỆT ĐỐI không try-catch ở đây.
[ApiController]
[Route("api/contract-signing-plans")]
[Authorize(Policy = "KeHoachKyKet.Read")]
public class ContractSigningPlansController(IMediator mediator) : ControllerBase
public class ContractSigningPlansController(
IMediator mediator,
IContractSigningPlanWorkflowService workflow) : ControllerBase
{
// ========================= Đọc =========================
@ -190,6 +195,21 @@ public class ContractSigningPlansController(IMediator mediator) : ControllerBase
return NoContent();
}
// ===================== Duyệt 3 trạm (W3) =====================
// 🔴 HỢP-ĐỒNG LITERAL (lead chốt @S161 — 2 lane BE/FE khớp từng tên field):
// body { "action": "submit"|"approve"|"return"|"reject", "comment": string|null }
// response { "phase": int, "currentWorkflowStepIndex": int|null,
// "currentApprovalLevelOrder": int|null }
// Policy `KeHoachKyKet.Update` (KHÔNG Create/Delete): duyệt = GHI vào phiếu đã có.
// Đi THẲNG service (không qua MediatR) vì đây là 1 thao tác state-machine, không phải
// CQRS command — mirror `ContractsController` gọi `IContractWorkflowService`.
[HttpPost("{id:guid}/transitions")]
[Authorize(Policy = "KeHoachKyKet.Update")]
public async Task<ActionResult<ContractSigningPlanTransitionResult>> Transition(
Guid id, [FromBody] ContractSigningPlanTransitionBody body, CancellationToken ct)
=> Ok(await workflow.TransitionAsync(id, body.Action, body.Comment, ct));
// ========================= Body records =========================
// PUT header: KHÔNG mang `Id` trong body (id lấy từ route) — tránh 2 nguồn sự thật.
// Mọi field nullable = null-safe: client không gửi ⇒ giữ giá trị cũ (#73).
@ -197,4 +217,12 @@ public class ContractSigningPlansController(IMediator mediator) : ControllerBase
string? GhiChu = null,
string? HoSoLink = null,
Guid? ApprovalWorkflowId = null);
// Transition: `Action` BẮT BUỘC (thiếu ⇒ service ném 409 "Hành động không hợp lệ"),
// `Comment` tuỳ chọn. Tên property PascalCase bind đúng JSON "action"/"comment" —
// ASP.NET Core mặc định camelCase + đọc không phân biệt hoa/thường
// (`Api/Program.cs:33-34` chỉ thêm converter DateTime, KHÔNG đổi naming policy).
public record ContractSigningPlanTransitionBody(
string Action,
string? Comment = null);
}