Exports & Imports — QA Plan
Exports — auth & rate
| Step | Expected |
|---|---|
Member (non-admin/owner) calls POST /export/members | 403 Only owners and admins can export data. |
| Owner calls with empty filters | Job enqueued; CSV contains every member row. |
| 11 export requests in 60 seconds from one IP | 11th returns 429. |
Exports — content
| Step | Expected |
|---|---|
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 names | Renders correctly (UTF-8 BOM present). |
| Job completes | Email lands with a presigned download URL; URL works for ~1h. |
| Job 3h after completion | GET /jobs/:jobId returns the row with a fresh presigned downloadUrl (r2.getPresignedUrl regenerates). |
Exports — failure
| Step | Expected |
|---|---|
| Force a DB outage mid-processor | Job lands in failed with errorMessage. |
| Force an R2 upload failure | Same. |
| Email send fails | Job still marked completed; sendExportEmail warning logged. Download URL still served via the API endpoint. |
Analytics tab CSV (synchronous)
| Step | Expected |
|---|---|
GET /analytics/export?tab=revenue-summary as owner | 200 with Content-Disposition: attachment; filename="revenue-summary-<date>.csv". |
Unknown tab | 400. |
| Member calls | 403. |
Imports — Arbox Public V3
| Step | Expected |
|---|---|
Owner posts { apiKey } | One encrypted Arbox provider row exists; no email or password field is accepted. |
GET /provider-config | Returns { ..., hasCredentials: true } with no plaintext. |
| Start Arbox members + plans import | Job snapshot contains no credential; worker loads the encrypted key and calls only Public V3. |
Source user has active: false | Imported member is recorded inactive when that authorized V3 field is present. |
Trigger entities.leads: true | 400; leads remain unavailable pending official API validation. |
| Source contains address, ID, emergency, medical, RFID, or child-status data | None of those fields persist in Taikan profiles. |
| Concurrent trigger while one is running | 409 Active import already exists. |
Whitelist ["[email protected]", "[email protected]"] | Only those two emails create memberships. |
| Cancel a pending job | Status flips to cancelled; queue job removed. |
| Arbox import completes on a box whose transactions carry document links | Each 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 import | No receipt is duplicated. |
| Arbox fails some transaction windows | Import still completes; a warning toast names how many periods are missing; re-running fills them. |
| Coach opens a member’s profile | No receipts request is made and no section renders. |
| Erase a member with imported receipts | Their imported_receipts rows are gone. |
Arbox break-glass
| Step | Expected |
|---|---|
Gate unset or not true | Configure, revoke, and run are denied. |
Clerk actor lacks privateMetadata.arboxBreakGlass === true | Configure, revoke, and run are denied. |
| Run lacks customer authorization, internal approval, or valid bounded window | Run is denied before a provider request. |
| Successful run | Encrypted credential remains server-side; run row records actor, references, window, status, and aggregate counts only. |
| Failed provider request | Run row is marked failed; no secret, token, or response payload is stored. |
| Revoke | The dedicated encrypted credential row is deleted. |
Imports — CSV
| Step | Expected |
|---|---|
| Upload a 4MB CSV | 200 with preview + suggested mapping. |
| Upload a 6MB CSV | 400 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 upload | 404 / 409. |
| Confirm with valid mapping, run | Job lands in_progress, then completed. |
Confirm a job whose status is already in_progress | 409. |
| Upload then cancel before confirming | Stale pending CSV job is dropped on next upload via cancelStalePendingCsvJobs. |
Imports — idempotency
| Step | Expected |
|---|---|
| Run the same Arbox import twice | Members upserted; no duplicate users rows. |
| Member exists with a different email cased differently | Treated as the same row (case-insensitive match on import path). |
| Plan exists with the same name | Update, 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.resultsis the source of truth for “what landed and what didn’t”.- No PostHog event today; gap.