Skip to content

Runsignup connector: field_mappings plumbing for question → Effort routing#2002

Merged
moveson merged 6 commits into
masterfrom
oveson/claude/1998-runsignup-question-mapping
May 7, 2026
Merged

Runsignup connector: field_mappings plumbing for question → Effort routing#2002
moveson merged 6 commits into
masterfrom
oveson/claude/1998-runsignup-question-mapping

Conversation

@moveson

@moveson moveson commented May 6, 2026

Copy link
Copy Markdown
Collaborator

Refs #1998. This is the plumbing PR — UI for race-director-side configuration is follow-up.

What it does

Adds a per-event-Connection JSON column (field_mappings) that the Runsignup sync pipeline reads and applies, routing question responses into the Effort model's comments / emergency_contact / emergency_phone columns.

Mapping shape

Stored on connections.field_mappings (jsonb, defaults to []):

[
  { "source_question_id": 100, "destination": "emergency_contact" },
  { "source_question_id": 200, "destination": "comments", "suppress_when": "No", "value_when_present": "First Attempt" },
  { "source_question_id": 201, "destination": "comments" }
]
  • destination: one of comments, emergency_contact, emergency_phone.
  • suppress_when: skip this mapping when the response exactly matches this string.
  • value_when_present: use this string instead of the raw response (mostly relevant for comments — e.g. emit "First Attempt" rather than the literal "Yes").
  • Multiple comments-mapped questions concatenate in mapping order via ", ".

Layers changed

Layer Change
Migration add_column :connections, :field_mappings, :jsonb, default: [], null: false
Connectors::Runsignup::Request::GetParticipants passes include_questions=T so question_responses come back from AWS
Connectors::Runsignup::Models::Participant adds :comments, :emergency_contact, :emergency_phone slots
Connectors::Runsignup::FetchEventParticipants accepts field_mappings: kwarg, applies it via new attributes_from_field_mappings helper
Interactors::SyncRunsignupParticipants reads field_mappings off each per-event runsignup Connection, forwards it; new attrs added to RELEVANT_ATTRIBUTES

Configuring a race manually (until UI lands)

conn = event.connections.from_service(:runsignup).find_by(source_type: "Event")
conn.update!(field_mappings: [
  { "source_question_id" => 74423,  "destination" => "emergency_contact" },
  { "source_question_id" => 74424,  "destination" => "emergency_phone" },
  { "source_question_id" => 182149, "destination" => "comments", "suppress_when" => "No", "value_when_present" => "First Attempt" },
  { "source_question_id" => 182151, "destination" => "comments" },
  { "source_question_id" => 182153, "destination" => "comments" },
])

Next sync from the connection management page will populate effort.comments, effort.emergency_contact, effort.emergency_phone for that event's participants.

Out of scope (follow-up PR)

  • UI on the connection setup page for picking question_id → destination via a dropdown.
  • Auto-detection / "suggest a mapping" assistance.

Test plan

  • spec/lib/connectors/runsignup/fetch_event_participants_spec.rb — 5 new examples covering mapping application, suppress rule, value_when_present override, no-mappings fallback, missing question_responses payload.
  • spec/services/interactors/sync_runsignup_participants_spec.rb — 2 new examples covering field_mappings flow-through and the resulting Effort column writes.
  • Rubocop clean on touched files.
  • Manual end-to-end against race 174571 once this is merged: configure mapping via console, trigger sync from UI, verify Effort comments/emergency columns populated.

🤖 Generated with Claude Code

moveson and others added 6 commits May 6, 2026 16:14
Adds a per-event-Connection JSON column (field_mappings) that the
sync pipeline reads and applies, routing Runsignup question_responses
into the Effort model's comments / emergency_contact / emergency_phone
columns.

Mapping shape:
  [
    { source_question_id: 100, destination: "emergency_contact" },
    { source_question_id: 200, destination: "comments",
      suppress_when: "No", value_when_present: "First Attempt" },
    { source_question_id: 201, destination: "comments" },
  ]

  - destination: one of comments / emergency_contact / emergency_phone
  - suppress_when: skip this mapping when the response equals this string
  - value_when_present: use this string instead of the raw response
    (only meaningful for comments)
  - multiple comments-mapped questions concatenate in mapping order
    via ", "

Layers:

- Migration: add_column :connections, :field_mappings, :jsonb,
  default: [], null: false.
- Connectors::Runsignup::Request::GetParticipants now passes
  include_questions=T so question_responses come back from AWS.
- Connectors::Runsignup::Models::Participant adds :comments,
  :emergency_contact, :emergency_phone slots.
- Connectors::Runsignup::FetchEventParticipants accepts a
  field_mappings: kwarg, applies it inside participant_from_raw via
  a new attributes_from_field_mappings helper.
- Interactors::SyncRunsignupParticipants reads field_mappings off
  each per-event runsignup Connection and forwards. The new attrs
  are added to RELEVANT_ATTRIBUTES so the sync loop assigns them
  onto the Effort.

This is the plumbing PR. UI for race-director-side configuration is
follow-up work; for now the mapping is set via console:

  conn = event.connections.from_service(:runsignup).find_by(source_type: "Event")
  conn.update!(field_mappings: [...])

Refs: #1998

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The default belongs at the constructor; .perform is just a thin
forwarder.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously: `cache-version: ${{ env.ImageOS }}-${{ env.ImageVersion }}`.
GitHub Actions' `${{ env.X }}` only resolves workflow-level `env:`
declarations, not runtime environment variables set by the runner —
so this evaluated to a literal `-` (both fragments empty). The intent
was to bust the bundler cache when the runner image rotates so the
smarter_csv native extension gets rebuilt; in practice it busted the
cache exactly once (when the line was first added) and never again.

Today's CI on PR #2002 hit the same SIGILL in smarter_csv's native
parse_context_c that the original fix was meant to prevent.

Capture ImageOS / ImageVersion via a shell step (where they ARE set
by the runner as actual env vars) and reference its outputs as the
cache key. Now image rotations naturally invalidate the cache.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The connector now passes include_questions=T on every participants
request so question_responses come back. The four existing cassettes
under spec/fixtures/vcr/runsignup/get_participants matched on the
full URI and have been updated to include the new param in its
alphabetical position (Faraday sorts query params alphabetically
on the wire).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@moveson moveson merged commit 19d122b into master May 7, 2026
7 checks passed
@moveson moveson deleted the oveson/claude/1998-runsignup-question-mapping branch May 7, 2026 00:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant