using System.Text.Json; using System.Text.Json.Serialization; using TeleWave.Domain.Library; namespace TeleWave.Application.Programming.Templates; /// /// Окно детского времени: с до во времени канала /// в эфир идёт только контент не строже . Окно может переходить /// через полночь (23:00–06:00) — тогда границы сравниваются в обратную сторону. /// public sealed record AudienceWindow(TimeOnly From, TimeOnly To, ShowAudience MaxAudience) { public bool Contains(TimeOnly moment) => From <= To ? moment >= From && moment < To : moment >= From || moment < To; } /// /// Правила отбора кандидатов на уровне канала (см. 3.8). Это жёсткие фильтры: они отсекают /// недопустимое до жребия, поэтому не требуют пересборки и не ломают воспроизводимость. /// /// Область действия — весь канал: дейпарты сюда не заведены намеренно, окна детского времени /// и так задаются временем, а потолок повторов осмыслен только целиком по каналу. /// public sealed record PlanningRules( IReadOnlyList? MaxAudienceByTime = null, /// Не чаще Max раз за WindowDays суток (null — без ограничения). RepeatLimitRule? MaxRepeatsInWindow = null, // ── Пост-проверки: считаются по готовой ленте, дают предупреждения и ничего не переигрывают. ── /// Потолок врезок в часе, минуты (null — не проверять). int? MaxBreakMinutesPerHour = null, /// Потолок доли одного жанра за вещательные сутки, проценты (null — не проверять). int? MaxGenreSharePercent = null, /// Потолок доли эфира, отданной фону, проценты (null — не проверять). int? MaxFallbackSharePercent = null ) { private static readonly JsonSerializerOptions Options = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, Converters = { new JsonStringEnumConverter() }, }; /// Возрастной потолок в указанный момент времени канала (null — без ограничения). public ShowAudience? AudienceAt(TimeOnly moment) { if (MaxAudienceByTime is not { Count: > 0 } windows) return null; // Пересекающиеся окна разрешаются в пользу строгого: детское время не должно // отменяться более широким окном, случайно наложенным сверху. var applicable = windows.Where(w => w.Contains(moment)).Select(w => w.MaxAudience).ToList(); return applicable.Count > 0 ? applicable.Min() : null; } public string ToJson() => JsonSerializer.Serialize(this, Options); public static PlanningRules? FromJson(string? json) { if (string.IsNullOrWhiteSpace(json)) return null; try { return JsonSerializer.Deserialize(json, Options); } catch (JsonException) { return null; } } } public sealed record RepeatLimitRule(int WindowDays, int Max);