allMatches(); $this->assertCount(3, $matches); $this->assertSame(72, $matches[0]['match_no']); $this->assertSame(104, $matches[2]['match_no']); } public function test_updates_checked_form_rows_across_all_schedule() { $repository = new InMemoryWorldCupScheduleRepository(); $service = new WorldCupScheduleUpdateService($repository); $result = $service->updateMatches([ [ 'enabled' => '1', 'match_no' => 72, 'home_team' => 'Croatia Updated', 'away_team' => 'Ghana', 'venue' => 'Philadelphia', 'kickoff_at' => '2026-06-27 21:00:00', 'status' => 'scheduled', ], [ 'enabled' => '0', 'match_no' => 104, 'home_team' => 'France', 'away_team' => 'Brazil', 'status' => 'closed', ], ], 'admin-01'); $this->assertTrue($result['success']); $this->assertSame(1, $result['data']['updated']); $this->assertSame(1, $result['data']['skipped']); $this->assertSame('Croatia Updated', $repository->matches[72]['home_team']); $this->assertSame('Winner 104 A', $repository->matches[104]['home_team']); } public function test_updates_only_unresolved_matches_for_selected_date() { $repository = new InMemoryWorldCupScheduleRepository(); $service = new WorldCupScheduleUpdateService($repository); $result = $service->updateUnresolvedMatches('2026-07-19', [ [ 'match_no' => 104, 'home_team' => 'France', 'away_team' => 'Brazil', 'venue' => 'East Rutherford', ], [ 'match_no' => 103, 'home_team' => 'Argentina', 'away_team' => 'England', ], [ 'match_no' => 72, 'home_team' => 'Croatia', 'away_team' => 'Ghana', ], ], 'admin-01'); $this->assertTrue($result['success']); $this->assertSame(1, $result['data']['updated']); $this->assertSame(2, $result['data']['skipped']); $this->assertSame('France', $repository->matches[104]['home_team']); $this->assertSame('Brazil', $repository->matches[104]['away_team']); $this->assertSame('closed', $repository->matches[104]['status']); $this->assertSame('Winner 103 A', $repository->matches[103]['home_team']); $this->assertSame('Croatia', $repository->matches[72]['home_team']); $this->assertSame('schedule_update', $repository->audits[0]['action']); } public function test_allows_operator_to_open_match_when_status_is_scheduled() { $repository = new InMemoryWorldCupScheduleRepository(); $service = new WorldCupScheduleUpdateService($repository); $result = $service->updateUnresolvedMatches('2026-07-19', [ [ 'match_no' => 104, 'home_team' => 'France', 'away_team' => 'Brazil', 'status' => 'scheduled', ], ], 'admin-01'); $this->assertTrue($result['success']); $this->assertSame('scheduled', $repository->matches[104]['status']); } public function test_rejects_invalid_payload_rows() { $repository = new InMemoryWorldCupScheduleRepository(); $service = new WorldCupScheduleUpdateService($repository); $result = $service->updateUnresolvedMatches('2026-07-19', [ ['match_no' => '', 'home_team' => 'France', 'away_team' => 'Brazil'], ['match_no' => 104, 'home_team' => '', 'away_team' => 'Brazil'], ['match_no' => 104, 'home_team' => 'France', 'away_team' => 'Brazil', 'status' => 'open'], ], 'admin-01'); $this->assertFalse($result['success']); $this->assertSame(0, $result['data']['updated']); $this->assertCount(3, $result['data']['errors']); } public function test_rejects_invalid_schedule_date() { $repository = new InMemoryWorldCupScheduleRepository(); $service = new WorldCupScheduleUpdateService($repository); $result = $service->updateUnresolvedMatches('2026/07/19', [], 'admin-01'); $this->assertFalse($result['success']); $this->assertSame('Invalid schedule date', $result['message']); } public function test_imports_existing_matches_from_csv_rows_by_match_id() { $repository = new InMemoryWorldCupScheduleRepository(); $service = new WorldCupScheduleUpdateService($repository); $result = $service->importExistingMatches([ [ 'match_id' => '104', 'home_name' => 'France', 'away_name' => 'Brazil', ], [ 'match_id' => '103', 'home_name' => 'Argentina', 'away_name' => 'England', ], [ 'match_id' => '', 'home_name' => 'Spain', 'away_name' => 'Portugal', ], ], 'admin-01'); $this->assertTrue($result['success']); $this->assertSame(2, $result['data']['updated']); $this->assertSame(0, $result['data']['skipped']); $this->assertCount(1, $result['data']['errors']); $this->assertSame('France', $repository->matches[104]['home_team']); $this->assertSame('Brazil', $repository->matches[104]['away_team']); $this->assertSame('schedule_import', $repository->audits[0]['action']); } } class InMemoryWorldCupScheduleRepository implements WorldCupScheduleRepositoryInterface { public $matches = [ 72 => [ 'match_no' => 72, 'home_team' => 'Croatia', 'away_team' => 'Ghana', 'venue' => 'Philadelphia', 'kickoff_at' => '2026-06-27 21:00:00', 'status' => 'scheduled', ], 103 => [ 'match_no' => 103, 'home_team' => 'Winner 103 A', 'away_team' => 'Winner 103 B', 'venue' => 'Miami Gardens', 'kickoff_at' => '2026-07-18 21:00:00', 'status' => 'closed', ], 104 => [ 'match_no' => 104, 'home_team' => 'Winner 104 A', 'away_team' => 'Winner 104 B', 'venue' => 'East Rutherford', 'kickoff_at' => '2026-07-19 19:00:00', 'status' => 'closed', ], ]; public $audits = []; public function unresolvedMatchesByDate(string $scheduleDate): array { return array_values(array_filter($this->matches, function (array $match) use ($scheduleDate) { return substr($match['kickoff_at'], 0, 10) === $scheduleDate && $this->isUnresolved($match); })); } public function allMatches(): array { ksort($this->matches); return array_values($this->matches); } public function matchesByDate(string $scheduleDate): array { return array_values(array_filter($this->matches, function (array $match) use ($scheduleDate) { return substr($match['kickoff_at'], 0, 10) === $scheduleDate; })); } public function updateMatchByNoAndDate( int $matchNo, string $scheduleDate, array $attributes ): bool { if (!isset($this->matches[$matchNo])) { return false; } if (substr($this->matches[$matchNo]['kickoff_at'], 0, 10) !== $scheduleDate) { return false; } if (!$this->isUnresolved($this->matches[$matchNo])) { return false; } $this->matches[$matchNo] = array_merge($this->matches[$matchNo], $attributes); return true; } public function updateMatchByNo(int $matchNo, array $attributes): bool { if (!isset($this->matches[$matchNo])) { return false; } $this->matches[$matchNo] = array_merge($this->matches[$matchNo], $attributes); return true; } public function updateMatchById(int $matchId, array $attributes): bool { return $this->updateMatchByNo($matchId, $attributes); } public function writeScheduleAudit(string $actor, string $action, array $payload): void { $this->audits[] = compact('actor', 'action', 'payload'); } private function isUnresolved(array $match): bool { return strpos($match['home_team'], 'Winner ') === 0 || strpos($match['away_team'], 'Winner ') === 0; } }