Skip to content

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 24h

On 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:

ReminderConditionEmail Type
7d3 < daysUntilExpiry ≤ 7SUBSCRIPTION_EXPIRING_7D
3d1 < daysUntilExpiry ≤ 3SUBSCRIPTION_EXPIRING_3D
1d0 ≤ daysUntilExpiry ≤ 1SUBSCRIPTION_EXPIRING_1D
graceStatus is in grace periodSUBSCRIPTION_GRACE_PERIOD
expiredStatus is expiredSUBSCRIPTION_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:

  1. Batch fetchgetSentReminderTypes() loads all sent reminder types for the current subscription into a Set<string> in a single DB query
  2. O(1) lookup — each window check tests membership before sending
  3. 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