Skip to content

feat(notifications): add scheduled email reports - #1841

Merged
github-actions[bot] merged 1 commit into
Smartdevs17:mainfrom
retkatmun:feat/scheduled-email-reports-1758
Oct 19, 2026
Merged

github-actions[bot] merged 1 commit into
Smartdevs17:mainfrom
retkatmun:feat/scheduled-email-reports-1758

Conversation

@retkatmun

Copy link
Copy Markdown
Contributor

Closes #1758

Summary

Merchants can now schedule recurring analytics summary emails (daily, weekly, or monthly) delivered via the existing EmailProvider (SendGrid or SES). Previously reports had to be pulled manually.

Changes

New files:

File Purpose
backend/services/notification/scheduledReportService.ts Core service: schedule CRUD, recipient management, email rendering, sending, and runDue() batch processing. Includes InMemoryReportStore with an injectable ReportStore interface for swap-in persistence.
backend/services/notification/scheduledReportRouter.ts Plain Node.js HTTP router mounted at /scheduled-reports. Routes: POST (create), GET (list), GET/:id, PATCH/:id, DELETE/:id, POST/:id/recipients, DELETE/:id/recipients, POST/:id/send-now.
backend/services/notification/jobs/scheduledReportJob.ts createScheduledReportPoller() (15-min setInterval loop), registerScheduledReportJob() (WeightedFairQueue), createScheduledReportHandler() (queue handler factory).
backend/services/notification/__tests__/scheduledReportService.test.ts 49 unit tests covering all public API paths.

Modified:

  • backend/server.ts — wires the service, router, and poller into the server lifecycle; poller is stopped cleanly on shutdown.

Design decisions

  • Decoupled from analytics. runDue() accepts a snapshotFor() callback so the service has no hard dependency on SubscriptionAnalyticsService — easy to test and swap.
  • Partial failure tolerance. sendReport() iterates recipients individually; one bad address doesn't block the rest; errors are collected in the result.
  • Clock-drift safety. nextSendAt() normalises to the start of the hour so minor drift doesn't cause a full-period skip.
  • Pluggable persistence. InMemoryReportStore is the default; a real DB store can be injected via the ReportStore interface without touching the service.
  • XSS hardening. escapeHtml() applied to all user-controlled strings in HTML output; subject is escaped once, not double-escaped.

Acceptance criteria (from issue)

  • Schedule configurable — frequency (daily/weekly/monthly), report type, start time, enable/disable
  • Recipients managed — add/remove via API, deduplicated, normalised to lowercase
  • Tested — 49 unit tests, all passing

Testing

npx jest --config jest.backend.config.js \
  --testMatch '**/backend/services/notification/__tests__/scheduledReportService.test.ts'

Test Suites: 1 passed
Tests:       49 passed, 0 failed

Covers: nextSendAt() for all frequencies, schedule CRUD validation, recipient management, email rendering for all 4 report types, HTML escaping, partial send failures, post-send timestamp advance, runDue() skipping disabled/empty schedules, and snapshot error isolation.

Implements issue Smartdevs17#1758. Merchants can now schedule recurring analytics
summary emails (daily / weekly / monthly) delivered via the existing
EmailProvider (SendGrid or SES).

New files
  backend/services/notification/scheduledReportService.ts
    - ScheduledReportService: CRUD for schedules, recipient management,
      buildReportEmail() (pure renderer), sendReport(), runDue()
    - InMemoryReportStore with injectable ReportStore interface for swap-in
    - nextSendAt() helper with clock-drift-safe hour normalisation
    - Report types: analytics_summary, mrr_breakdown, churn_report, full_report

  backend/services/notification/scheduledReportRouter.ts
    - Plain Node.js HTTP router (no framework) mounted at /scheduled-reports
    - POST / GET (list) / GET /:id / PATCH /:id / DELETE /:id
    - POST /:id/recipients  DELETE /:id/recipients
    - POST /:id/send-now (ad-hoc trigger)

  backend/services/notification/jobs/scheduledReportJob.ts
    - createScheduledReportPoller() — lightweight setInterval loop (15 min)
    - registerScheduledReportJob() — registers with WeightedFairQueue
    - createScheduledReportHandler() — queue handler factory

  backend/services/notification/__tests__/scheduledReportService.test.ts
    - 49 unit tests; all passing

Modified
  backend/server.ts — wires the service, router, and poller into the
    server lifecycle; poller stopped cleanly on shutdown

Closes Smartdevs17#1758
@github-actions
github-actions Bot merged commit 3427288 into Smartdevs17:main Oct 19, 2026
27 of 67 checks passed
@retkatmun
retkatmun deployed to security-review October 19, 2026 14:10 — with GitHub Actions Active

This branch was successfully deployed

1 active deployment
security-review — d01258b4 Deployed Oct 19, 2026 by retkatmun via Security Dashboard #1394
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add scheduled email reports

1 participant