Skip to content

Add CSV client/pet bulk import#22

Merged
bradburch merged 14 commits into
custom-servicesfrom
csv-client-import
Jul 11, 2026
Merged

Add CSV client/pet bulk import#22
bradburch merged 14 commits into
custom-servicesfrom
csv-client-import

Conversation

@bradburch

Copy link
Copy Markdown
Owner

Summary

  • Sitters can bulk-import their client list from a CSV file (4 fixed columns: Client Email, Client Name, Pet Name, Pet Type — one row per pet) via a new POST /:slug/admin/customers/import route and a matching upload UI in the Clients section.
  • No new dependencies, no schema changes, no multipart upload — the browser reads the file as text and sends it as a plain JSON body, reusing the existing insertInvitedCustomer/addEndUserPet idempotent repo functions.
  • Re-uploading the same file (or a repeat with overlapping data) is safe: customers are reused by email (existing idempotency), and pets are deduped by name per-client both within one file and across repeated uploads.
  • Bad rows don't block good ones — invalid emails skip the row; invalid/missing pet data still creates the client and skips only the pet, with a per-row reason reported back.
  • Sitter chooses whether importing sends the standard invite email, and invites only ever go to genuinely new customers (never a pre-existing one, regardless of prior status).
  • Example CSV committed to the repo (docs/examples/) and downloadable directly from the import UI (public/).

Notable review findings (all resolved)

  • Whole-branch review caught a real gap: the per-row import loop had no error isolation, so a single row's DB-level failure (e.g. a race between two concurrent imports for the same tenant) would 500 the entire request even though earlier rows in the same loop had already committed. Fixed with a per-row try/catch that degrades any DB-level failure to a skipped row instead of crashing the request, with a test proving the failure path (verified to genuinely fail without the fix).
  • A stale file-input UX issue (the picker didn't clear after a successful import, risking accidental resubmission) was caught and fixed during task-level review.
  • Two low-risk limitations were reviewed and accepted rather than fixed: the hand-rolled CSV parser's quote-handling isn't fully RFC4180-strict (stray mid-field quotes are rare in real names), and pet-dedup is sequential-only (safe for realistic re-upload scenarios, not for true simultaneous concurrent imports).

Test plan

  • npm run typecheck
  • npm run lint
  • npm test (250/250 passing)
  • npm run build
  • npx wrangler deploy --dry-run
  • Independent whole-branch code review (spec/non-goal alignment, tenant isolation, concurrency/error-handling edge cases) — approved

🤖 Generated with Claude Code

bradburch and others added 13 commits July 10, 2026 11:35
…ar reconciliation)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…aces

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
POST /:slug/admin/customers/import parses a CSV of client/pet rows,
upserts customers and pets via existing repo helpers, dedups pets per
client, and optionally sends invites to genuinely new customers.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Add the ImportResult type to represent the response from a bulk CSV import,
and add the import method to adminApi.customers to call the backend import endpoint.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Uncontrolled <input type="file"> doesn't clear from a state reset alone;
remount it via a key that bumps on success so the sitter can't
accidentally resubmit the same file.
…hole request

Wrap each row's DB-mutating work in try/catch and skip the row on failure
instead of throwing uncaught -- previously processed rows in the same
request had already been committed to D1, so a mid-loop throw silently
left partial results with no response body at all.
A large import triggers ~5 sequential D1 calls per pet-bearing row and can
exceed Workers' subrequest/CPU ceiling partway through the loop. That abort
happens outside the per-row try/catch, so it returns a bare 500 with no
partial-import report even though earlier rows already committed. Check the
row count up front and fail fast with an actionable error instead.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…lines

parseCsvRows() used to silently skip blank lines, but admin.ts computed each
reported row number positionally against that filtered array. A blank line
anywhere before a bad row shifted every subsequent skippedRows entry off from
the sitter's actual spreadsheet line, undermining the whole point of the
partial-import report.

parseCsvRows() now emits every line (blank lines become a single ['']
row), keeping array index aligned with source line number. The import loop
gained an explicit blank-row check ahead of the malformed-row check so blank
lines are still silently skipped, but line alignment for everything after
them stays correct.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…p guard

Rows are processed with sequential awaits, so by the time the same email
recurs on a later row, getEndUserByEmail already sees the prior row's
committed insert and existing is truthy — the .some() dedup branch could
never actually fire. endUserId was also unused past this point, so
freshCustomers collapses to string[]. Behavior-neutral; all existing import
tests still pass.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Resolves conflicts from the concurrently-merged sitter booking dashboard
feature: both branches added routes to the same admin.ts chain and types
to the same api.ts file — merged to keep both sets of additions intact.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@bradburch bradburch merged commit 1e236f0 into custom-services Jul 11, 2026
2 checks passed
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