Skip to content

feat(api): GET /v1/customer/export.csv — CSV dump of company's customers#67

Merged
CryptoJones merged 1 commit into
masterfrom
feat/customer-csv-export
May 18, 2026
Merged

feat(api): GET /v1/customer/export.csv — CSV dump of company's customers#67
CryptoJones merged 1 commit into
masterfrom
feat/customer-csv-export

Conversation

@CryptoJones
Copy link
Copy Markdown
Owner

Summary

CSV export for the customer list. text/csv response with Content-Disposition: attachment; common reporting need that currently requires the operator to script the bycompany list endpoint and serialize themselves.

5000-row hard cap. Oversize results get a trailing # truncated... comment row signalling the caller to page via offset. RFC 4180 escaping (every field quoted, embedded quotes doubled).

Auth shape mirrors /v1/customer/search: master keys must specify companyId (no global export), non-master keys auto-scope to their own company.

Test plan

  • vitest: 255 passing + 4 integration skipped (was 249 + 4 skipped)
  • Auth contract (403 without header)
  • Query validation (unknown param, limit cap, negative offset)
  • Route mounted (not treated as /v1/customer/:id)
  • Live integration: CSV body shape + truncation message (deferred)

Proudly Made in Nebraska. Go Big Red! 🌽 https://xkcd.com/2347/

Common reporting need: pipe the customer list into a spreadsheet.
New endpoint returns text/csv with the documented field order +
Content-Disposition: attachment so browsers download as
\`customers-company-<id>.csv\`.

Field order matches the Customer JSON schema (custId, names,
addresses, contact info, custCompId). Every field quoted to dodge
the comma-in-name problem; embedded quotes doubled per RFC 4180.

Auth shape mirrors search/bulk:
  - missing authKey                                    -> 403
  - non-master + companyId mismatching auth scope     -> 403
  - non-master without companyId                       -> auto-scope
  - master without companyId                          -> 400

5000-row hard cap per call. Oversize results append a
\`# truncated at <N> rows; re-call with offset=<M>\` comment row
so callers know to page rather than silently dropping data.

Tests: 5 cases covering auth contract, query validation
(unknown param rejected, limit > 5000 rejected, negative offset
rejected), response shape on the DB-failure path, route mounting.

Suite: 35 files / 255 passing + 4 integration skipped (was 34 / 249).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@CryptoJones CryptoJones merged commit e07a8bd into master May 18, 2026
2 of 3 checks passed
@CryptoJones CryptoJones deleted the feat/customer-csv-export branch May 18, 2026 02:10
CryptoJones added a commit that referenced this pull request May 18, 2026
#68)

Companion to /v1/customer/export.csv (#67). Time-entry invoicing
is the natural billing pipeline:

  GET /v1/timeentry/export.csv?customerId=42&from=...&to=...

Filters mirror the existing /v1/timeentry/bycompany/:id:
customerId, from, to (ISO 8601 date-time range on teStartedAt).
Date-range params are permissive on bad input — silent drop, not
400 — so a long-running export script doesn't break on a typo.

5000-row hard cap with `# truncated...` trailing comment when
exceeded. Field order:
  teId, teCustId, teCompId, teStartedAt, teEndedAt, teMinutes,
  teBillable, teDescription
(matches the JSON shape, Date values serialize to ISO).

Auth mirrors customer/export.csv:
  - master without companyId  -> 400
  - non-master + mismatching companyId  -> 403
  - non-master without companyId  -> auto-scope

Tests: 6 cases covering auth contract, query validation
(unknown param, oversize limit, bad ISO datetime, well-formed
query), and route mounting (not parsed as :id).

Suite: 36 files / 261 passing + 4 integration skipped (was 35 / 255).

Co-authored-by: Aaron K. Clark <akclark@thenetwerk.net>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant