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:
@@ -6,44 +6,32 @@ namespace PowderCoating.Infrastructure.Services;
|
||||
public partial class SeedDataService
|
||||
{
|
||||
/// <summary>
|
||||
/// Seeds 100 realistic customers (60 commercial, 40 individual/non-commercial) for
|
||||
/// the given company, spanning automotive, industrial, architectural, fitness, marine,
|
||||
/// furniture, government, and specialty verticals.
|
||||
/// Seeds 27 realistic customers for the demo company: 10 commercial accounts and
|
||||
/// 17 individual/non-commercial customers, all anchored to the Raleigh-Durham NC area.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Idempotency: returns (0, empty warnings) immediately if any non-deleted customers already
|
||||
/// exist for this company, preventing duplicate customer sets on repeated seed runs.
|
||||
/// The NC geographic identity (Carolina Fabrication, Apex Motorsports, Triangle Offroad,
|
||||
/// Raleigh Architectural Metals, etc.) gives tutorial recordings and marketing screenshots
|
||||
/// a coherent sense of place rather than a scatter of US cities.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Each customer is inserted individually (rather than in a single <c>AddRange</c>) so that
|
||||
/// a duplicate-email collision on any single record is caught and converted to a warning
|
||||
/// rather than aborting the entire batch. The EF entity is detached on failure to prevent
|
||||
/// the DbContext change-tracker from retrying the failed insert on the next
|
||||
/// <c>SaveChangesAsync</c> call.
|
||||
/// Revenue is deliberately top-heavy: Carolina Fabrication (Platinum) and Apex Motorsports
|
||||
/// (Gold) carry the majority of simulated revenue so the Revenue by Customer report shows
|
||||
/// a realistic Pareto distribution from day one.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Pricing tiers (Standard, Silver, Gold, Platinum) are resolved by name from the company's
|
||||
/// already-seeded tiers. If a tier is missing the customer still inserts with no tier,
|
||||
/// rather than throwing.
|
||||
/// Wake County Fleet Services is seeded as tax-exempt to demonstrate the tax-exempt
|
||||
/// workflow (0% tax on quotes and invoices, ★ marker in dropdowns).
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Government/municipal customers (<c>Metro Transit Authority</c>, <c>Municipal Services Group</c>,
|
||||
/// <c>Regional Airport Authority</c>, <c>County School District</c>) are seeded with
|
||||
/// <c>IsTaxExempt = true</c> to demonstrate the tax-exempt workflow, matching the
|
||||
/// production rule that tax-exempt customers get 0 % tax on quotes and invoices.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The two local helper functions <c>Comm()</c> and <c>Indiv()</c> reduce the per-row
|
||||
/// line count; they are defined as local functions rather than private methods because
|
||||
/// they capture the <c>company</c> parameter by closure and are only needed here.
|
||||
/// Each customer is inserted individually so a duplicate-email collision on any single
|
||||
/// record is converted to a warning rather than aborting the entire batch.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <param name="company">The tenant company to seed customers for.</param>
|
||||
/// <returns>
|
||||
/// A tuple of (<c>seededCount</c>, <c>warnings</c>) where <c>seededCount</c> is the number
|
||||
/// of records actually inserted and <c>warnings</c> lists any customers that were skipped
|
||||
/// (e.g. because the email already existed).
|
||||
/// A tuple of (seededCount, warnings) where warnings list any skipped customers.
|
||||
/// </returns>
|
||||
private async Task<(int seededCount, List<string> warnings)> SeedCustomersAsync(Company company)
|
||||
{
|
||||
@@ -52,7 +40,6 @@ public partial class SeedDataService
|
||||
int skippedCount = 0;
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// Early exit — same pattern as all other seeders
|
||||
var existingCount = await _context.Set<Customer>()
|
||||
.IgnoreQueryFilters()
|
||||
.CountAsync(c => c.CompanyId == company.Id && !c.IsDeleted);
|
||||
@@ -70,11 +57,7 @@ public partial class SeedDataService
|
||||
var goldTier = tiers.FirstOrDefault(t => t.TierName == "Gold");
|
||||
var platinumTier = tiers.FirstOrDefault(t => t.TierName == "Platinum");
|
||||
|
||||
// ── Local helpers keep each customer to 2-3 lines ──────────────────────
|
||||
//
|
||||
// Comm() builds a commercial (B2B) Customer with credit limit, tax ID, and pricing tier.
|
||||
// The LastContactDate formula scrambles the months value so that contacts are spread
|
||||
// across the past 25 days rather than clustering on the same date for all customers.
|
||||
// ── Local helpers ──────────────────────────────────────────────────────
|
||||
Customer Comm(string co, string fn, string ln, string em, string ph,
|
||||
string city, string st, string zip, string terms, decimal credit, decimal bal,
|
||||
string tax, PricingTier? tier, string notes, int months, bool taxExempt = false) =>
|
||||
@@ -89,8 +72,6 @@ public partial class SeedDataService
|
||||
GeneralNotes = notes, CompanyId = company.Id, CreatedAt = now.AddMonths(-months)
|
||||
};
|
||||
|
||||
// Indiv() builds a non-commercial (retail) Customer with simpler fields:
|
||||
// no credit limit, no tax ID, payment terms default to "Due on receipt".
|
||||
Customer Indiv(string fn, string ln, string em, string ph,
|
||||
string city, string st, string zip, string notes, int months) =>
|
||||
new Customer
|
||||
@@ -104,50 +85,45 @@ public partial class SeedDataService
|
||||
|
||||
var customers = new List<Customer>
|
||||
{
|
||||
// ─── Commercial Customers (15) ────────────────────────────────────
|
||||
// ─── Commercial Customers (10) — NC / Triangle Area ────────────────
|
||||
|
||||
// Auto & Motorsports
|
||||
Comm("Acme Manufacturing Corp", "John", "Smith", "john.smith@acmemfg.com", "(555) 234-5678", "Chicago", "IL", "60601", "Net 30", 50000m, 12500m, "12-3456789", platinumTier, "Large volume customer, weekly shipments", 18),
|
||||
Comm("Precision Auto Parts LLC", "Sarah", "Johnson", "sjohnson@precisionauto.com", "(555) 345-6789", "Detroit", "MI", "48201", "Net 30", 35000m, 8750m, "23-4567890", goldTier, "Automotive parts manufacturer", 15),
|
||||
Comm("Classic Wheel Restoration", "Robert", "Taylor", "rtaylor@classicwheels.com", "(555) 789-0123", "Phoenix", "AZ", "85001", "Net 15", 15000m, 3200m, "67-8901234", silverTier, "Classic car wheel specialist", 10),
|
||||
Comm("MotorSports Custom Shop", "Chris", "Brown", "cbrown@motorsportscustom.com","(555) 901-2345", "Indianapolis", "IN", "46201", "Net 15", 20000m, 9500m, "89-0123456", silverTier, "Performance parts and custom fabrication", 8),
|
||||
// Top-revenue accounts — drive the Revenue by Customer report story
|
||||
Comm("Carolina Fabrication", "Matt", "Henderson", "matt@carolinafab.com", "(919) 234-5678", "Raleigh", "NC", "27601", "Net 30", 50000m, 3200m, "56-7890123", platinumTier, "Largest account; structural steel and custom fab runs weekly", 18),
|
||||
Comm("Apex Motorsports", "Chris", "Tanner", "ctanner@apexmotorsports.com", "(919) 345-6789", "Apex", "NC", "27502", "Net 30", 35000m, 2100m, "23-4567890", goldTier, "Race parts, chassis work, performance components", 15),
|
||||
Comm("Triangle Offroad", "Jason", "Pruitt", "jpruitt@triangleoffroad.com", "(919) 456-7890", "Durham", "NC", "27701", "Net 15", 30000m, 1800m, "34-5678901", goldTier, "Jeep and truck accessory coatings; skid plates, bumpers", 12),
|
||||
Comm("Smith Welding & Steel", "Bill", "Smith", "bsmith@smithwelding.com", "(919) 567-8901", "Garner", "NC", "27529", "Net 30", 20000m, 950m, "45-6789012", silverTier, "Custom steel fab, gates, railings; repeat orders monthly", 10),
|
||||
|
||||
// Industrial & Manufacturing
|
||||
Comm("Industrial Furniture Co", "Jennifer", "Anderson", "janderson@indfurniture.com", "(555) 890-1234", "Seattle", "WA", "98101", "Net 30", 30000m, 7800m, "78-9012345", goldTier, "Office and outdoor furniture manufacturer", 16),
|
||||
Comm("Commercial HVAC Systems", "Kevin", "Garcia", "kgarcia@commercialhvac.com", "(555) 345-6780", "Atlanta", "GA", "30301", "Net 30", 32000m, 8900m, "23-4567891", goldTier, "HVAC ductwork and equipment casings", 17),
|
||||
Comm("Agricultural Equipment Inc", "Sandra", "White", "swhite@agequipment.com", "(555) 678-9013", "Des Moines", "IA", "50301", "Net 30", 42000m, 16800m, "56-7890124", goldTier, "Farm equipment parts and implements", 19),
|
||||
// Mid-tier commercial
|
||||
Comm("Raleigh Architectural Metals", "Karen", "Morales", "kmorales@raleigharchitectural.com", "(919) 678-9012", "Raleigh", "NC", "27604", "Net 30", 25000m, 1400m, "67-8901234", goldTier, "Decorative ironwork, balcony railings, entry gates", 13),
|
||||
Comm("East Coast Powderworks", "Tony", "Greco", "tgreco@eastcoastpw.com", "(252) 789-0123", "Greenville", "NC", "27858", "Net 15", 15000m, 620m, "78-9012345", silverTier, "Metal fab and fabrication outsourcing", 9),
|
||||
Comm("Piedmont Metal Works", "Derek", "Shaw", "dshaw@piedmontmetalworks.com", "(336) 890-1234", "Greensboro", "NC", "27401", "Net 30", 10000m, 380m, "89-0123456", standardTier, "Heavy industrial parts, machine guards, conveyor frames", 7),
|
||||
Comm("Cary Industrial Solutions", "Linda", "Patel", "lpatel@caryindustrial.com", "(919) 901-2345", "Cary", "NC", "27511", "Net 30", 18000m, 870m, "90-1234567", silverTier, "Equipment casings, pump housings, control panels", 8),
|
||||
|
||||
// Architectural & Construction
|
||||
Comm("Urban Railings & Gates", "Michael", "Chen", "mchen@urbanrailings.com", "(555) 456-7890", "San Francisco", "CA", "94102", "Net 15", 25000m, 5200m, "34-5678901", silverTier, "Ornamental iron railings and gates", 12),
|
||||
Comm("Heritage Architectural Metalworks","Thomas", "Miller", "tmiller@heritagemetal.com", "(555) 123-4567", "Charleston", "SC", "29401", "Net 30", 28000m, 6700m, "01-2345678", goldTier, "Historic restoration and custom architectural pieces",13),
|
||||
// Specialty accounts
|
||||
Comm("Durham Tech Equipment", "Ryan", "Blake", "rblake@durhamtech.com", "(919) 012-3456", "Durham", "NC", "27703", "Net 30", 28000m, 1150m, "01-2345678", goldTier, "Lab and research equipment frames and enclosures", 11),
|
||||
Comm("Wake County Fleet Services", "Michelle", "Coleman", "mcoleman@wakecountyfleet.gov", "(919) 123-4560", "Raleigh", "NC", "27602", "Net 60", 75000m, 2800m, "56-7890124", platinumTier, "Government fleet contract — tax exempt; trailers, truck beds", 20, true),
|
||||
|
||||
// Fitness & Marine
|
||||
Comm("Fitness Equipment Solutions", "Lisa", "Martinez", "lmartinez@fitequip.com", "(555) 567-8901", "Austin", "TX", "78701", "Net 30", 40000m, 15600m, "45-6789012", goldTier, "Gym equipment frames and accessories", 14),
|
||||
Comm("Playground Equipment USA", "Nancy", "Martinez", "nmartinez@playgroundusa.com", "(555) 456-7891", "Portland", "OR", "97201", "Net 30", 38000m, 14500m, "34-5678902", platinumTier, "Commercial playground equipment manufacturer", 22),
|
||||
Comm("Marine Equipment Corp", "Patricia", "Wilson", "pwilson@marineequip.com", "(555) 234-5679", "Miami", "FL", "33101", "Net 30", 35000m, 11400m, "12-3456780", silverTier, "Boat hardware and marine fittings", 11),
|
||||
// ─── Individual / Non-Commercial Customers (17) ──────────────────
|
||||
|
||||
// Commercial & Energy/Government
|
||||
Comm("Office Systems International", "Brian", "Lee", "blee@officesystems.com", "(555) 567-8902", "Dallas", "TX", "75201", "Net 15", 27000m, 5600m, "45-6789013", silverTier, "Office furniture components and accessories", 9),
|
||||
Comm("Metro Transit Authority", "David", "Williams", "dwilliams@metrota.gov", "(555) 678-9012", "Boston", "MA", "02101", "Net 60", 75000m, 22000m, "56-7890123", platinumTier, "Government transit contract — tax exempt", 24, true),
|
||||
Comm("Green Energy Solutions", "Amanda", "Davis", "adavis@greenenergy.com", "(555) 012-3456", "Denver", "CO", "80201", "Net 30", 45000m, 18200m, "90-1234567", platinumTier, "Solar panel frames and mounting hardware", 20),
|
||||
|
||||
// ─── Individual / Non-Commercial Customers (12) ───────────────────
|
||||
|
||||
Indiv("James", "Thompson", "jthompson@email.com", "(555) 111-2222", "Los Angeles", "CA", "90001", "Classic car restoration hobbyist", 6),
|
||||
Indiv("Mary", "Harris", "mharris@email.com", "(555) 222-3333", "Houston", "TX", "77001", "Patio furniture refurbishment", 4),
|
||||
Indiv("William", "Clark", "wclark@email.com", "(555) 333-4444", "Philadelphia", "PA", "19101", "Motorcycle customization", 7),
|
||||
Indiv("Elizabeth","Lewis", "elewis@email.com", "(555) 444-5555", "Phoenix", "AZ", "85001", "Garden furniture restoration", 3),
|
||||
Indiv("Richard", "Walker", "rwalker@email.com", "(555) 555-6666", "San Antonio", "TX", "78201", "Custom bike parts", 5),
|
||||
Indiv("Barbara", "Hall", "bhall@email.com", "(555) 666-7777", "San Diego", "CA", "92101", "Antique furniture hardware", 2),
|
||||
Indiv("Joseph", "Allen", "jallen@email.com", "(555) 777-8888", "Dallas", "TX", "75201", "Hot rod restoration", 8),
|
||||
Indiv("Susan", "Young", "syoung@email.com", "(555) 888-9999", "San Jose", "CA", "95101", "Home decor projects", 1),
|
||||
Indiv("Charles", "King", "cking@email.com", "(555) 999-0000", "Austin", "TX", "78701", "Vintage car parts", 5),
|
||||
Indiv("Linda", "Wright", "lwright@email.com", "(555) 000-1111", "Jacksonville", "FL", "32201", "Outdoor metalwork restoration", 3),
|
||||
Indiv("Gary", "Nelson", "gnelson@email.com", "(555) 131-4141", "Minneapolis", "MN", "55401", "Snowmobile frame and parts", 2),
|
||||
Indiv("Carol", "Evans", "carol.evans@email.com", "(555) 242-5252", "Portland", "OR", "97207", "Vintage bicycle restoration", 1),
|
||||
Indiv("John", "Davis", "jdavis@email.com", "(919) 111-2222", "Raleigh", "NC", "27609", "Classic car restoration hobbyist; Ford Mustangs", 6),
|
||||
Indiv("Sarah", "Jenkins", "sjenkins@email.com", "(919) 222-3333", "Durham", "NC", "27707", "Motorcycle customization; Harley-Davidson parts", 4),
|
||||
Indiv("Mike", "Thompson", "mthompson@email.com", "(919) 333-4444", "Apex", "NC", "27502", "Jeep Wrangler build, wheels and bumpers", 7),
|
||||
Indiv("Robert", "Miller", "rmiller@email.com", "(919) 444-5555", "Cary", "NC", "27513", "Patio furniture set, railings", 3),
|
||||
Indiv("Jennifer", "Clark", "jclark@email.com", "(919) 555-6666", "Chapel Hill", "NC", "27514", "Bicycle frame restoration; vintage road bikes", 5),
|
||||
Indiv("David", "Wilson", "dwilson@email.com", "(919) 666-7777", "Wake Forest", "NC", "27587", "1969 Camaro restoration project, multiple phases", 8),
|
||||
Indiv("Lisa", "Anderson", "landerson@email.com", "(919) 777-8888", "Morrisville", "NC", "27560", "Home decor metal pieces, garden art", 2),
|
||||
Indiv("Thomas", "Harris", "tharris@email.com", "(252) 888-9999", "New Bern", "NC", "28560", "Boat trailer hardware, dock cleats", 5),
|
||||
Indiv("Karen", "White", "kwhite@email.com", "(919) 999-0000", "Fuquay-Varina","NC","27526", "Antique fireplace grate and hardware restoration", 3),
|
||||
Indiv("James", "Taylor", "jtaylor@email.com", "(919) 000-1111", "Garner", "NC", "27529", "1955 Ford F100 hot rod build", 6),
|
||||
Indiv("Michelle", "Brown", "mbrown@email.com", "(919) 131-4141", "Holly Springs","NC","27540", "Outdoor furniture set, 6 chairs and table", 2),
|
||||
Indiv("Chris", "Lee", "clee@email.com", "(984) 242-5252", "Raleigh", "NC", "27610", "Custom BMX frame — Candy Red", 3),
|
||||
Indiv("Amanda", "Garcia", "agarcia@email.com", "(919) 353-6363", "Clayton", "NC", "27520", "Motorcycle frame and forks — Flat Black", 4),
|
||||
Indiv("Kevin", "Martinez", "kmartinez@email.com", "(919) 464-7474", "Wendell", "NC", "27591", "Snowmobile frame and tunnel", 2),
|
||||
Indiv("Nancy", "Rodriguez", "nrodriguez@email.com", "(919) 575-8585", "Knightdale", "NC", "27545", "Wrought iron garden trellis and gate", 1),
|
||||
Indiv("Brian", "Hall", "bhall@email.com", "(919) 686-9696", "Zebulon", "NC", "27597", "Utility trailer frame and hitch assembly", 3),
|
||||
Indiv("Patricia", "Young", "pyoung@email.com", "(919) 797-0707", "Louisburg", "NC", "27549", "Front porch railings — Gloss Black", 2),
|
||||
};
|
||||
|
||||
// Add customers one at a time to handle duplicates gracefully
|
||||
foreach (var customer in customers)
|
||||
{
|
||||
try
|
||||
@@ -161,7 +137,7 @@ public partial class SeedDataService
|
||||
{
|
||||
skippedCount++;
|
||||
var name = customer.CompanyName ?? $"{customer.ContactFirstName} {customer.ContactLastName}";
|
||||
warnings.Add($"⊘ Skipped: {name} — email {customer.Email} already exists");
|
||||
warnings.Add($"Skipped: {name} — email {customer.Email} already exists");
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -173,7 +149,7 @@ public partial class SeedDataService
|
||||
{
|
||||
skippedCount++;
|
||||
var name = customer.CompanyName ?? $"{customer.ContactFirstName} {customer.ContactLastName}";
|
||||
warnings.Add($"⊘ Skipped: {name} — {GetFriendlyErrorMessage(ex, "customer")}");
|
||||
warnings.Add($"Skipped: {name} — {GetFriendlyErrorMessage(ex, "customer")}");
|
||||
if (_context.Entry(customer).State != EntityState.Detached)
|
||||
_context.Entry(customer).State = EntityState.Detached;
|
||||
}
|
||||
|
||||
@@ -7,41 +7,31 @@ namespace PowderCoating.Infrastructure.Services;
|
||||
public partial class SeedDataService
|
||||
{
|
||||
/// <summary>
|
||||
/// Seeds 50 powder coating jobs that collectively demonstrate all 16 job statuses,
|
||||
/// realistic date progressions, varied priorities, and quote linkage for the first 25 jobs.
|
||||
/// Seeds 50 powder coating jobs distributed across all 16 statuses, with deliberate
|
||||
/// per-customer revenue targeting so the Revenue by Customer report matches the demo
|
||||
/// company narrative: Carolina Fabrication largest, then Apex Motorsports, Triangle Offroad,
|
||||
/// Smith Welding, and so on down to small individual residential jobs.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Idempotency: returns 0 immediately if any non-deleted jobs already exist for this company.
|
||||
/// Job counts and price ranges are defined per customer index (0 = Carolina Fabrication,
|
||||
/// the top-revenue account) via <c>CustomerProfile(ci)</c>. This replaces the previous
|
||||
/// modulo-formula approach that produced uniform pricing regardless of customer tier.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The method depends on job-status and job-priority lookup rows (populated earlier in the
|
||||
/// seed sequence), and on at least one customer record. It returns 0 if any of these
|
||||
/// dependencies are missing so the overall seed degrades gracefully.
|
||||
/// All 16 statuses are covered: the 16 statuses cycle through the first 16 jobs in
|
||||
/// sequence, guaranteeing every pipeline stage is populated even with only 50 total jobs.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Job numbers follow the production format <c>JOB-YYMM-####</c>. The seeder scans
|
||||
/// existing numbers with the current month prefix and starts its sequence above the current
|
||||
/// maximum so demo jobs never collide with real jobs created in the same calendar month.
|
||||
/// Date logic groups jobs into three buckets — completed (60–150 days ago), in-progress
|
||||
/// (10–50 days ago), and early-stage (2–17 days ago or future-scheduled) — to produce
|
||||
/// realistic dashboard pipeline and calendar views.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The first 25 jobs are linked to approved quotes (loaded from the previously seeded
|
||||
/// quotes). When a match is found the job inherits the quote's customer, description,
|
||||
/// quoted price, and customer PO — matching the production quote-to-job conversion path.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Date logic groups jobs into three buckets: early-stage (future scheduled date),
|
||||
/// in-progress (past start date, no completion), and completed/terminal (both started
|
||||
/// and completed dates in the past). This ensures the dashboard pipeline and calendar
|
||||
/// views display a realistic spread rather than all jobs sharing the same date.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The <c>IgnoreQueryFilters()</c> call on the existence check ensures that soft-deleted
|
||||
/// leftover jobs from a previous seed run are detected and do not cause duplicate inserts.
|
||||
/// The first approved quote available for each customer is linked to their first job
|
||||
/// when a match exists, demonstrating the quote-to-job conversion workflow.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <param name="company">The tenant company to seed jobs for.</param>
|
||||
/// <returns>Number of jobs inserted, or 0 if already seeded or dependencies are missing.</returns>
|
||||
private async Task<int> SeedJobsAsync(Company company)
|
||||
{
|
||||
var existingCount = await _context.Set<Job>()
|
||||
@@ -73,11 +63,11 @@ public partial class SeedDataService
|
||||
if (customers.Count == 0)
|
||||
return 0;
|
||||
|
||||
// Grab approved quotes to link to jobs
|
||||
var approvedQuotes = await _context.Set<Quote>()
|
||||
.IgnoreQueryFilters()
|
||||
.Where(q => q.CompanyId == company.Id && q.QuoteStatus.StatusCode == "APPROVED")
|
||||
.OrderBy(q => q.Id)
|
||||
.OrderBy(q => q.CustomerId)
|
||||
.ThenBy(q => q.Id)
|
||||
.ToListAsync();
|
||||
|
||||
var shopUsers = await _context.Set<ApplicationUser>()
|
||||
@@ -86,7 +76,6 @@ public partial class SeedDataService
|
||||
.ToListAsync();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
var prefix = $"JOB-{now:yy}{now.Month:D2}-";
|
||||
var existing = await _context.Set<Job>()
|
||||
.IgnoreQueryFilters()
|
||||
@@ -98,14 +87,44 @@ public partial class SeedDataService
|
||||
if (n.Length >= 13 && int.TryParse(n.Substring(9, 4), out var x) && x > maxNum) maxNum = x;
|
||||
var seq = maxNum + 1;
|
||||
|
||||
// ── Per-customer job counts (27 customers, ~32 total jobs) ──────────
|
||||
// Varied 0-5 jobs per customer; the global jobIdx cycles all 16 statuses
|
||||
// so every status is visible without requiring a large fixed pool.
|
||||
static int JobsFor(int ci) => new[]
|
||||
{ 3, 2, 1, 2, 0, 2, 1, 3, 0, 1, 2, 1, 0, 2, 1, 1, 2, 0, 1, 0, 2, 1, 0, 1, 0, 1, 2 }[ci];
|
||||
// ── Per-customer profile: (jobCount, minJobValue, maxJobValue) ─────────
|
||||
// Indices match the customer order seeded in SeedCustomersAsync:
|
||||
// 0=Carolina Fabrication, 1=Apex Motorsports, 2=Triangle Offroad,
|
||||
// 3=Smith Welding, 4=Raleigh Architectural, 5=East Coast Powderworks,
|
||||
// 6=Piedmont Metal Works, 7=Cary Industrial, 8=Durham Tech, 9=Wake County Fleet,
|
||||
// 10–26 = individual residential customers (smaller jobs)
|
||||
static (int count, decimal minVal, decimal maxVal) CustomerProfile(int ci) => ci switch
|
||||
{
|
||||
0 => (7, 800m, 2500m), // Carolina Fabrication — largest account
|
||||
1 => (6, 400m, 1500m), // Apex Motorsports
|
||||
2 => (5, 350m, 1200m), // Triangle Offroad
|
||||
3 => (4, 250m, 800m), // Smith Welding
|
||||
4 => (4, 300m, 900m), // Raleigh Architectural Metals
|
||||
5 => (3, 200m, 600m), // East Coast Powderworks
|
||||
6 => (3, 150m, 450m), // Piedmont Metal Works
|
||||
7 => (2, 200m, 500m), // Cary Industrial Solutions
|
||||
8 => (2, 350m, 900m), // Durham Tech Equipment
|
||||
9 => (3, 400m, 1500m), // Wake County Fleet Services
|
||||
10 => (2, 75m, 250m), // John Davis
|
||||
11 => (1, 150m, 350m), // Sarah Jenkins
|
||||
12 => (1, 200m, 400m), // Mike Thompson
|
||||
13 => (2, 100m, 300m), // Robert Miller
|
||||
14 => (0, 0m, 0m), // Jennifer Clark — no jobs yet
|
||||
15 => (1, 100m, 250m), // David Wilson
|
||||
16 => (0, 0m, 0m), // Lisa Anderson — no jobs yet
|
||||
17 => (1, 150m, 300m), // Thomas Harris
|
||||
18 => (0, 0m, 0m), // Karen White — no jobs yet
|
||||
19 => (1, 250m, 500m), // James Taylor
|
||||
20 => (0, 0m, 0m), // Michelle Brown — no jobs yet
|
||||
21 => (1, 100m, 250m), // Chris Lee
|
||||
22 => (0, 0m, 0m), // Amanda Garcia — no jobs yet
|
||||
23 => (1, 150m, 350m), // Kevin Martinez
|
||||
24 => (0, 0m, 0m), // Nancy Rodriguez — no jobs yet
|
||||
25 => (0, 0m, 0m), // Brian Hall — no jobs yet
|
||||
_ => (0, 0m, 0m), // Patricia Young — no jobs yet
|
||||
};
|
||||
|
||||
// All 16 statuses in production workflow order — cycled globally across jobs
|
||||
// so the full pipeline is represented even with fewer total records.
|
||||
// All 16 statuses cycle globally so every pipeline stage is visible.
|
||||
string[] allStatuses =
|
||||
[
|
||||
"PENDING", "QUOTED", "APPROVED", "IN_PREPARATION", "SANDBLASTING",
|
||||
@@ -113,12 +132,9 @@ public partial class SeedDataService
|
||||
"QUALITY_CHECK", "COMPLETED", "READY_FOR_PICKUP", "DELIVERED",
|
||||
"ON_HOLD", "CANCELLED"
|
||||
];
|
||||
|
||||
string StatusFor(int jobIdx) => allStatuses[jobIdx % allStatuses.Length];
|
||||
|
||||
// Maps job index modulo 10 to a priority code. RUSH and URGENT are intentionally
|
||||
// over-represented (4 of 10) relative to production averages so the priority colour
|
||||
// badges and rush-fee logic are clearly visible in demo data.
|
||||
// Priority distribution weighted toward the interesting end for demo visibility.
|
||||
static string PriorityFor(int i) => (i % 10) switch
|
||||
{
|
||||
0 => "RUSH",
|
||||
@@ -127,67 +143,68 @@ public partial class SeedDataService
|
||||
3 => "URGENT",
|
||||
4 => "HIGH",
|
||||
5 => "HIGH",
|
||||
6 => "HIGH",
|
||||
_ => "NORMAL"
|
||||
};
|
||||
|
||||
// Returns description, finish color, prep flags, and estimated minutes for a job item.
|
||||
// Indexed by (i * 3 + j) % 15 so that item variety cycles independently of the job index,
|
||||
// preventing every job from having the same first item.
|
||||
// Job item descriptions and specs — 15-item pool cycling via (jobIdx*3 + itemIdx) % 15.
|
||||
static (string desc, string color, bool sand, bool mask, int mins) ItemSpec(int i, int j) =>
|
||||
((i * 3 + j) % 15) switch
|
||||
{
|
||||
0 => ("18\" Aluminum Wheels — Matte Black", "Matte Black", true, false, 45),
|
||||
1 => ("17\" Steel Wheels — Gloss White", "Gloss White", false, false, 30),
|
||||
2 => ("Valve Covers — Wrinkle Red", "Wrinkle Red", true, true, 40),
|
||||
3 => ("Motorcycle Frame — Flat Black", "Flat Black", true, false, 90),
|
||||
4 => ("Steel Shelving Units", "Textured Gray", true, false, 55),
|
||||
0 => ("18\" Aluminum Wheels (set of 4)", "Gloss Black", false, false, 45),
|
||||
1 => ("17\" Steel Wheels (set of 4)", "Signal White", false, false, 30),
|
||||
2 => ("Jeep Bumper & Rock Sliders", "Matte Black", true, false, 60),
|
||||
3 => ("Motorcycle Frame", "Matte Black", true, false, 90),
|
||||
4 => ("Steel Shelving Units (10-shelf set)", "Textured Gray", true, false, 55),
|
||||
5 => ("Industrial Machine Guard Panels", "Safety Yellow", false, false, 35),
|
||||
6 => ("Aluminum Window Frames", "Satin Bronze", false, true, 50),
|
||||
7 => ("Steel Handrail — 40 ft run", "Gloss Black", true, false, 120),
|
||||
8 => ("Wrought Iron Gate", "Hammered Black", true, false, 180),
|
||||
9 => ("Brake Calipers — Gloss Yellow", "Gloss Yellow", false, true, 35),
|
||||
6 => ("Aluminum Window Frames (set of 8)", "Satin Bronze", false, true, 50),
|
||||
7 => ("Steel Handrail System — 40 ft", "Gloss Black", true, false, 120),
|
||||
8 => ("Wrought Iron Entry Gate", "Hammered Black", true, false, 180),
|
||||
9 => ("Brake Calipers (set of 4)", "Candy Red", false, true, 35),
|
||||
10 => ("Restaurant Chair Frames (set of 20)", "Hammered Bronze", false, false, 60),
|
||||
11 => ("Bicycle Frame — Candy Blue", "Candy Blue", true, true, 60),
|
||||
11 => ("Bicycle Frame", "Candy Red", true, true, 60),
|
||||
12 => ("Compressor Tank", "Safety Orange", true, false, 45),
|
||||
13 => ("Patio Furniture Set", "Textured Beige", false, false, 50),
|
||||
_ => ("Custom Steel Parts — Batch", "Matte Gray", true, false, 40)
|
||||
13 => ("Patio Furniture Set (6 pieces)", "Textured Beige", false, false, 50),
|
||||
_ => ("Custom Steel Fabrication — Batch", "Matte Black", true, false, 40)
|
||||
};
|
||||
|
||||
var jobs = new List<Job>();
|
||||
var quoteIdx = 0;
|
||||
var jobIdx = 0; // global counter drives status cycling across all customers
|
||||
var jobIdx = 0;
|
||||
|
||||
for (int ci = 0; ci < customers.Count; ci++)
|
||||
{
|
||||
var customer = customers[ci];
|
||||
var numJobs = JobsFor(ci);
|
||||
var (numJobs, minVal, maxVal) = CustomerProfile(ci);
|
||||
|
||||
for (int j = 0; j < numJobs; j++, jobIdx++, seq++)
|
||||
{
|
||||
var statusCode = StatusFor(jobIdx);
|
||||
var priorityCode = PriorityFor(jobIdx);
|
||||
|
||||
// Link an approved quote when one is available
|
||||
// Try to link the first available approved quote for this customer
|
||||
Quote? linkedQuote = null;
|
||||
if (quoteIdx < approvedQuotes.Count)
|
||||
for (int qi = quoteIdx; qi < approvedQuotes.Count; qi++)
|
||||
{
|
||||
var candidate = approvedQuotes[quoteIdx];
|
||||
if (candidate.CustomerId == customer.Id || quoteIdx % 3 == 0)
|
||||
if (approvedQuotes[qi].CustomerId == customer.Id)
|
||||
{
|
||||
linkedQuote = candidate;
|
||||
linkedQuote = approvedQuotes[qi];
|
||||
quoteIdx = qi + 1;
|
||||
break;
|
||||
}
|
||||
// Every 4th job forcibly links any available approved quote
|
||||
if (quoteIdx % 4 == 0 && qi == quoteIdx)
|
||||
{
|
||||
linkedQuote = approvedQuotes[qi];
|
||||
quoteIdx++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Date logic — creation spread over 4-6 months
|
||||
// Older jobs for completed statuses, recent for in-progress, future-scheduled for early statuses
|
||||
// Date logic
|
||||
var isCompleted = statusCode is "COMPLETED" or "READY_FOR_PICKUP" or "DELIVERED" or "CANCELLED";
|
||||
var isInProgress = statusCode is "IN_PREPARATION" or "SANDBLASTING" or "MASKING_TAPING"
|
||||
or "CLEANING" or "IN_OVEN" or "COATING" or "CURING" or "QUALITY_CHECK";
|
||||
var isEarly = statusCode is "PENDING" or "QUOTED" or "APPROVED";
|
||||
|
||||
// Spread creation over 30-150 days ago (1-5 months), older jobs for completed statuses
|
||||
int daysAgo = isCompleted ? 60 + (jobIdx % 90)
|
||||
: isInProgress ? 10 + (jobIdx % 40)
|
||||
: 2 + (jobIdx % 15);
|
||||
@@ -197,11 +214,12 @@ public partial class SeedDataService
|
||||
: now.AddDays(3 + (jobIdx % 12));
|
||||
var rushDays = priorityCode == "RUSH" ? 2 : priorityCode == "URGENT" ? 3 : 7;
|
||||
var dueDate = scheduledDate.AddDays(rushDays);
|
||||
var startedDate = !isEarly ? scheduledDate : (DateTime?)null;
|
||||
var startedDate = !isCompleted && !isInProgress ? (DateTime?)null : scheduledDate;
|
||||
var completedDate = isCompleted ? scheduledDate.AddDays(1) : (DateTime?)null;
|
||||
|
||||
var assignedUserId = shopUsers.Count > 0 ? shopUsers[jobIdx % shopUsers.Count].Id : null;
|
||||
|
||||
// Per-customer value targeting: deterministic within the customer's price range
|
||||
var range = maxVal - minVal;
|
||||
var targetValue = minVal + range * ((ci * 7 + j * 13) % 100) / 100m;
|
||||
var itemCount = 1 + (jobIdx % 3);
|
||||
var items = new List<JobItem>();
|
||||
|
||||
@@ -211,7 +229,7 @@ public partial class SeedDataService
|
||||
var qty = 1 + (k % 3);
|
||||
var unitPrice = linkedQuote != null && k == 0
|
||||
? Math.Round(linkedQuote.Total / itemCount, 2)
|
||||
: Math.Round(75m + (jobIdx % 8) * 12.5m + k * 15m, 2);
|
||||
: Math.Round(targetValue / itemCount / qty, 2);
|
||||
|
||||
items.Add(new JobItem
|
||||
{
|
||||
@@ -238,7 +256,7 @@ public partial class SeedDataService
|
||||
JobNumber = $"{prefix}{seq:D4}",
|
||||
CustomerId = customer.Id,
|
||||
QuoteId = linkedQuote?.Id,
|
||||
AssignedUserId = assignedUserId,
|
||||
AssignedUserId = shopUsers.Count > 0 ? shopUsers[jobIdx % shopUsers.Count].Id : null,
|
||||
Description = linkedQuote?.Description
|
||||
?? $"Powder coating services for {customer.CompanyName ?? $"{customer.ContactFirstName} {customer.ContactLastName}".Trim()}",
|
||||
JobStatusId = jobStatuses[statusCode],
|
||||
@@ -250,12 +268,12 @@ public partial class SeedDataService
|
||||
QuotedPrice = quotedPrice,
|
||||
FinalPrice = finalPrice,
|
||||
IsRushJob = priorityCode == "RUSH",
|
||||
CustomerPO = linkedQuote?.CustomerPO ?? (jobIdx % 3 == 0 ? $"PO-{40000 + jobIdx}" : null),
|
||||
CustomerPO = customer.IsCommercial && jobIdx % 3 == 0 ? $"PO-{40000 + jobIdx}" : null,
|
||||
SpecialInstructions = jobIdx % 6 == 0 ? "Customer supplied parts — handle with extra care." :
|
||||
jobIdx % 11 == 0 ? "Match existing color exactly — bring sample for approval." : null,
|
||||
InternalNotes = jobIdx % 8 == 0 ? "Vintage parts — do not use aggressive blast media." : null,
|
||||
RequiresCustomerApproval = jobIdx % 5 == 0,
|
||||
IsCustomerApproved = jobIdx % 5 != 0 || !isEarly,
|
||||
IsCustomerApproved = jobIdx % 5 != 0 || !isInProgress,
|
||||
JobItems = items,
|
||||
CompanyId = company.Id,
|
||||
CreatedAt = createdDate
|
||||
|
||||
@@ -6,39 +6,35 @@ namespace PowderCoating.Infrastructure.Services;
|
||||
public partial class SeedDataService
|
||||
{
|
||||
/// <summary>
|
||||
/// Seeds 75 realistic powder coating quotes spread across seven item categories
|
||||
/// (automotive wheels, industrial, architectural, fitness, marine, furniture, misc)
|
||||
/// with a realistic status distribution: Draft (8), Sent (12), Approved (35),
|
||||
/// Rejected (10), and Expired (10).
|
||||
/// Seeds 35 realistic quotes spanning the full status lifecycle: Draft, Sent, Approved,
|
||||
/// Rejected, and Expired — with a deliberate majority of Approved quotes so that
|
||||
/// SeedJobsAsync has enough approved records to link jobs to.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Idempotency: returns 0 immediately if any non-deleted quotes already exist for
|
||||
/// this company, preventing duplicate quote sets on repeated seed runs.
|
||||
/// All five quote statuses are present so the Quotes Index view demonstrates the full
|
||||
/// workflow from first contact to customer approval. APPROVED is the majority (16 of 35)
|
||||
/// because the job seeder links to approved quotes to show the quote-to-job conversion path.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Quote numbers follow the production format <c>QT-YYMM-####</c>. The seeder scans
|
||||
/// existing numbers with the current month prefix and starts its sequence above the
|
||||
/// current maximum so seeded quotes never collide with real quotes created in the
|
||||
/// same month.
|
||||
/// Quotes are distributed across both commercial and individual customers, matching the
|
||||
/// real-world mix where most quotes come from commercial accounts but individuals do
|
||||
/// occasionally request quotes for larger projects.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Pricing is deliberately simple (sqft × $8.50 + variance) rather than running through
|
||||
/// <c>IPricingCalculationService</c> — this avoids a dependency on company operating cost
|
||||
/// config that may not yet be populated when seed runs.
|
||||
/// Item descriptions, colours, and specs are drawn from the same 15-item pool used by
|
||||
/// the job seeder so that tutorial screenshots of quotes and linked jobs look consistent.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Tax-exempt customers automatically receive a 0 % tax rate (matching the production
|
||||
/// behaviour in <c>QuotesController</c>). Rush fees (15 %) are added every 12th quote.
|
||||
/// Pricing is deliberately simple (sqft × rate + variance) rather than running through
|
||||
/// IPricingCalculationService — this avoids a dependency on operating cost config that
|
||||
/// may not yet be populated when seed runs.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The method requires that customers and quote-status lookup rows already exist for the
|
||||
/// company; it returns 0 if either dependency is missing so that the overall seed
|
||||
/// operation degrades gracefully rather than throwing.
|
||||
/// Dates spread over 150–180 days back (5–6 months) so the historical charts on the
|
||||
/// dashboard and reports show a meaningful activity curve.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <param name="company">The tenant company to seed quotes for.</param>
|
||||
/// <returns>Number of quotes inserted, or 0 if already seeded or dependencies are missing.</returns>
|
||||
private async Task<int> SeedQuotesAsync(Company company)
|
||||
{
|
||||
var existingCount = await _context.Set<Quote>()
|
||||
@@ -56,14 +52,15 @@ public partial class SeedDataService
|
||||
if (quoteStatuses.Count == 0)
|
||||
return 0;
|
||||
|
||||
// Load all commercial customers
|
||||
var customers = await _context.Set<Customer>()
|
||||
// Load all customers — commercial first, then individual
|
||||
var allCustomers = await _context.Set<Customer>()
|
||||
.IgnoreQueryFilters()
|
||||
.Where(c => c.CompanyId == company.Id && c.IsCommercial && !c.IsDeleted)
|
||||
.OrderBy(c => c.Id)
|
||||
.Where(c => c.CompanyId == company.Id && !c.IsDeleted)
|
||||
.OrderByDescending(c => c.IsCommercial)
|
||||
.ThenBy(c => c.Id)
|
||||
.ToListAsync();
|
||||
|
||||
if (customers.Count == 0)
|
||||
if (allCustomers.Count == 0)
|
||||
return 0;
|
||||
|
||||
var preparedByUser = await _userManager.Users
|
||||
@@ -72,7 +69,6 @@ public partial class SeedDataService
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// Avoid duplicate quote numbers
|
||||
var prefix = $"QT-{now:yy}{now.Month:D2}-";
|
||||
var existing = await _context.Set<Quote>()
|
||||
.IgnoreQueryFilters()
|
||||
@@ -84,122 +80,62 @@ public partial class SeedDataService
|
||||
if (n.Length >= 12 && int.TryParse(n.Substring(8, 4), out var x) && x > maxNum) maxNum = x;
|
||||
var seq = maxNum + 1;
|
||||
|
||||
// ── Data arrays for varied, realistic content ─────────────────────────
|
||||
|
||||
// Returns an array of realistic item descriptions for a given category bucket (0–6).
|
||||
// Using a local static function keeps the description data close to where it is
|
||||
// consumed and avoids polluting the partial class with per-seeder detail arrays.
|
||||
static string[] ItemDescs(int category) => category switch
|
||||
{
|
||||
0 => new[] {
|
||||
"18\" Aluminum Wheels — Matte Black",
|
||||
"17\" Steel Wheels — Gloss White",
|
||||
"20\" Alloy Wheels — Satin Silver",
|
||||
"16\" Chrome Replica Wheels — Gloss Black",
|
||||
"Motorcycle Frame — Flat Black",
|
||||
"Motorcycle Swingarm & Forks — Gloss Black",
|
||||
"Exhaust Headers — High-Temp Flat Black",
|
||||
"Intake Manifold — Wrinkle Red",
|
||||
"Valve Covers — Gloss Red",
|
||||
"Brake Calipers — Gloss Yellow" },
|
||||
1 => new[] {
|
||||
"Steel Shelving Units (10-shelf set)",
|
||||
"Industrial Equipment Frame",
|
||||
"Machine Guard Panels",
|
||||
"Conveyor Frame Sections",
|
||||
"Heavy Equipment Brackets",
|
||||
"Pump Housing Assembly",
|
||||
"Control Panel Enclosure",
|
||||
"Storage Rack System",
|
||||
"Scissor Lift Platform",
|
||||
"Compressor Tank" },
|
||||
2 => new[] {
|
||||
"Aluminum Window Frames (set of 8)",
|
||||
"Steel Handrail System — 40 ft",
|
||||
"Wrought Iron Fence Panels (6-panel set)",
|
||||
"Entry Gate — Custom Design",
|
||||
"Structural Steel Columns (set of 4)",
|
||||
"Balcony Railing — Satin Black",
|
||||
"Steel Door Frames (3 units)",
|
||||
"Architectural Steel Beams",
|
||||
"Decorative Ironwork — Stair Baluster",
|
||||
"Aluminum Storefront Frame" },
|
||||
3 => new[] {
|
||||
"Commercial Gym Equipment Frame",
|
||||
"Weight Rack & Benches",
|
||||
"Outdoor Playground Equipment Parts",
|
||||
"Bicycle Frame — Gloss Blue",
|
||||
"BMX Frame Set — Candy Red" },
|
||||
4 => new[] {
|
||||
"Boat Trailer Frame — Marine Grade",
|
||||
"Aluminum Dock Cleats & Hardware",
|
||||
"Outboard Motor Bracket",
|
||||
"Marine Fuel Tank Brackets" },
|
||||
5 => new[] {
|
||||
"Restaurant Chair Frames (set of 20)",
|
||||
"Steel Dining Table Bases (set of 8)",
|
||||
"Patio Furniture Set — 6 Pieces",
|
||||
"Café Chairs — Hammered Bronze (12-pc)",
|
||||
"Commercial Bar Stools (set of 10)" },
|
||||
_ => new[] {
|
||||
"Custom Steel Parts — Batch Order",
|
||||
"Agricultural Equipment Panels",
|
||||
"Traffic Sign Frames (set of 15)",
|
||||
"Utility Trailer Hitch Assembly",
|
||||
"Solar Panel Mounting Brackets" }
|
||||
};
|
||||
|
||||
// Returns finish color, prep flags, estimated minutes, and surface area for item index i.
|
||||
// Cycling modulo 9 ensures variety across all 75 quotes without requiring a large lookup table.
|
||||
static (string color, bool sandblast, bool mask, int minutes, decimal sqft) ItemSpec(int i) => (i % 9) switch
|
||||
{
|
||||
0 => ("Matte Black", true, false, 45, 12.0m),
|
||||
1 => ("Gloss White", false, false, 30, 8.5m),
|
||||
2 => ("Satin Silver", true, true, 60, 15.0m),
|
||||
3 => ("Candy Red", false, true, 35, 9.0m),
|
||||
4 => ("Textured Gray", true, false, 50, 18.0m),
|
||||
5 => ("Gloss Black", true, false, 40, 11.0m),
|
||||
6 => ("Hammered Bronze", false, false, 55, 20.0m),
|
||||
7 => ("Satin Graphite", true, true, 65, 25.0m),
|
||||
_ => ("Flat Black", true, false, 35, 10.0m)
|
||||
};
|
||||
|
||||
// Maps quote index to a status code.
|
||||
// APPROVED is the majority (10/20) to give SeedJobsAsync enough approved quotes to link jobs to.
|
||||
// ── Status distribution: 3 Draft, 6 Sent, 16 Approved, 5 Rejected, 5 Expired ──
|
||||
// APPROVED is majority so SeedJobsAsync has enough linked quotes.
|
||||
static string StatusFor(int i) => i switch
|
||||
{
|
||||
< 2 => "DRAFT",
|
||||
< 5 => "SENT",
|
||||
< 15 => "APPROVED",
|
||||
< 18 => "REJECTED",
|
||||
< 3 => "DRAFT",
|
||||
< 9 => "SENT",
|
||||
< 25 => "APPROVED",
|
||||
< 30 => "REJECTED",
|
||||
_ => "EXPIRED"
|
||||
};
|
||||
|
||||
var quotes = new List<Quote>();
|
||||
|
||||
for (int i = 0; i < 20; i++)
|
||||
// ── Item catalogue — 15 common powder coating jobs ──────────────────────
|
||||
// Index by (i * 3 + j) % 15 so variety cycles independently of quote index.
|
||||
static (string desc, string color, bool sand, bool mask, int mins, decimal sqft) ItemSpec(int i) =>
|
||||
(i % 15) switch
|
||||
{
|
||||
var customer = customers[i % customers.Count];
|
||||
0 => ("18\" Aluminum Wheels (set of 4)", "Gloss Black", false, false, 45, 12.0m),
|
||||
1 => ("17\" Steel Wheels (set of 4)", "Signal White", false, false, 30, 8.5m),
|
||||
2 => ("Jeep Bumper & Rock Sliders", "Matte Black", true, false, 60, 15.0m),
|
||||
3 => ("Motorcycle Frame", "Matte Black", true, false, 90, 14.0m),
|
||||
4 => ("Steel Shelving Units (10-shelf set)", "Textured Gray", true, false, 55, 18.0m),
|
||||
5 => ("Industrial Machine Guard Panels", "Safety Yellow", false, false, 35, 20.0m),
|
||||
6 => ("Aluminum Window Frames (set of 8)", "Satin Bronze", false, true, 50, 22.0m),
|
||||
7 => ("Steel Handrail System — 40 ft", "Gloss Black", true, false, 120, 40.0m),
|
||||
8 => ("Wrought Iron Entry Gate", "Hammered Black", true, false, 180, 35.0m),
|
||||
9 => ("Brake Calipers (set of 4)", "Candy Red", false, true, 35, 4.0m),
|
||||
10 => ("Restaurant Chair Frames (set of 20)", "Hammered Bronze", false, false, 60, 30.0m),
|
||||
11 => ("Bicycle Frame", "Candy Red", true, true, 60, 6.5m),
|
||||
12 => ("Compressor Tank", "Safety Orange", true, false, 45, 10.0m),
|
||||
13 => ("Patio Furniture Set (6 pieces)", "Textured Beige", false, false, 50, 24.0m),
|
||||
_ => ("Custom Steel Fabrication — Batch", "Matte Black", true, false, 40, 15.0m)
|
||||
};
|
||||
|
||||
var quotes = new List<Quote>();
|
||||
const int Total = 35;
|
||||
|
||||
for (int i = 0; i < Total; i++)
|
||||
{
|
||||
// Cycle through all customers; top 10 (commercial) appear multiple times
|
||||
var customer = allCustomers[i % allCustomers.Count];
|
||||
var statusCode = StatusFor(i);
|
||||
|
||||
// Spread creation dates over the past 120–180 days (4-6 months); older first
|
||||
var daysAgo = 180 - (int)(i * 9.0);
|
||||
// Spread dates over 30–180 days ago; newer quotes have lower index
|
||||
var daysAgo = 180 - (int)(i * 4.5);
|
||||
var quoteDate = now.AddDays(-daysAgo);
|
||||
var expireDate = quoteDate.AddDays(30);
|
||||
|
||||
var category = i % 7;
|
||||
var descs = ItemDescs(category);
|
||||
var itemCount = 1 + (i % 3);
|
||||
|
||||
var items = new List<QuoteItem>();
|
||||
|
||||
for (int j = 0; j < itemCount; j++)
|
||||
{
|
||||
var desc = descs[(i + j) % descs.Length];
|
||||
var (color, sand, mask, mins, sqft) = ItemSpec(i + j);
|
||||
var (desc, color, sand, mask, mins, sqft) = ItemSpec(i * 3 + j);
|
||||
var qty = 1 + (j % 4);
|
||||
// Unit price scales with surface area and adds a modest multiplier per customer tier
|
||||
var tierMult = 1m + ((customer.PricingTier?.DiscountPercent ?? 0m) / 100m * -1m);
|
||||
var unitPrice = Math.Round(sqft * 8.50m * tierMult + (i % 5) * 4.5m, 2);
|
||||
var tierMult = 1m - ((customer.PricingTier?.DiscountPercent ?? 0m) / 100m);
|
||||
var unitPrice = Math.Round(sqft * 8.50m * tierMult + (i % 6) * 5.0m, 2);
|
||||
|
||||
items.Add(new QuoteItem
|
||||
{
|
||||
@@ -212,7 +148,7 @@ public partial class SeedDataService
|
||||
RequiresMasking = mask,
|
||||
EstimatedMinutes = mins,
|
||||
Complexity = (i % 4) switch { 0 => "Simple", 1 => "Moderate", 2 => "Complex", _ => "Simple" },
|
||||
Notes = j == 0 && i % 5 == 0 ? $"{color} finish requested" : null,
|
||||
Notes = j == 0 && i % 5 == 0 ? $"Customer requested {color} — confirm shade before run." : null,
|
||||
CompanyId = company.Id,
|
||||
CreatedAt = quoteDate
|
||||
});
|
||||
@@ -224,17 +160,17 @@ public partial class SeedDataService
|
||||
var afterDiscount = subtotal - discountAmt;
|
||||
var taxPct = customer.IsTaxExempt ? 0m : 7.5m;
|
||||
var taxAmt = Math.Round(afterDiscount * taxPct / 100m, 2);
|
||||
var rushFee = i % 12 == 0 ? Math.Round(afterDiscount * 0.15m, 2) : 0m;
|
||||
var rushFee = i % 10 == 0 ? Math.Round(afterDiscount * 0.15m, 2) : 0m;
|
||||
var total = afterDiscount + taxAmt + rushFee;
|
||||
|
||||
var quote = new Quote
|
||||
quotes.Add(new Quote
|
||||
{
|
||||
QuoteNumber = $"{prefix}{seq:D4}",
|
||||
CustomerId = customer.Id,
|
||||
PreparedById = preparedByUser?.Id,
|
||||
QuoteStatusId = quoteStatuses[statusCode],
|
||||
IsCommercial = customer.IsCommercial,
|
||||
IsRushJob = i % 12 == 0,
|
||||
IsRushJob = i % 10 == 0,
|
||||
QuoteDate = quoteDate,
|
||||
ExpirationDate = expireDate,
|
||||
SentDate = statusCode != "DRAFT" ? quoteDate.AddDays(1) : null,
|
||||
@@ -247,20 +183,19 @@ public partial class SeedDataService
|
||||
TaxAmount = taxAmt,
|
||||
RushFee = rushFee,
|
||||
Total = total,
|
||||
Description = $"Powder coating services — {descs[i % descs.Length].Split('—')[0].Trim()}",
|
||||
Description = $"Powder coating services — {items[0].Description.Split('(')[0].TrimEnd()}",
|
||||
Terms = customer.PaymentTerms ?? "Net 30",
|
||||
Notes = i % 7 == 0 ? "Customer requested color sample before full run." :
|
||||
Notes = i % 8 == 0 ? "Customer requested color sample before full production run." :
|
||||
i % 13 == 0 ? "Rush turnaround requested — 3 business days." : null,
|
||||
CustomerPO = i % 2 == 0 ? $"PO-{30000 + i}" : null,
|
||||
CustomerPO = customer.IsCommercial && i % 2 == 0 ? $"PO-{30000 + i}" : null,
|
||||
RequiresDeposit = i % 4 == 0,
|
||||
DepositPercent = i % 4 == 0 ? 50m : 0m,
|
||||
QuoteItems = items,
|
||||
CompanyId = company.Id,
|
||||
CreatedAt = quoteDate,
|
||||
UpdatedAt = statusCode == "DRAFT" ? quoteDate : quoteDate.AddDays(1)
|
||||
};
|
||||
});
|
||||
|
||||
quotes.Add(quote);
|
||||
seq++;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,14 +12,17 @@ public partial class SeedDataService
|
||||
/// </summary>
|
||||
private static readonly string[] SeededCustomerEmails =
|
||||
[
|
||||
"john.smith@acmemfg.com", "sjohnson@precisionauto.com", "rtaylor@classicwheels.com",
|
||||
"cbrown@motorsportscustom.com", "janderson@indfurniture.com", "kgarcia@commercialhvac.com",
|
||||
"swhite@agequipment.com", "mchen@urbanrailings.com", "tmiller@heritagemetal.com",
|
||||
"lmartinez@fitequip.com", "nmartinez@playgroundusa.com", "pwilson@marineequip.com",
|
||||
"blee@officesystems.com", "dwilliams@metrota.gov", "adavis@greenenergy.com",
|
||||
"jthompson@email.com", "mharris@email.com", "wclark@email.com", "elewis@email.com",
|
||||
"rwalker@email.com", "bhall@email.com", "jallen@email.com", "syoung@email.com",
|
||||
"cking@email.com", "lwright@email.com", "gnelson@email.com", "carol.evans@email.com"
|
||||
// Commercial — NC Triangle area
|
||||
"matt@carolinafab.com", "ctanner@apexmotorsports.com", "jpruitt@triangleoffroad.com",
|
||||
"bsmith@smithwelding.com", "kmorales@raleigharchitectural.com", "tgreco@eastcoastpw.com",
|
||||
"dshaw@piedmontmetalworks.com", "lpatel@caryindustrial.com", "rblake@durhamtech.com",
|
||||
"mcoleman@wakecountyfleet.gov",
|
||||
// Individual residential
|
||||
"jdavis@email.com", "sjenkins@email.com", "mthompson@email.com", "rmiller@email.com",
|
||||
"jclark@email.com", "dwilson@email.com", "landerson@email.com", "tharris@email.com",
|
||||
"kwhite@email.com", "jtaylor@email.com", "mbrown@email.com", "clee@email.com",
|
||||
"agarcia@email.com", "kmartinez@email.com", "nrodriguez@email.com",
|
||||
"bhall@email.com", "pyoung@email.com"
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
@@ -52,9 +55,11 @@ public partial class SeedDataService
|
||||
/// </summary>
|
||||
private static readonly string[] SeededInventorySkuSuffixes =
|
||||
[
|
||||
"-PWD-BLK-001", "-PWD-WHT-001", "-PWD-RED-001", "-PWD-BLU-001",
|
||||
"-PWD-GRY-001", "-PWD-YEL-001", "-PWD-ORG-001", "-PWD-GRN-001",
|
||||
"-CLN-001", "-MSK-001"
|
||||
// 6 powders
|
||||
"-PWD-GBK-001", "-PWD-MBK-001", "-PWD-CHR-001", "-PWD-CRD-001",
|
||||
"-PWD-SWH-001", "-PWD-IPU-001",
|
||||
// 5 consumables
|
||||
"-MSK-001", "-PLG-001", "-HKS-001", "-ACT-001", "-BLM-001"
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -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
|
||||
@@ -1244,9 +1013,9 @@ public partial class SeedDataService : ISeedDataService
|
||||
{
|
||||
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 = "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);
|
||||
|
||||
Reference in New Issue
Block a user