Reports Module — Admin
Pluggable, registry-driven operational reports (orders, discount redemptions, customers) with date-ranged paginated views and async CSV/XLSX file exports.
A pluggable, registry-driven reporting surface: date-ranged, row-level operational reports (orders, discount redemptions, customers) with an on-screen paginated view and async CSV/XLSX file exports.
Source:
api-modules/reports/src/controllers/admin-report.controller.ts+admin-report-export.controller.ts.Optional plugin. Removing
ReportsModule.forRoot({ reports: [...] })fromapp.module.tsdisables every report + export route, stops the export worker and its retention cron, and leaves thereport_exporttable inert.
How it works
Each report is a ReportDefinition provider (key, title, description, dateField, columns, filters, filtersSchema, run()) registered via ReportsModule.forRoot({ reports: [...] }). The HTTP surface is generic — a report is addressed by its key; columns and filters are discovered from the metadata endpoint. Adding a report = add a definition class to the forRoot array; removing one = delete it (an external plugin exposes its report class for the host app to include).
- View is synchronous + paginated (
buildPaginatedResponse), filtered to a[from, to)window. Reports may also expose summary KPI cards (kpis+summary()) and a trend chart (trendSeries+trend()), served byGET .../summaryandGET .../trend— the metadata advertises which a report supports. - Export is asynchronous:
POST .../exportenqueues a BullMQ job; a worker runs the report in batches, writes a CSV/XLSX file to object storage, and flips thereport_exportrow toready. The admin polls the exports list and downloads the file. Files auto-expire after 7 days (daily retention sweep); the row remains asexpired.
A date range is required for exports and capped at 366 days. Export files are capped at 200,000 rows — beyond that the file is marked truncated (never silently cut).
Built-in reports
| Key | Row granularity | Date field | Filters |
|---|---|---|---|
orders | one order | order placed date | status, paymentStatus, vendorId (filter type vendor — UI renders a vendor-name picker; value is the vendor id) |
discounts | one discount redemption | redemption date | code |
customers | one customer (ordered in range) | order placed date | email |
Conventions
Authentication
All endpoints require a Better-Auth admin session and a role granting the matching permission.
| Endpoint | Permission |
|---|---|
GET /admin/reports, GET /admin/reports/:key, GET /admin/reports/:key/{meta,summary,trend} | reports: view |
GET /admin/report-exports, GET /admin/report-exports/:id | reports: view |
POST /admin/reports/:key/export | reports: export |
GET /admin/report-exports/:id/download | reports: export |
Response envelope
Successful responses are wrapped by ResponseInterceptor into { data, message, statusCode, metadata? }. List/run endpoints use the canonical metadata: { total, limit, offset, hasMore }. The download endpoint streams the raw file (Content-Disposition: attachment) and bypasses the envelope.
Endpoints
GET /admin/reports
List registered reports with their metadata.
Response data is an array of:
{
"key": "orders",
"title": "Orders",
"description": "Every order placed in the range with totals, status, and customer.",
"dateField": "Order placed date",
"columns": [{ "key": "orderNumber", "label": "Order #", "type": "string" }, /* … */],
"filters": [{ "key": "status", "label": "Order status", "type": "enum", "options": [/* … */] }]
}Column type is one of string | number | money | date | datetime | boolean (money is integer subunits).
Column type is one of string | number | money | date | datetime | boolean. When present, kpis ({key,label,type}) and trendSeries ({key,label,type:number|money}) tell the client to render summary cards and a trend chart.
GET /admin/reports/:key/meta
Metadata for a single report (same shape as one element above).
GET /admin/reports/:key/summary
Summary KPI values for the range. Same query params as the run endpoint (from, to, filters). Returns:
{ "kpis": [{ "key": "orders", "label": "Orders", "type": "number", "value": 1240 }, /* … */] }value is null when not computable; empty kpis when the report has no summary.
GET /admin/reports/:key/trend
Time-bucketed series for the range (bucketed by day, or by month for ranges over ~62 days). Returns:
{
"granularity": "day",
"series": [{ "key": "orders", "label": "Orders", "type": "number" }, /* … */],
"points": [{ "bucket": "<ISO>", "orders": 42, "revenue": 1839900 }, /* … */]
}GET /admin/reports/:key
Run a report (paginated, on-screen).
Query params:
| Param | Type | Notes |
|---|---|---|
from | ISO datetime | inclusive; omitted → last 30 days |
to | ISO datetime | exclusive; omitted → now |
filters | JSON string | report-specific, validated against filtersSchema |
limit | int (1–500) | default 100 |
offset | int | default 0 |
Response: paginated array of column-keyed row objects (metadata carries totals).
POST /admin/reports/:key/export
Queue an async export. Body:
{ "format": "csv" | "xlsx", "from": "<ISO>", "to": "<ISO>", "filters": { /* optional */ } }from/to are required. Returns the created report_export row (status: "pending").
GET /admin/report-exports
List export history (newest first). Query: reportKey?, status?, limit (default 50), offset. Each row:
{
"id": "…",
"reportKey": "orders",
"format": "csv",
"status": "pending | processing | ready | failed | expired",
"params": { "from": "<ISO>", "to": "<ISO>", "filters": {} },
"rowCount": 1240,
"truncated": false,
"fileName": "orders-20260101-20260201.csv",
"fileSize": 84213,
"error": null,
"createdAt": "<ISO>",
"completedAt": "<ISO>",
"expiresAt": "<ISO>",
"downloadUrl": "/admin/report-exports/…/download" // only when ready
}GET /admin/report-exports/:id
Status of a single export — poll this (or the list) until status: "ready".
GET /admin/report-exports/:id/download
Stream a ready export file as an attachment. 404 if not ready / expired / unknown.
Product Sold Metrics Module — Admin surface
Admin-facing HTTP surface for the per-product "units sold last month" metric: a monthly BullMQ recompute over confirmed orders, a manual re-trigger, and admin-configured display-override rules.
Reviews Module — Admin
HTTP surface for platform-admin moderation of product reviews: cross-vendor list with filters, full-field edit, lifecycle transitions (approve / reject / mark-spam), admin-create…