Skip to content

Audit and repair events whose course belongs to a different organization #2235

Description

@moveson

Background

Courses were originally envisioned as having no organization owner; every course now has an organization_id. Under the old rubric an event could reasonably use any course, so some events may still point at a course owned by a different organization than their event group's.

One such relic surfaced on staging 2026-08-21: 2023-marathon-test-100k (event group 2023-marathon-test) uses a course from another organization, which caused a 500 on the event edit form (Scout error group 124190 on ost-staging; see #2234 for the mechanism — the form's course selector only offers the event group organization's courses, so it silently blanked course_id).

Audit

SQL:

select events.id,
       events.slug            as event_slug,
       event_groups.slug      as event_group_slug,
       eg_org.name            as event_group_organization,
       courses.slug           as course_slug,
       c_org.name             as course_organization
from events
  join event_groups on event_groups.id = events.event_group_id
  join organizations eg_org on eg_org.id = event_groups.organization_id
  join courses on courses.id = events.course_id
  join organizations c_org on c_org.id = courses.organization_id
where courses.organization_id != event_groups.organization_id
order by c_org.name, courses.slug, events.id;

Console equivalent:

Event.joins(:event_group, :course)
     .where("courses.organization_id != event_groups.organization_id")
     .includes(:course, event_group: :organization)
     .map { |e| [e.slug, e.event_group.organization.name, e.course.slug, e.course.organization.name] }

Run in both production and staging (staging has at least the one known case).

Remediation guidance

Per affected course, the right fix depends on who actually uses it:

  • Course used only by one organization's events, but owned by another org: fix ownership — course.update(organization_id: <correct org>). No event changes needed.
  • Course genuinely shared by events from multiple organizations: needs a decision — likely duplicate the course per organization and repoint the minority events. Two cautions when repointing events.course_id:
    • It fires Interactors::ChangeEventCourse (split remapping) — do it through the model, not update_column, and verify split times afterward.
    • Splitting a shared course changes split_ids, which is the pooling boundary for SplitTimeQuery.typical_segment_time and Projection.sql — moving events off a shared course removes their times from the original course's statistics. Usually desirable (see Simulate/duplicate test events share the production course and corrupt real events' statistics #2169), but worth knowing before the change.

Follow-up hardening (optional)

Once the data is clean, consider a validation that an event's course belongs to its event group's organization, so new relics cannot be created (API and imports included). Check DuplicateEventGroup in particular — if it can copy an event group to a different organization while reusing the source course, it would recreate this state (related: #2169's proposal to deep-copy courses when duplicating).

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions