- Added new section in AGENTS.md detailing learned workspace facts for better understanding of settlement processes. - Updated AgentNodeDestroyController to remove unnecessary checks for admin users. - Enhanced AgentSettlement controllers to assert permissions for finance adjustments and bill operations. - Improved query scopes in AgentSettlement services to ensure proper data access based on admin roles. - Refactored methods in SettlementPartyEnrichment for better bill row enrichment and data handling. - Introduced new methods in AdminAgentSettlementScope for managing agent node visibility and finance adjustments.
1538 lines
54 KiB
PHP
1538 lines
54 KiB
PHP
<?php
|
||
|
||
namespace App\Services\AgentSettlement;
|
||
|
||
use App\Models\AdminUser;
|
||
use App\Support\AdminAgentSettlementScope;
|
||
use App\Support\AgentSettlementPeriodWindow;
|
||
use App\Support\CurrencyFormatter;
|
||
use App\Support\PlayerFundingMode;
|
||
use Carbon\Carbon;
|
||
use Illuminate\Support\Facades\DB;
|
||
|
||
/** 结算中心统一账务流水(credit_ledger + 收付 + 调账)。 */
|
||
final class SettlementCenterLedgerService
|
||
{
|
||
private const CREDIT_BIZ_TYPES = [
|
||
'bet_hold',
|
||
'bet_hold_release',
|
||
'game_settlement_loss',
|
||
'game_settlement_win',
|
||
'settlement_confirm',
|
||
'settlement_payout',
|
||
];
|
||
|
||
private const ADJUSTMENT_BIZ_TYPES = [
|
||
'adjustment',
|
||
'reversal',
|
||
'bad_debt',
|
||
];
|
||
|
||
private const SHARE_BIZ_TYPE = 'share_ledger';
|
||
|
||
public function __construct(
|
||
private readonly SettlementPartyEnrichment $partyEnrichment,
|
||
private readonly CreditLedgerBetFlowPresenter $betFlowPresenter,
|
||
) {}
|
||
/**
|
||
* @return array{
|
||
* items: list<array<string, mixed>>,
|
||
* 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<array<string, mixed>>,
|
||
* 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<int, object> $stubs
|
||
* @param array<int, object> $playerBills
|
||
* @return list<array<string, mixed>>
|
||
*/
|
||
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<int> $ids
|
||
* @return array<int, object>
|
||
*/
|
||
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<array<string, mixed>> $items
|
||
* @return list<array<string, mixed>>
|
||
*/
|
||
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<int> $ids
|
||
* @return list<object>
|
||
*/
|
||
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<int, array{play_code: string|null, draw_no: string|null, ticket_item_id: int}> $ticketRefs
|
||
* @return array<string, mixed>
|
||
*/
|
||
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<int, object>
|
||
*/
|
||
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<object>
|
||
*/
|
||
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<object>
|
||
*/
|
||
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<int> $ids
|
||
* @return list<object>
|
||
*/
|
||
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<object>
|
||
*/
|
||
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<int> $ids
|
||
* @return list<object>
|
||
*/
|
||
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<object>
|
||
*/
|
||
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<int> $ids
|
||
* @return list<object>
|
||
*/
|
||
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<string, mixed>
|
||
*/
|
||
/**
|
||
* @param array<int, array{play_code: string|null, draw_no: string|null, ticket_item_id: int}> $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<string, mixed>
|
||
*/
|
||
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<string, mixed>
|
||
*/
|
||
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<int, array{play_code: string|null, draw_no: string|null, ticket_item_id: int}> $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<string, mixed>
|
||
*/
|
||
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<string, mixed>
|
||
*/
|
||
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<string>
|
||
*/
|
||
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;
|
||
}
|
||
}
|