[CLAUDE] Infra: SeedDemoContractsAsync — 7 HĐ varied phases để UAT-ready
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m39s

User feedback: làm hết task pending. Phase 1: seed sample HĐ data để admin
login thấy ngay E2E hoạt động full (không cần tự tạo từ đầu).

## Idempotent guard

Skip nếu đã có bất kỳ HĐ nào tên bắt đầu "[DEMO]" — admin có thể xóa
demo + create thật bình thường, restart không clobber.

## 7 demo HĐ — covering 7 ContractTypes + 4-5 phases khác nhau

| # | Type | Final Phase | Tên HĐ | Giá trị | Details |
|---|---|---|---|---|---|
| 1 | ThauPhu | DangSoanThao | Thi công móng + cột tầng 1 — FLOCK 01 | 850M | 3 hạng mục (đào, đổ BT, lắp thép) |
| 2 | GiaoKhoan | DangGopY | Khoán nhân công xây + trát + sơn | 320M | 3 công việc + 2 comments demo |
| 3 | NhaCungCap | DangInKy | Cung cấp xi măng + sắt thép Q2/2026 | 1.2B | 3 SP (xi măng, thép D14/D18) |
| 4 | DichVu | DangTrinhKy | Thuê cẩu tháp 6 tháng | 540M | 1 DV (cẩu tháp Liebherr) |
| 5 | MuaBan | DaPhatHanh | Mua máy phát điện 250kVA | 850M | 2 SP có VAT 10% |
| 6 | NguyenTacNCC | DangGopY | HĐ nguyên tắc cung cấp vật tư 2026 | 0 (framework) | 2 SP với khung giá min/max |
| 7 | NguyenTacDV | DangSoanThao | HĐ nguyên tắc bảo trì TB 2026 | 0 (framework) | 1 DV với SLA |

## Workflow simulation

Loop transition phases tới finalPhase, insert ContractApproval row mỗi
phase với ApproverUserId mapping đúng role:
- DangKiemTraCCM → ccm.tran (CostControl)
- DangTrinhKy + DangDongDau → bod.huynh (Director)
- DaPhatHanh → hra.dang (HrAdmin)
- Khác → qs.hoang (Drafter — soạn thảo + đàm phán + in ký)

SkipCcm policy (DichVu/MuaBan/NguyenTacNcc/NguyenTacDv) bỏ DangKiemTraCCM
khỏi flow.

Mã HĐ auto gen qua codeGen.GenerateAsync (RG-001 format đầy đủ).

## Demo data gồm

- 7 Contracts (Header)
- ~14 Details rows (3+3+3+1+2+2+1)
- ~30 ContractApprovals (workflow history per phase)
- 2 ContractComments (demo conversation CCM↔Drafter)

## Build

dotnet build BE pass (0 error)

## Login test sau deploy

Admin → /my-contracts hoặc /contracts → thấy 7 [DEMO] HĐ với mã RG-001
+ varied phases. Click bất kỳ HĐ → Panel 2 thấy Tổng quan + Chi tiết +
Lịch sử duyệt full content.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-04-23 15:06:34 +07:00
parent 072ad6d014
commit 8bc9565127

View File

@ -44,6 +44,8 @@ public static class DbInitializer
var codeGen = sp.GetRequiredService<IContractCodeGenerator>();
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<User> 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<Contract> 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<User> userManager, ILogger logger)
{