01 — Product Overview (Scheduler Module)
- 1. What the module is
- 2. Core domain facts (from source)
- 3. Who uses it
- 4. Scope in / out
- 5. PRD native-app exclusion (flagged)
- 6. Client surface (screens) — see 05/06
- 7. Tenant context handling
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, anddocs/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 triggers —
removeOnComplete: { age: 3600, count: 100 },removeOnFail: { age: 86400 * 7 }(scheduler.service.ts:145-146). - Default schedules (all UTC,
scheduler.service.ts:48-119):
| Job name | Cron pattern | Meaning | Target queue |
|---|---|---|---|
overdue-scan | 0 6 * * * | daily 06:00 | invoice-generate |
daily-digest | 0 9 * * * | daily 09:00 | emails |
dashboard-rebuild | */5 * * * * | every 5 min | cache-rebuild |
biometric-sync | */15 * * * * | every 15 min | biometric-sync |
audit-flush | */1 * * * * | every minute | audit-write |
retention-archive | 0 2 * * 0 | Sun 02:00 | tenant-purge |
fee-reminder | 0 8 * * * | daily 08:00 | payment-reminder |
attendance-report-daily | 0 7 * * * | daily 07:00 | report-generate |
admission-reminder-scan | 0 8 * * * | daily 08:00 | admission-reminder |
admission-expiry-scan | 0 2 * * * | daily 02:00 | admission-expiry |
- Fan-out jobs — four job classes exist
(
scheduler.module.ts:15-22):OverdueScanJob(jobs/overdue-scan.job.ts:12-19, enqueuescheck-overdue),DailyDigestJob(jobs/daily-digest.job.ts:12-25,send-daily-digest),FeeReminderJob(jobs/fee-reminder.job.ts:16-47, onesend-payment-reminderper invoice due within 3 days),AttendanceReportJob(jobs/attendance-report.job.ts:13-28,generate-attendance-report, typedaily|weekly). - Custom schedules — any 5-field cron on 11 whitelisted queues
(
dto/create-schedule.dto.ts:4-28), created viaPOST /scheduler(scheduler.service.ts:198-208). - Admission lifecycle — two dedicated queues exist today:
admission-reminderandadmission-expiry(queue.constants.ts:15-16), with workersAdmissionReminderWorker/AdmissionExpiryWorker(bullmq.module.ts:18-20); admission workflow states (documents pending, interview) are(planned)perIMPLEMENTATION_PLAN.md:53-78.
3. Who uses it
| Actor | Relationship |
|---|---|
| Platform operator / superadmin | Owns scheduler health: list jobs, remove broken schedules, create custom schedules (scheduler.controller.ts:23-46) |
| School IT admin | Receives scheduled outputs (daily digest, attendance reports, fee reminders) for their tenant |
| Other modules | Their 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 boot | Per-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 schedule | Job 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 scans | Webhook /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).