>, * 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); $playerBills = $this->playerBillsMap($admin, $siteCode, $periodId); $items = []; $includeCredit = $this->includeEntryKind($filters, 'credit'); $includePayment = $this->includeEntryKind($filters, 'payment'); $includeAdjustment = $this->includeEntryKind($filters, 'adjustment'); if ($includeCredit) { $creditRows = $this->fetchCreditRows($admin, $siteCode, $range, $filters->playerId); foreach ($creditRows as $row) { $pid = (int) $row->player_id; $bill = $playerBills[$pid] ?? null; $items[] = $this->formatCreditEntry($row, $bill); } } if ($includePayment) { foreach ($this->fetchPaymentRows($admin, $siteCode, $periodId, $filters->playerId) as $row) { $items[] = $this->formatPaymentEntry($row); } } if ($includeAdjustment) { foreach ($this->fetchAdjustmentRows($admin, $siteCode, $periodId, $filters->playerId) as $row) { if ($filters->badDebtOnly && (string) $row->adjustment_type !== 'bad_debt') { continue; } $items[] = $this->formatAdjustmentEntry($row); } } $items = $this->applyFilters($items, $filters); usort($items, static function (array $a, array $b): int { return strcmp((string) ($b['created_at'] ?? ''), (string) ($a['created_at'] ?? '')); }); $total = count($items); $offset = max(0, ($page - 1) * $perPage); $pageItems = array_slice($items, $offset, $perPage); return [ 'items' => array_values($pageItems), 'total' => $total, 'page' => $page, 'per_page' => $perPage, 'ledger_source' => 'settlement_ledger', ]; } /** * @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->txnNo !== null) { $needle = strtolower($filters->txnNo); $hay = strtolower((string) ($row['txn_no'] ?? '')); if (! str_contains($hay, $needle)) { return false; } } if ($filters->playerAccount !== null) { $needle = strtolower($filters->playerAccount); $haystack = strtolower(implode(' ', array_filter([ (string) ($row['username'] ?? ''), (string) ($row['nickname'] ?? ''), (string) ($row['site_player_id'] ?? ''), ]))); if (! str_contains($haystack, $needle)) { return false; } } if ($filters->bizType !== null && ($row['biz_type'] ?? '') !== $filters->bizType) { return false; } if ($filters->billStatus !== null && ($row['bill_status'] ?? '') !== $filters->billStatus) { 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 { if ($settlementPeriodId !== null && $settlementPeriodId > 0) { $period = DB::table('settlement_periods')->where('id', $settlementPeriodId)->first(); if ($period === null) { return null; } return [ Carbon::parse($period->period_start)->startOfDay(), Carbon::parse($period->period_end)->endOfDay(), ]; } $from = $createdFrom !== null && $createdFrom !== '' ? Carbon::parse($createdFrom)->startOfDay() : null; $to = $createdTo !== null && $createdTo !== '' ? Carbon::parse($createdTo)->endOfDay() : null; if ($from === null && $to === null) { return null; } return [ $from ?? Carbon::parse('1970-01-01')->startOfDay(), $to ?? Carbon::now()->endOfDay(), ]; } /** * @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); } AdminDataScope::applyToPlayersAlias($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'); }) ->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.funding_mode', 'p.auth_source', 'p.default_currency', ]) ->orderByDesc('cl.id'); AdminDataScope::applyToPlayersAlias($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(); } /** * @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) ->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.auth_source', 'p.funding_mode', 'p.default_currency', ]) ->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); } AdminDataScope::applyToPlayersAlias($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(); } /** * @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) ->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.auth_source', 'p.funding_mode', 'p.default_currency', ]) ->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); } AdminDataScope::applyToPlayersAlias($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(); } /** * @return array */ private function formatCreditEntry(object $row, ?object $bill): array { $amount = (int) $row->amount; $billId = $bill !== null ? (int) $bill->id : null; return $this->baseRow( entryKind: 'credit', entryId: (int) $row->id, txnPrefix: 'CL', playerId: (int) $row->player_id, 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, ); } /** * @return array */ private function formatPaymentEntry(object $row): array { $amount = (int) $row->amount; return $this->baseRow( entryKind: 'payment', entryId: (int) $row->id, txnPrefix: 'PAY', playerId: (int) $row->player_id, 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.($row->method ? ' · '.$row->method : ''), ); } /** * @return array */ private function formatAdjustmentEntry(object $row): array { $amount = (int) $row->amount; $type = (string) $row->adjustment_type; return $this->baseRow( entryKind: 'adjustment', entryId: (int) $row->id, txnPrefix: 'ADJ', playerId: (int) $row->player_id, 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, ); } /** * @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, ): 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, '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; } }