fix: 增强配置发布校验与关闭玩法清理提示

1. 发布赔率、玩法配置和风控封顶草稿前校验空配置、重复项、金额范围和合法性
2. 限制赔率返水与佣金比例在 0 到 1 之间
3. 投注预览和下单遇到已关闭玩法时返回需清理注项明细
This commit is contained in:
2026-05-16 09:54:47 +08:00
parent 87637f2e4a
commit f7f6c58b02
8 changed files with 299 additions and 6 deletions

View File

@@ -9,6 +9,7 @@ use App\Lottery\ConfigVersionStatus;
use App\Lottery\ErrorCode;
use Database\Seeders\CurrencySeeder;
use Database\Seeders\PlayTypeSeeder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Database\Seeders\OperationalConfigV1Seeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
@@ -35,6 +36,19 @@ function mintConfigAdminToken(): string
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');
@@ -86,6 +100,23 @@ test('admin play config draft publish flow', function (): void {
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();
});
@@ -151,3 +182,24 @@ test('admin can delete draft risk cap version', function (): void {
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);
});

View File

@@ -255,11 +255,45 @@ test('ticket place rejects disabled play from active catalog', function (): void
])
->assertStatus(400)
->assertJsonPath('code', ErrorCode::PlayModeClosed->value)
->assertJsonPath('msg', __('wallet.2002', [], 'zh'));
->assertJsonPath('msg', __('wallet.2002', [], 'zh'))
->assertJsonPath('data.cleanup_hint', '玩法已关闭,相关注项已清理')
->assertJsonPath('data.cleanup_lines.0.client_line_no', 1)
->assertJsonPath('data.cleanup_lines.0.play_code', 'big');
expect(TicketOrder::query()->count())->toBe(0);
});
test('ticket preview returns cleanup hint when draft contains closed play', function (): void {
$player = ticketPlayerWithWallet();
ticketOpenDraw();
$versionId = PlayConfigVersion::query()
->where('status', ConfigVersionStatus::Active->value)
->value('id');
expect($versionId)->not->toBeNull();
PlayConfigItem::query()
->where('version_id', $versionId)
->where('play_code', 'big')
->update(['is_enabled' => false]);
$this->withHeader('Authorization', 'Bearer dev:'.$player->id)
->withHeader('X-Locale', 'zh')
->postJson('/api/v1/ticket/preview', [
'draw_id' => '20260511-001',
'currency_code' => 'NPR',
'client_trace_id' => 'trace-preview-closed-play',
'lines' => [
['number' => '1234', 'play_code' => 'big', 'amount' => 10_000],
],
])
->assertStatus(400)
->assertJsonPath('code', ErrorCode::PlayModeClosed->value)
->assertJsonPath('msg', __('wallet.2002', [], 'zh'))
->assertJsonPath('data.cleanup_hint', '玩法已关闭,相关注项已清理')
->assertJsonPath('data.cleanup_lines.0.client_line_no', 1)
->assertJsonPath('data.cleanup_lines.0.play_code', 'big');
});
test('ticket place rejects bet amount below configured minimum', function (): void {
$player = ticketPlayerWithWallet();
ticketOpenDraw();