[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

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:
pqhuy1987
2026-07-31 18:21:39 +07:00
parent d6cb5469b3
commit 15349e8ee0
15 changed files with 766 additions and 83 deletions

View File

@ -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.