diff --git a/src/PowderCoating.Infrastructure/Services/SeedDataService.Customers.cs b/src/PowderCoating.Infrastructure/Services/SeedDataService.Customers.cs index 844b17b..15b2d1d 100644 --- a/src/PowderCoating.Infrastructure/Services/SeedDataService.Customers.cs +++ b/src/PowderCoating.Infrastructure/Services/SeedDataService.Customers.cs @@ -6,44 +6,32 @@ namespace PowderCoating.Infrastructure.Services; public partial class SeedDataService { /// - /// 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. /// /// /// - /// 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. /// /// - /// Each customer is inserted individually (rather than in a single AddRange) 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 - /// SaveChangesAsync 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. /// /// - /// 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). /// /// - /// Government/municipal customers (Metro Transit Authority, Municipal Services Group, - /// Regional Airport Authority, County School District) are seeded with - /// IsTaxExempt = true to demonstrate the tax-exempt workflow, matching the - /// production rule that tax-exempt customers get 0 % tax on quotes and invoices. - /// - /// - /// The two local helper functions Comm() and Indiv() reduce the per-row - /// line count; they are defined as local functions rather than private methods because - /// they capture the company 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. /// /// /// The tenant company to seed customers for. /// - /// A tuple of (seededCount, warnings) where seededCount is the number - /// of records actually inserted and warnings lists any customers that were skipped - /// (e.g. because the email already existed). + /// A tuple of (seededCount, warnings) where warnings list any skipped customers. /// private async Task<(int seededCount, List 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() .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 { - // ─── 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; } diff --git a/src/PowderCoating.Infrastructure/Services/SeedDataService.Jobs.cs b/src/PowderCoating.Infrastructure/Services/SeedDataService.Jobs.cs index d6b6c41..b05b68b 100644 --- a/src/PowderCoating.Infrastructure/Services/SeedDataService.Jobs.cs +++ b/src/PowderCoating.Infrastructure/Services/SeedDataService.Jobs.cs @@ -7,41 +7,31 @@ namespace PowderCoating.Infrastructure.Services; public partial class SeedDataService { /// - /// 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. /// /// /// - /// 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 CustomerProfile(ci). This replaces the previous + /// modulo-formula approach that produced uniform pricing regardless of customer tier. /// /// - /// 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. /// /// - /// Job numbers follow the production format JOB-YYMM-####. 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. /// /// - /// 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. - /// - /// - /// 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. - /// - /// - /// The IgnoreQueryFilters() 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. /// /// - /// The tenant company to seed jobs for. - /// Number of jobs inserted, or 0 if already seeded or dependencies are missing. private async Task SeedJobsAsync(Company company) { var existingCount = await _context.Set() @@ -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() .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() @@ -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() .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,83 +143,85 @@ 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), - 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), - 10 => ("Restaurant Chair Frames (set of 20)", "Hammered Bronze", false, false, 60), - 11 => ("Bicycle Frame — Candy Blue", "Candy Blue", 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) + 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 (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 Red", true, true, 60), + 12 => ("Compressor Tank", "Safety Orange", true, false, 45), + 13 => ("Patio Furniture Set (6 pieces)", "Textured Beige", false, false, 50), + _ => ("Custom Steel Fabrication — Batch", "Matte Black", true, false, 40) }; var jobs = new List(); 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 customer = customers[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"; + or "CLEANING" or "IN_OVEN" or "COATING" or "CURING" or "QUALITY_CHECK"; - // 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); - var createdDate = now.AddDays(-daysAgo); + int daysAgo = isCompleted ? 60 + (jobIdx % 90) + : isInProgress ? 10 + (jobIdx % 40) + : 2 + (jobIdx % 15); + var createdDate = now.AddDays(-daysAgo); var scheduledDate = isCompleted ? createdDate.AddDays(3 + (jobIdx % 5)) : isInProgress ? now.AddDays(-(jobIdx % 4)) : 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 completedDate = isCompleted ? scheduledDate.AddDays(1) : (DateTime?)null; + var rushDays = priorityCode == "RUSH" ? 2 : priorityCode == "URGENT" ? 3 : 7; + var dueDate = scheduledDate.AddDays(rushDays); + 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; - - var itemCount = 1 + (jobIdx % 3); - var items = new List(); + // 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(); for (int k = 0; k < itemCount; k++) { @@ -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 diff --git a/src/PowderCoating.Infrastructure/Services/SeedDataService.Quotes.cs b/src/PowderCoating.Infrastructure/Services/SeedDataService.Quotes.cs index 7d2013d..561a8bd 100644 --- a/src/PowderCoating.Infrastructure/Services/SeedDataService.Quotes.cs +++ b/src/PowderCoating.Infrastructure/Services/SeedDataService.Quotes.cs @@ -6,39 +6,35 @@ namespace PowderCoating.Infrastructure.Services; public partial class SeedDataService { /// - /// 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. /// /// /// - /// 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. /// /// - /// Quote numbers follow the production format QT-YYMM-####. 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. /// /// - /// Pricing is deliberately simple (sqft × $8.50 + variance) rather than running through - /// IPricingCalculationService — 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. /// /// - /// Tax-exempt customers automatically receive a 0 % tax rate (matching the production - /// behaviour in QuotesController). 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. /// /// - /// 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. /// /// - /// The tenant company to seed quotes for. - /// Number of quotes inserted, or 0 if already seeded or dependencies are missing. private async Task SeedQuotesAsync(Company company) { var existingCount = await _context.Set() @@ -56,14 +52,15 @@ public partial class SeedDataService if (quoteStatuses.Count == 0) return 0; - // Load all commercial customers - var customers = await _context.Set() + // Load all customers — commercial first, then individual + var allCustomers = await _context.Set() .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() .IgnoreQueryFilters() @@ -84,183 +80,122 @@ 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(); + // ── 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 + { + 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) + }; - for (int i = 0; i < 20; i++) + var quotes = new List(); + const int Total = 35; + + for (int i = 0; i < Total; i++) { - var customer = customers[i % customers.Count]; + // 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); - var quoteDate = now.AddDays(-daysAgo); + // 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(); + for (int j = 0; j < itemCount; j++) { - var desc = descs[(i + j) % descs.Length]; - var (color, sand, mask, mins, sqft) = ItemSpec(i + 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 (desc, color, sand, mask, mins, sqft) = ItemSpec(i * 3 + j); + var qty = 1 + (j % 4); + 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 { - Description = desc, - Quantity = qty, - SurfaceAreaSqFt = sqft * qty, - UnitPrice = unitPrice, - TotalPrice = unitPrice * qty, + Description = desc, + Quantity = qty, + SurfaceAreaSqFt = sqft * qty, + UnitPrice = unitPrice, + TotalPrice = unitPrice * qty, RequiresSandblasting = sand, - 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, - CompanyId = company.Id, - CreatedAt = quoteDate + RequiresMasking = mask, + EstimatedMinutes = mins, + Complexity = (i % 4) switch { 0 => "Simple", 1 => "Moderate", 2 => "Complex", _ => "Simple" }, + Notes = j == 0 && i % 5 == 0 ? $"Customer requested {color} — confirm shade before run." : null, + CompanyId = company.Id, + CreatedAt = quoteDate }); } - var subtotal = items.Sum(it => it.TotalPrice); - var discountPct = customer.PricingTier?.DiscountPercent ?? 0m; - var discountAmt = Math.Round(subtotal * discountPct / 100m, 2); - 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 total = afterDiscount + taxAmt + rushFee; + var subtotal = items.Sum(it => it.TotalPrice); + var discountPct = customer.PricingTier?.DiscountPercent ?? 0m; + var discountAmt = Math.Round(subtotal * discountPct / 100m, 2); + var afterDiscount = subtotal - discountAmt; + var taxPct = customer.IsTaxExempt ? 0m : 7.5m; + var taxAmt = Math.Round(afterDiscount * taxPct / 100m, 2); + 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, - QuoteDate = quoteDate, - ExpirationDate = expireDate, - SentDate = statusCode != "DRAFT" ? quoteDate.AddDays(1) : null, - ApprovedDate = statusCode == "APPROVED" ? quoteDate.AddDays(4) : null, - ItemsSubtotal = subtotal, - SubTotal = subtotal, - DiscountPercent = discountPct, - DiscountAmount = discountAmt, - TaxPercent = taxPct, - TaxAmount = taxAmt, - RushFee = rushFee, - Total = total, - Description = $"Powder coating services — {descs[i % descs.Length].Split('—')[0].Trim()}", - Terms = customer.PaymentTerms ?? "Net 30", - Notes = i % 7 == 0 ? "Customer requested color sample before full run." : - i % 13 == 0 ? "Rush turnaround requested — 3 business days." : null, - CustomerPO = 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) - }; + QuoteNumber = $"{prefix}{seq:D4}", + CustomerId = customer.Id, + PreparedById = preparedByUser?.Id, + QuoteStatusId = quoteStatuses[statusCode], + IsCommercial = customer.IsCommercial, + IsRushJob = i % 10 == 0, + QuoteDate = quoteDate, + ExpirationDate = expireDate, + SentDate = statusCode != "DRAFT" ? quoteDate.AddDays(1) : null, + ApprovedDate = statusCode == "APPROVED" ? quoteDate.AddDays(4) : null, + ItemsSubtotal = subtotal, + SubTotal = subtotal, + DiscountPercent = discountPct, + DiscountAmount = discountAmt, + TaxPercent = taxPct, + TaxAmount = taxAmt, + RushFee = rushFee, + Total = total, + Description = $"Powder coating services — {items[0].Description.Split('(')[0].TrimEnd()}", + Terms = customer.PaymentTerms ?? "Net 30", + Notes = i % 8 == 0 ? "Customer requested color sample before full production run." : + i % 13 == 0 ? "Rush turnaround requested — 3 business days." : 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++; } diff --git a/src/PowderCoating.Infrastructure/Services/SeedDataService.Remove.cs b/src/PowderCoating.Infrastructure/Services/SeedDataService.Remove.cs index 64178a7..a5f10a8 100644 --- a/src/PowderCoating.Infrastructure/Services/SeedDataService.Remove.cs +++ b/src/PowderCoating.Infrastructure/Services/SeedDataService.Remove.cs @@ -12,14 +12,17 @@ public partial class SeedDataService /// 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" ]; /// @@ -52,9 +55,11 @@ public partial class SeedDataService /// 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" ]; /// diff --git a/src/PowderCoating.Infrastructure/Services/SeedDataService.cs b/src/PowderCoating.Infrastructure/Services/SeedDataService.cs index 5de82a4..9220e31 100644 --- a/src/PowderCoating.Infrastructure/Services/SeedDataService.cs +++ b/src/PowderCoating.Infrastructure/Services/SeedDataService.cs @@ -742,325 +742,94 @@ public partial class SeedDataService : ISeedDataService } /// - /// 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. /// /// - /// Returns a tuple rather than a plain int because each item is saved individually - /// (one SaveChangesAsync 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 to guarantee - /// uniqueness across tenants in a shared database — e.g., DEMO-PWD-BLK-001. - /// A missing or empty CompanyCode throws because - /// SKU collisions would violate the unique index on the InventoryItems table. - /// - /// Category IDs are resolved by CategoryCode (e.g., "POWDER", "CLEANER") rather - /// than hard-coded IDs because lookup IDs differ per company and per environment. - /// - /// All powder items default to CoverageSqFtPerLb = 30 and - /// TransferEfficiency = 65, which are industry-standard starting values used by - /// the pricing engine when calculating powder needed per coat. + /// SKUs are prefixed with to guarantee uniqueness + /// across tenants in a shared database (e.g., DEMO-PWD-GBK-001). /// - /// The tenant company to seed inventory for. - /// - /// A tuple of (count of items successfully inserted, list of per-item warning messages - /// for skipped or failed items). - /// private async Task<(int seededCount, List warnings)> SeedInventoryItemsAsync(Company company) { var warnings = new List(); 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 { - 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 { - 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().AddRangeAsync(vendors);