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

reports/reports-routing.routes.ts

Components

reports/report-list/, report-add/, report-edit/, report-table/, external-report-list/, waiting-suppliers/

Services

reports/service/reports-repository.service.ts (implements ReportsAbstractService), reports/service/external-reports.service.ts

Models

reports/model/ — report-model.ts (incl. EditableReportModel, EditableReportFieldModel), report-list-model.ts, report-common.ts, external-report.ts, i-external-report.ts; reports/dto/external-report-dto.ts

Table config

reports/report-table/report-table.interfaces.ts

Pipes

reports/pipes/report.pipe.ts

Shared

shared/external-report/, shared/csv-export-service/

Fixtures

reports/mock/reports.mock.json, tables.mock.json

ReportsRepositoryService is provided in AppModule, because report configuration is consumed outside the reports module — notably by the supplier statements tab.

Routes

Route Screen

/reports/list

ReportListComponent — all configured reports.

/reports/external

ExternalReportListComponent.

/reports/waiting-suppliers

WaitingSuppliersReportComponent.

/reports/new

ReportAddComponent.

/reports/:reportKey/edit

ReportEditComponent.

/reports/:reportName

ReportTableComponent, with data: { supplierKeyInQueryParamsNotNeeded: true } — renders any configured report by name.

Route ordering matters here. :reportName is the last child, so the literal paths (list, external, new, waiting-suppliers) win. A new report configured with the name list or new would be unreachable. Adding a literal route after :reportName would make it unreachable.

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

GET reports/config

All report configurations → ReportListModel[].

GET reports/config/:uuid

One configuration → EditableReportModel.

POST reports/config

Create.

PUT reports/config/:uuid

Update.

DELETE reports/config/:uuid

Delete.

POST reports/config/:uuid/overrides

Add a field override (EditableReportFieldModel) — per-column customisation.

GET reports/db_objects

The available database views and tables — the source list for the add form.

GET reports/:uuid

A report record (ReportModel).

PUT reports/:uuid

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 options becomes a query parameter.

  • If options.make_dump is truthy the request is issued with responseType: '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

/suppliers/:key/reports/{list,:reportName}

SupplierReportsComponent with supplier-report-list/ and supplier-report-details/ — the same report machinery scoped to one supplier.

/suppliers/:key/statements

Rendered from route data: reportName: 'mp_statements', reportConfigName: 'v_mp_statements'. The marketplace statements "tab" is a report over a view. See marketplaces.

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

PLAID

PLAID-D

Bank transactions via Plaid.

BANK_CATEGORIES

PL-BT

Categorised bank transactions.

ACCESS_ONBOARDING_PREDICTIONS

BR-AC-ON

ML predictions at onboarding.

ACCESS_DAILY_PREDICTIONS

BR-AC-DL

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.

dataToString silently discards non-string values, so exportTransactions expects data that has already been stringified. Passing raw records produces a truncated file with no error. Use exportCsv with a prepared CSV string when in doubt.

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

  1. /reports/new → choose a database object from reports/db_objects.

  2. Select columns, set type and filterable per column, and editor if the column should be editable.

  3. Save → POST reports/config.

  4. The report is immediately reachable at /reports/<name> and appears in /reports/list.

  5. 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 SupplierService

getReport is not on the reports service.

Whole-config fetch to resolve one report

getReportConfiguration filters client-side.

Single-key field objects

Object.keys(column)[0] is the column name.

Column types are SQL types

timestamptz for dates.

:reportName is a catch-all

Name collisions with literal routes make a report unreachable.

dataToString drops non-strings

See the caution above.

Direct HTTP in WaitingSuppliersReportComponent

Bypasses the service layer.

Reports can write

editor: true columns are editable.