Reports
What it is
Three different things called "reports":
- Internal reports
-
A configuration record that names a database view or table plus a column list. Adding a report is a configuration change, not a code change — an administrator points a report at a view, chooses columns, sets types and filterability, and it becomes a route.
- External reports
-
Fixed feeds from third parties (Plaid bank transactions, bank categorisation, ML prediction sets), fetched by report key.
- Purpose-built reports
-
Hand-written screens, currently just the waiting-suppliers report.
Where it lives
| Concern | Location |
|---|---|
Routes |
|
Components |
|
Services |
|
Models |
|
Table config |
|
Pipes |
|
Shared |
|
Fixtures |
|
ReportsRepositoryService is provided in AppModule, because report configuration is
consumed outside the reports module — notably by the supplier statements tab.
Routes
| Route | Screen |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
Route ordering matters here. |
All reports routes deny REST_UW_ACCESS and ADVANCE_AUDITORS. INT_REP_ADMIN is the
permission for report administration.
Internal report configuration
ReportsRepositoryService on internalApiUrl under reports:
| Endpoint | Purpose |
|---|---|
|
All report configurations → |
|
One configuration → |
|
Create. |
|
Update. |
|
Delete. |
|
Add a field override ( |
|
The available database views and tables — the source list for the add form. |
|
A report record ( |
|
Update a report record. |
Deriving a table configuration
getReportConfiguration(reportName) is the bridge from configuration to grid. It
fetches all configurations, filters client-side by name, and reshapes:
{
title: config.label || config.name,
reportKey: config.report_key,
fields: config.fields.map((column) => {
const name = Object.keys(column)[0]; // each field is a single-key object
const obj = column[name];
return { name, show: true, filterable: obj.filterable, type: obj.type };
}),
}
Two things to note: the client filters the full config list rather than requesting one
by name, and each field arrives as a single-key object whose key is the column name —
hence the Object.keys(column)[0] idiom.
Column types
report-table.interfaces.ts:
export enum ReportColumnType {
Date = 'timestamptz', // the PostgreSQL type name
Text = 'text',
Currency = 'currency',
}
export interface ReportColumn {
data: string;
editor: boolean; // is the column editable in place
type: Date | string;
filter?: { label: string };
cellRenderer?: (cellData: any) => string;
}
export interface ReportTableConfiguration { title: string; columns: ReportColumn[]; }
Date = 'timestamptz' shows the leak: column types are database types, not display
types. currency is the exception — a presentation type with no direct SQL equivalent.
editor: true makes a report column editable, writing back through the report
endpoints. A report is therefore not necessarily read-only.
Serialisation
Report configuration is snake_case on the wire, and the service sets the casing
per call: DeserializeKeysFrom(UnderscoreCase) before reads,
SerializeKeysTo(UnderscoreCase) before writes. See
data modelling for why the ordering matters.
Running a report
SupplierService.getReport(reportName, options) is the query path — note it lives on
the supplier service, not the reports service:
GET reports/:reportName?<options as query params>
with two behaviours:
-
Every key in
optionsbecomes a query parameter. -
If
options.make_dumpis truthy the request is issued withresponseType: 'text'— the dump path returns raw text rather than JSON, for export.
toLowercaseUnderscore (shared/utils/) converts display labels to column names when
building queries.
supplierKeyInQueryParamsNotNeeded in route data suppresses the otherwise-required
supplier-key parameter — used by /reports/:reportName and by the supplier statements
tab.
Supplier-scoped reports
Two surfaces render reports inside the supplier workspace:
| Surface | Detail |
|---|---|
|
|
|
Rendered from route data:
|
SupplierService also exposes supplierKeyColumnName = 'supplier_key' and
supplierDepositTransactionColName = 'supplier_deposit_transactions' — column names the
report layer needs when scoping to a supplier.
External reports
ExternalReportsService on internalApiUrl:
GET reports/external_list → IExternalReport[]
GET reports/external/:reportKeys → IExternalReport[]
Keys are the ExternalReports enum
(shared/external-report/external-reports.enum.ts):
| Member | Key | Content |
|---|---|---|
|
|
Bank transactions via Plaid. |
|
|
Categorised bank transactions. |
|
|
ML predictions at onboarding. |
|
|
ML predictions recomputed daily. |
/reports/external lists them; the same four appear as underwriting tabs, each behind
its own feature flag. See underwriting.
The detail endpoint takes reportExternalKeys (plural) — multiple keys in one call.
Waiting suppliers
reports/waiting-suppliers/waiting-suppliers.component.ts is the odd one out: it calls
actionsToolsUrl directly from the component, not through a service. It reports
suppliers waiting on some action.
|
This is the only place in the reports module that talks to Actions Tools, and one of very few components in the app that issue HTTP directly. Do not copy the pattern; if you extend it, extract a service. |
CSV export
shared/csv-export-service/csv-export.service.ts:
exportCsv(data, filename) // Blob 'text/csv;charset=utf-8' → file-saver
exportTransactions(data, filename, type) // filename_MMDDYYYY[type].csv
dataToString concatenates only the string values of the input object, dropping
numbers and nested values.
|
|
For large server-side exports, prefer the make_dump path on getReport, which
returns text from the backend rather than assembling it in the browser.
AG Grid’s own export is also available on grids built with
shared/pyb-ag-grid/ — see table
generations.
Adding a report
-
/reports/new→ choose a database object fromreports/db_objects. -
Select columns, set
typeandfilterableper column, andeditorif the column should be editable. -
Save →
POST reports/config. -
The report is immediately reachable at
/reports/<name>and appears in/reports/list. -
Avoid names that collide with the literal child routes (
list,external,new,waiting-suppliers).
No deployment is required. This is the main reason report changes are handled by administrators rather than developers.
Gotchas
| Issue | Detail |
|---|---|
Query path lives on |
|
Whole-config fetch to resolve one report |
|
Single-key field objects |
|
Column types are SQL types |
|
|
Name collisions with literal routes make a report unreachable. |
|
See the caution above. |
Direct HTTP in |
Bypasses the service layer. |
Reports can write |
|