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:
@@ -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,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<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 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<JobItem>();
|
||||
// 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>();
|
||||
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user