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 (ExportProcessor → ExportService.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 accepts { apiKey } only:

  • Owner / admin only.
  • The API key is encrypted at rest and is never returned or copied into import_jobs.config.
  • Reconfiguring Arbox removes the previous encrypted provider row for that org.
  • DELETE /organizations/:orgId/import/provider-config/:configId erases the encrypted key.

Trigger and Public V3 import

POST /organizations/:orgId/import accepts an ImportConfigDto for source: 'arbox' with selected members and plans. Arbox lead import is rejected until an official endpoint is validated.

  • Owner check, then ensureNoActiveImport(orgId) — only one import per org at a time.
  • The job snapshot contains import selections only. It contains no API key, password, email, session, or token.
  • The worker reads the encrypted API key only at execution time.
  • The only customer import transport is https://arboxserver.arboxapp.com/api/public/v3, authenticated with api-key.
  • /users supplies members and /membershipTypes supplies plans. Member activity is derived from the documented Public V3 active field where supplied by the authorized source.
  • Missing source fields remain missing: there is no customer-session fallback and no inferred inactive-history claim.

Receipts (enrichment phase)

Before the per-member enrichment jobs fan out, ArboxReceiptImportService copies every receipt Arbox issued to the imported members into imported_receipts.

  • Source: GET /v3/reports/transactionsReport, read box-wide in consecutive 31-day windows (Arbox’s maximum; both ends inclusive) from the earliest Arbox created_time among imported members to today in the org timezone, 500 rows per page. One box-wide sweep is a few hundred requests; a per-member user_id sweep would be one sweep per member.
  • A row is a receipt when its receipt field holds an https:// document link. Failed charges carry none and are skipped. Rows of people who were not imported are dropped.
  • Each receipt is attached to the member’s membership, with the box-local time converted to an instant, the amount in cents, the org currency, installments and the card’s last four digits.
  • Idempotent: (organization_id, source, external_id) is unique on the Arbox transaction id, so a re-run imports only what is missing.
  • A window Arbox fails to serve after retries is skipped and listed in results.receipts.failedWindows; the owner sees a warning toast and re-running the import fills the gap. The import itself still completes.
  • History only. Receipts never enter payment_transactions, so billing, refunds and revenue analytics never count money Taikan did not collect.
  • Staff with payments:manage see them on the member’s Memberships tab under “Receipts from Arbox” (GET /organizations/:orgId/payments/members/:membershipId/imported-receipts). The section is absent for members with none.
  • The document link is a bearer URL: Arbox’s invoicing provider (Rivhit on the pilot box) serves the PDF, naming the member, to anyone holding it. Member erasure deletes the rows.
  • Boxes on Arbox’s own invoicing module may not put a link in receipt; their documents sit behind /v3/finance/reports/*, which is not read.

Data minimization

Normal Arbox and CSV member imports persist selected identity, contact, birthday, gender, membership, and plan data. They do not persist address, national ID, secondary/emergency contacts, medical certificates, RFID/check-in identifiers, or child-status fields.

Break-glass session workflow

Customer imports never use a management API or customer password. The only session capability is the server-side ArboxBreakGlassService, invoked through scripts/arbox-break-glass.ts; it has no HTTP controller or UI.

Every configuration or revoke requires the environment gate and Clerk entitlement. Every run additionally requires:

  1. ARBOX_BREAK_GLASS_ENABLED=true;
  2. a Clerk session token for a user whose privateMetadata.arboxBreakGlass is exactly true;
  3. a customer-authorization reference and an internal-approval reference;
  4. a non-expired, bounded authorization window.

Credentials are encrypted in arbox_break_glass_credentials; runs record actor, authorization references, bounded window, outcome, and aggregate counts in arbox_break_glass_runs. Secrets, session tokens, and report payloads are not recorded in jobs or audit records. Revoke deletes the credential row.

Legacy credential scrub

Migration 0135_arbox_import_security.sql deletes legacy Arbox provider credentials and removes API-key, email, password, and legacy Arbox credential fields from existing Arbox job snapshots.

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, taikanField> }; 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.

Completion

  • CSV member imports complete when all member jobs finish. They never read Arbox credentials or enqueue the Arbox enrichment phase.
  • A queued legacy CSV enrichment job completes without invoking Arbox.

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.

Managed members

Spec: managed-members/behavior.md §3.4.

  • With the managed-members flag on, a CSV row with a truthy isChild column or no email becomes a managed member, linked to the adult named in parentEmail / parentPhone with a pending link (apps/api/src/import/import.service.ts:793-806). With it off, rows behave as before (emailless rows skipped).
  • Rows that can’t be placed go to the reconciliation queue: no_guardian, no_birth_date, guardian_ambiguous (import.service.ts:1367-1371; GET/POST import/jobs/:jobId/reconciliation). No screen for it yet. Surname matching is never used.
  • The Arbox direct sync does not create children; only the CSV import does.