create([ 'username' => 'risk_pool_admin', 'name' => 'Risk QA', 'email' => null, 'password' => Hash::make('secret-strong'), 'status' => 0, ]); grantSuperAdminRole($admin); return $admin->createToken('test', ['*'], now()->addDay())->plainTextToken; } test('admin risk pools index returns rows for draw', function (): void { $draw = Draw::query()->create([ 'draw_no' => '20260512-001', 'business_date' => '2026-05-12', 'sequence_no' => 1, 'status' => 'open', 'start_time' => now()->subHour(), 'close_time' => now()->addHour(), 'draw_time' => now()->addHours(2), 'cooling_end_time' => null, 'result_source' => null, 'current_result_version' => 0, 'settle_version' => 0, 'is_reopened' => false, ]); RiskPool::query()->create([ 'draw_id' => $draw->id, 'normalized_number' => '1234', 'total_cap_amount' => 1_000_000, 'locked_amount' => 200_000, 'remaining_amount' => 800_000, 'sold_out_status' => 0, 'version' => 1, ]); RiskPool::query()->create([ 'draw_id' => $draw->id, 'normalized_number' => '9999', 'total_cap_amount' => 100, 'locked_amount' => 100, 'remaining_amount' => 0, 'sold_out_status' => 1, 'version' => 2, ]); $token = mintRiskAdminToken(); $this->withHeader('Authorization', 'Bearer '.$token) ->getJson('/api/v1/admin/draws/'.$draw->id.'/risk-pools?per_page=10') ->assertOk() ->assertJsonPath('data.draw_no', '20260512-001') ->assertJsonPath('data.meta.total', 2); $this->withHeader('Authorization', 'Bearer '.$token) ->getJson('/api/v1/admin/draws/'.$draw->id.'/risk-pools?sold_out_only=1') ->assertOk() ->assertJsonPath('data.meta.total', 1) ->assertJsonPath('data.items.0.normalized_number', '9999') ->assertJsonPath('data.items.0.is_sold_out', true); }); test('admin risk pools index filters by number and high risk usage', function (): void { $draw = Draw::query()->create([ 'draw_no' => '20260512-004', 'business_date' => '2026-05-12', 'sequence_no' => 4, 'status' => 'open', 'start_time' => now()->subHour(), 'close_time' => now()->addHour(), 'draw_time' => now()->addHours(2), 'cooling_end_time' => null, 'result_source' => null, 'current_result_version' => 0, 'settle_version' => 0, 'is_reopened' => false, ]); RiskPool::query()->create([ 'draw_id' => $draw->id, 'normalized_number' => '1288', 'total_cap_amount' => 1_000, 'locked_amount' => 850, 'remaining_amount' => 150, 'sold_out_status' => 0, 'version' => 1, ]); RiskPool::query()->create([ 'draw_id' => $draw->id, 'normalized_number' => '5678', 'total_cap_amount' => 1_000, 'locked_amount' => 100, 'remaining_amount' => 900, 'sold_out_status' => 0, 'version' => 1, ]); $token = mintRiskAdminToken(); $this->withHeader('Authorization', 'Bearer '.$token) ->getJson('/api/v1/admin/draws/'.$draw->id.'/risk-pools?high_risk_only=1') ->assertOk() ->assertJsonPath('data.meta.total', 1) ->assertJsonPath('data.items.0.normalized_number', '1288'); $this->withHeader('Authorization', 'Bearer '.$token) ->getJson('/api/v1/admin/draws/'.$draw->id.'/risk-pools?normalized_number=67') ->assertOk() ->assertJsonPath('data.meta.total', 1) ->assertJsonPath('data.items.0.normalized_number', '5678'); }); test('admin can manually close and recover a risk pool number', function (): void { $draw = Draw::query()->create([ 'draw_no' => '20260512-005', 'business_date' => '2026-05-12', 'sequence_no' => 5, 'status' => 'open', 'start_time' => now()->subHour(), 'close_time' => now()->addHour(), 'draw_time' => now()->addHours(2), 'cooling_end_time' => null, 'result_source' => null, 'current_result_version' => 0, 'settle_version' => 0, 'is_reopened' => false, ]); RiskPool::query()->create([ 'draw_id' => $draw->id, 'normalized_number' => '2468', 'total_cap_amount' => 1_000, 'locked_amount' => 300, 'remaining_amount' => 700, 'sold_out_status' => 0, 'version' => 1, ]); $token = mintRiskAdminToken(); $this->withHeader('Authorization', 'Bearer '.$token) ->postJson('/api/v1/admin/draws/'.$draw->id.'/risk-pools/2468/manual-close') ->assertOk() ->assertJsonPath('data.normalized_number', '2468') ->assertJsonPath('data.is_sold_out', true) ->assertJsonPath('data.version', 2); $this->assertDatabaseHas('risk_pool_lock_logs', [ 'draw_id' => $draw->id, 'normalized_number' => '2468', 'action_type' => 'close', 'amount' => 0, 'source_reason' => 'admin_manual_close', ]); expect(fn () => app(RiskPoolService::class)->preview($draw->id, [['number_4d' => '2468', 'amount' => 1]])) ->toThrow(TicketOperationException::class, 'risk_sold_out'); expect(fn () => app(RiskPoolService::class)->acquire($draw->id, null, [['number_4d' => '2468', 'amount' => 1]])) ->toThrow(TicketOperationException::class, 'risk_sold_out'); }); test('admin can recover a manually closed risk pool number', function (): void { $draw = Draw::query()->create([ 'draw_no' => '20260512-006', 'business_date' => '2026-05-12', 'sequence_no' => 6, 'status' => 'open', 'start_time' => now()->subHour(), 'close_time' => now()->addHour(), 'draw_time' => now()->addHours(2), 'cooling_end_time' => null, 'result_source' => null, 'current_result_version' => 0, 'settle_version' => 0, 'is_reopened' => false, ]); RiskPool::query()->create([ 'draw_id' => $draw->id, 'normalized_number' => '2468', 'total_cap_amount' => 1_000, 'locked_amount' => 300, 'remaining_amount' => 700, 'sold_out_status' => 1, 'version' => 2, ]); $token = mintRiskAdminToken(); $this->withHeader('Authorization', 'Bearer '.$token) ->postJson('/api/v1/admin/draws/'.$draw->id.'/risk-pools/2468/recover') ->assertOk() ->assertJsonPath('data.normalized_number', '2468') ->assertJsonPath('data.is_sold_out', false) ->assertJsonPath('data.version', 3); $this->assertDatabaseHas('risk_pool_lock_logs', [ 'draw_id' => $draw->id, 'normalized_number' => '2468', 'action_type' => 'recover', 'amount' => 0, 'source_reason' => 'admin_manual_recover', ]); expect(app(RiskPoolService::class)->acquire($draw->id, null, [['number_4d' => '2468', 'amount' => 1]])) ->toBe(1); }); test('admin risk pool lock logs include ticket_no when linked', function (): void { $draw = Draw::query()->create([ 'draw_no' => '20260512-002', 'business_date' => '2026-05-12', 'sequence_no' => 2, 'status' => 'open', 'start_time' => now()->subHour(), 'close_time' => now()->addHour(), 'draw_time' => now()->addHours(2), 'cooling_end_time' => null, 'result_source' => null, 'current_result_version' => 0, 'settle_version' => 0, 'is_reopened' => false, ]); RiskPoolLockLog::query()->create([ 'draw_id' => $draw->id, 'normalized_number' => '5678', 'ticket_item_id' => null, 'action_type' => 'lock', 'amount' => 50, 'source_reason' => 'ticket_place', 'created_at' => now(), ]); $token = mintRiskAdminToken(); $this->withHeader('Authorization', 'Bearer '.$token) ->getJson('/api/v1/admin/draws/'.$draw->id.'/risk-pool-lock-logs') ->assertOk() ->assertJsonPath('data.meta.total', 1) ->assertJsonPath('data.items.0.amount', 50); }); test('admin risk pool show 404 when pool missing', function (): void { $draw = Draw::query()->create([ 'draw_no' => '20260512-003', 'business_date' => '2026-05-12', 'sequence_no' => 3, 'status' => 'open', 'start_time' => now()->subHour(), 'close_time' => now()->addHour(), 'draw_time' => now()->addHours(2), 'cooling_end_time' => null, 'result_source' => null, 'current_result_version' => 0, 'settle_version' => 0, 'is_reopened' => false, ]); $token = mintRiskAdminToken(); $this->withHeader('Authorization', 'Bearer '.$token) ->getJson('/api/v1/admin/draws/'.$draw->id.'/risk-pools/0000') ->assertStatus(404); });