feat(streak-freeze): freeze packs bought with Cores, auto-applied on missed days#4011
Conversation
…ery GraphQL surface Adds the data model and GraphQL surface for automated streak freeze: a freezesAvailable counter on user_streak, a streak_freeze Product type (seeded with 3 placeholder-priced packs), a freeze UserStreakAction type, and an optOutStreakFreeze setting. Exposes streakFreezeProducts, userStreakFreezeDates, and purchaseStreakFreeze (Cores purchase with a 5-freeze stockpile cap) following the existing recoverStreak/award patterns. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Wires freeze consumption into both reset paths (the hourly cron and the lazy checkAndClearUserStreak read path) so behavior is identical: when a reset would occur, compute the missed days beyond the existing grace period and, if the user's balance covers all of them (has Cores access, hasn't opted out), consume one freeze per missed day and emit one api.v1.user-streak-updated event per frozen day instead of clearing the streak. Otherwise the existing reset-and-recover flow is unchanged. The gap calculation now folds in the most recent freeze action (in addition to the existing recover action) so a freeze consumed in one cron run advances the "last covered day" and the next run does not re-freeze or double-count the same missed day. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds streak_freeze_used and streak_freeze_depleted notification types, title/builder entries, and a worker subscribed to the existing api.v1.user-streak-updated topic that reacts only to the freeze discriminator payload (ignores the regular CDC-driven updates other subscribers on that topic care about). Emits StreakFreezeUsed per frozen day, plus StreakFreezeDepleted when no freezes remain; uniqueKey is the frozen day so repeated cron runs stay idempotent at the notification layer. No email template yet, so email defaults to muted like other placeholder notification types. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
🍹 The Update (preview) for dailydotdev/api/prod (at 21cf0bb) was successful. Resource Changes Name Type Operation
~ vpc-native-daily-digest-cron kubernetes:batch/v1:CronJob update
~ vpc-native-temporal-deployment kubernetes:apps/v1:Deployment update
~ vpc-native-user-profile-updated-sync-cron kubernetes:batch/v1:CronJob update
~ vpc-native-rotate-daily-quests-cron kubernetes:batch/v1:CronJob update
~ vpc-native-update-views-cron kubernetes:batch/v1:CronJob update
~ vpc-native-clean-zombie-users-cron kubernetes:batch/v1:CronJob update
~ vpc-native-bg-deployment kubernetes:apps/v1:Deployment update
~ vpc-native-update-achievement-rarity-cron kubernetes:batch/v1:CronJob update
~ vpc-native-validate-active-users-cron kubernetes:batch/v1:CronJob update
~ vpc-native-user-posts-analytics-refresh-cron kubernetes:batch/v1:CronJob update
~ vpc-native-clean-expired-better-auth-sessions-cron kubernetes:batch/v1:CronJob update
~ vpc-native-private-deployment kubernetes:apps/v1:Deployment update
~ vpc-native-update-tags-str-cron kubernetes:batch/v1:CronJob update
~ vpc-native-update-current-streak-cron kubernetes:batch/v1:CronJob update
+ vpc-native-api-db-migration-0c0a6e9c kubernetes:batch/v1:Job create
~ vpc-native-check-analytics-report-cron kubernetes:batch/v1:CronJob update
~ vpc-native-rotate-weekly-quests-cron kubernetes:batch/v1:CronJob update
~ vpc-native-channel-highlights-cron kubernetes:batch/v1:CronJob update
~ vpc-native-clean-zombie-user-companies-cron kubernetes:batch/v1:CronJob update
~ vpc-native-update-source-public-threshold-cron kubernetes:batch/v1:CronJob update
~ vpc-native-channel-digests-cron kubernetes:batch/v1:CronJob update
~ vpc-native-clean-channel-highlights-cron kubernetes:batch/v1:CronJob update
~ vpc-native-clean-stale-user-transactions-cron kubernetes:batch/v1:CronJob update
~ vpc-native-expire-super-agent-trial-cron kubernetes:batch/v1:CronJob update
~ vpc-native-sync-subscription-with-cio-cron kubernetes:batch/v1:CronJob update
~ vpc-native-clean-old-notifications-cron kubernetes:batch/v1:CronJob update
~ vpc-native-clean-zombie-images-cron kubernetes:batch/v1:CronJob update
~ vpc-native-update-trending-cron kubernetes:batch/v1:CronJob update
~ vpc-native-generic-referral-reminder-cron kubernetes:batch/v1:CronJob update
~ vpc-native-user-profile-analytics-history-clickhouse-cron kubernetes:batch/v1:CronJob update
- vpc-native-api-db-migration-84decd9a kubernetes:batch/v1:Job delete
~ vpc-native-materialize-monthly-best-post-archives-cron kubernetes:batch/v1:CronJob update
~ vpc-native-deployment kubernetes:apps/v1:Deployment update
- vpc-native-api-clickhouse-migration-84decd9a kubernetes:batch/v1:Job delete
~ vpc-native-clean-zombie-opportunities-cron kubernetes:batch/v1:CronJob update
~ vpc-native-squad-posts-analytics-refresh-cron kubernetes:batch/v1:CronJob update
~ vpc-native-personalized-digest-cron kubernetes:batch/v1:CronJob update
~ vpc-native-hourly-notification-cron kubernetes:batch/v1:CronJob update
~ vpc-native-post-analytics-history-day-clickhouse-cron kubernetes:batch/v1:CronJob update
~ vpc-native-user-profile-analytics-clickhouse-cron kubernetes:batch/v1:CronJob update
~ vpc-native-subscription-anniversary-achievements-cron kubernetes:batch/v1:CronJob update
... and 17 other changes |
# Conflicts: # src/workers/newNotificationV2Mail.ts
| // Attempts to consume one streak freeze per missed day. Returns true when the | ||
| // streak was frozen (and therefore should NOT be reset), false otherwise (the | ||
| // caller is expected to fall back to resetting the streak). | ||
| export const tryConsumeStreakFreeze = async ( |
There was a problem hiding this comment.
should this iterate over multiple days instead of restoring at specific point in time, can you even miss multiple days, it happens day by day no?
So i would expect each day when streak is reset, we try and consume single streak freeze, no multiple ones via missedDays.
There was a problem hiding this comment.
In the healthy-cron case it is one day at a time — the hourly cron triggers on the first day beyond grace, so missedDays has length 1 and one freeze is consumed per day. The array form only kicks in on the catch-up paths (lazy checkAndClearUserStreak when the cron didn't get there, cron downtime), where the gap can span several days at once. There, freezing a single day wouldn't keep the streak alive — the remaining gap still fails checkUserStreak on the next evaluation — so we either cover the whole gap or consume nothing and fall back to reset/restore. Each consumed freeze is still recorded as a per-day dated UserStreakAction so lastFreezeAt bookkeeping stays day-accurate.
There was a problem hiding this comment.
What happens if user bought 3 resets but is missing 5 days? Do we consume or leave since user loses streak either way?
There was a problem hiding this comment.
We leave them. In tryConsumeStreakFreeze (src/common/users.ts), the guard freezesAvailable < missedDays.length returns false before touching anything — no
transaction runs, no UserStreakAction rows are written, no events fire. So with 3 freezes and 5 missed days: the user keeps all 3 freezes, the streak
resets, and the recover flow (100 Cores restore) becomes their fallback — after which the 3 freezes are still banked for next time.
There was a problem hiding this comment.
But yeah worth noting it likely never would happen (only if user buys streak freeze after already loosing so many basically)
There was a problem hiding this comment.
ok, sounds good then, lets see how it goes
…agsSchema The updateNotificationSettings zod schema strips unknown keys, so the new streak_freeze_used/streak_freeze_depleted preferences were silently dropped on write. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
@claude review |
|
Claude finished @capJavert's task in 5m 36s —— View job Claude Code ReviewReviewed the full diff ( 🟡 Worth a look1. Notifications are published inside the cron's DB transaction — 2. Multi-day catch-up emits one notification per day — 3. 🟢 Nits
❓ Question — missed-day dates around weekendsIn Nothing here is blocking; #1–#3 are the ones I'd weigh before merge. Nice work on the test coverage (cron, purchase, and worker paths are all exercised). |
…ent, reject restricted products - tryConsumeStreakFreeze now returns freeze events instead of publishing them, so the cron publishes after its transaction commits — a rollback can no longer emit notifications for freezes that were never consumed - the freeze decrement is a guarded atomic UPDATE instead of writing a snapshot-derived value, so concurrent purchases aren't clobbered - purchaseStreakFreeze rejects restricted products, matching the streakFreezeProducts query filter Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Addressed the review feedback in 21e985b7 (rebased: see latest):
🤖 Generated with Claude Code |
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
What
Backend for streak freezes: users buy freeze packs with Cores (cap 5); the streak cron consumes a freeze instead of resetting when a day is missed (grace-first), with an opt-out setting.
UserStreak.freezesAvailablecolumn + migration,UserStreakActionfreeze records, seed productsfreezesAvailableexposed onuserStreakupdateCurrentStreakcron auto-consumes a freeze on miss instead of resettingSettings.optOutStreakFreeze+ migrationstreak_freeze_usedandstreak_freeze_depleted(in-app subscribed, email muted)Frontend
dailydotdev/apps#6340
🤖 Generated with Claude Code