[CLAUDE] Infra: Mở rộng seed master (15 NCC + 8 Project) + backfill demo HĐ
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m51s

User feedback: thêm master data NCC + Project + cập nhật HĐ đang thiếu/
trùng nhau.

## SeedDemoMasterDataAsync — per-code idempotent

Trước: skip toàn bộ nếu Suppliers + Projects table đã có row → không add
được seed mới khi expand list.

Sau: per-Code check — add từng item nếu Code chưa tồn tại, skip nếu có.
Cho phép expand seed list theo thời gian mà không clobber data admin
đã add manual.

## 15 demo suppliers (5 SupplierType × 3 entities)

- NhaCungCap (5): VLXD-ABC, Xi măng Hà Tiên, Thép Hòa Phát, Cadivi
  Điện, Tiền Phong Cấp thoát nước
- NhaThauPhu (4): Thăng Long XD, Cô Công Cơ khí, MEP Hà Nội, Next Stage
  Nội thất
- ToDoi (3): Hoàng Nam (cốp pha), Bắc Ninh (xây trát), Hà Trang (ốp lát)
- DonViDichVu (3): Clean Pro, Hồng Phát Vận chuyển, Long An Bảo vệ
- ChuDauTu (3): Vingroup, Sun Group, Masterise Homes

Đầy đủ TaxCode + Phone + Email + ContactPerson + Address.

## 8 demo projects (đa dạng quy mô + giai đoạn)

- FLOCK01 (sẵn) + SKYGARDEN (sẵn) + INDUSTRIAL (sẵn)
- VHOMES-OP (Vinhomes Ocean Park 3, 350B)
- ECOPARK-VL (Ecopark Văn Lâm, 180B)
- BWTOWER (Văn phòng BW Tower 25 tầng, 95B)
- WAREHOUSE-LB (Kho vận Long Biên, 32B)
- RESORT-PQ (Resort 5* Phú Quốc 250 phòng, 220B)

## Backfill demo HĐ supplier + project

Vấn đề trước: 7 demo HĐ tất cả dùng cùng 1 supplier (FirstOrDefault)
+ cùng 1 project → list nhìn không thực tế.

DemoSupplierByType + DemoProjectByType maps mới:
| ContractType | Supplier | Project |
|---|---|---|
| HopDongThauPhu | NTP-XD (thầu phụ) | FLOCK01 |
| HopDongGiaoKhoan | TD-NEHOANG (tổ đội) | FLOCK01 |
| HopDongNhaCungCap | NCC-XIMANG | VHOMES-OP |
| HopDongDichVu | DV-VANCHUYEN | RESORT-PQ |
| HopDongMuaBan | NCC-DIEN | BWTOWER |
| HopDongNguyenTacNCC | NCC-THEP | ECOPARK-VL |
| HopDongNguyenTacDichVu | DV-CLEAN | WAREHOUSE-LB |

SeedDemoContractsAsync 2 path:
1. Lần đầu (no [DEMO] HĐ): tạo mới với supplier+project diverse per type
2. Đã có [DEMO] HĐ: gọi BackfillDemoContractsSupplierProjectAsync —
   loop từng demo HĐ, update supplier_id + project_id nếu mismatch với
   map (idempotent, log count updated)

Match đúng business: HĐ thầu phụ → NTP, HĐ NCC → NCC supplier, HĐ DV →
DV supplier, HĐ chủ đầu tư (BypassCCM) → CDT supplier (admin tự gán).

## Build

dotnet build BE pass (0 error)

## Note deploy

Lần CI/CD deploy `e8XXXXX` lên prod sẽ:
- Add 10 supplier mới + 5 project mới (existing 5+3 giữ nguyên)
- Backfill 7 demo HĐ → switch sang đúng supplier + project per type
- Log: "Seeded 10 demo suppliers", "Seeded 5 demo projects",
  "Backfill [DEMO] contracts supplier+project: 6 updated."
  (6 vì FLOCK01 vẫn dùng cho ThauPhu/GiaoKhoan unchanged)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-04-23 15:54:39 +07:00
parent e53cd3a3b2
commit bcdc00706c

View File

@ -148,6 +148,53 @@ public static class DbInitializer
} }
} }
// Backfill: re-assign supplier + project cho [DEMO] HĐ existing → đa dạng
// theo DemoSupplierByType / DemoProjectByType. Idempotent (chỉ update nếu
// mismatch). Hữu ích khi expand seed list rồi muốn HĐ cũ pick supplier mới.
private static async Task BackfillDemoContractsSupplierProjectAsync(
ApplicationDbContext db,
IReadOnlyDictionary<string, Supplier> suppliersByCode,
IReadOnlyDictionary<string, Project> projectsByCode,
ILogger logger)
{
var demos = await db.Contracts
.Where(c => c.TenHopDong != null && c.TenHopDong!.StartsWith("[DEMO]"))
.ToListAsync();
if (demos.Count == 0) return;
int updated = 0;
foreach (var c in demos)
{
var newSupplierId = c.SupplierId;
var newProjectId = c.ProjectId;
if (DemoSupplierByType.TryGetValue(c.Type, out var sCode)
&& suppliersByCode.TryGetValue(sCode, out var s)
&& c.SupplierId != s.Id)
{
newSupplierId = s.Id;
}
if (DemoProjectByType.TryGetValue(c.Type, out var pCode)
&& projectsByCode.TryGetValue(pCode, out var p)
&& c.ProjectId != p.Id)
{
newProjectId = p.Id;
}
if (newSupplierId != c.SupplierId || newProjectId != c.ProjectId)
{
c.SupplierId = newSupplierId;
c.ProjectId = newProjectId;
updated++;
}
}
if (updated > 0)
{
await db.SaveChangesAsync();
logger.LogInformation("Backfill [DEMO] contracts supplier+project: {Count} updated.", updated);
}
}
private static async Task BackfillContractCodesAsync( private static async Task BackfillContractCodesAsync(
ApplicationDbContext db, IContractCodeGenerator codeGen, ILogger logger) ApplicationDbContext db, IContractCodeGenerator codeGen, ILogger logger)
{ {
@ -270,29 +317,65 @@ public static class DbInitializer
} }
} }
// Map ContractType → preferred supplier code (đa dạng dữ liệu demo theo
// đúng business: ThauPhu/GiaoKhoan đi với NTP/TĐ, NCC/MuaBan/NTNcc đi
// với NCC, DichVu/NTDv đi với DV).
private static readonly Dictionary<ContractType, string> DemoSupplierByType = new()
{
[ContractType.HopDongThauPhu] = "NTP-XD",
[ContractType.HopDongGiaoKhoan] = "TD-NEHOANG",
[ContractType.HopDongNhaCungCap] = "NCC-XIMANG",
[ContractType.HopDongDichVu] = "DV-VANCHUYEN",
[ContractType.HopDongMuaBan] = "NCC-DIEN",
[ContractType.HopDongNguyenTacNCC] = "NCC-THEP",
[ContractType.HopDongNguyenTacDichVu] = "DV-CLEAN",
};
// Map ContractType → preferred project code (đa dạng dự án per HĐ)
private static readonly Dictionary<ContractType, string> DemoProjectByType = new()
{
[ContractType.HopDongThauPhu] = "FLOCK01",
[ContractType.HopDongGiaoKhoan] = "FLOCK01",
[ContractType.HopDongNhaCungCap] = "VHOMES-OP",
[ContractType.HopDongDichVu] = "RESORT-PQ",
[ContractType.HopDongMuaBan] = "BWTOWER",
[ContractType.HopDongNguyenTacNCC] = "ECOPARK-VL",
[ContractType.HopDongNguyenTacDichVu] = "WAREHOUSE-LB",
};
// Seed 7 demo HĐ — 1 per ContractType, varied phases (Đang soạn / Góp ý / // 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 // Đã 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 đã // login thấy ngay sample data hoạt động full E2E.
// có HĐ nào tên bắt đầu "[DEMO]". //
// Idempotent 2 paths:
// 1. Lần đầu (no [DEMO] HĐ): create 7 HĐ varied supplier+project per type
// 2. Đã có [DEMO] HĐ: chạy backfill — update supplier+project nếu mismatched
// với DemoSupplierByType/DemoProjectByType (đa dạng dữ liệu).
private static async Task SeedDemoContractsAsync( private static async Task SeedDemoContractsAsync(
ApplicationDbContext db, UserManager<User> userManager, ApplicationDbContext db, UserManager<User> userManager,
IContractCodeGenerator codeGen, ILogger logger) IContractCodeGenerator codeGen, ILogger logger)
{ {
if (await db.Contracts.AnyAsync(c => c.TenHopDong != null && c.TenHopDong!.StartsWith("[DEMO]"))) // Lookup supplier + project map theo Code (cần cả 2 path)
{ var suppliersByCode = await db.Suppliers.ToDictionaryAsync(s => s.Code, s => s);
logger.LogInformation("SeedDemoContracts: skip — đã có demo HĐ."); var projectsByCode = await db.Projects.ToDictionaryAsync(p => p.Code, p => p);
return; if (suppliersByCode.Count == 0 || projectsByCode.Count == 0)
}
// 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."); logger.LogWarning("SeedDemoContracts: skip — thiếu Supplier/Project.");
return; return;
} }
// Path 2: backfill nếu đã có [DEMO] HĐ
if (await db.Contracts.AnyAsync(c => c.TenHopDong != null && c.TenHopDong!.StartsWith("[DEMO]")))
{
await BackfillDemoContractsSupplierProjectAsync(db, suppliersByCode, projectsByCode, logger);
return;
}
// Path 1: tạo mới (lần đầu)
// Default fallback supplier + project nếu type-specific không có (shouldn't happen với expanded seed)
var fallbackSupplier = suppliersByCode.Values.First();
var fallbackProject = projectsByCode.Values.First();
var qsHoang = await userManager.FindByEmailAsync("qs.hoang@solutionerp.local"); var qsHoang = await userManager.FindByEmailAsync("qs.hoang@solutionerp.local");
var ccmTran = await userManager.FindByEmailAsync("ccm.tran@solutionerp.local"); var ccmTran = await userManager.FindByEmailAsync("ccm.tran@solutionerp.local");
var bodHuynh = await userManager.FindByEmailAsync("bod.huynh@solutionerp.local"); var bodHuynh = await userManager.FindByEmailAsync("bod.huynh@solutionerp.local");
@ -308,6 +391,10 @@ public static class DbInitializer
.Select(w => (Guid?)w.Id) .Select(w => (Guid?)w.Id)
.FirstOrDefaultAsync(); .FirstOrDefaultAsync();
// Pick supplier + project theo type (đa dạng dữ liệu demo)
var supplier = DemoSupplierByType.TryGetValue(type, out var sCode) && suppliersByCode.TryGetValue(sCode, out var s) ? s : fallbackSupplier;
var project = DemoProjectByType.TryGetValue(type, out var pCode) && projectsByCode.TryGetValue(pCode, out var p) ? p : fallbackProject;
var c = new Contract var c = new Contract
{ {
Type = type, Type = type,
@ -798,35 +885,79 @@ public static class DbInitializer
} }
} }
// Sample master data for UAT/demo. Gated by: only seed if tables are empty // Sample master data for UAT/demo — per-code idempotent (skip Code đã có,
// (respects admin's real data once they start adding). // add Code mới). Cho phép admin add/sửa supplier/project mà restart không
// clobber, đồng thời cho phép expand seed list theo thời gian.
private static async Task SeedDemoMasterDataAsync(ApplicationDbContext db, ILogger logger) private static async Task SeedDemoMasterDataAsync(ApplicationDbContext db, ILogger logger)
{
if (await db.Suppliers.AnyAsync() && await db.Projects.AnyAsync()) return;
if (!await db.Suppliers.AnyAsync())
{
db.Suppliers.AddRange(
new Supplier { Code = "NCC-VLXD", Name = "Công ty TNHH Vật liệu Xây dựng ABC", Type = SupplierType.NhaCungCap, TaxCode = "0100000001", Phone = "024 3888 1111", Email = "contact@vlxd-abc.vn", ContactPerson = "Nguyễn Văn An", Address = "123 Láng Hạ, Đống Đa, Hà Nội" },
new Supplier { Code = "NTP-XD", Name = "Công ty CP Xây dựng Thăng Long", Type = SupplierType.NhaThauPhu, TaxCode = "0100000002", Phone = "024 3888 2222", Email = "info@thanglong-xd.vn", ContactPerson = "Trần Thị Bình", Address = "45 Nguyễn Chí Thanh, Đống Đa, Hà Nội" },
new Supplier { Code = "TD-NEHOANG", Name = "Tổ đội Hoàng Nam", Type = SupplierType.ToDoi, Phone = "098 111 2233", ContactPerson = "Phạm Hoàng Nam", Address = "Cầu Giấy, Hà Nội", Note = "Tổ đội cốp pha 15 người" },
new Supplier { Code = "DV-CLEAN", Name = "Công ty TNHH Vệ sinh Công nghiệp Clean Pro", Type = SupplierType.DonViDichVu, TaxCode = "0100000004", Phone = "024 3888 4444", Email = "sales@cleanpro.vn", Address = "Trung Hòa, Cầu Giấy, Hà Nội" },
new Supplier { Code = "CDT-VIN", Name = "Tập đoàn Vingroup", Type = SupplierType.ChuDauTu, TaxCode = "0100109106", Phone = "024 3974 9999", Email = "contact@vingroup.net", Address = "7 Bằng Lăng 1, Vinhomes Riverside, Long Biên, Hà Nội", Note = "Chủ đầu tư - bypass CCM" }
);
logger.LogInformation("Seeded 5 demo suppliers");
}
if (!await db.Projects.AnyAsync())
{ {
var now = DateTime.UtcNow; var now = DateTime.UtcNow;
db.Projects.AddRange(
// 15 NCC covering 5 SupplierType (3 each)
var suppliersToSeed = new[]
{
// NhaCungCap (5) — vật tư xây dựng
new Supplier { Code = "NCC-VLXD", Name = "Công ty TNHH Vật liệu Xây dựng ABC", Type = SupplierType.NhaCungCap, TaxCode = "0100000001", Phone = "024 3888 1111", Email = "contact@vlxd-abc.vn", ContactPerson = "Nguyễn Văn An", Address = "123 Láng Hạ, Đống Đa, Hà Nội" },
new Supplier { Code = "NCC-XIMANG", Name = "Công ty CP Xi măng Hà Tiên", Type = SupplierType.NhaCungCap, TaxCode = "0100000005", Phone = "028 3829 5555", Email = "sales@hatien.vn", ContactPerson = "Lê Minh Tuấn", Address = "82 Lê Lợi, Q1, TP.HCM" },
new Supplier { Code = "NCC-THEP", Name = "Công ty CP Thép Hòa Phát", Type = SupplierType.NhaCungCap, TaxCode = "0100000006", Phone = "024 6266 4666", Email = "sales@hoaphat.com.vn", ContactPerson = "Trần Quốc Đạt", Address = "643 Phạm Văn Đồng, Bắc Từ Liêm, Hà Nội" },
new Supplier { Code = "NCC-DIEN", Name = "Công ty TNHH Thiết bị Điện Cadivi", Type = SupplierType.NhaCungCap, TaxCode = "0100000007", Phone = "028 3925 9889", Email = "info@cadivi.com.vn", ContactPerson = "Hoàng Thị Lan", Address = "70 Nguyễn Tri Phương, Q5, TP.HCM" },
new Supplier { Code = "NCC-NUOC", Name = "Công ty TNHH Vật tư Cấp thoát nước Tiền Phong", Type = SupplierType.NhaCungCap, TaxCode = "0100000008", Phone = "0225 3876 666", Email = "sales@tienphong-pvc.vn", ContactPerson = "Vũ Văn Bình", Address = "KCN Đình Vũ, Hải Phòng" },
// NhaThauPhu (4) — thầu phụ thi công
new Supplier { Code = "NTP-XD", Name = "Công ty CP Xây dựng Thăng Long", Type = SupplierType.NhaThauPhu, TaxCode = "0100000002", Phone = "024 3888 2222", Email = "info@thanglong-xd.vn", ContactPerson = "Trần Thị Bình", Address = "45 Nguyễn Chí Thanh, Đống Đa, Hà Nội" },
new Supplier { Code = "NTP-COCONG", Name = "Công ty CP Xây dựng & Cơ khí Cô Công", Type = SupplierType.NhaThauPhu, TaxCode = "0100000009", Phone = "024 3776 8888", Email = "info@cocong.com.vn", ContactPerson = "Đỗ Mạnh Hùng", Address = "Khu CN Quang Minh, Mê Linh, Hà Nội" },
new Supplier { Code = "NTP-MEPCO", Name = "Công ty TNHH MEP Hà Nội", Type = SupplierType.NhaThauPhu, TaxCode = "0100000010", Phone = "024 3556 9999", Email = "contact@mep-hn.vn", ContactPerson = "Nguyễn Thị Hồng", Address = "Cầu Giấy, Hà Nội", Note = "Chuyên thầu phụ M&E" },
new Supplier { Code = "NTP-NEXTAGE", Name = "Công ty CP Hoàn thiện Nội thất Next Stage", Type = SupplierType.NhaThauPhu, TaxCode = "0100000011", Phone = "024 6688 7777", Email = "sales@nextstage.vn", ContactPerson = "Phạm Văn Khoa", Address = "Hà Đông, Hà Nội" },
// ToDoi (3) — tổ đội nhân công
new Supplier { Code = "TD-NEHOANG", Name = "Tổ đội Hoàng Nam", Type = SupplierType.ToDoi, Phone = "098 111 2233", ContactPerson = "Phạm Hoàng Nam", Address = "Cầu Giấy, Hà Nội", Note = "Tổ đội cốp pha 15 người" },
new Supplier { Code = "TD-XAYTRAT", Name = "Tổ đội Xây trát Bắc Ninh", Type = SupplierType.ToDoi, Phone = "098 444 5566", ContactPerson = "Nguyễn Văn Khánh", Address = "Yên Phong, Bắc Ninh", Note = "Tổ đội xây trát 25 người" },
new Supplier { Code = "TD-OPLAT", Name = "Tổ đội Ốp lát Hà Trang", Type = SupplierType.ToDoi, Phone = "097 222 8899", ContactPerson = "Trần Thị Hà", Address = "Hà Đông, Hà Nội", Note = "Tổ đội ốp lát + sơn bả 12 người" },
// DonViDichVu (3) — dịch vụ
new Supplier { Code = "DV-CLEAN", Name = "Công ty TNHH Vệ sinh Công nghiệp Clean Pro", Type = SupplierType.DonViDichVu, TaxCode = "0100000004", Phone = "024 3888 4444", Email = "sales@cleanpro.vn", Address = "Trung Hòa, Cầu Giấy, Hà Nội" },
new Supplier { Code = "DV-VANCHUYEN", Name = "Công ty CP Vận chuyển Hồng Phát", Type = SupplierType.DonViDichVu, TaxCode = "0100000012", Phone = "024 3776 5555", Email = "info@hongphat-vc.vn", ContactPerson = "Lê Văn Dũng", Address = "Long Biên, Hà Nội", Note = "Vận chuyển vật tư + xe cẩu" },
new Supplier { Code = "DV-BAOVE", Name = "Công ty Bảo vệ Long An", Type = SupplierType.DonViDichVu, TaxCode = "0100000013", Phone = "024 3556 6666", Email = "service@longan-bv.vn", Address = "Thanh Xuân, Hà Nội" },
// ChuDauTu (3) — chủ đầu tư (bypass CCM)
new Supplier { Code = "CDT-VIN", Name = "Tập đoàn Vingroup", Type = SupplierType.ChuDauTu, TaxCode = "0100109106", Phone = "024 3974 9999", Email = "contact@vingroup.net", Address = "7 Bằng Lăng 1, Vinhomes Riverside, Long Biên, Hà Nội", Note = "Chủ đầu tư - bypass CCM" },
new Supplier { Code = "CDT-SUNGROUP", Name = "Tập đoàn Sun Group", Type = SupplierType.ChuDauTu, TaxCode = "0100000014", Phone = "024 3939 8888", Email = "contact@sungroup.com.vn", Address = "Hoàn Kiếm, Hà Nội", Note = "Chủ đầu tư - bypass CCM" },
new Supplier { Code = "CDT-MASTERISE", Name = "Masterise Homes", Type = SupplierType.ChuDauTu, TaxCode = "0100000015", Phone = "028 3939 7777", Email = "contact@masterisehomes.com", Address = "Q2, TP.HCM", Note = "Chủ đầu tư - bypass CCM" },
};
var existingSupplierCodes = await db.Suppliers.Select(s => s.Code).ToListAsync();
int addedSuppliers = 0;
foreach (var s in suppliersToSeed)
{
if (existingSupplierCodes.Contains(s.Code)) continue;
db.Suppliers.Add(s);
addedSuppliers++;
}
if (addedSuppliers > 0) logger.LogInformation("Seeded {Count} demo suppliers", addedSuppliers);
// 8 dự án (đa dạng quy mô + giai đoạn)
var projectsToSeed = new[]
{
new Project { Code = "FLOCK01", Name = "Dự án FLOCK 01 — Khu đô thị Mỹ Đình", StartDate = now.AddMonths(-3), EndDate = now.AddMonths(18), BudgetTotal = 120_000_000_000m, Note = "Dự án mẫu — demo" }, new Project { Code = "FLOCK01", Name = "Dự án FLOCK 01 — Khu đô thị Mỹ Đình", StartDate = now.AddMonths(-3), EndDate = now.AddMonths(18), BudgetTotal = 120_000_000_000m, Note = "Dự án mẫu — demo" },
new Project { Code = "SKYGARDEN", Name = "Sky Garden Residence", StartDate = now.AddMonths(-6), EndDate = now.AddMonths(12), BudgetTotal = 85_000_000_000m, Note = "Dự án mẫu — demo" }, new Project { Code = "SKYGARDEN", Name = "Sky Garden Residence", StartDate = now.AddMonths(-6), EndDate = now.AddMonths(12), BudgetTotal = 85_000_000_000m, Note = "Dự án mẫu — demo" },
new Project { Code = "INDUSTRIAL", Name = "Nhà máy Công nghiệp Yên Phong", StartDate = now.AddMonths(-1), EndDate = now.AddMonths(9), BudgetTotal = 45_000_000_000m, Note = "Dự án mẫu — demo" } new Project { Code = "INDUSTRIAL", Name = "Nhà máy Công nghiệp Yên Phong", StartDate = now.AddMonths(-1), EndDate = now.AddMonths(9), BudgetTotal = 45_000_000_000m, Note = "Dự án mẫu — demo" },
); new Project { Code = "VHOMES-OP", Name = "Vinhomes Ocean Park 3 — Khu Sapphire", StartDate = now.AddMonths(-12), EndDate = now.AddMonths(24), BudgetTotal = 350_000_000_000m, Note = "Khu đô thị quy mô lớn" },
logger.LogInformation("Seeded 3 demo projects"); new Project { Code = "ECOPARK-VL", Name = "Ecopark Văn Lâm — Tổ hợp biệt thự", StartDate = now.AddMonths(-4), EndDate = now.AddMonths(20), BudgetTotal = 180_000_000_000m, Note = "Biệt thự + nhà phố" },
} new Project { Code = "BWTOWER", Name = "Tòa nhà Văn phòng BW Tower — Cầu Giấy", StartDate = now.AddMonths(2), EndDate = now.AddMonths(28), BudgetTotal = 95_000_000_000m, Note = "Văn phòng cho thuê 25 tầng" },
new Project { Code = "WAREHOUSE-LB",Name = "Kho vận Long Biên — Phase 1", StartDate = now.AddMonths(-2), EndDate = now.AddMonths(8), BudgetTotal = 32_000_000_000m, Note = "Kho lạnh + kho thường" },
new Project { Code = "RESORT-PQ", Name = "Resort 5* Phú Quốc — Bãi Trường", StartDate = now.AddMonths(-8), EndDate = now.AddMonths(16), BudgetTotal = 220_000_000_000m, Note = "Resort 250 phòng + biệt thự" },
};
var existingProjectCodes = await db.Projects.Select(p => p.Code).ToListAsync();
int addedProjects = 0;
foreach (var p in projectsToSeed)
{
if (existingProjectCodes.Contains(p.Code)) continue;
db.Projects.Add(p);
addedProjects++;
}
if (addedProjects > 0) logger.LogInformation("Seeded {Count} demo projects", addedProjects);
if (addedSuppliers + addedProjects > 0)
await db.SaveChangesAsync(); await db.SaveChangesAsync();
} }