seed(CurrencySeeder::class); $this->seed(PlayTypeSeeder::class); $this->seed(OperationalConfigV1Seeder::class); }); function mintConfigAdminToken(): string { $admin = AdminUser::query()->create([ 'username' => 'config_admin', 'name' => 'Config QA', 'email' => null, 'password' => Hash::make('secret-strong'), 'status' => 0, ]); grantSuperAdminRole($admin); return $admin->createToken('test', ['*'], now()->addDay())->plainTextToken; } function oddsPutPayloadFromDetail(array $items): array { return collect($items)->map(fn (array $r) => [ 'play_code' => $r['play_code'], 'prize_scope' => $r['prize_scope'], 'odds_value' => (int) $r['odds_value'], 'rebate_rate' => (float) $r['rebate_rate'], 'commission_rate' => (float) $r['commission_rate'], 'currency_code' => $r['currency_code'], 'extra_config_json' => $r['extra_config_json'] ?? null, ])->all(); } test('play effective catalog is public and merged', function (): void { $resp = $this->getJson('/api/v1/play/effective?currency=NPR'); $resp->assertOk()->assertJsonPath('data.currency_code', 'NPR'); $plays = $resp->json('data.plays'); expect($plays)->toBeArray()->not->toBeEmpty(); expect($plays[0])->toHaveKeys(['play_code', 'config', 'odds', 'master_enabled']); }); test('admin play config draft publish flow', function (): void { $token = mintConfigAdminToken(); $active = PlayConfigVersion::query()->where('status', ConfigVersionStatus::Active->value)->firstOrFail(); $create = $this->postJson('/api/v1/admin/config/play-versions', [ 'reason' => 'test draft', ], ['Authorization' => 'Bearer '.$token]); $create->assertOk(); $draftId = (int) $create->json('data.id'); expect($draftId)->toBeGreaterThan(0); expect($create->json('data.status'))->toBe(ConfigVersionStatus::Draft->value); $types = PlayType::query()->orderBy('play_code')->get(); $itemPayload = []; foreach ($types as $t) { $itemPayload[] = [ 'play_code' => $t->play_code, 'is_enabled' => true, 'min_bet_amount' => 200, 'max_bet_amount' => 400_000_000, 'display_order' => (int) $t->sort_order, ]; } $this->putJson( '/api/v1/admin/config/play-versions/'.$draftId.'/items', ['items' => $itemPayload], ['Authorization' => 'Bearer '.$token], )->assertOk()->assertJsonPath('data.items.0.min_bet_amount', 200); $this->postJson( '/api/v1/admin/config/play-versions/'.$draftId.'/publish', [], ['Authorization' => 'Bearer '.$token], )->assertOk()->assertJsonPath('data.status', ConfigVersionStatus::Active->value); $active->refresh(); expect($active->status)->toBe(ConfigVersionStatus::Archived->value); expect(PlayConfigVersion::query()->where('status', ConfigVersionStatus::Active->value)->count())->toBe(1); }); test('admin play config publish rejects empty draft items', function (): void { $token = mintConfigAdminToken(); $create = $this->postJson('/api/v1/admin/config/play-versions', [ 'reason' => 'empty draft', ], ['Authorization' => 'Bearer '.$token]); $create->assertOk(); $draftId = (int) $create->json('data.id'); DB::table('play_config_items')->where('version_id', $draftId)->delete(); $this->postJson( '/api/v1/admin/config/play-versions/'.$draftId.'/publish', [], ['Authorization' => 'Bearer '.$token], )->assertStatus(422); }); test('admin play-types requires authentication', function (): void { $this->getJson('/api/v1/admin/play-types')->assertUnauthorized(); }); test('admin cannot delete active play config version', function (): void { $token = mintConfigAdminToken(); $active = PlayConfigVersion::query()->where('status', ConfigVersionStatus::Active->value)->firstOrFail(); $this->deleteJson('/api/v1/admin/config/play-versions/'.$active->id, [], [ 'Authorization' => 'Bearer '.$token, ]) ->assertStatus(400) ->assertJsonPath('code', ErrorCode::ConfigVersionCannotDeleteActive->value); }); test('admin can delete draft play config version', function (): void { $token = mintConfigAdminToken(); $create = $this->postJson('/api/v1/admin/config/play-versions', [ 'reason' => 'to delete', ], ['Authorization' => 'Bearer '.$token]); $create->assertOk(); $draftId = (int) $create->json('data.id'); $this->deleteJson('/api/v1/admin/config/play-versions/'.$draftId, [], [ 'Authorization' => 'Bearer '.$token, ]) ->assertOk() ->assertJsonPath('data.deleted', true); expect(PlayConfigVersion::query()->whereKey($draftId)->exists())->toBeFalse(); }); test('admin can delete draft odds version', function (): void { $token = mintConfigAdminToken(); $create = $this->postJson('/api/v1/admin/config/odds-versions', [ 'reason' => 'to delete', ], ['Authorization' => 'Bearer '.$token]); $create->assertOk(); $draftId = (int) $create->json('data.id'); $this->deleteJson('/api/v1/admin/config/odds-versions/'.$draftId, [], [ 'Authorization' => 'Bearer '.$token, ]) ->assertOk() ->assertJsonPath('data.deleted', true); expect(OddsVersion::query()->whereKey($draftId)->exists())->toBeFalse(); }); test('admin can delete draft risk cap version', function (): void { $token = mintConfigAdminToken(); $create = $this->postJson('/api/v1/admin/config/risk-cap-versions', [ 'reason' => 'to delete', ], ['Authorization' => 'Bearer '.$token]); $create->assertOk(); $draftId = (int) $create->json('data.id'); $this->deleteJson('/api/v1/admin/config/risk-cap-versions/'.$draftId, [], [ 'Authorization' => 'Bearer '.$token, ]) ->assertOk() ->assertJsonPath('data.deleted', true); expect(RiskCapVersion::query()->whereKey($draftId)->exists())->toBeFalse(); }); test('admin odds config rejects out of range rebate rate', function (): void { $token = mintConfigAdminToken(); $create = $this->postJson('/api/v1/admin/config/odds-versions', [ 'reason' => 'rate bound', ], ['Authorization' => 'Bearer '.$token]); $create->assertOk(); $draftId = (int) $create->json('data.id'); $detail = $this->getJson('/api/v1/admin/config/odds-versions/'.$draftId, [ 'Authorization' => 'Bearer '.$token, ])->assertOk()->json('data.items'); $payload = oddsPutPayloadFromDetail($detail); $payload[0]['rebate_rate'] = 1.2; $this->putJson( '/api/v1/admin/config/odds-versions/'.$draftId.'/items', ['items' => $payload], ['Authorization' => 'Bearer '.$token], )->assertStatus(422); });