[CLAUDE] App: YC-016 guard file-khi-duyệt KHKK theo TAY CẦM PHIẾU (slot 67 đóng)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 6m17s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 6m17s
Anh chốt "(c) THEO TAY CẦM PHIẾU": Nháp/Trả-lại (tay người soạn) ⇒ chỉ người soạn up file duyệt; Chờ-duyệt ⇒ chỉ thành viên quy trình đã pin (membership, không đòi đúng-lượt — chữ anh @S171 không thu hẹp); terminal DaDuyet/TuChoi ⇒ chặn cả hai (hồ sơ đã chốt). Guard mới EnsureApprovalAttachmentUploaderAsync (WorkflowGuards) wire vào UploadContractSigningPlanAttachmentCommandHandler — CHỈ rẽ nhánh Purpose=ApprovalAttachment; đường căn-cứ dossier giữ nguyên hành vi cũ (có control-test chặn nới guard ngoài phán quyết). Đóng lỗ đo prod @S172: drafter qua policy KeHoachKyKet.Update up được file vào phiếu không thuộc tay mình (#82 hai tầng — policy là quyền module, không phải quyền theo tay). FE 0-đổi (nút vốn chỉ trong dialog duyệt — display-layer; guard này là API-layer thật). Test 660 → 668 (+8: 3 nhánh tay × 2 chủ thể + ngoài-cuộc + terminal-2-vế + control đường-soạn). YC-016 vào sổ; YC-013 nhắc=2 (YC-015 verify 4-chân). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@ -1597,6 +1597,12 @@ public class UploadContractSigningPlanAttachmentCommandHandler(
|
||||
.FirstOrDefaultAsync(p => p.Id == request.ContractSigningPlanId, ct)
|
||||
?? throw new NotFoundException("ContractSigningPlan", request.ContractSigningPlanId);
|
||||
|
||||
// [YC-016 @S183 — "(c) THEO TAY CẦM PHIẾU"] CHỈ rẽ nhánh file-khi-duyệt; đường
|
||||
// căn-cứ dossier (purpose khác, người soạn upload lúc soạn) GIỮ NGUYÊN hành vi cũ.
|
||||
if (request.Purpose == ContractSigningPlanAttachmentPurpose.ApprovalAttachment)
|
||||
await ContractSigningPlanWorkflowGuards.EnsureApprovalAttachmentUploaderAsync(
|
||||
db, plan, currentUser.UserId, ct);
|
||||
|
||||
// Căn cứ gắn kèm (nếu có) phải thuộc ĐÚNG phiếu này — `DossierItemId` chỉ có index,
|
||||
// KHÔNG FK vật lý (`ContractSigningPlanAttachment.cs:14-19` — tránh multiple cascade
|
||||
// path) ⇒ DB không kiểm hộ, handler phải tự kiểm.
|
||||
|
||||
@ -2,6 +2,7 @@ using System.Text.RegularExpressions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SolutionErp.Application.Common.Exceptions;
|
||||
using SolutionErp.Application.Common.Interfaces;
|
||||
using SolutionErp.Domain.ContractSigningPlans;
|
||||
|
||||
namespace SolutionErp.Application.ContractSigningPlans;
|
||||
|
||||
@ -75,4 +76,52 @@ public static class ContractSigningPlanWorkflowGuards
|
||||
$"Phiếu nhóm N{approvalGroup} đang gắn quy trình N{k} — " +
|
||||
"vào phiếu (Nháp/Trả lại) đổi lại quy trình đúng nhóm trước khi trình.");
|
||||
}
|
||||
|
||||
/// [YC-016 @S183 — anh chốt "(c) THEO TAY CẦM PHIẾU"] File-khi-duyệt (`Purpose =
|
||||
/// ApprovalAttachment`) đi theo TAY đang cầm phiếu, không theo policy module:
|
||||
/// • Nháp / Trả lại (tay người soạn) ⇒ CHỈ người soạn (`CreatedBy`);
|
||||
/// • Chờ duyệt (tay người duyệt) ⇒ CHỈ thành viên quy trình đã pin
|
||||
/// (mọi `Level.ApproverUserId` của workflow — membership, KHÔNG đòi đúng-lượt:
|
||||
/// chữ anh @S171 "file do người duyệt tải lên trong quá trình duyệt" không thu
|
||||
/// hẹp về acting-level; siết thêm sau = 1 mệnh-đề);
|
||||
/// • Đã duyệt / Từ chối (terminal — không ai cầm) ⇒ CHẶN cả hai (hồ sơ đã chốt).
|
||||
///
|
||||
/// 🔴 Vì sao cần rào này dù endpoint đã có `[Authorize(Policy="KeHoachKyKet.Update")]`:
|
||||
/// policy là quyền MODULE (người soạn đương nhiên có để soạn phiếu) ⇒ đo prod @S172:
|
||||
/// `test.drafter@` up được file duyệt vào phiếu KHÔNG thuộc tay mình. Display-layer
|
||||
/// (nút chỉ nằm trong dialog duyệt) không phải rào — #82 hai tầng độc lập.
|
||||
public static async Task EnsureApprovalAttachmentUploaderAsync(
|
||||
IApplicationDbContext db, ContractSigningPlan plan, Guid? userId, CancellationToken ct)
|
||||
{
|
||||
switch (plan.Phase)
|
||||
{
|
||||
case ContractSigningPlanPhase.DangSoanThao:
|
||||
case ContractSigningPlanPhase.TraLai:
|
||||
if (userId is null || plan.CreatedBy != userId)
|
||||
throw new ConflictException(
|
||||
"Phiếu đang ở tay người soạn (Nháp/Trả lại) — chỉ người soạn được đính kèm file duyệt.");
|
||||
return;
|
||||
|
||||
case ContractSigningPlanPhase.ChoDuyet:
|
||||
{
|
||||
// Submit đã ép pin (`SubmitAsync` guard `ApprovalWorkflowId is not Guid ⇒ 409`)
|
||||
// nên nhánh null ở đây là fail-safe, không phải đường đi thường.
|
||||
if (plan.ApprovalWorkflowId is not Guid awId)
|
||||
throw new ConflictException(
|
||||
"Phiếu chưa gắn quy trình duyệt — không xác định được người duyệt.");
|
||||
|
||||
var isMember = userId is Guid uid && await db.ApprovalWorkflowLevels
|
||||
.AsNoTracking()
|
||||
.AnyAsync(l => l.Step!.ApprovalWorkflowId == awId && l.ApproverUserId == uid, ct);
|
||||
if (!isMember)
|
||||
throw new ConflictException(
|
||||
"Phiếu đang chờ duyệt — chỉ người duyệt trong quy trình được đính kèm file khi duyệt.");
|
||||
return;
|
||||
}
|
||||
|
||||
default: // DaDuyet / TuChoi — terminal, hồ sơ đã chốt
|
||||
throw new ConflictException(
|
||||
"Phiếu đã kết thúc — hồ sơ đã chốt, không đính kèm thêm file duyệt.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user