163 lines
6.8 KiB
C#
163 lines
6.8 KiB
C#
using Microsoft.EntityFrameworkCore;
|
||
using PowderCoating.Core.Entities;
|
||
|
||
namespace PowderCoating.Infrastructure.Services;
|
||
|
||
public partial class SeedDataService
|
||
{
|
||
/// <summary>
|
||
/// 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.
|
||
/// </summary>
|
||
/// <remarks>
|
||
/// <para>
|
||
/// Idempotency: returns 0 immediately if any non-deleted history rows already exist for
|
||
/// this company.
|
||
/// </para>
|
||
/// <para>
|
||
/// The method does not record arbitrary transitions — it follows the canonical 14-step
|
||
/// pipeline array (<c>PENDING → QUOTED → APPROVED → … → DELIVERED</c>) and generates
|
||
/// one <see cref="JobStatusHistory"/> row per transition step, from <c>PENDING</c> up to
|
||
/// and including the job's current status.
|
||
/// </para>
|
||
/// <para>
|
||
/// Terminal side-branch statuses are handled explicitly:
|
||
/// <list type="bullet">
|
||
/// <item><c>ON_HOLD</c> — assumed to have reached <c>QUALITY_CHECK</c> before pausing.</item>
|
||
/// <item><c>CANCELLED</c> — assumed to have been cancelled from <c>IN_PREPARATION</c>.</item>
|
||
/// </list>
|
||
/// </para>
|
||
/// <para>
|
||
/// Transition timestamps are spread ~6 hours apart starting from <c>job.CreatedAt</c>.
|
||
/// 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 <c>DateTime.UtcNow</c>.
|
||
/// </para>
|
||
/// <para>
|
||
/// All history rows are batched into a single <c>AddRangeAsync / SaveChangesAsync</c>
|
||
/// call for performance, since the total count can be several hundred rows (50 jobs × up
|
||
/// to 14 transitions each).
|
||
/// </para>
|
||
/// </remarks>
|
||
/// <param name="company">The tenant company to seed job status history for.</param>
|
||
/// <returns>Total number of history rows inserted, or 0 if already seeded or no jobs exist.</returns>
|
||
private async Task<int> SeedJobStatusHistoryAsync(Company company)
|
||
{
|
||
var existingCount = await _context.Set<JobStatusHistory>()
|
||
.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<JobStatusLookup>()
|
||
.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<Job>()
|
||
.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<JobStatusHistory>();
|
||
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<JobStatusHistory>().AddRangeAsync(history);
|
||
await _context.SaveChangesAsync();
|
||
|
||
return history.Count;
|
||
}
|
||
}
|