fix(work-orders): process all WeekRolled candidates across batches

Offset paging skipped remaining WOs after ledger writes; always refetch the first page of unprocessed candidates and cover with a >BatchSize test.
This commit is contained in:
Arthur Bassi 2026-07-10 11:01:52 -03:00
parent 8c33eb7f9a
commit f977483c17
4 changed files with 41 additions and 7 deletions

View file

@ -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);

View file

@ -21,7 +21,6 @@ namespace SeaHaven.DataServices.Interfaces
DateOnly sourceWeekStart,
DateOnly sourceWeekEnd,
int batchSize = 500,
int skip = 0,
CancellationToken cancellationToken = default);
Task<WeekRolledProcessOutcome> TryProcessWeekRolledAsync(

View file

@ -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();

View file

@ -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()
{