artisan('lottery:admin-auth-sync')->assertExitCode(0); }); test('settlement periods index includes bill summary per period', function (): void { $siteId = (int) DB::table('admin_sites')->where('is_default', true)->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(), ]); DB::table('settlement_bills')->insert([ [ 'settlement_period_id' => $periodId, 'bill_type' => 'player', 'owner_type' => 'player', 'owner_id' => 1, 'counterparty_type' => 'agent', 'counterparty_id' => 1, 'net_amount' => 1000, 'unpaid_amount' => 1000, 'paid_amount' => 0, 'status' => 'pending_confirm', 'created_at' => now(), 'updated_at' => now(), ], [ 'settlement_period_id' => $periodId, 'bill_type' => 'agent', 'owner_type' => 'agent', 'owner_id' => 1, 'counterparty_type' => 'platform', 'counterparty_id' => 0, 'net_amount' => 5000, 'unpaid_amount' => 5000, 'paid_amount' => 0, 'status' => 'confirmed', 'created_at' => now(), 'updated_at' => now(), ], ]); $admin = AdminUser::query()->create([ 'username' => 'period_summary_admin', 'name' => 'Summary', '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/settlement-periods?admin_site_id='.$siteId) ->assertOk() ->assertJsonPath('data.items.0.summary.player_bills', 1) ->assertJsonPath('data.items.0.summary.agent_bills', 1) ->assertJsonPath('data.items.0.summary.pending_confirm', 1) ->assertJsonPath('data.items.0.summary.awaiting_payment', 1) ->assertJsonPath('data.items.0.summary.total_unpaid', 6000); $service = app(AgentSettlementPeriodSummaryService::class); $summaries = $service->summariesForPeriodIds([$periodId]); expect($summaries[$periodId]['player_bills'])->toBe(1); expect($summaries[$periodId]['agent_bills'])->toBe(1); }); test('pipeline counts respect agent subtree when admin is bound', function (): void { $siteId = (int) DB::table('admin_sites')->where('is_default', true)->value('id'); $siteCode = (string) DB::table('admin_sites')->where('id', $siteId)->value('code'); $rootId = (int) DB::table('agent_nodes')->where('admin_site_id', $siteId)->where('depth', 0)->value('id'); $service = app(\App\Services\Agent\AgentNodeService::class); $super = AdminUser::query()->create([ 'username' => 'pipe_scope_super', 'name' => 'Super', 'email' => null, 'password' => Hash::make('secret-strong'), 'status' => 0, ]); grantSuperAdminRole($super); $branch = $service->createChild($super, [ 'parent_id' => $rootId, 'code' => 'pipe-branch', 'name' => 'Pipeline Branch', ]); $otherBranch = $service->createChild($super, [ 'parent_id' => $rootId, 'code' => 'pipe-other', 'name' => 'Pipeline Other', ]); $periodStart = now()->subDay()->toDateString(); $periodEnd = now()->addDay()->toDateString(); $periodId = (int) DB::table('settlement_periods')->insertGetId([ 'admin_site_id' => $siteId, 'period_start' => $periodStart, 'period_end' => $periodEnd, 'status' => 'open', 'created_at' => now(), 'updated_at' => now(), ]); $period = (object) [ 'id' => $periodId, 'period_start' => $periodStart, 'period_end' => $periodEnd, 'admin_site_id' => $siteId, ]; foreach ([$branch, $otherBranch] as $node) { $player = Player::query()->create([ 'site_code' => $siteCode, 'site_player_id' => 'native:pipe-'.$node->code, 'funding_mode' => PlayerFundingMode::CREDIT, 'username' => 'pipe_'.$node->code, 'default_currency' => 'NPR', 'status' => 0, 'agent_node_id' => $node->id, ]); DB::table('credit_ledger')->insert([ 'owner_type' => 'player', 'owner_id' => $player->id, 'amount' => -50, 'reason' => 'bet_hold', 'created_at' => now(), 'updated_at' => now(), ]); } $operator = AdminUser::query()->create([ 'username' => 'pipe_scope_ops', 'name' => 'Ops', 'email' => null, 'password' => Hash::make('secret-strong'), 'status' => 0, ]); grantPipelineAgentOperator($operator, $branch); $pipeline = app(AgentSettlementPeriodPipelineService::class); $scoped = $pipeline->countsForPeriods(collect([$period]), $operator); $all = $pipeline->countsForPeriods(collect([$period]), null); expect($scoped[$period->id]['credit_ledger_count'])->toBe(1) ->and($all[$period->id]['credit_ledger_count'])->toBe(2); }); test('pipeline game win loss total uses raw platform pnl or agent share profit for viewer', function (): void { $siteId = (int) DB::table('admin_sites')->where('is_default', true)->value('id'); $siteCode = (string) DB::table('admin_sites')->where('id', $siteId)->value('code'); $rootId = (int) DB::table('agent_nodes')->where('admin_site_id', $siteId)->where('depth', 0)->value('id'); $service = app(\App\Services\Agent\AgentNodeService::class); $super = AdminUser::query()->create([ 'username' => 'pipe_profit_super', 'name' => 'Super', 'email' => null, 'password' => Hash::make('secret-strong'), 'status' => 0, ]); grantSuperAdminRole($super); $branch = $service->createChild($super, [ 'parent_id' => $rootId, 'code' => 'pipe-profit-branch', 'name' => 'Profit Branch', ]); $otherBranch = $service->createChild($super, [ 'parent_id' => $rootId, 'code' => 'pipe-profit-other', 'name' => 'Profit Other', ]); $periodStart = now()->subDay()->toDateString(); $periodEnd = now()->addDay()->toDateString(); $periodId = (int) DB::table('settlement_periods')->insertGetId([ 'admin_site_id' => $siteId, 'period_start' => $periodStart, 'period_end' => $periodEnd, 'status' => 'open', 'created_at' => now(), 'updated_at' => now(), ]); $period = (object) [ 'id' => $periodId, 'period_start' => $periodStart, 'period_end' => $periodEnd, 'admin_site_id' => $siteId, ]; $settledAt = now()->toDateTimeString(); foreach ([ [$branch, 300, 700, 1_000, 0], [$otherBranch, 900, 100, -200, 0], ] as [$node, $agentProfit, $platformProfit, $gameWinLoss, $basicRebate]) { $player = Player::query()->create([ 'site_code' => $siteCode, 'site_player_id' => 'native:profit-'.$node->code, 'funding_mode' => PlayerFundingMode::CREDIT, 'username' => 'profit_'.$node->code, 'default_currency' => 'NPR', 'status' => 0, 'agent_node_id' => $node->id, ]); $ticketItemId = createPipelineProfitTicketItem($player, 'T-'.$node->code); DB::table('share_ledger')->insert([ 'ticket_item_id' => $ticketItemId, 'player_id' => $player->id, 'agent_node_id' => $node->id, 'agent_path' => json_encode([$node->id]), 'share_snapshot' => json_encode([ 'total_shares' => [(string) $node->code => 30.0], 'chain_codes' => [(string) $node->code], ]), 'game_win_loss' => $gameWinLoss, 'basic_rebate' => $basicRebate, 'shared_net_win_loss' => $gameWinLoss - $basicRebate, 'allocations_json' => json_encode([ (string) $node->code => $agentProfit, 'platform' => $platformProfit, ]), 'settled_at' => $settledAt, 'created_at' => $settledAt, 'updated_at' => $settledAt, ]); } $operator = AdminUser::query()->create([ 'username' => 'pipe_profit_ops', 'name' => 'Ops', 'email' => null, 'password' => Hash::make('secret-strong'), 'status' => 0, ]); grantPipelineAgentOperator($operator, $branch); $pipeline = app(AgentSettlementPeriodPipelineService::class); $platformView = $pipeline->countsForPeriods(collect([$period]), $super); $agentView = $pipeline->countsForPeriods(collect([$period]), $operator); expect($platformView[$period->id]['win_loss_scope'])->toBe('platform') ->and($platformView[$period->id]['game_win_loss_total'])->toBe(800) ->and($agentView[$period->id]['win_loss_scope'])->toBe('agent') ->and($agentView[$period->id]['game_win_loss_total'])->toBe(300); }); function createPipelineProfitTicketItem(Player $player, string $ticketNo): int { $draw = \App\Models\Draw::query()->create([ 'draw_no' => 'DRAW-'.$ticketNo, 'business_date' => now()->toDateString(), 'sequence_no' => random_int(1, 9999), 'status' => \App\Lottery\DrawStatus::Open->value, 'current_result_version' => 0, 'settle_version' => 0, 'is_reopened' => false, ]); $orderId = (int) DB::table('ticket_orders')->insertGetId([ 'order_no' => 'ORD-'.$ticketNo, 'player_id' => $player->id, 'draw_id' => $draw->id, 'currency_code' => 'NPR', 'total_bet_amount' => 10_000, 'total_rebate_amount' => 0, 'total_actual_deduct' => 10_000, 'total_estimated_payout' => 0, 'status' => 'confirmed', 'submit_source' => 'h5', 'client_trace_id' => null, 'created_at' => now(), 'updated_at' => now(), ]); return (int) DB::table('ticket_items')->insertGetId([ 'ticket_no' => $ticketNo, 'order_id' => $orderId, 'player_id' => $player->id, 'draw_id' => $draw->id, 'original_number' => null, 'normalized_number' => '1234', 'play_code' => 'big', 'dimension' => 2, 'digit_slot' => null, 'bet_mode' => null, 'unit_bet_amount' => 10_000, 'total_bet_amount' => 10_000, 'rebate_rate_snapshot' => 0, 'commission_rate_snapshot' => 0, 'actual_deduct_amount' => 10_000, 'odds_snapshot_json' => null, 'rule_snapshot_json' => null, 'combination_count' => 1, 'estimated_max_payout' => 0, 'risk_locked_amount' => 0, 'status' => 'settled_lose', 'win_amount' => 0, 'jackpot_win_amount' => 0, ]); } function grantPipelineAgentOperator(AdminUser $admin, \App\Models\AgentNode $agent): void { $now = now(); $roleId = DB::table('admin_roles')->insertGetId([ 'slug' => 'pipe_ops_'.$admin->id, 'code' => 'pipe_ops_'.$admin->id, 'name' => 'Pipeline Ops', 'status' => 1, 'is_system' => false, 'sort_order' => 0, 'created_at' => $now, 'updated_at' => $now, ]); $actionIds = DB::table('admin_menu_actions') ->whereIn('permission_code', ['agent.node.view']) ->pluck('id'); foreach ($actionIds as $actionId) { DB::table('admin_role_menu_actions')->insert([ 'role_id' => $roleId, 'menu_action_id' => (int) $actionId, ]); } DB::table('admin_user_site_roles')->insert([ 'admin_user_id' => $admin->id, 'site_id' => (int) $agent->admin_site_id, 'role_id' => $roleId, 'granted_at' => $now, ]); DB::table('admin_user_agents')->insert([ 'admin_user_id' => $admin->id, 'agent_node_id' => (int) $agent->id, 'is_primary' => true, 'granted_at' => $now, ]); DB::table('admin_user_agent_roles')->insert([ 'admin_user_id' => $admin->id, 'agent_node_id' => (int) $agent->id, 'role_id' => $roleId, 'granted_at' => $now, ]); }