diff --git a/src/Backend/SolutionErp.Infrastructure/Persistence/DbInitializer.cs b/src/Backend/SolutionErp.Infrastructure/Persistence/DbInitializer.cs index 33287b4..90bc7b5 100644 --- a/src/Backend/SolutionErp.Infrastructure/Persistence/DbInitializer.cs +++ b/src/Backend/SolutionErp.Infrastructure/Persistence/DbInitializer.cs @@ -44,6 +44,8 @@ public static class DbInitializer var codeGen = sp.GetRequiredService(); await BackfillContractCodesAsync(db, codeGen, logger); + await SeedDemoContractsAsync(db, userManager, codeGen, logger); + await WarnDefaultAdminPasswordAsync(userManager, logger); } @@ -268,6 +270,244 @@ public static class DbInitializer } } + // Seed 7 demo HĐ — 1 per ContractType, varied phases (Đang soạn / Góp ý / + // Đã in ký / Đã phát hành), với Details + Approvals + Comments để admin + // login thấy ngay sample data hoạt động full E2E. Idempotent: skip nếu đã + // có HĐ nào tên bắt đầu "[DEMO]". + private static async Task SeedDemoContractsAsync( + ApplicationDbContext db, UserManager userManager, + IContractCodeGenerator codeGen, ILogger logger) + { + if (await db.Contracts.AnyAsync(c => c.TenHopDong != null && c.TenHopDong!.StartsWith("[DEMO]"))) + { + logger.LogInformation("SeedDemoContracts: skip — đã có demo HĐ."); + return; + } + + // Lookup các tham chiếu cần + var supplier = await db.Suppliers.FirstOrDefaultAsync(); + var project = await db.Projects.FirstOrDefaultAsync(); + if (supplier is null || project is null) + { + logger.LogWarning("SeedDemoContracts: skip — thiếu Supplier/Project."); + return; + } + + var qsHoang = await userManager.FindByEmailAsync("qs.hoang@solutionerp.local"); + var ccmTran = await userManager.FindByEmailAsync("ccm.tran@solutionerp.local"); + var bodHuynh = await userManager.FindByEmailAsync("bod.huynh@solutionerp.local"); + var hraDang = await userManager.FindByEmailAsync("hra.dang@solutionerp.local"); + var qsDeptId = (await db.Departments.FirstOrDefaultAsync(d => d.Code == "QS"))?.Id; + var nowUtc = DateTime.UtcNow; + + async Task createDemoAsync( + ContractType type, ContractPhase finalPhase, string ten, decimal giaTri, string? noiDung) + { + var activeWfId = await db.WorkflowDefinitions.AsNoTracking() + .Where(w => w.ContractType == type && w.IsActive) + .Select(w => (Guid?)w.Id) + .FirstOrDefaultAsync(); + + var c = new Contract + { + Type = type, + Phase = ContractPhase.DangSoanThao, // Tạo ở phase 2, transition lên sau + SupplierId = supplier.Id, + ProjectId = project.Id, + DepartmentId = qsDeptId, + DrafterUserId = qsHoang?.Id, + GiaTri = giaTri, + TenHopDong = $"[DEMO] {ten}", + NoiDung = noiDung, + BypassProcurementAndCCM = false, + WorkflowDefinitionId = activeWfId, + SlaDeadline = nowUtc.AddDays(7), + }; + c.MaHopDong = await codeGen.GenerateAsync(c, project.Code, supplier.Code); + db.Contracts.Add(c); + await db.SaveChangesAsync(); + + // Mock approvals + Phase advance qua manual write (skip workflow service vì + // service expect ICurrentUser context). Thực tế chỉ cần ContractApproval + // history + cập nhật Phase entity. + ContractPhase[] flow = type is ContractType.HopDongDichVu or ContractType.HopDongMuaBan + or ContractType.HopDongNguyenTacNCC or ContractType.HopDongNguyenTacDichVu + ? [ContractPhase.DangGopY, ContractPhase.DangDamPhan, ContractPhase.DangInKy, + ContractPhase.DangTrinhKy, ContractPhase.DangDongDau, ContractPhase.DaPhatHanh] + : [ContractPhase.DangGopY, ContractPhase.DangDamPhan, ContractPhase.DangInKy, + ContractPhase.DangKiemTraCCM, ContractPhase.DangTrinhKy, + ContractPhase.DangDongDau, ContractPhase.DaPhatHanh]; + + var current = ContractPhase.DangSoanThao; + foreach (var next in flow) + { + if (next > finalPhase) break; + Guid? actorId = next switch + { + ContractPhase.DangKiemTraCCM => ccmTran?.Id, + ContractPhase.DangTrinhKy or ContractPhase.DangDongDau => bodHuynh?.Id, + ContractPhase.DaPhatHanh => hraDang?.Id, + _ => qsHoang?.Id, + }; + db.ContractApprovals.Add(new ContractApproval + { + ContractId = c.Id, + FromPhase = current, + ToPhase = next, + ApproverUserId = actorId, + Decision = ApprovalDecision.Approve, + Comment = next == ContractPhase.DangGopY ? "Demo seed — chuyển góp ý" : null, + ApprovedAt = nowUtc, + }); + current = next; + } + c.Phase = current; + return c; + } + + async Task addThauPhuDetail(Guid cid, int order, string hangMuc, string dvt, decimal kl, decimal dg) + { + db.ThauPhuDetails.Add(new Domain.Contracts.Details.ThauPhuDetail + { + ContractId = cid, Order = order, HangMuc = hangMuc, DonViTinh = dvt, + KhoiLuong = kl, DonGia = dg, ThanhTien = kl * dg, + }); + } + async Task addNccDetail(Guid cid, int order, string ma, string ten, string dvt, decimal sl, decimal dg) + { + db.NhaCungCapDetails.Add(new Domain.Contracts.Details.NhaCungCapDetail + { + ContractId = cid, Order = order, MaSP = ma, TenSP = ten, DonViTinh = dvt, + SoLuong = sl, DonGia = dg, ThanhTien = sl * dg, + }); + } + async Task addMuaBanDetail(Guid cid, int order, string ma, string ten, string dvt, decimal sl, decimal dg, decimal vat) + { + db.MuaBanDetails.Add(new Domain.Contracts.Details.MuaBanDetail + { + ContractId = cid, Order = order, MaSP = ma, TenSP = ten, DonViTinh = dvt, + SoLuong = sl, DonGia = dg, ThueVAT = vat, ThanhTien = sl * dg * (1 + vat / 100m), + }); + } + async Task addDichVuDetail(Guid cid, int order, string ma, string ten, string dvt, decimal tg, decimal dg) + { + db.DichVuDetails.Add(new Domain.Contracts.Details.DichVuDetail + { + ContractId = cid, Order = order, MaDichVu = ma, TenDichVu = ten, DonViTinh = dvt, + ThoiGian = tg, DonGia = dg, ThanhTien = tg * dg, + }); + } + + // 1. HĐ Thầu phụ — DangSoanThao (đang soạn) + var c1 = await createDemoAsync( + ContractType.HopDongThauPhu, ContractPhase.DangSoanThao, + "Thi công móng + cột tầng 1 — FLOCK 01", 850_000_000m, + "Bao gồm đào móng, đổ bê tông móng cột, lắp cốt thép."); + await addThauPhuDetail(c1.Id, 1, "Đào móng công trình", "m3", 120, 250_000); + await addThauPhuDetail(c1.Id, 2, "Đổ bê tông móng cột", "m3", 80, 1_800_000); + await addThauPhuDetail(c1.Id, 3, "Lắp dựng cốt thép", "kg", 5000, 18_000); + + // 2. HĐ Giao khoán — DangGopY (đang góp ý) + var c2 = await createDemoAsync( + ContractType.HopDongGiaoKhoan, ContractPhase.DangGopY, + "Khoán nhân công xây tường + trát + sơn — FLOCK 01", 320_000_000m, + "Phần hoàn thiện tầng 2 + 3."); + db.GiaoKhoanDetails.Add(new Domain.Contracts.Details.GiaoKhoanDetail + { + ContractId = c2.Id, Order = 1, MaCongViec = "XAY-TUONG", TenCongViec = "Xây tường gạch 110", + DonViTinh = "m2", KhoiLuong = 800, DonGia = 180_000, ThanhTien = 144_000_000m, + }); + db.GiaoKhoanDetails.Add(new Domain.Contracts.Details.GiaoKhoanDetail + { + ContractId = c2.Id, Order = 2, MaCongViec = "TRAT-TUONG", TenCongViec = "Trát tường 2 mặt", + DonViTinh = "m2", KhoiLuong = 1600, DonGia = 80_000, ThanhTien = 128_000_000m, + }); + db.GiaoKhoanDetails.Add(new Domain.Contracts.Details.GiaoKhoanDetail + { + ContractId = c2.Id, Order = 3, MaCongViec = "SON-NUOC", TenCongViec = "Sơn nước nội thất", + DonViTinh = "m2", KhoiLuong = 1600, DonGia = 30_000, ThanhTien = 48_000_000m, + }); + + // 3. HĐ Nhà cung cấp — DangInKy (đang in ký) + var c3 = await createDemoAsync( + ContractType.HopDongNhaCungCap, ContractPhase.DangInKy, + "Cung cấp xi măng + sắt thép Q2/2026", 1_200_000_000m, + "Đợt 1: 200 tấn xi măng + 50 tấn thép."); + await addNccDetail(c3.Id, 1, "XM-PCB40", "Xi măng PCB40 50kg", "tan", 200, 1_800_000); + await addNccDetail(c3.Id, 2, "THEP-D14", "Thép cây phi 14", "kg", 30000, 18_500); + await addNccDetail(c3.Id, 3, "THEP-D18", "Thép cây phi 18", "kg", 20000, 18_800); + + // 4. HĐ Dịch vụ — DangTrinhKy (đang trình ký BOD) + var c4 = await createDemoAsync( + ContractType.HopDongDichVu, ContractPhase.DangTrinhKy, + "Thuê cẩu tháp 6 tháng — FLOCK 01", 540_000_000m, + "Cẩu tháp Liebherr — Q2-Q3/2026."); + await addDichVuDetail(c4.Id, 1, "VC-CAN-TRUC", "Cẩu tháp Liebherr 320 EC-H", "thang", 6, 90_000_000); + + // 5. HĐ Mua bán — DaPhatHanh (đã phát hành — full flow) + var c5 = await createDemoAsync( + ContractType.HopDongMuaBan, ContractPhase.DaPhatHanh, + "Mua máy phát điện 250kVA", 850_000_000m, + "1 máy phát điện chính + 1 dự phòng cho công trường."); + await addMuaBanDetail(c5.Id, 1, "MP-250KVA", "Máy phát điện Cummins 250kVA", "cai", 1, 720_000_000, 10); + await addMuaBanDetail(c5.Id, 2, "MP-100KVA", "Máy phát điện dự phòng 100kVA", "cai", 1, 50_000_000, 10); + + // 6. HĐ Nguyên tắc NCC — DangGopY (framework agreement) + var c6 = await createDemoAsync( + ContractType.HopDongNguyenTacNCC, ContractPhase.DangGopY, + "HĐ nguyên tắc cung cấp vật tư xây dựng 2026", 0m, + "Khung giá tham chiếu — đặt mua qua PO theo từng đợt."); + db.NguyenTacNccDetails.Add(new Domain.Contracts.Details.NguyenTacNccDetail + { + ContractId = c6.Id, Order = 1, NhomSP = "Xi măng", + TenSP = "Xi măng PCB40 50kg", DonViTinh = "tan", + DonGiaToiThieu = 1_700_000, DonGiaToiDa = 1_900_000, + DieuKienThanhToan = "Net 30 — chuyển khoản", + }); + db.NguyenTacNccDetails.Add(new Domain.Contracts.Details.NguyenTacNccDetail + { + ContractId = c6.Id, Order = 2, NhomSP = "Sắt thép", + TenSP = "Thép cây phi 10-32", DonViTinh = "kg", + DonGiaToiThieu = 17_500, DonGiaToiDa = 19_500, + DieuKienThanhToan = "Net 30 — chuyển khoản", + }); + + // 7. HĐ Nguyên tắc Dịch vụ — DangSoanThao + var c7 = await createDemoAsync( + ContractType.HopDongNguyenTacDichVu, ContractPhase.DangSoanThao, + "HĐ nguyên tắc bảo trì thiết bị 2026", 0m, + "Khung dịch vụ bảo trì máy móc — gọi theo nhu cầu."); + db.NguyenTacDvDetails.Add(new Domain.Contracts.Details.NguyenTacDvDetail + { + ContractId = c7.Id, Order = 1, LoaiDichVu = "Bảo trì", + TenDichVu = "Bảo trì máy phát điện", DonViTinh = "lan", + DonGiaToiThieu = 5_000_000, DonGiaToiDa = 8_000_000, + SLA = "Phản hồi 24h, hoàn thành 72h", + }); + + // Comments demo cho HĐ #2 (DangGopY) + if (ccmTran is not null) + { + db.ContractComments.Add(new ContractComment + { + ContractId = c2.Id, UserId = ccmTran.Id, Phase = ContractPhase.DangGopY, + Content = "[DEMO] CCM góp ý: Đề nghị bổ sung điều khoản phạt chậm tiến độ + bảo hành 12 tháng.", + }); + } + if (qsHoang is not null) + { + db.ContractComments.Add(new ContractComment + { + ContractId = c2.Id, UserId = qsHoang.Id, Phase = ContractPhase.DangGopY, + Content = "[DEMO] Drafter phản hồi: Đã ghi nhận, sẽ cập nhật bản v2.", + }); + } + + await db.SaveChangesAsync(); + logger.LogInformation("Seed 7 demo contracts: {C1} {C2} {C3} {C4} {C5} {C6} {C7}", + c1.MaHopDong, c2.MaHopDong, c3.MaHopDong, c4.MaHopDong, c5.MaHopDong, c6.MaHopDong, c7.MaHopDong); + } + // Phase 5.1 security: log warning nếu admin vẫn dùng password mặc định sau deploy production. private static async Task WarnDefaultAdminPasswordAsync(UserManager userManager, ILogger logger) {