WorldCupOddsServiceTest.php 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. <?php
  2. namespace Tests\Unit;
  3. use App\Services\WorldCup\Repositories\WorldCupOddsRepositoryInterface;
  4. use App\Services\WorldCup\WorldCupOddsService;
  5. use Tests\TestCase;
  6. class WorldCupOddsServiceTest extends TestCase
  7. {
  8. public function test_save_odds_updates_previous_odds_and_active_state()
  9. {
  10. $repository = new InMemoryWorldCupOddsRepository();
  11. $service = new WorldCupOddsService($repository);
  12. $result = $service->saveOdds([
  13. 'market' => '1x2',
  14. 'match_id' => 10,
  15. 'selection' => 'home',
  16. 'decimal_odds' => 2.35,
  17. 'is_active' => 1,
  18. 'locked_weight' => 9,
  19. ]);
  20. $this->assertTrue($result['success']);
  21. $this->assertSame(2.15, $repository->odds[0]['previous_odds']);
  22. $this->assertSame(2.35, $repository->odds[0]['decimal_odds']);
  23. $this->assertSame(1, $repository->odds[0]['is_active']);
  24. $this->assertSame(9, $repository->odds[0]['locked_weight']);
  25. }
  26. public function test_save_winner_market_allows_null_match_id()
  27. {
  28. $repository = new InMemoryWorldCupOddsRepository();
  29. $service = new WorldCupOddsService($repository);
  30. $result = $service->saveOdds([
  31. 'market' => 'winner',
  32. 'match_id' => null,
  33. 'selection' => 'Brazil',
  34. 'decimal_odds' => 6.5,
  35. 'is_active' => 1,
  36. 'locked_weight' => 99,
  37. ]);
  38. $this->assertTrue($result['success']);
  39. $created = $repository->findOdds('winner', null, 'Brazil');
  40. $this->assertSame('winner', $created['market']);
  41. $this->assertNull($created['match_id']);
  42. }
  43. public function test_match_odds_only_returns_active_1x2_odds_grouped_by_match()
  44. {
  45. $repository = new InMemoryWorldCupOddsRepository();
  46. $service = new WorldCupOddsService($repository);
  47. $odds = $service->activeMatchOdds([10, 11]);
  48. $this->assertArrayHasKey(10, $odds);
  49. $this->assertCount(2, $odds[10]);
  50. $this->assertSame('home', $odds[10][0]['selection']);
  51. $this->assertArrayNotHasKey(11, $odds);
  52. }
  53. public function test_list_odds_includes_match_team_pair_for_admin_table()
  54. {
  55. $repository = new InMemoryWorldCupOddsRepository();
  56. $service = new WorldCupOddsService($repository);
  57. $odds = $service->listOdds();
  58. $this->assertSame('Brazil', $odds[0]['home_team']);
  59. $this->assertSame('Serbia', $odds[0]['away_team']);
  60. $this->assertSame('Brazil vs Serbia', $odds[0]['team_pair']);
  61. $this->assertSame('Brazil 胜', $odds[0]['selection_label']);
  62. $this->assertSame('平局', $odds[1]['selection_label']);
  63. $this->assertSame('-', $odds[3]['team_pair']);
  64. $this->assertSame('Brazil', $odds[3]['selection_label']);
  65. }
  66. public function test_validation_rejects_invalid_market_or_decimal_odds()
  67. {
  68. $service = new WorldCupOddsService(new InMemoryWorldCupOddsRepository());
  69. $invalidMarket = $service->saveOdds([
  70. 'market' => 'spread',
  71. 'match_id' => 10,
  72. 'selection' => 'home',
  73. 'decimal_odds' => 2.0,
  74. ]);
  75. $invalidOdds = $service->saveOdds([
  76. 'market' => '1x2',
  77. 'match_id' => 10,
  78. 'selection' => 'home',
  79. 'decimal_odds' => 1.0,
  80. ]);
  81. $this->assertFalse($invalidMarket['success']);
  82. $this->assertSame('Invalid market', $invalidMarket['message']);
  83. $this->assertFalse($invalidOdds['success']);
  84. $this->assertSame('Decimal odds must be greater than 1', $invalidOdds['message']);
  85. }
  86. public function test_imports_1x2_odds_from_csv_rows()
  87. {
  88. $repository = new InMemoryWorldCupOddsRepository();
  89. $service = new WorldCupOddsService($repository);
  90. $result = $service->importOddsRows('1x2', [
  91. [
  92. 'match_id' => '10',
  93. 'home_win' => '2.40',
  94. 'draw' => '3.25',
  95. 'away_win' => '3.10',
  96. ],
  97. [
  98. 'match_id' => '',
  99. 'home_win' => '2.00',
  100. 'draw' => '3.00',
  101. 'away_win' => '4.00',
  102. ],
  103. ]);
  104. $this->assertTrue($result['success']);
  105. $this->assertSame(3, $result['data']['updated']);
  106. $this->assertSame(0, $result['data']['skipped']);
  107. $this->assertCount(1, $result['data']['errors']);
  108. $this->assertSame(2.40, $repository->findOdds('1x2', 10, 'home')['decimal_odds']);
  109. $this->assertSame(3.25, $repository->findOdds('1x2', 10, 'draw')['decimal_odds']);
  110. $this->assertSame(3.10, $repository->findOdds('1x2', 10, 'away')['decimal_odds']);
  111. $this->assertSame(2.15, $repository->findOdds('1x2', 10, 'home')['previous_odds']);
  112. }
  113. public function test_imports_winner_odds_from_draftkings_decimal()
  114. {
  115. $repository = new InMemoryWorldCupOddsRepository();
  116. $service = new WorldCupOddsService($repository);
  117. $result = $service->importOddsRows('winner', [
  118. [
  119. 'team_name' => 'Brazil',
  120. 'draftkings_decimal' => '10.00',
  121. ],
  122. [
  123. 'team_name' => 'Argentina',
  124. 'draftkings_decimal' => '10.50',
  125. ],
  126. [
  127. 'team_name' => 'France',
  128. 'draftkings_decimal' => '',
  129. ],
  130. ]);
  131. $this->assertTrue($result['success']);
  132. $this->assertSame(2, $result['data']['updated']);
  133. $this->assertCount(1, $result['data']['errors']);
  134. $this->assertSame(10.00, $repository->findOdds('winner', null, 'Brazil')['decimal_odds']);
  135. $this->assertSame(10.50, $repository->findOdds('winner', null, 'Argentina')['decimal_odds']);
  136. $this->assertSame(6.5, $repository->findOdds('winner', null, 'Brazil')['previous_odds']);
  137. }
  138. }
  139. class InMemoryWorldCupOddsRepository implements WorldCupOddsRepositoryInterface
  140. {
  141. public $odds = [
  142. [
  143. 'odds_id' => 1,
  144. 'market' => '1x2',
  145. 'match_id' => 10,
  146. 'selection' => 'home',
  147. 'decimal_odds' => 2.15,
  148. 'previous_odds' => 2.05,
  149. 'is_active' => 1,
  150. 'locked_weight' => 1,
  151. 'home_team' => 'Brazil',
  152. 'away_team' => 'Serbia',
  153. ],
  154. [
  155. 'odds_id' => 2,
  156. 'market' => '1x2',
  157. 'match_id' => 10,
  158. 'selection' => 'draw',
  159. 'decimal_odds' => 3.2,
  160. 'previous_odds' => null,
  161. 'is_active' => 1,
  162. 'locked_weight' => 0,
  163. 'home_team' => 'Brazil',
  164. 'away_team' => 'Serbia',
  165. ],
  166. [
  167. 'odds_id' => 3,
  168. 'market' => '1x2',
  169. 'match_id' => 11,
  170. 'selection' => 'home',
  171. 'decimal_odds' => 1.8,
  172. 'previous_odds' => null,
  173. 'is_active' => 0,
  174. 'locked_weight' => 0,
  175. 'home_team' => 'Argentina',
  176. 'away_team' => 'France',
  177. ],
  178. [
  179. 'odds_id' => 4,
  180. 'market' => 'winner',
  181. 'match_id' => null,
  182. 'selection' => 'Brazil',
  183. 'decimal_odds' => 6.5,
  184. 'previous_odds' => null,
  185. 'is_active' => 1,
  186. 'locked_weight' => 99,
  187. 'home_team' => null,
  188. 'away_team' => null,
  189. ],
  190. ];
  191. public function allOdds(array $filters = []): array
  192. {
  193. return $this->odds;
  194. }
  195. public function activeMatchOdds(array $matchIds): array
  196. {
  197. return array_values(array_filter($this->odds, function (array $odds) use ($matchIds) {
  198. return $odds['market'] === '1x2'
  199. && in_array((int)$odds['match_id'], $matchIds, true)
  200. && (int)$odds['is_active'] === 1;
  201. }));
  202. }
  203. public function activeWinnerOdds(): array
  204. {
  205. return array_values(array_filter($this->odds, function (array $odds) {
  206. return $odds['market'] === 'winner' && (int)$odds['is_active'] === 1;
  207. }));
  208. }
  209. public function findOdds(string $market, ?int $matchId, string $selection): ?array
  210. {
  211. foreach ($this->odds as $odds) {
  212. if ($odds['market'] === $market
  213. && $odds['match_id'] === $matchId
  214. && $odds['selection'] === $selection) {
  215. return $odds;
  216. }
  217. }
  218. return null;
  219. }
  220. public function upsertOdds(array $odds): array
  221. {
  222. foreach ($this->odds as $index => $current) {
  223. if ($current['market'] === $odds['market']
  224. && $current['match_id'] === $odds['match_id']
  225. && $current['selection'] === $odds['selection']) {
  226. $odds['odds_id'] = $current['odds_id'];
  227. $this->odds[$index] = array_merge($current, $odds);
  228. return $this->odds[$index];
  229. }
  230. }
  231. $odds['odds_id'] = count($this->odds) + 1;
  232. $this->odds[] = $odds;
  233. return $odds;
  234. }
  235. }