Skip to content

FreeScout Pipeline

Syncs Rondo Club member data to FreeScout helpdesk as customers, enriching support tickets with member context. Also downloads FreeScout conversations and creates activities in Rondo Club.

Runs daily at 8:00 AM (Amsterdam time).

Terminal window
scripts/sync.sh freescout # Production (with locking + email report)
node pipelines/sync-freescout.js --verbose # Direct execution (verbose)
pipelines/sync-freescout.js
├── Check credentials (FREESCOUT_API_KEY + FREESCOUT_URL)
├── steps/submit-freescout-sync.js
│ ├── steps/prepare-freescout-customers.js → data/freescout-sync.sqlite
│ └── Submit to FreeScout API → FreeScout customers
└── Conversations pipeline
├── steps/download-freescout-conversations.js → data/freescout-sync.sqlite
├── steps/prepare-freescout-conversations.js → activity payloads
└── steps/submit-freescout-activities.js → Rondo Club activities

Before running, pipelines/sync-freescout.js verifies that FREESCOUT_API_KEY and FREESCOUT_URL are configured in .env. If not, the pipeline exits with an error.

Script: steps/prepare-freescout-customers.js (called internally by steps/submit-freescout-sync.js)

  1. Reads member data from data/rondo-sync.sqliterondo_club_members
  2. Reads team assignments from data/rondo-sync.sqliterondo_club_work_history
  3. Reads contribution data from data/nikki-sync.sqlitenikki_contributions
  4. Builds customer records with:
    • Name, email, phone from Rondo Club member data
    • Photo URL from member data
    • Website URLs from contact info
    • Team memberships (comma-separated)
    • KNVB ID, member since date
    • Latest Nikki contribution balance and status
  5. Computes source_hash per customer
  6. Upserts into data/freescout-sync.sqlitefreescout_customers

Script: steps/submit-freescout-sync.js Function: runSubmit({ logger, verbose, force })

  1. Reads customers from data/freescout-sync.sqlite where source_hash != last_synced_hash
  2. For each changed customer:
    • No freescout_id: POST /api/customers (create new customer)
    • Has freescout_id: PUT /api/customers/{freescout_id} (update existing)
  3. After creating/updating, syncs custom fields via PUT /api/customers/{id}/customer_fields
  4. Stores returned FreeScout customer ID as freescout_id
  5. Updates last_synced_hash on success
  6. Rate limited: exponential backoff on 5xx errors (1s, 2s, 4s)

Output: { total, synced, created, updated, skipped, deleted, errors }

Sent to POST/PUT /api/customers:

FreeScout FieldSourceOrigin
firstNameacf.first_namerondo_club_members.data_json
lastNameacf.last_namerondo_club_members.data_json
emails[].valueEmail from contact_info repeaterrondo_club_members.data_json
phones[].valueMobile from contact_info repeaterrondo_club_members.data_json
photoUrlPhoto URLrondo_club_members.data_json
websites[].valueWebsite URLs from contact_info repeaterrondo_club_members.data_json

Sent to PUT /api/customers/{id}/customer_fields:

FreeScout Custom FieldField IDSourceOrigin
union_teams1All current team names, comma-separatedrondo_club_work_history
public_person_id4KNVB IDrondo_club_members
member_since5acf['lid-sinds']rondo_club_members
nikki_saldo7Most recent year’s outstanding balancenikki_contributions
nikki_status8Most recent year’s payment statusnikki_contributions

Field IDs are configurable via FREESCOUT_FIELD_* environment variables.

The RelationEnd field from Sportlink member functions is included in FreeScout customer sync data. This tracks when a member’s club-level function ended (e.g., when they stopped being “Voorzitter”).

The conversations pipeline downloads conversations from FreeScout and creates corresponding activities in Rondo Club, providing a unified timeline of member interactions.

  1. Download - Fetches conversations from FreeScout API
  2. Prepare - Matches conversations to Rondo Club persons via email/customer ID
  3. Submit - Creates activities on person records in Rondo Club

Conversations are tracked in data/freescout-sync.sqlitefreescout_conversations table to avoid duplicate activity creation. Each conversation is stored with its FreeScout ID and sync state.

The conversations pipeline is:

  • Integrated into the main FreeScout pipeline orchestrator
  • Runs as part of the daily cron schedule
  • Visible on the sync dashboard
DatabaseTableUsage
rondo-sync.sqliterondo_club_membersMember data (name, contact, KNVB ID)
rondo-sync.sqliterondo_club_work_historyCurrent team assignments
nikki-sync.sqlitenikki_contributionsFinancial contribution data
freescout-sync.sqlitefreescout_customersCustomer → FreeScout ID mapping + hashes
freescout-sync.sqlitefreescout_conversationsConversation tracking for activity sync
FlagEffect
--verboseDetailed per-customer logging
--forceSkip change detection, sync all customers
  • Missing credentials cause immediate exit (not a silent skip)
  • Individual customer sync failures don’t stop the pipeline
  • 5xx errors trigger exponential backoff (up to 3 retries)
  • Conversation sync failures are tracked independently
  • All errors collected in summary report
FilePurpose
pipelines/sync-freescout.jsPipeline orchestrator (customers + conversations)
steps/submit-freescout-sync.jsFreeScout API sync + customer preparation
steps/prepare-freescout-customers.jsCustomer data preparation
steps/download-freescout-conversations.jsDownload conversations from FreeScout
steps/prepare-freescout-conversations.jsMatch conversations to persons
steps/submit-freescout-activities.jsCreate activities in Rondo Club
lib/freescout-db.jsFreeScout SQLite operations (customers + conversations)
lib/freescout-client.jsFreeScout HTTP client + credential check
lib/rondo-club-db.jsRondo Club data lookup
lib/nikki-db.jsNikki contribution lookup
lib/http-client.jsHTTP request utilities