feat(reports-p3): BullMQ render + email + schedule poll for report_runs
- new report-render.service.ts: renderReportRun(reportRunId) +
emailReportRun(reportRunId). Render path fetches the run row,
advances status to 'rendering', resolves the kind→fetcher+template
pair from REPORT_RENDER_MAP (dashboard→pipeline, clients→activity,
berths→occupancy, interests→revenue), generates the PDF, uploads to
storage, mirrors onto `files` so the standard download/attachment
surfaces serve it, and stamps storageKey + sizeBytes + status='complete'.
Failure path stamps 'failed' + errorMessage + compensating
storage.delete to keep blobs from orphaning. Email path resolves the
schedule's recipients + the rendered file via the standard
resolveAttachments port-isolation check, sends one message per
recipient via the existing sendEmail helper, and stamps emailedAt.
- reports worker (src/lib/queue/workers/reports.ts) gains 3 jobs:
- 'report-schedules-poll': scans report_schedules where enabled=true
AND nextRunAt <= now, mints a report_runs row per due schedule via
createReportRun (triggeredBy='schedule'), advances next_run_at via
nextRunFor() BEFORE enqueue so a downstream failure doesn't pin the
schedule on the same tick, then enqueues report-run-render.
- 'report-run-render': calls renderReportRun + auto-cascades into
report-run-email when the run was schedule-triggered.
- 'report-run-email': calls emailReportRun.
These coexist with the legacy 'report-scheduler' + 'generate-report'
jobs operating on scheduled_reports/generated_reports.
- scheduler.ts registers 'report-schedules-poll' on a 1-minute cron so
the system catches due schedules even when no API event nudges them.
- POST /api/v1/reports/runs now enqueues 'report-run-render' after
createReportRun. Enqueue failures are logged + swallowed so the API
still returns 201; the schedule poll picks pending rows up as a
safety net.
Verified: tsc clean, 1493/1493 vitest.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@ import { NextResponse } from 'next/server';
|
||||
import { withAuth, withPermission } from '@/lib/api/helpers';
|
||||
import { parseBody, parseQuery } from '@/lib/api/route-helpers';
|
||||
import { errorResponse } from '@/lib/errors';
|
||||
import { getQueue } from '@/lib/queue';
|
||||
import { createReportRun, listReportRuns } from '@/lib/services/report-runs.service';
|
||||
import { createReportRunSchema, listReportRunsSchema } from '@/lib/validators/reports';
|
||||
|
||||
@@ -49,6 +50,25 @@ export const POST = withAuth(
|
||||
userAgent: ctx.userAgent,
|
||||
},
|
||||
});
|
||||
// P3: hand off to the BullMQ render worker. Failure to enqueue
|
||||
// doesn't abort the create — the schedule poll picks up pending
|
||||
// rows on the next tick as a safety net.
|
||||
try {
|
||||
await getQueue('reports').add(
|
||||
'report-run-render',
|
||||
{ reportRunId: row.id },
|
||||
{ jobId: `report-run-render:${row.id}` },
|
||||
);
|
||||
} catch (queueErr) {
|
||||
// Logged but swallowed so the API still returns 201 with the row.
|
||||
// The pending row stays visible in /reports/runs so the rep can
|
||||
// see it queued; a future ops job can rescue stuck pendings.
|
||||
const { logger } = await import('@/lib/logger');
|
||||
logger.error(
|
||||
{ queueErr, reportRunId: row.id },
|
||||
'Failed to enqueue render after createReportRun',
|
||||
);
|
||||
}
|
||||
return NextResponse.json({ data: row }, { status: 201 });
|
||||
} catch (error) {
|
||||
return errorResponse(error);
|
||||
|
||||
Reference in New Issue
Block a user