WorldCupReviewServiceTest.php 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. <?php
  2. namespace Tests\Unit;
  3. use App\Services\WorldCup\Repositories\WorldCupReviewRepositoryInterface;
  4. use App\Services\WorldCup\WorldCupReviewService;
  5. use Tests\TestCase;
  6. class WorldCupReviewServiceTest extends TestCase
  7. {
  8. public function test_approve_sends_claim_mail_and_writes_audit()
  9. {
  10. $repository = new InMemoryWorldCupReviewRepository();
  11. $service = new WorldCupReviewService($repository);
  12. $result = $service->approve(1, 'admin-01');
  13. $this->assertTrue($result['success']);
  14. $this->assertSame('approved', $repository->rewards[1]['status']);
  15. $this->assertSame(6000, $repository->mails[1001][0]['amount']);
  16. $this->assertSame(6000, $repository->mails[1002][0]['amount']);
  17. $this->assertSame('Claim your $60 referral reward', $repository->mails[1001][0]['title']);
  18. $this->assertSame('Claim your $60 welcome reward', $repository->mails[1002][0]['title']);
  19. $this->assertStringContainsString('Reward: $60', $repository->mails[1001][0]['text']);
  20. $this->assertStringContainsString('first deposit of $120', $repository->mails[1002][0]['text']);
  21. $this->assertSame('approve', $repository->audits[0]['action']);
  22. }
  23. public function test_reject_requires_reason_and_writes_audit()
  24. {
  25. $repository = new InMemoryWorldCupReviewRepository();
  26. $service = new WorldCupReviewService($repository);
  27. $missingReason = $service->reject(1, 'admin-01', '');
  28. $result = $service->reject(1, 'admin-01', 'same_device');
  29. $this->assertFalse($missingReason['success']);
  30. $this->assertSame('Reason is required', $missingReason['message']);
  31. $this->assertTrue($result['success']);
  32. $this->assertSame('rejected', $repository->rewards[1]['status']);
  33. $this->assertSame(WorldCupReviewService::REJECT_REASON, $repository->rewards[1]['reason_code']);
  34. $this->assertSame('Your referral reward could not be approved', $repository->mails[1001][0]['title']);
  35. $this->assertSame('Welcome reward not approved', $repository->mails[1002][0]['title']);
  36. $this->assertStringContainsString(WorldCupReviewService::REJECT_REASON, $repository->mails[1001][0]['text']);
  37. $this->assertStringContainsString('Your welcome reward could not be approved.', $repository->mails[1002][0]['text']);
  38. $this->assertSame('reject', $repository->audits[0]['action']);
  39. }
  40. public function test_hold_moves_reviewing_reward_to_on_hold()
  41. {
  42. $repository = new InMemoryWorldCupReviewRepository();
  43. $service = new WorldCupReviewService($repository);
  44. $result = $service->hold(1, 'admin-01');
  45. $this->assertTrue($result['success']);
  46. $this->assertSame('on_hold', $repository->rewards[1]['status']);
  47. $this->assertSame('hold', $repository->audits[0]['action']);
  48. }
  49. public function test_clawback_only_allows_approved_reward()
  50. {
  51. $repository = new InMemoryWorldCupReviewRepository();
  52. $service = new WorldCupReviewService($repository);
  53. $notApproved = $service->clawback(1, 'admin-01', 'fraud', true);
  54. $approved = $service->clawback(3, 'admin-01', 'fraud', true);
  55. $this->assertFalse($notApproved['success']);
  56. $this->assertSame('Only approved rewards can be clawed back', $notApproved['message']);
  57. $this->assertTrue($approved['success']);
  58. $this->assertSame('clawed_back', $repository->rewards[3]['status']);
  59. $this->assertSame(1001, $repository->banned[0]);
  60. $this->assertSame(1004, $repository->banned[1]);
  61. $this->assertSame(10000, $repository->clawedBack[1001]);
  62. $this->assertSame(10000, $repository->clawedBack[1004]);
  63. }
  64. public function test_batch_approve_rejects_high_risk_rewards()
  65. {
  66. $repository = new InMemoryWorldCupReviewRepository();
  67. $service = new WorldCupReviewService($repository);
  68. $result = $service->batchApprove([1, 2], 'admin-01');
  69. $this->assertFalse($result['success']);
  70. $this->assertSame('High risk rewards must be reviewed one by one', $result['message']);
  71. $this->assertSame('reviewing', $repository->rewards[1]['status']);
  72. $this->assertSame('reviewing', $repository->rewards[2]['status']);
  73. }
  74. public function test_batch_reject_requires_reason()
  75. {
  76. $repository = new InMemoryWorldCupReviewRepository();
  77. $service = new WorldCupReviewService($repository);
  78. $missingReason = $service->batchReject([1, 2], 'admin-01', '');
  79. $result = $service->batchReject([1, 2], 'admin-01', 'bulk_signup');
  80. $this->assertFalse($missingReason['success']);
  81. $this->assertSame('Reason is required', $missingReason['message']);
  82. $this->assertTrue($result['success']);
  83. $this->assertSame('rejected', $repository->rewards[1]['status']);
  84. $this->assertSame('rejected', $repository->rewards[2]['status']);
  85. }
  86. }
  87. class InMemoryWorldCupReviewRepository implements WorldCupReviewRepositoryInterface
  88. {
  89. public $rewards = [
  90. 1 => [
  91. 'reward_id' => 1,
  92. 'referrer_id' => 1001,
  93. 'invitee_id' => 1002,
  94. 'first_deposit_amt' => 12000,
  95. 'reward_each' => 6000,
  96. 'total_liability' => 12000,
  97. 'risk_level' => 'low',
  98. 'status' => 'reviewing',
  99. ],
  100. 2 => [
  101. 'reward_id' => 2,
  102. 'referrer_id' => 1001,
  103. 'invitee_id' => 1003,
  104. 'first_deposit_amt' => 30000,
  105. 'reward_each' => 10000,
  106. 'total_liability' => 20000,
  107. 'risk_level' => 'high',
  108. 'status' => 'reviewing',
  109. ],
  110. 3 => [
  111. 'reward_id' => 3,
  112. 'referrer_id' => 1001,
  113. 'invitee_id' => 1004,
  114. 'first_deposit_amt' => 30000,
  115. 'reward_each' => 10000,
  116. 'total_liability' => 20000,
  117. 'risk_level' => 'low',
  118. 'status' => 'approved',
  119. ],
  120. ];
  121. public $mails = [];
  122. public $clawedBack = [];
  123. public $banned = [];
  124. public $audits = [];
  125. public function findReward(int $rewardId): ?array
  126. {
  127. return $this->rewards[$rewardId] ?? null;
  128. }
  129. public function findRewards(array $rewardIds): array
  130. {
  131. return array_values(array_filter($this->rewards, function (array $reward) use ($rewardIds) {
  132. return in_array((int)$reward['reward_id'], $rewardIds, true);
  133. }));
  134. }
  135. public function updateRewardStatus(
  136. int $rewardId,
  137. string $status,
  138. string $actor,
  139. string $reasonCode = null
  140. ): void {
  141. $this->rewards[$rewardId]['status'] = $status;
  142. $this->rewards[$rewardId]['review_by'] = $actor;
  143. $this->rewards[$rewardId]['reason_code'] = $reasonCode;
  144. }
  145. public function payReward(array $reward): void
  146. {
  147. $this->mails[$reward['referrer_id']][] = [
  148. 'title' => 'Claim your $60 referral reward',
  149. 'text' => 'Reward: $60',
  150. 'amount' => $reward['reward_each'],
  151. ];
  152. $this->mails[$reward['invitee_id']][] = [
  153. 'title' => 'Claim your $60 welcome reward',
  154. 'text' => 'Welcome! Your referral reward is ready to claim. '
  155. . 'You signed up with an invite and made your first deposit of $120. '
  156. . 'Reward: $60 (50% of your first deposit). '
  157. . 'Tap Claim to add it to your balance.',
  158. 'amount' => $reward['reward_each'],
  159. ];
  160. }
  161. public function sendRejectMail(array $reward, string $reason): void
  162. {
  163. $this->mails[$reward['referrer_id']][] = [
  164. 'title' => 'Your referral reward could not be approved',
  165. 'text' => 'Reason: ' . $reason,
  166. 'amount' => 0,
  167. ];
  168. $this->mails[$reward['invitee_id']][] = [
  169. 'title' => 'Welcome reward not approved',
  170. 'text' => 'Your welcome reward could not be approved.' . "\n"
  171. . 'Reason: ' . $reason . '.' . "\n"
  172. . 'If you think this is a mistake, contact support.',
  173. 'amount' => 0,
  174. ];
  175. }
  176. public function clawbackReward(array $reward, bool $banUsers): void
  177. {
  178. $this->clawedBack[$reward['referrer_id']] = ($this->clawedBack[$reward['referrer_id']] ?? 0) + $reward['reward_each'];
  179. $this->clawedBack[$reward['invitee_id']] = ($this->clawedBack[$reward['invitee_id']] ?? 0) + $reward['reward_each'];
  180. if ($banUsers) {
  181. $this->banned[] = $reward['referrer_id'];
  182. $this->banned[] = $reward['invitee_id'];
  183. }
  184. }
  185. public function writeAudit(
  186. int $rewardId,
  187. string $actor,
  188. string $action,
  189. ?string $reasonCode,
  190. ?string $beforeStatus,
  191. ?string $afterStatus,
  192. array $payload = []
  193. ): void {
  194. $this->audits[] = compact(
  195. 'rewardId',
  196. 'actor',
  197. 'action',
  198. 'reasonCode',
  199. 'beforeStatus',
  200. 'afterStatus',
  201. 'payload'
  202. );
  203. }
  204. public function queue(array $filters): array
  205. {
  206. return [];
  207. }
  208. public function kpi(): array
  209. {
  210. return [];
  211. }
  212. public function auditLogs($filters): array
  213. {
  214. return [];
  215. }
  216. }