[CLAUDE] Contract: K3 SP-002 8 workflow nhom KHKK-N1-8 + port level-finalize + designer (0-mig) + 5 test
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 6m10s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 6m10s
Seed 8 workflow type-10 theo ROSTER LIVE prod (QT-DN-V2-001, lead verify tay @SQLEXPRESS: 3 tram PRO->CCM->CEO, 11 nguoi, resolve EMAIL, idempotent per-Code; co Finalize CHI cap chuong.phan). Port nhanh level-finalize mirror PE :866-876 (EndedByLevelFinalize K2-de-K3-tieu + ApplyApprovedValuesOnFinalize truoc Phase; opt-out applyLevelFinalize 3-tang interface/service/controller, default true). Sweep ep-false va-3: GO CREATE ep-false Finalize type-10 (OG-9 supersede PHA-VO S155); GIU PUT-Conflict (luat versioning CHUNG - spec liet nham, khai lech); GIU ep-false SkipToFinal type-10 (K3-test O-1 chong config-lie mới) + FE disable o SkipToFinal cho KHKK (gate-K3 F-2). Rao (v) pin-dung-nhom regex KHKK-N[1-8]. +8 MenuKeys AwV2_KhkkN* (All 56->64, policies 224->256 may-dem) + 8 menu-row + resolvePath fe-admin phep-am Khkk_ 0-hit. 5 test K3 (T-a..T-e, anti-tautology). Gate K3 PASS-WITH-FLAGS 8f/0-blocker (sub-reviewer-gate-k3.md): F-1 checkbox opt-out man duyet = KHAI HANDOFF dau-viec-#1 lan sau (tram CEO tam thoi qua Designer bo co); F-2 va; F-3 dong truoc do. Suite 614/0 (45D+569I). K3-BE 2 luot engine-exit -> che nho + lead-solo (bai: task-hep-Write-som song). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@ -209,7 +209,8 @@ public class ContractSigningPlansController(
|
||||
[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));
|
||||
=> Ok(await workflow.TransitionAsync(id, body.Action, body.Comment,
|
||||
body.ApplyLevelFinalize ?? true, ct));
|
||||
|
||||
// ========================= Body records =========================
|
||||
// PUT header: KHÔNG mang `Id` trong body (id lấy từ route) — tránh 2 nguồn sự thật.
|
||||
@ -223,7 +224,10 @@ public class ContractSigningPlansController(
|
||||
// `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).
|
||||
// [K3 S165] `ApplyLevelFinalize` — OPT-OUT khuôn PE S96: null/true = auto kết-thúc-tại-cấp
|
||||
// khi Cấp có `AllowApproverFinalize`; false = người duyệt BỎ tick ⇒ trình tiếp CEO.
|
||||
public record ContractSigningPlanTransitionBody(
|
||||
string Action,
|
||||
string? Comment = null);
|
||||
string? Comment = null,
|
||||
bool? ApplyLevelFinalize = null);
|
||||
}
|
||||
|
||||
@ -375,10 +375,17 @@ public class CreateAwDefinitionCommandHandler(IApplicationDbContext db)
|
||||
AllowApproverEditBudget = l.AllowApproverEditBudget,
|
||||
// [W1 S161 — reviewer F-8, F-B3 containment TẦNG BE] type=10 KHKK: 2 đường
|
||||
// early-finalize anh phán "PHÁ VỠ" (S155), W3 không port code path ⇒ ép false
|
||||
// server-side (FE đã ép ở levelFlagsPayload — đây chặn curl bypass). PUT
|
||||
// sửa-tại-chỗ đã có echo-so-BẰNG chặn đổi cờ ⇒ chỉ cần guard ở CREATE.
|
||||
// 🔄 [K3 S165 — OG-9 SUPERSEDE "PHÁ VỠ" @S155, sweep vá-3] GỠ ép-false
|
||||
// type-10 cho `AllowApproverFinalize`: owner @S164 đảo phán quyết ("a Chương
|
||||
// quyết cái gì cần CEO") — service KHKK ĐÃ port nhánh level-finalize cùng
|
||||
// wave ⇒ cờ hết config-lie #78. Đường bật cờ = TẠO VERSION MỚI (đây);
|
||||
// PUT sửa-tại-chỗ vẫn chặn đổi cờ (luật versioning CHUNG mọi type — spec
|
||||
// vá-3 liệt nhầm vào "cụm ép-false", lead giữ + khai lệch sub-implbe-k3.md).
|
||||
// 🔴 `AllowApproverSkipToFinal` GIỮ ép-false CHO RIÊNG type-10 (K3-test O-1):
|
||||
// service KHKK 0 lần đọc cờ này ("giữ nguyên không port" — 3-ĐIỂM-ADAPT (b))
|
||||
// ⇒ echo nó cho type-10 là ĐẺ config-lie #78 mới (DB true, máy im).
|
||||
AllowApproverSkipToFinal = typeEnum != ApprovalWorkflowApplicableType.ContractSigningPlan && l.AllowApproverSkipToFinal,
|
||||
AllowApproverFinalize = typeEnum != ApprovalWorkflowApplicableType.ContractSigningPlan && l.AllowApproverFinalize,
|
||||
AllowApproverFinalize = l.AllowApproverFinalize,
|
||||
AllowApproverDelete = l.AllowApproverDelete, // [Mig 68 S155] F6
|
||||
}).ToList(),
|
||||
})
|
||||
|
||||
@ -406,6 +406,19 @@ public class CreateContractSigningPlanCommandHandler(
|
||||
// ---- Rào (iv) WORKFLOW TYPE — copy trọn 2 nhánh Proposal `:258-267`.
|
||||
await ContractSigningPlanScope.EnsureWorkflowTypeAsync(db, request.ApprovalWorkflowId, ct);
|
||||
|
||||
// ---- Rào (v) [K3 S165 — QĐ-3 "K3 chỉ ĐỌC ApprovalGroup"] Workflow họ nhóm
|
||||
// `KHKK-N{n}` ⇒ n PHẢI khớp nhóm duyệt của phiếu (phiếu nhóm 3 pin quy trình nhóm 5
|
||||
// = 409). Code NGOÀI họ (type-10 legacy/chung) → cho qua — ràng buộc chỉ áp cho
|
||||
// 8 workflow nhóm seed @K3.
|
||||
var wfCode = await db.ApprovalWorkflows.AsNoTracking()
|
||||
.Where(w => w.Id == request.ApprovalWorkflowId)
|
||||
.Select(w => w.Code).FirstOrDefaultAsync(ct);
|
||||
var groupMatch = System.Text.RegularExpressions.Regex.Match(wfCode ?? string.Empty, "^KHKK-N([1-8])$");
|
||||
if (groupMatch.Success && int.Parse(groupMatch.Groups[1].Value) != approvalGroup)
|
||||
throw new ConflictException(
|
||||
$"Quy trình '{wfCode}' thuộc nhóm duyệt N{groupMatch.Groups[1].Value}, " +
|
||||
$"không khớp nhóm duyệt N{approvalGroup} của phiếu — chọn đúng quy trình của nhóm.");
|
||||
|
||||
// 🔴 GEN MÃ TRƯỚC KHI `Add` — `ContractSigningPlanCodeGenerator` TỰ COMMIT
|
||||
// (`ContractSigningPlanCodeGenerator.cs:23-43`: transaction SERIALIZABLE +
|
||||
// `SaveChangesAsync` nội bộ). Nếu phiếu đã nằm trong change-tracker lúc gọi, lượt
|
||||
|
||||
@ -26,6 +26,7 @@ public interface IContractSigningPlanWorkflowService
|
||||
Guid planId,
|
||||
string action,
|
||||
string? comment,
|
||||
bool applyLevelFinalize = true,
|
||||
CancellationToken ct = default);
|
||||
}
|
||||
|
||||
|
||||
@ -79,6 +79,23 @@ public static class MenuKeys
|
||||
public const string ApprovalWorkflowDuyetNccV2 = "AwV2_DuyetNcc"; // leaf cho Duyệt NCC mới
|
||||
public const string ApprovalWorkflowDuyetNccPhuongAnV2 = "AwV2_DuyetNccPhuongAn"; // leaf cho Duyệt NCC và Giải pháp mới
|
||||
|
||||
// [K3 nhịp-1 — S164/S165] 8 leaf Designer cho 8 quy trình duyệt KHKK theo NHÓM
|
||||
// DUYỆT N1..N8 (danh mục SOL-CCM-SP-002 Mig 70 chia 86 dòng thành 8 nhóm; phiếu
|
||||
// KHKK chốt nhóm lúc tạo — Mig 71). Mỗi nhóm 1 workflow `KHKK-N{n}` type-10 do
|
||||
// `DbInitializer.SeedKhkkGroupWorkflowsAsync` seed.
|
||||
// 🔴 8 key này VÀO `All` (lệnh lead K3-N1) ⇒ sinh 32 policy AwV2_KhkkN{1..8}.{R,C,U,D}
|
||||
// ⇒ 2 số canonical `docs/STATUS.md` đổi: Menu keys +8 · Policies +32 (DERIVED |All|×|Actions|).
|
||||
// 🔴 Vào `All` thì BẮT BUỘC có menu-row tương ứng trong `SeedMenuTreeAsync` —
|
||||
// thiếu row = Permission FK nổ ngay lần boot đầu (invariant test T6 K1).
|
||||
public const string ApprovalWorkflowKhkkN1 = "AwV2_KhkkN1";
|
||||
public const string ApprovalWorkflowKhkkN2 = "AwV2_KhkkN2";
|
||||
public const string ApprovalWorkflowKhkkN3 = "AwV2_KhkkN3";
|
||||
public const string ApprovalWorkflowKhkkN4 = "AwV2_KhkkN4";
|
||||
public const string ApprovalWorkflowKhkkN5 = "AwV2_KhkkN5";
|
||||
public const string ApprovalWorkflowKhkkN6 = "AwV2_KhkkN6";
|
||||
public const string ApprovalWorkflowKhkkN7 = "AwV2_KhkkN7";
|
||||
public const string ApprovalWorkflowKhkkN8 = "AwV2_KhkkN8";
|
||||
|
||||
// [S61 Mig 50] Module Ngân sách cũ (Budgets + Bg_List/Bg_Create/Bg_Pending)
|
||||
// đã XÓA — thay bằng "Ngân sách gói thầu" nhúng trong phiếu PE
|
||||
// (PeWorkItemBudgets per cặp Dự án × Hạng mục). Menu/Permission rows cũ
|
||||
@ -185,6 +202,9 @@ public static class MenuKeys
|
||||
KeHoachKyKet, // [W1 Mig 69 — S161] GĐ2 KHKK: const :41 vào All ⇒ sinh policy KeHoachKyKet.{Read,Create,Update,Delete}
|
||||
System, Users, Roles, Permissions, MenuVisibility, Workflows, PeWorkflows,
|
||||
ApprovalWorkflowsV2, ApprovalWorkflowDuyetNccV2, ApprovalWorkflowDuyetNccPhuongAnV2, // Mig 22
|
||||
// [K3 nhịp-1 — S164/S165] 8 leaf Designer cho 8 nhóm duyệt KHKK (+8 key ⇒ +32 policy).
|
||||
ApprovalWorkflowKhkkN1, ApprovalWorkflowKhkkN2, ApprovalWorkflowKhkkN3, ApprovalWorkflowKhkkN4,
|
||||
ApprovalWorkflowKhkkN5, ApprovalWorkflowKhkkN6, ApprovalWorkflowKhkkN7, ApprovalWorkflowKhkkN8,
|
||||
];
|
||||
|
||||
public static readonly string[] Actions = ["Read", "Create", "Update", "Delete"];
|
||||
|
||||
@ -164,6 +164,12 @@ public static class DbInitializer
|
||||
await SeedSampleTravelRequestWorkflowV2Async(db, userManager, logger);
|
||||
await SeedSampleVehicleBookingWorkflowV2Async(db, userManager, logger);
|
||||
|
||||
// [K3 nhịp-1 — S164/S165] 8 quy trình duyệt KHKK theo nhóm duyệt N1..N8
|
||||
// (ApplicableType=ContractSigningPlan=10). INFRASTRUCTURE seed NOT gated
|
||||
// DemoSeed (gotcha #51) — prod cần đủ 8 quy trình ngay deploy đầu, thiếu thì
|
||||
// Drafter không pick được quy trình lúc tạo phiếu KHKK. Idempotent per-Code.
|
||||
await SeedKhkkGroupWorkflowsAsync(db, userManager, logger);
|
||||
|
||||
await WarnDefaultAdminPasswordAsync(userManager, logger);
|
||||
}
|
||||
|
||||
@ -517,6 +523,169 @@ public static class DbInitializer
|
||||
logger.LogInformation("Seeded sample ApprovalWorkflow V2 for VehicleBooking: QT-XE-V2-001 v01");
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// [K3 nhịp-1 — S164/S165] 8 QUY TRÌNH DUYỆT KHKK theo NHÓM DUYỆT N1..N8
|
||||
// ============================================================================
|
||||
// Danh mục Hợp đồng SOL-CCM-SP-002 (Mig 70) chia 86 dòng vào 8 nhóm duyệt
|
||||
// (`ContractCatalogEntry.ApprovalGroup` 1..8); phiếu KHKK chốt nhóm lúc TẠO
|
||||
// (`ContractSigningPlan.ApprovalGroup`, Mig 71) ⇒ mỗi nhóm cần 1 quy trình V2
|
||||
// riêng để pin. Code `KHKK-N{n}`, ApplicableType=ContractSigningPlan (=10).
|
||||
//
|
||||
// ĐỘI HÌNH: 3 trạm PRO → CCM → BGĐ, sao đúng workflow Duyệt NCC prod bản LIVE
|
||||
// (`QT-DN-V2-001`, ApplicableType=1) theo owner OG-2 — "lấy đúng bộ người duyệt
|
||||
// đang cấu hình ở workflow Duyệt NCC prod". Cấp 1 mỗi trạm = OR-of-N (N row cùng
|
||||
// Order ⇒ bất kỳ ai trong cấp duyệt là xong cấp đó), Cấp 2 = trưởng phòng.
|
||||
//
|
||||
// INFRASTRUCTURE seed — NOT gated DemoSeed (gotcha #51).
|
||||
// Idempotent PER-CODE (`KHKK-N{n}`), CỐ Ý KHÔNG dùng khuôn `AnyAsync(w.Applicable
|
||||
// Type == ...)` của 6 seeder mẫu phía trên: ở đây 8 workflow DÙNG CHUNG 1 type,
|
||||
// check theo type sẽ chặn 7 nhóm còn lại ngay khi nhóm đầu vừa được tạo.
|
||||
//
|
||||
// Resolve người duyệt THEO EMAIL. User vắng mặt (DB dev thiếu người thật) ⇒ BỎ QUA
|
||||
// đúng slot đó + log warning, KHÔNG throw (seeder chạy lúc boot — throw = API chết).
|
||||
// Cấp không còn ai ⇒ bỏ cấp (Order được đánh LẠI liên tục 1,2,… để không vi phạm
|
||||
// ràng buộc sequential của Designer); Bước không còn cấp nào ⇒ bỏ bước; workflow
|
||||
// 0 bước vẫn TẠO (giữ mã cho phiếu pin + admin bổ sung người qua Designer) + log.
|
||||
private static async Task SeedKhkkGroupWorkflowsAsync(
|
||||
ApplicationDbContext db, UserManager<User> userManager, ILogger logger)
|
||||
{
|
||||
// Roster VERIFY LIVE trên prod 2026-07-31 (bản QT-DN-V2-001 IsActive=1) —
|
||||
// bước × phòng × [Cấp 1 OR-of-N] × [Cấp 2] × cờ finalize của Cấp 2.
|
||||
var roster = new (string StepName, string DeptCode, string[] Level1, string[] Level2, bool Level2Finalize)[]
|
||||
{
|
||||
("Phòng Cung Ứng", "PRO",
|
||||
new[]
|
||||
{
|
||||
"phuong.nguyen@solutions.com.vn", // Nguyễn Thị Bích Phượng
|
||||
"long.chau@solutions.com.vn", // Châu Tạ Kim Long
|
||||
"duy.nguyen@solutions.com.vn", // Nguyễn Văn Duy
|
||||
"binh.lethanh@solutions.com.vn", // Bình Lê Thanh (⟂ binh.le bên CCM)
|
||||
},
|
||||
new[] { "tra.bui@solutions.com.vn" }, // Bùi Lê Thủy Trà (TP.PRO)
|
||||
false),
|
||||
|
||||
("P. Kiểm soát Chi phí", "CCM",
|
||||
new[]
|
||||
{
|
||||
"luu.tran@solutions.com.vn", // Trần Xuân Lưu
|
||||
"truong.letrandang@solutions.com.vn", // Lê Trần Đăng Trường (⟂ truong.nguyen bên BGĐ)
|
||||
"binh.le@solutions.com.vn", // Lê Văn Bình
|
||||
"nguyen.ho@solutions.com.vn", // Hồ Thị Nữ Nguyên
|
||||
},
|
||||
new[] { "chuong.phan@solutions.com.vn" }, // Phan Văn Chương (TP.CCM)
|
||||
// 🔴 AllowApproverFinalize=true (owner OG-3/OG-9: a Chương quyết lên CEO hay
|
||||
// kết thúc). Nhánh service port ở nhịp sau CÙNG WAVE trước push nên không
|
||||
// phải config-lie #78.
|
||||
true),
|
||||
|
||||
("Ban Giám đốc", "BOD",
|
||||
new[] { "truong.nguyen@solutions.com.vn" }, // Nguyễn Văn Trường (CEO)
|
||||
Array.Empty<string>(),
|
||||
false),
|
||||
};
|
||||
|
||||
// Resolve email → UserId MỘT lượt (11 email, dùng lại cho cả 8 workflow).
|
||||
var usersByEmail = new Dictionary<string, Guid>(StringComparer.OrdinalIgnoreCase);
|
||||
var missingEmails = new List<string>();
|
||||
foreach (var email in roster.SelectMany(r => r.Level1.Concat(r.Level2))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
var user = await userManager.FindByEmailAsync(email);
|
||||
if (user is null) { missingEmails.Add(email); continue; }
|
||||
usersByEmail[email] = user.Id;
|
||||
}
|
||||
if (missingEmails.Count > 0)
|
||||
logger.LogWarning(
|
||||
"SeedKhkkGroupWorkflowsAsync: {Count} email người duyệt KHÔNG có user — bỏ qua slot đó: {Emails}",
|
||||
missingEmails.Count, string.Join(", ", missingEmails));
|
||||
|
||||
var deptIdByCode = (await db.Departments.ToListAsync())
|
||||
.GroupBy(d => d.Code)
|
||||
.ToDictionary(g => g.Key, g => (Guid?)g.First().Id);
|
||||
|
||||
var seeded = 0;
|
||||
for (var n = 1; n <= 8; n++)
|
||||
{
|
||||
var code = $"KHKK-N{n}";
|
||||
if (await db.ApprovalWorkflows.AnyAsync(w => w.Code == code)) continue;
|
||||
|
||||
var wf = new ApprovalWorkflow
|
||||
{
|
||||
Code = code,
|
||||
Version = 1,
|
||||
ApplicableType = ApprovalWorkflowApplicableType.ContractSigningPlan,
|
||||
Name = $"Duyệt KHKK — Nhóm {n}",
|
||||
Description =
|
||||
$"Quy trình duyệt Kế hoạch ký kết HĐ cho nhóm duyệt N{n} (danh mục SOL-CCM-SP-002). " +
|
||||
"3 trạm PRO → CCM → BGĐ, đội hình sao từ workflow Duyệt NCC prod. " +
|
||||
"Admin có thể chỉnh người/quyền tại chỗ hoặc tạo version mới qua Designer.",
|
||||
IsActive = true,
|
||||
IsUserSelectable = true,
|
||||
ActivatedAt = DateTime.UtcNow,
|
||||
};
|
||||
|
||||
var stepOrder = 1;
|
||||
foreach (var r in roster)
|
||||
{
|
||||
var step = new ApprovalWorkflowStep
|
||||
{
|
||||
ApprovalWorkflow = wf,
|
||||
Order = stepOrder,
|
||||
Name = r.StepName,
|
||||
DepartmentId = deptIdByCode.GetValueOrDefault(r.DeptCode),
|
||||
};
|
||||
|
||||
var levelOrder = 1;
|
||||
foreach (var (emails, finalize) in new[] { (r.Level1, false), (r.Level2, r.Level2Finalize) })
|
||||
{
|
||||
var approverIds = emails
|
||||
.Where(usersByEmail.ContainsKey)
|
||||
.Select(e => usersByEmail[e])
|
||||
.ToArray();
|
||||
if (approverIds.Length == 0) continue; // cấp rỗng → bỏ, KHÔNG chừa lỗ Order
|
||||
|
||||
foreach (var approverId in approverIds)
|
||||
{
|
||||
step.Levels.Add(new ApprovalWorkflowLevel
|
||||
{
|
||||
Step = step,
|
||||
Order = levelOrder, // N row cùng Order = OR-of-N trong 1 Cấp
|
||||
Name = $"Cấp {levelOrder}",
|
||||
ApproverUserId = approverId,
|
||||
AllowApproverFinalize = finalize,
|
||||
});
|
||||
}
|
||||
levelOrder++;
|
||||
}
|
||||
|
||||
if (step.Levels.Count == 0)
|
||||
{
|
||||
logger.LogWarning(
|
||||
"SeedKhkkGroupWorkflowsAsync: {Code} bỏ Bước \"{Step}\" — 0 người duyệt resolve được",
|
||||
code, r.StepName);
|
||||
continue;
|
||||
}
|
||||
|
||||
wf.Steps.Add(step);
|
||||
stepOrder++;
|
||||
}
|
||||
|
||||
if (wf.Steps.Count == 0)
|
||||
logger.LogWarning(
|
||||
"SeedKhkkGroupWorkflowsAsync: {Code} tạo RỖNG (0 Bước có người) — admin phải thêm người qua Designer",
|
||||
code);
|
||||
|
||||
db.ApprovalWorkflows.Add(wf);
|
||||
seeded++;
|
||||
}
|
||||
|
||||
if (seeded > 0)
|
||||
{
|
||||
await db.SaveChangesAsync();
|
||||
logger.LogInformation("Seeded {Count} KHKK group workflows (K3 SP-002)", seeded);
|
||||
}
|
||||
}
|
||||
|
||||
// Seed 4 master catalogs với defaults cho user nhập liệu Details. Idempotent:
|
||||
// skip per-table nếu đã có row (admin có thể đã thêm/sửa — không clobber).
|
||||
private static async Task SeedCatalogsAsync(ApplicationDbContext db, ILogger logger)
|
||||
@ -1808,6 +1977,18 @@ public static class DbInitializer
|
||||
(MenuKeys.ApprovalWorkflowsV2, "Quy trình duyệt (Mới)", MenuKeys.System, 96, "Workflow"),
|
||||
(MenuKeys.ApprovalWorkflowDuyetNccV2, "Duyệt NCC (Mới)", MenuKeys.ApprovalWorkflowsV2, 1, "FileCheck"),
|
||||
(MenuKeys.ApprovalWorkflowDuyetNccPhuongAnV2, "Duyệt NCC và Giải pháp (Mới)", MenuKeys.ApprovalWorkflowsV2, 2, "FileCheck"),
|
||||
// [K3 nhịp-1 — S164/S165] 8 leaf Designer cho 8 nhóm duyệt KHKK (workflow
|
||||
// `KHKK-N{n}` type-10 seed ở SeedKhkkGroupWorkflowsAsync). Key CÓ trong
|
||||
// MenuKeys.All ⇒ Admin được grant tự động qua loop SeedAdminPermissionsAsync;
|
||||
// upsert dưới là idempotent per-Key (key đã có → chỉ re-set Order rồi continue).
|
||||
(MenuKeys.ApprovalWorkflowKhkkN1, "Duyệt KHKK — Nhóm 1", MenuKeys.ApprovalWorkflowsV2, 3, "FileSignature"),
|
||||
(MenuKeys.ApprovalWorkflowKhkkN2, "Duyệt KHKK — Nhóm 2", MenuKeys.ApprovalWorkflowsV2, 4, "FileSignature"),
|
||||
(MenuKeys.ApprovalWorkflowKhkkN3, "Duyệt KHKK — Nhóm 3", MenuKeys.ApprovalWorkflowsV2, 5, "FileSignature"),
|
||||
(MenuKeys.ApprovalWorkflowKhkkN4, "Duyệt KHKK — Nhóm 4", MenuKeys.ApprovalWorkflowsV2, 6, "FileSignature"),
|
||||
(MenuKeys.ApprovalWorkflowKhkkN5, "Duyệt KHKK — Nhóm 5", MenuKeys.ApprovalWorkflowsV2, 7, "FileSignature"),
|
||||
(MenuKeys.ApprovalWorkflowKhkkN6, "Duyệt KHKK — Nhóm 6", MenuKeys.ApprovalWorkflowsV2, 8, "FileSignature"),
|
||||
(MenuKeys.ApprovalWorkflowKhkkN7, "Duyệt KHKK — Nhóm 7", MenuKeys.ApprovalWorkflowsV2, 9, "FileSignature"),
|
||||
(MenuKeys.ApprovalWorkflowKhkkN8, "Duyệt KHKK — Nhóm 8", MenuKeys.ApprovalWorkflowsV2, 10, "FileSignature"),
|
||||
// [S61 Mig 50] Menu module Ngân sách cũ (Budgets + 3 leaf Bg_*) đã XÓA —
|
||||
// rows cũ trên DB cleanup qua SQL trong migration (idempotent DELETE).
|
||||
// Module Nhân sự (Phase 10.1 G-H1 — Mig 34 S33). Root operational HR.
|
||||
|
||||
@ -34,16 +34,17 @@ namespace SolutionErp.Infrastructure.Services;
|
||||
//
|
||||
// 3 ĐIỂM ADAPT:
|
||||
// (a) Finalize choke-point: helper `ApplyApprovedValuesOnFinalize` gọi ở MỌI nhánh set
|
||||
// `DaDuyet` — xem khối LUẬT ngay trên helper. 🔴 KHAI RÕ: service này có **ĐÚNG MỘT**
|
||||
// nhánh set DaDuyet, KHÔNG có site thứ hai kiểu admin-override của nguồn copy
|
||||
// (`ContractWorkflowService.cs:190-193` gán `contract.Phase = targetPhase` qua BIẾN).
|
||||
// Lý do: hợp-đồng transition KHKK chỉ nhận 4 action cố định, KHÔNG có tham số
|
||||
// `targetPhase` ⇒ Admin muốn kết thúc phiếu vẫn phải đi đúng đường `approve`
|
||||
// (Admin chỉ được miễn vế "có tên trong Cấp", không được miễn choke-point).
|
||||
// Đó chính là write-path 2 mà test T3 đo.
|
||||
// (b) 🔴 KHÔNG PORT 2 nhánh kết thúc sớm (`AllowApproverFinalize`, `CeoApprovalThreshold`)
|
||||
// và cũng KHÔNG port `AllowApproverSkipToFinal` (`:318-352` nguồn copy) — chủ dự án
|
||||
// phán "PHÁ VỠ" @S155. W3 chỉ đường THƯỜNG: PMH → CCM → CEO ký thật.
|
||||
// `DaDuyet` — xem khối LUẬT ngay trên helper. 🔄 [K3 S165] service NAY có **HAI** nhánh
|
||||
// set DaDuyet (đường-thường Cấp-cuối + level-finalize dưới đây), CẢ HAI qua choke-point;
|
||||
// vẫn KHÔNG có site gán-qua-BIẾN kiểu nguồn copy (`ContractWorkflowService.cs:190-193`):
|
||||
// hợp-đồng transition KHKK chỉ nhận 4 action cố định, KHÔNG có tham số `targetPhase`
|
||||
// ⇒ Admin kết thúc phiếu vẫn đi đúng đường `approve` (miễn vế "có tên trong Cấp",
|
||||
// KHÔNG miễn choke-point). Tự-kiểm 2 lệnh grep ở khối LUẬT phải cùng khớp.
|
||||
// (b) 🔄 [K3 S165 — OG-9 SUPERSEDE "PHÁ VỠ" @S155] Owner @S164 đảo phán quyết (verbatim:
|
||||
// "chỗ budget chốt chặn là a Chương… bài toán trần budget xem như đóng") ⇒ ĐÃ PORT
|
||||
// nhánh `AllowApproverFinalize` (level-finalize, OPT-OUT khuôn PE S96 — cờ seed cho
|
||||
// TP.CCM Phan Văn Chương @K3). VẪN KHÔNG port `CeoApprovalThreshold` (ngưỡng máy —
|
||||
// NĐUQ = human-gate vĩnh viễn, OG-3) và `AllowApproverSkipToFinal` giữ nguyên không port.
|
||||
// (c) Trả lại = `TraLai` TOÀN PHẦN (về người soạn, xoá con-trỏ; gửi lại chạy LẠI từ Bước 1
|
||||
// Cấp 1). 4 return-mode per-level (`ApprovalWorkflow.cs:116-129`) CỐ Ý chưa đọc —
|
||||
// mở thêm mode là đổi hành vi duyệt, thuộc quyền lead.
|
||||
@ -87,6 +88,7 @@ public class ContractSigningPlanWorkflowService(
|
||||
Guid planId,
|
||||
string action,
|
||||
string? comment,
|
||||
bool applyLevelFinalize = true,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
// Rào action đứng TRƯỚC mọi truy vấn: gõ sai tên hành động là lỗi dữ liệu gửi lên,
|
||||
@ -121,7 +123,7 @@ public class ContractSigningPlanWorkflowService(
|
||||
break;
|
||||
|
||||
case "approve":
|
||||
await ApproveV2Async(plan, actorId, isAdmin, normalizedComment, ct);
|
||||
await ApproveV2Async(plan, actorId, isAdmin, normalizedComment, applyLevelFinalize, ct);
|
||||
break;
|
||||
|
||||
case "return":
|
||||
@ -242,7 +244,8 @@ public class ContractSigningPlanWorkflowService(
|
||||
// approve — approver Cấp đang chờ duyệt (OR-of-N)
|
||||
// =====================================================================
|
||||
private async Task ApproveV2Async(
|
||||
ContractSigningPlan plan, Guid actorId, bool isAdmin, string? comment, CancellationToken ct)
|
||||
ContractSigningPlan plan, Guid actorId, bool isAdmin, string? comment,
|
||||
bool applyLevelFinalize, CancellationToken ct)
|
||||
{
|
||||
if (plan.Phase != ContractSigningPlanPhase.ChoDuyet)
|
||||
throw new ConflictException("Chỉ duyệt được phiếu đang ở trạng thái Đã gửi duyệt.");
|
||||
@ -264,6 +267,27 @@ public class ContractSigningPlanWorkflowService(
|
||||
|
||||
var fromPhase = plan.Phase;
|
||||
|
||||
// ---- [K3 S165 — OG-3/OG-9 port level-finalize, mirror PE `:866-876`] Cấp có cờ
|
||||
// `AllowApproverFinalize` (seed K3: TP.CCM Phan Văn Chương, Bước 2 Cấp 2) được KẾT THÚC
|
||||
// quy trình TẠI ĐÂY thay vì trình tiếp CEO. OPT-OUT khuôn PE S96: client gửi
|
||||
// `applyLevelFinalize=false` (người duyệt BỎ tick "Cấp này KẾT THÚC") ⇒ rơi xuống
|
||||
// advance thường. Supersede "PHÁ VỠ" @S155 theo lời owner @S164 (xem 3-ĐIỂM-ADAPT (b)).
|
||||
// Cờ RUNTIME `EndedByLevelFinalize` = Mig 71 K2 đẻ — K3 tiêu thụ, HẾT ghost-window.
|
||||
if (actingLevel.AllowApproverFinalize && applyLevelFinalize)
|
||||
{
|
||||
plan.EndedByLevelFinalize = true;
|
||||
ApplyApprovedValuesOnFinalize(plan); // LUẬT choke-point: TRƯỚC khi gán Phase
|
||||
plan.Phase = ContractSigningPlanPhase.DaDuyet;
|
||||
plan.CurrentWorkflowStepIndex = null;
|
||||
plan.CurrentApprovalLevelOrder = null;
|
||||
await LogSigningPlanTransitionAsync(
|
||||
plan, fromPhase, plan.Phase, actorId, ApprovalDecision.Approve, comment,
|
||||
actingLevel.Id,
|
||||
$"Duyệt Bước {currentIdx + 1} Cấp {currentLevelOrder} — KẾT THÚC TẠI CẤP (level-finalize), kế hoạch ĐÃ DUYỆT, chốt giá",
|
||||
ct);
|
||||
return;
|
||||
}
|
||||
|
||||
// ---- Advance con-trỏ ĐÔI: còn Cấp trong Bước → Cấp++; hết Cấp → Bước kế (Cấp 1);
|
||||
// hết Bước → FINALIZE.
|
||||
if (currentLevelOrder < maxLevelOrder)
|
||||
|
||||
Reference in New Issue
Block a user