using System.Text.Json; using Microsoft.EntityFrameworkCore; using JobTrackerApi.Data; namespace JobTrackerApi.Services { public sealed class DailyExportHostedService : BackgroundService { private readonly IServiceProvider _sp; private readonly ILogger _logger; private readonly IConfiguration _cfg; private readonly AppPaths _paths; private readonly IStartupReadiness _startupReadiness; public DailyExportHostedService( IServiceProvider sp, ILogger logger, IConfiguration cfg, AppPaths paths, IStartupReadiness startupReadiness) { _sp = sp; _logger = logger; _cfg = cfg; _paths = paths; _startupReadiness = startupReadiness; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { var enabled = _cfg.GetValue("Exports:DailyEnabled", true); await _startupReadiness.WaitUntilReadyAsync(stoppingToken); if (!enabled) { _logger.LogInformation("Daily export disabled (Exports:DailyEnabled=false)."); return; } var hour = _cfg.GetValue("Exports:DailyHourLocal", 2); if (hour < 0 || hour > 23) hour = 2; while (!stoppingToken.IsCancellationRequested) { var now = DateTime.Now; var next = new DateTime(now.Year, now.Month, now.Day, hour, 0, 0); if (next <= now) next = next.AddDays(1); var delay = next - now; _logger.LogInformation("Next daily export scheduled at {Next}.", next); try { await Task.Delay(delay, stoppingToken); } catch (TaskCanceledException) { break; } try { await RunExport(stoppingToken); } catch (Exception ex) { _logger.LogError(ex, "Daily export failed."); } } } private async Task RunExport(CancellationToken ct) { var folder = _paths.GetExportsRoot(_cfg["Exports:DailyFolder"]); Directory.CreateDirectory(folder); using var scope = _sp.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); var rules = await db.RuleSettings.AsNoTracking().FirstOrDefaultAsync(ct); var owners = await db.JobApplications .AsNoTracking() .OrderByDescending(job => job.DateApplied) .Select(job => job.OwnerUserId) .Distinct() .ToListAsync(ct); if (owners.Count <= 1) { var companies = await db.Companies.AsNoTracking().OrderBy(c => c.Name).ToListAsync(ct); var jobs = await db.JobApplications.AsNoTracking().OrderByDescending(j => j.DateApplied).ToListAsync(ct); var correspondence = await db.Correspondences.AsNoTracking().OrderBy(c => c.Date).ToListAsync(ct); var attachments = await db.Attachments.AsNoTracking().OrderBy(a => a.UploadDate).ToListAsync(ct); var events = await db.JobEvents.AsNoTracking().OrderBy(e => e.At).ToListAsync(ct); var export = new { Version = "dailyexport.v1", CreatedAt = DateTime.Now, Companies = companies, JobApplications = jobs, Correspondence = correspondence, Attachments = attachments, Events = events, Rules = rules }; var json = JsonSerializer.Serialize(export, new JsonSerializerOptions { WriteIndented = true }); var file = Path.Combine(folder, $"daily_export_{DateTime.Now:yyyyMMdd}.json"); await File.WriteAllTextAsync(file, json, ct); _logger.LogInformation("Daily export written: {File}.", file); return; } foreach (var owner in owners) { var ownerKey = string.IsNullOrWhiteSpace(owner) ? "_unassigned" : owner; var ownerJobs = await db.JobApplications .AsNoTracking() .Where(job => job.OwnerUserId == owner) .OrderByDescending(job => job.DateApplied) .ToListAsync(ct); var ownerJobIds = ownerJobs.Select(job => job.Id).ToList(); var export = new { Version = "dailyexport.v2", CreatedAt = DateTime.Now, OwnerUserId = owner, Companies = await db.Companies.AsNoTracking().Where(company => company.OwnerUserId == owner).OrderBy(company => company.Name).ToListAsync(ct), JobApplications = ownerJobs, Correspondence = await db.Correspondences.AsNoTracking().Where(message => ownerJobIds.Contains(message.JobApplicationId)).OrderBy(message => message.Date).ToListAsync(ct), Attachments = await db.Attachments.AsNoTracking().Where(attachment => ownerJobIds.Contains(attachment.JobApplicationId)).OrderBy(attachment => attachment.UploadDate).ToListAsync(ct), Events = await db.JobEvents.AsNoTracking().Where(jobEvent => ownerJobIds.Contains(jobEvent.JobApplicationId)).OrderBy(jobEvent => jobEvent.At).ToListAsync(ct), Rules = rules }; var json = JsonSerializer.Serialize(export, new JsonSerializerOptions { WriteIndented = true }); var file = Path.Combine(folder, $"daily_export_{ownerKey}_{DateTime.Now:yyyyMMdd}.json"); await File.WriteAllTextAsync(file, json, ct); _logger.LogInformation("Daily export written: {File}.", file); } } } }