diff --git a/src/PowderCoating.Web/BackgroundServices/ColumbiaCatalogSyncBackgroundService.cs b/src/PowderCoating.Web/BackgroundServices/ColumbiaCatalogSyncBackgroundService.cs new file mode 100644 index 0000000..0e8a9ae --- /dev/null +++ b/src/PowderCoating.Web/BackgroundServices/ColumbiaCatalogSyncBackgroundService.cs @@ -0,0 +1,112 @@ +using System.Globalization; +using PowderCoating.Application.Constants; +using PowderCoating.Application.Interfaces; + +namespace PowderCoating.Web.BackgroundServices; + +/// +/// Runs the Columbia Coatings catalog sync on a schedule. Wakes hourly and triggers a full sync +/// only when the master switch (ColumbiaSyncEnabled) is on and the configured interval +/// (ColumbiaSyncIntervalDays) has elapsed since the last successful run. A full sync is +/// cheap (~25 API calls), so an hourly due-check is negligible; the actual work runs at most once +/// per interval. No-ops quietly when disabled or unconfigured. +/// +public class ColumbiaCatalogSyncBackgroundService : BackgroundService +{ + private readonly IServiceScopeFactory _scopeFactory; + private readonly ILogger _logger; + + private static readonly TimeSpan CheckInterval = TimeSpan.FromHours(1); + private static readonly TimeSpan StartupDelay = TimeSpan.FromMinutes(2); + + /// + /// Uses because a is a + /// singleton and the sync service / platform settings are scoped. + /// + public ColumbiaCatalogSyncBackgroundService( + IServiceScopeFactory scopeFactory, + ILogger logger) + { + _scopeFactory = scopeFactory; + _logger = logger; + } + + /// + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + _logger.LogInformation("ColumbiaCatalogSyncBackgroundService started."); + + try + { + await Task.Delay(StartupDelay, stoppingToken); + + while (!stoppingToken.IsCancellationRequested) + { + await RunIfDueAsync(stoppingToken); + await Task.Delay(CheckInterval, stoppingToken); + } + } + catch (OperationCanceledException) + { + // Shutting down — expected. + } + } + + /// + /// Checks the enable switch and the elapsed interval, and runs a sync when due. Failures from + /// the sync itself are reported on its result (and recorded in platform settings) rather than + /// thrown, so a bad run never tears down the loop. + /// + private async Task RunIfDueAsync(CancellationToken ct) + { + using var scope = _scopeFactory.CreateScope(); + var settings = scope.ServiceProvider.GetRequiredService(); + + if (!await settings.GetBoolAsync(ColumbiaIntegrationConstants.SettingEnabled)) + return; // master switch off + + var intervalDays = Math.Max(1, await settings.GetIntAsync( + ColumbiaIntegrationConstants.SettingIntervalDays, + ColumbiaIntegrationConstants.DefaultSyncIntervalDays)); + + if (!IsDue(await settings.GetAsync(ColumbiaIntegrationConstants.SettingLastSyncedAt), intervalDays)) + return; // synced recently enough + + var sync = scope.ServiceProvider.GetRequiredService(); + + try + { + _logger.LogInformation("Columbia scheduled sync starting (interval {Days}d).", intervalDays); + var result = await sync.RunSyncAsync(ct); + + if (result.Success) + _logger.LogInformation("Columbia scheduled sync complete: {Summary}", result.Summary); + else + _logger.LogWarning("Columbia scheduled sync did not succeed: {Error}", result.ErrorMessage); + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) + { + _logger.LogError(ex, "Columbia scheduled sync threw unexpectedly."); + } + } + + /// + /// A sync is due when there is no recorded last-sync timestamp, or the configured number of + /// days has elapsed since it. An unparseable timestamp is treated as "due". + /// + private static bool IsDue(string? lastSyncedRaw, int intervalDays) + { + if (string.IsNullOrWhiteSpace(lastSyncedRaw)) + return true; + + if (!DateTime.TryParse(lastSyncedRaw, CultureInfo.InvariantCulture, + DateTimeStyles.RoundtripKind, out var lastSynced)) + return true; + + return DateTime.UtcNow - lastSynced.ToUniversalTime() >= TimeSpan.FromDays(intervalDays); + } +} diff --git a/src/PowderCoating.Web/Program.cs b/src/PowderCoating.Web/Program.cs index 2a8db78..63e73f2 100644 --- a/src/PowderCoating.Web/Program.cs +++ b/src/PowderCoating.Web/Program.cs @@ -259,6 +259,7 @@ builder.Services.AddHostedService(); builder.Services.AddHostedService(); builder.Services.AddHostedService(); builder.Services.AddHostedService(); +builder.Services.AddHostedService(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped();