Interactive visualizer for Lucra pool tournament lifecycle & date filtering logic
matchupId takes the lenient path.
These surfaces fetch the matchup via getByPk(matchupId) or getById(matchupId) — no query filters.
The only timing validation is validateTournamentMatchupHasStarted().
functions/remote-action-tenants-api/src/handlers/matchup-pool-tournaments/update-scores.tsMatchupPoolTournamentService.getById(matchupId) — direct lookup, no filtersMatchupPoolTournamentService.updateUsersScores() :1033-1062set-scores-validations.ts:6-13 — validateTournamentMatchupHasStarted()functions/remote-action-matchup/src/handlers/tournaments/update-score.tsMatchupPoolTournamentService.getById(matchupId) — direct lookup, no filtersMatchupPoolTournamentService.updateUsersScores()UpdateUserScore_PoolTournamentstarts_at. If starts_at is NULL, there are zero timing checks.
These surfaces resolve matchups via findOpenMatchups() or findOpenMatchupsByMetadata(),
which enforce status and expiration filters in the query. However, if a matchupId is provided
as an identifier, the lookup falls back to getByPk() and bypasses all query filters,
behaving identically to the By Matchup ID path.
Every "By Matchup Metadata" endpoint below also accepts a matchupId field. When provided, the
metadata/gameId/locationId fields are ignored and the system calls getByPk(matchupId) directly —
skipping all status and expiration filters. The request silently behaves as a "By Matchup ID" call.
This means the strict protections shown below only apply when the caller does not pass a matchupId.
matchupId is provided instead of metadata/gameId, lookup uses getByPk() — status and expiration checks are bypassed.
functions/remote-action-tenants-api/src/handlers/matchup-pool-tournaments/update-scores-matching.tsMatchupScoreIngestionService.ingestTournamentScores() :98-186matchup-matching.service.ts:searchTournamentsAndGamesIds() :92-159matchup.service.ts:findOpenMatchupsByMetadata() :343-388matchupId is provided, getByPk() is used — no query filters applied.
functions/remote-action-tenants-api/src/handlers/handle-score-ingestion.tsMatchupScoreIngestionService.ingestGeneralScores() for both tournament + rec game typesmatchupId is provided, getByPk() is used — no query filters applied.
functions/remote-action-matchup/src/handlers/tournaments/update-score-matching.tsMatchupScoreIngestionService.ingestTournamentScores()UpdateUserScoreMatching_PoolTournamentfunctions/remote-action-tenants-api/src/handlers/recreational-games/update-scores-matching.tsMatchupScoreIngestionService.ingestRecreationalGameScores()| Surface | Lookup | Status | expires_at | starts_at | Auth |
|---|---|---|---|---|---|
PUT .../pool-tournament/:id/users-scores |
by ID | NONE | NONE | checked | API Key |
tournaments_update_user_score |
by ID | NONE | NONE | checked | User JWT |
POST .../pool-tournament/user-score |
query* | filtered | filtered | checked | API Key |
POST .../user-score |
query* | filtered | filtered | checked | API Key |
tournaments_update_user_score_matching |
query* | filtered | filtered | checked | User JWT |
POST .../recreational-games/user-score |
query* | filtered | filtered | checked | API Key |
* Every "query" row above becomes identical to the "by ID" rows when
a matchupId is passed — all status and expiration filters are bypassed via getByPk().
The strict checks only apply when matching by metadata, gameId, or locationId.
All score surfaces eventually call MatchupPoolTournamentService.updateUsersScores() which runs one timing check:
validateTournamentMatchupHasStarted(matchup)starts_at is NULL → skip (allow)starts_at → throw NOT_STARTEDstarts_at → pass
layers/.../matchup/utils/set-scores-validations.ts:6-13matchup-pool-tournament.service.ts:1039
By Matchup Metadata surfaces resolve matchups through MatchupMatchingService. The identifier provided determines the code path:
matchupId → getByPk(matchupId) — NO filters, returns any matchupmatchupMetadata → findOpenMatchupsByMetadata() — status + expiration filteredgameId / locationId → findOpenMatchups() — status + expiration filteredlayers/.../matchup/matchup-matching.service.ts:92-159matchup.service.ts:findOpenMatchups():343-370 and :findOpenMatchupsByMetadata():372-388All possible timing scenarios. Score columns reflect the two lookup methods. See Score Surfaces tab for details.
| Scenario | NOW vs starts_at | NOW vs expires_at | Visible? | Can Join? | Score (by ID) | Score (by query) |
|---|
featured_games_page_items actionexpiresAt = new Date().toISOString()status IN [Open, Confirmed]expires_at >= NOW()Note: starts_at is used only for sorting joined tournaments, never for filtering visibility. When includeClosedTournaments=true, status Closed is also included.
Open or Confirmed (PoolTournament)NOW() < expires_at (if expires_at is set)Note: starts_at is NOT checked for joining. A user can join a tournament that hasn't started yet.
Score submission has multiple code paths depending on the surface and identifier used. See the Score Surfaces tab for the full breakdown.
getById(matchupId) — no filtersvalidateTournamentMatchupHasStarted()status IN [Open, Confirmed, Locked, PendingOutcomes]expires_at > NOW()validateTournamentMatchupHasStarted()matchupId is provided directly.Score submission files are detailed in the Score Surfaces tab.
| File | Role | Key Logic |
|---|---|---|
| Visibility & Feed | ||
metadata/actions.yaml | Action definition | Routes featured_games_page_items to handler |
functions/remote-action-recommended-matchups/src/index.ts | Handler router | Action switch dispatch |
.../handlers/handle-featured-games-page-items.ts | Main handler | Calls getTournaments() + getFeaturedGames() |
.../repository/get-tournaments.ts | Tournament query | expiresAt = new Date().toISOString(), sorting logic |
| Joining | ||
layers/.../matchup-validation.service.ts | Join validation | validateForJoining() :115-167 |
| Score Submission — Handlers (see Score Surfaces for category breakdown) | ||
functions/remote-action-tenants-api/.../update-scores.ts | REST handler | PUT /pool-tournament/:id/users-scores |
functions/remote-action-tenants-api/.../update-scores-matching.ts | REST handler | POST /pool-tournament/user-score |
functions/remote-action-tenants-api/.../handle-score-ingestion.ts | REST handler | POST /user-score |
functions/remote-action-matchup/.../update-score.ts | GraphQL handler | tournaments_update_user_score |
functions/remote-action-matchup/.../update-score-matching.ts | GraphQL handler | tournaments_update_user_score_matching |
functions/remote-action-tenants-api/.../recreational-games/update-scores-matching.ts | REST handler | POST /recreational-games/user-score |
| Score Submission — Shared Services | ||
layers/.../matchup-pool-tournament.service.ts | Core service | updateUsersScores() :1033-1062, findBy(), getQueryFilters() |
layers/.../matchup.service.ts | Base matchup service | findOpenMatchups() :343-370, findOpenMatchupsByMetadata() :372-388 |
layers/.../matchup-score-ingestion.service.ts | Score ingestion | ingestTournamentScores() :98-186, ingestGeneralScores() |
layers/.../matchup-matching.service.ts | Matchup resolution | searchTournamentsAndGamesIds() :92-159, findTournamentsForUser() |
layers/.../utils/set-scores-validations.ts | Score validation | validateTournamentMatchupHasStarted() :6-13 |
| Creation & Data Model | ||
layers/.../utils/pool-tournament-mappers.utils.type.ts | DTO mapping | starts_at defaults to today, expires_at pass-through |
layers/.../dto/base.ts | Input validation | Both dates @IsOptional() @IsDateString() |
layers/.../entities/base-matchup.entity.ts | Entity definition | All date fields optional strings |
| Field | Pool Tournament Default | Source |
|---|---|---|
created_at | Auto-set by database | PostgreSQL default |
starts_at | dto.startsAt ?? DateTime.now().toISODate() (today) | pool-tournament-mappers.utils.type.ts:L147-200 |
expires_at | dto.expiresAt (no default — can be NULL) | pool-tournament-mappers.utils.type.ts:L147-200 |
closed_at | Set when tournament closes | — |
expires_at < starts_at — this creates a misconfigured tournament that expires before it starts, making it unscoreable.