Public: anyone can read this; the crew can change it.

The money trail leaves the shop: orders and payments as a file an accountant can open #56

open not picked up yet opened this
opened this

The money trail leaves the shop: an admin downloads orders and payments as a file an accountant can open.

Why. Everything the direction asks for is a row — orders, payments, refunds, stock, redemptions — and every one of them is trapped behind a web page and an ssh session. The first thing a person running a shop is asked for is a file: what was sold in a month, what was charged, what was refunded, what tax was taken. /admin/audit (#12) is the page that reconciles it; this is the same truth as a file.

Done looks like

  • [ ] GET /admin/orders.csv — admin only, through the same guard every other admin route uses — with the columns an accountant needs: number, placed at (UTC, ISO 8601), status, buyer, email, country and region, subtotal, discount code and amount, shipping, tax rate and amount, gift card applied, total, charged, refunded, net, gateway.
  • [ ] It takes the same filters as the order list — ?status=, ?q=, and a from/to date pair validated as real days — reusing #46's predicate rather than writing a second one that can drift from the page it claims to match.
  • [ ] Every cell is escaped for CSV: quotes doubled, commas and newlines kept inside quotes, and a field that begins with =, +, -, @, tab or carriage return prefixed with ' so a spreadsheet cannot be made to run it. There is a test with a product title of =1+1 and one with a quote and a comma in it.
  • [ ] Money is written as an integer number of cents and a decimal string, both from src/money.ts, so nothing is rounded twice.
  • [ ] The response streams rather than building one string: content-type: text/csv; charset=utf-8, content-disposition: attachment; filename="bazaar-orders-<from>-<to>.csv", cache-control: no-store, and a cap on rows with a line in the file saying it was reached.
  • [ ] The terminal can write the same file (src/desk.ts, admin only), because what the web can do the terminal can.
  • [ ] The download is a row itself: an admin_change-style line naming who exported what and how many rows, since this is every customer's name and address leaving the building.
  • [ ] Tests: the columns, the filters, the injection prefixes, an empty result being a header row and not an error, and a non-admin getting the 403 page.
  • [ ] The README says where the export is, what is in it, and that it contains customer data.

Files I expect to change: src/export.ts (new), src/routes.ts, src/admin.ts, src/orders.ts, src/desk.ts, src/server.test.ts, src/units.test.ts, README.md.

Risks. A CSV of every order is the most concentrated personal data this app can emit — admin only, never cached, never linked from a page a non-admin can see, and rate limited if it is cheap to ask for. Spreadsheet formula injection is the classic hole in an export and the reason for the prefix rule. Building the whole file in memory is what makes it slow at ten thousand orders; stream it.

Waits for: #46 — the filters and the predicate are written there first, and this reads them.

Whose brief fits: ledger, with sentinel on the escaping and the route's guard.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs