feat(api): auto-activate streak freeze on inactive day (#108) - #179
Conversation
Add a dedicated StreakFreezeAutoActivationService BackgroundService that auto-activates a streak freeze for a Pro user who held an active streak but logged nothing on their fully-elapsed local "yesterday". - Mirrors SlipAlertSchedulerService / HabitDueDateAdvancementService: poll interval, conservative UTC pre-filter, authoritative per-user TimeZoneHelper local-yesterday guard, single SaveChanges per tick. - Pro-only (matches ActivateStreakFreezeCommand). Spends one freeze per missed day, bounded by MaxStreakFreezesAccumulated (inventory) and MaxStreakFreezesPerMonth (monthly). - Presence-based: inserting a StreakFreeze row for the missed date preserves the streak on the next on-read RecalculateAsync; no direct streak mutation. - Idempotent: new SentStreakFreezeAlert guard entity (unique UserId+FrozenDate) plus the existing StreakFreeze unique index, both re-checked before spending. - Notifies via in-app Notification + push (IPushNotificationService); localized copy via LocaleHelper, mirroring GoalDeadlineNotificationService. - EF migration AddSentStreakFreezeAlert; account reset purges the guard table; registered in ServiceCollectionExtensions and BackgroundServiceHealthCheck. Unit tests cover the eligibility predicate, local-yesterday computation, interval default, notification copy, and the guard entity. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
One correctness bug: LoadRecentCompletionsAsync omits !h.IsDeleted from the habit filter, but UserStreakService.LoadStreakDataAsync (the source of truth for streak computation) requires both !h.IsDeleted && !h.IsBadHabit. A user who logged a now-deleted habit on the missed date will be treated as 'active' by this service, preventing the auto-freeze — yet UserStreakService won't count that log, so the streak breaks anyway. One-character fix: add && !h.IsDeleted to the WHERE predicate on line 108. Everything else looks solid: idempotency design, monthly cap, Pro guard, migration, health-check registration, and unit test coverage are all correct.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
The second commit removed the manual ActivateStreakFreezeCommand cleanly, but the one correctness bug I flagged in my prior review was not addressed: LoadRecentCompletionsAsync still omits !h.IsDeleted from the habit WHERE predicate (line 108). One-character fix — see inline comment. Everything else is solid.
…ck (#108) Aligns StreakFreezeAutoActivationService.LoadRecentCompletionsAsync with UserStreakService.LoadStreakDataAsync by filtering out soft-deleted habits when computing recent completions, so a deleted habit's log can no longer count as activity for a date and suppress the auto-freeze. Adds DB-backed tests locking the LoadRecentCompletionsAsync contract for soft-deleted and live habit logs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
The third commit cleanly resolves the correctness bug I flagged in both prior reviews: LoadRecentCompletionsAsync now filters !h.IsDeleted && !h.IsBadHabit on the habit predicate (line 108), matching the UserStreakService source of truth, and two DB-backed unit tests lock that contract against regression. The rest of the implementation holds up on re-read: idempotency is solid (double-guard via StreakFreeze unique index + SentStreakFreezeAlert), the monthly cap window is correct, Pro-only guard is enforced in memory after the conservative UTC pre-filter, SentStreakFreezeAlerts is purged in AccountResetRepository, the health check entry uses a 3× staleness threshold, and the migration/EF config is consistent with the other guard-entity pattern in the codebase. Good to merge.
Refs thomasluizon/orbit-ui-mobile#108
What
A dedicated
StreakFreezeAutoActivationServiceBackgroundService that auto-activates a streak freeze for a Pro user who held an active streak but logged nothing on their fully-elapsed local "yesterday". The paired UI PR makes the Today-header streak badge tappable.How
SlipAlertSchedulerService/HabitDueDateAdvancementService: configurable poll interval (BackgroundServices:StreakFreezeIntervalMinutes, default 60), conservative UTCAddDays(-1)pre-filter, then authoritative per-userTimeZoneHelperlocal-yesterday guard; singleSaveChangesAsyncper tick.user.HasProAccess, matchingActivateStreakFreezeCommand). Spends one freeze per missed day, bounded byMaxStreakFreezesAccumulated(inventory viaUser.ConsumeStreakFreeze()) andMaxStreakFreezesPerMonth(calendar-month window).StreakFreezerow for the missed date is sufficient —UserStreakService.ComputeCurrentStreaktreats any freeze-covered date as covered, so the next on-readRecalculateAsyncpreservesCurrentStreakwithout any direct mutation here.SentStreakFreezeAlertguard entity (unique(UserId, FrozenDate)) + the existingStreakFreezeunique(UserId, UsedOnDate)index, both batch-loaded and re-checked before spending → no double-spend, no duplicate notification across polls.Notification.Create+ push (IPushNotificationService.SendToUserAsync), deep-linking to/streak. Copy is server-generated and localized viaLocaleHelper.IsPortuguese, mirroringGoalDeadlineNotificationService(no AI dependency).AddSentStreakFreezeAlert(table + unique index + cascade FK).AccountResetRepositorypurges the new guard table. Registered inServiceCollectionExtensionsandBackgroundServiceHealthCheck.HabitDueDateAdvancementService— kept dedicated so streak logic never re-hides overdue habits.Rebase note
The
ServiceCollectionExtensions.cshosted-services region (~345) differs from the regions touched by #78 (Stripe ~308-326) and #79 (AI-tools ~153-198) — this is a trivial rebase at merge.Tests / validation
dotnet buildclean (0 errors).dotnet testunit projects green: Infrastructure.Tests 931, Domain.Tests 344, Application.Tests 1679. The freeze-preserves-streak invariant is already covered by existingUserStreakServiceTests(not duplicated).Jwt:SecretKey+ DB) — flagged as manual.Paired UI PR
thomasluizon/orbit-ui-mobile#129