diff --git a/app/Http/Controllers/Api/V1/Admin/Config/PlayConfigItemsReplaceController.php b/app/Http/Controllers/Api/V1/Admin/Config/PlayConfigItemsReplaceController.php index 26120b3..f0113b2 100644 --- a/app/Http/Controllers/Api/V1/Admin/Config/PlayConfigItemsReplaceController.php +++ b/app/Http/Controllers/Api/V1/Admin/Config/PlayConfigItemsReplaceController.php @@ -37,10 +37,18 @@ final class PlayConfigItemsReplaceController extends Controller $data = $request->validate([ 'items' => ['required', 'array', 'min:1'], 'items.*.play_code' => ['required', 'string', 'max:32', Rule::exists('play_types', 'play_code')], + 'items.*.category' => ['required', 'string', 'max:16'], + 'items.*.dimension' => ['nullable', 'integer', 'min:0', 'max:255'], + 'items.*.bet_mode' => ['nullable', 'string', 'max:32'], + 'items.*.display_name_zh' => ['required', 'string', 'max:64'], + 'items.*.display_name_en' => ['nullable', 'string', 'max:64'], + 'items.*.display_name_ne' => ['nullable', 'string', 'max:64'], 'items.*.is_enabled' => ['sometimes', 'boolean'], 'items.*.min_bet_amount' => ['required', 'integer', 'min:0'], 'items.*.max_bet_amount' => ['required', 'integer', 'min:0'], 'items.*.display_order' => ['sometimes', 'integer'], + 'items.*.supports_multi_number' => ['sometimes', 'boolean'], + 'items.*.reserved_rule_json' => ['sometimes', 'nullable', 'array'], 'items.*.rule_text_zh' => ['sometimes', 'nullable', 'string'], 'items.*.rule_text_en' => ['sometimes', 'nullable', 'string'], 'items.*.rule_text_ne' => ['sometimes', 'nullable', 'string'], diff --git a/app/Models/PlayConfigItem.php b/app/Models/PlayConfigItem.php index 0a70617..1019a1d 100644 --- a/app/Models/PlayConfigItem.php +++ b/app/Models/PlayConfigItem.php @@ -11,13 +11,21 @@ final class PlayConfigItem extends Model protected $fillable = [ 'version_id', 'play_code', + 'category', + 'dimension', + 'bet_mode', + 'display_name_zh', + 'display_name_en', + 'display_name_ne', 'is_enabled', 'min_bet_amount', 'max_bet_amount', 'display_order', + 'supports_multi_number', 'rule_text_zh', 'rule_text_en', 'rule_text_ne', + 'reserved_rule_json', 'extra_config_json', ]; @@ -25,10 +33,13 @@ final class PlayConfigItem extends Model { return [ 'version_id' => 'integer', + 'dimension' => 'integer', 'is_enabled' => 'boolean', 'min_bet_amount' => 'integer', 'max_bet_amount' => 'integer', 'display_order' => 'integer', + 'supports_multi_number' => 'boolean', + 'reserved_rule_json' => 'json', 'extra_config_json' => 'json', ]; } diff --git a/app/Models/PlayConfigVersion.php b/app/Models/PlayConfigVersion.php index 7f27849..3a42af9 100644 --- a/app/Models/PlayConfigVersion.php +++ b/app/Models/PlayConfigVersion.php @@ -45,7 +45,9 @@ final class PlayConfigVersion extends Model /** @return HasMany */ public function items(): HasMany { - return $this->hasMany(PlayConfigItem::class, 'version_id'); + return $this->hasMany(PlayConfigItem::class, 'version_id') + ->orderBy('display_order') + ->orderBy('play_code'); } public function isDraft(): bool diff --git a/app/Services/Config/EffectivePlayCatalogService.php b/app/Services/Config/EffectivePlayCatalogService.php index cb44c8b..0df3cb0 100644 --- a/app/Services/Config/EffectivePlayCatalogService.php +++ b/app/Services/Config/EffectivePlayCatalogService.php @@ -4,7 +4,6 @@ namespace App\Services\Config; use App\Models\Currency; use App\Models\OddsItem; -use App\Models\PlayType; use App\Models\OddsVersion; use App\Models\RiskCapItem; use App\Models\PlayConfigItem; @@ -37,8 +36,6 @@ final class EffectivePlayCatalogService ->where('status', ConfigVersionStatus::Active->value) ->firstOrFail(); - $playTypes = PlayType::query()->orderBy('sort_order')->orderBy('play_code')->get(); - /** @var Collection $configByCode */ $configByCode = PlayConfigItem::query() ->where('version_id', $playVersion->id) @@ -58,26 +55,30 @@ final class EffectivePlayCatalogService ->orderBy('normalized_number') ->get(); - $plays = $playTypes->map(function (PlayType $pt) use ($configByCode, $oddsByPlay): array { - $c = $configByCode->get($pt->play_code); - $items = $oddsByPlay->get($pt->play_code, collect()); - $o = $this->pickPrimaryOddsItem($items); + $plays = $configByCode->values() + ->sortBy([ + ['display_order', 'asc'], + ['play_code', 'asc'], + ]) + ->map(function (PlayConfigItem $c) use ($oddsByPlay): array { + $items = $oddsByPlay->get($c->play_code, collect()); + $o = $this->pickPrimaryOddsItem($items); - return [ - 'play_code' => $pt->play_code, - 'category' => $pt->category, - 'dimension' => $pt->dimension, - 'bet_mode' => $pt->bet_mode, - 'display_name_zh' => $pt->display_name_zh, - 'display_name_en' => $pt->display_name_en, - 'display_name_ne' => $pt->display_name_ne, - 'sort_order' => (int) $pt->sort_order, - 'supports_multi_number' => (bool) $pt->supports_multi_number, - 'master_enabled' => (bool) $pt->is_enabled, - 'config' => $c === null ? null : $this->serializePlayConfigItem($c), - 'odds' => $o === null ? null : $this->serializeOddsItem($o), - ]; - })->values()->all(); + return [ + 'play_code' => $c->play_code, + 'category' => $c->category, + 'dimension' => $c->dimension === null ? null : (int) $c->dimension, + 'bet_mode' => $c->bet_mode, + 'display_name_zh' => $c->display_name_zh, + 'display_name_en' => $c->display_name_en, + 'display_name_ne' => $c->display_name_ne, + 'sort_order' => (int) $c->display_order, + 'supports_multi_number' => (bool) $c->supports_multi_number, + 'master_enabled' => (bool) $c->is_enabled, + 'config' => $this->serializePlayConfigItem($c), + 'odds' => $o === null ? null : $this->serializeOddsItem($o), + ]; + })->values()->all(); return [ 'currency_code' => $currency->code, @@ -144,10 +145,18 @@ final class EffectivePlayCatalogService private function serializePlayConfigItem(PlayConfigItem $r): array { return [ + 'category' => $r->category, + 'dimension' => $r->dimension === null ? null : (int) $r->dimension, + 'bet_mode' => $r->bet_mode, + 'display_name_zh' => $r->display_name_zh, + 'display_name_en' => $r->display_name_en, + 'display_name_ne' => $r->display_name_ne, 'is_enabled' => (bool) $r->is_enabled, 'min_bet_amount' => (int) $r->min_bet_amount, 'max_bet_amount' => (int) $r->max_bet_amount, 'display_order' => (int) $r->display_order, + 'supports_multi_number' => (bool) $r->supports_multi_number, + 'reserved_rule_json' => $r->reserved_rule_json, 'rule_text_zh' => $r->rule_text_zh, 'rule_text_en' => $r->rule_text_en, 'rule_text_ne' => $r->rule_text_ne, diff --git a/app/Services/Config/PlayConfigStreamService.php b/app/Services/Config/PlayConfigStreamService.php index 03bdf65..27fce6c 100644 --- a/app/Services/Config/PlayConfigStreamService.php +++ b/app/Services/Config/PlayConfigStreamService.php @@ -54,10 +54,18 @@ final class PlayConfigStreamService PlayConfigItem::query()->create([ 'version_id' => $draft->id, 'play_code' => $row->play_code, + 'category' => $row->category, + 'dimension' => $row->dimension, + 'bet_mode' => $row->bet_mode, + 'display_name_zh' => $row->display_name_zh, + 'display_name_en' => $row->display_name_en, + 'display_name_ne' => $row->display_name_ne, 'is_enabled' => $row->is_enabled, 'min_bet_amount' => $row->min_bet_amount, 'max_bet_amount' => $row->max_bet_amount, 'display_order' => $row->display_order, + 'supports_multi_number' => $row->supports_multi_number, + 'reserved_rule_json' => $row->reserved_rule_json, 'rule_text_zh' => $row->rule_text_zh, 'rule_text_en' => $row->rule_text_en, 'rule_text_ne' => $row->rule_text_ne, @@ -69,10 +77,18 @@ final class PlayConfigStreamService PlayConfigItem::query()->create([ 'version_id' => $draft->id, 'play_code' => $pt->play_code, + 'category' => $pt->category, + 'dimension' => $pt->dimension, + 'bet_mode' => $pt->bet_mode, + 'display_name_zh' => $pt->display_name_zh, + 'display_name_en' => $pt->display_name_en, + 'display_name_ne' => $pt->display_name_ne, 'is_enabled' => (bool) $pt->is_enabled, 'min_bet_amount' => 100, 'max_bet_amount' => 500_000_000, 'display_order' => (int) $pt->sort_order, + 'supports_multi_number' => (bool) $pt->supports_multi_number, + 'reserved_rule_json' => $pt->reserved_rule_json, 'rule_text_zh' => null, 'rule_text_en' => null, 'rule_text_ne' => null, @@ -97,10 +113,18 @@ final class PlayConfigStreamService PlayConfigItem::query()->create([ 'version_id' => $draft->id, 'play_code' => (string) $row['play_code'], + 'category' => $row['category'] ?? null, + 'dimension' => $row['dimension'] ?? null, + 'bet_mode' => $row['bet_mode'] ?? null, + 'display_name_zh' => $row['display_name_zh'] ?? null, + 'display_name_en' => $row['display_name_en'] ?? null, + 'display_name_ne' => $row['display_name_ne'] ?? null, 'is_enabled' => (bool) ($row['is_enabled'] ?? true), 'min_bet_amount' => (int) ($row['min_bet_amount'] ?? 0), 'max_bet_amount' => (int) ($row['max_bet_amount'] ?? 0), 'display_order' => (int) ($row['display_order'] ?? 0), + 'supports_multi_number' => (bool) ($row['supports_multi_number'] ?? false), + 'reserved_rule_json' => $row['reserved_rule_json'] ?? null, 'rule_text_zh' => isset($row['rule_text_zh']) ? (string) $row['rule_text_zh'] : null, 'rule_text_en' => isset($row['rule_text_en']) ? (string) $row['rule_text_en'] : null, 'rule_text_ne' => isset($row['rule_text_ne']) ? (string) $row['rule_text_ne'] : null, @@ -221,6 +245,14 @@ final class PlayConfigStreamService if ($maxBet < $minBet) { $errors["items.$index.max_bet_amount"][] = '最大下注额不能小于最小下注额'; } + + if ($row->display_name_zh === null || $row->display_name_zh === '') { + $errors["items.$index.display_name_zh"][] = '显示名称不能为空'; + } + + if ($row->display_order === null) { + $errors["items.$index.display_order"][] = '排序不能为空'; + } } if ($errors !== []) { diff --git a/app/Services/Ticket/PlayCatalogResolver.php b/app/Services/Ticket/PlayCatalogResolver.php index 807b2e4..6555748 100644 --- a/app/Services/Ticket/PlayCatalogResolver.php +++ b/app/Services/Ticket/PlayCatalogResolver.php @@ -3,7 +3,6 @@ namespace App\Services\Ticket; use App\Models\OddsItem; -use App\Models\PlayType; use App\Lottery\ErrorCode; use App\Models\OddsVersion; use App\Models\RiskCapItem; @@ -80,18 +79,10 @@ final class PlayCatalogResolver } /** - * @return array{play_type: PlayType, play_config: PlayConfigItem, odds_items: Collection} + * @return array{play_config: PlayConfigItem, odds_items: Collection} */ public function resolve(string $playCode, string $currencyCode): array { - $playType = PlayType::query()->where('play_code', $playCode)->first(); - if ($playType === null) { - throw new TicketOperationException('play_not_found', ErrorCode::BetPlayUnsupported->value); - } - if (! $playType->is_enabled) { - throw new TicketOperationException('play_master_disabled', ErrorCode::PlayModeClosed->value); - } - $playVersion = PlayConfigVersion::query() ->where('status', ConfigVersionStatus::Active->value) ->firstOrFail(); @@ -120,7 +111,6 @@ final class PlayCatalogResolver } return [ - 'play_type' => $playType, 'play_config' => $playConfig, 'odds_items' => $oddsItems, ]; diff --git a/app/Services/Ticket/PlayRuleEngine.php b/app/Services/Ticket/PlayRuleEngine.php index 22c6cbc..ef2abe2 100644 --- a/app/Services/Ticket/PlayRuleEngine.php +++ b/app/Services/Ticket/PlayRuleEngine.php @@ -3,7 +3,6 @@ namespace App\Services\Ticket; use App\Models\OddsItem; -use App\Models\PlayType; use App\Lottery\ErrorCode; use App\Models\PlayConfigItem; use Illuminate\Support\Collection; @@ -20,7 +19,7 @@ final class PlayRuleEngine * @param Collection $oddsItems * @return array */ - public function evaluateLine(array $line, PlayType $playType, PlayConfigItem $playConfig, Collection $oddsItems): array + public function evaluateLine(array $line, PlayConfigItem $playConfig, Collection $oddsItems): array { $playCode = (string) $line['play_code']; $dimension = $line['dimension'] ?? null; @@ -61,9 +60,9 @@ final class PlayRuleEngine 'original_number' => (string) $line['number'], 'normalized_number' => $number, 'play_code' => $playCode, - 'dimension' => $this->toDimensionInt(is_string($dimension) ? $dimension : null, $playType), + 'dimension' => $this->toDimensionInt(is_string($dimension) ? $dimension : null, $playConfig), 'digit_slot' => $digitSlotInt, - 'bet_mode' => $playType->bet_mode, + 'bet_mode' => $playConfig->bet_mode, 'unit_bet_amount' => $unitBetAmount, 'total_bet_amount' => $totalBetAmount, 'rebate_rate_snapshot' => number_format($rebateRate, 4, '.', ''), @@ -300,13 +299,13 @@ final class PlayRuleEngine return $oddsItems->firstOrFail(); } - private function toDimensionInt(?string $dimension, PlayType $playType): ?int + private function toDimensionInt(?string $dimension, PlayConfigItem $playConfig): ?int { return match ($dimension) { 'D2' => 2, 'D3' => 3, 'D4' => 4, - default => $playType->dimension === null ? null : (int) $playType->dimension, + default => $playConfig->dimension === null ? null : (int) $playConfig->dimension, }; } } diff --git a/app/Services/Ticket/TicketPlacementService.php b/app/Services/Ticket/TicketPlacementService.php index 287f98a..8cc4bfa 100644 --- a/app/Services/Ticket/TicketPlacementService.php +++ b/app/Services/Ticket/TicketPlacementService.php @@ -83,7 +83,6 @@ final class TicketPlacementService } $evaluated = $this->ruleEngine->evaluateLine( (array) $line, - $resolved['play_type'], $resolved['play_config'], $resolved['odds_items'], ); diff --git a/app/Services/Ticket/TicketPreviewService.php b/app/Services/Ticket/TicketPreviewService.php index 57888ef..e0e1ce3 100644 --- a/app/Services/Ticket/TicketPreviewService.php +++ b/app/Services/Ticket/TicketPreviewService.php @@ -54,7 +54,6 @@ final class TicketPreviewService } $evaluated = $this->ruleEngine->evaluateLine( (array) $line, - $resolved['play_type'], $resolved['play_config'], $resolved['odds_items'], ); diff --git a/app/Support/AdminConfigPresenter.php b/app/Support/AdminConfigPresenter.php index 4094944..8001490 100644 --- a/app/Support/AdminConfigPresenter.php +++ b/app/Support/AdminConfigPresenter.php @@ -63,10 +63,18 @@ final class AdminConfigPresenter return [ 'id' => (int) $r->id, 'play_code' => $r->play_code, + 'category' => $r->category, + 'dimension' => $r->dimension === null ? null : (int) $r->dimension, + 'bet_mode' => $r->bet_mode, + 'display_name_zh' => $r->display_name_zh, + 'display_name_en' => $r->display_name_en, + 'display_name_ne' => $r->display_name_ne, 'is_enabled' => (bool) $r->is_enabled, 'min_bet_amount' => (int) $r->min_bet_amount, 'max_bet_amount' => (int) $r->max_bet_amount, 'display_order' => (int) $r->display_order, + 'supports_multi_number' => (bool) $r->supports_multi_number, + 'reserved_rule_json' => $r->reserved_rule_json, 'rule_text_zh' => $r->rule_text_zh, 'rule_text_en' => $r->rule_text_en, 'rule_text_ne' => $r->rule_text_ne, diff --git a/database/migrations/2026_05_16_000100_add_snapshot_columns_to_play_config_items_table.php b/database/migrations/2026_05_16_000100_add_snapshot_columns_to_play_config_items_table.php new file mode 100644 index 0000000..b27cf94 --- /dev/null +++ b/database/migrations/2026_05_16_000100_add_snapshot_columns_to_play_config_items_table.php @@ -0,0 +1,79 @@ +string('category', 16)->nullable()->after('play_code'); + $table->unsignedTinyInteger('dimension')->nullable()->after('category'); + $table->string('bet_mode', 32)->nullable()->after('dimension'); + $table->string('display_name_zh', 64)->nullable()->after('bet_mode'); + $table->string('display_name_en', 64)->nullable()->after('display_name_zh'); + $table->string('display_name_ne', 64)->nullable()->after('display_name_en'); + $table->boolean('supports_multi_number')->default(false)->after('display_name_ne'); + $table->json('reserved_rule_json')->nullable()->after('supports_multi_number'); + }); + + $playTypes = DB::table('play_types') + ->select([ + 'play_code', + 'category', + 'dimension', + 'bet_mode', + 'display_name_zh', + 'display_name_en', + 'display_name_ne', + 'supports_multi_number', + 'reserved_rule_json', + ]) + ->get() + ->keyBy('play_code'); + + DB::table('play_config_items') + ->select(['id', 'play_code']) + ->orderBy('id') + ->chunkById(200, function ($rows) use ($playTypes): void { + foreach ($rows as $row) { + $pt = $playTypes->get($row->play_code); + if ($pt === null) { + continue; + } + + DB::table('play_config_items') + ->where('id', $row->id) + ->update([ + 'category' => $pt->category, + 'dimension' => $pt->dimension, + 'bet_mode' => $pt->bet_mode, + 'display_name_zh' => $pt->display_name_zh, + 'display_name_en' => $pt->display_name_en, + 'display_name_ne' => $pt->display_name_ne, + 'supports_multi_number' => (bool) $pt->supports_multi_number, + 'reserved_rule_json' => $pt->reserved_rule_json, + ]); + } + }, 'id'); + } + + public function down(): void + { + Schema::table('play_config_items', function (Blueprint $table): void { + $table->dropColumn([ + 'category', + 'dimension', + 'bet_mode', + 'display_name_zh', + 'display_name_en', + 'display_name_ne', + 'supports_multi_number', + 'reserved_rule_json', + ]); + }); + } +}; diff --git a/database/seeders/OperationalConfigV1Seeder.php b/database/seeders/OperationalConfigV1Seeder.php index bae1b91..f3b05bd 100644 --- a/database/seeders/OperationalConfigV1Seeder.php +++ b/database/seeders/OperationalConfigV1Seeder.php @@ -66,10 +66,18 @@ final class OperationalConfigV1Seeder extends Seeder PlayConfigItem::query()->create([ 'version_id' => $playVersion->id, 'play_code' => $pt->play_code, + 'category' => $pt->category, + 'dimension' => $pt->dimension, + 'bet_mode' => $pt->bet_mode, + 'display_name_zh' => $pt->display_name_zh, + 'display_name_en' => $pt->display_name_en, + 'display_name_ne' => $pt->display_name_ne, 'is_enabled' => (bool) $pt->is_enabled, 'min_bet_amount' => 100, 'max_bet_amount' => 500_000_000, 'display_order' => (int) $pt->sort_order, + 'supports_multi_number' => (bool) $pt->supports_multi_number, + 'reserved_rule_json' => $pt->reserved_rule_json, 'rule_text_zh' => null, 'rule_text_en' => null, 'rule_text_ne' => null, diff --git a/database/seeders/PlayOperationalAlignmentSeeder.php b/database/seeders/PlayOperationalAlignmentSeeder.php index b0f8b47..a1d4e84 100644 --- a/database/seeders/PlayOperationalAlignmentSeeder.php +++ b/database/seeders/PlayOperationalAlignmentSeeder.php @@ -61,10 +61,18 @@ final class PlayOperationalAlignmentSeeder extends Seeder PlayConfigItem::query()->create([ 'version_id' => $vid, 'play_code' => $pt->play_code, + 'category' => $pt->category, + 'dimension' => $pt->dimension, + 'bet_mode' => $pt->bet_mode, + 'display_name_zh' => $pt->display_name_zh, + 'display_name_en' => $pt->display_name_en, + 'display_name_ne' => $pt->display_name_ne, 'is_enabled' => (bool) $pt->is_enabled, 'min_bet_amount' => self::MIN_BET, 'max_bet_amount' => self::MAX_BET, 'display_order' => (int) $pt->sort_order, + 'supports_multi_number' => (bool) $pt->supports_multi_number, + 'reserved_rule_json' => $pt->reserved_rule_json, 'rule_text_zh' => null, 'rule_text_en' => null, 'rule_text_ne' => null, diff --git a/tests/Feature/OperationalConfigAcceptanceTest.php b/tests/Feature/OperationalConfigAcceptanceTest.php index 2571c70..7295bbe 100644 --- a/tests/Feature/OperationalConfigAcceptanceTest.php +++ b/tests/Feature/OperationalConfigAcceptanceTest.php @@ -3,7 +3,7 @@ /** * 验收自动化(对应测试任务 §5 / 完成标准 §12.6): * - * - 配置改动是否生效 → 赔率 / 玩法限额发布后 GET /api/v1/play/effective 可读出新值;PATCH play-types 可改目录开关。 + * - 配置改动是否生效 → 赔率 / 玩法限额发布后 GET /api/v1/play/effective 可读出新值;玩法目录开关随版本草稿发布后生效。 * - 配置改动是否影响已下注订单 → ticket_items 行的 odds_snapshot_json 不因新赔率版本发布而被改写(注单链路尚未实现时,用数据不变量验证)。 * - 配置历史是否可追溯 → odds_versions 存在 archived + active;audit_logs 记录 publish。 * @@ -73,10 +73,18 @@ test('§12.6 published play limits are visible on public effective catalog witho foreach (PlayType::query()->orderBy('play_code')->get() as $t) { $itemPayload[] = [ 'play_code' => $t->play_code, + 'category' => $t->category, + 'dimension' => $t->dimension, + 'bet_mode' => $t->bet_mode, + 'display_name_zh' => $t->display_name_zh ?? $t->play_code, + 'display_name_en' => $t->display_name_en, + 'display_name_ne' => $t->display_name_ne, 'is_enabled' => true, 'min_bet_amount' => 777, 'max_bet_amount' => 400_000_000, 'display_order' => (int) $t->sort_order, + 'supports_multi_number' => (bool) $t->supports_multi_number, + 'reserved_rule_json' => $t->reserved_rule_json, ]; } @@ -245,16 +253,47 @@ test('§5 existing ticket_items odds snapshot row is not mutated when new odds v expect($plays->firstWhere('play_code', 'big')['odds']['odds_value'])->toBe(9_999_999); }); -test('§12.6 PATCH play-types controls master_enabled on public catalog without code deploy', function (): void { +test('§12.6 published play config controls master_enabled on public catalog without code deploy', function (): void { $token = acceptanceMintAdminToken(); $auth = ['Authorization' => 'Bearer '.$token]; - $this->patchJson('/api/v1/admin/play-types/big', ['is_enabled' => false], $auth)->assertOk(); + $create = $this->postJson('/api/v1/admin/config/play-versions', ['reason' => 'acceptance master'], $auth); + $create->assertOk(); + $draftId = (int) $create->json('data.id'); + + $rows = $this->getJson('/api/v1/admin/config/play-versions/'.$draftId, $auth)->assertOk()->json('data.items'); + $payload = collect($rows)->map(fn (array $r) => [ + 'play_code' => $r['play_code'], + 'category' => $r['category'], + 'dimension' => $r['dimension'], + 'bet_mode' => $r['bet_mode'], + 'display_name_zh' => $r['display_name_zh'], + 'display_name_en' => $r['display_name_en'], + 'display_name_ne' => $r['display_name_ne'], + 'is_enabled' => $r['is_enabled'], + 'min_bet_amount' => (int) $r['min_bet_amount'], + 'max_bet_amount' => (int) $r['max_bet_amount'], + 'display_order' => (int) $r['display_order'], + 'supports_multi_number' => (bool) $r['supports_multi_number'], + 'reserved_rule_json' => $r['reserved_rule_json'] ?? null, + 'rule_text_zh' => $r['rule_text_zh'] ?? null, + 'rule_text_en' => $r['rule_text_en'] ?? null, + 'rule_text_ne' => $r['rule_text_ne'] ?? null, + 'extra_config_json' => $r['extra_config_json'] ?? null, + ])->all(); + + foreach ($payload as &$row) { + if ($row['play_code'] === 'big') { + $row['is_enabled'] = false; + } + } + unset($row); + + $this->putJson('/api/v1/admin/config/play-versions/'.$draftId.'/items', ['items' => $payload], $auth)->assertOk(); + $this->postJson('/api/v1/admin/config/play-versions/'.$draftId.'/publish', [], $auth)->assertOk(); $plays = collect($this->getJson('/api/v1/play/effective?currency=NPR')->assertOk()->json('data.plays')); expect($plays->firstWhere('play_code', 'big')['master_enabled'])->toBeFalse(); - - $this->patchJson('/api/v1/admin/play-types/big', ['is_enabled' => true], $auth)->assertOk(); }); test('§5 risk cap publish is audited and version history exists', function (): void { @@ -303,10 +342,18 @@ test('§5 play_config publish is audited', function (): void { foreach (PlayType::query()->orderBy('play_code')->get() as $t) { $itemPayload[] = [ 'play_code' => $t->play_code, + 'category' => $t->category, + 'dimension' => $t->dimension, + 'bet_mode' => $t->bet_mode, + 'display_name_zh' => $t->display_name_zh ?? $t->play_code, + 'display_name_en' => $t->display_name_en, + 'display_name_ne' => $t->display_name_ne, 'is_enabled' => true, 'min_bet_amount' => 100, 'max_bet_amount' => 500_000_000, 'display_order' => (int) $t->sort_order, + 'supports_multi_number' => (bool) $t->supports_multi_number, + 'reserved_rule_json' => $t->reserved_rule_json, ]; } diff --git a/tests/Feature/OperationalConfigApiTest.php b/tests/Feature/OperationalConfigApiTest.php index 1c5d797..2fe2a56 100644 --- a/tests/Feature/OperationalConfigApiTest.php +++ b/tests/Feature/OperationalConfigApiTest.php @@ -75,10 +75,18 @@ test('admin play config draft publish flow', function (): void { foreach ($types as $t) { $itemPayload[] = [ 'play_code' => $t->play_code, + 'category' => $t->category, + 'dimension' => $t->dimension, + 'bet_mode' => $t->bet_mode, + 'display_name_zh' => $t->display_name_zh ?? $t->play_code, + 'display_name_en' => $t->display_name_en, + 'display_name_ne' => $t->display_name_ne, 'is_enabled' => true, 'min_bet_amount' => 200, 'max_bet_amount' => 400_000_000, 'display_order' => (int) $t->sort_order, + 'supports_multi_number' => (bool) $t->supports_multi_number, + 'reserved_rule_json' => $t->reserved_rule_json, ]; }