using Microsoft.EntityFrameworkCore; using PowderCoating.Core.Entities; 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). /// /// /// /// Idempotency: returns 0 immediately if any non-deleted quotes already exist for /// this company, preventing duplicate quote sets on repeated seed runs. /// /// /// 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. /// /// /// 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. /// /// /// Tax-exempt customers automatically receive a 0 % tax rate (matching the production /// behaviour in QuotesController). Rush fees (15 %) are added every 12th quote. /// /// /// 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. /// /// /// 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() .IgnoreQueryFilters() .CountAsync(q => q.CompanyId == company.Id && !q.IsDeleted); if (existingCount > 0) return 0; var quoteStatuses = await _context.Set() .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() .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() .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(); 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(); 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().AddRangeAsync(quotes); await _context.SaveChangesAsync(); return quotes.Count; } }