artisan('lottery:admin-auth-sync')->assertExitCode(0); }); function drawViewOnlyToken(): string { $admin = AdminUser::query()->create([ 'username' => 'draw_view_only_admin', 'name' => 'Draw View', 'email' => null, 'password' => Hash::make('secret-strong'), 'status' => 0, ]); $role = AdminRole::query()->create([ 'slug' => 'draw_view_only_role', 'name' => 'Draw view only role', ]); $role->syncLegacyPermissionSlugs(['prd.draw_result.view']); $siteId = AdminUser::defaultAdminSiteId(); $admin->roles()->sync([ (int) $role->id => ['site_id' => $siteId, 'granted_at' => now()], ]); return $admin->fresh()->createToken('test', ['*'], now()->addDay())->plainTextToken; } function drawViewOnlyFixtureDraw(): Draw { return Draw::query()->create([ 'draw_no' => '20260604-099', 'business_date' => '2026-06-04', 'sequence_no' => 99, 'status' => DrawStatus::Settled->value, 'start_time' => now()->subHours(3), 'close_time' => now()->subHours(2), 'draw_time' => now()->subHours(1), 'result_source' => 'rng', 'current_result_version' => 1, 'settle_version' => 1, 'is_reopened' => false, ]); } test('partial draw review codes do not infer manage slug', function (): void { $granted = AdminPermissionBridge::legacySlugsGrantedByMenuActionCodes(['draw.review.publish']); expect($granted)->not->toContain('prd.draw_result.manage'); }); test('draw view only admin profile excludes manage and cannot store draw', function (): void { $admin = AdminUser::query()->create([ 'username' => 'draw_view_only_profile', 'name' => 'Draw View', 'email' => null, 'password' => Hash::make('secret-strong'), 'status' => 0, ]); $role = AdminRole::query()->create([ 'slug' => 'draw_view_only_role_profile', 'name' => 'Draw view only role', ]); $role->syncLegacyPermissionSlugs(['prd.draw_result.view']); $siteId = AdminUser::defaultAdminSiteId(); $admin->roles()->sync([ (int) $role->id => ['site_id' => $siteId, 'granted_at' => now()], ]); $profile = AdminAuthProfile::fromAdmin($admin->fresh()); expect($profile['permissions'])->toContain('prd.draw_result.view') ->not->toContain('prd.draw_result.manage'); $token = $admin->fresh()->createToken('test', ['*'], now()->addDay())->plainTextToken; $this->withHeader('Authorization', 'Bearer '.$token) ->postJson('/api/v1/admin/draws', [ 'admin_site_id' => $siteId, 'draw_no' => 'test-view-only-001', 'start_time' => now()->toIso8601String(), 'close_time' => now()->addHour()->toIso8601String(), 'draw_time' => now()->addHours(2)->toIso8601String(), ]) ->assertForbidden(); }); test('draw view only list and show omit finance and operational fields', function (): void { $token = drawViewOnlyToken(); $draw = drawViewOnlyFixtureDraw(); $listPayload = $this->withHeader('Authorization', 'Bearer '.$token) ->getJson('/api/v1/admin/draws') ->assertOk() ->json('data'); $listRow = collect($listPayload['items'])->firstWhere('id', $draw->id); expect($listRow)->not->toBeNull() ->and($listRow)->not->toHaveKey('total_bet_minor') ->and($listRow)->not->toHaveKey('result_source') ->and($listPayload['capabilities']['can_manage_draw_results'])->toBeFalse() ->and($listPayload['capabilities']['can_view_draw_finance'])->toBeFalse(); $show = $this->withHeader('Authorization', 'Bearer '.$token) ->getJson('/api/v1/admin/draws/'.$draw->id) ->assertOk() ->json('data'); expect($show)->not->toHaveKeys([ 'result_source', 'current_result_version', 'settle_version', 'is_reopened', 'created_at', 'updated_at', ]) ->and($show['result_batch_counts'])->not->toHaveKey('pending_review') ->and($show['result_batch_counts'])->not->toHaveKey('total') ->and($show['capabilities']['can_manage_draw_results'])->toBeFalse(); }); test('draw view only cannot read finance summary and result batches hide ops metadata', function (): void { $token = drawViewOnlyToken(); $draw = drawViewOnlyFixtureDraw(); $this->withHeader('Authorization', 'Bearer '.$token) ->getJson('/api/v1/admin/draws/'.$draw->id.'/finance-summary') ->assertForbidden(); $payload = $this->withHeader('Authorization', 'Bearer '.$token) ->getJson('/api/v1/admin/draws/'.$draw->id.'/result-batches') ->assertOk() ->json('data'); expect($payload['capabilities']['can_manage_draw_results'])->toBeFalse(); foreach ($payload['batches'] as $batch) { expect($batch)->not->toHaveKeys([ 'source_type', 'rng_seed_hash', 'created_by', 'confirmed_by', 'created_at', 'updated_at', ]); } $hasPending = DB::table('draw_result_batches') ->where('draw_id', $draw->id) ->where('status', 'pending_review') ->exists(); if ($hasPending) { $statuses = collect($payload['batches'])->pluck('status')->unique()->all(); expect($statuses)->not->toContain('pending_review'); } });