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

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:

  1. MembershipsService.requireMembership — must be owner or admin, else 403.
  2. Insert export_jobs row with status: 'pending', config: <dto>, startedBy: <userId>.
  3. Enqueue export BullMQ job { jobId, orgId }.
  4. Return { data: { jobId } }.

Processor (ExportProcessorExportService.processExport)

  1. Mark job in_progress, startedAt.
  2. Build a memberships query with the filters; join users (left-join member_profiles).
  3. Whitelist columns from config.fields if present.
  4. Encode as CSV with UTF-8 BOM () for Excel compatibility.
  5. Upload to exports/<orgId>/<jobId>.csv.
  6. Mark job completed, results: { rowCount, fileSize }, fileKey.
  7. Email the requester via EmailService.send with a 1h presigned download URL.

Status polling

  • GET /organizations/:orgId/export/jobs — list jobs newest first.
  • GET /organizations/:orgId/export/jobs/:jobId — when status === 'completed', the response includes a freshly-minted downloadUrl valid for the default presign TTL.

Failure

  • Errors in the processor mark the job failed with errorMessage. 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_configs row 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_jobs row (source: 'arbox', status: 'pending').
  • Enqueue import job { jobId, orgId } with 2 attempts + exponential backoff.

Processor → orchestrateImport

  1. Mark job in_progress.
  2. Switch on job.source:
    • arboxorchestrateArboxImport.
    • csvorchestrateCsvImport.
  3. On exception, mark failed with errorMessage; intermediate results preserved.

Arbox orchestration

Plans phase (inline, fast): importArboxPlans upserts plans.

Members phase:

  • If arboxEmail + arboxPassword are present, take the Management API path:
    1. loginForManagement(email, password) → token.
    2. Fetch active + inactive reports in parallel.
    3. Merge by user_fk (active wins on overlap).
    4. Normalize each into NormalizedMemberData with arboxStatus: 'active' | 'inactive'.
    5. Fan out to import-member queue with profileExtras: { arboxUserId: user_fk }.
  • Else fall back to Business API:
    1. fetchUsers(apiKey) returns the active member list only.
    2. 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).
  • arboxUserId written to member_profiles for 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_jobs with source: '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, in pending state.
  • Merges the confirmed mapping into import_jobs.config.
  • Enqueues import job.

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

FailureSurfaceRecovery
Owner role missing403 Only owners and admins can import/exportRe-attempt with the owner account.
Two imports started simultaneously409 Active import already existsWait for the running one to finish or cancel.
Arbox auth expiredJob moves to failed; errorMessage cites 401Re-enter credentials.
Single member row fails (bad email, missing required field)BullMQ retries up to 3, then drops; error captured in results.errorsOwner inspects + fixes upstream, reruns.
Encrypted credentials unreadable (FIT-120)First job to use them fails decryptingRe-enter credentials.
CSV exceeds 5 MB400 File too largeSplit the CSV.
CSV preview parses with garbled headerssuggestMapping returns empty; owner maps manually.UI flow.