diff --git a/Events.php b/Events.php index 1cfb6e2..9c2cefc 100644 --- a/Events.php +++ b/Events.php @@ -180,7 +180,10 @@ public static function onCronBeforeAction($event): void $competition->updateAttributes(['last_synced_at' => KickoffTime::dbAt($now)]); $settings->set($stateKey, $now); - if ($report->isSuccess() && $report->updated > 0) { + if ($report->updated > 0) { + // Score regardless of partial errors: scoring is idempotent + // and a bad record (e.g. an undrawn knockout fixture) must + // not block scoring of the games that imported cleanly. (new ScoringService($competition))->scoreAllFinishedGames(); (new MatchdayBonusService($competition))->awardForCompleteMatchdays(); } @@ -221,7 +224,10 @@ private static function runSyncForActiveCompetitions($event, bool $syncFixtures) $competition->updateAttributes(['last_synced_at' => KickoffTime::nowDb()]); self::log($controller, "Kickoff results [{$competition->slug}]: " . $report->summary()); - if ($report->isSuccess() && $report->updated > 0) { + if ($report->updated > 0) { + // Score regardless of partial errors: scoring is idempotent + // and a bad record (e.g. an undrawn knockout fixture) must + // not block scoring of the games that imported cleanly. $scored = (new ScoringService($competition))->scoreAllFinishedGames(); self::log($controller, "Kickoff scoring [{$competition->slug}]: {$scored} tip(s) updated."); } diff --git a/adapters/HumHubApiAdapter.php b/adapters/HumHubApiAdapter.php index f4a5e0f..049f7d5 100644 --- a/adapters/HumHubApiAdapter.php +++ b/adapters/HumHubApiAdapter.php @@ -274,6 +274,12 @@ private function applyGame( } $homeExt = (string) ($matchData['home_external_id'] ?? ''); $awayExt = (string) ($matchData['away_external_id'] ?? ''); + if ($homeExt === '' || $awayExt === '') { + // Knockout fixture whose teams aren't drawn yet — expected state, + // not an error. Skip so it doesn't poison the sync report. + $report->skipped++; + return; + } $home = $teamsByExternalId[$homeExt] ?? null; $away = $teamsByExternalId[$awayExt] ?? null; if ($home === null || $away === null) { diff --git a/controllers/AdminController.php b/controllers/AdminController.php index 58cc90f..69c69b9 100644 --- a/controllers/AdminController.php +++ b/controllers/AdminController.php @@ -145,6 +145,7 @@ public function actionSetupWm2026() } if (!$competition->save()) { + Yii::error('FWC 2026 setup: could not create competition: ' . implode(', ', $competition->getFirstErrors()), 'kickoff'); Yii::$app->session->setFlash('error', Yii::t( 'KickoffModule.base', 'Could not create competition: {errors}', @@ -159,6 +160,13 @@ public function actionSetupWm2026() $metadataReport = $adapter->applyMetadata($competition); + // Fixtures may already include finished games (e.g. set up mid-tournament), + // so score them now — syncFixtures alone doesn't, and waiting for cron + // would leave the competition showing finished games with no points. + (new ScoringService($competition))->scoreAllFinishedGames(); + (new MatchdayBonusService($competition))->awardForCompleteMatchdays(); + (new SpecialBetResolver())->autoResolveAll($competition); + $type = $fixturesReport->isSuccess() && $metadataReport->isSuccess() ? 'success' : 'warning'; $message = Yii::t( 'KickoffModule.base', @@ -309,6 +317,7 @@ private function autoLoadInitialSchedule(Competition $competition): void ['summary' => $report->summary()], )); } elseif (!$report->isSuccess()) { + Yii::error('Schedule auto-load reported: ' . $report->summary() . ' — ' . implode('; ', $report->errors), 'kickoff'); Yii::$app->session->setFlash('warning', Yii::t( 'KickoffModule.base', 'Schedule auto-load reported: {summary} — {errors}', @@ -379,7 +388,10 @@ public function actionSyncResults($id) $competition->updateAttributes(['last_synced_at' => KickoffTime::nowDb()]); $this->flashReport($report, Yii::t('KickoffModule.base', 'Results sync')); - if ($report->isSuccess() && $report->updated > 0) { + if ($report->updated > 0) { + // Score regardless of partial errors: scoring is idempotent and a + // bad record (e.g. an undrawn knockout fixture) must not block + // scoring of the games that imported cleanly. $tipCount = (new ScoringService($competition))->scoreAllFinishedGames(); $awarded = (new MatchdayBonusService($competition))->awardForCompleteMatchdays(); $msg = Yii::t('KickoffModule.base', '{n} tip(s) scored.', ['n' => $tipCount]); @@ -832,7 +844,9 @@ private function flashReport(SyncReport $report, string $label): void if ($report->isSuccess()) { Yii::$app->session->setFlash('success', $message); } else { - Yii::$app->session->setFlash('error', $message . ' — ' . implode('; ', $report->errors)); + $message .= ' — ' . implode('; ', $report->errors); + Yii::$app->session->setFlash('error', $message); + Yii::error($message, 'kickoff'); } } } diff --git a/controllers/CompetitionController.php b/controllers/CompetitionController.php index d59ef8d..bb67069 100644 --- a/controllers/CompetitionController.php +++ b/controllers/CompetitionController.php @@ -498,6 +498,7 @@ public function actionTips($slug) )); } if ($errors > 0) { + Yii::error("{$errors} tip(s) could not be saved.", 'kickoff'); Yii::$app->session->setFlash('error', Yii::t( 'KickoffModule.base', '{n} tip(s) could not be saved.', @@ -664,6 +665,7 @@ public function actionSpecialBetTips($slug) )); } if ($errors > 0) { + Yii::error("{$errors} special bet tip(s) could not be saved.", 'kickoff'); Yii::$app->session->setFlash('error', Yii::t( 'KickoffModule.base', '{n} special bet tip(s) could not be saved.', diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 28e67cb..22c2a35 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,6 +1,12 @@ Changelog ========= +1.0.8 (June 12, 2026) +--------------------- +- Fix: Undrawn knockout fixtures no longer mark the sync as failed, which had blocked point calculation for the whole competition. +- Fix: Finished games are now scored after every sync and during FWC 2026 setup, not only on a fully error-free sync. +- Enh: Sync and tip-save failures are now written to the application log. + 1.0.7 (June 9, 2026) -------------------- - Enh: Banner added to info, rules and leaderboard pages; nav buttons reordered (Competition → Leaderboard → Rules → Info), admin moved to top-right; headings no longer repeat the competition name. diff --git a/module.json b/module.json index 8c14577..29dc303 100644 --- a/module.json +++ b/module.json @@ -10,7 +10,7 @@ "world cup", "leaderboard" ], - "version": "1.0.7", + "version": "1.0.8", "humhub": { "minVersion": "1.18" },