NWP-101: add export options - #167
Open
chronoloq wants to merge 1 commit into
Open
Conversation
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>
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
The Export button on
/paymentsnow 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.tsagainst the existingEXPORT_COLUMNSallowlist: 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=allruns 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 test— 42 passed (3 files), against a 28 passed baseline before the change.src/lib/csv.test.tswent from 9 to 23 tests, extended rather than replaced; no existing assertion was modified.curl against the dev server at
/api/payments/export:payments-filtered-2026-09-10.csv· header omitslast4?columns=currency,idcurrency,id(requested order kept)?status=disputedpayments-disputed-2026-09-10.csv?status=disputed&scope=allpayments-all-2026-09-10.csv?columns=cardholder_ssn/?columns=/?columns=ID/?columns=id,,amount{"error":"Select at least one valid column to export."}?columns=id,id,amountid,amount(duplicate collapsed)Row counts confirm scope: 32 rows for
status=disputedvs 1657 forscope=all.npm testpassesAcceptance criteria
The two unticked criteria are implemented, and their server-side halves are proven above —
scopeswitching 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
sortPaymentsinsrc/data/queries.tscompares amounts withString(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 whensort=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 incomponents.md. The dialog is built on the existingDrawerprimitive (Radix dialog), which supplies the focus trap, focus return, and Escape handling.npm testdoes not start on Node 18 in this environment (ERR_REQUIRE_ESMloadingvitest.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