NWP-101: add export options - #162
Open
douglasgimli wants to merge 11 commits into
Open
Conversation
Plan for the payments export options dialog: server-side export with validated columns and scope, reusing the query builder behind GET /api/payments. Follows docs/specs/TEMPLATE.md. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Add DEFAULT_EXPORT_COLUMNS (every column except last4) and parseExportColumns, which checks the client-supplied column list against EXPORT_COLUMNS and rejects empty, unknown, or repeated names with a user-safe message. Tests extend src/lib/csv.test.ts. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Add EXPORT_SCOPES with parseExportScope (absent means current filter, anything but current/all is rejected) and exportScopeToken, which turns the validated scope and filters into the word for the filename: the status when one is set, 'filtered' for other filters, otherwise 'all'. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
exportFilename now takes the scope token, producing payments-<token>-<UTC date>.csv as the ticket asks. The route passes the current-filter token so the build stays green; the full scope and column handling lands in the next commit. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
GET /api/payments/export now reads columns and scope from the query, checks both against the allowlists in src/lib/csv.ts, and answers 400 with a user-safe message on anything else. scope=all drops the filter fields but still runs through filterPayments and sortPayments, so there is one query path. Rows are never paginated. Route tests cover the happy path, both scopes, the filename header, and every rejection. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Client component on the existing Drawer: scope radios (current filter by default), a live row count read from GET /api/payments, column checkboxes with the card last four off by default, and a Download link to GET /api/payments/export that is disabled while no column is selected. Every input has a label; Escape and focus return come from Radix. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The Export button on /payments now opens the options dialog instead of downloading straight away. The current filter query is passed through without the page number, so the export and the row count always cover the whole filtered set. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Mark the spec done and record what actually happened: the route test file stayed because NextRequest runs under vitest, npm test went from 28 to 50 passing, and both open questions were decided before code. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Both drawer keyframes moved the panel from translateX(-100%), so a panel anchored on the right entered from the left edge and crossed the screen. Use 100%, matching Tremor's upstream Drawer. Surfaced by the export options dialog; the mobile sidebar uses the same content positioning and benefits too. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The single 23:00Z instant passed under a local-date implementation on any machine at or west of UTC. Adding a 00:30Z instant makes the pair fail for every non-zero offset. Found by the verifier's mutation check. Co-Authored-By: Claude Fable 5.1 <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 goes into a payments export before the file is created. The Export button on the payments page opens a panel with two choices: which columns to include, with the card last four switched off unless someone turns it on, and whether to export the current filter or every payment. The panel shows how many rows the file will contain, the filename says which scope and date it covers, and an empty column selection cannot produce a file.
How I verified it
All commands run in this session from
build-battle/merchant-consoleon this branch at6c9d36d.npm test—Test Files 4 passed (4)/Tests 50 passed (50). Main had 28 passing in 3 files; nothing was removed or skipped.npm run lint—✔ No ESLint warnings or errorsnpm run build—✓ Compiled successfully, exit code 0Browser, http://localhost:3000/payments?status=disputed — Export opens a panel titled "Export payments". With "Current filter" selected it reads "33 rows", matching the table footer; switching to "All payments" reads "1,658 rows". "Card last four" is the only unchecked column. Unchecking every column turns Download into a disabled button and shows "Select at least one column to download." Escape closes the panel. The download itself was not clicked; file contents were checked with curl instead.
curl -sI "http://localhost:3000/api/payments/export?status=disputed"—content-disposition: attachment; filename="payments-disputed-2026-09-10.csv"; with?scope=allthe filename ispayments-all-2026-09-10.csv; with?merchantId=mch_01it ispayments-filtered-2026-09-10.csv.curl -s ".../export?columns=id,pan"—{"message":"Unknown column \"pan\""}with status 400;?columns=returns 400Select at least one column;?scope=nopereturns 400Unknown scope "nope".curl -s ".../export?scope=all&status=disputed" | wc -l— 1658 rows against 33 for?status=disputed, so scope=all ignores the filter and nothing is paginated.npm testpassesNew behavior is covered by a test
Checked it in the browser
Acceptance criteria
csv.test.ts"defaults to every column except the card last four, in the standard order" and "accepts a subset and keeps the requested order";route.test.ts"defaults to every column except the card last four"; browser: last four unchecked on open.csv.test.ts"defaults to the current filter when the param is absent";route.test.ts"ignores the filters when scope is all"; browser: "Current filter" preselected, "33 rows" then "1,658 rows".payments-disputed-2026-08-13.csv. —csv.test.ts"puts the scope token before the date, as in the ticket example" asserts that exact string;route.test.ts"names the file after the status filter and the UTC date"; curl output above.route.test.ts"formats the amount once and keeps the currency in its own column"; the serializer still formats through the existing money helper and the existing$250.00and"$1,234,567.89"tests are unchanged.route.test.ts"rejects an empty column selection with a 400" so a hand-built URL cannot produce one either.Definition of done: the column serializer tests live in the existing
src/lib/csv.test.ts(9 tests before, 22 after); branch and every commit subject carryNWP-101.Bugs fixed along the way
build-battle/merchant-console/tailwind.config.ts— both drawer keyframes moved the panel fromtranslateX(-100%), so a Drawer anchored on the right slid in from the left edge and crossed the screen. Root cause: the sign is flipped relative to Tremor's upstream Drawer. This is the first feature to open the Drawer on desktop, which is why nobody had seen it; the mobile sidebar uses the same panel and benefits too. Now100%, verified by reading the live@keyframesrules in the browser.Deliberately not done
src/data/queries.ts, sosort=amountorders 9 above 100. The export inherits it when that sort is requested. Out of this ticket; it needs its own failing-then-passing test and PR.created_at,last4), because the existing tests pin that contract; the panel shows readable labels instead.Notes for the reviewer
docs/specs/NWP-101-export-options.mdand was written before the code.scope=alldrops the filter fields but still goes through the shared filter and sort functions, so there is one query path, andsort/directionare still honoured.🤖 Generated with Claude Code