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 — provider config
| Step | Expected |
|---|---|
Owner posts { apiKey, email, password } | Row inserted with credentials_encrypted; existing row deactivated. |
GET /provider-config | Returns { ..., hasCredentials: true } with no plaintext. |
Rotate CREDENTIAL_ENCRYPTION_KEY then attempt to use the saved config | Decrypt fails; import job lands failed (FIT-120). Re-enter credentials. |
Imports — Arbox (Business API)
| Step | Expected |
|---|---|
Trigger with arboxApiKey only, entities: { plans: true, members: true } | Job lands in_progress; Business API path runs. Plans created first, members second. |
| Concurrent trigger while one is running | 409 Active import already exists. |
Whitelist ["a@x.com", "b@x.com"] | Only those two emails create memberships. |
| Source has 1500 members, one with malformed email | 1499 succeed; the malformed row lands in results.members.errors[] after 3 retries. |
| Cancel a pending job | Status flips to cancelled; queue job removed. |
Imports — Arbox (Management API)
| Step | Expected |
|---|---|
Trigger with arboxEmail + arboxPassword | loginForManagement succeeds; active + inactive reports fetched in parallel. |
| Member is in both reports | Active wins on merge; arboxStatus = 'active'. |
| Login fails (bad password) | Job lands failed with provider 401 message. |
| Enrichment phase runs after Management import | import-enrich queue receives jobs; populated fields visible in member_profiles. |
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.