From ed5a983003e1b14f7dc4242d570349b3b9fcf4be Mon Sep 17 00:00:00 2001 From: kang Date: Tue, 30 Jun 2026 17:55:16 +0800 Subject: [PATCH] =?UTF-8?q?feat(wallet):=20=E5=A2=9E=E5=BC=BA=E8=B5=84?= =?UTF-8?q?=E9=87=91=E6=B5=81=E6=B0=B4=E5=92=8C=E4=BF=A1=E7=94=A8=E8=B4=A6?= =?UTF-8?q?=E6=9C=AC=E7=9A=84=E5=85=AC=E5=85=B1=E7=B1=BB=E5=9E=8B=E8=BF=87?= =?UTF-8?q?=E6=BB=A4=EF=BC=8C=E6=94=AF=E6=8C=81=E6=B8=B8=E6=88=8F=E7=BB=93?= =?UTF-8?q?=E7=AE=97=E5=92=8C=E8=B4=A6=E5=8D=95=E7=BB=93=E7=AE=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../WalletTransactionListController.php | 42 ++++++- .../Api/V1/Wallet/WalletLogsController.php | 2 +- app/Services/Agent/AgentProfileService.php | 21 +++- .../SettlementCenterLedgerService.php | 19 ++- .../Wallet/PlayerLedgerLogsService.php | 108 +++++++++++++++--- tests/Feature/AdminCreditLedgerFilterTest.php | 58 ++++++++++ tests/Feature/AdminWalletApiTest.php | 60 ++++++++++ tests/Feature/CreditWalletLogsTest.php | 98 +++++++++++++++- 8 files changed, 384 insertions(+), 24 deletions(-) diff --git a/app/Http/Controllers/Api/V1/Admin/Wallet/WalletTransactionListController.php b/app/Http/Controllers/Api/V1/Admin/Wallet/WalletTransactionListController.php index 0e30794..3a3e027 100644 --- a/app/Http/Controllers/Api/V1/Admin/Wallet/WalletTransactionListController.php +++ b/app/Http/Controllers/Api/V1/Admin/Wallet/WalletTransactionListController.php @@ -36,6 +36,16 @@ final class WalletTransactionListController extends Controller private const ALLOWED_STATUS = ['posted', 'pending_reconcile', 'reversed']; + /** 后台筛选兼容玩家端资金流水公共类型,也接受底层 biz_type。 */ + private const PUBLIC_BIZ_TYPE_MAP = [ + 'transfer_in' => ['transfer_in'], + 'transfer_out' => ['transfer_out'], + 'refund' => ['transfer_out_refund'], + 'reversal' => ['reversal', 'bet_reverse'], + 'bet' => ['bet_deduct', 'bet'], + 'prize' => ['settle_payout', 'prize', 'jackpot_manual_payout'], + ]; + public function __construct( private readonly PlayerLedgerLogsService $ledgerLogs, ) {} @@ -101,7 +111,16 @@ final class WalletTransactionListController extends Controller } if (! empty($validated['biz_type'])) { - $query->where('biz_type', trim((string) $validated['biz_type'])); + $bizTypes = $this->resolveBizTypeFilter((string) $validated['biz_type']); + if ($bizTypes === []) { + return ApiResponse::success([ + 'items' => [], + 'total' => 0, + 'page' => $page, + 'per_page' => $perPage, + ]); + } + $query->whereIn('biz_type', $bizTypes); } if ($request->boolean('abnormal')) { @@ -167,4 +186,25 @@ final class WalletTransactionListController extends Controller 'auth_source' => $p?->auth_source, ]; } + + /** + * @return list + */ + private function resolveBizTypeFilter(string $raw): array + { + $parts = array_filter(array_map('trim', explode(',', $raw))); + if ($parts === []) { + return []; + } + + $resolved = []; + foreach ($parts as $part) { + $key = strtolower($part); + foreach (self::PUBLIC_BIZ_TYPE_MAP[$key] ?? [$key] as $bizType) { + $resolved[] = $bizType; + } + } + + return array_values(array_unique($resolved)); + } } diff --git a/app/Http/Controllers/Api/V1/Wallet/WalletLogsController.php b/app/Http/Controllers/Api/V1/Wallet/WalletLogsController.php index 0402574..b290647 100644 --- a/app/Http/Controllers/Api/V1/Wallet/WalletLogsController.php +++ b/app/Http/Controllers/Api/V1/Wallet/WalletLogsController.php @@ -14,7 +14,7 @@ use App\Services\Wallet\PlayerLedgerLogsService; /** * PRD §10.1.1:`GET /api/v1/wallet/logs` — 钱包/信用流水(按玩家资金模式分表)。 * - * Query:`page`、`size`(每页条数,默认 20)、`type`(逗号分隔:transfer_in,transfer_out,bet,prize,refund,reversal) + * Query:`page`、`size`(每页条数,默认 20)、`type`(逗号分隔:transfer_in,transfer_out,bet,prize,refund,reversal,game_settlement,bill_settlement,rebate) */ final class WalletLogsController extends Controller { diff --git a/app/Services/Agent/AgentProfileService.php b/app/Services/Agent/AgentProfileService.php index 57c4269..85c5ed8 100644 --- a/app/Services/Agent/AgentProfileService.php +++ b/app/Services/Agent/AgentProfileService.php @@ -137,10 +137,15 @@ final class AgentProfileService $profile->refresh(); $available = max(0, (int) $profile->credit_limit - (int) $profile->allocated_credit); + $totalShareRate = (float) $profile->total_share_rate; + $parentTotalShareRate = $this->parentTotalShareRateForProfile($profile); return [ 'agent_node_id' => (int) $profile->agent_node_id, - 'total_share_rate' => (float) $profile->total_share_rate, + 'total_share_rate' => $totalShareRate, + 'relative_share_rate' => $parentTotalShareRate > 0 + ? round($totalShareRate / $parentTotalShareRate * 100, 2) + : null, 'credit_limit' => (int) $profile->credit_limit, 'allocated_credit' => (int) $profile->allocated_credit, 'used_credit' => (int) $profile->used_credit, @@ -154,6 +159,20 @@ final class AgentProfileService ]; } + private function parentTotalShareRateForProfile(AgentProfile $profile): float + { + $node = AgentNode::query()->find((int) $profile->agent_node_id); + if ($node === null || $node->parent_id === null) { + return 0.0; + } + + $parentProfile = AgentProfile::query() + ->where('agent_node_id', (int) $node->parent_id) + ->first(); + + return $parentProfile !== null ? (float) $parentProfile->total_share_rate : 100.0; + } + public function profileForNode(int $agentNodeId): ?AgentProfile { return AgentProfile::query()->where('agent_node_id', $agentNodeId)->first(); diff --git a/app/Services/AgentSettlement/SettlementCenterLedgerService.php b/app/Services/AgentSettlement/SettlementCenterLedgerService.php index 8ba7c16..ffb3264 100644 --- a/app/Services/AgentSettlement/SettlementCenterLedgerService.php +++ b/app/Services/AgentSettlement/SettlementCenterLedgerService.php @@ -17,12 +17,15 @@ use App\Support\AgentSettlementPeriodWindow; final class SettlementCenterLedgerService { private const CREDIT_BIZ_TYPES = [ + 'bet', 'bet_hold', + 'game_settlement', 'bet_hold_release', 'game_settlement_loss', 'game_settlement_win', 'game_settlement_reversal', 'settlement_confirm', + 'bill_settlement', 'settlement_payout', ]; @@ -34,6 +37,12 @@ final class SettlementCenterLedgerService private const SHARE_BIZ_TYPE = 'share_ledger'; + private const CREDIT_PUBLIC_BIZ_TYPE_MAP = [ + 'bet' => ['bet_hold'], + 'game_settlement' => ['bet_hold_release', 'game_settlement_loss', 'game_settlement_win', 'game_settlement_reversal'], + 'bill_settlement' => ['settlement_confirm', 'settlement_payout'], + ]; + public function __construct( private readonly SettlementPartyEnrichment $partyEnrichment, private readonly CreditLedgerBetFlowPresenter $betFlowPresenter, @@ -304,7 +313,7 @@ final class SettlementCenterLedgerService } if ($filters->bizType !== null && $filters->bizType !== '') { - $query->where('cl.reason', $filters->bizType); + $query->whereIn('cl.reason', $this->resolveCreditReasonFilter($filters->bizType)); } elseif ($filters->betFlowOnly) { $query->whereIn('cl.reason', [ 'bet_hold', @@ -319,6 +328,14 @@ final class SettlementCenterLedgerService return $query; } + /** + * @return list + */ + private function resolveCreditReasonFilter(string $bizType): array + { + return self::CREDIT_PUBLIC_BIZ_TYPE_MAP[$bizType] ?? [$bizType]; + } + private function paymentStubQuery( AdminUser $admin, string $siteCode, diff --git a/app/Services/Wallet/PlayerLedgerLogsService.php b/app/Services/Wallet/PlayerLedgerLogsService.php index 845a24e..2598c3c 100644 --- a/app/Services/Wallet/PlayerLedgerLogsService.php +++ b/app/Services/Wallet/PlayerLedgerLogsService.php @@ -34,12 +34,14 @@ final class PlayerLedgerLogsService /** PRD 对外类型 → credit_ledger.reason(信用盘不用钱包「派彩」口径) */ private const CREDIT_TYPE_TO_REASON = [ - 'bet' => ['bet_hold', 'game_settlement_loss'], + 'bet' => ['bet_hold'], + 'game_settlement' => ['bet_hold_release', 'game_settlement_loss', 'game_settlement_win', 'game_settlement_reversal'], 'reversal' => ['bet_hold_release', 'game_settlement_reversal'], 'refund' => ['settlement_confirm'], 'win_credit' => ['game_settlement_win'], 'credit_release' => ['game_settlement_win', 'settlement_confirm', 'bet_hold_release'], - 'bill_settlement' => ['settlement_payout'], + 'bill_settlement' => ['settlement_confirm', 'settlement_payout'], + 'rebate' => [], 'transfer_in' => [], 'transfer_out' => [], ]; @@ -423,24 +425,43 @@ final class PlayerLedgerLogsService string $typeFilterRaw, ): array { $reasonFilter = $this->resolveCreditReasonFilter($typeFilterRaw); + $includeRebates = $this->creditFilterIncludesRebates($typeFilterRaw); - if (is_array($reasonFilter) && $reasonFilter === []) { + if (is_array($reasonFilter) && $reasonFilter === [] && ! $includeRebates) { return ['items' => [], 'total' => 0, 'page' => $page, 'per_page' => $perPage]; } - $paginator = $this->creditLedgerQuery((int) $player->id, $reasonFilter) - ->paginate($perPage, ['*'], 'page', $page); - $currency = (string) $player->default_currency; - $skipRows = max(0, ($paginator->currentPage() - 1) * $paginator->perPage()); + $creditRows = is_array($reasonFilter) && $reasonFilter === [] + ? collect() + : $this->creditLedgerQuery((int) $player->id, $reasonFilter)->get(); + $rebateRows = $includeRebates ? $this->rebateRecordRows((int) $player->id) : collect(); + $rows = $creditRows + ->map(fn (object $row): object => (object) ['source' => 'credit', 'row' => $row]) + ->concat($rebateRows->map(fn (object $row): object => (object) ['source' => 'rebate', 'row' => $row])) + ->sortByDesc(fn (object $entry): string => (string) ($entry->row->created_at ?? '')) + ->values(); + + $total = $rows->count(); + $offset = max(0, ($page - 1) * $perPage); + $pageRows = $rows->slice($offset, $perPage)->values(); + $priorCreditRows = $rows + ->slice(0, $offset) + ->filter(fn (object $entry): bool => $entry->source === 'credit') + ->count(); $runningMinor = $this->advanceCreditLedgerRunningMinor( (int) $player->id, $reasonFilter, $this->playerCreditService->availableCreditMinor($player, $currency), - $skipRows, + $priorCreditRows, ); - $items = $paginator->getCollection() - ->map(function (object $row) use (&$runningMinor, $player, $currency): array { + $items = $pageRows + ->map(function (object $entry) use (&$runningMinor, $player, $currency): array { + if ($entry->source === 'rebate') { + return $this->formatPlayerRebateRow($entry->row, $player, $currency); + } + + $row = $entry->row; $amount = (int) $row->amount; $reason = (string) $row->reason; $affectsBalance = $this->creditReasonAffectsAvailableBalance($reason); @@ -462,9 +483,9 @@ final class PlayerLedgerLogsService return [ 'items' => $items, - 'total' => $paginator->total(), - 'page' => $paginator->currentPage(), - 'per_page' => $paginator->perPage(), + 'total' => $total, + 'page' => $page, + 'per_page' => $perPage, ]; } @@ -501,6 +522,18 @@ final class PlayerLedgerLogsService return $this->resolveTypeFilterMap($raw, self::CREDIT_TYPE_TO_REASON); } + private function creditFilterIncludesRebates(string $raw): bool + { + $raw = trim($raw); + if ($raw === '') { + return true; + } + + $parts = array_filter(array_map('trim', explode(',', $raw))); + + return in_array('rebate', array_map(Str::lower(...), $parts), true); + } + /** * @param array> $map * @return list|null @@ -605,6 +638,48 @@ final class PlayerLedgerLogsService ]; } + private function rebateRecordRows(int $playerId) + { + return DB::table('rebate_records') + ->where('player_id', $playerId) + ->whereNull('reversal_of_id') + ->orderByDesc('created_at') + ->orderByDesc('id') + ->get(); + } + + /** + * @return array + */ + private function formatPlayerRebateRow(object $row, Player $player, string $currency): array + { + $amount = (int) $row->rebate_amount; + + return [ + 'log_id' => 'RB-'.$row->id, + 'type' => 'rebate', + 'biz_type' => 'rebate_'.$row->status, + 'amount' => $amount, + 'amount_formatted' => CurrencyFormatter::fromMinor(abs($amount)), + 'amount_abs' => abs($amount), + 'amount_abs_formatted' => CurrencyFormatter::fromMinor(abs($amount)), + 'direction' => $amount >= 0 ? 'in' : 'out', + 'currency_code' => $currency, + 'balance_after' => null, + 'balance_after_formatted' => null, + 'affects_available_credit' => false, + 'ref_id' => 'ticket_item#'.$row->ticket_item_id, + 'idempotent_key' => null, + 'external_ref_no' => null, + 'status' => (string) $row->status, + 'remark' => 'period_rebate', + 'created_at' => $this->isoTimestamp($row->created_at ?? null), + 'ledger_source' => 'rebate_record', + 'funding_mode' => (string) ($player->funding_mode ?? PlayerFundingMode::CREDIT), + 'auth_source' => $player->auth_source, + ]; + } + /** 账期收付记账不改变 player_credit_accounts,不参与可用信用倒推。 */ private function creditReasonAffectsAvailableBalance(string $reason): bool { @@ -702,10 +777,9 @@ final class PlayerLedgerLogsService private function creditReasonToPublicType(string $reason): string { return match ($reason) { - 'bet_hold', 'game_settlement_loss' => 'bet', - 'bet_hold_release', 'game_settlement_reversal' => 'reversal', - 'settlement_confirm' => 'refund', - 'game_settlement_win' => 'win_credit', + 'bet_hold' => 'bet', + 'bet_hold_release', 'game_settlement_loss', 'game_settlement_win', 'game_settlement_reversal' => 'game_settlement', + 'settlement_confirm' => 'bill_settlement', 'settlement_payout' => 'bill_settlement', default => $reason, }; diff --git a/tests/Feature/AdminCreditLedgerFilterTest.php b/tests/Feature/AdminCreditLedgerFilterTest.php index 548a2a7..c767527 100644 --- a/tests/Feature/AdminCreditLedgerFilterTest.php +++ b/tests/Feature/AdminCreditLedgerFilterTest.php @@ -94,6 +94,64 @@ test('credit ledger biz_type payment_record excludes credit rows', function (): ->assertJsonPath('data.items.0.entry_kind', 'credit'); }); +test('credit ledger accepts public game settlement filter', function (): void { + $site = DB::table('admin_sites')->where('is_default', true)->first(); + $siteId = (int) $site->id; + $siteCode = (string) $site->code; + + $player = Player::query()->create([ + 'site_code' => $siteCode, + 'site_player_id' => 'native:game-settlement-filter', + 'funding_mode' => PlayerFundingMode::CREDIT, + 'username' => 'game_settlement_filter_user', + 'default_currency' => 'NPR', + 'status' => 0, + ]); + + DB::table('credit_ledger')->insert([ + [ + 'owner_type' => 'player', + 'owner_id' => $player->id, + 'amount' => -100, + 'reason' => 'bet_hold', + 'created_at' => now()->subMinute(), + 'updated_at' => now()->subMinute(), + ], + [ + 'owner_type' => 'player', + 'owner_id' => $player->id, + 'amount' => 100, + 'reason' => 'bet_hold_release', + 'created_at' => now(), + 'updated_at' => now(), + ], + [ + 'owner_type' => 'player', + 'owner_id' => $player->id, + 'amount' => 200, + 'reason' => 'game_settlement_win', + 'created_at' => now(), + 'updated_at' => now(), + ], + ]); + + $admin = AdminUser::query()->create([ + 'username' => 'ledger_game_settlement_super', + 'name' => 'Game Settlement Ledger', + 'email' => null, + 'password' => Hash::make('secret-strong'), + 'status' => 0, + ]); + grantSuperAdminRole($admin); + $token = $admin->createToken('test', ['*'], now()->addDay())->plainTextToken; + + $this->withHeader('Authorization', 'Bearer '.$token) + ->getJson('/api/v1/admin/credit-ledger?admin_site_id='.$siteId.'&reason=game_settlement') + ->assertOk() + ->assertJsonPath('data.total', 2) + ->assertJsonPath('data.items.0.entry_kind', 'credit'); +}); + test('credit ledger index includes payment on agent bill', function (): void { $site = DB::table('admin_sites')->where('is_default', true)->first(); $siteId = (int) $site->id; diff --git a/tests/Feature/AdminWalletApiTest.php b/tests/Feature/AdminWalletApiTest.php index 33c1bf1..855a181 100644 --- a/tests/Feature/AdminWalletApiTest.php +++ b/tests/Feature/AdminWalletApiTest.php @@ -352,6 +352,66 @@ test('admin lists wallet transactions and filters abnormal', function (): void { ->assertJsonPath('data.items.0.status', 'pending_reconcile'); }); +test('admin wallet transactions accepts public flow type filters', function (): void { + $token = makeAdminToken(); + + $player = Player::query()->create([ + 'site_code' => 'main', + 'site_player_id' => 'public-flow-filter', + 'username' => null, + 'nickname' => null, + 'default_currency' => 'NPR', + 'status' => 0, + ]); + + $wallet = PlayerWallet::query()->create([ + 'player_id' => $player->id, + 'wallet_type' => 'lottery', + 'currency_code' => 'NPR', + 'balance' => 5000, + 'frozen_balance' => 0, + 'status' => 0, + 'version' => 1, + ]); + + foreach ([ + ['WX_admin_bet', 'bet_deduct', 2], + ['WX_admin_prize', 'settle_payout', 1], + ['WX_admin_reversal', 'bet_reverse', 1], + ] as [$txnNo, $bizType, $direction]) { + WalletTxn::query()->create([ + 'txn_no' => $txnNo, + 'player_id' => $player->id, + 'wallet_id' => $wallet->id, + 'biz_type' => $bizType, + 'biz_no' => $txnNo, + 'direction' => $direction, + 'amount' => 100, + 'balance_before' => 1000, + 'balance_after' => 1000, + 'status' => 'posted', + ]); + } + + $this->withHeader('Authorization', 'Bearer '.$token) + ->getJson('/api/v1/admin/wallet/transactions?biz_type=bet') + ->assertOk() + ->assertJsonPath('data.total', 1) + ->assertJsonPath('data.items.0.biz_type', 'bet_deduct'); + + $this->withHeader('Authorization', 'Bearer '.$token) + ->getJson('/api/v1/admin/wallet/transactions?biz_type=prize') + ->assertOk() + ->assertJsonPath('data.total', 1) + ->assertJsonPath('data.items.0.biz_type', 'settle_payout'); + + $this->withHeader('Authorization', 'Bearer '.$token) + ->getJson('/api/v1/admin/wallet/transactions?biz_type=reversal') + ->assertOk() + ->assertJsonPath('data.total', 1) + ->assertJsonPath('data.items.0.biz_type', 'bet_reverse'); +}); + test('admin transfer reverse is idempotent under concurrent reconcile', function (): void { $player = Player::query()->create([ 'site_code' => 'main', diff --git a/tests/Feature/CreditWalletLogsTest.php b/tests/Feature/CreditWalletLogsTest.php index 907b3f4..06d8752 100644 --- a/tests/Feature/CreditWalletLogsTest.php +++ b/tests/Feature/CreditWalletLogsTest.php @@ -106,7 +106,7 @@ test('credit player wallet logs distinguish win credit from bill settlement', fu $response->assertOk() ->assertJsonPath('data.total', 2) - ->assertJsonPath('data.items.0.type', 'win_credit') + ->assertJsonPath('data.items.0.type', 'game_settlement') ->assertJsonPath('data.items.0.biz_type', 'game_settlement_win') ->assertJsonPath('data.items.0.affects_available_credit', true) ->assertJsonPath('data.items.1.type', 'bill_settlement') @@ -126,13 +126,105 @@ test('credit player wallet logs distinguish win credit from bill settlement', fu ->getJson('/api/v1/wallet/logs?type=win_credit') ->assertOk() ->assertJsonPath('data.total', 1) - ->assertJsonPath('data.items.0.type', 'win_credit'); + ->assertJsonPath('data.items.0.type', 'game_settlement'); $this->withHeader('Authorization', 'Bearer dev:'.$player->id) ->getJson('/api/v1/wallet/logs?type=credit_release') ->assertOk() ->assertJsonPath('data.total', 1) - ->assertJsonPath('data.items.0.type', 'win_credit'); + ->assertJsonPath('data.items.0.type', 'game_settlement'); + + $this->withHeader('Authorization', 'Bearer dev:'.$player->id) + ->getJson('/api/v1/wallet/logs?type=game_settlement') + ->assertOk() + ->assertJsonPath('data.total', 1) + ->assertJsonPath('data.items.0.type', 'game_settlement'); +}); + +test('credit player wallet logs include period rebate records without affecting available credit', function (): void { + $player = Player::query()->create([ + 'site_code' => 'default_site', + 'site_player_id' => 'native:logs-rebate', + 'auth_source' => PlayerAuthSource::LOTTERY_NATIVE, + 'funding_mode' => PlayerFundingMode::CREDIT, + 'username' => 'credit_logs_rebate', + 'default_currency' => 'NPR', + 'status' => 0, + ]); + + DB::table('player_credit_accounts')->insert([ + 'player_id' => $player->id, + 'credit_limit' => 500, + 'used_credit' => 0, + 'frozen_credit' => 0, + 'created_at' => now(), + 'updated_at' => now(), + ]); + + $drawId = (int) DB::table('draws')->insertGetId([ + 'draw_no' => 'RB-LOG-001', + 'business_date' => now()->toDateString(), + 'sequence_no' => 1, + 'status' => 'settled', + 'created_at' => now(), + 'updated_at' => now(), + ]); + $orderId = (int) DB::table('ticket_orders')->insertGetId([ + 'order_no' => 'RB-LOG-ORDER-001', + 'player_id' => $player->id, + 'draw_id' => $drawId, + 'currency_code' => 'NPR', + 'status' => 'settled', + 'created_at' => now(), + 'updated_at' => now(), + ]); + $ticketItemId = (int) DB::table('ticket_items')->insertGetId([ + 'ticket_no' => 'RB-LOG-TICKET-001', + 'order_id' => $orderId, + 'player_id' => $player->id, + 'draw_id' => $drawId, + 'normalized_number' => '1234', + 'play_code' => '4d', + 'total_bet_amount' => 10000, + 'status' => 'settled', + 'created_at' => now(), + 'updated_at' => now(), + ]); + + DB::table('rebate_records')->insert([ + 'player_id' => $player->id, + 'ticket_item_id' => $ticketItemId, + 'game_type' => '*', + 'valid_bet_amount' => 10000, + 'rebate_rate' => 0.01, + 'rebate_amount' => 100, + 'rebate_type' => 'basic', + 'owner_agent_id' => null, + 'status' => 'accrued', + 'created_at' => now(), + 'updated_at' => now(), + ]); + + $this->withHeader('Authorization', 'Bearer dev:'.$player->id) + ->getJson('/api/v1/wallet/logs?page=1&size=10') + ->assertOk() + ->assertJsonPath('data.total', 1) + ->assertJsonPath('data.items.0.type', 'rebate') + ->assertJsonPath('data.items.0.biz_type', 'rebate_accrued') + ->assertJsonPath('data.items.0.ledger_source', 'rebate_record') + ->assertJsonPath('data.items.0.affects_available_credit', false) + ->assertJsonPath('data.items.0.balance_after', null); + + $this->withHeader('Authorization', 'Bearer dev:'.$player->id) + ->getJson('/api/v1/wallet/logs?type=rebate') + ->assertOk() + ->assertJsonPath('data.total', 1) + ->assertJsonPath('data.items.0.type', 'rebate'); + + $this->withHeader('Authorization', 'Bearer dev:'.$player->id) + ->getJson('/api/v1/wallet/logs?type=bet') + ->assertOk() + ->assertJsonPath('data.total', 0); }); test('credit wallet logs page 2 balance_after continues from page 1 baseline', function (): void {