mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 07:13:12 +00:00
Past Due must track the deadline (Due Date), not Schedule On. Keep dueDate and scheduledDate PATCH mutations independent so rescheduling alone does not clear Past Due.
166 lines
6.1 KiB
C#
166 lines
6.1 KiB
C#
using Data.SeaHavenIndustries;
|
|
using Data.SeaHavenIndustries.Enums;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using SeaHaven.DataServices.Interfaces;
|
|
|
|
namespace SeaHaven.DataServices.Implementation
|
|
{
|
|
public class WorkOrderDomainJobDataService : IWorkOrderDomainJobDataService
|
|
{
|
|
private static readonly LifecycleStatus?[] TerminalStatuses =
|
|
{
|
|
LifecycleStatus.Completed,
|
|
LifecycleStatus.Canceled
|
|
};
|
|
|
|
private readonly ApplicationDbContext _context;
|
|
|
|
public WorkOrderDomainJobDataService(ApplicationDbContext context)
|
|
{
|
|
_context = context;
|
|
}
|
|
|
|
public async Task<IReadOnlyList<int>> GetWeekRolledCandidatesAsync(
|
|
DateOnly sourceWeekStart,
|
|
DateOnly sourceWeekEnd,
|
|
int batchSize = 500,
|
|
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)
|
|
.Where(w => w.istemplate != true)
|
|
.Where(w => w.ScheduledDate != null)
|
|
.Where(w =>
|
|
w.ScheduledDate!.Value.Date >= weekStartDate.Date
|
|
&& w.ScheduledDate.Value.Date <= weekEndDate.Date)
|
|
.Where(w => !TerminalStatuses.Contains(w.LifecycleStatus))
|
|
.Where(w => !_context.WorkOrderWeekRolledLedgers.Any(l =>
|
|
l.WorkOrderId == w.Id && l.SourceWeekStart == sourceWeekStart))
|
|
.OrderBy(w => w.Id)
|
|
.Take(batchSize)
|
|
.Select(w => w.Id)
|
|
.ToListAsync(cancellationToken);
|
|
}
|
|
|
|
public async Task<WeekRolledProcessOutcome> TryProcessWeekRolledAsync(
|
|
int workOrderId,
|
|
DateOnly sourceWeekStart,
|
|
string correlationId,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
await using var transaction = await _context.Database.BeginTransactionAsync(cancellationToken);
|
|
|
|
var alreadyProcessed = await _context.WorkOrderWeekRolledLedgers
|
|
.AnyAsync(l => l.WorkOrderId == workOrderId && l.SourceWeekStart == sourceWeekStart, cancellationToken);
|
|
|
|
if (alreadyProcessed)
|
|
{
|
|
await transaction.RollbackAsync(cancellationToken);
|
|
return new WeekRolledProcessOutcome { Skipped = true };
|
|
}
|
|
|
|
var workOrder = await _context.workOrders
|
|
.FirstOrDefaultAsync(w => w.Id == workOrderId, cancellationToken);
|
|
|
|
if (workOrder == null)
|
|
{
|
|
await transaction.RollbackAsync(cancellationToken);
|
|
return new WeekRolledProcessOutcome { Skipped = true };
|
|
}
|
|
|
|
_context.WorkOrderWeekRolledLedgers.Add(new WorkOrderWeekRolledLedger
|
|
{
|
|
WorkOrderId = workOrderId,
|
|
SourceWeekStart = sourceWeekStart,
|
|
ProcessedAt = DateTime.UtcNow,
|
|
CorrelationId = correlationId
|
|
});
|
|
|
|
var oldCarriedOver = workOrder.CarriedOver;
|
|
workOrder.CarriedOver = oldCarriedOver + 1;
|
|
|
|
try
|
|
{
|
|
await _context.SaveChangesAsync(cancellationToken);
|
|
await transaction.CommitAsync(cancellationToken);
|
|
}
|
|
catch (DbUpdateException)
|
|
{
|
|
await transaction.RollbackAsync(cancellationToken);
|
|
return new WeekRolledProcessOutcome { Skipped = true };
|
|
}
|
|
|
|
return new WeekRolledProcessOutcome
|
|
{
|
|
Processed = true,
|
|
OldCarriedOver = oldCarriedOver,
|
|
NewCarriedOver = workOrder.CarriedOver
|
|
};
|
|
}
|
|
|
|
public async Task<PastDueRefreshResult> RefreshPastDueFlagsAsync(
|
|
DateTime utcNow,
|
|
int batchSize = 500,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var today = utcNow.Date;
|
|
var setCount = 0;
|
|
var clearedCount = 0;
|
|
var examinedCount = 0;
|
|
var skip = 0;
|
|
|
|
while (true)
|
|
{
|
|
var batch = await _context.workOrders
|
|
.Where(w => w.IsDeleted != true)
|
|
.Where(w => w.istemplate != true)
|
|
.Where(w => w.DueDate != null)
|
|
.OrderBy(w => w.Id)
|
|
.Skip(skip)
|
|
.Take(batchSize)
|
|
.ToListAsync(cancellationToken);
|
|
|
|
if (batch.Count == 0)
|
|
break;
|
|
|
|
foreach (var workOrder in batch)
|
|
{
|
|
examinedCount++;
|
|
var shouldBePastDue = workOrder.DueDate!.Value.Date < today
|
|
&& !TerminalStatuses.Contains(workOrder.LifecycleStatus);
|
|
var hasFlag = workOrder.OperationalFlags.HasFlag(OperationalFlags.PastDue);
|
|
|
|
if (shouldBePastDue && !hasFlag)
|
|
{
|
|
workOrder.OperationalFlags |= OperationalFlags.PastDue;
|
|
setCount++;
|
|
}
|
|
else if (!shouldBePastDue && hasFlag)
|
|
{
|
|
workOrder.OperationalFlags &= ~OperationalFlags.PastDue;
|
|
clearedCount++;
|
|
}
|
|
}
|
|
|
|
await _context.SaveChangesAsync(cancellationToken);
|
|
skip += batchSize;
|
|
|
|
if (batch.Count < batchSize)
|
|
break;
|
|
}
|
|
|
|
return new PastDueRefreshResult
|
|
{
|
|
SetCount = setCount,
|
|
ClearedCount = clearedCount,
|
|
ExaminedCount = examinedCount
|
|
};
|
|
}
|
|
}
|
|
}
|