Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

01 — Product Overview (Scheduler Module)

The Scheduler is the recurring-job engine of StudyLyon. It registers BullMQ repeatable jobs at boot and fans them out to per-domain queues. Derived from src/modules/scheduler/** (module, service, controller, DTO, jobs/*.job.ts), src/infrastructure/bullmq/ (queue constants, module, DLQ setup), src/infrastructure/workers/*.worker.ts, and docs/IMPLEMENTATION_PLAN.md. Nothing here is invented; plan-only capability is marked (planned), roadmap-only (forward-looking), analytics (proposed). Shared platform context: 00-shared/01.


1. What the module is

An internal infrastructure module whose job is to keep recurring work happening: daily fee reminders, attendance report generation, cache rebuilds, audit flushing, admission lifecycle scans, and retention cleanup. It has no user-facing feature surface — its "users" are other modules' queues and the platform operator who keeps it healthy.

The mechanism is BullMQ repeatable jobs, not @Cron: on module init the service registers 10 default repeatable jobs onto 10 queues (scheduler.service.ts:47-126), each queue.add(..., { repeat: { pattern, tz } }) (scheduler.service.ts:135-147). Registration is idempotent — an existing job with the same name+pattern is skipped (scheduler.service.ts:128-133).

2. Core domain facts (from source)

  • Trigger model — a repeatable job is { name, pattern, queue, queueName, tz } (scheduler.service.ts:11-18). Every job runs in UTC (:54, :62, :70, :76, :82, :89, :96, :103, :110, :117).
  • Job payload convention — every trigger enqueues { eventType, tenantId: 'system', correlationId: crypto.randomUUID(), actorId: 'scheduler' } (scheduler.service.ts:136-142). tenantId: 'system' means triggers are tenant-neutral; tenant scoping happens in the workers (see §7).
  • Retention on triggersremoveOnComplete: { age: 3600, count: 100 }, removeOnFail: { age: 86400 * 7 } (scheduler.service.ts:145-146).
  • Default schedules (all UTC, scheduler.service.ts:48-119):
Job nameCron patternMeaningTarget queue
overdue-scan0 6 * * *daily 06:00invoice-generate
daily-digest0 9 * * *daily 09:00emails
dashboard-rebuild*/5 * * * *every 5 mincache-rebuild
biometric-sync*/15 * * * *every 15 minbiometric-sync
audit-flush*/1 * * * *every minuteaudit-write
retention-archive0 2 * * 0Sun 02:00tenant-purge
fee-reminder0 8 * * *daily 08:00payment-reminder
attendance-report-daily0 7 * * *daily 07:00report-generate
admission-reminder-scan0 8 * * *daily 08:00admission-reminder
admission-expiry-scan0 2 * * *daily 02:00admission-expiry
  • Fan-out jobs — four job classes exist (scheduler.module.ts:15-22): OverdueScanJob (jobs/overdue-scan.job.ts:12-19, enqueues check-overdue), DailyDigestJob (jobs/daily-digest.job.ts:12-25, send-daily-digest), FeeReminderJob (jobs/fee-reminder.job.ts:16-47, one send-payment-reminder per invoice due within 3 days), AttendanceReportJob (jobs/attendance-report.job.ts:13-28, generate-attendance-report, type daily|weekly).
  • Custom schedules — any 5-field cron on 11 whitelisted queues (dto/create-schedule.dto.ts:4-28), created via POST /scheduler (scheduler.service.ts:198-208).
  • Admission lifecycle — two dedicated queues exist today: admission-reminder and admission-expiry (queue.constants.ts:15-16), with workers AdmissionReminderWorker / AdmissionExpiryWorker (bullmq.module.ts:18-20); admission workflow states (documents pending, interview) are (planned) per IMPLEMENTATION_PLAN.md:53-78.

3. Who uses it

ActorRelationship
Platform operator / superadminOwns scheduler health: list jobs, remove broken schedules, create custom schedules (scheduler.controller.ts:23-46)
School IT adminReceives scheduled outputs (daily digest, attendance reports, fee reminders) for their tenant
Other modulesTheir workers consume scheduler-triggered jobs off the shared queues
End users (students/parents/teachers)Only via delivered outputs (emails, reports, reminders) — never touch the scheduler

4. Scope in / out

In scope (implemented)Out of scope / gaps
10 default repeatable jobs registered at bootPer-tenant cron scheduling (triggers are tenantId: 'system', scheduler.service.ts:139)
List / create / remove repeatable jobs (scheduler.controller.ts:23-46)Run history, last-run/failed timestamps, logs — console (proposed), no endpoint
Manual trigger of a scheduleJob logs / DLQ viewer UI — (proposed)
Custom cron + queue whitelist (create-schedule.dto.ts:4-16)whatsapp, in-app, attendance-process, webhook-deliver are queues but not in the create whitelist (dto:4-16)
Retry + DLQ on queues (bullmq.module.ts:60-65, dlq.setup.ts:5-27)Report template scheduling — (planned) (IMPLEMENTATION_PLAN.md:233)
Retention trigger (retention-archive)Retention of documents (soft-delete cleanup, GDPR erasure, cold storage) — (planned) (IMPLEMENTATION_PLAN.md:176)
Admission reminder/expiry scansWebhook /retry /test /metrics /pause(planned) (IMPLEMENTATION_PLAN.md:177)

5. PRD native-app exclusion (flagged)

Per 00-shared/01 §9: PRODUCT_REQUIREMENTS_DOCUMENT.md:144 puts native mobile apps out of Phase 1 scope (roadmap Phase 3 plans a read-only companion). The decision recorded with the product owner is that these docs specify a Flutter admin console now, to the scheduler's (proposed) console API surface; any conflict with the web-first roadmap resolves in favor of these docs unless the roadmap is amended.

6. Client surface (screens) — see 05/06

Scheduled Jobs console — all (proposed): Jobs list · Job detail / run history · Create custom schedule · Manual trigger · Job logs · DLQ viewer. The only screens backed by real endpoints today are list/create/remove (scheduler.controller.ts:23-46).

7. Tenant context handling

Workers restore tenant context with tenantContext.run({...}, () => work) (report.worker.ts:18-30, finance.worker.ts:27-29, attendance.worker.ts:27-29, admission-reminder.worker.ts:30-32, admission-expiry.worker.ts, inapp.worker.ts:19-21). Gap: FeeReminderJob queries InvoiceRepository directly (fee-reminder.job.ts:20-23) without wrapping itself in tenantContext.run — tenant scoping of that query depends on whatever context is ambient in the scheduler process, while trigger payloads carry tenantId: 'system'. See 14_QA (tenant isolation).