feat/Update_Controllers_to_Allow_for_Premium_Membership
This commit is contained in:
@@ -1,145 +1,82 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using JobTrackerApi.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace JobTrackerApi.Services
|
||||
namespace JobTrackerApi.Services;
|
||||
|
||||
public sealed class DailyExportHostedService(
|
||||
BackgroundTenantRunner tenants,
|
||||
ILogger<DailyExportHostedService> logger,
|
||||
IConfiguration configuration,
|
||||
AppPaths paths,
|
||||
IStartupReadiness startupReadiness) : BackgroundService
|
||||
{
|
||||
public sealed class DailyExportHostedService : BackgroundService
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
private readonly IServiceProvider _sp;
|
||||
private readonly ILogger<DailyExportHostedService> _logger;
|
||||
private readonly IConfiguration _cfg;
|
||||
private readonly AppPaths _paths;
|
||||
private readonly IStartupReadiness _startupReadiness;
|
||||
|
||||
public DailyExportHostedService(
|
||||
IServiceProvider sp,
|
||||
ILogger<DailyExportHostedService> logger,
|
||||
IConfiguration cfg,
|
||||
AppPaths paths,
|
||||
IStartupReadiness startupReadiness)
|
||||
await startupReadiness.WaitUntilReadyAsync(stoppingToken);
|
||||
if (!IsEnabled())
|
||||
{
|
||||
_sp = sp;
|
||||
_logger = logger;
|
||||
_cfg = cfg;
|
||||
_paths = paths;
|
||||
_startupReadiness = startupReadiness;
|
||||
logger.LogInformation("Daily export worker disabled; both Workers:DailyExportEnabled and Exports:DailyEnabled must be true.");
|
||||
return;
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
var hour = configuration.GetValue("Exports:DailyHourLocal", 2);
|
||||
if (hour is < 0 or > 23) hour = 2;
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
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.");
|
||||
}
|
||||
}
|
||||
var now = DateTime.Now;
|
||||
var next = new DateTime(now.Year, now.Month, now.Day, hour, 0, 0);
|
||||
if (next <= now) next = next.AddDays(1);
|
||||
logger.LogInformation("Next daily export scheduled at {Next}.", next);
|
||||
await Task.Delay(next - now, stoppingToken);
|
||||
await RunOnceAsync(stoppingToken);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RunExport(CancellationToken ct)
|
||||
public Task<BackgroundWorkerRunResult> RunOnceAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (!IsEnabled()) return Task.FromResult(BackgroundWorkerRunResult.Disabled);
|
||||
return tenants.RunForJobOwnersAsync("daily-export", ExportOwnerAsync, cancellationToken);
|
||||
}
|
||||
|
||||
private bool IsEnabled() =>
|
||||
configuration.GetValue("Workers:DailyExportEnabled", false) &&
|
||||
configuration.GetValue("Exports:DailyEnabled", true);
|
||||
|
||||
private async Task ExportOwnerAsync(IServiceProvider services, CancellationToken cancellationToken)
|
||||
{
|
||||
var db = services.GetRequiredService<JobTrackerContext>();
|
||||
var owner = db.CurrentUserId ?? throw new InvalidOperationException("Daily export requires an explicit owner scope.");
|
||||
var jobs = await db.JobApplications.AsNoTracking().OrderByDescending(job => job.DateApplied).ToListAsync(cancellationToken);
|
||||
var jobIds = jobs.Select(job => job.Id).ToList();
|
||||
var export = new
|
||||
{
|
||||
var folder = _paths.GetExportsRoot(_cfg["Exports:DailyFolder"]);
|
||||
Version = "dailyexport.v2",
|
||||
CreatedAt = DateTime.Now,
|
||||
OwnerUserId = owner,
|
||||
Companies = await db.Companies.AsNoTracking().OrderBy(company => company.Name).ToListAsync(cancellationToken),
|
||||
JobApplications = jobs,
|
||||
Correspondence = await db.Correspondences.AsNoTracking().Where(message => jobIds.Contains(message.JobApplicationId)).OrderBy(message => message.Date).ToListAsync(cancellationToken),
|
||||
Attachments = await db.Attachments.AsNoTracking().Where(attachment => jobIds.Contains(attachment.JobApplicationId)).OrderBy(attachment => attachment.UploadDate).ToListAsync(cancellationToken),
|
||||
Events = await db.JobEvents.AsNoTracking().Where(jobEvent => jobIds.Contains(jobEvent.JobApplicationId)).OrderBy(jobEvent => jobEvent.At).ToListAsync(cancellationToken),
|
||||
Rules = await RulesEngine.GetSettings(db, cancellationToken),
|
||||
};
|
||||
|
||||
Directory.CreateDirectory(folder);
|
||||
|
||||
using var scope = _sp.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<JobTrackerContext>();
|
||||
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);
|
||||
}
|
||||
var folder = paths.GetExportsRoot(configuration["Exports:DailyFolder"]);
|
||||
Directory.CreateDirectory(folder);
|
||||
var ownerKey = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(owner))).ToLowerInvariant();
|
||||
var finalPath = Path.Combine(folder, $"daily_export_{ownerKey}_{DateTime.Now:yyyyMMdd}.json");
|
||||
var temporaryPath = finalPath + $".{Guid.NewGuid():N}.tmp";
|
||||
try
|
||||
{
|
||||
await File.WriteAllTextAsync(temporaryPath, JsonSerializer.Serialize(export, new JsonSerializerOptions { WriteIndented = true }), cancellationToken);
|
||||
File.Move(temporaryPath, finalPath, overwrite: true);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (File.Exists(temporaryPath)) File.Delete(temporaryPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user