- Added a comprehensive plan for bracket expansion, including support for 4, 8, 16, 32, and 68 team formats. - Introduced a template-based bracket system with predefined templates for NCAA March Madness, NFL Playoffs, NBA Playoffs, and simple brackets. - Updated UI flow for bracket creation, allowing admins to select templates and assign participants flexibly. - Enhanced database schema to accommodate new scoring rules and bracket templates. - Proposed updates to scoring logic to handle non-scoring rounds and participant placements correctly. - Documented implementation phases for gradual rollout of new features. - Addressed critical bugs in the playoff event processing and scoring logic, ensuring proper advancement and scoring rules across multiple leagues.
337 lines
11 KiB
Markdown
337 lines
11 KiB
Markdown
# Phase 2 Code Review - Bug Report
|
|
|
|
## 🔴 CRITICAL BUGS
|
|
|
|
### Bug #1: Winner Advancement Logic Is Incorrect
|
|
**File:** `app/models/playoff-match.ts:205-247`
|
|
|
|
**Issue:** The `advanceWinner()` function uses "fill participant1 first, then participant2" logic, which doesn't guarantee proper bracket structure.
|
|
|
|
**Current Code:**
|
|
```typescript
|
|
// Determine which participant slot to fill
|
|
// For simplicity, fill participant1 first, then participant2
|
|
if (!nextMatch.participant1Id) {
|
|
await updatePlayoffMatch(nextMatch.id, { participant1Id: winnerId });
|
|
} else if (!nextMatch.participant2Id) {
|
|
await updatePlayoffMatch(nextMatch.id, { participant2Id: winnerId });
|
|
}
|
|
```
|
|
|
|
**Problem:**
|
|
For Quarterfinals → Semifinals:
|
|
- QF Match 1 winner should → SF Match 1, participant1
|
|
- QF Match 2 winner should → SF Match 1, participant2
|
|
- QF Match 3 winner should → SF Match 2, participant1
|
|
- QF Match 4 winner should → SF Match 2, participant2
|
|
|
|
But with current logic:
|
|
- QF Match 1 winner → SF Match 1, participant1 ✅
|
|
- QF Match 2 winner → SF Match 1, participant2 ✅
|
|
- QF Match 3 winner → SF Match 2, participant1 ✅
|
|
- QF Match 4 winner → SF Match 2, participant2 ✅
|
|
|
|
Actually, this WORKS for simple cases, but breaks if matches complete out of order:
|
|
- If QF Match 2 completes before Match 1, winner goes to SF1 participant1
|
|
- Then QF Match 1 winner goes to SF1 participant2
|
|
- **This reverses the bracket structure!**
|
|
|
|
**Fix:**
|
|
```typescript
|
|
// Determine which participant slot to fill based on match number
|
|
if (match.round === "Quarterfinals") {
|
|
nextRound = "Semifinals";
|
|
nextMatchNumber = match.matchNumber <= 2 ? 1 : 2;
|
|
|
|
// Odd matches (1, 3) → participant1, Even matches (2, 4) → participant2
|
|
const participantSlot = match.matchNumber % 2 === 1 ? 'participant1Id' : 'participant2Id';
|
|
|
|
await updatePlayoffMatch(nextMatch.id, { [participantSlot]: winnerId });
|
|
} else if (match.round === "Semifinals") {
|
|
nextRound = "Finals";
|
|
nextMatchNumber = 1;
|
|
|
|
// SF Match 1 → Finals participant1, SF Match 2 → Finals participant2
|
|
const participantSlot = match.matchNumber === 1 ? 'participant1Id' : 'participant2Id';
|
|
|
|
await updatePlayoffMatch(nextMatch.id, { [participantSlot]: winnerId });
|
|
}
|
|
```
|
|
|
|
**Impact:** Bracket structure breaks if matches complete out of order. Semifinals and Finals may have wrong matchups.
|
|
|
|
**Covered in Future Plans?** Yes, bracket-expansion.md will rewrite this entirely with template system.
|
|
|
|
---
|
|
|
|
### Bug #2: Multiple Fantasy Leagues with Different Scoring Rules
|
|
**File:** `app/models/scoring-calculator.ts:66-74`
|
|
|
|
**Issue:** When processing playoff events, we only use the FIRST fantasy season's scoring rules, even if multiple fantasy leagues with different rules use the same sports season.
|
|
|
|
**Current Code:**
|
|
```typescript
|
|
const seasonSport = event.sportsSeason.seasonSports[0];
|
|
if (!seasonSport) {
|
|
throw new Error(`No fantasy seasons found for sports season ${event.sportsSeasonId}`);
|
|
}
|
|
|
|
const scoringRules = await getScoringRules(seasonSport.seasonId, db);
|
|
```
|
|
|
|
**Problem:**
|
|
Scenario:
|
|
- League A uses "2024 NFL Playoffs" with scoring: 1st=100, 2nd=70
|
|
- League B uses "2024 NFL Playoffs" with scoring: 1st=200, 2nd=150
|
|
|
|
When Super Bowl completes:
|
|
- We calculate points using League A's rules (100 pts)
|
|
- Store `pointsAwarded: 100` in `participantResults`
|
|
- League B also sees 100 pts instead of 200 pts
|
|
|
|
**Actually Not a Bug (Misleading Design):**
|
|
Upon further review, `pointsAwarded` in `participantResults` is stored but **never used**!
|
|
|
|
In `calculateTeamScore()` (line 294):
|
|
```typescript
|
|
const points = calculateFantasyPoints(result.finalPosition, scoringRules);
|
|
```
|
|
|
|
It recalculates points from `finalPosition` using each fantasy season's own scoring rules.
|
|
|
|
**The Real Issues:**
|
|
1. **Confusing Design**: We store `pointsAwarded` but never use it
|
|
2. **Wasted Storage**: Storing incorrect values that are never read
|
|
3. **Potential Future Bug**: Someone might assume `pointsAwarded` is correct and use it
|
|
|
|
**Recommendation:**
|
|
Either:
|
|
- **Option A**: Stop storing `pointsAwarded` entirely (per plans/scoring-system.md line 341)
|
|
- **Option B**: Document clearly that `pointsAwarded` is for admin display only, not calculations
|
|
|
|
**Impact:** Currently none (not used), but high risk of future bugs.
|
|
|
|
**Covered in Future Plans?** Not explicitly mentioned. This is a design cleanup issue.
|
|
|
|
---
|
|
|
|
## 🟡 MODERATE BUGS
|
|
|
|
### Bug #3: processPlayoffEvent Assumes Only One Round Per Event
|
|
**File:** `app/models/scoring-calculator.ts:25-136`
|
|
|
|
**Issue:** The function processes matches based on `event.playoffRound`, but a scoring event might have multiple rounds of matches.
|
|
|
|
**Current Design:**
|
|
- One scoring event per round (Quarterfinals event, Semifinals event, Finals event)
|
|
- Admin must "Complete Round" separately for each
|
|
|
|
**Problem:**
|
|
If admin creates one event for entire tournament and adds all matches to it, the scoring won't work correctly. We'd need to know which round we're processing.
|
|
|
|
**Actually Not a Bug (By Design):**
|
|
Looking at the UI flow in `admin.sports-seasons.$id.events.$eventId.bracket.server.ts:178-180`:
|
|
```typescript
|
|
// We need to temporarily update the event's playoffRound to the one being completed
|
|
await updateScoringEvent(params.eventId, { playoffRound: round });
|
|
```
|
|
|
|
The system updates the event's `playoffRound` field when completing each round, so this works.
|
|
|
|
**Potential Issue:**
|
|
What if admin completes rounds out of order? (Completes Finals before Semifinals?)
|
|
|
|
Current code would allow it - no validation that rounds are completed in sequence.
|
|
|
|
**Impact:** Low - admin would have to intentionally do something wrong.
|
|
|
|
**Covered in Future Plans?** bracket-expansion.md will address this with template system.
|
|
|
|
---
|
|
|
|
### Bug #4: No Validation for Completing Rounds Out of Order
|
|
**File:** `app/routes/admin.sports-seasons.$id.events.$eventId.bracket.server.ts:144-206`
|
|
|
|
**Issue:** Admin can complete rounds in any order (Finals before Quarterfinals, etc.)
|
|
|
|
**Current Code:**
|
|
```typescript
|
|
// Verify all matches in this round are complete
|
|
const roundMatches = matches.filter((m) => m.round === round);
|
|
const allComplete = roundMatches.every((m) => m.isComplete);
|
|
if (!allComplete) {
|
|
return { error: `Not all matches in ${round} are complete` };
|
|
}
|
|
```
|
|
|
|
Only checks if matches in the selected round are complete, not whether previous rounds are complete.
|
|
|
|
**Problem:**
|
|
1. Admin completes Finals first (assigns 1st and 2nd place)
|
|
2. Later completes Semifinals (assigns 3rd and 4th place)
|
|
3. **Participants now have multiple placement records!**
|
|
|
|
Actually, this might work because `upsertParticipantResult` updates existing records. But it's confusing and could lead to data inconsistency.
|
|
|
|
**Fix:**
|
|
Add validation to ensure rounds are completed in order (QF → SF → Finals).
|
|
|
|
**Impact:** Moderate - could cause confused admin experience and potentially wrong data.
|
|
|
|
**Covered in Future Plans?** Not explicitly, but template system might make this clearer.
|
|
|
|
---
|
|
|
|
### Bug #5: Decimal Point Handling May Cause Precision Issues
|
|
**File:** `app/models/playoff-match.ts:125-126`, `database/schema.ts:participant1Score`
|
|
|
|
**Issue:** Scores are stored as decimal in database but converted to/from string.
|
|
|
|
**Current Code:**
|
|
```typescript
|
|
participant1Score: participant1Score?.toString(),
|
|
participant2Score: participant2Score?.toString(),
|
|
```
|
|
|
|
**Database Schema:**
|
|
```typescript
|
|
participant1Score: decimal("participant1_score", { precision: 10, scale: 2 })
|
|
```
|
|
|
|
**Problem:**
|
|
When reading scores back, we get strings: `"27.50"` not numbers: `27.5`
|
|
|
|
The UI displays: `parseFloat(match.participant1Score)` but this could show `27.5` instead of `27.50` (minor cosmetic issue).
|
|
|
|
**Impact:** Very low - cosmetic only for score display.
|
|
|
|
**Fix:** Parse to float when displaying, or use proper decimal type handling.
|
|
|
|
---
|
|
|
|
## 🟢 MINOR ISSUES / CODE QUALITY
|
|
|
|
### Issue #1: Unused Import in complete-round Action
|
|
**File:** `app/routes/admin.sports-seasons.$id.events.$eventId.bracket.server.ts:183-191`
|
|
|
|
**Code:**
|
|
```typescript
|
|
const { findSeasonSportsBySportsSeasonId } = await import("~/models/season-sport");
|
|
const seasonSports = await findSeasonSportsBySportsSeasonId(params.id);
|
|
if (seasonSports.length === 0) {
|
|
return { error: "No fantasy seasons found for this sports season" };
|
|
}
|
|
```
|
|
|
|
**Issue:** We check if seasonSports exist but never use them. The check is good validation, but we could simplify.
|
|
|
|
**Impact:** None - just unnecessary code.
|
|
|
|
---
|
|
|
|
### Issue #2: Error Swallowing in advanceWinner
|
|
**File:** `app/routes/admin.sports-seasons.$id.events.$eventId.bracket.server.ts:124-132`
|
|
|
|
**Code:**
|
|
```typescript
|
|
try {
|
|
await advanceWinner(matchId, winnerId);
|
|
} catch (error) {
|
|
// It's ok if advancement fails (e.g., next match already filled)
|
|
console.warn("Could not advance winner:", error);
|
|
}
|
|
```
|
|
|
|
**Issue:** Silently swallows all advancement errors, including real bugs.
|
|
|
|
**Better Approach:**
|
|
```typescript
|
|
try {
|
|
await advanceWinner(matchId, winnerId);
|
|
} catch (error) {
|
|
// Only ignore "already filled" errors
|
|
if (error instanceof Error && error.message.includes("already has both participants")) {
|
|
console.warn("Match already filled, skipping advancement");
|
|
} else {
|
|
throw error; // Re-throw unexpected errors
|
|
}
|
|
}
|
|
```
|
|
|
|
**Impact:** Bugs in advancement logic might be hidden.
|
|
|
|
---
|
|
|
|
### Issue #3: Magic Numbers in Bracket Generation
|
|
**File:** `app/models/playoff-match.ts:220-226`
|
|
|
|
**Code:**
|
|
```typescript
|
|
if (match.round === "Quarterfinals") {
|
|
nextRound = "Semifinals";
|
|
// Match 1,2 -> SF1, Match 3,4 -> SF2
|
|
nextMatchNumber = match.matchNumber <= 2 ? 1 : 2;
|
|
}
|
|
```
|
|
|
|
**Issue:** Hard-coded bracket structure won't work for larger brackets.
|
|
|
|
**Impact:** Will be rewritten in bracket-expansion.md anyway.
|
|
|
|
---
|
|
|
|
## 📊 SUMMARY
|
|
|
|
| Severity | Count | Critical? |
|
|
|----------|-------|-----------|
|
|
| 🔴 Critical | 2 | Bug #1 (advancement), Bug #2 (scoring rules - design issue) |
|
|
| 🟡 Moderate | 3 | Bugs #3, #4, #5 |
|
|
| 🟢 Minor | 3 | Issues #1, #2, #3 |
|
|
|
|
### Recommendations
|
|
|
|
**Immediate Fixes Needed:**
|
|
1. ✅ **Fix Bug #1** (Winner advancement) - High priority, breaks brackets
|
|
2. ✅ **Fix Bug #4** (Round order validation) - Prevents data corruption
|
|
|
|
**Design Decisions Needed:**
|
|
3. **Bug #2** (pointsAwarded) - Decide: Remove field or document it's unused?
|
|
4. **Issue #2** (Error swallowing) - Better error handling
|
|
|
|
**Can Wait for Phase 2.6+ (Bracket Expansion):**
|
|
5. Bug #3 (multiple rounds) - Will be fixed by template system
|
|
6. Bug #5 (decimals) - Cosmetic only
|
|
7. Issue #1 (unused validation) - Minor cleanup
|
|
8. Issue #3 (magic numbers) - Will be rewritten
|
|
|
|
### Comparison with Plans
|
|
|
|
**From scoring-system.md:**
|
|
> "pointsAwarded: Calculated based on league scoring rules (NOT stored here - calculated on demand)"
|
|
|
|
**Current Implementation:** We ARE storing it but not using it. ❌ Not following plan.
|
|
|
|
**From bracket-expansion.md:**
|
|
> "Phase 2.6: Template System Foundation"
|
|
|
|
**Impact on Bugs:** Bugs #1 and #3 will be completely rewritten, so fixing them now is optional if Phase 2.6 is coming soon.
|
|
|
|
### Decision Point
|
|
|
|
**Should we fix bugs now or wait for bracket-expansion?**
|
|
|
|
**Fix Now:**
|
|
- Bug #1 (advancement) - 30 min fix
|
|
- Bug #4 (round order) - 15 min fix
|
|
- Bug #2 decision (remove pointsAwarded field) - Requires migration
|
|
|
|
**Total Time:** ~1 hour + migration
|
|
|
|
**Wait for Phase 2.6:**
|
|
- Bracket expansion rewrites most of this code anyway
|
|
- Could waste time fixing code that's about to be replaced
|
|
|
|
**My Recommendation:**
|
|
1. Fix Bug #4 (round order validation) now - prevents data corruption
|
|
2. Document Bug #1 and #2, wait for bracket-expansion to fix properly
|
|
3. Make decision on pointsAwarded field (keep but document, or remove entirely)
|