diff --git a/eFormAPI/Plugins/TimePlanning.Pn/TimePlanning.Pn.Test/PlanRegistrationHelperTests.cs b/eFormAPI/Plugins/TimePlanning.Pn/TimePlanning.Pn.Test/PlanRegistrationHelperTests.cs
index 0bba518b..3f0f8322 100644
--- a/eFormAPI/Plugins/TimePlanning.Pn/TimePlanning.Pn.Test/PlanRegistrationHelperTests.cs
+++ b/eFormAPI/Plugins/TimePlanning.Pn/TimePlanning.Pn.Test/PlanRegistrationHelperTests.cs
@@ -877,4 +877,121 @@ public void GenerateExcelDashboard_FlagOn_FormatsShiftCellsAsHHmmss()
// Assert.That(pkg2.Workbook.Worksheets.First().Cells[2, 9].Text, Is.EqualTo("15:30"));
Assert.Pass("Captured for future fixture work; see XML doc above.");
}
+
+ ///
+ /// Customer 855 regression: with UseOneMinuteIntervals = true, a 3-minute
+ /// pause stamped via Pause1StartedAt/Pause1StoppedAt must aggregate to 3 —
+ /// not be rounded to 0 by the legacy (Pause1Id * 5) - 5 formula. The
+ /// companion Pause1Id = 1 is intentionally lossy (storage formula
+ /// (3/5)+1 = 1) and must be ignored when the flag is on.
+ ///
+ [Test]
+ public void AggregatePauseMinutes_OneMinuteInterval_RoundTrips3MinPause()
+ {
+ // Arrange
+ var someDate = new DateTime(2026, 5, 7, 0, 0, 0);
+ var pr = new PlanRegistration
+ {
+ Pause1StartedAt = someDate.AddHours(12),
+ Pause1StoppedAt = someDate.AddHours(12).AddMinutes(3),
+ Pause1Id = 1, // lossy legacy companion (3/5)+1 = 1; must be IGNORED when flag is on
+ };
+
+ // Act
+ var result = PlanRegistrationHelper.AggregatePauseMinutes(pr, useOneMinuteIntervals: true);
+
+ // Assert
+ Assert.That(result, Is.EqualTo(3));
+ }
+
+ ///
+ /// Documents the legacy lossy behavior: with UseOneMinuteIntervals = false,
+ /// a 3-minute pause stored as Pause1Id = 1 reads back as 0 minutes via the
+ /// 5-minute-tick formula. Catches accidental flag-off regressions where a
+ /// row that should be precise (flag on) silently reverts to legacy math.
+ ///
+ [Test]
+ public void AggregatePauseMinutes_LegacyMode_3MinPauseReturns0_DocumentsLossy()
+ {
+ // Arrange
+ var someDate = new DateTime(2026, 5, 7, 0, 0, 0);
+ var pr = new PlanRegistration
+ {
+ Pause1StartedAt = someDate.AddHours(12),
+ Pause1StoppedAt = someDate.AddHours(12).AddMinutes(3),
+ Pause1Id = 1, // legacy companion of a 3-min pause: (3/5)+1 = 1 → 0 min via legacy formula
+ };
+
+ // Act
+ var result = PlanRegistrationHelper.AggregatePauseMinutes(pr, useOneMinuteIntervals: false);
+
+ // Assert
+ Assert.That(result, Is.EqualTo(0));
+ }
+
+ ///
+ /// Verifies that AggregatePauseMinutes sums precise pauses across multiple
+ /// slots when UseOneMinuteIntervals is on (3 + 7 = 10 minutes).
+ ///
+ [Test]
+ public void AggregatePauseMinutes_OneMinuteInterval_SumsAcrossMultipleSlots()
+ {
+ // Arrange
+ var someDate = new DateTime(2026, 5, 7, 0, 0, 0);
+ var pr = new PlanRegistration
+ {
+ Pause1StartedAt = someDate.AddHours(10),
+ Pause1StoppedAt = someDate.AddHours(10).AddMinutes(3),
+ Pause2StartedAt = someDate.AddHours(14),
+ Pause2StoppedAt = someDate.AddHours(14).AddMinutes(7),
+ };
+
+ // Act
+ var result = PlanRegistrationHelper.AggregatePauseMinutes(pr, useOneMinuteIntervals: true);
+
+ // Assert
+ Assert.That(result, Is.EqualTo(10));
+ }
+
+ ///
+ /// Sanity check that the legacy 5-minute-tick path still returns the
+ /// canonical value: Pause1Id = 4 → (4 * 5) - 5 = 15 minutes.
+ ///
+ [Test]
+ public void AggregatePauseMinutes_LegacyMode_15MinPauseReturns15()
+ {
+ // Arrange
+ var pr = new PlanRegistration
+ {
+ Pause1Id = 4, // legacy 15-minute pause: (4 * 5) - 5 = 15
+ };
+
+ // Act
+ var result = PlanRegistrationHelper.AggregatePauseMinutes(pr, useOneMinuteIntervals: false);
+
+ // Assert
+ Assert.That(result, Is.EqualTo(15));
+ }
+
+ ///
+ /// When UseOneMinuteIntervals is on, the legacy Pause*Id field must be
+ /// ignored: a row with Pause1Id = 4 (legacy 15 min) but no DateTime stamps
+ /// is treated as zero pause.
+ ///
+ [Test]
+ public void AggregatePauseMinutes_OneMinuteInterval_NullStampsContributeZero()
+ {
+ // Arrange
+ var pr = new PlanRegistration
+ {
+ Pause1Id = 4, // legacy 15-min pause; flag-on path must ignore this
+ // Pause1StartedAt / Pause1StoppedAt left null
+ };
+
+ // Act
+ var result = PlanRegistrationHelper.AggregatePauseMinutes(pr, useOneMinuteIntervals: true);
+
+ // Assert
+ Assert.That(result, Is.EqualTo(0));
+ }
}
\ 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 61ffb337..f6956cd3 100644
--- a/eFormAPI/Plugins/TimePlanning.Pn/TimePlanning.Pn/Infrastructure/Helpers/PlanRegistrationHelper.cs
+++ b/eFormAPI/Plugins/TimePlanning.Pn/TimePlanning.Pn/Infrastructure/Helpers/PlanRegistrationHelper.cs
@@ -447,6 +447,50 @@ long ShiftSeconds(DateTime? startAt, DateTime? stopAt, int startId, int stopId,
return Math.Max(0, nettoSeconds);
}
+ ///
+ /// Aggregates pause minutes for a PlanRegistration.
+ ///
+ /// When useOneMinuteIntervals is true, sums the (Pause*StoppedAt - Pause*StartedAt)
+ /// DateTime deltas in seconds across all 5 pause slots that have BOTH stamps populated,
+ /// then rounds to whole minutes. This preserves second/minute precision for pauses
+ /// recorded by workers on UseOneMinuteIntervals-enabled sites (e.g. a 3-min pause
+ /// reads as 3, not 0).
+ ///
+ /// When useOneMinuteIntervals is false, falls back to the legacy 5-minute-tick
+ /// formula: for each Pause*Id > 0, contribute (Pause*Id * 5) - 5 minutes. (Pause*Id
+ /// stores break in 5-minute ticks plus a +1 sentinel: Pause1Id = 1 means 0 min,
+ /// Pause1Id = 4 means 15 min, etc.)
+ ///
+ public static int AggregatePauseMinutes(PlanRegistration pr, bool useOneMinuteIntervals)
+ {
+ if (useOneMinuteIntervals)
+ {
+ long totalSeconds = 0;
+ totalSeconds += PauseSpanSeconds(pr.Pause1StartedAt, pr.Pause1StoppedAt);
+ totalSeconds += PauseSpanSeconds(pr.Pause2StartedAt, pr.Pause2StoppedAt);
+ totalSeconds += PauseSpanSeconds(pr.Pause3StartedAt, pr.Pause3StoppedAt);
+ totalSeconds += PauseSpanSeconds(pr.Pause4StartedAt, pr.Pause4StoppedAt);
+ totalSeconds += PauseSpanSeconds(pr.Pause5StartedAt, pr.Pause5StoppedAt);
+ return (int)(totalSeconds / 60); // round down to whole minutes
+ }
+
+ // Legacy 5-minute-tick path
+ var totalMinutes = 0;
+ if (pr.Pause1Id > 0) totalMinutes += (pr.Pause1Id * 5) - 5;
+ if (pr.Pause2Id > 0) totalMinutes += (pr.Pause2Id * 5) - 5;
+ if (pr.Pause3Id > 0) totalMinutes += (pr.Pause3Id * 5) - 5;
+ if (pr.Pause4Id > 0) totalMinutes += (pr.Pause4Id * 5) - 5;
+ if (pr.Pause5Id > 0) totalMinutes += (pr.Pause5Id * 5) - 5;
+ return totalMinutes;
+ }
+
+ private static long PauseSpanSeconds(DateTime? startedAt, DateTime? stoppedAt)
+ {
+ if (startedAt == null || stoppedAt == null) return 0;
+ var span = stoppedAt.Value - startedAt.Value;
+ return span.TotalSeconds > 0 ? (long)span.TotalSeconds : 0;
+ }
+
///
/// Phase 2 — write the second-precision NettoHours / Flex / SumFlex chain.
///
@@ -1547,21 +1591,7 @@ await dbContext.PlanRegistrations.AsNoTracking()
Pause5StoppedAt = planRegistration.Pause5StoppedAt
};
- planningModel.PauseMinutes += planRegistration.Pause1Id > 0
- ? (planRegistration.Pause1Id * 5) - 5
- : 0;
- planningModel.PauseMinutes += planRegistration.Pause2Id > 0
- ? (planRegistration.Pause2Id * 5) - 5
- : 0;
- planningModel.PauseMinutes += planRegistration.Pause3Id > 0
- ? (planRegistration.Pause3Id * 5) - 5
- : 0;
- planningModel.PauseMinutes += planRegistration.Pause4Id > 0
- ? (planRegistration.Pause4Id * 5) - 5
- : 0;
- planningModel.PauseMinutes += planRegistration.Pause5Id > 0
- ? (planRegistration.Pause5Id * 5) - 5
- : 0;
+ planningModel.PauseMinutes += AggregatePauseMinutes(planRegistration, dbAssignedSite.UseOneMinuteIntervals);
// planningModel.PauseMinutes = planningModel.PauseMinutes > 0 ? planningModel.PauseMinutes - 5 : 0;