Initial commit
This commit is contained in:
@@ -0,0 +1,272 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using PowderCoating.Core.Entities;
|
||||
|
||||
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).
|
||||
/// </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.
|
||||
/// </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.
|
||||
/// </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.
|
||||
/// </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.
|
||||
/// </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.
|
||||
/// </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>()
|
||||
.IgnoreQueryFilters()
|
||||
.CountAsync(q => q.CompanyId == company.Id && !q.IsDeleted);
|
||||
|
||||
if (existingCount > 0)
|
||||
return 0;
|
||||
|
||||
var quoteStatuses = await _context.Set<QuoteStatusLookup>()
|
||||
.IgnoreQueryFilters()
|
||||
.Where(s => s.CompanyId == company.Id)
|
||||
.ToDictionaryAsync(s => s.StatusCode, s => s.Id);
|
||||
|
||||
if (quoteStatuses.Count == 0)
|
||||
return 0;
|
||||
|
||||
// Load all commercial customers
|
||||
var customers = await _context.Set<Customer>()
|
||||
.IgnoreQueryFilters()
|
||||
.Where(c => c.CompanyId == company.Id && c.IsCommercial && !c.IsDeleted)
|
||||
.OrderBy(c => c.Id)
|
||||
.ToListAsync();
|
||||
|
||||
if (customers.Count == 0)
|
||||
return 0;
|
||||
|
||||
var preparedByUser = await _userManager.Users
|
||||
.Where(u => u.CompanyId == company.Id)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// Avoid duplicate quote numbers
|
||||
var prefix = $"QT-{now:yy}{now.Month:D2}-";
|
||||
var existing = await _context.Set<Quote>()
|
||||
.IgnoreQueryFilters()
|
||||
.Where(q => q.QuoteNumber.StartsWith(prefix))
|
||||
.Select(q => q.QuoteNumber)
|
||||
.ToListAsync();
|
||||
var maxNum = 0;
|
||||
foreach (var n in existing)
|
||||
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 following the distribution plan above.
|
||||
// APPROVED is the majority (35/75) to give SeedJobsAsync enough approved quotes to link jobs to.
|
||||
static string StatusFor(int i) => i switch
|
||||
{
|
||||
< 8 => "DRAFT",
|
||||
< 20 => "SENT",
|
||||
< 55 => "APPROVED",
|
||||
< 65 => "REJECTED",
|
||||
_ => "EXPIRED"
|
||||
};
|
||||
|
||||
var quotes = new List<Quote>();
|
||||
|
||||
for (int i = 0; i < 75; i++)
|
||||
{
|
||||
var customer = customers[i % customers.Count];
|
||||
var statusCode = StatusFor(i);
|
||||
|
||||
// Spread creation dates over the past 90 days; older first
|
||||
var daysAgo = 90 - (int)(i * 1.2);
|
||||
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 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);
|
||||
|
||||
items.Add(new QuoteItem
|
||||
{
|
||||
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
|
||||
});
|
||||
}
|
||||
|
||||
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 quote = 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)
|
||||
};
|
||||
|
||||
quotes.Add(quote);
|
||||
seq++;
|
||||
}
|
||||
|
||||
await _context.Set<Quote>().AddRangeAsync(quotes);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
return quotes.Count;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user