artisan('lottery:admin-auth-sync')->assertExitCode(0); }); test('bound agent cannot open or close site settlement period', function (): void { $siteId = (int) DB::table('admin_sites')->where('is_default', true)->value('id'); $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' => 'period_scope_super', 'name' => 'Super', 'email' => null, 'password' => Hash::make('secret-strong'), 'status' => 0, ]); grantSuperAdminRole($super); $branch = $service->createChild($super, [ 'parent_id' => $rootId, 'code' => 'period-scope-branch', 'name' => 'Period Scope Branch', ]); $operator = AdminUser::query()->create([ 'username' => 'period_scope_ops', 'name' => 'Ops', 'email' => null, 'password' => Hash::make('secret-strong'), 'status' => 0, ]); grantPeriodScopeAgentOperator($operator, $branch); $token = $operator->createToken('test', ['*'], now()->addDay())->plainTextToken; $this->withHeader('Authorization', 'Bearer '.$token) ->postJson('/api/v1/admin/settlement-periods', [ 'admin_site_id' => $siteId, 'period_start' => '2026-06-01 00:00:00', 'period_end' => '2026-06-30 23:59:59', ]) ->assertForbidden(); $periodId = (int) DB::table('settlement_periods')->insertGetId([ 'admin_site_id' => $siteId, 'period_start' => now()->subWeek(), 'period_end' => now()->addWeek(), 'status' => 'open', 'created_at' => now(), 'updated_at' => now(), ]); $this->withHeader('Authorization', 'Bearer '.$token) ->postJson('/api/v1/admin/settlement-periods/'.$periodId.'/close') ->assertForbidden(); }); test('unsettled ticket warning uses settled_at when game result is posted', 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'); $player = Player::query()->create([ 'site_code' => $siteCode, 'site_player_id' => 'native:unsettled-scope', 'funding_mode' => PlayerFundingMode::CREDIT, 'username' => 'unsettled_scope', 'default_currency' => 'NPR', 'status' => 0, 'agent_node_id' => $rootId, ]); $periodStart = now()->subDays(3)->toDateString(); $periodEnd = now()->addDay()->toDateString(); $settledAt = now()->subDay(); $draw = Draw::query()->create([ 'draw_no' => 'DRAW-UNSETTLED-SCOPE', 'business_date' => now()->toDateString(), 'sequence_no' => random_int(1, 9999), 'status' => DrawStatus::Open->value, 'current_result_version' => 0, 'settle_version' => 0, 'is_reopened' => false, ]); $orderId = (int) DB::table('ticket_orders')->insertGetId([ 'order_no' => 'ORD-UNSETTLED-SCOPE', 'player_id' => $player->id, 'draw_id' => $draw->id, 'currency_code' => 'NPR', 'total_bet_amount' => 1000, 'total_rebate_amount' => 0, 'total_actual_deduct' => 1000, 'total_estimated_payout' => 0, 'status' => 'confirmed', 'submit_source' => 'h5', 'client_trace_id' => null, 'created_at' => now()->subMonth(), 'updated_at' => now(), ]); DB::table('ticket_items')->insert([ 'ticket_no' => 'T-UNSETTLED-SCOPE', 'order_id' => $orderId, 'player_id' => $player->id, 'draw_id' => $draw->id, 'normalized_number' => '1234', 'play_code' => 'big', 'dimension' => 2, 'unit_bet_amount' => 1000, 'total_bet_amount' => 1000, 'rebate_rate_snapshot' => 0, 'commission_rate_snapshot' => 0, 'actual_deduct_amount' => 1000, 'combination_count' => 1, 'estimated_max_payout' => 0, 'risk_locked_amount' => 0, 'status' => 'pending_payout', 'win_amount' => 5000, 'jackpot_win_amount' => 0, 'created_at' => now()->subMonth(), 'updated_at' => now(), 'settled_at' => $settledAt, 'agent_settled_at' => null, ]); $warning = app(UnsettledTicketPeriodWarning::class); $inPeriod = $warning->countForSite($siteId, $periodStart, $periodEnd); $outPeriod = $warning->countForSite( $siteId, now()->subMonth()->toDateString(), now()->subMonth()->addDay()->toDateString(), ); expect($inPeriod['count'])->toBe(1) ->and($outPeriod['count'])->toBe(0); }); function grantPeriodScopeAgentOperator(AdminUser $admin, \App\Models\AgentNode $agent): void { $now = now(); $roleId = DB::table('admin_roles')->insertGetId([ 'slug' => 'period_scope_ops_'.$admin->id, 'code' => 'period_scope_ops_'.$admin->id, 'name' => 'Period Scope Ops', 'status' => 1, 'is_system' => false, 'sort_order' => 0, 'created_at' => $now, 'updated_at' => $now, ]); $manageActionIds = DB::table('admin_menu_actions') ->whereIn('permission_code', ['prd.settlement.agent.manage', 'settlement.agent.manage']) ->pluck('id'); foreach ($manageActionIds 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, ]); }