WorldCupReferralRewardService.php 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. <?php
  2. namespace App\Services\WorldCup;
  3. use App\Services\WorldCup\Repositories\SqlWorldCupReferralRepository;
  4. use App\Services\WorldCup\Repositories\WorldCupReferralRepositoryInterface;
  5. use Log;
  6. class WorldCupReferralRewardService
  7. {
  8. private $repository;
  9. private $referralService;
  10. public function __construct(
  11. WorldCupReferralRepositoryInterface $repository = null,
  12. WorldCupReferralService $referralService = null
  13. ) {
  14. $this->repository = $repository ?: new SqlWorldCupReferralRepository();
  15. $this->referralService = $referralService ?: new WorldCupReferralService();
  16. }
  17. public function getInviteState(int $userId): array
  18. {
  19. $state = $this->repository->ensureUserState($userId);
  20. return [
  21. 'invite_code' => $state['invite_code'],
  22. 'referral_bound' => !empty($state['referred_by_user_id']),
  23. 'referred_by_user_id' => $state['referred_by_user_id'] ?? null,
  24. ];
  25. }
  26. public function bindInvite(int $inviteeId, string $inviteCode, string $bindType = 'manual'): array
  27. {
  28. $inviteCode = trim($inviteCode);
  29. if ($inviteCode === '') {
  30. return $this->fail('Invite code is required');
  31. }
  32. $this->repository->ensureUserState($inviteeId);
  33. $referrer = $this->repository->findUserByInviteCode($inviteCode);
  34. if (!$referrer) {
  35. return $this->fail('Invite code not found');
  36. }
  37. $referrerId = (int)$referrer['user_id'];
  38. if ($referrerId === $inviteeId) {
  39. return $this->fail('Cannot invite yourself');
  40. }
  41. $exists = $this->repository->findReferralByInvitee($inviteeId);
  42. if ($exists) {
  43. return [
  44. 'success' => true,
  45. 'status' => 'exists',
  46. 'data' => $exists,
  47. ];
  48. }
  49. $referral = $this->repository->bindReferral($referrerId, $inviteeId, $bindType);
  50. return [
  51. 'success' => true,
  52. 'status' => 'created',
  53. 'data' => $referral,
  54. ];
  55. }
  56. public function handleFirstDeposit(int $userId, float $payAmt, string $orderSn): array
  57. {
  58. if (!$this->repository->isFirstSuccessfulOrder($userId, $orderSn)) {
  59. Log::info('Order is not the first successful deposit', [
  60. 'user_id' => $userId,
  61. 'order_sn' => $orderSn,
  62. ]);
  63. return $this->ok('not_first_deposit');
  64. }
  65. $referral = $this->repository->findReferralByInvitee($userId);
  66. if (!$referral) {
  67. Log::info('No referral found for user', [
  68. 'user_id' => $userId,
  69. 'order_sn' => $orderSn,
  70. ]);
  71. return $this->ok('not_bound');
  72. }
  73. $exists = $this->repository->findRewardByInvitee($userId);
  74. if ($exists) {
  75. Log::info('Reward already exists for user', [
  76. 'user_id' => $userId,
  77. 'order_sn' => $orderSn,
  78. 'reward_id' => $exists['reward_id'],
  79. ]);
  80. return [
  81. 'success' => true,
  82. 'status' => 'exists',
  83. 'data' => $exists,
  84. ];
  85. }
  86. $firstDepositAmount = (int)round($payAmt * 100);
  87. $calculation = $this->referralService->calculateReward($firstDepositAmount);
  88. if (!$calculation['qualifies']) {
  89. Log::info('User is not qualified for reward', [
  90. 'user_id' => $userId,
  91. 'order_sn' => $orderSn,
  92. ]);
  93. return $this->ok('not_qualified');
  94. }
  95. $risk = $this->scoreRisk((int)$referral['referrer_id'], $userId);
  96. $reward = $this->repository->createReward([
  97. 'referrer_id' => (int)$referral['referrer_id'],
  98. 'invitee_id' => $userId,
  99. 'first_deposit_order_sn' => $orderSn,
  100. 'first_deposit_amt' => $firstDepositAmount,
  101. 'reward_each' => $calculation['reward_each'],
  102. 'total_liability' => $calculation['total_liability'],
  103. 'risk_score' => $risk['risk_score'],
  104. 'risk_level' => $risk['risk_level'],
  105. 'signals' => json_encode($risk['signals']),
  106. 'status' => 'reviewing',
  107. ]);
  108. return [
  109. 'success' => true,
  110. 'status' => 'created',
  111. 'data' => $reward,
  112. ];
  113. }
  114. public function generateMissingRewards(int $limit = 200): array
  115. {
  116. $orders = $this->repository->paidOrdersMissingRewards($limit);
  117. $result = [
  118. 'checked' => count($orders),
  119. 'created' => 0,
  120. 'skipped' => 0,
  121. ];
  122. foreach ($orders as $order) {
  123. $handled = $this->handleFirstDeposit(
  124. (int)$order['user_id'],
  125. ((float)$order['amount']) / 100,
  126. (string)$order['order_sn']
  127. );
  128. if (($handled['status'] ?? '') === 'created') {
  129. $result['created']++;
  130. } else {
  131. $result['skipped']++;
  132. }
  133. }
  134. return $result;
  135. }
  136. public function inviteLog(int $userId, string $type = 'invited', int $limit = 20): array
  137. {
  138. $type = $type === 'deposits' ? 'deposits' : 'invited';
  139. $limit = $this->normalizeLimit($limit);
  140. return [
  141. 'server_time' => time(),
  142. 'stats' => $this->repository->inviteStats($userId),
  143. 'type' => $type,
  144. 'list' => $this->repository->inviteLogs($userId, $type, $limit),
  145. ];
  146. }
  147. public function rewardLog(int $userId, int $limit = 20): array
  148. {
  149. $limit = $this->normalizeLimit($limit);
  150. return [
  151. 'stats' => $this->repository->rewardStats($userId),
  152. 'list' => $this->repository->rewardLogs($userId, $limit),
  153. ];
  154. }
  155. private function scoreRisk(int $referrerId, int $inviteeId): array
  156. {
  157. $referrer = $this->repository->findUserState($referrerId) ?: [];
  158. $invitee = $this->repository->findUserState($inviteeId) ?: [];
  159. $signals = [];
  160. $score = 0;
  161. if (!empty($referrer['device_fp'])
  162. && !empty($invitee['device_fp'])
  163. && $referrer['device_fp'] === $invitee['device_fp']) {
  164. $signals[] = 'same_device';
  165. $score += 40;
  166. }
  167. if (!empty($referrer['pay_account_hash'])
  168. && !empty($invitee['pay_account_hash'])
  169. && $referrer['pay_account_hash'] === $invitee['pay_account_hash']) {
  170. $signals[] = 'same_payment';
  171. $score += 40;
  172. }
  173. if (!empty($referrer['signup_ip'])
  174. && !empty($invitee['signup_ip'])
  175. && $referrer['signup_ip'] === $invitee['signup_ip']) {
  176. $signals[] = 'same_ip';
  177. $score += 20;
  178. }
  179. return [
  180. 'risk_score' => $score,
  181. 'risk_level' => $score >= 40 ? 'high' : ($score >= 20 ? 'medium' : 'low'),
  182. 'signals' => $signals,
  183. ];
  184. }
  185. private function normalizeLimit(int $limit): int
  186. {
  187. if ($limit <= 0) {
  188. return 20;
  189. }
  190. return min($limit, 100);
  191. }
  192. private function ok(string $status): array
  193. {
  194. return [
  195. 'success' => true,
  196. 'status' => $status,
  197. 'data' => [],
  198. ];
  199. }
  200. private function fail(string $message): array
  201. {
  202. return [
  203. 'success' => false,
  204. 'message' => $message,
  205. 'data' => [],
  206. ];
  207. }
  208. }