Skip to content

Model the whole domain: communities, sessions, ratings and operations - #33

Merged
elgorro merged 1 commit into
mainfrom
feat/data-model-foundation
Aug 12, 2026
Merged

Model the whole domain: communities, sessions, ratings and operations#33
elgorro merged 1 commit into
mainfrom
feat/data-model-foundation

Conversation

@elgorro

@elgorro elgorro commented Aug 12, 2026

Copy link
Copy Markdown
Owner

Lays down the data model the product needs, replacing a schema that was players,
teams, matches and tournaments — with an EloRating class nothing outside the
tests ever called.

Refs #32.

What changes

45 entities across eleven bounded contexts, one EF configuration per entity, and
five migrations that each apply and revert on their own.

Three changes are structural:

  • Rating moves from Player to CommunityMember. A player’s standing is
    relative to the people they play with, not global.
  • MatchAppearance records who actually played, with the rating before,
    after and delta. Teams are lineups and change; who played a given match does
    not. This is what makes ratings explainable and replayable.
  • Matches snapshot the scoring they were played under, so amending a rule
    set cannot rewrite the past.

Elo is no longer dead code. MatchRatingCalculator sits on top of
EloRating, and MatchQueries.CreateAsync applies ratings on entry. Community
and format are derived from the teams — a team already belongs to a community,
and its size is the format — so the WebAssembly client needed no new fields.

Money is double-entry in minor units, which makes dues, session fees, expenses
and sponsorship one mechanism rather than four.

Enforced by the database

Application-level checks race, so these are constraints:

  • Court reservations cannot overlap (EXCLUDE USING gist, half-open ranges)
  • Ledger amounts are positive and move between two different accounts
  • A member-balance account has a member; nothing else does
  • An item is on loan to at most one person at a time
  • A community has at most one current season

Verification

Each migration was applied against a throwaway Postgres 18, then the whole chain
reverted (46 tables back to 7) and rolled forward again.

  • Slug backfill — collisions suffixed (jonas-mueller, jonas-mueller-2),
    German transliterated (strassburger-weiss), degenerate names fall back to player
  • Ratings survive the move in both directionsDown() carries them back
    before the memberships holding them are dropped
  • Court overlap — five cases: overlapping rejected, abutting slots allowed,
    cancelled ignored, inverted range rejected
  • Money — negative amounts, same-account entries and member-balance accounts
    without a member all rejected
  • Appearances reconstructed from existing lineups, two a side, ratings left
    flat because the deltas that produced them were never recorded

22 tests pass. Release build clean, dotnet format --verify-no-changes clean.

Worth knowing when reviewing

  1. The Location backfill is a no-op in practice. The intent was to match
    free text against courts, but no courts exist at that point in the chain, so
    it all lands in LocationNote. EF detected a column rename, so nothing was
    lost. Turning those strings into venues would have meant fabricating data.

  2. TeamMember kept its composite key. A surrogate Id was planned;
    nothing needs one, and adding it means a primary key rebuild for no gain.
    Position and SortOrder were added as intended.

  3. The rating write path has no integration test. The calculator is well
    covered and the schema is verified, but the glue in CreateAsync is not —
    there is no infrastructure test project. See Integration test for the rating write path #34.

  4. Entities are grouped into folders by context but keep the flat
    Ssabba.Domain.Entities namespace
    , so no existing using churned.
    IDE0130 is silenced in .editorconfig with a note.

🤖 Generated with Claude Code

The schema was a thin slice — players, teams, matches, tournaments — with an
EloRating class that nothing outside the tests ever called. It carried no
community, no scheduling, no money, and no notion of format, so 2v2 and 6v6
were indistinguishable.

This lays down the model the product actually needs, across eleven bounded
contexts, in five migrations that each apply and revert on their own.

Three changes are structural and worth calling out:

- Rating moves from Player to CommunityMember. A player's standing is relative
  to the people they play with, not global.
- MatchAppearance records who actually played, with the rating before, after
  and delta. Teams are lineups and change; who played a given match does not.
  This is what makes ratings explainable and replayable.
- Matches snapshot the scoring they were played under, so amending a rule set
  cannot rewrite the past.

Elo is no longer dead code: MatchRatingCalculator sits on top of it and
MatchQueries.CreateAsync now applies ratings on entry. Community and format are
derived from the teams — a team already belongs to a community, and its size is
the format — so the client needed no new fields.

Money is double-entry from the start, in minor units, which makes dues, session
fees, expenses and sponsorship one mechanism rather than four.

Constraints the database enforces rather than the application: court
reservations cannot overlap (EXCLUDE USING gist), ledger amounts are positive
and move between two different accounts, an item is on loan to one person at a
time, and a community has at most one current season.

Each migration was applied and reverted against Postgres 18, including the
backfills: player slugs are derived and de-duplicated, ratings survive the move
in both directions, and appearances are reconstructed from existing lineups.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@elgorro
elgorro merged commit c956ff3 into main Aug 12, 2026
2 checks passed
@elgorro
elgorro deleted the feat/data-model-foundation branch August 12, 2026 12:49
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