Skip to content

NWP-101: add export options - #167

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

NWP-101: add export options#167
chronoloq wants to merge 1 commit into
JJFromTenex:mainfrom
chronoloq:NWP-101-export-options

Conversation

@chronoloq

Copy link
Copy Markdown

Ticket

Closes NWP-101

What changed

The Export button on /payments now opens an options dialog instead of downloading a fixed file. Ops picks which columns go in — card last four is off by default — and chooses whether to export the current filter or all payments, with the row count for each shown before anything downloads. The filename carries the scope and the UTC date (payments-disputed-2026-09-10.csv).

Column names arrive from the client, so they are validated server-side in src/lib/csv.ts against the existing EXPORT_COLUMNS allowlist: order is preserved, duplicates collapse, and an unknown, empty, or malformed list returns 400 rather than quietly falling back to a different column set. scope=all runs through the existing query builder with an empty filter set, so there is no second filter path, and the export still never paginates.

How I verified it

npm test42 passed (3 files), against a 28 passed baseline before the change. src/lib/csv.test.ts went from 9 to 23 tests, extended rather than replaced; no existing assertion was modified.

curl against the dev server at /api/payments/export:

Request Result
no params 200 · payments-filtered-2026-09-10.csv · header omits last4
?columns=currency,id 200 · header currency,id (requested order kept)
?status=disputed 200 · payments-disputed-2026-09-10.csv
?status=disputed&scope=all 200 · payments-all-2026-09-10.csv
?columns=cardholder_ssn / ?columns= / ?columns=ID / ?columns=id,,amount 400 · {"error":"Select at least one valid column to export."}
?columns=id,id,amount 200 · header id,amount (duplicate collapsed)

Row counts confirm scope: 32 rows for status=disputed vs 1657 for scope=all.

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

Acceptance criteria

  • Ops can choose which columns are included. Card last-four is off by default.
  • Ops can choose scope: current filter or all payments. Current filter is the default, and the row count is visible before download.
  • The filename reflects the scope and the date.
  • Amounts stay in minor units internally and are formatted once on the way out, with currency in its own column.
  • Deselecting every column disables Download rather than producing an empty file.

The two unticked criteria are implemented, and their server-side halves are proven above — scope switching is verified by curl, and an empty column list is refused with a 400 so an empty file cannot be produced. What is not verified is the dialog's own rendering: the row-count line and the disabled Download button were never confirmed in a browser, so I am not claiming them.

Bugs fixed along the way

Notes for the reviewer

sortPayments in src/data/queries.ts compares amounts with String(a.amount).localeCompare(...) — a lexicographic compare on integer minor units, so "9000" orders after "123456". Pre-existing and deliberately left alone as out of scope, but it does affect export row order when sort=amount.

The column picker uses native labeled checkboxes and radios inside <fieldset>s rather than adding @radix-ui/react-checkbox, per the "use what is here" rule in components.md. The dialog is built on the existing Drawer primitive (Radix dialog), which supplies the focus trap, focus return, and Escape handling.

npm test does not start on Node 18 in this environment (ERR_REQUIRE_ESM loading vitest.config.ts, which needs Node >= 20); the runs above used Node 25.2.1. No repo config was changed for this.

🤖 Generated with Claude Code

Ops exports the payments table several times a day with no say in what
comes out: every column, current filter only. The card last four landed in
every file, so anything sent to a merchant was scrubbed by hand first.

Adds an options dialog to the Export button on /payments: choose columns
(card last four off by default), choose scope (current filter or all
payments) with the row count shown before download, and a scope-aware
filename. Column names are validated server-side against the export
allowlist, and an unusable list is a 400 rather than a quiet fallback that
would hand ops different columns than they asked for.

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.

1 participant