Skip to content

Discipline Pipeline

Downloads discipline (tucht) cases from Sportlink and syncs them to Rondo Club as discipline_case posts linked to person records.

Runs weekly on Monday at 11:30 PM (Amsterdam time).

Terminal window
scripts/sync.sh discipline # Production (with locking + email report)
node pipelines/sync-discipline.js --verbose # Direct execution (verbose)
pipelines/sync-discipline.js
├── Step 1: steps/download-discipline-cases.js → data/rondo-sync.sqlite
└── Step 2: steps/submit-rondo-club-discipline.js → Rondo Club API

Script: steps/download-discipline-cases.js Function: runDownload({ logger, verbose })

  1. Launches headless Chromium via Playwright
  2. Logs into Sportlink Club
  3. Navigates to the discipline cases section
  4. Scrapes case data from the Sportlink interface
  5. For each case, extracts:
    • dossier_id (unique case identifier, e.g., “T-12345”)
    • public_person_id (KNVB ID of the person involved)
    • match_date, match_description, team_name
    • charge_codes, charge_description
    • sanction_description, processing_date
    • administrative_fee, is_charged
  6. Computes source_hash per case
  7. Upserts into data/rondo-sync.sqlitediscipline_cases table

Output: { success, caseCount }

Script: steps/submit-rondo-club-discipline.js Function: runSync({ logger, verbose, force })

  1. Reads cases from data/rondo-sync.sqlitediscipline_cases
  2. Looks up rondo_club_id for each case’s public_person_id from rondo_club_members
  3. Gets or creates the season taxonomy term (e.g., “2025-2026”):
    • GET /wp/v2/seizoen?slug=2025-2026
    • If not found: POST /wp/v2/seizoen
  4. For each case:
    • Checks if case already exists: GET /wp/v2/discipline-cases?meta_key=dossier_id&meta_value=T-12345
    • New case: POST /wp/v2/discipline-cases
    • Existing case: PUT /wp/v2/discipline-cases/{id}
    • Links to person via acf.person (Post Object field, single integer ID)
  5. Cases without a matching person are skipped (counted as skipped_no_person)

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

Post type: discipline_case REST endpoint: wp/v2/discipline-cases

Rondo Club ACF FieldSQLite ColumnTypeNotes
dossier_iddossier_idTextUnique case ID (e.g., T-12345). Has server-side uniqueness validation.
personrondo_club_members.rondo_club_idPost ObjectSingle integer ID (not array). Looked up via public_person_id.
match_datematch_dateDate PickerReturns Ymd format (e.g., “20260115”)
match_descriptionmatch_descriptionTexte.g., “JO11-1 vs Ajax JO11-2”
team_nameteam_nameTextTeam name from Sportlink
charge_codescharge_codesTextKNVB charge code (e.g., “R2.3”)
charge_descriptioncharge_descriptionTextareaFull charge description
sanction_descriptionsanction_descriptionTextareaPenalty/sanction description
processing_dateprocessing_dateDate PickerYmd format
administrative_feeadministrative_feeNumberFee in euros (e.g., 25.00)
is_chargedis_chargedTrue/FalseWhether fee was charged (“Is doorbelast”)

Taxonomy: seizoen (non-hierarchical, like tags)

  • Used to categorize cases by season (e.g., “2025-2026”)
  • Created automatically when new seasons are encountered
  • Term meta is_current_season marks the active season

Generated as: "{person_name} - {match_description} - {match_date}"

DatabaseTableUsage
rondo-sync.sqlitediscipline_casesCase data + dossier_id (unique key)
rondo-sync.sqliterondo_club_membersKNVB ID → Rondo Club ID lookup (for person linking)
  • ACF Pro (for Post Object fields and REST API integration)
  • Custom post type: discipline_case with show_in_rest = true
  • Taxonomy: seizoen with show_in_rest = true
  • Capability: fairplay - only users with this capability can view cases in the UI
  • All ACF fields must have show_in_rest = true
  • person field uses Post Object type (returns single integer, not array)
  • dossier_id has server-side uniqueness validation
FlagEffect
--verboseDetailed per-case logging
--forceSkip change detection, sync all cases
  • Download failure is logged but doesn’t prevent sync of previously downloaded cases
  • Cases without a matching person in Rondo Club are skipped (not an error)
  • Individual case sync failures don’t stop the pipeline
  • All errors collected in summary report
FilePurpose
pipelines/sync-discipline.jsPipeline orchestrator
steps/download-discipline-cases.jsSportlink discipline case scraping (Playwright)
steps/submit-rondo-club-discipline.jsRondo Club discipline case API sync
lib/discipline-db.jsDiscipline SQLite operations
lib/rondo-club-db.jsRondo Club member ID lookup
lib/rondo-club-client.jsRondo Club HTTP client
lib/sportlink-login.jsSportlink authentication