Skip to content

NWP-101: let ops choose export columns and scope - #181

Closed
mmudda wants to merge 1 commit into
JJFromTenex:mainfrom
mmudda:NWP-101-export-options
Closed

mmudda wants to merge 1 commit into
JJFromTenex:mainfrom
mmudda:NWP-101-export-options

Conversation

@mmudda

@mmudda mmudda commented Sep 10, 2026

Copy link
Copy Markdown

Ticket

Closes NWP-101

What changed

Ops can now decide what comes out of the payments export instead of taking every column of the current filter. The Export button on /payments opens an options panel: tick the columns you want (the card last four starts off), choose whether to export the current filter or all payments — with the row count for each shown before you commit — and the downloaded file is named for that scope and the UTC date, e.g. payments-disputed-2026-09-10.csv. Deselecting every column disables Download rather than producing a file with a header and nothing under it.

The export stays a plain link to a GET. The dialog only assembles the querystring; the route handler remains the single place that validates, filters, and serializes.

How I verified it

  • npm test passes
  • New behavior is covered by a test
  • Checked it in the browser

npm test34 passing across 3 files, including 5 new cases in src/lib/csv.test.ts covering column selection. npx tsc --noEmit and npm run lint both clean.

Against the dev server:

  • ?columns=id,amount&scope=all200, content-disposition: attachment; filename="payments-all-2026-09-10.csv"
  • ?columns=amount,id&status=disputed → first line is amount,id (requested order honoured), filename payments-disputed-2026-09-10.csv
  • ?columns= and ?columns=bogus400, body {"message":"Select at least one column to export."}
  • ?scope=all produced 1658 data rows, matching total from GET /api/payments and well past the 20-row page; ?status=disputed produced 33, matching that filter's total
  • ?columns=id,last4last4 present, confirming it is opt-in only

In the browser: opened /payments?status=disputed, clicked Export, confirmed "Card last four" is unchecked on open and that the row counts differ between the two scope options.

Acceptance criteria

  • Ops can choose which columns are included. Card last-four is off by default. — DEFAULT_EXPORT_COLUMNS in src/lib/csv.ts excludes last4; the dialog seeds its checkboxes from it.
  • Ops can choose scope: current filter or all payments. Current filter is the default, and the row count is visible before download. — Counts are computed server-side in page.tsx (queryPayments({}).total for the all-scope) and passed to the dialog, so they need no extra round trip.
  • The filename reflects the scope and the date. — exportFilename(label?, date?); label is the status filter, or all for the all-payments scope.
  • Amounts stay in minor units internally and are formatted once on the way out, with currency in its own column. — Unchanged from before; cell() calls formatMoney at the boundary and currency was already a separate column.
  • Deselecting every column disables Download rather than producing an empty file. — Disabled in the dialog, and independently a 400 from the route, so a hand-built URL cannot bypass it.

Bugs fixed along the way

None. Two things worth recording as not bugs, because both look like the traps the ticket warns about:

  • The export route was already exempt from pagination — it calls filterPayments/sortPayments directly and never paginate, so it always returned the full filtered set. The new "all payments" scope is simply fewer filters through the same builder, not a second query path.
  • Amount formatting and the separate currency column already satisfied the money rule, so that criterion needed no code change.

Notes for the reviewer

  • exportFilename signature changed from (date?) to (label?, date?). Its one existing test moved to exportFilename(undefined, date) and a labelled case was added. No other caller existed.
  • Filename labelling is a judgement call. The ticket's example is payments-disputed-2026-08-13.csv, so I put the status filter in the name and all for the all-payments scope. Merchant and search filters do not appear — they would make the name unwieldy and the ticket does not ask for them. Current filter with no status gives plain payments-<date>.csv.
  • Column order. parseExportColumns preserves whatever order the caller asked for, which is what the test pins. The dialog always submits in canonical EXPORT_COLUMNS order so the UI is predictable regardless of the order boxes were ticked.
  • The dialog is built on the existing Drawer primitive (src/components/Drawer.tsx, a Tremor wrapper over Radix Dialog) rather than a new component or dependency. Checkboxes and radios are native inputs with labels and fieldset/legend groups — there is no Checkbox component in this repo and components.md says to reach for what is here.
  • Not covered by a test: the route handler itself. The 400-on-empty-columns and the scope=all path were verified by curl only; the automated coverage is on the serializer and the column allowlist, which is what the DoD asked for. A route-level test would be a reasonable follow-up.

🤖 Generated with Claude Code

The payments export was fixed: every column, current filter only, card
last four in every file. Ops hand-edited each one before sending it to a
merchant.

Adds an options dialog to the Export button. Columns are selectable with
the card last four off by default, scope is either the current filter or
all payments with the row count shown, and the filename carries the scope
and the UTC date.

Column names arrive from the client, so parseExportColumns validates them
against EXPORT_COLUMNS before they reach the serializer or the filename;
an empty selection is a 400 rather than an empty file. Both scopes run
through the existing parseFilters/filterPayments builder, so the export is
never limited to the page on screen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants