WorldCupSettlementServiceTest.php 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  1. <?php
  2. namespace Tests\Unit;
  3. use App\Services\WorldCup\Repositories\WorldCupSettlementRepositoryInterface;
  4. use App\Services\WorldCup\WorldCupSettlementService;
  5. use Tests\TestCase;
  6. class WorldCupSettlementServiceTest extends TestCase
  7. {
  8. public function test_settle_match_marks_won_and_lost_bets_and_sends_reward_mail()
  9. {
  10. $repository = new InMemoryWorldCupSettlementRepository();
  11. $service = new WorldCupSettlementService($repository);
  12. $result = $service->settleMatch(10, 'home', 'admin-01');
  13. $this->assertTrue($result['success']);
  14. $this->assertSame(1, $result['data']['won_count']);
  15. $this->assertSame(1, $result['data']['lost_count']);
  16. $this->assertSame('finished', $repository->matches[10]['status']);
  17. $this->assertSame('home', $repository->matches[10]['result']);
  18. $this->assertSame('won', $repository->bets[1]['status']);
  19. $this->assertSame('lost', $repository->bets[2]['status']);
  20. $this->assertSame(2650, $repository->mails[1001][0]['amount']);
  21. $this->assertSame('You won $26.50 · Brazil vs Serbia', $repository->mails[1001][0]['title']);
  22. $this->assertStringContainsString('Your bet: Brazil win · $10.00 · Odds 2.15', $repository->mails[1001][0]['text']);
  23. $this->assertSame(2650, $result['data']['paid_amount']);
  24. $this->assertSame('settle_match', $repository->audits[0]['action']);
  25. }
  26. public function test_settle_match_is_idempotent_after_finished()
  27. {
  28. $repository = new InMemoryWorldCupSettlementRepository();
  29. $service = new WorldCupSettlementService($repository);
  30. $first = $service->settleMatch(10, 'home', 'admin-01');
  31. $second = $service->settleMatch(10, 'home', 'admin-01');
  32. $this->assertSame(1, $first['data']['won_count']);
  33. $this->assertFalse($second['success']);
  34. $this->assertSame('Match already settled', $second['message']);
  35. $this->assertCount(1, $repository->mails[1001]);
  36. $this->assertCount(1, $repository->audits);
  37. }
  38. public function test_settle_match_does_not_override_finished_result()
  39. {
  40. $repository = new InMemoryWorldCupSettlementRepository();
  41. $service = new WorldCupSettlementService($repository);
  42. $service->settleMatch(10, 'home', 'admin-01');
  43. $result = $service->settleMatch(10, 'away', 'admin-01');
  44. $this->assertFalse($result['success']);
  45. $this->assertSame('Match already settled', $result['message']);
  46. $this->assertSame('home', $repository->matches[10]['result']);
  47. $this->assertSame('won', $repository->bets[1]['status']);
  48. $this->assertSame('lost', $repository->bets[2]['status']);
  49. }
  50. public function test_settle_match_rejects_invalid_result()
  51. {
  52. $service = new WorldCupSettlementService(new InMemoryWorldCupSettlementRepository());
  53. $result = $service->settleMatch(10, 'Brazil', 'admin-01');
  54. $this->assertFalse($result['success']);
  55. $this->assertSame('Invalid match result', $result['message']);
  56. }
  57. public function test_settle_match_rejects_draw_outside_group_stage()
  58. {
  59. $repository = new InMemoryWorldCupSettlementRepository();
  60. $service = new WorldCupSettlementService($repository);
  61. $result = $service->settleMatch(11, 'draw', 'admin-01');
  62. $this->assertFalse($result['success']);
  63. $this->assertSame('Draw is only available for group stage', $result['message']);
  64. $this->assertSame('scheduled', $repository->matches[11]['status']);
  65. $this->assertNull($repository->matches[11]['result']);
  66. }
  67. public function test_settle_winner_market_pays_selected_team()
  68. {
  69. $repository = new InMemoryWorldCupSettlementRepository();
  70. $service = new WorldCupSettlementService($repository);
  71. $result = $service->settleWinner('Brazil', 'admin-01');
  72. $this->assertTrue($result['success']);
  73. $this->assertSame('won', $repository->bets[3]['status']);
  74. $this->assertSame('lost', $repository->bets[4]['status']);
  75. $this->assertSame(6500, $repository->mails[1003][0]['amount']);
  76. $this->assertSame('You won $65.00 · World Cup 2026 Winner', $repository->mails[1003][0]['title']);
  77. }
  78. }
  79. class InMemoryWorldCupSettlementRepository implements WorldCupSettlementRepositoryInterface
  80. {
  81. public $matches = [
  82. 10 => [
  83. 'match_id' => 10,
  84. 'stage' => 'group',
  85. 'home_team' => 'Brazil',
  86. 'away_team' => 'Serbia',
  87. 'status' => 'scheduled',
  88. 'result' => null,
  89. ],
  90. 11 => [
  91. 'match_id' => 11,
  92. 'stage' => 'round_16',
  93. 'home_team' => 'Argentina',
  94. 'away_team' => 'France',
  95. 'status' => 'scheduled',
  96. 'result' => null,
  97. ],
  98. ];
  99. public $bets = [
  100. 1 => [
  101. 'bet_id' => 1,
  102. 'user_id' => 1001,
  103. 'market' => '1x2',
  104. 'match_id' => 10,
  105. 'selection' => 'home',
  106. 'stake' => 1000,
  107. 'odds' => 2.15,
  108. 'status' => 'pending',
  109. 'potential_payout' => 2650,
  110. ],
  111. 2 => [
  112. 'bet_id' => 2,
  113. 'user_id' => 1002,
  114. 'market' => '1x2',
  115. 'match_id' => 10,
  116. 'selection' => 'away',
  117. 'stake' => 1000,
  118. 'odds' => 1.95,
  119. 'status' => 'pending',
  120. 'potential_payout' => 1950,
  121. ],
  122. 3 => [
  123. 'bet_id' => 3,
  124. 'user_id' => 1003,
  125. 'market' => 'winner',
  126. 'match_id' => null,
  127. 'selection' => 'Brazil',
  128. 'stake' => 1000,
  129. 'odds' => 6.5,
  130. 'status' => 'pending',
  131. 'potential_payout' => 6500,
  132. ],
  133. 4 => [
  134. 'bet_id' => 4,
  135. 'user_id' => 1004,
  136. 'market' => 'winner',
  137. 'match_id' => null,
  138. 'selection' => 'Argentina',
  139. 'stake' => 1000,
  140. 'odds' => 6.0,
  141. 'status' => 'pending',
  142. 'potential_payout' => 6000,
  143. ],
  144. ];
  145. public $mails = [];
  146. public $audits = [];
  147. public function findMatch(int $matchId): ?array
  148. {
  149. return $this->matches[$matchId] ?? null;
  150. }
  151. public function markMatchFinished(int $matchId, string $result): void
  152. {
  153. $this->matches[$matchId]['status'] = 'finished';
  154. $this->matches[$matchId]['result'] = $result;
  155. }
  156. public function pendingBetsForMatch(int $matchId): array
  157. {
  158. return array_values(array_map(function (array $bet) use ($matchId) {
  159. return array_merge($this->matches[$matchId], $bet);
  160. }, array_filter($this->bets, function (array $bet) use ($matchId) {
  161. return $bet['market'] === '1x2'
  162. && (int)$bet['match_id'] === $matchId
  163. && $bet['status'] === 'pending';
  164. })));
  165. }
  166. public function pendingWinnerBets(): array
  167. {
  168. return array_values(array_filter($this->bets, function (array $bet) {
  169. return $bet['market'] === 'winner' && $bet['status'] === 'pending';
  170. }));
  171. }
  172. public function markBetSettled(int $betId, string $status): void
  173. {
  174. $this->bets[$betId]['status'] = $status;
  175. }
  176. public function payBet(array $bet): int
  177. {
  178. $odds = (float)$bet['odds'];
  179. $payout = (int)$bet['potential_payout'];
  180. $title = $bet['market'] === 'winner'
  181. ? 'You won $65.00 · World Cup 2026 Winner'
  182. : 'You won $26.50 · Brazil vs Serbia';
  183. $text = $bet['market'] === 'winner'
  184. ? 'Your bet: Brazil to win · $10.00 · Odds ' . number_format($odds, 2)
  185. : 'Your bet: Brazil win · $10.00 · Odds ' . number_format($odds, 2);
  186. $this->mails[$bet['user_id']][] = [
  187. 'title' => $title,
  188. 'text' => $text,
  189. 'amount' => $payout,
  190. ];
  191. return $payout;
  192. }
  193. public function writeAudit(string $actor, string $action, array $payload): void
  194. {
  195. $this->audits[] = compact('actor', 'action', 'payload');
  196. }
  197. }