diff --git a/SeaHaven.DataServices/Implementation/WorkOrderDomainJobDataService.cs b/SeaHaven.DataServices/Implementation/WorkOrderDomainJobDataService.cs index 2d8433a..65d98bc 100644 --- a/SeaHaven.DataServices/Implementation/WorkOrderDomainJobDataService.cs +++ b/SeaHaven.DataServices/Implementation/WorkOrderDomainJobDataService.cs @@ -24,12 +24,13 @@ namespace SeaHaven.DataServices.Implementation DateOnly sourceWeekStart, DateOnly sourceWeekEnd, int batchSize = 500, - int skip = 0, CancellationToken cancellationToken = default) { var weekStartDate = sourceWeekStart.ToDateTime(TimeOnly.MinValue); var weekEndDate = sourceWeekEnd.ToDateTime(TimeOnly.MinValue); + // Always return the first page of remaining unprocessed candidates. + // Callers must not offset-page: ledger writes shrink this set after each batch. return await _context.workOrders .AsNoTracking() .Where(w => w.IsDeleted != true) @@ -42,7 +43,6 @@ namespace SeaHaven.DataServices.Implementation .Where(w => !_context.WorkOrderWeekRolledLedgers.Any(l => l.WorkOrderId == w.Id && l.SourceWeekStart == sourceWeekStart)) .OrderBy(w => w.Id) - .Skip(skip) .Take(batchSize) .Select(w => w.Id) .ToListAsync(cancellationToken); diff --git a/SeaHaven.DataServices/Interfaces/IWorkOrderDomainJobDataService.cs b/SeaHaven.DataServices/Interfaces/IWorkOrderDomainJobDataService.cs index 4c6af01..5fac4ba 100644 --- a/SeaHaven.DataServices/Interfaces/IWorkOrderDomainJobDataService.cs +++ b/SeaHaven.DataServices/Interfaces/IWorkOrderDomainJobDataService.cs @@ -21,7 +21,6 @@ namespace SeaHaven.DataServices.Interfaces DateOnly sourceWeekStart, DateOnly sourceWeekEnd, int batchSize = 500, - int skip = 0, CancellationToken cancellationToken = default); Task TryProcessWeekRolledAsync( diff --git a/SeaHaven.Services/Implementation/WorkOrderWeekRolledService.cs b/SeaHaven.Services/Implementation/WorkOrderWeekRolledService.cs index bf82574..ec4eebf 100644 --- a/SeaHaven.Services/Implementation/WorkOrderWeekRolledService.cs +++ b/SeaHaven.Services/Implementation/WorkOrderWeekRolledService.cs @@ -36,16 +36,17 @@ namespace SeaHaven.Services.Implementation var processed = 0; var skipped = 0; var failed = 0; - var skip = 0; _logger.LogInformation( "WeekRolled job started. CorrelationId={CorrelationId}, SourceWeekStart={SourceWeekStart}, SourceWeekEnd={SourceWeekEnd}", correlationId, sourceWeekStart, sourceWeekEnd); + // Always fetch the first page of remaining candidates. Processed rows write ledger + // entries that exclude them from the next query; offset paging would skip unprocessed rows. while (!cancellationToken.IsCancellationRequested) { var candidates = await _dataService.GetWeekRolledCandidatesAsync( - sourceWeekStart, sourceWeekEnd, BatchSize, skip, cancellationToken); + sourceWeekStart, sourceWeekEnd, BatchSize, cancellationToken); if (candidates.Count == 0) break; @@ -84,8 +85,6 @@ namespace SeaHaven.Services.Implementation if (candidates.Count < BatchSize) break; - - skip += BatchSize; } stopwatch.Stop(); diff --git a/SeaHavenIndustries.Tests/WorkOrderWeekRolledTests.cs b/SeaHavenIndustries.Tests/WorkOrderWeekRolledTests.cs index 9627581..4aef509 100644 --- a/SeaHavenIndustries.Tests/WorkOrderWeekRolledTests.cs +++ b/SeaHavenIndustries.Tests/WorkOrderWeekRolledTests.cs @@ -193,6 +193,42 @@ public class WorkOrderWeekRolledTests Assert.Equal(1, wo!.CarriedOver); } + [Fact] + public async Task WeekRolled_ProcessesAllEligible_WhenMoreThanBatchSize() + { + // WorkOrderWeekRolledService.BatchSize is 500; more than one page must all be carried over. + const int eligibleCount = 501; + + await using var context = CreateContext(); + for (var i = 1; i <= eligibleCount; i++) + { + context.workOrders.Add(new WorkOrder + { + Id = i, + ScheduledDate = new DateTime(2026, 6, 18), + LifecycleStatus = LifecycleStatus.Scheduled, + CarriedOver = 0 + }); + } + await context.SaveChangesAsync(); + + var service = CreateService(context); + var result = await service.ProcessWeekRolledAsync(SourceWeekStart); + + Assert.Equal(eligibleCount, result.Processed); + Assert.Equal(0, result.Skipped); + Assert.Equal(0, result.Failed); + + var carriedOverCount = await context.workOrders.CountAsync(w => w.CarriedOver == 1); + Assert.Equal(eligibleCount, carriedOverCount); + + var ledgerCount = await context.WorkOrderWeekRolledLedgers.CountAsync(); + Assert.Equal(eligibleCount, ledgerCount); + + var auditCount = await context.WorkOrderAuditLogs.CountAsync(l => l.Action == "WeekRolled"); + Assert.Equal(eligibleCount, auditCount); + } + [Fact] public async Task PastDueCache_SyncsOperationalFlags() {