12 — API Mapping (Scheduler Module)
- E1 — List repeatable jobs (implemented)
- E2 — Create custom repeatable job (implemented)
- E3 — Remove repeatable job (implemented)
- E4 — Run history
(proposed) - E5 — Job logs
(proposed) - E6 — Manual trigger
(proposed) - E7 — DLQ listing
(proposed) - E8 — DLQ replay / delete
(proposed) - Non-REST surface (the actual work)
- Client contract summary
Honest statement first: the scheduler has no monitoring API surface. Its public surface is three CRUD-ish endpoints on the repeatable-job registry; everything else the console needs (run history, logs, manual trigger, DLQ) is
(proposed)and would be new endpoints. The scheduler's real work happens through cron → BullMQ repeatables → queue internals, not REST. Wire contract per 00-shared/07 (base/api/v1, Bearer JWT, envelopes).
E1 — List repeatable jobs (implemented)
| Endpoint | GET /api/v1/scheduler (scheduler.controller.ts:23-28) |
| Guard | JwtAuthGuard + @Permissions('scheduler.read') (scheduler.controller.ts:18,24) |
| Request | none |
| Response | envelope data: {queue, name, pattern, tz}[] — iterates the 10 queues' getRepeatableJobs() (scheduler.service.ts:152-186), tz defaults 'UTC' (:181) |
| Notes | No status/last-run fields — console status badges are impossible from this endpoint alone (07 C1, 09 B2) |
E2 — Create custom repeatable job (implemented)
| Endpoint | POST /api/v1/scheduler (scheduler.controller.ts:30-35) |
| Guard | scheduler.create (:31) |
| Body | CreateScheduleDto — queue (11 whitelisted, dto:4-16), jobName, 5-field pattern (dto:20-24), optional payload/tz (dto:18-41) |
| Side effect | queue.add(jobName, payload, {repeat: {pattern, tz}, removeOnComplete {1h/100}, removeOnFail {7d}}) (scheduler.service.ts:198-208) |
| Errors | 400 invalid cron/queue; 401; 403; duplicate skipped silently (scheduler.service.ts:128-133) |
E3 — Remove repeatable job (implemented)
| Endpoint | DELETE /api/v1/scheduler?queue=&name=&pattern= (scheduler.controller.ts:37-46) |
| Guard | scheduler.delete (:38) |
| Side effect | removeRepeatable(jobName, {pattern, tz: 'UTC'}) (scheduler.service.ts:188-196); unknown queue → error from getQueue (:210-226) |
E4 — Run history (proposed)
GET /scheduler/runs?queue=&name= — last N runs (startedAt, status,
duration, error). No source today: BullMQ job state holds the data
(attemptsMade, finishedOn, failedReason per dlq.setup.ts:8-21),
retention window bounded by removeOnComplete {age:3600, count:100} and
removeOnFail {age: 7d} (scheduler.service.ts:145-146), failed jobs 14 d
global (bullmq.module.ts:63-64). Backs S2 + S1 badges.
E5 — Job logs (proposed)
GET /scheduler/jobs/:queue/:name/logs?level=&tail= — nestsjs-pino worker
logs (AGENTS.md stack) surfaced by jobId; no log-retrieval endpoint today.
Backs S4.
E6 — Manual trigger (proposed)
POST /scheduler/:queue/:name/trigger — re-enqueues the child job exactly
as the job class would (jobs/*.job.ts pattern: check-overdue,
send-daily-digest, send-payment-reminder, generate-attendance-report).
Must preserve correlationId so IdempotencyService dedup holds
(idempotency.service.ts:11-20). Backs S5.
E7 — DLQ listing (proposed)
GET /scheduler/dlq — records written by setupDlqListener when
attemptsMade >= (opts.attempts ?? 3) (dlq.setup.ts:8-9): originalQueue, originalJobId, originalJobName, data, failedReason, attemptsMade, failedAt
(:12-21). Backs S6.
E8 — DLQ replay / delete (proposed)
POST /scheduler/dlq/:id/retry (re-add to original queue, preserving
correlationId/tenantId), DELETE /scheduler/dlq/:id.
Non-REST surface (the actual work)
| Trigger | Mechanism | Target | Source |
|---|---|---|---|
| 10 default schedules | BullMQ repeatable queue.add(..., {repeat:{pattern,tz:'UTC'}}) | 10 queues | scheduler.service.ts:48-147 |
| Fan-out child jobs | direct queue.add from job classes | see table below | jobs/*.job.ts |
| Event-driven jobs | eventQueueMap route → QueueBridge | emails/push/in-app/audit-write/attendance-process/invoice-generate | event-queue-map.ts:6-43, queue-bridge.service.ts:40-75 |
| Failure path | retry 3× exp. 5 s → DLQ | per queue → dlq | bullmq.module.ts:60-65, dlq.setup.ts:5-27 |
| Repeatable | Cron (UTC) | Child job → queue |
|---|---|---|
overdue-scan | 0 6 * * * | check-overdue → invoice-generate (scheduler.service.ts:49-55, overdue-scan.job.ts:12-19) |
daily-digest | 0 9 * * * | send-daily-digest → emails (:56-62, daily-digest.job.ts:12-25) |
dashboard-rebuild | */5 * * * * | (payload-only) → cache-rebuild (:63-69) |
biometric-sync | */15 * * * * | → biometric-sync (:70-76) |
audit-flush | */1 * * * * | → audit-write (:77-83) |
retention-archive | 0 2 * * 0 | → tenant-purge (:84-90) |
fee-reminder | 0 8 * * * | per-invoice send-payment-reminder → payment-reminder (:91-97, fee-reminder.job.ts:16-47) |
attendance-report-daily | 0 7 * * * | generate-attendance-report → report-generate (:98-104, attendance-report.job.ts:13-28) |
admission-reminder-scan | 0 8 * * * | → admission-reminder (:105-111) |
admission-expiry-scan | 0 2 * * * | → admission-expiry (:112-118) |
Client contract summary
- Reads: E1 today; E4-E8 gated on
(proposed)endpoints — console builds against them as stubs behind a repository interface. - Tenant: scheduler registry is platform-level (
tenantId: 'system'trigger payloads,scheduler.service.ts:139) — console is superadmin surface, not tenant-scoped. - 401 → reauth; 403 → hide module; 429 → backoff poll (api tier, 00-shared/07 §4); 5xx → keep-last-good.
- Rate budget: 1 req/min poll = 60 req/h per console tab — fine.