using Microsoft.EntityFrameworkCore; using PowderCoating.Core.Entities; namespace PowderCoating.Infrastructure.Services; public partial class SeedDataService { /// /// Seeds a plausible status-transition history for every job belonging to the company, /// reconstructing the sequence of transitions a job must have passed through to reach /// its current status. /// /// /// /// Idempotency: returns 0 immediately if any non-deleted history rows already exist for /// this company. /// /// /// The method does not record arbitrary transitions — it follows the canonical 14-step /// pipeline array (PENDING → QUOTED → APPROVED → … → DELIVERED) and generates /// one row per transition step, from PENDING up to /// and including the job's current status. /// /// /// Terminal side-branch statuses are handled explicitly: /// /// ON_HOLD — assumed to have reached QUALITY_CHECK before pausing. /// CANCELLED — assumed to have been cancelled from IN_PREPARATION. /// /// /// /// Transition timestamps are spread ~6 hours apart starting from job.CreatedAt. /// This is an approximation chosen for demo realism; actual production transitions record /// the wall-clock time at which a user changes the status. A safety clamp prevents any /// generated timestamp from exceeding DateTime.UtcNow. /// /// /// All history rows are batched into a single AddRangeAsync / SaveChangesAsync /// call for performance, since the total count can be several hundred rows (50 jobs × up /// to 14 transitions each). /// /// /// The tenant company to seed job status history for. /// Total number of history rows inserted, or 0 if already seeded or no jobs exist. private async Task SeedJobStatusHistoryAsync(Company company) { var existingCount = await _context.Set() .IgnoreQueryFilters() .CountAsync(h => h.CompanyId == company.Id && !h.IsDeleted); if (existingCount > 0) return 0; // Load all job status lookups into a code → id map var statusMap = await _context.Set() .IgnoreQueryFilters() .Where(s => s.CompanyId == company.Id) .ToDictionaryAsync(s => s.StatusCode, s => s.Id); // Load jobs with their current status var jobs = await _context.Set() .IgnoreQueryFilters() .Include(j => j.JobStatus) .Where(j => j.CompanyId == company.Id && !j.IsDeleted) .OrderBy(j => j.Id) .ToListAsync(); if (jobs.Count == 0 || statusMap.Count == 0) return 0; // Ordered pipeline — each status code in the order a job advances through it. // ON_HOLD and CANCELLED are terminal side-branches handled separately. var pipeline = new[] { "PENDING", "QUOTED", "APPROVED", "IN_PREPARATION", "SANDBLASTING", "MASKING_TAPING", "CLEANING", "IN_OVEN", "COATING", "CURING", "QUALITY_CHECK", "COMPLETED", "READY_FOR_PICKUP", "DELIVERED" }; var pipelineIndex = pipeline .Select((code, idx) => (code, idx)) .ToDictionary(t => t.code, t => t.idx); var history = new List(); var now = DateTime.UtcNow; foreach (var job in jobs) { var currentCode = job.JobStatus.StatusCode; // Determine the sequence of transitions that happened to reach current state. // For ON_HOLD: assume it came from QUALITY_CHECK before going on hold. // For CANCELLED: assume cancelled from APPROVED or IN_PREPARATION. string[] codesTraversed; if (currentCode == "ON_HOLD") { // Traversed up to QUALITY_CHECK then went ON_HOLD codesTraversed = [.. pipeline.Take(pipelineIndex["QUALITY_CHECK"] + 1), "ON_HOLD"]; } else if (currentCode == "CANCELLED") { // Cancelled from IN_PREPARATION codesTraversed = ["PENDING", "QUOTED", "APPROVED", "IN_PREPARATION", "CANCELLED"]; } else if (pipelineIndex.TryGetValue(currentCode, out int curIdx)) { // Normal pipeline job — traversed from PENDING up to current status codesTraversed = pipeline.Take(curIdx + 1).ToArray(); } else { // Unknown status — just record a single PENDING → currentCode entry codesTraversed = ["PENDING", currentCode]; } // Spread transition dates backwards from job.CreatedAt. // Each step took roughly 4–8 hours, so transitions are spaced a few hours apart. // Jobs further along in the pipeline have older start dates. var stepCount = codesTraversed.Length - 1; // number of transitions if (stepCount <= 0) continue; // Job was created at job.CreatedAt; each transition is spaced ~6h apart // so the first transition (PENDING→QUOTED) happened ~6h after creation, etc. for (int t = 0; t < stepCount; t++) { var fromCode = codesTraversed[t]; var toCode = codesTraversed[t + 1]; if (!statusMap.TryGetValue(fromCode, out int fromId)) continue; if (!statusMap.TryGetValue(toCode, out int toId)) continue; // Spread: first transitions happened closer to job creation, // later ones closer to now. Add a few hours per step. var hoursOffset = (t + 1) * 6; var changedDate = job.CreatedAt.AddHours(hoursOffset); // Don't let transitions exceed "now" if (changedDate > now) changedDate = now.AddMinutes(-(stepCount - t) * 10); history.Add(new JobStatusHistory { JobId = job.Id, FromStatusId = fromId, ToStatusId = toId, ChangedDate = changedDate, Notes = null, CompanyId = company.Id, CreatedAt = changedDate }); } } if (history.Count == 0) return 0; await _context.Set().AddRangeAsync(history); await _context.SaveChangesAsync(); return history.Count; } }