Initial commit: JDE Scoping Tool migration project
Set up repository with legacy .NET Framework 4.8 source (OLD/), new .NET 10 Blazor solution (NEW/), OpenSpec specifications, documentation, and project configuration.
This commit is contained in:
@@ -0,0 +1,247 @@
|
||||
using JdeScoping.Core.Models;
|
||||
using JdeScoping.Core.Models.Enums;
|
||||
using JdeScoping.Core.Models.Infrastructure;
|
||||
using JdeScoping.DataSync.Configuration;
|
||||
using JdeScoping.DataSync.Contracts;
|
||||
using JdeScoping.DataSync.Models;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace JdeScoping.DataSync.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Checks schedules and determines which sync tasks need to be executed.
|
||||
/// </summary>
|
||||
public class ScheduleChecker : IScheduleChecker
|
||||
{
|
||||
private readonly IDataUpdateRepository _repository;
|
||||
private readonly IOptions<DataSyncOptions> _options;
|
||||
private readonly ILogger<ScheduleChecker> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ScheduleChecker"/> class.
|
||||
/// </summary>
|
||||
public ScheduleChecker(
|
||||
IDataUpdateRepository repository,
|
||||
IOptions<DataSyncOptions> options,
|
||||
ILogger<ScheduleChecker> logger)
|
||||
{
|
||||
_repository = repository ?? throw new ArgumentNullException(nameof(repository));
|
||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<List<DataUpdateTask>> GetPendingTasksAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var lastUpdates = await _repository.GetLastDataUpdatesAsync(cancellationToken);
|
||||
var tasks = new List<DataUpdateTask>();
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
foreach (var config in _options.Value.DataSources.Where(c => c.IsEnabled))
|
||||
{
|
||||
var task = CheckConfigSchedule(config, lastUpdates, now);
|
||||
if (task != null)
|
||||
{
|
||||
tasks.Add(task);
|
||||
}
|
||||
}
|
||||
|
||||
if (tasks.Count > 0)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Found {Count} pending sync tasks: {Tasks}",
|
||||
tasks.Count,
|
||||
string.Join(", ", tasks.Select(t => $"{t.TableName}({t.UpdateType})")));
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogDebug("No pending sync tasks found");
|
||||
}
|
||||
|
||||
return tasks;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks a single data source config and returns a task if sync is needed.
|
||||
/// Priority order: Mass > Daily > Hourly
|
||||
/// </summary>
|
||||
private DataUpdateTask? CheckConfigSchedule(
|
||||
DataSourceConfig config,
|
||||
Dictionary<string, DataUpdate> lastUpdates,
|
||||
DateTime now)
|
||||
{
|
||||
// Get last updates for each type
|
||||
var massKey = GetUpdateKey(config.TableName, UpdateTypes.Mass);
|
||||
var dailyKey = GetUpdateKey(config.TableName, UpdateTypes.Daily);
|
||||
var hourlyKey = GetUpdateKey(config.TableName, UpdateTypes.Hourly);
|
||||
|
||||
lastUpdates.TryGetValue(massKey, out var lastMass);
|
||||
lastUpdates.TryGetValue(dailyKey, out var lastDaily);
|
||||
lastUpdates.TryGetValue(hourlyKey, out var lastHourly);
|
||||
|
||||
// Check Mass first (highest priority)
|
||||
if (config.MassConfig.Enabled && NeedsMassSync(config, lastMass, now))
|
||||
{
|
||||
_logger.LogDebug(
|
||||
"Mass sync needed for {Table}: last={LastSync}, interval={Interval}m",
|
||||
config.TableName,
|
||||
lastMass?.EndDt.ToString("o") ?? "never",
|
||||
config.MassConfig.IntervalMinutes);
|
||||
|
||||
return CreateTask(config, UpdateTypes.Mass, null);
|
||||
}
|
||||
|
||||
// Check Daily
|
||||
if (config.DailyConfig.Enabled && NeedsDailySync(config, lastDaily, lastMass, now))
|
||||
{
|
||||
var minimumDt = CalculateMinimumDt(lastDaily, config.DailyConfig.IntervalMinutes);
|
||||
|
||||
_logger.LogDebug(
|
||||
"Daily sync needed for {Table}: last={LastSync}, interval={Interval}m, minDT={MinDT}",
|
||||
config.TableName,
|
||||
lastDaily?.EndDt.ToString("o") ?? "never",
|
||||
config.DailyConfig.IntervalMinutes,
|
||||
minimumDt?.ToString("o") ?? "null");
|
||||
|
||||
return CreateTask(config, UpdateTypes.Daily, minimumDt);
|
||||
}
|
||||
|
||||
// Check Hourly (uses Daily's last timestamp for MinimumDT calculation, per legacy behavior)
|
||||
if (config.HourlyConfig.Enabled && NeedsHourlySync(config, lastHourly, lastDaily, lastMass, now))
|
||||
{
|
||||
// Use daily update timestamp for lookback, not hourly
|
||||
var minimumDt = CalculateMinimumDt(lastDaily, config.DailyConfig.IntervalMinutes);
|
||||
|
||||
_logger.LogDebug(
|
||||
"Hourly sync needed for {Table}: last={LastSync}, interval={Interval}m, minDT={MinDT}",
|
||||
config.TableName,
|
||||
lastHourly?.EndDt.ToString("o") ?? "never",
|
||||
config.HourlyConfig.IntervalMinutes,
|
||||
minimumDt?.ToString("o") ?? "null");
|
||||
|
||||
return CreateTask(config, UpdateTypes.Hourly, minimumDt);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if a mass sync is needed.
|
||||
/// </summary>
|
||||
private bool NeedsMassSync(DataSourceConfig config, DataUpdate? lastMass, DateTime now)
|
||||
{
|
||||
// Never synced before - need mass sync
|
||||
if (lastMass == null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if successful last mass sync was within interval
|
||||
if (!lastMass.WasSuccessful)
|
||||
{
|
||||
// Last sync failed - try again
|
||||
return true;
|
||||
}
|
||||
|
||||
var nextSyncDue = lastMass.EndDt.AddMinutes(config.MassConfig.IntervalMinutes);
|
||||
return now > nextSyncDue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if a daily sync is needed.
|
||||
/// </summary>
|
||||
private bool NeedsDailySync(DataSourceConfig config, DataUpdate? lastDaily, DataUpdate? lastMass, DateTime now)
|
||||
{
|
||||
// If no mass sync ever happened, we need mass first
|
||||
if (lastMass == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Never done daily sync
|
||||
if (lastDaily == null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if successful last daily sync was within interval
|
||||
if (!lastDaily.WasSuccessful)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var nextSyncDue = lastDaily.EndDt.AddMinutes(config.DailyConfig.IntervalMinutes);
|
||||
return now > nextSyncDue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if an hourly sync is needed.
|
||||
/// </summary>
|
||||
private bool NeedsHourlySync(
|
||||
DataSourceConfig config,
|
||||
DataUpdate? lastHourly,
|
||||
DataUpdate? lastDaily,
|
||||
DataUpdate? lastMass,
|
||||
DateTime now)
|
||||
{
|
||||
// If no mass sync ever happened, we need mass first
|
||||
if (lastMass == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Never done hourly sync
|
||||
if (lastHourly == null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if successful last hourly sync was within interval
|
||||
if (!lastHourly.WasSuccessful)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var nextSyncDue = lastHourly.EndDt.AddMinutes(config.HourlyConfig.IntervalMinutes);
|
||||
return now > nextSyncDue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the MinimumDT for incremental updates using lookback multiplier.
|
||||
/// </summary>
|
||||
private DateTime? CalculateMinimumDt(DataUpdate? lastUpdate, int intervalMinutes)
|
||||
{
|
||||
if (lastUpdate == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var lookbackMinutes = _options.Value.LookbackMultiplier * intervalMinutes;
|
||||
return lastUpdate.EndDt.AddMinutes(-lookbackMinutes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a data update task.
|
||||
/// </summary>
|
||||
private static DataUpdateTask CreateTask(DataSourceConfig config, UpdateTypes updateType, DateTime? minimumDt)
|
||||
{
|
||||
return new DataUpdateTask
|
||||
{
|
||||
TableName = config.TableName,
|
||||
SourceSystem = config.SourceSystem,
|
||||
SourceData = config.SourceData,
|
||||
UpdateType = updateType,
|
||||
MinimumDt = minimumDt,
|
||||
Config = config
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the dictionary key for looking up last updates.
|
||||
/// </summary>
|
||||
private static string GetUpdateKey(string tableName, UpdateTypes updateType)
|
||||
{
|
||||
return $"{tableName}_{(int)updateType}";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user