using Microsoft.EntityFrameworkCore; using PowderCoating.Core.Entities; using PowderCoating.Core.Enums; namespace PowderCoating.Infrastructure.Services; public partial class SeedDataService { /// /// Seeds 7 purchase orders across three vendors covering a 3-month window: /// 3 Received (historical), 2 Submitted (in-flight), and 2 Draft (pending approval). /// This gives every PO status a visible example for demo walkthroughs. /// /// /// Vendors are resolved by partial name match against the company's vendor list — the /// same approach used by . PO numbers follow the convention /// PO-YYMM-#### stamped at seed time. /// /// Received POs link back to the that was created after receipt when /// one with a matching vendor invoice number exists; otherwise BillId is left null /// so the PO still seeds cleanly even if bills were skipped. /// /// Idempotency: returns 0 immediately if any purchase orders already exist for the company. /// /// The tenant company to seed purchase orders for. /// Number of PO records inserted, or 0 if already seeded. private async Task SeedPurchaseOrdersAsync(Company company) { var existingCount = await _context.Set() .IgnoreQueryFilters() .CountAsync(p => p.CompanyId == company.Id && !p.IsDeleted); if (existingCount > 0) return 0; var vendors = await _context.Set() .IgnoreQueryFilters() .Where(v => v.CompanyId == company.Id && !v.IsDeleted) .ToListAsync(); if (vendors.Count == 0) return 0; var prismatic = vendors.FirstOrDefault(v => v.CompanyName.Contains("Prismatic")) ?? vendors.First(); var columbia = vendors.FirstOrDefault(v => v.CompanyName.Contains("Columbia")) ?? vendors.First(); var harbor = vendors.FirstOrDefault(v => v.CompanyName.Contains("Harbor")) ?? vendors.First(); var grainger = vendors.FirstOrDefault(v => v.CompanyName.Contains("Grainger")) ?? vendors.First(); var localSupply = vendors.FirstOrDefault(v => v.CompanyName.Contains("Local")) ?? vendors.First(); // Resolve inventory item IDs for PO line items (optional — may be null if inventory // wasn't seeded yet; the PO seeds cleanly either way using the Description field). var glossBlack = await _context.Set().IgnoreQueryFilters() .FirstOrDefaultAsync(i => i.CompanyId == company.Id && i.SKU.EndsWith("-PWD-GBK-001") && !i.IsDeleted); var matteBlack = await _context.Set().IgnoreQueryFilters() .FirstOrDefaultAsync(i => i.CompanyId == company.Id && i.SKU.EndsWith("-PWD-MBK-001") && !i.IsDeleted); var superChrome = await _context.Set().IgnoreQueryFilters() .FirstOrDefaultAsync(i => i.CompanyId == company.Id && i.SKU.EndsWith("-PWD-CHR-001") && !i.IsDeleted); var candyRed = await _context.Set().IgnoreQueryFilters() .FirstOrDefaultAsync(i => i.CompanyId == company.Id && i.SKU.EndsWith("-PWD-CRD-001") && !i.IsDeleted); var blastMedia = await _context.Set().IgnoreQueryFilters() .FirstOrDefaultAsync(i => i.CompanyId == company.Id && i.SKU.EndsWith("-BLM-001") && !i.IsDeleted); var now = DateTime.UtcNow; var pfx = $"PO-{now:yy}{now.Month:D2}-"; var seq = 1; var seeded = 0; async Task AddPO(PurchaseOrder po) { po.PoNumber = $"{pfx}{seq++:D4}"; po.CompanyId = company.Id; foreach (var item in po.Items) { item.CompanyId = company.Id; item.CreatedAt = po.OrderDate; } await _context.Set().AddAsync(po); await _context.SaveChangesAsync(); seeded++; return po; } // ── RECEIVED (historical — tied to bills already in the system) ─────── // PO-1: Prismatic Powders — powder restock 3 months ago (Received, matches bill PP-77211) await AddPO(new PurchaseOrder { VendorId = prismatic.Id, Status = PurchaseOrderStatus.Received, OrderDate = now.AddDays(-95), ExpectedDeliveryDate = now.AddDays(-80), ReceivedDate = now.AddDays(-82), SubTotal = 1_145.00m, TotalAmount = 1_145.00m, Notes = "Quarterly powder restock — Q1", CreatedAt = now.AddDays(-95), Items = { new PurchaseOrderItem { InventoryItemId = matteBlack?.Id, Description = "Matte Black Powder — 50 lb bags", UnitOfMeasure = "bag", QuantityOrdered = 2, QuantityReceived = 2, UnitCost = 178.00m, LineTotal = 356.00m }, new PurchaseOrderItem { InventoryItemId = glossBlack?.Id, Description = "Gloss Black Powder — 50 lb bags", UnitOfMeasure = "bag", QuantityOrdered = 2, QuantityReceived = 2, UnitCost = 165.00m, LineTotal = 330.00m }, new PurchaseOrderItem { Description = "Satin Silver Powder — 25 lb bags", UnitOfMeasure = "bag", QuantityOrdered = 2, QuantityReceived = 2, UnitCost = 144.50m, LineTotal = 289.00m }, new PurchaseOrderItem { Description = "Masking Tape & Plugs Kit", UnitOfMeasure = "kit", QuantityOrdered = 1, QuantityReceived = 1, UnitCost = 170.00m, LineTotal = 170.00m } } }); // PO-2: Columbia Coatings — specialty colors 2 months ago (Received, matches bill CC-4401) await AddPO(new PurchaseOrder { VendorId = columbia.Id, Status = PurchaseOrderStatus.Received, OrderDate = now.AddDays(-70), ExpectedDeliveryDate = now.AddDays(-58), ReceivedDate = now.AddDays(-60), SubTotal = 986.00m, TotalAmount = 986.00m, Notes = "Specialty metallic & candy colors order", CreatedAt = now.AddDays(-70), Items = { new PurchaseOrderItem { InventoryItemId = candyRed?.Id, Description = "Candy Red Metallic — 10 lb bags", UnitOfMeasure = "bag", QuantityOrdered = 3, QuantityReceived = 3, UnitCost = 145.00m, LineTotal = 435.00m }, new PurchaseOrderItem { InventoryItemId = superChrome?.Id, Description = "Chrome Effect Powder — 10 lb bags", UnitOfMeasure = "bag", QuantityOrdered = 2, QuantityReceived = 2, UnitCost = 168.00m, LineTotal = 336.00m }, new PurchaseOrderItem { Description = "Hammertone Bronze — 10 lb bags", UnitOfMeasure = "bag", QuantityOrdered = 1, QuantityReceived = 1, UnitCost = 150.50m, LineTotal = 150.50m }, new PurchaseOrderItem { Description = "Ground Straps & Hooks", UnitOfMeasure = "lot", QuantityOrdered = 1, QuantityReceived = 1, UnitCost = 64.50m, LineTotal = 64.50m } } }); // PO-3: Harbor Freight Tools — consumables 6 weeks ago (Received, matches bill HBF-18822 timing) await AddPO(new PurchaseOrder { VendorId = harbor.Id, Status = PurchaseOrderStatus.Received, OrderDate = now.AddDays(-48), ExpectedDeliveryDate = now.AddDays(-40), ReceivedDate = now.AddDays(-42), SubTotal = 412.50m, TotalAmount = 412.50m, Notes = "Shop consumables & hardware restock", CreatedAt = now.AddDays(-48), Items = { new PurchaseOrderItem { Description = "J-Hook Hangers Assortment", UnitOfMeasure = "pkg", QuantityOrdered = 2, QuantityReceived = 2, UnitCost = 89.75m, LineTotal = 179.50m }, new PurchaseOrderItem { Description = "Masking Caps — Mixed (100-pack)", UnitOfMeasure = "box", QuantityOrdered = 2, QuantityReceived = 2, UnitCost = 60.00m, LineTotal = 120.00m }, new PurchaseOrderItem { Description = "Wire Brushes & Abrasives", UnitOfMeasure = "lot", QuantityOrdered = 1, QuantityReceived = 1, UnitCost = 113.00m, LineTotal = 113.00m } } }); // ── SUBMITTED (in-flight — awaiting delivery) ───────────────────────── // PO-4: Grainger Industrial Supply — safety equipment & filter replacement (matches open GRG-7714 partial bill) await AddPO(new PurchaseOrder { VendorId = grainger.Id, Status = PurchaseOrderStatus.Submitted, OrderDate = now.AddDays(-14), ExpectedDeliveryDate = now.AddDays(3), SubTotal = 648.00m, TotalAmount = 648.00m, Notes = "Blast room filter replacement + safety restocking", CreatedAt = now.AddDays(-14), Items = { new PurchaseOrderItem { Description = "HEPA Filter Cartridges — 12-pack", UnitOfMeasure = "box", QuantityOrdered = 2, QuantityReceived = 0, UnitCost = 189.00m, LineTotal = 378.00m }, new PurchaseOrderItem { Description = "Blast Nozzle Tungsten — 3/8\"", UnitOfMeasure = "ea", QuantityOrdered = 2, QuantityReceived = 0, UnitCost = 85.00m, LineTotal = 170.00m }, new PurchaseOrderItem { Description = "Safety Respirators (10-pack)", UnitOfMeasure = "box", QuantityOrdered = 1, QuantityReceived = 0, UnitCost = 100.00m, LineTotal = 100.00m } } }); // PO-5: Prismatic Powders — current month powder order (matches open bill PP-88530) await AddPO(new PurchaseOrder { VendorId = prismatic.Id, Status = PurchaseOrderStatus.Submitted, OrderDate = now.AddDays(-8), ExpectedDeliveryDate = now.AddDays(7), SubTotal = 1_050.00m, TotalAmount = 1_050.00m, Notes = "June powder restock — Matte Black + seasonal Gloss Red", CreatedAt = now.AddDays(-8), Items = { new PurchaseOrderItem { InventoryItemId = matteBlack?.Id, Description = "Matte Black Powder — 25 lb bags", UnitOfMeasure = "bag", QuantityOrdered = 6, QuantityReceived = 0, UnitCost = 89.00m, LineTotal = 534.00m }, new PurchaseOrderItem { Description = "Gloss Red Powder — 10 lb bags", UnitOfMeasure = "bag", QuantityOrdered = 2, QuantityReceived = 0, UnitCost = 132.00m, LineTotal = 264.00m }, new PurchaseOrderItem { Description = "Hanging Racks (10-pack)", UnitOfMeasure = "pkg", QuantityOrdered = 2, QuantityReceived = 0, UnitCost = 126.00m, LineTotal = 252.00m } } }); // ── DRAFT (pending review / approval) ───────────────────────────────── // PO-6: Harbor Freight Tools — shop tools pending manager approval await AddPO(new PurchaseOrder { VendorId = harbor.Id, Status = PurchaseOrderStatus.Draft, OrderDate = now.AddDays(-3), ExpectedDeliveryDate = now.AddDays(10), SubTotal = 318.75m, TotalAmount = 318.75m, Notes = "Monthly hardware restock — needs approval before submit", InternalNotes = "Manager review requested: higher than normal month due to extra hooks for upcoming Apex Motorsports batch.", CreatedAt = now.AddDays(-3), Items = { new PurchaseOrderItem { Description = "Hanging Racks & J-Hooks", UnitOfMeasure = "pkg", QuantityOrdered = 1, QuantityReceived = 0, UnitCost = 198.75m, LineTotal = 198.75m }, new PurchaseOrderItem { Description = "Masking Caps — Mixed (100-pack)", UnitOfMeasure = "box", QuantityOrdered = 2, QuantityReceived = 0, UnitCost = 60.00m, LineTotal = 120.00m } } }); // PO-7: Local Industrial Supply — blast media restock (inventory currently at zero) await AddPO(new PurchaseOrder { VendorId = localSupply.Id, Status = PurchaseOrderStatus.Draft, OrderDate = now.AddDays(-1), ExpectedDeliveryDate = now.AddDays(5), SubTotal = 385.00m, TotalAmount = 385.00m, Notes = "URGENT — blast media out of stock, production blocked on Pressure Pot Blaster", InternalNotes = "Rush order requested; call LIS rep for same-week delivery.", CreatedAt = now.AddDays(-1), Items = { new PurchaseOrderItem { InventoryItemId = blastMedia?.Id, Description = "Aluminum Oxide #80 Grit — 100 lb bag", UnitOfMeasure = "bag", QuantityOrdered = 5, QuantityReceived = 0, UnitCost = 77.00m, LineTotal = 385.00m } } }); return seeded; } }