Reports & exports
Back-office report catalogue with the columns and filters of every essential report, report runs with totals, and asynchronous PDF, Excel and CSV exports.
These operations deliver the essential reports of cahier §4 (« Indicateurs et rapports ») and the exports of cahier §9 (« PDF et Excel/CSV pour les listes et rapports autorisés »): the reports library (BO-59) and the export centre (BO-60) — ANA-03 to ANA-05.
Definitions are those of the dashboard card catalogue: revenue is HT, net of discounts, without delivery fees, counted on validatedAt, minus credit notes; orders and revenue are attributed to the delegate snapshotted at submission (BR-49); business days in Africa/Algiers.
| Permission | Unlocks on this page |
|---|---|
reports.view | Report catalogue and runs (plus the extra permission of each report) |
reports.export | Export jobs of reports; list exports also need the module's .export key |
pricing.view | Margin and stock-value columns |
Audit. Report runs are not journaled; every export request writes EXPORT (source, format, filters; row count on completion).
| Method | Path | Operation | Access | Apps | Release |
|---|---|---|---|---|---|
| GET | /admin/reports | admin.reports.listReport catalogue available to my role | reports.view | BO | V1 |
| GET | /admin/reports/{reportKey} | admin.reports.runRun a report with filters (paginated rows + totals) | reports.view | BO | V1 |
| POST | /admin/exports | admin.exports.createRequest an export (PDF, XLSX, CSV) of a list or report | reports.export | BO | V1 |
| GET | /admin/exports | admin.exports.listMy exports and their status | reports.export | BO | V1 |
| GET | /admin/exports/{exportId} | admin.exports.getExport status and download URL | reports.export | BO | V1 |
Report catalogue
| Key | Columns | Filters | Extra permission |
|---|---|---|---|
sales_by_period | period, orders, units, revenueHt, discountHt, vat, revenueTtc, averageBasketHt, resellers | from, to*, granularity (DAY/WEEK/MONTH), wilayaCode[], brandId[], categoryId[], channel[], delegateId[], customerType[] | — |
sales_by_zone | zone / wilaya / commune, orders, revenueHt, shareBps, activeResellers, averageBasketHt, deltaBps | from, to*, level (ZONE/WILAYA/COMMUNE), brandId[], categoryId[] | — |
sales_by_delegate | delegate, zones, orders, assistedOrders, revenueHt, firstOrders, newResellers, averageBasketHt | from, to*, zoneId, delegateId[] | — |
sales_by_reseller | number, storeName, wilaya, commune, customerType, delegate, orders, revenueHt, averageBasketHt, lastOrderAt, activity, balanceDue (finance.view) | from, to*, wilayaCode[], delegateId[], segmentId[], customerType[], activity | — |
sales_by_brand | brand, units, revenueHt, shareBps, deltaBps | from, to*, wilayaCode[], customerType[] | — |
sales_by_product | product, sku, brand, category, units, orders, resellers, revenueHt, avgUnitPriceHt, marginHt (pricing.view), shareBps | from, to*, brandId[], categoryId[], wilayaCode[] | — |
prospect_conversion | cohort (period of creation), source, delegate, prospects, contacted, interested, appInstalled, accountsCreated, firstOrders, conversionBps, medianDaysToFirstOrder | from, to*, delegateId[], source[], zoneId | crm.view |
first_orders | orderNumber, date, reseller, delegate, channel, totalHt, daysSinceSignup, assisted | from, to*, delegateId[], channel[] | — |
stockouts | sku, product, warehouse, outSince, daysOut, lastReceiptAt, refusedOrModifiedLines, availableNow | from, to*, warehouseId, brandId[], categoryId[] | stock.view |
slow_movers | sku, product, physical, unitsSold, lastSoldAt, daysOfCover, stockValueHt (pricing.view) | from, to*, warehouseId, brandId[], categoryId[], maxUnitsSold, minDaysSinceLastSale | stock.view |
orders_pending_late_cancelled_returned | orderNumber, reseller, status, bucket, submittedAt, ageHours, delegate, totalTtc, reason, channel | from, to*, bucket[] (PENDING/LATE/CANCELLED/RETURNED), channel[], wilayaCode[] | orders.view |
field_activity | delegate, day, toursPlanned, toursCompleted, visitsPlanned, visitsDone, visitsNotDone, completionBps, avgVisitMinutes, km, signups, installs, firstOrders, outOfToleranceCheckIns | from, to*, delegateId[], zoneId | field.view |
payments_expected | invoiceNumber, reseller, dueDate, totalTtc, paidAmount, balance, status, daysOverdue, method | from, to*, status[], resellerId, agingBucket | finance.view |
* required. LATE = waiting for confirmation longer than orders.confirmationReminderHours, or validated and not shipped within delivery.shippingSlaHours. Column types: STRING, INTEGER, MONEY, BPS, DATE, DATETIME, DECIMAL.
GET /admin/reports
admin.reports.list · Permission reports.view · Apps BO · Release V1 · Requirements ANA-03
Purpose. The reports available to the caller, with their columns and filters (the library renders forms from it).
Response 200 OK
{
"items": [
{
"key": "sales_by_zone", "title": "Chiffre d'affaires par zone", "category": "SALES", "exportFormats": ["XLSX", "CSV", "PDF"],
"columns": [{ "key": "label", "type": "STRING" }, { "key": "orders", "type": "INTEGER" }, { "key": "revenueHt", "type": "MONEY", "sortable": true }, { "key": "shareBps", "type": "BPS" }, { "key": "activeResellers", "type": "INTEGER" }, { "key": "averageBasketHt", "type": "MONEY" }, { "key": "deltaBps", "type": "BPS" }],
"filters": [{ "key": "from", "type": "DATE", "required": true }, { "key": "to", "type": "DATE", "required": true }, { "key": "level", "type": "ENUM", "values": ["ZONE", "WILAYA", "COMMUNE"] }, { "key": "brandId", "type": "UUID", "multiple": true }],
"defaultSort": { "field": "revenueHt", "order": "desc" }
}
]
}Errors
| Status | Code | When |
|---|---|---|
| 403 | PERMISSION_DENIED | Caller lacks reports.view |
Rules & side effects
- Reports whose extra permission the caller lacks are omitted; gated columns are omitted from
columns. Titles are localised byAccept-Language(report keys are not enum values).
Used by BO-59
GET /admin/reports/{reportKey}
admin.reports.run · Permission reports.view (+ the report's extra permission) · Apps BO · Release V1 · Requirements ANA-03 · ANA-04
Purpose. Runs a report with filters: paginated rows plus totals.
Request — the report's filters as query parameters, plus sort, order, page, pageSize (max 200).
Response 200 OK — sales_by_zone?from=2026-11-01&to=2026-11-15&level=WILAYA
{
"report": { "key": "sales_by_zone", "columns": ["label", "orders", "revenueHt", "shareBps", "activeResellers", "averageBasketHt", "deltaBps"] },
"filters": { "from": "2026-11-01", "to": "2026-11-15", "level": "WILAYA" },
"items": [
{ "key": "31", "label": "Oran", "orders": 61, "revenueHt": 112840000, "shareBps": 5263, "activeResellers": 88, "averageBasketHt": 1849836, "deltaBps": 1210 },
{ "key": "16", "label": "Alger", "orders": 38, "revenueHt": 71650000, "shareBps": 3342, "activeResellers": 54, "averageBasketHt": 1885526, "deltaBps": -340 }
],
"total": 9, "page": 1, "pageSize": 25,
"totals": { "orders": 118, "revenueHt": 214400000, "activeResellers": 172 },
"generatedAt": "2026-11-15T09:40:00Z", "dataAsOf": "2026-11-15T09:39:52Z"
}Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_FAILED | Missing required filter, unknown filter or sort column |
| 403 | PERMISSION_DENIED | Missing reports.view or the report's extra permission |
| 404 | NOT_FOUND | Unknown report key |
| 422 | REPORT_RANGE_TOO_LARGE | Range over 366 days (DAY granularity: 92) — details: { maxDays } |
Rules & side effects
- Read-only; served from analytics aggregates (
dataAsOf);totalscover the whole filtered set. Results cached 60 s per parameters and permission set.
Exports
ExportJob
interface ExportJob {
id: Uuid;
source: { kind: "LIST" | "REPORT"; key: string }; // LIST: resellers, orders, products, stock-levels, stock-movements, invoices, payments, visits, tours, tickets, audit-logs, delegates · REPORT: catalogue keys
format: "PDF" | "XLSX" | "CSV"; // ExportFormat
status: "QUEUED" | "RUNNING" | "SUCCEEDED" | "FAILED" | "EXPIRED"; // JobStatus
filters: Record<string, unknown>;
columns?: string[];
rowCount?: number;
progress?: number; // 0–100
file?: FileRef; // GENERATED_DOCUMENT; signed URL (5 min) re-issued on every GET
error?: { code: string; message: string };
requestedBy: UserSummary;
createdAt: Iso; startedAt?: Iso; finishedAt?: Iso;
expiresAt?: Iso; // finishedAt + exports.retentionDays (7)
}Limits: XLSX/CSV ≤ 100 000 rows, PDF ≤ 2 000 rows (422 EXPORT_TOO_LARGE); at most 3 running exports per user (429 RATE_LIMITED). Lifecycle: JobStatus.
POST /admin/exports
admin.exports.create · Permission reports.export (LIST sources: the module's .export) · Apps BO · Release V1 · Requirements ANA-05
Purpose. Requests a PDF, Excel or CSV export of a list or a report.
Request
{ "source": { "kind": "REPORT", "key": "sales_by_zone" }, "format": "XLSX", "filters": { "from": "2026-11-01", "to": "2026-11-15", "level": "WILAYA" } }Response 202 Accepted — an ExportJob (status QUEUED).
| LIST key | Permission |
|---|---|
resellers | crm.export |
orders | orders.export |
products | catalog.export |
stock-levels, stock-movements | stock.export |
invoices, payments | finance.export |
visits, tours, delegates | field.export |
tickets | reports.export |
audit-logs | audit.export |
Errors
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_FAILED | Unknown source, filter or column |
| 403 | PERMISSION_DENIED | Missing export permission, or a gated column requested |
| 422 | EXPORT_TOO_LARGE | details: { rowCount, maxRows, format } |
| 429 | RATE_LIMITED | 3 exports already running |
Rules & side effects
- The file contains only what the requester may see. Audit
EXPORT. On completion:export.ready(in-app; email when it took over 2 minutes) and SSEexport.ready.
Used by BO-59 · BO-60 · every list screen's export button
GET /admin/exports
admin.exports.list · Permission reports.export · Apps BO · Release V1 · Requirements ANA-05
Purpose. My exports and their status (the export centre). Request — status[], page / pageSize. Response 200 OK — Page<ExportJob> (own jobs only).
Errors
| Status | Code | When |
|---|---|---|
| 403 | PERMISSION_DENIED | Caller lacks reports.export |
Rules & side effects
- Read-only. Any user who can export (module
.exportkeys) sees their own jobs — Assumption.
Used by BO-60
GET /admin/exports/{exportId}
admin.exports.get · Permission reports.export · Apps BO · Release V1 · Requirements ANA-05
Purpose. Export status and a fresh download URL.
Response 200 OK
{
"id": "01930b40-5e6f-7a8b-9c0d-1e2f3a4b5c6d", "source": { "kind": "LIST", "key": "resellers" }, "format": "XLSX", "status": "SUCCEEDED",
"filters": { "wilayaCode": ["31"], "activity": "INACTIVE" }, "rowCount": 94, "progress": 100,
"file": { "id": "01930b41-1a2b-7c3d-8e4f-5a6b7c8d9e0f", "url": "https://files.hoco.dz/s/export-resellers-20261104.xlsx?sig=…", "mimeType": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "size": 28412, "purpose": "GENERATED_DOCUMENT" },
"requestedBy": { "id": "01929e55-2c3d-7e4f-9a1b-2c3d4e5f6a7b", "displayName": "Nadia Belkacem" },
"createdAt": "2026-11-04T09:12:00Z", "startedAt": "2026-11-04T09:12:01Z", "finishedAt": "2026-11-04T09:12:04Z", "expiresAt": "2026-11-11T09:12:04Z"
}Errors
| Status | Code | When |
|---|---|---|
| 403 | PERMISSION_DENIED | Not the requester |
| 404 | NOT_FOUND | Unknown export |
| 410 | EXPORT_EXPIRED | File purged after expiresAt |
Rules & side effects
fileis present only whenSUCCEEDED; each GET issues a new 5-minute signed URL.
Used by BO-60
Support & communication
Back-office support and messaging — ticket queue, replies and internal notes, after-sales resolutions, announcements with targeting, the notification log, templates and the event-to-channel rules.
Administration, settings & audit
Back-office administration — users, sessions, roles and permissions with guard rails, the activity journal, business settings, reference lists, app versions, support regions, data imports and global search.