saveOdds([ 'market' => '1x2', 'match_id' => 10, 'selection' => 'home', 'decimal_odds' => 2.35, 'is_active' => 1, 'locked_weight' => 9, ]); $this->assertTrue($result['success']); $this->assertSame(2.15, $repository->odds[0]['previous_odds']); $this->assertSame(2.35, $repository->odds[0]['decimal_odds']); $this->assertSame(1, $repository->odds[0]['is_active']); $this->assertSame(9, $repository->odds[0]['locked_weight']); } public function test_save_winner_market_allows_null_match_id() { $repository = new InMemoryWorldCupOddsRepository(); $service = new WorldCupOddsService($repository); $result = $service->saveOdds([ 'market' => 'winner', 'match_id' => null, 'selection' => 'Brazil', 'decimal_odds' => 6.5, 'is_active' => 1, 'locked_weight' => 99, ]); $this->assertTrue($result['success']); $created = $repository->findOdds('winner', null, 'Brazil'); $this->assertSame('winner', $created['market']); $this->assertNull($created['match_id']); } public function test_match_odds_only_returns_active_1x2_odds_grouped_by_match() { $repository = new InMemoryWorldCupOddsRepository(); $service = new WorldCupOddsService($repository); $odds = $service->activeMatchOdds([10, 11]); $this->assertArrayHasKey(10, $odds); $this->assertCount(2, $odds[10]); $this->assertSame('home', $odds[10][0]['selection']); $this->assertArrayNotHasKey(11, $odds); } public function test_list_odds_includes_match_team_pair_for_admin_table() { $repository = new InMemoryWorldCupOddsRepository(); $service = new WorldCupOddsService($repository); $odds = $service->listOdds(); $this->assertSame('Brazil', $odds[0]['home_team']); $this->assertSame('Serbia', $odds[0]['away_team']); $this->assertSame('Brazil vs Serbia', $odds[0]['team_pair']); $this->assertSame('Brazil 胜', $odds[0]['selection_label']); $this->assertSame('平局', $odds[1]['selection_label']); $this->assertSame('-', $odds[3]['team_pair']); $this->assertSame('Brazil', $odds[3]['selection_label']); } public function test_validation_rejects_invalid_market_or_decimal_odds() { $service = new WorldCupOddsService(new InMemoryWorldCupOddsRepository()); $invalidMarket = $service->saveOdds([ 'market' => 'spread', 'match_id' => 10, 'selection' => 'home', 'decimal_odds' => 2.0, ]); $invalidOdds = $service->saveOdds([ 'market' => '1x2', 'match_id' => 10, 'selection' => 'home', 'decimal_odds' => 1.0, ]); $this->assertFalse($invalidMarket['success']); $this->assertSame('Invalid market', $invalidMarket['message']); $this->assertFalse($invalidOdds['success']); $this->assertSame('Decimal odds must be greater than 1', $invalidOdds['message']); } public function test_imports_1x2_odds_from_csv_rows() { $repository = new InMemoryWorldCupOddsRepository(); $service = new WorldCupOddsService($repository); $result = $service->importOddsRows('1x2', [ [ 'match_id' => '10', 'home_win' => '2.40', 'draw' => '3.25', 'away_win' => '3.10', ], [ 'match_id' => '', 'home_win' => '2.00', 'draw' => '3.00', 'away_win' => '4.00', ], ]); $this->assertTrue($result['success']); $this->assertSame(3, $result['data']['updated']); $this->assertSame(0, $result['data']['skipped']); $this->assertCount(1, $result['data']['errors']); $this->assertSame(2.40, $repository->findOdds('1x2', 10, 'home')['decimal_odds']); $this->assertSame(3.25, $repository->findOdds('1x2', 10, 'draw')['decimal_odds']); $this->assertSame(3.10, $repository->findOdds('1x2', 10, 'away')['decimal_odds']); $this->assertSame(2.15, $repository->findOdds('1x2', 10, 'home')['previous_odds']); } public function test_imports_winner_odds_from_draftkings_decimal() { $repository = new InMemoryWorldCupOddsRepository(); $service = new WorldCupOddsService($repository); $result = $service->importOddsRows('winner', [ [ 'team_name' => 'Brazil', 'draftkings_decimal' => '10.00', ], [ 'team_name' => 'Argentina', 'draftkings_decimal' => '10.50', ], [ 'team_name' => 'France', 'draftkings_decimal' => '', ], ]); $this->assertTrue($result['success']); $this->assertSame(2, $result['data']['updated']); $this->assertCount(1, $result['data']['errors']); $this->assertSame(10.00, $repository->findOdds('winner', null, 'Brazil')['decimal_odds']); $this->assertSame(10.50, $repository->findOdds('winner', null, 'Argentina')['decimal_odds']); $this->assertSame(6.5, $repository->findOdds('winner', null, 'Brazil')['previous_odds']); } } class InMemoryWorldCupOddsRepository implements WorldCupOddsRepositoryInterface { public $odds = [ [ 'odds_id' => 1, 'market' => '1x2', 'match_id' => 10, 'selection' => 'home', 'decimal_odds' => 2.15, 'previous_odds' => 2.05, 'is_active' => 1, 'locked_weight' => 1, 'home_team' => 'Brazil', 'away_team' => 'Serbia', ], [ 'odds_id' => 2, 'market' => '1x2', 'match_id' => 10, 'selection' => 'draw', 'decimal_odds' => 3.2, 'previous_odds' => null, 'is_active' => 1, 'locked_weight' => 0, 'home_team' => 'Brazil', 'away_team' => 'Serbia', ], [ 'odds_id' => 3, 'market' => '1x2', 'match_id' => 11, 'selection' => 'home', 'decimal_odds' => 1.8, 'previous_odds' => null, 'is_active' => 0, 'locked_weight' => 0, 'home_team' => 'Argentina', 'away_team' => 'France', ], [ 'odds_id' => 4, 'market' => 'winner', 'match_id' => null, 'selection' => 'Brazil', 'decimal_odds' => 6.5, 'previous_odds' => null, 'is_active' => 1, 'locked_weight' => 99, 'home_team' => null, 'away_team' => null, ], ]; public function allOdds(array $filters = []): array { return $this->odds; } public function activeMatchOdds(array $matchIds): array { return array_values(array_filter($this->odds, function (array $odds) use ($matchIds) { return $odds['market'] === '1x2' && in_array((int)$odds['match_id'], $matchIds, true) && (int)$odds['is_active'] === 1; })); } public function activeWinnerOdds(): array { return array_values(array_filter($this->odds, function (array $odds) { return $odds['market'] === 'winner' && (int)$odds['is_active'] === 1; })); } public function findOdds(string $market, ?int $matchId, string $selection): ?array { foreach ($this->odds as $odds) { if ($odds['market'] === $market && $odds['match_id'] === $matchId && $odds['selection'] === $selection) { return $odds; } } return null; } public function upsertOdds(array $odds): array { foreach ($this->odds as $index => $current) { if ($current['market'] === $odds['market'] && $current['match_id'] === $odds['match_id'] && $current['selection'] === $odds['selection']) { $odds['odds_id'] = $current['odds_id']; $this->odds[$index] = array_merge($current, $odds); return $this->odds[$index]; } } $odds['odds_id'] = count($this->odds) + 1; $this->odds[] = $odds; return $odds; } }