using Campaign_Tracker.Server.Seed.Models; namespace Campaign_Tracker.Server.Seed; public sealed class SeedService : ISeedService { internal const string ElectionCycleJobEntityType = "ElectionCycleJob"; internal const string ReadinessFeatureKey = "FR29.ReadinessStatus"; internal const string OverdueMilestoneScenario = "OverdueMilestoneAlert"; private static readonly string[] ExpectedReferenceSeedKeys = [ "operational-status.not-started", "operational-status.in-progress", "operational-status.blocked", "operational-status.complete", "service-template.addressing", "service-template.sorting", "service-template.transportation", "service-template.office-copy", "extension-reference.election-cycle.primary", "extension-reference.election-cycle.general", "extension-reference.mail-class.first-class", "extension-reference.mail-class.standard", ]; private static readonly string[] ExpectedRequiredFieldSeedKeys = [ "required-field.election-cycle-job.municipality-profile-id", "required-field.election-cycle-job.legacy-jurisdiction-j-code", "required-field.election-cycle-job.election-date", "required-field.election-cycle-job.mail-date", "required-field.election-cycle-job.service-template", ]; private static readonly string[] ExpectedEscalationSeedKeys = [ "escalation.overdue-milestone.operations-lead", ]; private readonly ISeedDataStore _store; private readonly TimeProvider _timeProvider; public SeedService(ISeedDataStore store, TimeProvider timeProvider) { _store = store; _timeProvider = timeProvider; } public Task SeedAsync(CancellationToken cancellationToken = default) => _store.UpsertSeedDataAsync(CreateDefaults(_timeProvider.GetUtcNow()), cancellationToken); public async Task IsSeededAsync(CancellationToken cancellationToken = default) { var referenceValues = await _store.GetReferenceValuesAsync(cancellationToken); var requiredRules = await _store.GetRequiredFieldRulesAsync(cancellationToken); var escalationRules = await _store.GetEscalationRulesAsync(cancellationToken); return ContainsAll(referenceValues.Select(value => value.SeedKey), ExpectedReferenceSeedKeys) && ContainsAll(requiredRules.Select(rule => rule.SeedKey), ExpectedRequiredFieldSeedKeys) && ContainsAll(escalationRules.Select(rule => rule.SeedKey), ExpectedEscalationSeedKeys); } private static SeedDataSet CreateDefaults(DateTimeOffset now) => new() { ReferenceValues = [ Reference("operational-status.not-started", "OperationalStatus", "Not Started", "not-started", "Election-cycle job work has not started.", now), Reference("operational-status.in-progress", "OperationalStatus", "In Progress", "in-progress", "Election-cycle job work is actively in progress.", now), Reference("operational-status.blocked", "OperationalStatus", "Blocked", "blocked", "Election-cycle job work is blocked and needs intervention.", now), Reference("operational-status.complete", "OperationalStatus", "Complete", "complete", "Election-cycle job work is complete.", now), Reference("service-template.addressing", "ServiceTemplate", "Addressing", "addressing", "Default service template for addressing work.", now), Reference("service-template.sorting", "ServiceTemplate", "Sorting", "sorting", "Default service template for sorting work.", now), Reference("service-template.transportation", "ServiceTemplate", "Transportation", "transportation", "Default service template for transportation work.", now), Reference("service-template.office-copy", "ServiceTemplate", "Office Copy", "office-copy", "Default service template for office-copy work.", now), Reference("extension-reference.election-cycle.primary", "ElectionCycleType", "Primary", "primary", "Extension-layer election-cycle reference value for primary elections.", now), Reference("extension-reference.election-cycle.general", "ElectionCycleType", "General", "general", "Extension-layer election-cycle reference value for general elections.", now), Reference("extension-reference.mail-class.first-class", "MailClass", "First Class", "first-class", "Extension-layer mail-class reference value.", now), Reference("extension-reference.mail-class.standard", "MailClass", "Standard", "standard", "Extension-layer mail-class reference value.", now), ], RequiredFieldRules = [ RequiredField("required-field.election-cycle-job.municipality-profile-id", "Municipality Profile", "municipalityProfileId", "Election-cycle jobs must be linked to a municipality profile.", now), RequiredField("required-field.election-cycle-job.legacy-jurisdiction-j-code", "Legacy Jurisdiction Code", "legacyJurisdictionJCode", "Election-cycle jobs must keep the legacy jurisdiction bridge required by Story 1.8.", now), RequiredField("required-field.election-cycle-job.election-date", "Election Date", "electionDate", "Election-cycle jobs need an election date before readiness can pass.", now), RequiredField("required-field.election-cycle-job.mail-date", "Mail Date", "mailDate", "Election-cycle jobs need a planned mail date before readiness can pass.", now), RequiredField("required-field.election-cycle-job.service-template", "Service Template", "serviceTemplate", "Election-cycle jobs need a selected service template before readiness can pass.", now), ], EscalationRules = [ new() { SeedKey = "escalation.overdue-milestone.operations-lead", Name = "Overdue Milestone Operations Lead Alert", Description = "Escalates election-cycle jobs whose active milestone is overdue.", Scenario = OverdueMilestoneScenario, TriggerCondition = "activeMilestone.dueDate < today && job.status != 'complete'", Action = "NotifyOperationsLead", MilestoneBasis = "activeMilestone.dueDate", AlertWindow = TimeSpan.Zero, Priority = 1, Source = SeedRecordSource.SystemSeed, IsActive = true, CreatedAt = now, UpdatedAt = now, }, ], }; private static ReferenceValue Reference( string seedKey, string category, string name, string value, string description, DateTimeOffset now) => new() { SeedKey = seedKey, Category = category, Name = name, Value = value, Description = description, Source = SeedRecordSource.SystemSeed, IsActive = true, CreatedAt = now, UpdatedAt = now, }; private static RequiredFieldRule RequiredField( string seedKey, string name, string fieldPath, string description, DateTimeOffset now) => new() { SeedKey = seedKey, Name = name, Description = description, EntityType = ElectionCycleJobEntityType, FieldPath = fieldPath, ReadinessFeatureKey = ReadinessFeatureKey, IsRequired = true, Source = SeedRecordSource.SystemSeed, IsActive = true, CreatedAt = now, UpdatedAt = now, }; private static bool ContainsAll(IEnumerable actual, IEnumerable expected) { var actualSet = actual.ToHashSet(StringComparer.OrdinalIgnoreCase); return expected.All(actualSet.Contains); } }