[CLAUDE] Workflow: W3 seed 7 quy trình HD-* sao khuôn KHKK (roster builder chung, finalize=false có khai) + 7 key AwV2_Hd* by-construction + menu-row T6 + 2 row canonical STATUS; test T2/T6 khoá drift
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@ -170,6 +170,13 @@ public static class DbInitializer
|
||||
// Drafter không pick được quy trình lúc tạo phiếu KHKK. Idempotent per-Code.
|
||||
await SeedKhkkGroupWorkflowsAsync(db, userManager, logger);
|
||||
|
||||
// [S187 W3 — YC-023/YC-024] 7 quy trình duyệt HĐ theo LOẠI (`HD-*`,
|
||||
// ApplicableType=Contract=3). Cùng lý do ungated + idempotent per-Code như KHKK
|
||||
// ngay trên (gotcha #51). Đặt SAU KHKK vì dùng chung `ProCcmBodApprovalRoster`
|
||||
// và cùng khuôn 3 trạm — đọc 2 seeder cạnh nhau thấy ngay chỗ chúng LỆCH
|
||||
// (cờ finalize: KHKK true / HĐ false).
|
||||
await SeedContractTypeWorkflowsAsync(db, userManager, logger);
|
||||
|
||||
await WarnDefaultAdminPasswordAsync(userManager, logger);
|
||||
}
|
||||
|
||||
@ -546,13 +553,25 @@ public static class DbInitializer
|
||||
// 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)[]
|
||||
{
|
||||
// ĐỘI HÌNH 3 TRẠM DÙNG CHUNG — nguồn DUY NHẤT của "ai duyệt, trạm nào, thứ tự nào".
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
// 🔴 [S187 W3] NÂNG từ biến cục bộ của seeder KHKK lên field dùng chung, vì họ quy trình
|
||||
// HĐ (`SeedContractTypeWorkflowsAsync`, type-3) phải "SAO KHUÔN KHKK" theo owner (83).
|
||||
// Chép mảng lần thứ hai = 2 bản 11 email đi lệch nhau theo thời gian (đúng bug-class mà
|
||||
// `ContractSigningPlanWorkflowGuards:28-32` đang chống) ⇒ dùng CHUNG để 2 họ khớp
|
||||
// by-construction. Đo 2026-08-10 trên Dev: 8/8 workflow `KHKK-N*` có chữ ký cấu trúc
|
||||
// GIỐNG HỆT nhau (MD5 `7EB6CD21…` trên tập bước/phòng/cấp/email/cờ) ⇒ "khuôn chung" là
|
||||
// mô tả ĐO ĐƯỢC, không phải giả định.
|
||||
// ⚠️ Muốn 2 họ KHÁC người nhau thì TÁCH thành 2 mảng đặt cạnh nhau — ĐỪNG sửa tại chỗ,
|
||||
// sửa ở đây là đổi luôn họ kia mà không ai thấy. (Chỉnh người cho MỘT quy trình đang chạy
|
||||
// là việc của admin trên Designer, không phải của seeder: seeder idempotent per-Code nên
|
||||
// sau lần seed đầu nó không đụng vào DB nữa.)
|
||||
private static readonly (string StepName, string DeptCode, string[] Level1, string[] Level2, bool Level2Finalize)[]
|
||||
ProCcmBodApprovalRoster =
|
||||
[
|
||||
("Phòng Cung Ứng", "PRO",
|
||||
new[]
|
||||
{
|
||||
@ -576,15 +595,27 @@ public static class DbInitializer
|
||||
// 🔴 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.
|
||||
// 🔴 [S187 W3] Cờ này CHỈ ăn cho họ KHKK (type-10). Họ HĐ (type-3) truyền
|
||||
// `applyLevel2Finalize: false` — xem chú thích tại `SeedContractTypeWorkflowsAsync`:
|
||||
// máy duyệt type-3 (`ContractWorkflowService.ApproveV2Async`) KHÔNG có nhánh
|
||||
// finalize (đo: 0 hit `AllowApproverFinalize`; control dương PE `:870` + KHKK
|
||||
// `ContractSigningPlanWorkflowService.cs:286` đều CÓ) ⇒ seed `true` cho HĐ sẽ
|
||||
// khoe trên Designer một quyền mà engine không thi hành = đúng #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).
|
||||
// Resolve email → UserId MỘT lượt cho cả roster (dùng lại cho MỌI workflow của họ).
|
||||
// Thiếu user ⇒ ghi warning + bỏ đúng slot đó, KHÔNG throw (seeder chạy lúc boot).
|
||||
private static async Task<Dictionary<string, Guid>> ResolveRosterApproversAsync(
|
||||
UserManager<User> userManager,
|
||||
(string StepName, string DeptCode, string[] Level1, string[] Level2, bool Level2Finalize)[] roster,
|
||||
string seeder, ILogger logger)
|
||||
{
|
||||
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))
|
||||
@ -596,13 +627,83 @@ public static class DbInitializer
|
||||
}
|
||||
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));
|
||||
"{Seeder}: {Count} email người duyệt KHÔNG có user — bỏ qua slot đó: {Emails}",
|
||||
seeder, missingEmails.Count, string.Join(", ", missingEmails));
|
||||
return usersByEmail;
|
||||
}
|
||||
|
||||
var deptIdByCode = (await db.Departments.ToListAsync())
|
||||
private static async Task<Dictionary<string, Guid?>> LoadDepartmentIdsByCodeAsync(ApplicationDbContext db) =>
|
||||
(await db.Departments.ToListAsync())
|
||||
.GroupBy(d => d.Code)
|
||||
.ToDictionary(g => g.Key, g => (Guid?)g.First().Id);
|
||||
|
||||
// Dựng 3 trạm PRO → CCM → BGĐ theo `ProCcmBodApprovalRoster` vào MỘT workflow.
|
||||
// Cấp không còn ai ⇒ bỏ cấp (Order đá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 + log.
|
||||
// `applyLevel2Finalize=false` ⇒ ép cờ finalize của Cấp 2 về false cho MỌI trạm (họ nào mà
|
||||
// engine chưa có nhánh finalize thì truyền false — xem chú thích cờ trong roster).
|
||||
private static void AddProCcmBodStepsToWorkflow(
|
||||
ApprovalWorkflow wf,
|
||||
IReadOnlyDictionary<string, Guid> usersByEmail,
|
||||
IReadOnlyDictionary<string, Guid?> deptIdByCode,
|
||||
bool applyLevel2Finalize,
|
||||
string seeder, string code, ILogger logger)
|
||||
{
|
||||
var stepOrder = 1;
|
||||
foreach (var r in ProCcmBodApprovalRoster)
|
||||
{
|
||||
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, applyLevel2Finalize && 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(
|
||||
"{Seeder}: {Code} bỏ Bước \"{Step}\" — 0 người duyệt resolve được",
|
||||
seeder, code, r.StepName);
|
||||
continue;
|
||||
}
|
||||
|
||||
wf.Steps.Add(step);
|
||||
stepOrder++;
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task SeedKhkkGroupWorkflowsAsync(
|
||||
ApplicationDbContext db, UserManager<User> userManager, ILogger logger)
|
||||
{
|
||||
var usersByEmail = await ResolveRosterApproversAsync(
|
||||
userManager, ProCcmBodApprovalRoster, nameof(SeedKhkkGroupWorkflowsAsync), logger);
|
||||
var deptIdByCode = await LoadDepartmentIdsByCodeAsync(db);
|
||||
|
||||
var seeded = 0;
|
||||
for (var n = 1; n <= 8; n++)
|
||||
{
|
||||
@ -624,51 +725,12 @@ public static class DbInitializer
|
||||
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++;
|
||||
}
|
||||
// [S187 W3] Vòng dựng trạm CŨ (inline) đã nâng nguyên xi thành `AddProCcmBodStepsToWorkflow`.
|
||||
// `applyLevel2Finalize: true` ⇒ biểu thức trong builder là `true && r.Level2Finalize`
|
||||
// = ĐÚNG bằng `r.Level2Finalize` của dòng cũ ⇒ hành vi KHKK không đổi một li.
|
||||
AddProCcmBodStepsToWorkflow(
|
||||
wf, usersByEmail, deptIdByCode, applyLevel2Finalize: true,
|
||||
nameof(SeedKhkkGroupWorkflowsAsync), code, logger);
|
||||
|
||||
if (wf.Steps.Count == 0)
|
||||
logger.LogWarning(
|
||||
@ -686,6 +748,76 @@ public static class DbInitializer
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// [S187 W3 — YC-023/YC-024] 7 QUY TRÌNH DUYỆT HỢP ĐỒNG theo LOẠI HĐ
|
||||
// ============================================================================
|
||||
// Owner @S188: (80) trục = 7 LOẠI HĐ · (81) PA-A mỗi loại MỘT mã riêng, 0 cột mới ·
|
||||
// (83) đội hình SAO KHUÔN KHKK, anh tự chỉnh lại trong Admin Designer sau.
|
||||
// Mã + tên loại: `ContractApprovalWorkflowFamily` (Domain/Contracts) — NHÀ DUY NHẤT.
|
||||
// `ApplicableType = Contract` (=3) ⇒ Drafter pick được ở màn tạo HĐ (dropdown lọc
|
||||
// `IsUserSelectable && IsActive` theo type).
|
||||
//
|
||||
// INFRASTRUCTURE seed — NOT gated DemoSeed (gotcha #51): prod cần đủ 7 quy trình ngay
|
||||
// deploy đầu, thiếu thì HĐ loại đó không pin được quy trình nào.
|
||||
// Idempotent PER-CODE (`HD-*`) — CÙNG lý do như KHKK: 7 workflow DÙNG CHUNG 1 type nên
|
||||
// check theo `ApplicableType` sẽ chặn 6 loại còn lại ngay khi loại đầu vừa tạo (và ở đây
|
||||
// còn tệ hơn: type-3 ĐÃ có sẵn `QT-HD-V2-001` sample từ S29 ⇒ check-by-type = seed 0 cái).
|
||||
//
|
||||
// 🔴 CỜ `AllowApproverFinalize` CỐ Ý KHÔNG SAO NGUYÊN XI (`applyLevel2Finalize: false`):
|
||||
// roster KHKK bật finalize ở Cấp 2 trạm CCM, nhưng máy duyệt type-3
|
||||
// (`ContractWorkflowService`) đo ngày 2026-08-10 có **0 hit** `AllowApproverFinalize`
|
||||
// (control dương: PE `PurchaseEvaluationWorkflowService.cs:870` CÓ · KHKK
|
||||
// `ContractSigningPlanWorkflowService.cs:286` CÓ ⇒ thước sống, 0-hit là vắng THẬT).
|
||||
// Seed `true` khi engine không thi hành = Designer khoe một quyền không có thật —
|
||||
// đúng gotcha #78 (config-lie). Owner bật lại bằng 1 click trong Designer khi máy
|
||||
// type-3 có nhánh finalize. TRẠM/NGƯỜI/THỨ TỰ/OR-of-N giữ NGUYÊN theo (83).
|
||||
private static async Task SeedContractTypeWorkflowsAsync(
|
||||
ApplicationDbContext db, UserManager<User> userManager, ILogger logger)
|
||||
{
|
||||
var usersByEmail = await ResolveRosterApproversAsync(
|
||||
userManager, ProCcmBodApprovalRoster, nameof(SeedContractTypeWorkflowsAsync), logger);
|
||||
var deptIdByCode = await LoadDepartmentIdsByCodeAsync(db);
|
||||
|
||||
var seeded = 0;
|
||||
foreach (var (_, code, shortName) in ContractApprovalWorkflowFamily.Members)
|
||||
{
|
||||
if (await db.ApprovalWorkflows.AnyAsync(w => w.Code == code)) continue;
|
||||
|
||||
var wf = new ApprovalWorkflow
|
||||
{
|
||||
Code = code,
|
||||
Version = 1,
|
||||
ApplicableType = ApprovalWorkflowApplicableType.Contract,
|
||||
Name = $"Duyệt Hợp đồng — {shortName}",
|
||||
Description =
|
||||
$"Quy trình duyệt Hợp đồng loại \"{shortName}\". Đội hình sao khuôn quy trình " +
|
||||
"duyệt KHKK (3 trạm PRO → CCM → BGĐ). Admin 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,
|
||||
};
|
||||
|
||||
AddProCcmBodStepsToWorkflow(
|
||||
wf, usersByEmail, deptIdByCode, applyLevel2Finalize: false,
|
||||
nameof(SeedContractTypeWorkflowsAsync), code, logger);
|
||||
|
||||
if (wf.Steps.Count == 0)
|
||||
logger.LogWarning(
|
||||
"SeedContractTypeWorkflowsAsync: {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} contract-type approval workflows (W3 YC-023)", 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)
|
||||
@ -2156,6 +2288,27 @@ public static class DbInitializer
|
||||
}
|
||||
}
|
||||
|
||||
// [S187 W3 — YC-023/YC-024] 7 leaf Designer cho 7 quy trình duyệt HĐ theo LOẠI
|
||||
// (workflow `HD-*` type-3 seed ở `SeedContractTypeWorkflowsAsync`). Cùng cha
|
||||
// `ApprovalWorkflowsV2` với 8 leaf KHKK ngay trên, Order tiếp ngay sau (KHKK 3..10
|
||||
// ⇒ HĐ 11..17). Key CÓ trong `MenuKeys.All` ⇒ Admin được grant tự động qua loop
|
||||
// `SeedAdminPermissionsAsync`; upsert dưới idempotent per-Key.
|
||||
// 🔴 Key sinh bằng CHÍNH `MenuKeys.ApprovalWorkflowContractLeaf` trên CHÍNH
|
||||
// `ContractApprovalWorkflowFamily.Members` — tức CÙNG bộ sinh với
|
||||
// `MenuKeys.ContractWorkflowMenuKeys` đang nằm trong `All` ⇒ mọi key của họ đều
|
||||
// có menu-row BY-CONSTRUCTION (invariant T6: key vào `All` mà thiếu menu-row =
|
||||
// Permission FK nổ ngay lần boot đầu). Thêm loại HĐ thứ 8 chỉ phải sửa 1 nơi.
|
||||
// ⚠️ Nhãn ở đây chỉ ăn cho DB TRỐNG / key MỚI. Đổi nhãn sau khi đã seed BẮT BUỘC
|
||||
// thêm entry vào `labelBackfill` bên dưới — upsert chỉ re-set `Order` (gotcha #11).
|
||||
var awV2ContractOrder = 11;
|
||||
foreach (var (_, familyCode, shortName) in ContractApprovalWorkflowFamily.Members)
|
||||
{
|
||||
var leafKey = MenuKeys.ApprovalWorkflowContractLeaf(
|
||||
ContractApprovalWorkflowFamily.KeyToken(familyCode));
|
||||
tree.Add((leafKey, $"QT Duyệt HĐ — {shortName}",
|
||||
MenuKeys.ApprovalWorkflowsV2, awV2ContractOrder++, "FileSignature"));
|
||||
}
|
||||
|
||||
var existingItems = await db.MenuItems.ToDictionaryAsync(m => m.Key);
|
||||
var added = 0;
|
||||
var reordered = 0;
|
||||
|
||||
Reference in New Issue
Block a user