Skip to Content
Living documentation — last reviewed 2026-05-28
FeaturesExports ImportsExports & Imports — QA Plan

Exports & Imports — QA Plan

Exports — auth & rate

StepExpected
Member (non-admin/owner) calls POST /export/members403 Only owners and admins can export data.
Owner calls with empty filtersJob enqueued; CSV contains every member row.
11 export requests in 60 seconds from one IP11th returns 429.

Exports — content

StepExpected
Filter roles: ['coach']CSV contains only coach rows.
Filter statuses: ['active', 'invited']CSV contains only those statuses.
Filter fields: ['email', 'firstName']CSV has those two columns only, in the order they’re emitted by the select clause.
CSV opened in Excel with Hebrew namesRenders correctly (UTF-8 BOM present).
Job completesEmail lands with a presigned download URL; URL works for ~1h.
Job 3h after completionGET /jobs/:jobId returns the row with a fresh presigned downloadUrl (r2.getPresignedUrl regenerates).

Exports — failure

StepExpected
Force a DB outage mid-processorJob lands in failed with errorMessage.
Force an R2 upload failureSame.
Email send failsJob still marked completed; sendExportEmail warning logged. Download URL still served via the API endpoint.

Analytics tab CSV (synchronous)

StepExpected
GET /analytics/export?tab=revenue-summary as owner200 with Content-Disposition: attachment; filename="revenue-summary-<date>.csv".
Unknown tab400.
Member calls403.

Imports — Arbox Public V3

StepExpected
Owner posts { apiKey }One encrypted Arbox provider row exists; no email or password field is accepted.
GET /provider-configReturns { ..., hasCredentials: true } with no plaintext.
Start Arbox members + plans importJob snapshot contains no credential; worker loads the encrypted key and calls only Public V3.
Source user has active: falseImported member is recorded inactive when that authorized V3 field is present.
Trigger entities.leads: true400; leads remain unavailable pending official API validation.
Source contains address, ID, emergency, medical, RFID, or child-status dataNone of those fields persist in Taikan profiles.
Concurrent trigger while one is running409 Active import already exists.
Whitelist ["a@x.com", "b@x.com"]Only those two emails create memberships.
Cancel a pending jobStatus flips to cancelled; queue job removed.
Arbox import completes on a box whose transactions carry document linksEach imported member’s receipts appear under “Receipts from Arbox” on their Memberships tab, newest first; “Open receipt” opens the PDF; the toast counts the receipts.
Re-run the same importNo receipt is duplicated.
Arbox fails some transaction windowsImport still completes; a warning toast names how many periods are missing; re-running fills them.
Coach opens a member’s profileNo receipts request is made and no section renders.
Erase a member with imported receiptsTheir imported_receipts rows are gone.
Arbox import on a box with renewals”Memberships from Arbox” lists each member’s past and current memberships with dates and state; the toast counts them; each member has at most one subscription per plan.
Re-run the same importNo imported membership or subscription is duplicated.
Inactive Arbox memberTheir cancelled subscription’s cancelledAt is the end of their last Arbox membership, not the import day.
Arbox user with birthday 0000-00-00Member is created with no birthday.
Arbox users without emailNot imported; a warning toast gives their count.
Large import (thousands of members)“Member phase done” and “Enrichment done” log once each; the receipt sweep runs once.

Arbox break-glass

StepExpected
Gate unset or not trueConfigure, revoke, and run are denied.
Clerk actor lacks privateMetadata.arboxBreakGlass === trueConfigure, revoke, and run are denied.
Run lacks customer authorization, internal approval, or valid bounded windowRun is denied before a provider request.
Successful runEncrypted credential remains server-side; run row records actor, references, window, status, and aggregate counts only.
Failed provider requestRun row is marked failed; no secret, token, or response payload is stored.
RevokeThe dedicated encrypted credential row is deleted.

Imports — CSV

StepExpected
Upload a 4MB CSV200 with preview + suggested mapping.
Upload a 6MB CSV400 MaxFileSizeValidator.
Upload a non-CSV (rename .txt to .csv)400 file type validator (skipMagicNumbersValidation note: validator only checks extension; magic bytes skipped).
Confirm a job before upload404 / 409.
Confirm with valid mapping, runJob lands in_progress, then completed.
Confirm a job whose status is already in_progress409.
Upload then cancel before confirmingStale pending CSV job is dropped on next upload via cancelStalePendingCsvJobs.

Imports — idempotency

StepExpected
Run the same Arbox import twiceMembers upserted; no duplicate users rows.
Member exists with a different email cased differentlyTreated as the same row (case-insensitive match on import path).
Plan exists with the same nameUpdate, not duplicate.

Negative / robustness

  • Empty Arbox response (no members) → job completes immediately with 0 created.
  • BullMQ Redis outage → orchestration fails fast; new imports rejected until Redis recovers.
  • One member-fan-out job hits a DB unique violation → BullMQ retries; eventual success after the race resolves.

Observability

  • Pino logs every phase transition with [job:<id>].
  • import_jobs.results is the source of truth for “what landed and what didn’t”.
  • No PostHog event today; gap.