RecalculateShareRebates.php 5.3 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283
  1. <?php
  2. namespace App\Console\Commands;
  3. use App\Services\Sharing\BetFactSynchronizer;
  4. use App\Services\Sharing\EffectiveBet;
  5. use Illuminate\Console\Command;
  6. use Illuminate\Support\Facades\DB;
  7. use Illuminate\Support\Facades\Schema;
  8. class RecalculateShareRebates extends Command
  9. {
  10. protected $signature = 'share:recalculate-rebates {--write : Correct unpaid PC28 daily bases; default previews all history}';
  11. protected $description = 'Recalculate unpaid PC28 rebate bases without paying any wallet or changing paid rows';
  12. public function handle(BetFactSynchronizer $synchronizer): int
  13. {
  14. $result = $this->recalculate($synchronizer, (bool) $this->option('write'));
  15. $this->line(json_encode($result, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR));
  16. return $result['skipped'] === 0 ? self::SUCCESS : self::FAILURE;
  17. }
  18. public function recalculate(BetFactSynchronizer $synchronizer, bool $write = false): array
  19. {
  20. $result = ['write' => $write, 'reviewed' => 0, 'changed' => 0, 'unchanged' => 0, 'skipped' => 0, 'paid_untouched' => 0, 'rows' => []];
  21. if (!Schema::hasTable('rebates') || !Schema::hasTable('lhc_order') || !Schema::hasTable('lhc_order_item')) {
  22. throw new \RuntimeException('Rebate and current lottery source tables are required');
  23. }
  24. $groups = [];
  25. $undatedMembers = [];
  26. DB::table('lhc_order')->whereIn('type', [5, 6])->orderBy('id')->chunkById(200, function ($orders) use ($synchronizer, &$groups, &$undatedMembers) {
  27. foreach ($orders as $order) {
  28. $member = (string) $order->member_id;
  29. try { $date = EffectiveBet::newYorkDate($order->created_at ?? null); }
  30. catch (\Throwable $e) { $undatedMembers[$member] = 'An original betting date is missing or invalid'; continue; }
  31. $key = $member . '\0' . $date;
  32. $groups[$key] ??= ['total' => EffectiveBet::zero(), 'effective' => EffectiveBet::zero(), 'errors' => []];
  33. try {
  34. $snapshot = $synchronizer->lotteryOrder($order);
  35. if ($snapshot['facts'] === []) throw new \RuntimeException('Source member is demo or unavailable');
  36. if ($snapshot['unsettled']) throw new \RuntimeException('Original betting day still has unfinished bets');
  37. $groups[$key]['total'] = bcadd($groups[$key]['total'], $snapshot['total_bet'], EffectiveBet::SCALE);
  38. foreach ($snapshot['facts'] as $fact) {
  39. $groups[$key]['effective'] = bcadd($groups[$key]['effective'], $fact['effective_amount'], EffectiveBet::SCALE);
  40. }
  41. } catch (\Throwable $e) { $groups[$key]['errors'][] = 'order ' . $order->id . ': ' . $e->getMessage(); }
  42. }
  43. });
  44. $result['paid_untouched'] = DB::table('rebates')->where('status', 1)->count();
  45. DB::table('rebates')->where('status', 0)->orderBy('id')->chunkById(200, function ($rebates) use ($write, $groups, $undatedMembers, &$result) {
  46. foreach ($rebates as $rebate) {
  47. $result['reviewed']++;
  48. $key = (string) $rebate->member_id . '\0' . (string) $rebate->date;
  49. $group = $groups[$key] ?? null;
  50. $reason = $undatedMembers[(string) $rebate->member_id] ?? null;
  51. if ($reason === null && $group === null) $reason = 'No original-date source; legacy date cannot be inferred';
  52. if ($reason === null && $group['errors'] !== []) $reason = implode('; ', $group['errors']);
  53. if ($reason === null && bccomp($group['total'], (string) $rebate->betting_amount, 2) !== 0) {
  54. $reason = 'Original-date stake total differs from stored betting_amount; date/refund history needs review';
  55. }
  56. $row = ['id' => $rebate->id, 'member_id' => (string) $rebate->member_id, 'date' => $rebate->date,
  57. 'before' => bcadd((string) $rebate->effective_betting_amount, '0', 2), 'after' => null, 'status' => 'skipped', 'reason' => $reason];
  58. if ($reason !== null) { $result['skipped']++; $result['rows'][] = $row; continue; }
  59. $row['after'] = bcadd($group['effective'], '0', 2);
  60. if (bccomp($row['before'], $row['after'], 2) === 0) {
  61. $row['status'] = 'unchanged'; $result['unchanged']++;
  62. } elseif (!$write) {
  63. $row['status'] = 'would_change'; $result['changed']++;
  64. } else {
  65. // Compare-and-set keeps a concurrent settlement/payment from being overwritten.
  66. $updated = DB::table('rebates')->where('id', $rebate->id)->where('status', 0)
  67. ->where('betting_amount', $rebate->betting_amount)
  68. ->where('effective_betting_amount', $rebate->effective_betting_amount)
  69. ->update(['effective_betting_amount' => $row['after']]);
  70. if ($updated === 1) { $row['status'] = 'changed'; $result['changed']++; }
  71. else { $row['status'] = 'skipped'; $row['reason'] = 'Rebate changed during recalculation; retry preview'; $result['skipped']++; }
  72. }
  73. $result['rows'][] = $row;
  74. }
  75. });
  76. return $result;
  77. }
  78. }