>, * agent_edges: array, * agent_subtrees: array>, * platform_share_profit: int, * } */ public function aggregate(int $adminSiteId, string $periodStart, string $periodEnd): array { $siteCode = (string) DB::table('admin_sites')->where('id', $adminSiteId)->value('code'); $codeToId = AgentNode::query() ->where('admin_site_id', $adminSiteId) ->pluck('id', 'code') ->mapWithKeys(fn ($id, $code): array => [(string) $code => (int) $id]) ->all(); $rows = DB::table('share_ledger as sl') ->join('players as p', 'p.id', '=', 'sl.player_id') ->where('p.site_code', $siteCode) ->whereBetween('sl.settled_at', [$periodStart, $periodEnd]) ->select([ 'sl.player_id', 'sl.ticket_item_id', 'sl.agent_node_id', 'sl.share_snapshot', 'sl.game_win_loss', 'sl.basic_rebate', ]) ->orderBy('sl.id') ->get(); $players = []; $agentEdges = []; $agentSubtrees = []; $platformShareProfit = 0; foreach ($rows as $row) { $playerId = (int) $row->player_id; $snapshot = $this->resolveSnapshotFromLedgerRow($row); if ($snapshot === null) { $player = Player::query()->find($playerId); if ($player === null) { continue; } $built = $this->snapshotBuilder->buildForPlayer($player); $snapshot = [ 'total_shares' => $built['total_shares'], 'chain_codes' => $built['chain_codes'], ]; } $gameWinLoss = (int) $row->game_win_loss; $basicRebate = (int) $row->basic_rebate; $extraRebate = $this->extraRebateForTicketItem( (int) $row->ticket_item_id, $periodStart, $periodEnd, ); $extraByCode = []; if ($extraRebate > 0 && $snapshot['chain_codes'] !== []) { $extraByCode[$snapshot['chain_codes'][0]] = $extraRebate; } $result = $this->calculator->calculate( sharedNetWinLoss: 0, totalSharesByCode: $snapshot['total_shares'], extraRebateByCode: $extraByCode, gameWinLoss: $gameWinLoss, basicRebate: $basicRebate, chainFromPlayer: $snapshot['chain_codes'], ); $net = (int) round($result->playerNetSettlement); if (! isset($players[$playerId])) { $players[$playerId] = [ 'agent_node_id' => (int) $row->agent_node_id, 'game_win_loss' => 0, 'basic_rebate' => 0, 'extra_rebate' => 0, 'net_amount' => 0, ]; } $players[$playerId]['game_win_loss'] += $gameWinLoss; $players[$playerId]['basic_rebate'] += $basicRebate; $players[$playerId]['extra_rebate'] += $extraRebate; $players[$playerId]['net_amount'] += $net; foreach ($result->tierSettlements as $edge => $amount) { $agentEdges[$edge] = ($agentEdges[$edge] ?? 0) + (int) round($amount); } $platformShareProfit += (int) round($result->finalProfits['platform'] ?? 0); $pathIds = $this->resolveAgentPathIds($row, $snapshot, $codeToId); foreach ($pathIds as $agentId) { if (! isset($agentSubtrees[$agentId])) { $agentSubtrees[$agentId] = [ 'gross_win_loss' => 0, 'basic_rebate' => 0, 'extra_rebate' => 0, 'share_profit' => 0, 'player_count' => 0, '_players_seen' => [], ]; } $agentSubtrees[$agentId]['gross_win_loss'] += $gameWinLoss; $agentSubtrees[$agentId]['basic_rebate'] += $basicRebate; $agentSubtrees[$agentId]['extra_rebate'] += $extraRebate; if (! in_array($playerId, $agentSubtrees[$agentId]['_players_seen'], true)) { $agentSubtrees[$agentId]['_players_seen'][] = $playerId; $agentSubtrees[$agentId]['player_count']++; } } foreach ($snapshot['chain_codes'] as $code) { $agentId = $codeToId[$code] ?? 0; if ($agentId <= 0) { continue; } $profit = (int) round($result->finalProfits[$code] ?? 0); $agentSubtrees[$agentId]['share_profit'] = ($agentSubtrees[$agentId]['share_profit'] ?? 0) + $profit; } } foreach ($agentSubtrees as $id => $subtree) { unset($agentSubtrees[$id]['_players_seen']); } return [ 'players' => $players, 'agent_edges' => $agentEdges, 'agent_subtrees' => $agentSubtrees, 'platform_share_profit' => $platformShareProfit, ]; } /** * @param array{chain_codes: list, total_shares: array} $snapshot * @param array $codeToId * @return list */ private function resolveAgentPathIds(object $row, array $snapshot, array $codeToId): array { $raw = $row->share_snapshot ?? null; if ($raw !== null && $raw !== '') { $decoded = is_string($raw) ? json_decode($raw, true) : $raw; if (is_array($decoded) && is_array($decoded['agent_path'] ?? null)) { return array_values(array_map(intval(...), $decoded['agent_path'])); } } $ids = []; foreach ($snapshot['chain_codes'] as $code) { $id = $codeToId[$code] ?? 0; if ($id > 0) { $ids[] = $id; } } return $ids; } public function siteIdForPeriod(int $periodId): int { return (int) DB::table('settlement_periods')->where('id', $periodId)->value('admin_site_id'); } private function extraRebateForTicketItem(int $ticketItemId, string $periodStart, string $periodEnd): int { if ($ticketItemId <= 0) { return 0; } return (int) DB::table('rebate_records') ->where('ticket_item_id', $ticketItemId) ->where('rebate_type', 'extra') ->whereIn('status', ['accrued', 'reversed']) ->whereBetween('created_at', [$periodStart, $periodEnd]) ->sum('rebate_amount'); } /** * @return array{total_shares: array, chain_codes: list}|null */ private function resolveSnapshotFromLedgerRow(object $row): ?array { $raw = $row->share_snapshot ?? null; if ($raw === null || $raw === '') { return null; } $decoded = is_string($raw) ? json_decode($raw, true) : $raw; if (! is_array($decoded)) { return null; } $totalShares = $decoded['total_shares'] ?? null; $chainCodes = $decoded['chain_codes'] ?? null; if (! is_array($totalShares) || ! is_array($chainCodes) || $chainCodes === []) { return null; } $shares = []; foreach ($totalShares as $code => $rate) { $shares[(string) $code] = (float) $rate; } return [ 'total_shares' => $shares, 'chain_codes' => array_values(array_map(strval(...), $chainCodes)), ]; } }