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