>, * total: int, * page: int, * per_page: int, * ledger_source: string, * } */ public function listUnified( AdminUser $admin, string $siteCode, int $page, int $perPage, SettlementLedgerListFilters $filters = new SettlementLedgerListFilters, ): array { $periodId = $filters->settlementPeriodId; $range = $this->resolveCreatedRange($periodId, $filters->createdFrom, $filters->createdTo); $settledRange = $this->resolveSettledRange($periodId, $filters->createdFrom, $filters->createdTo); $playerBills = $this->playerBillsMap($admin, $siteCode, $periodId); $stubQueries = []; if ($this->shouldIncludeLedgerStub($filters, 'credit')) { $creditStub = $this->creditStubQuery($admin, $siteCode, $range, $filters); if ($creditStub !== null) { $stubQueries[] = $creditStub; } } if ($this->shouldIncludeLedgerStub($filters, 'payment')) { $paymentStub = $this->paymentStubQuery($admin, $siteCode, $periodId, $filters); if ($paymentStub !== null) { $stubQueries[] = $paymentStub; } } if ($this->shouldIncludeLedgerStub($filters, 'adjustment')) { $adjustmentStub = $this->adjustmentStubQuery($admin, $siteCode, $periodId, $filters); if ($adjustmentStub !== null) { $stubQueries[] = $adjustmentStub; } } if ($this->shouldIncludeLedgerStub($filters, 'share')) { $shareStub = $this->shareStubQuery($admin, $siteCode, $settledRange, $filters); if ($shareStub !== null) { $stubQueries[] = $shareStub; } } if ($stubQueries === []) { return [ 'items' => [], 'total' => 0, 'page' => $page, 'per_page' => $perPage, 'ledger_source' => 'settlement_ledger', ]; } $offset = max(0, ($page - 1) * $perPage); if (count($stubQueries) === 1) { $base = $stubQueries[0]; $total = (int) (clone $base)->count(); $stubs = (clone $base) ->orderByDesc('sort_at') ->orderByDesc('entry_id') ->offset($offset) ->limit($perPage) ->get(); } else { $union = null; foreach ($stubQueries as $stubQuery) { $union = $union === null ? $stubQuery : $union->unionAll($stubQuery); } $wrapped = DB::query()->fromSub($union, 'ledger_page'); $total = (int) (clone $wrapped)->count(); $stubs = $wrapped ->orderByDesc('sort_at') ->orderByDesc('entry_id') ->offset($offset) ->limit($perPage) ->get(); } $items = $this->hydrateLedgerStubs($admin, $siteCode, $stubs, $playerBills); $items = $this->applyFilters($items, $filters); $filteredCount = count($items); if ($filteredCount !== count($stubs)) { $total = $filteredCount; } return [ 'items' => array_values($items), 'total' => $total, 'page' => $page, 'per_page' => $perPage, 'ledger_source' => 'settlement_ledger', ]; } /** * 下注流水简化展示:下注占用 + 按注单合并的开奖结算。 * * @return array{ * items: list>, * total: int, * page: int, * per_page: int, * ledger_source: string, * } */ public function listBetFlowSimplified( AdminUser $admin, string $siteCode, int $page, int $perPage, SettlementLedgerListFilters $filters = new SettlementLedgerListFilters, ): array { $periodId = $filters->settlementPeriodId; $range = $this->resolveCreatedRange($periodId, $filters->createdFrom, $filters->createdTo); $rows = $this->fetchBetFlowCreditRows($admin, $siteCode, $range, $filters); $playerBills = $this->playerBillsMap($admin, $siteCode, $periodId); $ticketIds = []; foreach ($rows as $row) { if ((string) ($row->ref_type ?? '') === 'ticket_item') { $ticketId = (int) ($row->ref_id ?? 0); if ($ticketId > 0) { $ticketIds[] = $ticketId; } } } $ticketRefs = $this->partyEnrichment->loadTicketRefs(array_values(array_unique($ticketIds))); $items = $this->betFlowPresenter->simplifyCreditRows( $rows, $ticketRefs, function (object $row) use ($playerBills, $ticketRefs): array { $pid = (int) ($row->player_id ?? 0); return $this->formatCreditEntry($row, $playerBills[$pid] ?? null, $ticketRefs); }, function (object $row) use ($playerBills, $ticketRefs): array { $pid = (int) ($row->player_id ?? 0); $formatted = $this->formatCreditEntry($row, $playerBills[$pid] ?? null, $ticketRefs); $ticketId = (int) ($row->ref_id ?? 0); if ($ticketId > 0) { $formatted['txn_no'] = 'CLS-T'.$ticketId; $formatted['row_key'] = 'settlement-'.$ticketId; } return $formatted; }, ); if ($filters->bizType === CreditLedgerBetFlowPresenter::DISPLAY_BET_HOLD || $filters->bizType === CreditLedgerBetFlowPresenter::DISPLAY_GAME_SETTLEMENT) { $items = array_values(array_filter( $items, static fn (array $item): bool => ($item['biz_type'] ?? '') === $filters->bizType, )); } $total = count($items); $offset = max(0, ($page - 1) * $perPage); $pageItems = array_slice($items, $offset, $perPage); return [ 'items' => $pageItems, 'total' => $total, 'page' => $page, 'per_page' => $perPage, 'ledger_source' => 'credit_ledger', ]; } private function shouldIncludeLedgerStub(SettlementLedgerListFilters $filters, string $kind): bool { if (! $this->includeEntryKind($filters, $kind)) { return false; } if ($filters->bizType === null || $filters->bizType === '') { return true; } return match ($kind) { 'credit' => in_array($filters->bizType, self::CREDIT_BIZ_TYPES, true), 'payment' => $filters->bizType === 'payment_record', 'adjustment' => in_array($filters->bizType, self::ADJUSTMENT_BIZ_TYPES, true), 'share' => $filters->bizType === self::SHARE_BIZ_TYPE || ($filters->entryKind === 'share' && ($filters->bizType === null || $filters->bizType === '')), default => false, }; } /** * @param array{0: Carbon, 1: Carbon}|null $range */ private function shareStubQuery( AdminUser $admin, string $siteCode, ?array $range, SettlementLedgerListFilters $filters, ): ?\Illuminate\Database\Query\Builder { $query = DB::table('share_ledger as sl') ->join('players as p', 'p.id', '=', 'sl.player_id') ->where('p.site_code', $siteCode) ->whereNull('sl.reversal_of_id') ->selectRaw("'share' as entry_kind, sl.id as entry_id, sl.settled_at as sort_at"); AdminAgentSettlementScope::applyDirectPlayersToAlias($query, $admin, 'p'); $this->applyLedgerPlayerFilters($query, 'p', $filters); if ($range !== null) { $query->whereBetween('sl.settled_at', $range); } $this->applyTxnNoStubFilter($query, 'sl.id', 'SL', $filters->txnNo); return $query; } /** * @param array{0: Carbon, 1: Carbon}|null $range */ private function creditStubQuery( AdminUser $admin, string $siteCode, ?array $range, SettlementLedgerListFilters $filters, ): ?\Illuminate\Database\Query\Builder { $query = DB::table('credit_ledger as cl') ->join('players as p', function ($join): void { $join->on('p.id', '=', 'cl.owner_id') ->where('cl.owner_type', '=', 'player'); }) ->where('p.site_code', $siteCode) ->where('p.funding_mode', PlayerFundingMode::CREDIT) ->selectRaw("'credit' as entry_kind, cl.id as entry_id, cl.created_at as sort_at"); AdminAgentSettlementScope::applyDirectPlayersToAlias($query, $admin, 'p'); $this->applyLedgerPlayerFilters($query, 'p', $filters); if ($range !== null) { $query->whereBetween('cl.created_at', $range); } if ($filters->bizType !== null && $filters->bizType !== '') { $query->where('cl.reason', $filters->bizType); } elseif ($filters->betFlowOnly) { $query->whereIn('cl.reason', [ 'bet_hold', 'bet_hold_release', 'game_settlement_loss', ]); } $this->applyTxnNoStubFilter($query, 'cl.id', 'CL', $filters->txnNo); return $query; } private function paymentStubQuery( AdminUser $admin, string $siteCode, ?int $periodId, SettlementLedgerListFilters $filters, ): ?\Illuminate\Database\Query\Builder { $adminSiteId = (int) DB::table('admin_sites')->where('code', $siteCode)->value('id'); if ($adminSiteId <= 0) { return null; } $query = DB::table('payment_records as pr') ->join('settlement_bills as sb', 'sb.id', '=', 'pr.settlement_bill_id') ->join('settlement_periods as sp', 'sp.id', '=', 'sb.settlement_period_id') ->where('sp.admin_site_id', $adminSiteId) ->leftJoin('players as p', function ($join): void { $join->on('p.id', '=', 'sb.owner_id') ->where('sb.owner_type', '=', 'player'); }) ->selectRaw("'payment' as entry_kind, pr.id as entry_id, pr.created_at as sort_at"); if ($periodId !== null && $periodId > 0) { $query->where('sb.settlement_period_id', $periodId); } $this->applyLedgerSiteScope($query, $admin, 'sp'); AdminAgentSettlementScope::applySubtreeToBillsQuery($query, $admin, 'sb'); $this->applyPaymentPlayerFilters($query, $filters); if ($filters->billStatus !== null && $filters->billStatus !== '') { $query->where('sb.status', $filters->billStatus); } $this->applyTxnNoStubFilter($query, 'pr.id', 'PAY', $filters->txnNo); return $query; } private function applyPaymentPlayerFilters( \Illuminate\Database\Query\Builder $query, SettlementLedgerListFilters $filters, ): void { if ($filters->playerId !== null && $filters->playerId > 0) { $query->where('sb.owner_type', 'player') ->where('sb.owner_id', $filters->playerId); } if ($filters->playerAccount !== null && $filters->playerAccount !== '') { $query->where('sb.owner_type', 'player'); $this->applyLedgerPlayerFilters($query, 'p', $filters); } } private function adjustmentStubQuery( AdminUser $admin, string $siteCode, ?int $periodId, SettlementLedgerListFilters $filters, ): ?\Illuminate\Database\Query\Builder { $adminSiteId = (int) DB::table('admin_sites')->where('code', $siteCode)->value('id'); if ($adminSiteId <= 0) { return null; } $query = DB::table('settlement_adjustments as sa') ->join('settlement_periods as sp', 'sp.id', '=', 'sa.settlement_period_id') ->where('sp.admin_site_id', $adminSiteId) ->leftJoin('settlement_bills as sb', 'sb.id', '=', 'sa.original_bill_id') ->leftJoin('players as p', function ($join): void { $join->on('p.id', '=', 'sb.owner_id') ->where('sb.owner_type', '=', 'player'); }) ->selectRaw("'adjustment' as entry_kind, sa.id as entry_id, sa.created_at as sort_at"); if ($periodId !== null && $periodId > 0) { $query->where('sa.settlement_period_id', $periodId); } if ($filters->badDebtOnly) { $query->where('sa.adjustment_type', 'bad_debt'); } elseif ($filters->entryKind === 'adjustment') { $query->where('sa.adjustment_type', '!=', 'bad_debt'); } $this->applyLedgerSiteScope($query, $admin, 'sp'); $this->applyAdjustmentPlayerScope($query, $admin, $siteCode, $filters); if ($filters->billStatus !== null && $filters->billStatus !== '') { $query->where('sb.status', $filters->billStatus); } if ($filters->bizType !== null && $filters->bizType !== '') { $query->where('sa.adjustment_type', $filters->bizType); } $this->applyTxnNoStubFilter($query, 'sa.id', 'ADJ', $filters->txnNo); return $query; } private function applyAdjustmentPlayerScope( \Illuminate\Database\Query\Builder $query, AdminUser $admin, string $siteCode, SettlementLedgerListFilters $filters, ): void { $query->where(function (\Illuminate\Database\Query\Builder $outer) use ($admin, $siteCode, $filters): void { $outer->whereNull('p.id') ->orWhere(function (\Illuminate\Database\Query\Builder $scoped) use ($admin, $siteCode, $filters): void { $scoped->where('p.site_code', $siteCode); AdminAgentSettlementScope::applyDirectPlayersToAlias($scoped, $admin, 'p'); $this->applyLedgerPlayerFilters($scoped, 'p', $filters); }); }); } private function applyTxnNoStubFilter( \Illuminate\Database\Query\Builder $query, string $idColumn, string $prefix, ?string $txnNo, ): void { if ($txnNo === null || $txnNo === '') { return; } $needle = strtolower(trim($txnNo)); $query->where(function (\Illuminate\Database\Query\Builder $match) use ($idColumn, $prefix, $needle): void { if (ctype_digit($needle)) { $match->where($idColumn, (int) $needle); } $match->orWhereRaw( 'LOWER(CONCAT(?, \'-\', '.$idColumn.')) LIKE ?', [$prefix, '%'.$needle.'%'], ); }); } private function applyLedgerSiteScope(\Illuminate\Database\Query\Builder $query, AdminUser $admin, string $periodsAlias): void { $siteIds = $admin->accessibleAdminSiteIds(); if ($siteIds === null) { return; } if ($siteIds === []) { $query->whereRaw('0 = 1'); return; } $query->whereIn($periodsAlias.'.admin_site_id', $siteIds); } private function applyLedgerPlayerFilters( \Illuminate\Database\Query\Builder $query, string $playerAlias, SettlementLedgerListFilters $filters, ): void { if ($filters->playerId !== null && $filters->playerId > 0) { $query->where("{$playerAlias}.id", $filters->playerId); } if ($filters->playerAccount !== null && $filters->playerAccount !== '') { $like = '%'.addcslashes($filters->playerAccount, '%_\\').'%'; $query->where(function (\Illuminate\Database\Query\Builder $match) use ($playerAlias, $like): void { $match->where("{$playerAlias}.username", 'like', $like) ->orWhere("{$playerAlias}.site_player_id", 'like', $like) ->orWhere("{$playerAlias}.nickname", 'like', $like); }); } } /** * @param \Illuminate\Support\Collection $stubs * @param array $playerBills * @return list> */ private function hydrateLedgerStubs( AdminUser $admin, string $siteCode, \Illuminate\Support\Collection $stubs, array $playerBills, ): array { if ($stubs->isEmpty()) { return []; } $creditIds = []; $paymentIds = []; $adjustmentIds = []; $shareIds = []; foreach ($stubs as $stub) { $kind = (string) $stub->entry_kind; $id = (int) $stub->entry_id; if ($kind === 'credit') { $creditIds[] = $id; } elseif ($kind === 'payment') { $paymentIds[] = $id; } elseif ($kind === 'adjustment') { $adjustmentIds[] = $id; } elseif ($kind === 'share') { $shareIds[] = $id; } } $creditById = []; if ($creditIds !== []) { foreach ($this->fetchCreditRowsByIds($admin, $siteCode, $creditIds) as $row) { $creditById[(int) $row->id] = $row; } } $ticketRefs = $this->partyEnrichment->loadTicketRefs( array_values(array_filter(array_map( static fn (object $row): int => (string) ($row->ref_type ?? '') === 'ticket_item' ? (int) ($row->ref_id ?? 0) : 0, array_values($creditById), ), static fn (int $id): bool => $id > 0)), ); $creditBillRefs = $this->settlementBillsByIds( $admin, array_values(array_filter(array_map( static fn (object $row): int => (string) ($row->ref_type ?? '') === 'settlement_bill' ? (int) ($row->ref_id ?? 0) : 0, array_values($creditById), ), static fn (int $id): bool => $id > 0)), ); $paymentById = []; if ($paymentIds !== []) { foreach ($this->fetchPaymentRowsByIds($admin, $siteCode, $paymentIds) as $row) { $paymentById[(int) $row->id] = $row; } } $adjustmentById = []; if ($adjustmentIds !== []) { foreach ($this->fetchAdjustmentRowsByIds($admin, $siteCode, $adjustmentIds) as $row) { $adjustmentById[(int) $row->id] = $row; } } $shareById = []; if ($shareIds !== []) { foreach ($this->fetchShareRowsByIds($admin, $siteCode, $shareIds) as $row) { $shareById[(int) $row->id] = $row; } } $shareTicketRefs = $this->partyEnrichment->loadTicketRefs( array_values(array_filter(array_map( static fn (object $row): int => (int) ($row->ticket_item_id ?? 0), array_values($shareById), ), static fn (int $id): bool => $id > 0)), ); $items = []; foreach ($stubs as $stub) { $kind = (string) $stub->entry_kind; $id = (int) $stub->entry_id; if ($kind === 'credit' && isset($creditById[$id])) { $row = $creditById[$id]; $pid = (int) $row->player_id; $bill = (string) ($row->ref_type ?? '') === 'settlement_bill' ? ($creditBillRefs[(int) ($row->ref_id ?? 0)] ?? null) : ($playerBills[$pid] ?? null); $items[] = $this->formatCreditEntry($row, $bill, $ticketRefs); } elseif ($kind === 'payment' && isset($paymentById[$id])) { $items[] = $this->formatPaymentEntry($paymentById[$id]); } elseif ($kind === 'adjustment' && isset($adjustmentById[$id])) { $items[] = $this->formatAdjustmentEntry($adjustmentById[$id]); } elseif ($kind === 'share' && isset($shareById[$id])) { $items[] = $this->formatShareEntry($shareById[$id], $shareTicketRefs); } } return $items; } /** * @param list $ids * @return array */ private function settlementBillsByIds(AdminUser $admin, array $ids): array { $ids = array_values(array_unique(array_filter($ids, static fn (int $id): bool => $id > 0))); if ($ids === []) { return []; } $query = DB::table('settlement_bills as sb') ->whereIn('sb.id', $ids) ->select([ 'sb.id', 'sb.owner_id as player_id', 'sb.status', 'sb.bill_type', 'sb.net_amount', 'sb.unpaid_amount', 'sb.paid_amount', 'sb.settlement_period_id', ]); AdminAgentSettlementScope::applySubtreeToBillsQuery($query, $admin, 'sb'); $map = []; foreach ($query->get() as $bill) { $map[(int) $bill->id] = $bill; } return $map; } /** * @return array{0: Carbon|null, 1: Carbon|null} */ private function includeEntryKind(SettlementLedgerListFilters $filters, string $kind): bool { if ($filters->badDebtOnly) { return $kind === 'adjustment'; } $selected = $filters->entryKind; if ($selected === null || $selected === '' || $selected === 'all') { return true; } return $selected === $kind; } /** * @param list> $items * @return list> */ private function applyFilters(array $items, SettlementLedgerListFilters $filters): array { return array_values(array_filter($items, function (array $row) use ($filters): bool { if ($filters->badDebtOnly) { if (($row['entry_kind'] ?? '') !== 'adjustment' || ($row['biz_type'] ?? '') !== 'bad_debt') { return false; } } elseif ($filters->entryKind === 'adjustment') { if (($row['entry_kind'] ?? '') === 'adjustment' && ($row['biz_type'] ?? '') === 'bad_debt') { return false; } } if ($filters->actionableOnly) { $actions = $row['available_actions'] ?? []; $operational = array_filter( $actions, static fn (string $a): bool => ! in_array($a, ['view_player', 'view_bill'], true), ); if ($operational === []) { return false; } } return true; })); } private function resolveCreatedRange( ?int $settlementPeriodId, ?string $createdFrom, ?string $createdTo, ): ?array { return $this->resolvePeriodRange($settlementPeriodId, $createdFrom, $createdTo); } /** * @return array{0: Carbon, 1: Carbon}|null */ private function resolveSettledRange( ?int $settlementPeriodId, ?string $createdFrom, ?string $createdTo, ): ?array { return $this->resolvePeriodRange($settlementPeriodId, $createdFrom, $createdTo); } /** * @return array{0: Carbon, 1: Carbon}|null */ private function resolvePeriodRange( ?int $settlementPeriodId, ?string $rangeFrom, ?string $rangeTo, ): ?array { if ($settlementPeriodId !== null && $settlementPeriodId > 0) { $period = DB::table('settlement_periods')->where('id', $settlementPeriodId)->first(); if ($period === null) { return null; } return AgentSettlementPeriodWindow::bounds( (string) $period->period_start, (string) $period->period_end, ); } $from = $rangeFrom !== null && $rangeFrom !== '' ? Carbon::parse($rangeFrom)->startOfDay() : null; $to = $rangeTo !== null && $rangeTo !== '' ? Carbon::parse($rangeTo)->endOfDay() : null; if ($from === null && $to === null) { return null; } return [ $from ?? Carbon::parse('1970-01-01')->startOfDay(), $to ?? Carbon::now()->endOfDay(), ]; } /** * @param list $ids * @return list */ private function fetchShareRowsByIds(AdminUser $admin, string $siteCode, array $ids): array { if ($ids === []) { return []; } $query = DB::table('share_ledger as sl') ->join('players as p', 'p.id', '=', 'sl.player_id') ->leftJoin('ticket_items as ti', 'ti.id', '=', 'sl.ticket_item_id') ->leftJoin('draws as d', 'd.id', '=', 'ti.draw_id') ->leftJoin('agent_nodes as da', 'da.id', '=', 'p.agent_node_id') ->leftJoin('agent_nodes as pa', 'pa.id', '=', 'da.parent_id') ->leftJoin('agent_nodes as sla', 'sla.id', '=', 'sl.agent_node_id') ->where('p.site_code', $siteCode) ->whereIn('sl.id', $ids) ->select([ 'sl.id', 'sl.ticket_item_id', 'sl.player_id', 'sl.agent_node_id as share_agent_node_id', 'sl.shared_net_win_loss', 'sl.game_win_loss', 'sl.settled_at', 'p.site_code', 'p.site_player_id', 'p.username', 'p.nickname', 'p.agent_node_id', 'p.funding_mode', 'p.auth_source', 'p.default_currency', 'ti.play_code', 'd.draw_no', 'da.id as direct_agent_id', 'da.code as direct_agent_code', 'da.name as direct_agent_name', 'pa.id as parent_agent_id', 'pa.code as parent_agent_code', 'pa.name as parent_agent_name', 'sla.code as share_agent_code', 'sla.name as share_agent_name', ]); AdminAgentSettlementScope::applyDirectPlayersToAlias($query, $admin, 'p'); return $query->get()->all(); } /** * @param array $ticketRefs * @return array */ private function formatShareEntry(object $row, array $ticketRefs): array { $ticketId = (int) ($row->ticket_item_id ?? 0); $ticketRef = $ticketRefs[$ticketId] ?? null; $signed = (int) ($row->shared_net_win_loss ?? 0); return array_merge( $this->baseRow( entryKind: 'share', entryId: (int) $row->id, txnPrefix: 'SL', playerId: (int) $row->player_id, row: $row, bizType: self::SHARE_BIZ_TYPE, signedAmount: $signed, createdAt: $row->settled_at, ledgerSource: 'share_ledger', settlementBillId: null, billStatus: null, billType: null, billUnpaid: null, refLabel: $ticketId > 0 ? '#'.$ticketId : null, refType: $ticketId > 0 ? 'ticket_item' : null, refId: $ticketId > 0 ? $ticketId : null, ), $this->partyFieldsFromRow($row), [ 'play_code' => $ticketRef['play_code'] ?? $row->play_code ?? null, 'draw_no' => $ticketRef['draw_no'] ?? $row->draw_no ?? null, 'ticket_item_id' => $ticketId > 0 ? $ticketId : null, 'available_actions' => ['view_player'], ], ); } /** * @return array */ private function playerBillsMap(AdminUser $admin, string $siteCode, ?int $periodId): array { $query = DB::table('settlement_bills as sb') ->join('settlement_periods as sp', 'sp.id', '=', 'sb.settlement_period_id') ->join('players as p', function ($join): void { $join->on('p.id', '=', 'sb.owner_id')->where('sb.owner_type', '=', 'player'); }) ->where('p.site_code', $siteCode) ->where('sb.bill_type', 'player') ->select([ 'sb.id', 'sb.owner_id as player_id', 'sb.status', 'sb.bill_type', 'sb.net_amount', 'sb.unpaid_amount', 'sb.paid_amount', 'sb.settlement_period_id', ]) ->orderByDesc('sb.id'); if ($periodId !== null && $periodId > 0) { abort_if(! AdminAgentSettlementScope::periodAccessible($admin, $periodId), 403); $query->where('sb.settlement_period_id', $periodId); } AdminAgentSettlementScope::applyDirectPlayersToAlias($query, $admin, 'p'); $map = []; foreach ($query->limit(500)->get() as $bill) { $pid = (int) $bill->player_id; if (! isset($map[$pid])) { $map[$pid] = $bill; continue; } $existing = $map[$pid]; if ((string) $bill->status === 'pending_confirm') { $map[$pid] = $bill; } elseif ((string) $existing->status !== 'pending_confirm' && (int) $bill->unpaid_amount > 0 && (int) $existing->unpaid_amount <= 0) { $map[$pid] = $bill; } } return $map; } /** * @param array{0: Carbon, 1: Carbon}|null $range * @return list */ private function fetchCreditRows( AdminUser $admin, string $siteCode, ?array $range, ?int $playerId, ): array { $query = DB::table('credit_ledger as cl') ->join('players as p', function ($join): void { $join->on('p.id', '=', 'cl.owner_id') ->where('cl.owner_type', '=', 'player'); }) ->leftJoin('agent_nodes as da', 'da.id', '=', 'p.agent_node_id') ->leftJoin('agent_nodes as pa', 'pa.id', '=', 'da.parent_id') ->where('p.site_code', $siteCode) ->where('p.funding_mode', PlayerFundingMode::CREDIT) ->select([ 'cl.id', 'cl.amount', 'cl.reason', 'cl.ref_type', 'cl.ref_id', 'cl.created_at', 'p.id as player_id', 'p.site_code', 'p.site_player_id', 'p.username', 'p.nickname', 'p.agent_node_id', 'p.funding_mode', 'p.auth_source', 'p.default_currency', 'da.id as direct_agent_id', 'da.code as direct_agent_code', 'da.name as direct_agent_name', 'pa.id as parent_agent_id', 'pa.code as parent_agent_code', 'pa.name as parent_agent_name', ]) ->orderByDesc('cl.id'); AdminAgentSettlementScope::applyDirectPlayersToAlias($query, $admin, 'p'); if ($playerId !== null && $playerId > 0) { $query->where('p.id', $playerId); } if ($range !== null) { $query->whereBetween('cl.created_at', $range); } return $query->limit(500)->get()->all(); } /** * @param array{0: Carbon, 1: Carbon}|null $range * @return list */ private function fetchBetFlowCreditRows( AdminUser $admin, string $siteCode, ?array $range, SettlementLedgerListFilters $filters, ): array { $query = DB::table('credit_ledger as cl') ->join('players as p', function ($join): void { $join->on('p.id', '=', 'cl.owner_id') ->where('cl.owner_type', '=', 'player'); }) ->leftJoin('agent_nodes as da', 'da.id', '=', 'p.agent_node_id') ->leftJoin('agent_nodes as pa', 'pa.id', '=', 'da.parent_id') ->where('p.site_code', $siteCode) ->where('p.funding_mode', PlayerFundingMode::CREDIT) ->whereIn('cl.reason', [ 'bet_hold', 'bet_hold_release', 'game_settlement_loss', ]) ->select([ 'cl.id', 'cl.amount', 'cl.reason', 'cl.ref_type', 'cl.ref_id', 'cl.created_at', 'p.id as player_id', 'p.site_code', 'p.site_player_id', 'p.username', 'p.nickname', 'p.agent_node_id', 'p.funding_mode', 'p.auth_source', 'p.default_currency', 'da.id as direct_agent_id', 'da.code as direct_agent_code', 'da.name as direct_agent_name', 'pa.id as parent_agent_id', 'pa.code as parent_agent_code', 'pa.name as parent_agent_name', ]) ->orderByDesc('cl.id'); AdminAgentSettlementScope::applyDirectPlayersToAlias($query, $admin, 'p'); $this->applyLedgerPlayerFilters($query, 'p', $filters); if ($range !== null) { $query->whereBetween('cl.created_at', $range); } return $query->limit(5000)->get()->all(); } /** * @param list $ids * @return list */ private function fetchCreditRowsByIds(AdminUser $admin, string $siteCode, array $ids): array { if ($ids === []) { return []; } $query = DB::table('credit_ledger as cl') ->join('players as p', function ($join): void { $join->on('p.id', '=', 'cl.owner_id') ->where('cl.owner_type', '=', 'player'); }) ->leftJoin('agent_nodes as da', 'da.id', '=', 'p.agent_node_id') ->leftJoin('agent_nodes as pa', 'pa.id', '=', 'da.parent_id') ->where('p.site_code', $siteCode) ->where('p.funding_mode', PlayerFundingMode::CREDIT) ->whereIn('cl.id', $ids) ->select([ 'cl.id', 'cl.amount', 'cl.reason', 'cl.ref_type', 'cl.ref_id', 'cl.created_at', 'p.id as player_id', 'p.site_code', 'p.site_player_id', 'p.username', 'p.nickname', 'p.agent_node_id', 'p.funding_mode', 'p.auth_source', 'p.default_currency', 'da.id as direct_agent_id', 'da.code as direct_agent_code', 'da.name as direct_agent_name', 'pa.id as parent_agent_id', 'pa.code as parent_agent_code', 'pa.name as parent_agent_name', ]); AdminAgentSettlementScope::applyDirectPlayersToAlias($query, $admin, 'p'); return $query->get()->all(); } /** * @return list */ private function fetchPaymentRows( AdminUser $admin, string $siteCode, ?int $periodId, ?int $playerId, ): array { $query = DB::table('payment_records as pr') ->join('settlement_bills as sb', 'sb.id', '=', 'pr.settlement_bill_id') ->join('settlement_periods as sp', 'sp.id', '=', 'sb.settlement_period_id') ->join('players as p', function ($join): void { $join->on('p.id', '=', 'sb.owner_id') ->where('sb.owner_type', '=', 'player'); }) ->where('p.site_code', $siteCode) ->leftJoin('agent_nodes as da', 'da.id', '=', 'p.agent_node_id') ->leftJoin('agent_nodes as pa', 'pa.id', '=', 'da.parent_id') ->select([ 'pr.id', 'pr.amount', 'pr.method', 'pr.status', 'pr.created_at', 'pr.settlement_bill_id', 'sb.status as bill_status', 'sb.bill_type', 'sb.unpaid_amount', 'p.id as player_id', 'p.site_player_id', 'p.username', 'p.nickname', 'p.agent_node_id', 'p.auth_source', 'p.funding_mode', 'p.default_currency', 'da.id as direct_agent_id', 'da.code as direct_agent_code', 'da.name as direct_agent_name', 'pa.id as parent_agent_id', 'pa.code as parent_agent_code', 'pa.name as parent_agent_name', ]) ->orderByDesc('pr.id'); if ($periodId !== null && $periodId > 0) { $query->where('sb.settlement_period_id', $periodId); } if ($playerId !== null && $playerId > 0) { $query->where('p.id', $playerId); } AdminAgentSettlementScope::applyDirectPlayersToAlias($query, $admin, 'p'); $siteIds = $admin->accessibleAdminSiteIds(); if ($siteIds !== null) { if ($siteIds === []) { return []; } $query->whereIn('sp.admin_site_id', $siteIds); } return $query->limit(300)->get()->all(); } /** * @param list $ids * @return list */ private function fetchPaymentRowsByIds(AdminUser $admin, string $siteCode, array $ids): array { if ($ids === []) { return []; } $adminSiteId = (int) DB::table('admin_sites')->where('code', $siteCode)->value('id'); if ($adminSiteId <= 0) { return []; } $query = DB::table('payment_records as pr') ->join('settlement_bills as sb', 'sb.id', '=', 'pr.settlement_bill_id') ->join('settlement_periods as sp', 'sp.id', '=', 'sb.settlement_period_id') ->where('sp.admin_site_id', $adminSiteId) ->leftJoin('players as p', function ($join): void { $join->on('p.id', '=', 'sb.owner_id') ->where('sb.owner_type', '=', 'player'); }) ->leftJoin('agent_nodes as owner_an', function ($join): void { $join->on('owner_an.id', '=', 'sb.owner_id') ->where('sb.owner_type', '=', 'agent'); }) ->leftJoin('agent_nodes as da', 'da.id', '=', 'p.agent_node_id') ->leftJoin('agent_nodes as pa', 'pa.id', '=', 'da.parent_id') ->whereIn('pr.id', $ids) ->select([ 'pr.id', 'pr.amount', 'pr.method', 'pr.status', 'pr.created_at', 'pr.settlement_bill_id', 'sb.status as bill_status', 'sb.bill_type', 'sb.unpaid_amount', 'p.id as player_id', 'p.site_player_id', 'p.username', 'p.nickname', 'p.agent_node_id', 'p.auth_source', 'p.funding_mode', 'p.default_currency', DB::raw('COALESCE(da.id, owner_an.id) as direct_agent_id'), DB::raw('COALESCE(da.code, owner_an.code) as direct_agent_code'), DB::raw('COALESCE(da.name, owner_an.name) as direct_agent_name'), 'pa.id as parent_agent_id', 'pa.code as parent_agent_code', 'pa.name as parent_agent_name', ]) ->selectRaw('COALESCE(p.site_code, ?) as site_code', [$siteCode]); AdminAgentSettlementScope::applySubtreeToBillsQuery($query, $admin, 'sb'); $this->applyLedgerSiteScope($query, $admin, 'sp'); return $query->get()->all(); } /** * @return list */ private function fetchAdjustmentRows( AdminUser $admin, string $siteCode, ?int $periodId, ?int $playerId, ): array { $query = DB::table('settlement_adjustments as sa') ->leftJoin('settlement_bills as sb', 'sb.id', '=', 'sa.original_bill_id') ->leftJoin('settlement_periods as sp', 'sp.id', '=', 'sa.settlement_period_id') ->leftJoin('players as p', function ($join): void { $join->on('p.id', '=', 'sb.owner_id') ->where('sb.owner_type', '=', 'player'); }) ->where('p.site_code', $siteCode) ->leftJoin('agent_nodes as da', 'da.id', '=', 'p.agent_node_id') ->leftJoin('agent_nodes as pa', 'pa.id', '=', 'da.parent_id') ->select([ 'sa.id', 'sa.amount', 'sa.adjustment_type', 'sa.reason', 'sa.created_at', 'sa.original_bill_id as settlement_bill_id', 'sb.status as bill_status', 'sb.bill_type', 'sb.unpaid_amount', 'p.id as player_id', 'p.site_player_id', 'p.username', 'p.nickname', 'p.agent_node_id', 'p.auth_source', 'p.funding_mode', 'p.default_currency', 'da.id as direct_agent_id', 'da.code as direct_agent_code', 'da.name as direct_agent_name', 'pa.id as parent_agent_id', 'pa.code as parent_agent_code', 'pa.name as parent_agent_name', ]) ->orderByDesc('sa.id'); if ($periodId !== null && $periodId > 0) { $query->where('sa.settlement_period_id', $periodId); } if ($playerId !== null && $playerId > 0) { $query->where('p.id', $playerId); } AdminAgentSettlementScope::applyDirectPlayersToAlias($query, $admin, 'p'); $siteIds = $admin->accessibleAdminSiteIds(); if ($siteIds !== null) { if ($siteIds === []) { return []; } $query->whereIn('sp.admin_site_id', $siteIds); } return $query->limit(300)->get()->all(); } /** * @param list $ids * @return list */ private function fetchAdjustmentRowsByIds(AdminUser $admin, string $siteCode, array $ids): array { if ($ids === []) { return []; } $query = DB::table('settlement_adjustments as sa') ->leftJoin('settlement_bills as sb', 'sb.id', '=', 'sa.original_bill_id') ->leftJoin('settlement_periods as sp', 'sp.id', '=', 'sa.settlement_period_id') ->leftJoin('players as p', function ($join): void { $join->on('p.id', '=', 'sb.owner_id') ->where('sb.owner_type', '=', 'player'); }) ->where('p.site_code', $siteCode) ->whereIn('sa.id', $ids) ->leftJoin('agent_nodes as da', 'da.id', '=', 'p.agent_node_id') ->leftJoin('agent_nodes as pa', 'pa.id', '=', 'da.parent_id') ->select([ 'sa.id', 'sa.amount', 'sa.adjustment_type', 'sa.reason', 'sa.created_at', 'sa.original_bill_id as settlement_bill_id', 'sb.status as bill_status', 'sb.bill_type', 'sb.unpaid_amount', 'p.id as player_id', 'p.site_player_id', 'p.username', 'p.nickname', 'p.agent_node_id', 'p.auth_source', 'p.funding_mode', 'p.default_currency', 'da.id as direct_agent_id', 'da.code as direct_agent_code', 'da.name as direct_agent_name', 'pa.id as parent_agent_id', 'pa.code as parent_agent_code', 'pa.name as parent_agent_name', ]); AdminAgentSettlementScope::applyDirectPlayersToAlias($query, $admin, 'p'); $this->applyLedgerSiteScope($query, $admin, 'sp'); return $query->get()->all(); } /** * @return array */ /** * @param array $ticketRefs */ private function formatCreditEntry(object $row, ?object $bill, array $ticketRefs = []): array { $amount = (int) $row->amount; $billId = $bill !== null ? (int) $bill->id : null; $ticketRef = $this->resolveTicketRef($row, $ticketRefs); return array_merge( $this->baseRow( entryKind: 'credit', entryId: (int) $row->id, txnPrefix: 'CL', playerId: (int) ($row->player_id ?? 0), row: $row, bizType: (string) $row->reason, signedAmount: $amount, createdAt: $row->created_at, ledgerSource: 'credit_ledger', settlementBillId: $billId, billStatus: $bill !== null ? (string) $bill->status : null, billType: $bill !== null ? (string) $bill->bill_type : null, billUnpaid: $bill !== null ? (int) $bill->unpaid_amount : null, refType: isset($row->ref_type) ? (string) $row->ref_type : null, refId: isset($row->ref_id) ? (int) $row->ref_id : null, ), $this->partyFieldsFromRow($row), $ticketRef, ); } /** * @return array */ private function formatPaymentEntry(object $row): array { $amount = (int) $row->amount; $method = trim((string) ($row->method ?? '')); $methodLabel = $method !== '' && ! ctype_digit($method) ? ' · '.$method : ''; return array_merge( $this->baseRow( entryKind: 'payment', entryId: (int) $row->id, txnPrefix: 'PAY', playerId: (int) ($row->player_id ?? 0), row: $row, bizType: 'payment_record', signedAmount: $amount, createdAt: $row->created_at, ledgerSource: 'payment_record', settlementBillId: (int) $row->settlement_bill_id, billStatus: (string) ($row->bill_status ?? ''), billType: (string) ($row->bill_type ?? ''), billUnpaid: isset($row->unpaid_amount) ? (int) $row->unpaid_amount : null, refLabel: 'bill#'.$row->settlement_bill_id.$methodLabel, ), $this->partyFieldsFromRow($row), ); } /** * @return array */ private function formatAdjustmentEntry(object $row): array { $amount = (int) $row->amount; $type = (string) $row->adjustment_type; return array_merge( $this->baseRow( entryKind: 'adjustment', entryId: (int) $row->id, txnPrefix: 'ADJ', playerId: (int) ($row->player_id ?? 0), row: $row, bizType: $type, signedAmount: $amount, createdAt: $row->created_at, ledgerSource: 'settlement_adjustment', settlementBillId: (int) $row->settlement_bill_id, billStatus: (string) ($row->bill_status ?? ''), billType: (string) ($row->bill_type ?? ''), billUnpaid: isset($row->unpaid_amount) ? (int) $row->unpaid_amount : null, refLabel: $row->reason !== null && $row->reason !== '' ? (string) $row->reason : 'bill#'.$row->settlement_bill_id, ), $this->partyFieldsFromRow($row), ); } /** * @param array $ticketRefs * @return array{play_code: string|null, draw_no: string|null, ticket_item_id: int|null} */ private function resolveTicketRef(object $row, array $ticketRefs): array { if ((string) ($row->ref_type ?? '') !== 'ticket_item') { return ['play_code' => null, 'draw_no' => null, 'ticket_item_id' => null]; } $ticketId = (int) ($row->ref_id ?? 0); $ref = $ticketRefs[$ticketId] ?? null; return [ 'play_code' => $ref['play_code'] ?? null, 'draw_no' => $ref['draw_no'] ?? null, 'ticket_item_id' => $ticketId > 0 ? $ticketId : null, ]; } /** * @return array */ private function partyFieldsFromRow(object $row): array { $directId = (int) ($row->direct_agent_id ?? $row->agent_node_id ?? 0); $parentId = (int) ($row->parent_agent_id ?? 0); return [ 'direct_agent_id' => $directId > 0 ? $directId : null, 'direct_agent_label' => $directId > 0 ? $this->partyEnrichment->formatAgent((object) [ 'name' => $row->direct_agent_name ?? null, 'code' => $row->direct_agent_code ?? null, ], $directId) : null, 'parent_agent_id' => $parentId > 0 ? $parentId : null, 'parent_agent_label' => $parentId > 0 ? $this->partyEnrichment->formatAgent((object) [ 'name' => $row->parent_agent_name ?? null, 'code' => $row->parent_agent_code ?? null, ], $parentId) : null, ]; } /** * @return array */ private function baseRow( string $entryKind, int $entryId, string $txnPrefix, int $playerId, object $row, string $bizType, int $signedAmount, mixed $createdAt, string $ledgerSource, ?int $settlementBillId, ?string $billStatus, ?string $billType, ?int $billUnpaid, ?string $refLabel = null, ?string $refType = null, ?int $refId = null, ): array { $amountAbs = abs($signedAmount); $currency = (string) ($row->default_currency ?? ''); return [ 'entry_kind' => $entryKind, 'id' => $entryId, 'row_key' => $entryKind.'-'.$entryId, 'txn_no' => $txnPrefix.'-'.$entryId, 'player_id' => $playerId, 'site_code' => $row->site_code ?? null, 'site_player_id' => $row->site_player_id ?? null, 'username' => $row->username ?? null, 'nickname' => $row->nickname ?? null, 'biz_type' => $bizType, 'ref_type' => $refType, 'ref_id' => $refId, 'biz_no' => $refLabel ?? $this->creditRefLabel($row), 'direction' => $signedAmount >= 0 ? 1 : 2, 'amount' => $amountAbs, 'amount_formatted' => CurrencyFormatter::fromMinor($amountAbs), 'signed_amount' => $signedAmount, 'currency_code' => $currency, 'status' => 'posted', 'created_at' => $createdAt !== null ? Carbon::parse($createdAt)->toIso8601String() : null, 'ledger_source' => $ledgerSource, 'funding_mode' => (string) ($row->funding_mode ?? PlayerFundingMode::CREDIT), 'auth_source' => $row->auth_source ?? null, 'settlement_bill_id' => $settlementBillId, 'bill_status' => $billStatus, 'bill_type' => $billType, 'bill_unpaid_amount' => $billUnpaid, 'available_actions' => $this->resolveActions( $entryKind, $settlementBillId, $billStatus, $billType, $billUnpaid, ), ]; } /** * @return list */ private function resolveActions( string $entryKind, ?int $billId, ?string $billStatus, ?string $billType, ?int $billUnpaid, ): array { $actions = ['view_player']; if ($billId === null || $billId <= 0) { return $actions; } $actions[] = 'view_bill'; if ($billStatus === 'pending_confirm') { $actions[] = 'confirm'; } if ($billStatus !== null && in_array($billStatus, ['confirmed', 'partial_paid', 'overdue'], true) && ($billUnpaid ?? 0) > 0) { $actions[] = 'payment'; } if ($billStatus !== null && in_array($billStatus, ['confirmed', 'partial_paid', 'settled', 'overdue'], true) && ! in_array((string) $billType, ['adjustment', 'reversal', 'bad_debt'], true)) { $actions[] = 'adjustment'; $actions[] = 'reversal'; } if ($billStatus !== null && in_array($billStatus, ['confirmed', 'partial_paid', 'overdue'], true) && ($billUnpaid ?? 0) > 0 && ! in_array((string) $billType, ['adjustment', 'reversal', 'bad_debt'], true)) { $actions[] = 'bad_debt'; } return array_values(array_unique($actions)); } private function creditRefLabel(object $row): ?string { if (! isset($row->ref_type) || $row->ref_type === null || $row->ref_id === null) { return null; } return (string) $row->ref_type.'#'.$row->ref_id; } }