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 @@ -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.");
}

/// <summary>
/// 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.
/// </summary>
[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));
}

/// <summary>
/// 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.
/// </summary>
[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));
}

/// <summary>
/// Verifies that AggregatePauseMinutes sums precise pauses across multiple
/// slots when UseOneMinuteIntervals is on (3 + 7 = 10 minutes).
/// </summary>
[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));
}

/// <summary>
/// Sanity check that the legacy 5-minute-tick path still returns the
/// canonical value: Pause1Id = 4 → (4 * 5) - 5 = 15 minutes.
/// </summary>
[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));
}

/// <summary>
/// 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.
/// </summary>
[Test]
public void AggregatePauseMinutes_OneMinuteInterval_NullStampsContributeZero()
{
// Arrange
var pr = new PlanRegistration
Comment on lines +981 to +985
{
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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,50 @@ long ShiftSeconds(DateTime? startAt, DateTime? stopAt, int startId, int stopId,
return Math.Max(0, nettoSeconds);
}

/// <summary>
/// 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 &gt; 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.)
/// </summary>
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
}
Comment on lines +466 to +475

// 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;
}

/// <summary>
/// Phase 2 — write the second-precision NettoHours / Flex / SumFlex chain.
///
Expand Down Expand Up @@ -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;

Expand Down
Loading