seed(CurrencySeeder::class); $this->seed(LotterySettingsSeeder::class); }); test('credit player wallet logs reads credit_ledger not wallet_txns', function (): void { $player = Player::query()->create([ 'site_code' => 'default_site', 'site_player_id' => 'native:logs-1', 'auth_source' => PlayerAuthSource::LOTTERY_NATIVE, 'funding_mode' => PlayerFundingMode::CREDIT, 'username' => 'credit_logs', 'default_currency' => 'NPR', 'status' => 0, ]); DB::table('player_credit_accounts')->insert([ 'player_id' => $player->id, 'credit_limit' => 200, 'used_credit' => 10, 'frozen_credit' => 0, 'created_at' => now(), 'updated_at' => now(), ]); DB::table('credit_ledger')->insert([ 'owner_type' => 'player', 'owner_id' => $player->id, 'amount' => -1000, 'reason' => 'bet_hold', 'ref_type' => 'bet', 'ref_id' => null, '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.ledger_source', 'credit_ledger') ->assertJsonPath('data.funding_mode', PlayerFundingMode::CREDIT) ->assertJsonPath('data.auth_source', PlayerAuthSource::LOTTERY_NATIVE) ->assertJsonPath('data.total', 1) ->assertJsonPath('data.items.0.type', 'bet') ->assertJsonPath('data.items.0.biz_type', 'bet_hold') ->assertJsonPath('data.items.0.ledger_source', 'credit_ledger'); }); test('credit player wallet logs distinguish win credit from bill settlement', function (): void { $player = Player::query()->create([ 'site_code' => 'default_site', 'site_player_id' => 'native:logs-2', 'auth_source' => PlayerAuthSource::LOTTERY_NATIVE, 'funding_mode' => PlayerFundingMode::CREDIT, 'username' => 'credit_logs_2', '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(), ]); DB::table('credit_ledger')->insert([ [ 'owner_type' => 'player', 'owner_id' => $player->id, 'amount' => 3600, 'reason' => 'settlement_payout', 'ref_type' => 'settlement_bill', 'ref_id' => 25, 'created_at' => now()->subMinute(), 'updated_at' => now()->subMinute(), ], [ 'owner_type' => 'player', 'owner_id' => $player->id, 'amount' => 1200, 'reason' => 'game_settlement_win', 'ref_type' => 'ticket_item', 'ref_id' => 99, 'created_at' => now(), 'updated_at' => now(), ], ]); $response = $this->withHeader('Authorization', 'Bearer dev:'.$player->id) ->getJson('/api/v1/wallet/logs?page=1&size=10'); $response->assertOk() ->assertJsonPath('data.total', 2) ->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') ->assertJsonPath('data.items.1.biz_type', 'settlement_payout') ->assertJsonPath('data.items.1.affects_available_credit', false) ->assertJsonPath('data.items.1.balance_after', null); expect($response->json('data.items.0.balance_after'))->not->toBeNull(); $this->withHeader('Authorization', 'Bearer dev:'.$player->id) ->getJson('/api/v1/wallet/logs?type=bill_settlement') ->assertOk() ->assertJsonPath('data.total', 1) ->assertJsonPath('data.items.0.type', 'bill_settlement'); $this->withHeader('Authorization', 'Bearer dev:'.$player->id) ->getJson('/api/v1/wallet/logs?type=win_credit') ->assertOk() ->assertJsonPath('data.total', 1) ->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', '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 wallet logs cap available credit after win above credit limit', function (): void { $player = Player::query()->create([ 'site_code' => 'default_site', 'site_player_id' => 'native:logs-win-cap', 'auth_source' => PlayerAuthSource::LOTTERY_NATIVE, 'funding_mode' => PlayerFundingMode::CREDIT, 'username' => 'credit_logs_win_cap', 'default_currency' => 'NPR', 'status' => 0, ]); DB::table('player_credit_accounts')->insert([ 'player_id' => $player->id, 'credit_limit' => 200, 'used_credit' => 0, 'frozen_credit' => 0, 'created_at' => now(), 'updated_at' => now(), ]); DB::table('credit_ledger')->insert([ [ 'owner_type' => 'player', 'owner_id' => $player->id, 'amount' => -5000, 'reason' => 'bet_hold', 'ref_type' => 'bet', 'ref_id' => 1, 'created_at' => now()->subMinutes(2), 'updated_at' => now()->subMinutes(2), ], [ 'owner_type' => 'player', 'owner_id' => $player->id, 'amount' => 5000, 'reason' => 'bet_hold_release', 'ref_type' => 'ticket_item', 'ref_id' => 1, 'created_at' => now()->subMinute(), 'updated_at' => now()->subMinute(), ], [ 'owner_type' => 'player', 'owner_id' => $player->id, 'amount' => 50000, 'reason' => 'game_settlement_win', 'ref_type' => 'ticket_item', 'ref_id' => 1, 'created_at' => now(), 'updated_at' => now(), ], ]); $data = $this->withHeader('Authorization', 'Bearer dev:'.$player->id) ->getJson('/api/v1/wallet/logs?page=1&size=10') ->assertOk() ->json('data'); expect($data['items'][0]['biz_type'])->toBe('game_settlement_win') ->and($data['items'][0]['balance_after'])->toBe(20000) ->and($data['items'][1]['biz_type'])->toBe('bet_hold_release') ->and($data['items'][1]['balance_after'])->toBe(0) ->and($data['items'][2]['biz_type'])->toBe('bet_hold') ->and($data['items'][2]['balance_after'])->toBe(0); }); 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 { $player = Player::query()->create([ 'site_code' => 'default_site', 'site_player_id' => 'native:logs-page2', 'auth_source' => PlayerAuthSource::LOTTERY_NATIVE, 'funding_mode' => PlayerFundingMode::CREDIT, 'username' => 'credit_logs_page2', 'default_currency' => 'NPR', 'status' => 0, ]); DB::table('player_credit_accounts')->insert([ 'player_id' => $player->id, 'credit_limit' => 500, 'used_credit' => 12, 'frozen_credit' => 0, 'created_at' => now(), 'updated_at' => now(), ]); for ($i = 1; $i <= 12; $i++) { DB::table('credit_ledger')->insert([ 'owner_type' => 'player', 'owner_id' => $player->id, 'amount' => -1000, 'reason' => 'bet_hold', 'ref_type' => 'bet', 'ref_id' => $i, 'created_at' => now()->subMinutes(12 - $i), 'updated_at' => now()->subMinutes(12 - $i), ]); } $page1 = $this->withHeader('Authorization', 'Bearer dev:'.$player->id) ->getJson('/api/v1/wallet/logs?page=1&size=10') ->assertOk() ->json('data'); $page2 = $this->withHeader('Authorization', 'Bearer dev:'.$player->id) ->getJson('/api/v1/wallet/logs?page=2&size=10') ->assertOk() ->json('data'); expect($page1['total'])->toBe(12); expect($page2['items'])->toHaveCount(2); $page1LastBalanceAfter = $page1['items'][9]['balance_after']; $page2FirstBalanceAfter = $page2['items'][0]['balance_after']; // Page 2 must continue the running balance from page 1, capped by credit limit. expect($page2FirstBalanceAfter)->toBe(min($page1LastBalanceAfter + 1000, 50000)); }); test('credit player activity merges hold release loss and rebate into one order result', function (): void { $player = Player::query()->create([ 'site_code' => 'default_site', 'site_player_id' => 'native:activity-loss', 'auth_source' => PlayerAuthSource::LOTTERY_NATIVE, 'funding_mode' => PlayerFundingMode::CREDIT, 'username' => 'credit_activity_loss', 'default_currency' => 'NPR', 'status' => 0, ]); DB::table('player_credit_accounts')->insert([ 'player_id' => $player->id, 'credit_limit' => 500, 'used_credit' => 21, 'frozen_credit' => 0, 'created_at' => now(), 'updated_at' => now(), ]); $drawId = (int) DB::table('draws')->insertGetId([ 'draw_no' => 'ACTIVITY-LOSS-001', 'business_date' => now()->toDateString(), 'sequence_no' => 1, 'status' => 'settled', 'created_at' => now()->subMinutes(10), 'updated_at' => now(), ]); $orderId = (int) DB::table('ticket_orders')->insertGetId([ 'order_no' => 'ORDER-ACTIVITY-LOSS-001', 'player_id' => $player->id, 'draw_id' => $drawId, 'currency_code' => 'NPR', 'total_bet_amount' => 2100, 'total_rebate_amount' => 0, 'total_actual_deduct' => 2100, 'total_estimated_payout' => 50000, 'status' => 'placed', 'created_at' => now()->subMinutes(10), 'updated_at' => now(), ]); $ticketItemId = (int) DB::table('ticket_items')->insertGetId([ 'ticket_no' => 'TICKET-ACTIVITY-LOSS-001', 'order_id' => $orderId, 'player_id' => $player->id, 'draw_id' => $drawId, 'original_number' => '1234', 'normalized_number' => '1234', 'play_code' => '4d', 'total_bet_amount' => 2100, 'actual_deduct_amount' => 2100, 'status' => 'settled_lose', 'win_amount' => 0, 'jackpot_win_amount' => 0, 'settled_at' => now(), 'created_at' => now()->subMinutes(10), 'updated_at' => now(), ]); DB::table('share_ledger')->insert([ 'ticket_item_id' => $ticketItemId, 'player_id' => $player->id, 'agent_node_id' => null, 'game_win_loss' => 2100, 'basic_rebate' => 11, 'shared_net_win_loss' => 2089, 'settled_at' => now(), 'created_at' => now(), 'updated_at' => now(), ]); DB::table('rebate_records')->insert([ 'player_id' => $player->id, 'ticket_item_id' => $ticketItemId, 'game_type' => '4d', 'valid_bet_amount' => 2100, 'rebate_rate' => 0.0052, 'rebate_amount' => 11, 'rebate_type' => 'basic', 'owner_agent_id' => null, 'status' => 'accrued', 'created_at' => now(), 'updated_at' => now(), ]); DB::table('credit_ledger')->insert([ [ 'owner_type' => 'player', 'owner_id' => $player->id, 'amount' => -2100, 'reason' => 'bet_hold', 'ref_type' => 'ticket_order', 'ref_id' => $orderId, 'created_at' => now()->subMinutes(10), 'updated_at' => now()->subMinutes(10), ], [ 'owner_type' => 'player', 'owner_id' => $player->id, 'amount' => 2100, 'reason' => 'bet_hold_release', 'ref_type' => 'ticket_item', 'ref_id' => $ticketItemId, 'created_at' => now(), 'updated_at' => now(), ], [ 'owner_type' => 'player', 'owner_id' => $player->id, 'amount' => -2100, 'reason' => 'game_settlement_loss', 'ref_type' => 'ticket_item', 'ref_id' => $ticketItemId, 'created_at' => now(), 'updated_at' => now(), ], ]); $response = $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.activity_kind', 'draw_result') ->assertJsonPath('data.items.0.biz_type', 'settled_loss') ->assertJsonPath('data.items.0.order_no', 'ORDER-ACTIVITY-LOSS-001') ->assertJsonPath('data.items.0.draw_no', 'ACTIVITY-LOSS-001') ->assertJsonPath('data.items.0.ticket_no', 'TICKET-ACTIVITY-LOSS-001') ->assertJsonPath('data.items.0.stake_amount', 2100) ->assertJsonPath('data.items.0.rebate_amount', 11) ->assertJsonPath('data.items.0.net_amount', -2089) ->assertJsonPath('data.items.0.balance_after', 47900); expect($response->json('data.items'))->toHaveCount(1); $this->withHeader('Authorization', 'Bearer dev:'.$player->id) ->getJson('/api/v1/wallet/logs?type=bet') ->assertOk() ->assertJsonPath('data.total', 0); $newerOrderId = (int) DB::table('ticket_orders')->insertGetId([ 'order_no' => 'ORDER-ACTIVITY-PENDING-001', 'player_id' => $player->id, 'draw_id' => $drawId, 'currency_code' => 'NPR', 'total_bet_amount' => 100, 'total_rebate_amount' => 0, 'total_actual_deduct' => 100, 'total_estimated_payout' => 1000, 'status' => 'placed', 'created_at' => now()->addSecond(), 'updated_at' => now()->addSecond(), ]); DB::table('ticket_items')->insert([ 'ticket_no' => 'TICKET-ACTIVITY-PENDING-001', 'order_id' => $newerOrderId, 'player_id' => $player->id, 'draw_id' => $drawId, 'original_number' => '5678', 'normalized_number' => '5678', 'play_code' => '4d', 'total_bet_amount' => 100, 'actual_deduct_amount' => 100, 'status' => 'pending_draw', 'created_at' => now()->addSecond(), 'updated_at' => now()->addSecond(), ]); DB::table('player_credit_accounts') ->where('player_id', $player->id) ->update(['used_credit' => 22, 'updated_at' => now()]); $this->withHeader('Authorization', 'Bearer dev:'.$player->id) ->getJson('/api/v1/wallet/logs?type=game_settlement') ->assertOk() ->assertJsonPath('data.total', 1) ->assertJsonPath('data.items.0.balance_after', 47900); }); test('credit player activities describe wins refunds and settlement reversals in player terms', function (): void { $player = Player::query()->create([ 'site_code' => 'default_site', 'site_player_id' => 'native:activity-outcomes', 'auth_source' => PlayerAuthSource::LOTTERY_NATIVE, 'funding_mode' => PlayerFundingMode::CREDIT, 'username' => 'credit_activity_outcomes', 'default_currency' => 'NPR', 'status' => 0, ]); DB::table('player_credit_accounts')->insert([ 'player_id' => $player->id, 'credit_limit' => 1000, 'used_credit' => 7, 'frozen_credit' => 0, 'created_at' => now(), 'updated_at' => now(), ]); $drawId = (int) DB::table('draws')->insertGetId([ 'draw_no' => 'ACTIVITY-OUTCOMES-001', 'business_date' => now()->toDateString(), 'sequence_no' => 1, 'status' => 'open', 'created_at' => now(), 'updated_at' => now(), ]); $createOrder = function ( string $orderNo, string $ticketNo, string $orderStatus, string $ticketStatus, int $stake, int $minutesAgo, ) use ($player, $drawId): array { $createdAt = now()->subMinutes($minutesAgo); $orderId = (int) DB::table('ticket_orders')->insertGetId([ 'order_no' => $orderNo, 'player_id' => $player->id, 'draw_id' => $drawId, 'currency_code' => 'NPR', 'total_bet_amount' => $stake, 'total_rebate_amount' => 0, 'total_actual_deduct' => $stake, 'total_estimated_payout' => 5000, 'status' => $orderStatus, 'created_at' => $createdAt, 'updated_at' => $createdAt, ]); $ticketId = (int) DB::table('ticket_items')->insertGetId([ 'ticket_no' => $ticketNo, 'order_id' => $orderId, 'player_id' => $player->id, 'draw_id' => $drawId, 'original_number' => '1234', 'normalized_number' => '1234', 'play_code' => '4d', 'total_bet_amount' => $stake, 'actual_deduct_amount' => $stake, 'status' => $ticketStatus, 'win_amount' => $ticketStatus === 'settled_win' ? 4000 : 0, 'jackpot_win_amount' => 0, 'settled_at' => $ticketStatus === 'settled_win' ? $createdAt : null, 'created_at' => $createdAt, 'updated_at' => $createdAt, ]); return [$orderId, $ticketId]; }; [, $winningTicketId] = $createOrder( 'ORDER-ACTIVITY-WIN-001', 'TICKET-ACTIVITY-WIN-001', 'placed', 'settled_win', 1000, 3, ); DB::table('share_ledger')->insert([ 'ticket_item_id' => $winningTicketId, 'player_id' => $player->id, 'agent_node_id' => null, 'game_win_loss' => -4000, 'basic_rebate' => 0, 'shared_net_win_loss' => -4000, 'settled_at' => now()->subMinutes(3), 'created_at' => now()->subMinutes(3), 'updated_at' => now()->subMinutes(3), ]); $createOrder( 'ORDER-ACTIVITY-REFUND-001', 'TICKET-ACTIVITY-REFUND-001', 'refunded', 'refunded', 500, 2, ); [, $reversedTicketId] = $createOrder( 'ORDER-ACTIVITY-REVERSED-001', 'TICKET-ACTIVITY-REVERSED-001', 'placed', 'pending_draw', 700, 1, ); $originalShareId = (int) DB::table('share_ledger')->insertGetId([ 'ticket_item_id' => $reversedTicketId, 'player_id' => $player->id, 'agent_node_id' => null, 'game_win_loss' => 700, 'basic_rebate' => 0, 'shared_net_win_loss' => 700, 'settled_at' => now()->subMinute(), 'created_at' => now()->subMinute(), 'updated_at' => now()->subMinute(), ]); DB::table('share_ledger')->insert([ 'ticket_item_id' => $reversedTicketId, 'player_id' => $player->id, 'agent_node_id' => null, 'game_win_loss' => -700, 'basic_rebate' => 0, 'shared_net_win_loss' => -700, 'reversal_of_id' => $originalShareId, 'settled_at' => now(), 'created_at' => now(), 'updated_at' => now(), ]); $items = collect($this->withHeader('Authorization', 'Bearer dev:'.$player->id) ->getJson('/api/v1/wallet/logs?page=1&size=10') ->assertOk() ->assertJsonPath('data.total', 3) ->json('data.items')) ->keyBy('order_no'); expect($items['ORDER-ACTIVITY-WIN-001']['activity_kind'])->toBe('draw_result') ->and($items['ORDER-ACTIVITY-WIN-001']['biz_type'])->toBe('settled_win') ->and($items['ORDER-ACTIVITY-WIN-001']['win_amount'])->toBe(4000) ->and($items['ORDER-ACTIVITY-WIN-001']['net_amount'])->toBe(4000) ->and($items['ORDER-ACTIVITY-REFUND-001']['activity_kind'])->toBe('refund') ->and($items['ORDER-ACTIVITY-REFUND-001']['biz_type'])->toBe('bet_refund') ->and($items['ORDER-ACTIVITY-REFUND-001']['net_amount'])->toBe(500) ->and($items['ORDER-ACTIVITY-REVERSED-001']['activity_kind'])->toBe('bet') ->and($items['ORDER-ACTIVITY-REVERSED-001']['activity_status'])->toBe('pending') ->and($items['ORDER-ACTIVITY-REVERSED-001']['net_amount'])->toBe(-700); }); test('credit hold records the real ticket order reference when provided', function (): void { $player = Player::query()->create([ 'site_code' => 'default_site', 'site_player_id' => 'native:hold-order-ref', 'auth_source' => PlayerAuthSource::LOTTERY_NATIVE, 'funding_mode' => PlayerFundingMode::CREDIT, 'username' => 'credit_hold_order_ref', '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(), ]); app(PlayerCreditService::class)->holdForBet($player, 2100, 77); $row = DB::table('credit_ledger')->where('owner_id', $player->id)->first(); expect($row?->ref_type)->toBe('ticket_order') ->and((int) $row?->ref_id)->toBe(77); }); test('credit player activity shows each partial period payment as one player-facing record', function (): void { $player = Player::query()->create([ 'site_code' => 'default_site', 'site_player_id' => 'native:period-payments', 'auth_source' => PlayerAuthSource::LOTTERY_NATIVE, 'funding_mode' => PlayerFundingMode::CREDIT, 'username' => 'credit_period_payments', '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(), ]); $siteId = (int) DB::table('admin_sites')->where('code', 'default_site')->value('id'); $periodId = (int) DB::table('settlement_periods')->insertGetId([ 'admin_site_id' => $siteId, 'period_start' => now()->subWeek(), 'period_end' => now(), 'status' => 'closed', 'created_at' => now(), 'updated_at' => now(), ]); $billId = (int) DB::table('settlement_bills')->insertGetId([ 'settlement_period_id' => $periodId, 'bill_type' => 'player', 'owner_type' => 'player', 'owner_id' => $player->id, 'counterparty_type' => 'agent', 'counterparty_id' => 0, 'net_amount' => 300, 'paid_amount' => 300, 'unpaid_amount' => 0, 'status' => 'settled', 'created_at' => now(), 'updated_at' => now(), ]); DB::table('payment_records')->insert([ [ 'settlement_bill_id' => $billId, 'payer_type' => 'player', 'payer_id' => $player->id, 'payee_type' => 'agent', 'payee_id' => 0, 'amount' => 100, 'status' => 'confirmed', 'confirmed_at' => now()->subMinute(), 'created_at' => now()->subMinute(), 'updated_at' => now()->subMinute(), ], [ 'settlement_bill_id' => $billId, 'payer_type' => 'player', 'payer_id' => $player->id, 'payee_type' => 'agent', 'payee_id' => 0, 'amount' => 200, 'status' => 'confirmed', 'confirmed_at' => now(), 'created_at' => now(), 'updated_at' => now(), ], ]); $this->withHeader('Authorization', 'Bearer dev:'.$player->id) ->getJson('/api/v1/wallet/logs?type=bill_settlement') ->assertOk() ->assertJsonPath('data.total', 2) ->assertJsonPath('data.items.0.activity_kind', 'period_settlement') ->assertJsonPath('data.items.0.biz_type', 'period_paid') ->assertJsonPath('data.items.0.net_amount', -200) ->assertJsonPath('data.items.1.net_amount', -100); }); test('credit player activity paginates after grouping orders', function (): void { $player = Player::query()->create([ 'site_code' => 'default_site', 'site_player_id' => 'native:activity-pages', 'auth_source' => PlayerAuthSource::LOTTERY_NATIVE, 'funding_mode' => PlayerFundingMode::CREDIT, 'username' => 'credit_activity_pages', 'default_currency' => 'NPR', 'status' => 0, ]); DB::table('player_credit_accounts')->insert([ 'player_id' => $player->id, 'credit_limit' => 500, 'used_credit' => 11, 'frozen_credit' => 0, 'created_at' => now(), 'updated_at' => now(), ]); $drawId = (int) DB::table('draws')->insertGetId([ 'draw_no' => 'ACTIVITY-PAGE-001', 'business_date' => now()->toDateString(), 'sequence_no' => 1, 'status' => 'open', 'created_at' => now(), 'updated_at' => now(), ]); for ($i = 1; $i <= 11; $i++) { $placedAt = now()->subMinutes(11 - $i); $orderId = (int) DB::table('ticket_orders')->insertGetId([ 'order_no' => 'ORDER-ACTIVITY-PAGE-'.$i, 'player_id' => $player->id, 'draw_id' => $drawId, 'currency_code' => 'NPR', 'total_bet_amount' => 100, 'total_rebate_amount' => 0, 'total_actual_deduct' => 100, 'total_estimated_payout' => 1000, 'status' => 'placed', 'created_at' => $placedAt, 'updated_at' => $placedAt, ]); DB::table('ticket_items')->insert([ 'ticket_no' => 'TICKET-ACTIVITY-PAGE-'.$i, 'order_id' => $orderId, 'player_id' => $player->id, 'draw_id' => $drawId, 'original_number' => '1234', 'normalized_number' => '1234', 'play_code' => '4d', 'total_bet_amount' => 100, 'actual_deduct_amount' => 100, 'status' => 'pending_draw', 'created_at' => $placedAt, 'updated_at' => $placedAt, ]); } $page1 = $this->withHeader('Authorization', 'Bearer dev:'.$player->id) ->getJson('/api/v1/wallet/logs?page=1&size=10') ->assertOk() ->json('data'); $page2 = $this->withHeader('Authorization', 'Bearer dev:'.$player->id) ->getJson('/api/v1/wallet/logs?page=2&size=10') ->assertOk() ->json('data'); expect($page1['total'])->toBe(11) ->and($page1['items'])->toHaveCount(10) ->and($page2['items'])->toHaveCount(1) ->and($page2['items'][0]['activity_kind'])->toBe('bet'); });