From fed3a0969592bc96dbb27a782d49191026285757 Mon Sep 17 00:00:00 2001 From: "alexei.dolgolyov" Date: Sat, 9 May 2026 15:40:35 +0300 Subject: [PATCH] refactor: hoist Moscow offset + sport labels into shared helpers (HIGH) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * New Marathon.Domain.ValueObjects.MoscowTime with Offset, Now, and EndOfMoscowDay(DateOnly) — replaces ~15 inline TimeSpan.FromHours(3) literals across Domain/Application/Infrastructure/UI. * New Marathon.UI.Services.SportLabels.Resolve(IStringLocalizer, int) — replaces 6 near-identical SportLabel switch bodies in EventListShell, Events/Detail, Anomalies/AnomalyFeed, Results/ResultsList, Results/ResultsLoader, and AnomalyCard. Single source of truth for the 6/11/22723/43658 sport-code mapping. Pages keep a one-liner wrapper so the call sites stay terse. --- .../UseCases/DetectAnomaliesUseCase.cs | 4 +-- .../AnomalyDetection/AnomalyDetector.cs | 4 +-- src/Marathon.Domain/Entities/Event.cs | 4 +-- .../ValueObjects/MoscowTime.cs | 32 +++++++++++++++++++ .../Parsers/EventListingParserBase.cs | 4 +-- .../Scraping/Parsers/EventOddsParser.cs | 4 +-- .../Scraping/Parsers/MoscowDateParser.cs | 7 ++-- .../Scraping/Parsers/ResultsParser.cs | 4 +-- .../Scraping/Parsers/ServerTimeProvider.cs | 5 ++- src/Marathon.UI/Components/AnomalyCard.razor | 9 +----- src/Marathon.UI/Components/ExportDialog.razor | 7 ++-- .../Pages/Anomalies/AnomalyFeed.razor | 15 ++------- src/Marathon.UI/Pages/Events/Detail.razor | 9 +----- .../Pages/Results/ResultsList.razor | 17 +++------- .../Pages/Results/ResultsLoader.razor | 17 +++------- .../Pages/Shared/EventListShell.razor | 15 ++------- .../Services/EventBrowsingState.cs | 5 +-- src/Marathon.UI/Services/SportLabels.cs | 26 +++++++++++++++ 18 files changed, 92 insertions(+), 96 deletions(-) create mode 100644 src/Marathon.Domain/ValueObjects/MoscowTime.cs create mode 100644 src/Marathon.UI/Services/SportLabels.cs diff --git a/src/Marathon.Application/UseCases/DetectAnomaliesUseCase.cs b/src/Marathon.Application/UseCases/DetectAnomaliesUseCase.cs index 4e4692d..1e420c7 100644 --- a/src/Marathon.Application/UseCases/DetectAnomaliesUseCase.cs +++ b/src/Marathon.Application/UseCases/DetectAnomaliesUseCase.cs @@ -2,6 +2,7 @@ using Marathon.Application.Abstractions; using Marathon.Application.Configuration; using Marathon.Domain.AnomalyDetection; using Marathon.Domain.Entities; +using Marathon.Domain.ValueObjects; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -24,7 +25,6 @@ namespace Marathon.Application.UseCases; /// public sealed class DetectAnomaliesUseCase { - private static readonly TimeSpan MoscowOffset = TimeSpan.FromHours(3); private static readonly TimeSpan SnapshotLookback = TimeSpan.FromHours(24); // Dedup window: two anomalies for the same event within this window are considered duplicates. @@ -67,7 +67,7 @@ public sealed class DetectAnomaliesUseCase var events = await _eventRepo.ListAsync(ct); int newAnomalyCount = 0; - var now = DateTimeOffset.UtcNow.ToOffset(MoscowOffset); + var now = MoscowTime.Now; var from = now - SnapshotLookback; // Hoisted outside the per-event loop: load existing anomalies ONCE per cycle diff --git a/src/Marathon.Domain/AnomalyDetection/AnomalyDetector.cs b/src/Marathon.Domain/AnomalyDetection/AnomalyDetector.cs index 810f541..e570584 100644 --- a/src/Marathon.Domain/AnomalyDetection/AnomalyDetector.cs +++ b/src/Marathon.Domain/AnomalyDetection/AnomalyDetector.cs @@ -27,8 +27,6 @@ namespace Marathon.Domain.AnomalyDetection; /// public sealed class AnomalyDetector { - private static readonly TimeSpan MoscowOffset = TimeSpan.FromHours(3); - private readonly int _suspensionGapSeconds; private readonly decimal _oddsFlipThreshold; private readonly int _minSnapshotCount; @@ -156,7 +154,7 @@ public sealed class AnomalyDetector return new Anomaly( Id: Guid.NewGuid(), EventId: eventId, - DetectedAt: DateTimeOffset.UtcNow.ToOffset(MoscowOffset), + DetectedAt: MoscowTime.Now, Kind: AnomalyKind.SuspensionFlip, Score: clampedScore, EvidenceJson: evidenceJson); diff --git a/src/Marathon.Domain/Entities/Event.cs b/src/Marathon.Domain/Entities/Event.cs index 5d20b14..f1ac593 100644 --- a/src/Marathon.Domain/Entities/Event.cs +++ b/src/Marathon.Domain/Entities/Event.cs @@ -21,8 +21,6 @@ public sealed record Event( string Side1Name, string Side2Name) { - private static readonly TimeSpan MoscowOffset = TimeSpan.FromHours(3); - public EventId Id { get; } = Id ?? throw new ArgumentNullException(nameof(Id)); public SportCode Sport { get; } = Sport ?? throw new ArgumentNullException(nameof(Sport)); @@ -37,7 +35,7 @@ public sealed record Event( public string Category { get; } = Category ?? string.Empty; - public DateTimeOffset ScheduledAt { get; } = ScheduledAt.Offset == MoscowOffset + public DateTimeOffset ScheduledAt { get; } = ScheduledAt.Offset == MoscowTime.Offset ? ScheduledAt : throw new ArgumentException( $"ScheduledAt must be in Europe/Moscow time (UTC+03:00). " + diff --git a/src/Marathon.Domain/ValueObjects/MoscowTime.cs b/src/Marathon.Domain/ValueObjects/MoscowTime.cs new file mode 100644 index 0000000..a2fb63c --- /dev/null +++ b/src/Marathon.Domain/ValueObjects/MoscowTime.cs @@ -0,0 +1,32 @@ +namespace Marathon.Domain.ValueObjects; + +/// +/// Single source of truth for the Moscow timezone offset. +/// +/// +/// +/// marathonbet.by serves all timestamps in Moscow time (UTC+3) and the domain +/// invariant on +/// rejects any other offset. Code that constructs +/// values for events, results, snapshots, or test fixtures MUST use this +/// constant rather than re-deriving TimeSpan.FromHours(3). +/// +/// +public static class MoscowTime +{ + /// The Moscow time offset (UTC+3). + public static readonly TimeSpan Offset = TimeSpan.FromHours(3); + + /// Current Moscow time. + public static DateTimeOffset Now => DateTimeOffset.UtcNow.ToOffset(Offset); + + /// + /// Returns the inclusive end-of-day for the given Moscow date — i.e., + /// the moment one second before the next day starts. Used by date-range + /// filters where the user picks "to: 2026-05-09" meaning "through the + /// rest of that day." + /// + public static DateTimeOffset EndOfMoscowDay(DateOnly date) => + new DateTimeOffset(date.ToDateTime(TimeOnly.MinValue), Offset) + .AddDays(1).AddSeconds(-1); +} diff --git a/src/Marathon.Infrastructure/Scraping/Parsers/EventListingParserBase.cs b/src/Marathon.Infrastructure/Scraping/Parsers/EventListingParserBase.cs index e2d718d..86e5116 100644 --- a/src/Marathon.Infrastructure/Scraping/Parsers/EventListingParserBase.cs +++ b/src/Marathon.Infrastructure/Scraping/Parsers/EventListingParserBase.cs @@ -17,8 +17,6 @@ namespace Marathon.Infrastructure.Scraping.Parsers; /// public abstract class EventListingParserBase { - private static readonly TimeSpan MoscowOffset = TimeSpan.FromHours(3); - protected readonly IServerTimeProvider ServerTimeProvider; protected readonly ILogger Logger; @@ -36,7 +34,7 @@ public abstract class EventListingParserBase CancellationToken ct) { var serverTime = ServerTimeProvider.ExtractServerTime(html) - ?? DateTimeOffset.UtcNow.ToOffset(MoscowOffset); + ?? MoscowTime.Now; var config = AngleSharpConfig.Default; using var context = BrowsingContext.New(config); diff --git a/src/Marathon.Infrastructure/Scraping/Parsers/EventOddsParser.cs b/src/Marathon.Infrastructure/Scraping/Parsers/EventOddsParser.cs index 580c046..2fae14f 100644 --- a/src/Marathon.Infrastructure/Scraping/Parsers/EventOddsParser.cs +++ b/src/Marathon.Infrastructure/Scraping/Parsers/EventOddsParser.cs @@ -18,8 +18,6 @@ namespace Marathon.Infrastructure.Scraping.Parsers; /// public sealed partial class EventOddsParser : IEventOddsParser { - private static readonly TimeSpan MoscowOffset = TimeSpan.FromHours(3); - private readonly IServerTimeProvider _serverTime; private readonly PeriodScopeMapper _periodMapper; private readonly ILogger _logger; @@ -69,7 +67,7 @@ public sealed partial class EventOddsParser : IEventOddsParser ArgumentNullException.ThrowIfNull(html); var capturedAt = _serverTime.ExtractServerTime(html) - ?? DateTimeOffset.UtcNow.ToOffset(MoscowOffset); + ?? MoscowTime.Now; var config = AngleSharpConfig.Default; using var context = BrowsingContext.New(config); diff --git a/src/Marathon.Infrastructure/Scraping/Parsers/MoscowDateParser.cs b/src/Marathon.Infrastructure/Scraping/Parsers/MoscowDateParser.cs index 3626ebf..0636281 100644 --- a/src/Marathon.Infrastructure/Scraping/Parsers/MoscowDateParser.cs +++ b/src/Marathon.Infrastructure/Scraping/Parsers/MoscowDateParser.cs @@ -1,5 +1,6 @@ using System.Globalization; using System.Text.RegularExpressions; +using Marathon.Domain.ValueObjects; namespace Marathon.Infrastructure.Scraping.Parsers; @@ -13,8 +14,6 @@ namespace Marathon.Infrastructure.Scraping.Parsers; /// public static partial class MoscowDateParser { - private static readonly TimeSpan MoscowOffset = TimeSpan.FromHours(3); - // Matches "HH:MM" [GeneratedRegex(@"^\s*(\d{1,2}):(\d{2})\s*$", RegexOptions.CultureInvariant)] private static partial Regex TimeOnlyRegex(); @@ -70,7 +69,7 @@ public static partial class MoscowDateParser var scheduled = new DateTimeOffset( today.Year, today.Month, today.Day, hour, minute, 0, - MoscowOffset); + MoscowTime.Offset); // If the computed time is already in the past (same day but earlier), // that's fine — the event may have already started (live) or the listing @@ -98,7 +97,7 @@ public static partial class MoscowDateParser if (candidate < DateOnly.FromDateTime(anchorDate)) year++; // e.g., anchor is Dec 2026 and event is in Jan 2027 - return new DateTimeOffset(year, month, day, hour, minute, 0, MoscowOffset); + return new DateTimeOffset(year, month, day, hour, minute, 0, MoscowTime.Offset); } return null; diff --git a/src/Marathon.Infrastructure/Scraping/Parsers/ResultsParser.cs b/src/Marathon.Infrastructure/Scraping/Parsers/ResultsParser.cs index 1961bc4..8f221c4 100644 --- a/src/Marathon.Infrastructure/Scraping/Parsers/ResultsParser.cs +++ b/src/Marathon.Infrastructure/Scraping/Parsers/ResultsParser.cs @@ -21,8 +21,6 @@ namespace Marathon.Infrastructure.Scraping.Parsers; /// public sealed partial class ResultsParser : IResultsParser { - private static readonly TimeSpan MoscowOffset = TimeSpan.FromHours(3); - private readonly ILogger _logger; // Matches score patterns like "2:1", "2:1 (1:1)", "2:1 (0:0) (2:1)" @@ -101,7 +99,7 @@ public sealed partial class ResultsParser : IResultsParser if (string.IsNullOrWhiteSpace(eventIdRaw)) return null; - var completedAt = DateTimeOffset.UtcNow.ToOffset(MoscowOffset); + var completedAt = MoscowTime.Now; return new EventResult( new DomainEventId(eventIdRaw), diff --git a/src/Marathon.Infrastructure/Scraping/Parsers/ServerTimeProvider.cs b/src/Marathon.Infrastructure/Scraping/Parsers/ServerTimeProvider.cs index 6006086..75f9e9d 100644 --- a/src/Marathon.Infrastructure/Scraping/Parsers/ServerTimeProvider.cs +++ b/src/Marathon.Infrastructure/Scraping/Parsers/ServerTimeProvider.cs @@ -1,4 +1,5 @@ using System.Text.RegularExpressions; +using Marathon.Domain.ValueObjects; using Microsoft.Extensions.Logging; namespace Marathon.Infrastructure.Scraping.Parsers; @@ -17,8 +18,6 @@ public sealed partial class ServerTimeProvider : IServerTimeProvider RegexOptions.CultureInvariant)] private static partial Regex ServerTimeRegex(); - private static readonly TimeSpan MoscowOffset = TimeSpan.FromHours(3); - private readonly ILogger _logger; public ServerTimeProvider(ILogger logger) @@ -47,6 +46,6 @@ public sealed partial class ServerTimeProvider : IServerTimeProvider var minute = int.Parse(match.Groups[5].Value); var second = int.Parse(match.Groups[6].Value); - return new DateTimeOffset(year, month, day, hour, minute, second, MoscowOffset); + return new DateTimeOffset(year, month, day, hour, minute, second, MoscowTime.Offset); } } diff --git a/src/Marathon.UI/Components/AnomalyCard.razor b/src/Marathon.UI/Components/AnomalyCard.razor index a933d73..91246b0 100644 --- a/src/Marathon.UI/Components/AnomalyCard.razor +++ b/src/Marathon.UI/Components/AnomalyCard.razor @@ -215,14 +215,7 @@ _ => kind.ToString(), }; - private string SportLabel(int code) => code switch - { - 6 => L["Sport.Basketball"], - 11 => L["Sport.Football"], - 22723 => L["Sport.Tennis"], - 43658 => L["Sport.Hockey"], - _ => string.Format(System.Globalization.CultureInfo.InvariantCulture, "Sport {0}", code), - }; + private string SportLabel(int code) => SportLabels.Resolve(L, code); private static string FormatRate(decimal? r) => r is { } v ? v.ToString("0.00", System.Globalization.CultureInfo.InvariantCulture) diff --git a/src/Marathon.UI/Components/ExportDialog.razor b/src/Marathon.UI/Components/ExportDialog.razor index 357b39b..34883fe 100644 --- a/src/Marathon.UI/Components/ExportDialog.razor +++ b/src/Marathon.UI/Components/ExportDialog.razor @@ -113,7 +113,7 @@ protected override void OnInitialized() { - var moscow = DateTimeOffset.UtcNow.ToOffset(TimeSpan.FromHours(3)); + var moscow = MoscowTime.Now; _from = InitialFrom ?? moscow.AddDays(-1).Date; _to = InitialTo ?? moscow.AddDays(1).Date; _kind = InitialKind; @@ -140,10 +140,9 @@ try { // Use Moscow offset to match domain ScheduledAt invariant. - var moscow = TimeSpan.FromHours(3); var range = new AppDateRange( - new DateTimeOffset(_from.Value.Date, moscow), - new DateTimeOffset(_to.Value.Date.AddDays(1).AddSeconds(-1), moscow)); + new DateTimeOffset(_from.Value.Date, MoscowTime.Offset), + MoscowTime.EndOfMoscowDay(DateOnly.FromDateTime(_to.Value.Date))); var path = await ExportUseCase.ExecuteAsync(range, _kind, CancellationToken.None); Dialog.Close(DialogResult.Ok(path)); diff --git a/src/Marathon.UI/Pages/Anomalies/AnomalyFeed.razor b/src/Marathon.UI/Pages/Anomalies/AnomalyFeed.razor index e4948e7..0292305 100644 --- a/src/Marathon.UI/Pages/Anomalies/AnomalyFeed.razor +++ b/src/Marathon.UI/Pages/Anomalies/AnomalyFeed.razor @@ -252,8 +252,7 @@ { if (DateTimeOffset.TryParse(e.Value?.ToString(), out var v)) { - var moscow = TimeSpan.FromHours(3); - await UpdateFilter(_filter with { From = new DateTimeOffset(v.Date, moscow) }); + await UpdateFilter(_filter with { From = new DateTimeOffset(v.Date, MoscowTime.Offset) }); } } @@ -261,8 +260,7 @@ { if (DateTimeOffset.TryParse(e.Value?.ToString(), out var v)) { - var moscow = TimeSpan.FromHours(3); - await UpdateFilter(_filter with { To = new DateTimeOffset(v.Date, moscow).AddDays(1).AddSeconds(-1) }); + await UpdateFilter(_filter with { To = MoscowTime.EndOfMoscowDay(DateOnly.FromDateTime(v.Date)) }); } } @@ -283,14 +281,7 @@ _ => L["Anomaly.Severity.Low"], }; - private string SportLabel(int code) => code switch - { - 6 => L["Sport.Basketball"], - 11 => L["Sport.Football"], - 22723 => L["Sport.Tennis"], - 43658 => L["Sport.Hockey"], - _ => string.Format(System.Globalization.CultureInfo.InvariantCulture, "Sport {0}", code), - }; + private string SportLabel(int code) => SportLabels.Resolve(L, code); private static string FormatDate(DateTimeOffset? value) => value?.ToString("yyyy-MM-dd") ?? string.Empty; diff --git a/src/Marathon.UI/Pages/Events/Detail.razor b/src/Marathon.UI/Pages/Events/Detail.razor index 4f25fa6..ed39878 100644 --- a/src/Marathon.UI/Pages/Events/Detail.razor +++ b/src/Marathon.UI/Pages/Events/Detail.razor @@ -306,14 +306,7 @@ _ => "—", }; - private string SportLabel(int code) => code switch - { - 6 => L["Sport.Basketball"], - 11 => L["Sport.Football"], - 22723 => L["Sport.Tennis"], - 43658 => L["Sport.Hockey"], - _ => $"Sport {code}", - }; + private string SportLabel(int code) => SportLabels.Resolve(L, code); private string BetTypeLabel(BetType t) => t switch { diff --git a/src/Marathon.UI/Pages/Results/ResultsList.razor b/src/Marathon.UI/Pages/Results/ResultsList.razor index faa63c0..7d534ee 100644 --- a/src/Marathon.UI/Pages/Results/ResultsList.razor +++ b/src/Marathon.UI/Pages/Results/ResultsList.razor @@ -164,8 +164,6 @@ @code { - private static readonly TimeSpan MoscowOffset = TimeSpan.FromHours(3); - private DateTimeOffset _from; private DateTimeOffset _to; private string _searchInput = string.Empty; @@ -181,7 +179,7 @@ protected override async Task OnInitializedAsync() { - var todayMoscow = new DateTimeOffset(DateTime.UtcNow.Date, TimeSpan.Zero).ToOffset(MoscowOffset); + var todayMoscow = new DateTimeOffset(DateTime.UtcNow.Date, TimeSpan.Zero).ToOffset(MoscowTime.Offset); _from = todayMoscow.AddDays(-30); _to = todayMoscow.AddDays(1).AddSeconds(-1); @@ -246,7 +244,7 @@ { if (DateTimeOffset.TryParse(e.Value?.ToString(), out var v)) { - _from = new DateTimeOffset(v.Date, MoscowOffset); + _from = new DateTimeOffset(v.Date, MoscowTime.Offset); await LoadAsync(); } } @@ -255,7 +253,7 @@ { if (DateTimeOffset.TryParse(e.Value?.ToString(), out var v)) { - _to = new DateTimeOffset(v.Date, MoscowOffset).AddDays(1).AddSeconds(-1); + _to = new DateTimeOffset(v.Date, MoscowTime.Offset).AddDays(1).AddSeconds(-1); await LoadAsync(); } } @@ -311,14 +309,7 @@ _ => "draw", }; - private string SportLabel(int code) => code switch - { - 6 => L["Sport.Basketball"], - 11 => L["Sport.Football"], - 22723 => L["Sport.Tennis"], - 43658 => L["Sport.Hockey"], - _ => $"Sport {code}", - }; + private string SportLabel(int code) => SportLabels.Resolve(L, code); public void Dispose() { diff --git a/src/Marathon.UI/Pages/Results/ResultsLoader.razor b/src/Marathon.UI/Pages/Results/ResultsLoader.razor index c707d13..bc30faf 100644 --- a/src/Marathon.UI/Pages/Results/ResultsLoader.razor +++ b/src/Marathon.UI/Pages/Results/ResultsLoader.razor @@ -204,8 +204,6 @@ @code { - private static readonly TimeSpan MoscowOffset = TimeSpan.FromHours(3); - private DateTimeOffset _from; private DateTimeOffset _to; private LoaderMode _mode = LoaderMode.AllInRange; @@ -234,7 +232,7 @@ protected override async Task OnInitializedAsync() { - var todayMoscow = new DateTimeOffset(DateTime.UtcNow.Date, TimeSpan.Zero).ToOffset(MoscowOffset); + var todayMoscow = new DateTimeOffset(DateTime.UtcNow.Date, TimeSpan.Zero).ToOffset(MoscowTime.Offset); _from = todayMoscow.AddDays(-7); _to = todayMoscow.AddDays(1).AddSeconds(-1); await ReloadCandidatesAsync(); @@ -347,7 +345,7 @@ { if (DateTimeOffset.TryParse(e.Value?.ToString(), out var v)) { - _from = new DateTimeOffset(v.Date, MoscowOffset); + _from = new DateTimeOffset(v.Date, MoscowTime.Offset); await ReloadCandidatesAsync(); } } @@ -356,7 +354,7 @@ { if (DateTimeOffset.TryParse(e.Value?.ToString(), out var v)) { - _to = new DateTimeOffset(v.Date, MoscowOffset).AddDays(1).AddSeconds(-1); + _to = new DateTimeOffset(v.Date, MoscowTime.Offset).AddDays(1).AddSeconds(-1); await ReloadCandidatesAsync(); } } @@ -400,14 +398,7 @@ _ => o.ToString(), }; - private string SportLabel(int code) => code switch - { - 6 => L["Sport.Basketball"], - 11 => L["Sport.Football"], - 22723 => L["Sport.Tennis"], - 43658 => L["Sport.Hockey"], - _ => $"Sport {code}", - }; + private string SportLabel(int code) => SportLabels.Resolve(L, code); private static string FormatDate(DateTimeOffset value) => value.ToString("yyyy-MM-dd"); diff --git a/src/Marathon.UI/Pages/Shared/EventListShell.razor b/src/Marathon.UI/Pages/Shared/EventListShell.razor index 7713e5c..9bffc48 100644 --- a/src/Marathon.UI/Pages/Shared/EventListShell.razor +++ b/src/Marathon.UI/Pages/Shared/EventListShell.razor @@ -437,8 +437,7 @@ { if (DateTimeOffset.TryParse(e.Value?.ToString(), out var v)) { - var moscow = TimeSpan.FromHours(3); - await UpdateFilter(_filter with { From = new DateTimeOffset(v.Date, moscow) }); + await UpdateFilter(_filter with { From = new DateTimeOffset(v.Date, MoscowTime.Offset) }); } } @@ -446,8 +445,7 @@ { if (DateTimeOffset.TryParse(e.Value?.ToString(), out var v)) { - var moscow = TimeSpan.FromHours(3); - await UpdateFilter(_filter with { To = new DateTimeOffset(v.Date, moscow).AddDays(1).AddSeconds(-1) }); + await UpdateFilter(_filter with { To = MoscowTime.EndOfMoscowDay(DateOnly.FromDateTime(v.Date)) }); } } @@ -519,14 +517,7 @@ private static string FormatDate(DateTimeOffset value) => value.ToString("yyyy-MM-dd"); - private string SportLabel(int code) => code switch - { - 6 => L["Sport.Basketball"], - 11 => L["Sport.Football"], - 22723 => L["Sport.Tennis"], - 43658 => L["Sport.Hockey"], - _ => $"Sport {code}", - }; + private string SportLabel(int code) => SportLabels.Resolve(L, code); private readonly record struct RatesSnap(decimal? Win1, decimal? Draw, decimal? Win2); diff --git a/src/Marathon.UI/Services/EventBrowsingState.cs b/src/Marathon.UI/Services/EventBrowsingState.cs index f3f6b00..9e15f5a 100644 --- a/src/Marathon.UI/Services/EventBrowsingState.cs +++ b/src/Marathon.UI/Services/EventBrowsingState.cs @@ -1,3 +1,5 @@ +using Marathon.Domain.ValueObjects; + namespace Marathon.UI.Services; /// @@ -54,8 +56,7 @@ public sealed class EventBrowsingState public static PageFilter Default(DateTime nowUtc) { // Default window: -1d .. +7d in Moscow time, the same TZ events use. - var moscow = TimeSpan.FromHours(3); - var midnight = new DateTimeOffset(nowUtc.Date, TimeSpan.Zero).ToOffset(moscow); + var midnight = new DateTimeOffset(nowUtc.Date, TimeSpan.Zero).ToOffset(MoscowTime.Offset); return new PageFilter( From: midnight.AddDays(-1), To: midnight.AddDays(7), diff --git a/src/Marathon.UI/Services/SportLabels.cs b/src/Marathon.UI/Services/SportLabels.cs new file mode 100644 index 0000000..3917ffc --- /dev/null +++ b/src/Marathon.UI/Services/SportLabels.cs @@ -0,0 +1,26 @@ +using System.Globalization; +using Marathon.UI.Resources; +using Microsoft.Extensions.Localization; + +namespace Marathon.UI.Services; + +/// +/// Single source of truth for the sport-code → display-label mapping that +/// every list/detail page in the RCL needs. Resolves the four canonical +/// data-sport-treeId values from the marathonbet.by spike (6 = basketball, +/// 11 = football, 22723 = tennis, 43658 = hockey) against shared +/// localization resources, with a stable invariant fallback for unknown +/// codes so the UI degrades gracefully if the bookmaker adds a new sport. +/// +public static class SportLabels +{ + public static string Resolve(IStringLocalizer localizer, int code) => + code switch + { + 6 => localizer["Sport.Basketball"], + 11 => localizer["Sport.Football"], + 22723 => localizer["Sport.Tennis"], + 43658 => localizer["Sport.Hockey"], + _ => string.Format(CultureInfo.InvariantCulture, "Sport {0}", code), + }; +}