Appearance
SubscriptionReminderManager
Manages subscription renewal reminder emails with deduplication tracking.
Purpose
Sends time-based email reminders as paid subscriptions approach expiration. Only regular (paid) subscriptions are processed — trial and sponsored subscriptions are silently skipped.
High-Level Design
Scheduling via TaskScheduler
The manager does not run on its own schedule. A SubscriptionReminderHandler task is registered with TaskScheduler and runs every 24 hours (first check 1 hour after DO initialization). Each execution calls processReminders(), which is a synchronous single-pass evaluation.
DO alarm → TaskScheduler.processDueTasks()
→ SubscriptionReminderHandler.execute()
→ subscriptionReminderManager.processReminders()
→ reschedule in 24hOn failure, the task handler returns shouldRetry: true for automatic retry.
Reminder Window Logic
Each processReminders() call computes daysUntilExpiry from the current subscription's cycleEndDate and evaluates five reminder types in order:
| Reminder | Condition | Email Type |
|---|---|---|
7d | 3 < daysUntilExpiry ≤ 7 | SUBSCRIPTION_EXPIRING_7D |
3d | 1 < daysUntilExpiry ≤ 3 | SUBSCRIPTION_EXPIRING_3D |
1d | 0 ≤ daysUntilExpiry ≤ 1 | SUBSCRIPTION_EXPIRING_1D |
grace | Status is in grace period | SUBSCRIPTION_GRACE_PERIOD |
expired | Status is expired | SUBSCRIPTION_EXPIRED |
Wind-down subscriptions (cancelled but still active) still receive reminders — the user may choose to re-subscribe.
Deduplication Strategy
Each reminder type is sent at most once per subscription cycle. The approach:
- Batch fetch —
getSentReminderTypes()loads all sent reminder types for the current subscription into aSet<string>in a single DB query - O(1) lookup — each window check tests membership before sending
- Immediate Set update — after
markReminderSent()persists to DB, the in-memory Set is also updated, preventing duplicate sends within the same processing cycle
Lifecycle Reset
When a subscription renews or upgrades, PlanChangeManager calls clearReminders(subscriptionId) to delete all reminder records for that subscription. This ensures the new billing cycle starts with a clean slate.
Edge Cases
- Day 0 inclusion: The 1-day window uses
>= 0(not> 0) so reminders fire on the actual expiry day - Email payload: Template data includes plan name, days remaining, and renewal/resubscribe URLs — assembled by
EmailNotificationManager - 24h granularity: Because the task runs daily, a subscription could expire between checks. The window ranges overlap enough (7→3→1→expired→grace) to avoid missed reminders in practice
See Also
- SubscriptionManager - Subscription lifecycle management
- EmailNotificationManager - Email queuing and delivery
- UserPaperTradePortfolio - Parent DO