Skip to content

Store a goal against the period it was scored in, like a substitution #83

Description

@JaspervdM80

What is wrong

The two kinds of match event are stored differently, and only one of them records where in the match it happened.

GameSubstitution GameGoal
Which half GamePeriodId → period → Half() nothing
When AtSeconds (real elapsed clock) nothing
Minute derived at display time Minute + AdditionalMinute, frozen when it was written

A goal's half is only implied. Minute is capped at the end of the half it was scored in, so on a 70-minute game 1–35 reads as the first half and 36–70 as the second. That is inference from a display value, not a fact on the row, and it has two consequences:

  • It moves under stored data. Editing GameDurationMinutes shifts the half boundary, and goals already on file are silently reinterpreted — a goal written 36 stops meaning what it meant.
  • The minute cannot be corrected. A substitution's minute is derived from AtSeconds and its period, so fixing a period's StartedAtSeconds fixes the substitutions with it. A goal's minute is frozen, so the same correction leaves the goals wrong and the two kinds of event disagreeing on the same timeline.

AdditionalMinute and the pair-comparison ordering that goes with it (MatchMinute.CompareTo) exist only because the goal row has no real clock reading to sort on.

What to do instead

Give a goal the same two facts a substitution already carries:

  • GamePeriodId — nullable FK to GamePeriod, set when the goal is logged from /live
  • AtSeconds — nullable, the real elapsed match clock, same source as GameSubstitution.AtSeconds

Both nullable because a goal typed by hand on /result has neither a clock nor a period behind it. Minute stays as the fallback for exactly those rows, rather than being presentation stored on every goal.

Then:

  • the displayed minute derives through MatchClockReport for both event types, from one code path
  • ordering is by real elapsed seconds instead of comparing a (minute, additional) pair
  • GameGoal.AdditionalMinute and MatchMinute.CompareTo can go
  • correcting a period's timings corrects the goals in it, as it already does for substitutions

Scope

  • Migration: add GamePeriodId (nullable FK, cascade like GameSubstitution) and AtSeconds (nullable int) to GameGoals; drop AdditionalMinute. No backfill — nothing left in an old row says whether a stored 37 was stoppage time or a minute typed in by hand, so existing goals keep reading and sorting exactly as they do today, off Minute alone.
  • MatchGoalService.LogGoalAsync — write the period and the clock reading instead of the minute pair
  • MatchClockReport — one MinuteOf path covering goals and substitutions, plus the fallback for rows with no clock
  • ScoreProgressionReport, LiveMatch.Timeline, MatchResult goal list — order on elapsed seconds, falling back to Minute for rows without one
  • Docs: models.md (the GameGoal table and the note under GameSubstitution), ui_components.md (the MatchMinute bullet), known_issues.md (the "a goal's minute is not one number" entry describes the shape this replaces)

Background

The 35+2 display and the pair ordering landed in "Run the live match in halves, and write a minute the way football does" (#80 follow-up branch). That fixed the visible bug — first-half stoppage events sorting after the restart — but stored the fix as a display value on the goal rather than recording where the goal actually happened. This issue is the model change that makes the same behaviour fall out of the data.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions