Add scheduled Columbia catalog sync background service
Phase 4: automation. ColumbiaCatalogSyncBackgroundService wakes hourly and runs a full sync only when ColumbiaSyncEnabled is on and ColumbiaSyncIntervalDays has elapsed since the last successful run (tracked via the ColumbiaLastSyncedAt setting). No-ops quietly when disabled or unconfigured. The hourly due-check is negligible; the actual sync runs at most once per interval. Sync failures are recorded on the result/settings, never thrown, so a bad run can't kill the loop. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,112 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using PowderCoating.Application.Constants;
|
||||||
|
using PowderCoating.Application.Interfaces;
|
||||||
|
|
||||||
|
namespace PowderCoating.Web.BackgroundServices;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Runs the Columbia Coatings catalog sync on a schedule. Wakes hourly and triggers a full sync
|
||||||
|
/// only when the master switch (<c>ColumbiaSyncEnabled</c>) is on and the configured interval
|
||||||
|
/// (<c>ColumbiaSyncIntervalDays</c>) 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.
|
||||||
|
/// </summary>
|
||||||
|
public class ColumbiaCatalogSyncBackgroundService : BackgroundService
|
||||||
|
{
|
||||||
|
private readonly IServiceScopeFactory _scopeFactory;
|
||||||
|
private readonly ILogger<ColumbiaCatalogSyncBackgroundService> _logger;
|
||||||
|
|
||||||
|
private static readonly TimeSpan CheckInterval = TimeSpan.FromHours(1);
|
||||||
|
private static readonly TimeSpan StartupDelay = TimeSpan.FromMinutes(2);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Uses <see cref="IServiceScopeFactory"/> because a <see cref="BackgroundService"/> is a
|
||||||
|
/// singleton and the sync service / platform settings are scoped.
|
||||||
|
/// </summary>
|
||||||
|
public ColumbiaCatalogSyncBackgroundService(
|
||||||
|
IServiceScopeFactory scopeFactory,
|
||||||
|
ILogger<ColumbiaCatalogSyncBackgroundService> logger)
|
||||||
|
{
|
||||||
|
_scopeFactory = scopeFactory;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
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.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
private async Task RunIfDueAsync(CancellationToken ct)
|
||||||
|
{
|
||||||
|
using var scope = _scopeFactory.CreateScope();
|
||||||
|
var settings = scope.ServiceProvider.GetRequiredService<IPlatformSettingsService>();
|
||||||
|
|
||||||
|
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<IColumbiaCatalogSyncService>();
|
||||||
|
|
||||||
|
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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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".
|
||||||
|
/// </summary>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -259,6 +259,7 @@ builder.Services.AddHostedService<StripeWebhookRetentionBackgroundService>();
|
|||||||
builder.Services.AddHostedService<SetupWizardReminderBackgroundService>();
|
builder.Services.AddHostedService<SetupWizardReminderBackgroundService>();
|
||||||
builder.Services.AddHostedService<RecurringTransactionService>();
|
builder.Services.AddHostedService<RecurringTransactionService>();
|
||||||
builder.Services.AddHostedService<AppointmentReminderBackgroundService>();
|
builder.Services.AddHostedService<AppointmentReminderBackgroundService>();
|
||||||
|
builder.Services.AddHostedService<ColumbiaCatalogSyncBackgroundService>();
|
||||||
builder.Services.AddScoped<ISubscriptionService, SubscriptionService>();
|
builder.Services.AddScoped<ISubscriptionService, SubscriptionService>();
|
||||||
builder.Services.AddScoped<IStripeService, StripeService>();
|
builder.Services.AddScoped<IStripeService, StripeService>();
|
||||||
builder.Services.AddScoped<IStripeConnectService, StripeConnectService>();
|
builder.Services.AddScoped<IStripeConnectService, StripeConnectService>();
|
||||||
|
|||||||
Reference in New Issue
Block a user