Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
/paymentsopens 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 testpassesnpm test— 34 passing across 3 files, including 5 new cases insrc/lib/csv.test.tscovering column selection.npx tsc --noEmitandnpm run lintboth clean.Against the dev server:
?columns=id,amount&scope=all→200,content-disposition: attachment; filename="payments-all-2026-09-10.csv"?columns=amount,id&status=disputed→ first line isamount,id(requested order honoured), filenamepayments-disputed-2026-09-10.csv?columns=and?columns=bogus→400, body{"message":"Select at least one column to export."}?scope=allproduced 1658 data rows, matchingtotalfromGET /api/paymentsand well past the 20-row page;?status=disputedproduced 33, matching that filter's total?columns=id,last4→last4present, confirming it is opt-in onlyIn 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
DEFAULT_EXPORT_COLUMNSinsrc/lib/csv.tsexcludeslast4; the dialog seeds its checkboxes from it.page.tsx(queryPayments({}).totalfor the all-scope) and passed to the dialog, so they need no extra round trip.exportFilename(label?, date?); label is the status filter, orallfor the all-payments scope.cell()callsformatMoneyat the boundary andcurrencywas already a separate column.400from 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:
filterPayments/sortPaymentsdirectly and neverpaginate, 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.Notes for the reviewer
exportFilenamesignature changed from(date?)to(label?, date?). Its one existing test moved toexportFilename(undefined, date)and a labelled case was added. No other caller existed.payments-disputed-2026-08-13.csv, so I put the status filter in the name andallfor 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 plainpayments-<date>.csv.parseExportColumnspreserves whatever order the caller asked for, which is what the test pins. The dialog always submits in canonicalEXPORT_COLUMNSorder so the UI is predictable regardless of the order boxes were ticked.Drawerprimitive (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 andfieldset/legendgroups — there is no Checkbox component in this repo andcomponents.mdsays to reach for what is here.400-on-empty-columns and thescope=allpath 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