Demo seed Phase 1: NC identity, spec inventory, revenue targeting

- Customers: 10 NC commercial (Carolina Fabrication, Apex Motorsports, Triangle
  Offroad, Smith Welding, Raleigh Architectural Metals, etc.) + 17 residential,
  all anchored to Raleigh-Durham area for cohesive tutorial identity
- Inventory: 6 spec powders (Gloss Black, Matte Black, Super Chrome, Candy Red,
  Signal White, Illusion Purple) + 5 consumables (Tape, Silicone Plugs, Hooks,
  Acetone, Blast Media); 2 low-stock + 1 out-of-stock for dashboard alerts
- Vendors: updated to spec (Prismatic Powders, Columbia Coatings, Harbor Freight,
  Grainger, Local Industrial Supply)
- Quotes: 35 quotes (was 20) with 5-status distribution; dates span 5-6 months
- Jobs: 50 jobs (was ~32) with per-customer price ranges so Revenue by Customer
  report shows realistic Pareto curve (Carolina Fabrication largest, etc.)
- Remove.cs: fingerprints updated for all 27 new customer emails + 11 new SKUs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-10 22:12:47 -04:00
parent 01f6897d08
commit 1255bc0670
5 changed files with 352 additions and 649 deletions
@@ -742,325 +742,94 @@ public partial class SeedDataService : ISeedDataService
}
/// <summary>
/// Seeds ten representative inventory items (eight powder colours, one cleaner, one
/// masking tape roll) for the company, linking each to the appropriate category lookup.
/// Seeds 11 inventory items (6 powder colours + 5 consumables) for the company.
/// Two powders are intentionally below reorder point (low-stock alert) and one
/// consumable is at zero (out-of-stock), matching the demo company spec.
/// </summary>
/// <remarks>
/// Returns a tuple rather than a plain int because each item is saved individually
/// (one <c>SaveChangesAsync</c> call per item) so that a duplicate-SKU error on one
/// item does not roll back the entire batch. Failed items are captured as per-item
/// warning strings rather than aborting the seeder.
/// Powders are the six colours featured in the demo company's jobs and quotes:
/// Gloss Black, Matte Black, Super Chrome (low), Candy Red (low), Signal White,
/// Illusion Purple. Consumables are the five shop supplies shown in tutorials:
/// Masking Tape, Silicone Plugs (out-of-stock), Hanging Hooks, Acetone, Blast Media.
///
/// SKUs are prefixed with the company's <see cref="Company.CompanyCode"/> to guarantee
/// uniqueness across tenants in a shared database e.g., <c>DEMO-PWD-BLK-001</c>.
/// A missing or empty CompanyCode throws <see cref="InvalidOperationException"/> because
/// SKU collisions would violate the unique index on the InventoryItems table.
///
/// Category IDs are resolved by <c>CategoryCode</c> (e.g., "POWDER", "CLEANER") rather
/// than hard-coded IDs because lookup IDs differ per company and per environment.
///
/// All powder items default to <c>CoverageSqFtPerLb = 30</c> and
/// <c>TransferEfficiency = 65</c>, which are industry-standard starting values used by
/// the pricing engine when calculating powder needed per coat.
/// SKUs are prefixed with <see cref="Company.CompanyCode"/> to guarantee uniqueness
/// across tenants in a shared database (e.g., DEMO-PWD-GBK-001).
/// </remarks>
/// <param name="company">The tenant company to seed inventory for.</param>
/// <returns>
/// A tuple of (count of items successfully inserted, list of per-item warning messages
/// for skipped or failed items).
/// </returns>
private async Task<(int seededCount, List<string> warnings)> SeedInventoryItemsAsync(Company company)
{
var warnings = new List<string>();
int seededCount = 0;
// Validate company code
if (string.IsNullOrWhiteSpace(company.CompanyCode))
{
throw new InvalidOperationException($"Company {company.CompanyName} (ID: {company.Id}) has no CompanyCode. Cannot seed inventory with unique SKUs.");
}
throw new InvalidOperationException($"Company {company.CompanyName} (ID: {company.Id}) has no CompanyCode.");
var skuPrefix = company.CompanyCode;
// Get category lookups to link items properly
var categories = await _context.InventoryCategoryLookups
.IgnoreQueryFilters()
.Where(c => c.CompanyId == company.Id && !c.IsDeleted)
.ToListAsync();
var powderCategory = categories.FirstOrDefault(c => c.CategoryCode == "POWDER");
var cleanerCategory = categories.FirstOrDefault(c => c.CategoryCode == "CLEANER");
var maskingCategory = categories.FirstOrDefault(c => c.CategoryCode == "MASKING");
var powderCat = categories.FirstOrDefault(c => c.CategoryCode == "POWDER");
var cleanerCat = categories.FirstOrDefault(c => c.CategoryCode == "CLEANER");
var maskingCat = categories.FirstOrDefault(c => c.CategoryCode == "MASKING");
var abrasiveCat = categories.FirstOrDefault(c => c.CategoryCode == "ABRASIVE");
var consumeCat = categories.FirstOrDefault(c => c.CategoryCode == "CONSUMABLE");
// Use company code prefix to ensure unique SKUs across companies
// ── Helper: powder item ───────────────────────────────────────────────
InventoryItem Pwd(string sku, string name, string color, string ral, string finish,
string mfr, string mfrPn, int qty, int reorder, int reorderQty, decimal cost) =>
new InventoryItem
{
SKU = $"{skuPrefix}-{sku}", Name = name, Description = $"{finish} {color} powder coating",
Category = "Powder", InventoryCategoryId = powderCat?.Id,
ColorName = color, ColorCode = ral, Finish = finish,
Manufacturer = mfr, ManufacturerPartNumber = mfrPn,
QuantityOnHand = qty, UnitOfMeasure = "lbs",
ReorderPoint = reorder, ReorderQuantity = reorderQty,
MinimumStock = reorder / 2, MaximumStock = reorderQty * 4,
UnitCost = cost, AverageCost = cost, LastPurchasePrice = cost,
LastPurchaseDate = DateTime.UtcNow.AddDays(-30),
CoverageSqFtPerLb = 30m, TransferEfficiency = 65m,
IsActive = true, CompanyId = company.Id, CreatedAt = DateTime.UtcNow
};
// ── Helper: supply/consumable item ────────────────────────────────────
InventoryItem Supply(string sku, string name, string desc, string cat,
int? catId, string uom, int qty, int reorder, int reorderQty, decimal cost) =>
new InventoryItem
{
SKU = $"{skuPrefix}-{sku}", Name = name, Description = desc,
Category = cat, InventoryCategoryId = catId,
QuantityOnHand = qty, UnitOfMeasure = uom,
ReorderPoint = reorder, ReorderQuantity = reorderQty,
MinimumStock = reorder / 2, MaximumStock = reorderQty * 4,
UnitCost = cost, AverageCost = cost, LastPurchasePrice = cost,
LastPurchaseDate = DateTime.UtcNow.AddDays(-20),
IsActive = true, CompanyId = company.Id, CreatedAt = DateTime.UtcNow
};
// ── 6 Powders (2 low-stock, 0 out-of-stock) ──────────────────────────
// Super Chrome (40 lbs) and Candy Red (25 lbs) are below reorder point
// so the dashboard low-stock alert card is populated on first load.
var inventoryItems = new List<InventoryItem>
{
new InventoryItem
{
SKU = $"{skuPrefix}-PWD-BLK-001",
Name = "Matte Black Powder",
Description = "High-quality matte black powder coating",
Category = "Powder",
InventoryCategoryId = powderCategory?.Id,
ColorName = "Matte Black",
ColorCode = "RAL 9005",
Finish = "Matte",
Manufacturer = "Tiger Drylac",
ManufacturerPartNumber = "TG-MB-001",
QuantityOnHand = 500,
UnitOfMeasure = "lbs",
ReorderPoint = 100,
ReorderQuantity = 250,
MinimumStock = 50,
MaximumStock = 1000,
UnitCost = 4.50m,
AverageCost = 4.50m,
LastPurchasePrice = 4.50m,
LastPurchaseDate = DateTime.UtcNow.AddDays(-30),
CoverageSqFtPerLb = 30m,
TransferEfficiency = 65m,
IsActive = true,
CompanyId = company.Id,
CreatedAt = DateTime.UtcNow
},
new InventoryItem
{
SKU = $"{skuPrefix}-PWD-WHT-001",
Name = "Gloss White Powder",
Description = "High-gloss white powder coating",
Category = "Powder",
InventoryCategoryId = powderCategory?.Id,
ColorName = "Gloss White",
ColorCode = "RAL 9010",
Finish = "Gloss",
Manufacturer = "Tiger Drylac",
ManufacturerPartNumber = "TG-GW-001",
QuantityOnHand = 400,
UnitOfMeasure = "lbs",
ReorderPoint = 100,
ReorderQuantity = 250,
MinimumStock = 50,
MaximumStock = 1000,
UnitCost = 4.25m,
AverageCost = 4.25m,
LastPurchasePrice = 4.25m,
LastPurchaseDate = DateTime.UtcNow.AddDays(-25),
CoverageSqFtPerLb = 30m,
TransferEfficiency = 65m,
IsActive = true,
CompanyId = company.Id,
CreatedAt = DateTime.UtcNow
},
new InventoryItem
{
SKU = $"{skuPrefix}-PWD-RED-001",
Name = "Gloss Red Powder",
Description = "Vibrant gloss red powder coating",
Category = "Powder",
InventoryCategoryId = powderCategory?.Id,
ColorName = "Traffic Red",
ColorCode = "RAL 3020",
Finish = "Gloss",
Manufacturer = "Tiger Drylac",
ManufacturerPartNumber = "TG-GR-001",
QuantityOnHand = 150,
UnitOfMeasure = "lbs",
ReorderPoint = 50,
ReorderQuantity = 100,
MinimumStock = 25,
MaximumStock = 500,
UnitCost = 5.75m,
AverageCost = 5.75m,
LastPurchasePrice = 5.75m,
LastPurchaseDate = DateTime.UtcNow.AddDays(-20),
CoverageSqFtPerLb = 30m,
TransferEfficiency = 65m,
IsActive = true,
CompanyId = company.Id,
CreatedAt = DateTime.UtcNow
},
new InventoryItem
{
SKU = $"{skuPrefix}-PWD-BLU-001",
Name = "Metallic Blue Powder",
Description = "Metallic blue powder coating with shimmer",
Category = "Powder",
InventoryCategoryId = powderCategory?.Id,
ColorName = "Metallic Blue",
ColorCode = "RAL 5002",
Finish = "Metallic",
Manufacturer = "Axalta",
ManufacturerPartNumber = "AX-MB-001",
QuantityOnHand = 200,
UnitOfMeasure = "lbs",
ReorderPoint = 75,
ReorderQuantity = 150,
MinimumStock = 25,
MaximumStock = 500,
UnitCost = 6.25m,
AverageCost = 6.25m,
LastPurchasePrice = 6.25m,
LastPurchaseDate = DateTime.UtcNow.AddDays(-15),
CoverageSqFtPerLb = 30m,
TransferEfficiency = 65m,
IsActive = true,
CompanyId = company.Id,
CreatedAt = DateTime.UtcNow
},
new InventoryItem
{
SKU = $"{skuPrefix}-PWD-GRY-001",
Name = "Textured Gray Powder",
Description = "Textured gray powder coating for industrial use",
Category = "Powder",
InventoryCategoryId = powderCategory?.Id,
ColorName = "Textured Gray",
ColorCode = "RAL 7037",
Finish = "Textured",
Manufacturer = "Axalta",
ManufacturerPartNumber = "AX-TG-001",
QuantityOnHand = 300,
UnitOfMeasure = "lbs",
ReorderPoint = 75,
ReorderQuantity = 150,
MinimumStock = 50,
MaximumStock = 600,
UnitCost = 5.00m,
AverageCost = 5.00m,
LastPurchasePrice = 5.00m,
LastPurchaseDate = DateTime.UtcNow.AddDays(-10),
CoverageSqFtPerLb = 30m,
TransferEfficiency = 65m,
IsActive = true,
CompanyId = company.Id,
CreatedAt = DateTime.UtcNow
},
new InventoryItem
{
SKU = $"{skuPrefix}-PWD-YEL-001",
Name = "Safety Yellow Powder",
Description = "High-visibility safety yellow powder coating",
Category = "Powder",
InventoryCategoryId = powderCategory?.Id,
ColorName = "Safety Yellow",
ColorCode = "RAL 1003",
Finish = "Gloss",
Manufacturer = "Tiger Drylac",
ManufacturerPartNumber = "TG-SY-001",
QuantityOnHand = 125,
UnitOfMeasure = "lbs",
ReorderPoint = 50,
ReorderQuantity = 100,
MinimumStock = 25,
MaximumStock = 400,
UnitCost = 5.50m,
AverageCost = 5.50m,
LastPurchasePrice = 5.50m,
LastPurchaseDate = DateTime.UtcNow.AddDays(-5),
CoverageSqFtPerLb = 30m,
TransferEfficiency = 65m,
IsActive = true,
CompanyId = company.Id,
CreatedAt = DateTime.UtcNow
},
new InventoryItem
{
SKU = $"{skuPrefix}-PWD-ORG-001",
Name = "Orange Powder",
Description = "Bright orange powder coating",
Category = "Powder",
InventoryCategoryId = powderCategory?.Id,
ColorName = "Pure Orange",
ColorCode = "RAL 2004",
Finish = "Gloss",
Manufacturer = "Axalta",
ManufacturerPartNumber = "AX-PO-001",
QuantityOnHand = 100,
UnitOfMeasure = "lbs",
ReorderPoint = 40,
ReorderQuantity = 80,
MinimumStock = 20,
MaximumStock = 300,
UnitCost = 5.85m,
AverageCost = 5.85m,
LastPurchasePrice = 5.85m,
LastPurchaseDate = DateTime.UtcNow.AddDays(-12),
CoverageSqFtPerLb = 30m,
TransferEfficiency = 65m,
IsActive = true,
CompanyId = company.Id,
CreatedAt = DateTime.UtcNow
},
new InventoryItem
{
SKU = $"{skuPrefix}-PWD-GRN-001",
Name = "Forest Green Powder",
Description = "Deep forest green powder coating",
Category = "Powder",
InventoryCategoryId = powderCategory?.Id,
ColorName = "Forest Green",
ColorCode = "RAL 6009",
Finish = "Matte",
Manufacturer = "Tiger Drylac",
ManufacturerPartNumber = "TG-FG-001",
QuantityOnHand = 175,
UnitOfMeasure = "lbs",
ReorderPoint = 60,
ReorderQuantity = 120,
MinimumStock = 30,
MaximumStock = 400,
UnitCost = 5.25m,
AverageCost = 5.25m,
LastPurchasePrice = 5.25m,
LastPurchaseDate = DateTime.UtcNow.AddDays(-8),
CoverageSqFtPerLb = 30m,
TransferEfficiency = 65m,
IsActive = true,
CompanyId = company.Id,
CreatedAt = DateTime.UtcNow
},
new InventoryItem
{
SKU = $"{skuPrefix}-CLN-001",
Name = "Pre-Treatment Cleaner",
Description = "Industrial degreaser and cleaner",
Category = "Cleaner",
InventoryCategoryId = cleanerCategory?.Id,
QuantityOnHand = 50,
UnitOfMeasure = "gallons",
ReorderPoint = 10,
ReorderQuantity = 25,
MinimumStock = 5,
MaximumStock = 100,
UnitCost = 12.50m,
AverageCost = 12.50m,
LastPurchasePrice = 12.50m,
LastPurchaseDate = DateTime.UtcNow.AddDays(-20),
IsActive = true,
CompanyId = company.Id,
CreatedAt = DateTime.UtcNow
},
new InventoryItem
{
SKU = $"{skuPrefix}-MSK-001",
Name = "High-Temp Masking Tape",
Description = "Heat-resistant masking tape for powder coating",
Category = "Masking",
InventoryCategoryId = maskingCategory?.Id,
QuantityOnHand = 200,
UnitOfMeasure = "rolls",
ReorderPoint = 50,
ReorderQuantity = 100,
MinimumStock = 25,
MaximumStock = 500,
UnitCost = 8.75m,
AverageCost = 8.75m,
LastPurchasePrice = 8.75m,
LastPurchaseDate = DateTime.UtcNow.AddDays(-15),
IsActive = true,
CompanyId = company.Id,
CreatedAt = DateTime.UtcNow
}
Pwd("PWD-GBK-001", "Gloss Black", "Gloss Black", "RAL 9005", "Gloss", "Prismatic Powders", "PP-GBK-001", 300, 80, 200, 4.50m),
Pwd("PWD-MBK-001", "Matte Black", "Matte Black", "RAL 9005", "Matte", "Prismatic Powders", "PP-MBK-001", 500, 100, 250, 4.50m),
Pwd("PWD-CHR-001", "Super Chrome", "Super Chrome", "RAL 9006", "Chrome", "Columbia Coatings", "CC-CHR-001", 40, 100, 150, 8.75m), // LOW STOCK
Pwd("PWD-CRD-001", "Candy Red", "Candy Red", "RAL 3028", "Candy", "Prismatic Powders", "PP-CRD-001", 25, 50, 100, 6.50m), // LOW STOCK
Pwd("PWD-SWH-001", "Signal White", "Signal White", "RAL 9003", "Gloss", "Columbia Coatings", "CC-SWH-001", 400, 80, 200, 4.25m),
Pwd("PWD-IPU-001", "Illusion Purple","Illusion Purple","RAL 4005", "Metallic", "Prismatic Powders", "PP-IPU-001", 150, 60, 120, 7.25m),
// ── 5 Consumables (1 out-of-stock) ───────────────────────────────
// Silicone Plugs at qty=0 so the dashboard shows one out-of-stock item.
Supply("MSK-001", "High-Temp Masking Tape", "2-inch heat-resistant masking tape", "Masking Supplies", maskingCat?.Id, "rolls", 80, 30, 100, 8.75m),
Supply("PLG-001", "Silicone Plugs Assorted", "Assorted silicone masking plugs (bag of 100)", "Masking Supplies", maskingCat?.Id, "bags", 0, 50, 100, 14.50m), // OUT OF STOCK
Supply("HKS-001", "Powder Coating Hooks", "Steel hanging hooks for racking parts", "Consumables", consumeCat?.Id, "count", 200, 50, 200, 0.35m),
Supply("ACT-001", "Acetone Degreaser", "Industrial acetone for pre-coating degreasing", "Cleaner", cleanerCat?.Id, "gallons", 20, 5, 25, 18.00m),
Supply("BLM-001", "Aluminum Oxide Blast Media","120-grit aluminum oxide blasting media", "Abrasive Media", abrasiveCat?.Id, "lbs", 250, 100, 250, 1.85m),
};
// Add inventory items one at a time to handle duplicates gracefully
@@ -1242,11 +1011,11 @@ public partial class SeedDataService : ISeedDataService
var vendors = new List<Vendor>
{
new Vendor { CompanyId = company.Id, CompanyName = "Prismatic Powders", ContactName = "Sales", Email = "sales@prismaticpowders.com", Phone = "800-867-4445", Website = "https://www.prismaticpowders.com", IsActive = true, Country = "USA", CreatedAt = DateTime.UtcNow },
new Vendor { CompanyId = company.Id, CompanyName = "Columbia Coatings", ContactName = "Sales", Email = "info@columbiacoatings.com", Phone = "888-265-8247", Website = "https://www.columbiacoatings.com", IsActive = true, Country = "USA", CreatedAt = DateTime.UtcNow },
new Vendor { CompanyId = company.Id, CompanyName = "Sherwin-Williams Industrial", ContactName = "Account Rep", Email = "industrial@sherwin-williams.com", Phone = "800-524-5979", Website = "https://www.sherwin-williams.com", IsActive = true, Country = "USA", CreatedAt = DateTime.UtcNow },
new Vendor { CompanyId = company.Id, CompanyName = "Ace Hardware Supply", ContactName = "Purchasing", Email = "supply@acehardware.com", Phone = "630-990-6600", Website = "https://www.acehardware.com", IsActive = true, Country = "USA", CreatedAt = DateTime.UtcNow },
new Vendor { CompanyId = company.Id, CompanyName = "Fastenal Industrial", ContactName = "Sales Team", Email = "sales@fastenal.com", Phone = "507-454-5374", Website = "https://www.fastenal.com", IsActive = true, Country = "USA", CreatedAt = DateTime.UtcNow },
new Vendor { CompanyId = company.Id, CompanyName = "Prismatic Powders", ContactName = "Sales", Email = "sales@prismaticpowders.com", Phone = "800-867-4445", Website = "https://www.prismaticpowders.com", IsActive = true, Country = "USA", CreatedAt = DateTime.UtcNow },
new Vendor { CompanyId = company.Id, CompanyName = "Columbia Coatings", ContactName = "Sales", Email = "info@columbiacoatings.com", Phone = "888-265-8247", Website = "https://www.columbiacoatings.com", IsActive = true, Country = "USA", CreatedAt = DateTime.UtcNow },
new Vendor { CompanyId = company.Id, CompanyName = "Harbor Freight Tools", ContactName = "Purchasing", Email = "purchasing@harborfreight.com", Phone = "800-444-3353", Website = "https://www.harborfreight.com", IsActive = true, Country = "USA", CreatedAt = DateTime.UtcNow },
new Vendor { CompanyId = company.Id, CompanyName = "Grainger Industrial Supply",ContactName = "Account Rep", Email = "accounts@grainger.com", Phone = "800-472-4643", Website = "https://www.grainger.com", IsActive = true, Country = "USA", CreatedAt = DateTime.UtcNow },
new Vendor { CompanyId = company.Id, CompanyName = "Local Industrial Supply", ContactName = "Sales Team", Email = "sales@localindustrialsupply.com", Phone = "(919) 555-0100", IsActive = true, Country = "USA", CreatedAt = DateTime.UtcNow },
};
await _context.Set<Vendor>().AddRangeAsync(vendors);