Conversation
Export now opens a dialog where ops chooses columns (card last four off by default) and scope (current filter or all payments), with row counts shown before download. The route validates columns and scope against allowlists, returns 400 on bad input, and names the file by scope and UTC date. Both scopes go through the existing query builder, unpaged. Adds a Dialog primitive on @radix-ui/react-dialog, and switches the payments page to parseFilters so the dialog's count matches the file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Merchant console CLAUDE.md and README described seed data as JSON; it is generated in src/data/generate.ts. Adds test, lint and build commands and how pages reach the data. components.md listed a Dialog that did not exist. Root CLAUDE.md gains Release Standards, and the PR template gains a Business impact field. Co-Authored-By: Claude Opus 5.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
Export on
/paymentsnow opens a dialog instead of downloading straight away. Ops picks which columns go in the file, with card last four off by default. They also pick the scope, either the current filter or all payments, and each option shows how many rows it will produce. The server checks the columns and scope against allowlists before reading anything. The file is named after the scope and the UTC date, for examplepayments-disputed-2026-09-23.csv. Both scopes go through the existing query builder without paging, so the file holds every matching row, not just the page on screen.Business impact
Ops can send merchant exports without deleting card numbers by hand first, which removes 3–4 hours a month of cleanup and the risk of the near-miss from last quarter.
How I verified it
npm test: 28 passing before the change, and 35 passing after (Test Files 3 passed (3) · Tests 35 passed (35)). The 9 existingcsv.test.tscases pass unchanged, and 7 new ones cover the column parser, the default set, the empty selection, the scope and the filename.npx tsc --noEmitis clean, andnpm run lintreports no warnings or errors.In the running dev server, on
/payments?status=disputed:Called the route directly:
columns=amount,id,statusreturned headersamount,id,statusin that order and 33 rows, not the 20 on page 1. The filename waspayments-disputed-2026-09-23.csvand the file contained no4242.page=2in the query and nocolumns, it still returned all 33 rows with the default nine columns (nolast4).scope=allreturned 1,658 rows inpayments-all-2026-09-23.csv.columns=id,drop%20table,scope=bogusandcolumns=each returned 400 with a JSONerrormessage.npm testpassesNew behavior is covered by a test (column parsing, scope and filename. The route and dialog were checked by hand; see notes)
Checked it in the browser
Acceptance criteria
columnsparam, the default set excludeslast4.payments-disputed-2026-09-23.csv. A current filter with no status givespayments-filtered-…, and all payments givespayments-all-…. Merchant IDs and search text are never put into the filename.toCsvand is unchanged; see notes.columns=with a 400.Bugs fixed along the way
src/app/payments/page.tsxbuilt its filters by hand instead of callingparseFilters, so it droppedsort,direction,fromandto, which the export route honours. The row count in the dialog could therefore disagree with the rows in the file. The page now usesparseFilters, the same parser as the route..claude/rules/components.mdlisted aDialogcomponent that didn't exist. I addedsrc/components/Dialog.tsx, built on the existing@radix-ui/react-dialogdependency the same wayDrawer.tsxis, and corrected the rule.CLAUDE.mdandREADME.mdsaid seed data is JSON. It's generated insrc/data/generate.ts. Both are corrected.Notes for the reviewer
sortPaymentsinsrc/data/queries.ts:81sorts amounts as strings, so amount order is wrong in both the table and the export. It's outside this ticket, and I didn't touch it.$250.00), beside a separatecurrencycolumn.csv.test.tslocks in that format and says NWP-101 "changes which columns ship, not how a cell is written", so I kept it.toCsv's own default is still all ten columns, because an existing test locks that in. The last-four-off default lives inDEFAULT_EXPORT_COLUMNS, which both the route and the dialog use./spec, so there's nodocs/specs/NWP-101-*.md.CLAUDE.mdand the Business impact field this PR uses to the PR template. Drop it if you'd rather review it on its own branch./ship-readyflagged problems that predate this ticket: float money math and local-time day grouping insrc/data/metrics.ts, and missing labels on the filter-bar inputs. They're outside this ticket and not addressed here.package-lock.jsonwas already modified before this work and isn't part of this PR.🤖 Generated with Claude Code