NWP-101: add export options - #164
Open
kumarjith wants to merge 2 commits into
Open
Conversation
The payments export was fixed: every column, current filter only. The card last four shipped in every file, so anything going to a merchant was edited by hand first. Adds an options dialog on the existing Export button: - Columns are selectable, with the card last four off by default (DEFAULT_EXPORT_COLUMNS in src/lib/csv.ts). - Scope is current filter or all payments, with both row counts shown before download. Both scopes go through the existing query builder, so an export is never limited to the current page. - parseExportColumns validates column names against the allowlist server side; the route returns 400 rather than serving an empty file. - The filename names the scope: payments-disputed-2026-08-13.csv. Amounts stay in minor units and are still formatted once, in cell(), beside their own currency column. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Records the rules we are working to: no direct commits to main, test evidence attached before merging, and a one-line business impact summary on every pull request. 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 payments export was fixed: every column, current filter only. The card last four shipped in every file, so anything going to a merchant had to be cleaned up by hand first. The Export button on
/paymentsnow opens an options dialog. Ops picks which columns are included, with the card last four off by default, and picks whether to export the current filter or all payments, with the row count for both shown before download. The filename names the scope, and column names are validated against an allowlist on the server rather than trusted from the query string. Both scopes run through the existing query builder behindGET /api/payments, so an export is never limited to the page the table happens to be showing.How I verified it
npm test— 37 passed (37), 3 files. The baseline before this change was 28 passed; the 9 new tests are all insrc/lib/csv.test.ts.npx next lintreported no warnings or errors, andnpx tsc --noEmitwas clean.Against the dev server with
curl:?columns=nope400with{"message":"Select at least one column to export."}columnsparamid,created_at,merchant,description,status,method,card_brand,amount,currency— nolast4?columns=amount,currency,id$473.78,USD,pay_001644?status=disputedcontent-dispositionfilenamepayments-disputed-2026-09-10.csv?status=disputed&scope=allpayments-all-2026-09-10.csv?search=orderpayments-filtered-2026-09-10.csv?status=disputed&columns=idvs same with&scope=allIn the browser on
/payments?status=disputed:Clicked Export. The dialog opened with the heading "Export payments".
Scope showed "Current filter (33 rows)" selected and "All payments (1,658 rows)" unselected.
In the Columns list, "Card last four" was the only unchecked box; the other nine were checked.
Unchecked all nine remaining boxes. The message "Select at least one column to export." appeared under the list, the footer read "33 rows · 0 columns", and Download rendered as a disabled button rather than a link.
Rechecked Status then Payment ID. The Download link href read
/api/payments/export?status=disputed&columns=id%2Cstatus&scope=filtered— the canonical column order, not the order clicked.Selected "All payments". The href changed to
/api/payments/export?columns=id%2Cstatus&scope=all, dropping the filter param.Pressed Escape. The dialog closed.
npm testpassesNew behavior is covered by a test
Checked it in the browser
Acceptance criteria
payments-disputed-2026-08-13.csv.Bugs fixed along the way
None fixed. Three found and deliberately left, all pre-existing and all outside this ticket:
src/data/metrics.ts:25— the bucket keys on line 18 come fromlastUtcDays()and are UTC, but payments are bucketed withnew Date(payment.createdAt).toLocaleDateString("en-CA"), which is server local time. On a host west of UTC, late-evening payments land in the wrong key or match no bucket and hit thecontinueon line 27, dropping them from the volume chart.utcDayKeyinsrc/lib/dates.tsis the fix.src/data/metrics.ts:31,34— money is accumulated as a float in major units (bucket.captured += payment.amount / 100) and re-multiplied byMath.round(bucket.captured * 100)on report. The rounding hides drift rather than preventing it; summing the integer minor units needs no rounding at all.src/data/queries.ts:81—sortPaymentscompares amounts as strings viaString(a.amount).localeCompare(String(b.amount)), so9000sorts before25000. The export reusessortPaymentsand inherits it.I left the sort bug alone rather than changing the ordering of every existing export in the same pull request that changes its columns.
Notes for the reviewer
Drawer, notDialog..claude/rules/components.mdstates thatsrc/components/has aDialogcomponent. It does not.Drawer.tsxis the Radix Dialog wrapper that exists, and it had no consumers anywhere in the app before this change. Either the rule file or the component inventory needs correcting.Checkboxcomponent and@radix-ui/react-checkboxis not a dependency, so the dialog uses native inputs styled with Tailwind rather than adding a dependency. Each has anhtmlForlabel and both groups are wrapped in afieldsetwith alegend.parseExportColumnsreturning an empty array is what refuses the request; the disabled Download is a convenience. Unknown column names are dropped rather than erroring, so a stale bookmark degrades to fewer columns instead of failing outright.allagrees from both directions. The dialog drops the filter params when scope isall, and the route ignores filters whenscope=all, so a hand-edited URL behaves the same as the dialog.src/app/payments/page.tsx:36-45re-implements the status allowlist inline instead of callingparseFilters, so there are two allowlists for one field. Pre-existing, not a security issue since the values only reach in-memory predicates, but it will drift. Left as-is.docs: add Release Standards to the root CLAUDE.md. Happy to drop it if you would rather this PR were only NWP-101.🤖 Generated with Claude Code