Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1703,4 +1703,46 @@ public void GetDeclaredPayCodes_EmptyRuleSet_ReturnsEmpty()
{
Assert.That(TimePlanningWorkingHoursService.GetDeclaredPayCodes(new PayRuleSet()), Is.Empty);
}

[Test]
public void Anchor_SkipsRowsBeyondTheCursor_AndSeedsFromTheLastComputedRow()
{
// A site whose chain is computed through 2026-06-10. The 06-11 row was
// created ahead of time and never filled in -- SumFlexEnd 0 -- so anchoring
// on it would discard the balance. The anchor must be the 06-10 row.
var site = new AssignedSite
{
SiteId = 1,
FlexChainComputedThrough = new DateTime(2026, 6, 10)
};
var computed = new PlanRegistration
{
SdkSitId = 1, Date = new DateTime(2026, 6, 10), SumFlexEnd = 42.5
};
var uncomputed = new PlanRegistration
{
SdkSitId = 1, Date = new DateTime(2026, 6, 11), SumFlexEnd = 0
};

var anchor = PlanRegistrationHelper.ResolveChainAnchor(
new[] { computed, uncomputed }, site, new DateTime(2026, 6, 12));

Assert.That(anchor, Is.SameAs(computed));
}

[Test]
public void Anchor_WithNullCursor_FallsBackToTheImmediatelyPrecedingRow()
{
var site = new AssignedSite { SiteId = 1, FlexChainComputedThrough = null };
var previous = new PlanRegistration
{
SdkSitId = 1, Date = new DateTime(2026, 6, 11), SumFlexEnd = 7.0
};

var anchor = PlanRegistrationHelper.ResolveChainAnchor(
new[] { previous }, site, new DateTime(2026, 6, 12));

Assert.That(anchor, Is.SameAs(previous),
"a null cursor means nothing is known -- behave exactly as before");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,26 @@ public static void RecalculatePlanHoursFromShifts(PlanRegistration pr, bool useO
RecalculatePlanHoursFromShifts(pr);
}

/// <summary>
/// The row a chain recompute should seed from: the latest candidate strictly
/// before <paramref name="windowStart"/> that the site's cursor vouches for.
/// A row after the cursor may be one created ahead of time and never filled
/// in, whose zero would silently discard the accumulated balance.
/// A null cursor means nothing is known, so behave exactly as before.
/// </summary>
public static PlanRegistration? ResolveChainAnchor(
IEnumerable<PlanRegistration> candidates, AssignedSite site, DateTime windowStart)
{
var eligible = candidates.Where(x => x.Date < windowStart);

Comment on lines +374 to +378
if (site.FlexChainComputedThrough is { } cursor)
{
eligible = eligible.Where(x => x.Date <= cursor);
}

return eligible.OrderByDescending(x => x.Date).FirstOrDefault();
}

public static async Task<TimePlanningPlanningModel> UpdatePlanRegistrationsInPeriod(
List<PlanRegistration> planningsInPeriod,
TimePlanningPlanningModel siteModel,
Expand Down Expand Up @@ -495,6 +515,7 @@ await dbContext.PlanRegistrations.AsNoTracking()
.Where(x => x.WorkflowState != Constants.WorkflowStates.Removed)
.Where(x => x.Date < planRegistration.Date
&& x.SdkSitId == dbAssignedSite.SiteId)
.Where(x => dbAssignedSite.FlexChainComputedThrough == null || x.Date <= dbAssignedSite.FlexChainComputedThrough)
.OrderByDescending(x => x.Date)
.FirstOrDefaultAsync();

Expand Down Expand Up @@ -787,6 +808,7 @@ await dbContext.PlanRegistrations.AsNoTracking()
.Where(x => x.WorkflowState != Constants.WorkflowStates.Removed)
.Where(x => x.Date < planRegistration.Date
&& x.SdkSitId == dbAssignedSite.SiteId)
.Where(x => dbAssignedSite.FlexChainComputedThrough == null || x.Date <= dbAssignedSite.FlexChainComputedThrough)
.OrderByDescending(x => x.Date)
.FirstOrDefaultAsync();

Expand Down Expand Up @@ -1180,6 +1202,7 @@ await dbContext.PlanRegistrations.AsNoTracking()
.Where(x => x.WorkflowState != Constants.WorkflowStates.Removed)
.Where(x => x.Date < planRegistration.Date
&& x.SdkSitId == dbAssignedSite.SiteId)
.Where(x => dbAssignedSite.FlexChainComputedThrough == null || x.Date <= dbAssignedSite.FlexChainComputedThrough)
.OrderByDescending(x => x.Date)
.FirstOrDefaultAsync();

Expand Down Expand Up @@ -1460,6 +1483,7 @@ await dbContext.PlanRegistrations.AsNoTracking()
.Where(x => x.WorkflowState != Constants.WorkflowStates.Removed)
.Where(x => x.Date < planRegistration.Date
&& x.SdkSitId == dbAssignedSite.SiteId)
.Where(x => dbAssignedSite.FlexChainComputedThrough == null || x.Date <= dbAssignedSite.FlexChainComputedThrough)
.OrderByDescending(x => x.Date)
.FirstOrDefaultAsync();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,7 @@ await innerDbContext.PlanRegistrations.AsNoTracking()
.Where(x => x.WorkflowState != Constants.WorkflowStates.Removed)
.Where(x => x.Date < missingDate
&& x.SdkSitId == dbAssignedSite.SiteId)
.Where(x => dbAssignedSite.FlexChainComputedThrough == null || x.Date <= dbAssignedSite.FlexChainComputedThrough)
.OrderByDescending(x => x.Date)
.FirstOrDefaultAsync();

Expand Down Expand Up @@ -595,6 +596,7 @@ await dbContext.PlanRegistrations.AsNoTracking()
.Where(x => x.WorkflowState != Constants.WorkflowStates.Removed)
.Where(x => x.Date < missingDate
&& x.SdkSitId == dbAssignedSite.SiteId)
.Where(x => dbAssignedSite.FlexChainComputedThrough == null || x.Date <= dbAssignedSite.FlexChainComputedThrough)
.OrderByDescending(x => x.Date)
.FirstOrDefaultAsync();

Expand Down
Loading