Implement template export/import endpoints and enhance grid generation logic
ci / build-backend (push) Successful in 1m31s
ci / build-frontend (push) Successful in 1m5s
ci / tests (push) Successful in 3m54s
ci / sonar (push) Successful in 4m40s

Added new endpoints for exporting and importing grid configurations in TemplateEndpoints, allowing for better management of template data. Enhanced the GenerateGridCommandHandler to support seasonal layers in grid generation, improving scheduling accuracy during holiday periods. Updated related classes and records to accommodate these changes, ensuring a cohesive integration of new features. Improved documentation for clarity and maintainability.
This commit is contained in:
Leonid Pershin
2026-07-28 02:15:04 +03:00
parent 7bee84c548
commit f7e7b5f7c3
45 changed files with 2929 additions and 242 deletions
@@ -75,7 +75,9 @@ public class SchedulePlannerTests
DateTimeOffset? start = null,
int horizonHours = 6,
IReadOnlyList<PlanningUnit>? fallback = null,
IReadOnlyList<PlanningSlot>? background = null
IReadOnlyList<PlanningSlot>? background = null,
IReadOnlyList<PadUnit>? pad = null,
TimeSpan maxPad = default
) =>
new(
Guid.NewGuid(),
@@ -85,9 +87,13 @@ public class SchedulePlannerTests
fallback ?? [Unit(1, Guid.NewGuid(), 0)],
SegmentSeconds: 2,
UtcOffsetMinutes: 0,
background ?? []
background ?? [],
pad ?? [],
maxPad
);
private static PadUnit Ad(int minutes) => new(Unit(minutes, Guid.Empty, 0), PlannedItemKind.Ad);
private static PlanningResult Run(PlanningInput input) =>
TeleWave.Domain.Programming.Planning.SchedulePlanner.Plan(input, new FirstAlways());
@@ -350,6 +356,72 @@ public class SchedulePlannerTests
Assert.Equal(element.ElementId, program.Trace.ElementId);
}
[Fact]
public void Pad_FillsSeamBeforeSlotWithAds()
{
// Слот-якорь начинается через полчаса после конца предыдущего: шов до объявленного времени
// на настоящем ТВ занимает реклама, а не ещё одна серия.
var anchor = Slot(T0.AddMinutes(30), 30, [Element(1, 30)], isAnchor: true);
var result = Run(
Input(
[Slot(T0, 15, [Element(1, 15)]), anchor],
horizonHours: 1,
pad: [Ad(5)],
maxPad: TimeSpan.FromMinutes(30)
)
);
var ads = result.Items.Where(i => i.Kind == PlannedItemKind.Ad).ToList();
Assert.Equal(3, ads.Count);
Assert.Equal(T0.AddMinutes(15), ads[0].StartsAtUtc);
// К объявленному времени шов заканчивается: якорь начинается ровно в свои полчаса.
Assert.Equal(anchor.TargetStartUtc, ads[^1].EndsAtUtc);
}
[Fact]
public void Pad_IsCappedSoLongHolesStayProgramming()
{
// Дыра в час: рекламой закрывается только потолок одного добора, остальное — программы.
var background = Slot(T0, 24 * 60, [Element(6, 10)]);
var result = Run(
Input(
[Slot(T0.AddHours(1), 30, [Element(1, 30)], isAnchor: true)],
horizonHours: 2,
pad: [Ad(5)],
maxPad: TimeSpan.FromMinutes(10),
background: [background]
)
);
var ads = result.Items.Where(i => i.Kind == PlannedItemKind.Ad).ToList();
Assert.Equal(
TimeSpan.FromMinutes(10),
ads.Aggregate(TimeSpan.Zero, (sum, a) => sum + (a.EndsAtUtc - a.StartsAtUtc))
);
Assert.Contains(result.Items, i => i.SlotId == background.SlotId);
}
[Fact]
public void Pad_ClosesTailWhenProgrammeDoesNotFit()
{
// Остаток в пять минут: серия фона туда не влезает, и хвост занимает реклама, а не филлер.
var background = Slot(T0, 24 * 60, [Element(4, 30)]);
var result = Run(
Input(
[Slot(T0, 55, [Element(1, 55)])],
horizonHours: 1,
pad: [Ad(5)],
background: [background]
)
);
Assert.Single(result.Items, i => i.Kind == PlannedItemKind.Ad);
Assert.DoesNotContain(result.Items, i => i.Kind == PlannedItemKind.Fallback);
}
[Fact]
public void Background_PlaysInLeftoverInsteadOfEmergencyFiller()
{