87 lines
4.0 KiB
C#
87 lines
4.0 KiB
C#
using JobTrackerApi.Data;
|
||
using JobTrackerApi.Models;
|
||
using Microsoft.EntityFrameworkCore;
|
||
|
||
namespace JobTrackerApi.Services;
|
||
|
||
public sealed class SavedJobSearchAlertHostedService(
|
||
BackgroundTenantRunner tenants,
|
||
IConfiguration configuration,
|
||
ILogger<SavedJobSearchAlertHostedService> logger,
|
||
IStartupReadiness startupReadiness,
|
||
TimeProvider timeProvider) : BackgroundService
|
||
{
|
||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||
{
|
||
await startupReadiness.WaitUntilReadyAsync(stoppingToken);
|
||
if (!IsEnabled())
|
||
{
|
||
logger.LogInformation("Saved job search alert worker disabled.");
|
||
return;
|
||
}
|
||
|
||
await Task.Delay(TimeSpan.FromSeconds(30), timeProvider, stoppingToken);
|
||
while (!stoppingToken.IsCancellationRequested)
|
||
{
|
||
await RunOnceAsync(stoppingToken);
|
||
await Task.Delay(GetInterval(), timeProvider, stoppingToken);
|
||
}
|
||
}
|
||
|
||
public Task<BackgroundWorkerRunResult> RunOnceAsync(CancellationToken cancellationToken) =>
|
||
IsEnabled()
|
||
? tenants.RunForSavedSearchOwnersAsync("saved-job-search-alerts", ProcessOwnerAsync, cancellationToken)
|
||
: Task.FromResult(BackgroundWorkerRunResult.Disabled);
|
||
|
||
private bool IsEnabled() => configuration.GetValue("Workers:SavedJobSearchAlertsEnabled", true);
|
||
|
||
private TimeSpan GetInterval() => TimeSpan.FromHours(Math.Clamp(
|
||
configuration.GetValue("JobDiscovery:SavedSearchIntervalHours", 6), 1, 24));
|
||
|
||
private async Task ProcessOwnerAsync(IServiceProvider services, CancellationToken cancellationToken)
|
||
{
|
||
var db = services.GetRequiredService<JobTrackerContext>();
|
||
var runner = services.GetRequiredService<SavedJobSearchRunner>();
|
||
var now = timeProvider.GetUtcNow();
|
||
var dueBefore = now - GetInterval();
|
||
var useNorwegian = string.Equals(
|
||
await db.Users.AsNoTracking().Where(user => user.Id == db.CurrentUserId).Select(user => user.UiLanguage).FirstOrDefaultAsync(cancellationToken),
|
||
"nb",
|
||
StringComparison.OrdinalIgnoreCase);
|
||
var searches = await db.SavedJobSearches.Include(search => search.Results)
|
||
.Where(search => search.IsActive)
|
||
.OrderBy(search => search.Id)
|
||
.ToListAsync(cancellationToken);
|
||
|
||
foreach (var search in searches.Where(search => search.LastRunAtUtc is null || search.LastRunAtUtc <= dueBefore))
|
||
{
|
||
var result = await runner.RunAsync(search, cancellationToken);
|
||
// The first automatic run establishes a baseline. Alert only for vacancies
|
||
// that appear after that baseline so existing results never look "new".
|
||
if (!result.WasInitialRun && result.NewCount > 0)
|
||
{
|
||
db.UserNotifications.Add(new UserNotification
|
||
{
|
||
Id = Guid.NewGuid(),
|
||
OwnerUserId = search.OwnerUserId,
|
||
Kind = "saved_search_results",
|
||
Title = useNorwegian
|
||
? result.NewCount == 1 ? "Ny stilling funnet" : $"{result.NewCount} nye stillinger funnet"
|
||
: result.NewCount == 1 ? "New vacancy found" : $"{result.NewCount} new vacancies found",
|
||
Message = useNorwegian
|
||
? result.NewCount == 1
|
||
? $"Det lagrede søket «{search.Name}» fant en ny stilling."
|
||
: $"Det lagrede søket «{search.Name}» fant {result.NewCount} nye stillinger."
|
||
: result.NewCount == 1
|
||
? $"Your saved search ‘{search.Name}’ found a new vacancy."
|
||
: $"Your saved search ‘{search.Name}’ found {result.NewCount} new vacancies.",
|
||
LinkPath = "/discover",
|
||
CreatedAtUtc = now.UtcDateTime
|
||
});
|
||
}
|
||
}
|
||
|
||
await db.SaveChangesAsync(cancellationToken);
|
||
}
|
||
}
|