diff --git a/eFormAPI/Plugins/TimePlanning.Pn/TimePlanning.Pn.Test/PlanRegistrationHelperTests.cs b/eFormAPI/Plugins/TimePlanning.Pn/TimePlanning.Pn.Test/PlanRegistrationHelperTests.cs
index bb52df4f..b2ef0a4a 100644
--- a/eFormAPI/Plugins/TimePlanning.Pn/TimePlanning.Pn.Test/PlanRegistrationHelperTests.cs
+++ b/eFormAPI/Plugins/TimePlanning.Pn/TimePlanning.Pn.Test/PlanRegistrationHelperTests.cs
@@ -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");
+ }
}
\ No newline at end of file
diff --git a/eFormAPI/Plugins/TimePlanning.Pn/TimePlanning.Pn/Infrastructure/Helpers/PlanRegistrationHelper.cs b/eFormAPI/Plugins/TimePlanning.Pn/TimePlanning.Pn/Infrastructure/Helpers/PlanRegistrationHelper.cs
index 41c86a7c..23e90fe8 100644
--- a/eFormAPI/Plugins/TimePlanning.Pn/TimePlanning.Pn/Infrastructure/Helpers/PlanRegistrationHelper.cs
+++ b/eFormAPI/Plugins/TimePlanning.Pn/TimePlanning.Pn/Infrastructure/Helpers/PlanRegistrationHelper.cs
@@ -364,6 +364,26 @@ public static void RecalculatePlanHoursFromShifts(PlanRegistration pr, bool useO
RecalculatePlanHoursFromShifts(pr);
}
+ ///
+ /// The row a chain recompute should seed from: the latest candidate strictly
+ /// before 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.
+ ///
+ public static PlanRegistration? ResolveChainAnchor(
+ IEnumerable candidates, AssignedSite site, DateTime windowStart)
+ {
+ var eligible = candidates.Where(x => x.Date < windowStart);
+
+ if (site.FlexChainComputedThrough is { } cursor)
+ {
+ eligible = eligible.Where(x => x.Date <= cursor);
+ }
+
+ return eligible.OrderByDescending(x => x.Date).FirstOrDefault();
+ }
+
public static async Task UpdatePlanRegistrationsInPeriod(
List planningsInPeriod,
TimePlanningPlanningModel siteModel,
@@ -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();
@@ -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();
@@ -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();
@@ -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();
diff --git a/eFormAPI/Plugins/TimePlanning.Pn/TimePlanning.Pn/Services/TimePlanningPlanningService/TimePlanningPlanningService.cs b/eFormAPI/Plugins/TimePlanning.Pn/TimePlanning.Pn/Services/TimePlanningPlanningService/TimePlanningPlanningService.cs
index 95fbbfad..bd95b632 100644
--- a/eFormAPI/Plugins/TimePlanning.Pn/TimePlanning.Pn/Services/TimePlanningPlanningService/TimePlanningPlanningService.cs
+++ b/eFormAPI/Plugins/TimePlanning.Pn/TimePlanning.Pn/Services/TimePlanningPlanningService/TimePlanningPlanningService.cs
@@ -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();
@@ -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();