Exports & Imports — Behavior
Exports
Enqueue
POST /organizations/:orgId/export/members with body ExportMembersDto:
{
roles?: ('owner' | 'admin' | 'coach' | 'member')[];
statuses?: ('active' | 'invited' | 'suspended' | 'cancelled')[];
fields?: string[]; // whitelist of columns
}Steps:
MembershipsService.requireMembership— must beowneroradmin, else403.- Insert
export_jobsrow withstatus: 'pending',config: <dto>,startedBy: <userId>. - Enqueue
exportBullMQ job{ jobId, orgId }. - Return
{ data: { jobId } }.
Processor (ExportProcessor → ExportService.processExport)
- Mark job
in_progress,startedAt. - Build a
membershipsquery with the filters; joinusers(left-joinmember_profiles). - Whitelist columns from
config.fieldsif present. - Encode as CSV with UTF-8 BOM (
) for Excel compatibility. - Upload to
exports/<orgId>/<jobId>.csv. - Mark job
completed,results: { rowCount, fileSize },fileKey. - Email the requester via
EmailService.sendwith a 1h presigned download URL.
Status polling
GET /organizations/:orgId/export/jobs— list jobs newest first.GET /organizations/:orgId/export/jobs/:jobId— whenstatus === 'completed', the response includes a freshly-minteddownloadUrlvalid for the default presign TTL.
Failure
- Errors in the processor mark the job
failedwitherrorMessage. The user sees the failure in the export jobs list. No automatic retry — owner re-issues.
Rate limit
@Throttle({ default: { ttl: 60000, limit: 10 } })on the controller — 10 requests per minute per IP.
Analytics tab CSV (synchronous)
GET /organizations/:orgId/analytics/export?tab=<tab> — owner / admin only. Streams CSV synchronously (no queue). Tabs: revenue-summary, revenue-trend, plan-distribution, members-summary, members-growth, popular-classes, class-utilization, workouts-summary. The handler builds the same rows the dashboard uses then writes them as CSV with a Content-Disposition: attachment header.
Imports — Arbox
Provider configuration
POST /organizations/:orgId/import/provider-config body { apiKey, email?, password? }:
- Owner / admin only.
- Existing
import_provider_configsrow for this org+provider is deactivated. - Credentials encrypted via
CredentialEncryptionService.encrypt. - New row persisted with
isActive: true.
GET /organizations/:orgId/import/provider-config returns metadata only — hasCredentials: true is the only credential-shape signal; plaintext never returns.
Trigger
POST /organizations/:orgId/import body ImportConfigDto:
{
source: 'arbox';
entities: { members: boolean; plans: boolean; leads?: boolean };
arboxApiKey?: string;
arboxEmail?: string;
arboxPassword?: string;
memberFields?: MemberFieldsDto;
emailWhitelist?: string[];
}- Owner check, then
ensureNoActiveImport(orgId)— only one import per org at a time. - Inline credentials override stored config (used for one-off imports).
- Insert
import_jobsrow (source: 'arbox',status: 'pending'). - Enqueue
importjob{ jobId, orgId }with 2 attempts + exponential backoff.
Processor → orchestrateImport
- Mark job
in_progress. - Switch on
job.source:arbox→orchestrateArboxImport.csv→orchestrateCsvImport.
- On exception, mark
failedwitherrorMessage; intermediateresultspreserved.
Arbox orchestration
Plans phase (inline, fast): importArboxPlans upserts plans.
Members phase:
- If
arboxEmail+arboxPasswordare present, take the Management API path:loginForManagement(email, password)→ token.- Fetch active + inactive reports in parallel.
- Merge by
user_fk(active wins on overlap). - Normalize each into
NormalizedMemberDatawitharboxStatus: 'active' | 'inactive'. - Fan out to
import-memberqueue withprofileExtras: { arboxUserId: user_fk }.
- Else fall back to Business API:
fetchUsers(apiKey)returns the active member list only.- Same normalization pipeline;
profileExtras: { arboxUserId }.
Per-member job (import-member processor):
- Idempotent upsert into
users+memberships+member_profiles. - Whitelist check (skip if email not in whitelist).
arboxUserIdwritten tomember_profilesfor downstream enrichment.- 3 attempts, exponential backoff (2s base).
Optional enrichment phase (import-enrich): re-queries the Business API for fuller data per Arbox user id, populating fields the Management report lacks.
FIT-120 — id encryption bug
Provider credentials are persisted encrypted by CredentialEncryptionService, which uses CREDENTIAL_ENCRYPTION_KEY from the environment. When that key is rotated, previously-encrypted rows cannot be decrypted on next use. The current mitigation is operational: rotate slowly and re-enter credentials in the dashboard. A proper fix needs envelope encryption with a per-row key id so re-key migrations are non-destructive.
Imports — CSV
Upload
POST /organizations/:orgId/import/csv/upload (multipart):
- 5 MB cap,
FileTypeValidator({ fileType: 'csv', skipMagicNumbersValidation: true }). - Cancels stale pending CSV jobs for the org.
ensureNoActiveImport(orgId)guard.- Parses with
CsvParseService.parseAndValidate(buffer)→{ headers, rows, totalRows }. suggestMapping(headers)returns a heuristic header → field map (email,firstName,phone, etc.).- Inserts
import_jobswithsource: 'csv',status: 'pending',config: { parsedRows, headers, suggestedMapping }. - Returns
{ jobId, headers, suggestedMapping, previewRows: rows.slice(0,5), totalRows }.
Confirm
POST /organizations/:orgId/import/csv/jobs/:jobId/confirm body CsvImportConfirmDto:
{
columnMapping: { memberFields: Record<csvHeader, fitkitField> };
entities: { members, plans, leads };
memberFields?: MemberFieldsDto;
planFields?: PlanFieldsDto;
}- Owner only.
- Validates the job exists, is
csv, inpendingstate. - Merges the confirmed mapping into
import_jobs.config. - Enqueues
importjob.
Cancel
DELETE /organizations/:orgId/import/jobs/:jobId — only effective on pending or in_progress jobs.
Job status
GET /organizations/:orgId/import/jobs and /jobs/:jobId mirror the export endpoints — both return the full row with phase, results, errorMessage.
Idempotency
- Imports upsert by email + org. Re-running an Arbox import overwrites profile data with the latest source.
- Single in-flight import per org (
ensureNoActiveImport). - Member-level retries handled by BullMQ (3 attempts).
Failure modes
| Failure | Surface | Recovery |
|---|---|---|
| Owner role missing | 403 Only owners and admins can import/export | Re-attempt with the owner account. |
| Two imports started simultaneously | 409 Active import already exists | Wait for the running one to finish or cancel. |
| Arbox auth expired | Job moves to failed; errorMessage cites 401 | Re-enter credentials. |
| Single member row fails (bad email, missing required field) | BullMQ retries up to 3, then drops; error captured in results.errors | Owner inspects + fixes upstream, reruns. |
| Encrypted credentials unreadable (FIT-120) | First job to use them fails decrypting | Re-enter credentials. |
| CSV exceeds 5 MB | 400 File too large | Split the CSV. |
| CSV preview parses with garbled headers | suggestMapping returns empty; owner maps manually. | UI flow. |