From 7e1b9164f647d1d7e70df99261ac343b3f2a9009 Mon Sep 17 00:00:00 2001 From: frizat Date: Sun, 4 Oct 2026 15:31:47 -0500 Subject: [PATCH] Never drop a push for lack of a matching sport app A user with only the CFB app installed missed an NFL bloody alert: per-sport routing filtered out their only device. Prefer the matching app; fall back to any device the user has. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01DAWjtJgXaMYbzZXYJjTEYs --- Client.React/CHANGELOG.md | 1 + Server.UnitTests/NotificationDispatcherTests.cs | 16 ++++++++++++++++ Server/Services/NotificationDispatcher.cs | 9 ++++++--- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/Client.React/CHANGELOG.md b/Client.React/CHANGELOG.md index a42e08e0..3a2274d5 100644 --- a/Client.React/CHANGELOG.md +++ b/Client.React/CHANGELOG.md @@ -6,6 +6,7 @@ Format: `## ` release headings and single-line `- ` bullets only — no bold, li ## 2026-10-04 +- Fixed: if you only have one IV League app installed (NFL or college), you now get alerts for both sports there instead of missing the other sport's alerts - Fixed: NFL notifications also arrived on the college app (and vice versa) when both were installed — each app now only gets its own sport's alerts - Fixed: the first week-result notification run of the season sent a push for every already-finished week (Weeks 1-4) — a week's result can now only be pushed within 48 hours of that week ending, never later - Improved: in dark mode, team logos switch to ESPN's dark-background versions (NFL and college), so logos with dark lettering like Ohio State's stay readable diff --git a/Server.UnitTests/NotificationDispatcherTests.cs b/Server.UnitTests/NotificationDispatcherTests.cs index 2033d52e..e4af7f2f 100644 --- a/Server.UnitTests/NotificationDispatcherTests.cs +++ b/Server.UnitTests/NotificationDispatcherTests.cs @@ -113,4 +113,20 @@ await BuildDispatcher(isDevelopment: false, railwayEnvironmentName: "production" await _pushSender.Received(1).SendAsync(Arg.Is(s => s.Endpoint == "https://push.example/old"), Arg.Any(), Arg.Any()); await _pushSender.DidNotReceive().SendAsync(Arg.Is(s => s.Endpoint == excluded), Arg.Any(), Arg.Any()); } + + // Many users install only one app but are in both sports' leagues — a push must never be + // dropped just because the user has no app for that sport (a real NFL alert was, 2026-10-04). + [Fact] + public async Task DispatchAsync_UserWithOnlyTheOtherSportsApp_StillGetsThePush() + { + _preferencesService.GetAsync("user-1").Returns(new NotificationPreferencesDto { NotifyWeekResult = true }); + _subscriptionService.GetForUserAsync("user-1").Returns([ + new PushSubscription { Id = 1, UserId = "user-1", Endpoint = "https://push.example/cfb", P256dh = "p", Auth = "a", Sport = LeagueType.Cfb }, + ]); + + await BuildDispatcher(isDevelopment: false, railwayEnvironmentName: "production") + .DispatchAsync("user-1", p => p.NotifyWeekResult, new PushPayload("T", "B", Sport: LeagueType.Nfl)); + + await _pushSender.Received(1).SendAsync(Arg.Is(s => s.Endpoint == "https://push.example/cfb"), Arg.Any(), Arg.Any()); + } } diff --git a/Server/Services/NotificationDispatcher.cs b/Server/Services/NotificationDispatcher.cs index 779dce68..650b7980 100644 --- a/Server/Services/NotificationDispatcher.cs +++ b/Server/Services/NotificationDispatcher.cs @@ -22,11 +22,14 @@ public async Task DispatchAsync(string userId, Func payload.Sport is null || s.Sport is null || s.Sport == payload.Sport) .ToList(); + var subscriptions = matching.Count > 0 ? matching : allSubscriptions; if (subscriptions.Count == 0) return; if (!DeploymentEnvironment.IsProduction(environment))