12 — API Mapping (Reports Module)
- 0. Module-wide request envelope & client policy
- E1 — Generate a report
- E2 — Get job status and result (polling contract)
- E3 — Download result
(planned) - E4 — List jobs (needed by S3)
- E5 — Files (delivery path for future exports)
(planned) - E6 — Scheduler (scheduled reports)
- Worker contract (server-side context)
- Client-side error mapping table
- Optimistic / undo
Exact wire contract for every screen → endpoint. Base
/api/v1; envelope per 00-shared/07 andresponse-envelope.interceptor.ts/http-exception.filter.ts. Endpoints fromreports.controller.ts; business rules fromreports.service.ts,report-job.repository.ts,report-job.schema.ts. Controller guard chain:JwtAuthGuardonly (reports.controller.ts:9).
0. Module-wide request envelope & client policy
| Aspect | Contract |
|---|---|
| Base | https://api.<domain>/api/v1 |
| Headers | Authorization: Bearer <accessToken>; x-request-id; Content-Type: application/json |
| Success | {success:true, message:"OK", data, timestamp, requestId} (response-envelope.interceptor.ts:48-53) |
| Error | {success:false, message, error:{code,details?}, timestamp, requestId} (http-exception.filter.ts:27-35) |
| Tenancy | tenantId never in body — from JWT via TenantContextService; repository tenant-scoped (base.repository.ts:21-23); worker restores tenant context (report.worker.ts:20-29) |
| RBAC | ⚠ Not enforced on reports endpoints. report.generate / report.read exist (permissions.constants.ts:39-40) but the controller carries only @UseGuards(JwtAuthGuard) (reports.controller.ts:9) and no @Permissions decorator. Client must hide/disable by permission list until server enforces (OQ — see 14 §9). |
| Polling | No server push; client polls GET /reports/:jobId per 10 §1 |
E1 — Generate a report
| Endpoint | POST /reports/generate (reports.controller.ts:14-18) |
| Request | GenerateReportDto (generate-report.dto.ts:5-34): type (req, enum report_card|attendance_summary|fee_summary — report-job.schema.ts:7-11), studentId?/classId?/examId? (@IsMongoId), startDate?/endDate? (@IsString) |
| Success | 201 (Nest default — blueprint says 202, 04-Modules/Reports.md:48; code returns the default, gap) data: {jobId, status:"queued"} (reports.service.ts:43) |
| Semantics | Creates report_jobs doc (:31-34), enqueues BullMQ job generate on report-generate queue with {jobId, tenantId, type, params} (:36-41). No idempotency key — each POST = new job |
| Errors | 400 (enum/mongo-id validation); 401; 429; 5xx (queue down) |
| Screen | S2 (06 §S2) |
E2 — Get job status and result (polling contract)
| Endpoint | GET /reports/:jobId (reports.controller.ts:20-24) |
| Success | 200 data: ReportJobDocument — full doc (report-job.schema.ts:21-43) incl. type, status, params, result?, error?, completedAt?, createdAt |
| 404 | RESOURCE_NOT_FOUND, Report job not found. (reports.service.ts:48) — tenant-scoped; cross-tenant id also 404s |
| Polling | Client polls every 2 s while status ∈ {queued, processing}; terminal statuses completed/failed stop the cycle (10 §1); 404 → stop |
| Screen | S4/S5 (06 §S4-S5) |
E3 — Download result (planned)
| Endpoint | GET /reports/:jobId/download — blueprint only (04-Modules/Reports.md:26) |
| Status | NOT in code. No controller route, no PDF/CSV serializer, no file write. Blueprint intent: streamed downloads (:49), files under sl/{tenantId}/reports/{uuid} with TTL cleanup (:50) |
| Interim | Until it lands: results are JSON on the job doc (E2); export CTAs hidden (06 §S5) |
E4 — List jobs (needed by S3)
| Endpoint | None. Index {tenantId, createdAt:-1} exists (report-job.schema.ts:48) but no GET /reports route |
| Client | local persisted history of submitted jobIds; (forward-looking) server GET /reports?status= |
E5 — Files (delivery path for future exports) (planned)
| Upload | POST /files/upload (multipart, file.upload — files.controller.ts:29-41) |
| Read/download | GET /files/:id (file.read, :49-53); GET /files/:id/download (file.read, :55-64) → Content-Type + Content-Disposition: attachment; filename="…" streamed buffer (files.service.ts:58-64) |
| Delete | DELETE /files/:id (file.delete, :66-71) — soft delete |
| Note | Report generation does NOT write files today; E3 will need this path |
E6 — Scheduler (scheduled reports)
| Endpoint | none (admin API (planned), perms scheduler.read/create/delete, permissions.constants.ts:94-96) |
| Behavior | AttendanceReportJob.execute enqueues generate-attendance-report on the report-generate queue (attendance-report.job.ts:13-24); scheduler service wires queue QUEUE.REPORT_GENERATE (scheduler.service.ts:32,102,163,219) |
| Payload | {eventType:"attendance-report", tenantId, correlationId, actorId:"scheduler", payload:{reportType:"daily"|"weekly"}} (attendance-report.job.ts:16-21) |
| Note | Enqueued jobs use jobName generate-attendance-report; ReportsService.executeJob is reached via the worker (report.worker.ts:34) and switches on job doc type (reports.service.ts:64-76) — scheduled jobs must land as attendance_summary jobs to be processed |
Worker contract (server-side context)
- Queue:
QUEUE.REPORT_GENERATE = 'report-generate'(queue.constants.ts:10). - Processor:
@Processor('report-generate')(report.worker.ts:7);process()restores tenant context thenreportsService.executeJob(jobId)(:18-35). executeJob: markProcessing → type switch (report_card/attendance_summary/fee_summary—reports.service.ts:64-76) → markCompleted w/ result, or markFailed w/ error (:78-82; repository:21-47).eventQueueMaphas NO report route —ReportGeneratednotifications(planned)(event-queue-map.ts).
Client-side error mapping table
| Screen | code | UI |
|---|---|---|
| S2 submit | 400 | inline field error |
| S2 submit | 401 | silent refresh → resubmit |
| S2 submit | 429 | countdown, no auto-retry |
| S2 submit | 5xx | snackbar + form kept (re-POST safe — new job per POST) |
| S4 poll | 404 | not-found state, back |
| S4 poll | 429 | pause poll 30 s |
| S4 poll | 5xx/network | banner + manual retry (10 §1) |
| any (future) | 403 | shared 403 screen — server emits none today (no @Permissions) |
Optimistic / undo
- Generate: no optimistic result (async); navigate on 201 only.
- Retry: new job; previous failed job remains in history (audit trail).
- No undo anywhere — jobs are immutable once created.