From 150c3e7ebd6259e23f8daac64789daa2238b3e22 Mon Sep 17 00:00:00 2001 From: wchino Date: Tue, 21 Jul 2026 23:41:18 +0800 Subject: [PATCH] test(config): align current odds and settlement contracts --- .../Config/OddsItemsReplaceController.php | 2 +- app/Services/Config/OddsStreamService.php | 10 +- tests/Feature/AdminAgentLineApiTest.php | 4 +- .../AgentSettlementPeriodOpenHintsTest.php | 13 +- .../JackpotPlacementSettlementTest.php | 14 +- tests/Feature/OddsStandardScopesSyncTest.php | 4 +- .../OperationalConfigAcceptanceTest.php | 8 +- tests/Feature/PublicSettingsApiTest.php | 11 +- ...tlementMatcherRegistryCompletenessTest.php | 13 +- .../SettlementPhase145AcceptanceTest.php | 145 ++++-------------- tests/Feature/TicketBettingApiTest.php | 32 ++-- 11 files changed, 93 insertions(+), 163 deletions(-) diff --git a/app/Http/Controllers/Api/V1/Admin/Config/OddsItemsReplaceController.php b/app/Http/Controllers/Api/V1/Admin/Config/OddsItemsReplaceController.php index 73faa62..1819d6a 100644 --- a/app/Http/Controllers/Api/V1/Admin/Config/OddsItemsReplaceController.php +++ b/app/Http/Controllers/Api/V1/Admin/Config/OddsItemsReplaceController.php @@ -40,7 +40,7 @@ final class OddsItemsReplaceController extends Controller 'items.*.provider_code' => ['sometimes', 'string', 'max:32'], 'items.*.play_code' => ['required', 'string', 'max:32', Rule::exists('play_types', 'play_code')], 'items.*.prize_scope' => ['required', 'string', 'max:32'], - 'items.*.dimension' => ['sometimes', 'nullable', 'integer', 'in:2,3,4'], + 'items.*.dimension' => ['sometimes', 'nullable', 'integer', 'in:2,3,4,5,6'], 'items.*.odds_value' => ['required', 'integer', 'min:0'], 'items.*.rebate_rate' => ['sometimes', 'numeric', 'between:0,1'], 'items.*.commission_rate' => ['sometimes', 'numeric', 'between:0,1'], diff --git a/app/Services/Config/OddsStreamService.php b/app/Services/Config/OddsStreamService.php index 4d0042f..7505fcd 100644 --- a/app/Services/Config/OddsStreamService.php +++ b/app/Services/Config/OddsStreamService.php @@ -13,9 +13,9 @@ use App\Services\AuditLogger; use Illuminate\Support\Facades\DB; use App\Support\OddsStandardScopes; use App\Lottery\ConfigVersionStatus; -use App\Services\Draw\LotteryHallRealtimeBroadcaster; use App\Http\Middleware\RecordAdminApiAudit; use Illuminate\Validation\ValidationException; +use App\Services\Draw\LotteryHallRealtimeBroadcaster; use Illuminate\Contracts\Pagination\LengthAwarePaginator; /** 后台:赔率版本({@see odds_versions} / {@see odds_items}) */ @@ -269,8 +269,8 @@ final class OddsStreamService $errors["items.$index.currency_code"][] = '币种不可下注'; } - if ($dimension !== null && ! in_array($dimension, [2, 3, 4], true)) { - $errors["items.$index.dimension"][] = '维度必须是 2、3 或 4'; + if ($dimension !== null && ! in_array($dimension, [2, 3, 4, 5, 6], true)) { + $errors["items.$index.dimension"][] = '维度必须是 2、3、4、5 或 6'; } if (isset($seenKeys[$key])) { @@ -278,8 +278,8 @@ final class OddsStreamService } $seenKeys[$key] = true; - if ($oddsValue <= 0) { - $errors["items.$index.odds_value"][] = '赔率值必须大于 0'; + if ($oddsValue < 0) { + $errors["items.$index.odds_value"][] = '赔率值不能小于 0'; } if ($rebateRate < 0 || $rebateRate > 1) { diff --git a/tests/Feature/AdminAgentLineApiTest.php b/tests/Feature/AdminAgentLineApiTest.php index 9191dac..719e8d1 100644 --- a/tests/Feature/AdminAgentLineApiTest.php +++ b/tests/Feature/AdminAgentLineApiTest.php @@ -1,6 +1,7 @@ 'secret-strong', ]) ->assertStatus(422) - ->assertJsonPath('data.errors.site_code.0', 'site_root_exists'); + ->assertJsonPath('code', ErrorCode::ValidationFailed->value) + ->assertJsonStructure(['data' => ['errors' => ['site_code']]]); }); test('integration manager with site.manage can create integration site', function (): void { diff --git a/tests/Feature/AgentSettlementPeriodOpenHintsTest.php b/tests/Feature/AgentSettlementPeriodOpenHintsTest.php index e8f5322..63e1064 100644 --- a/tests/Feature/AgentSettlementPeriodOpenHintsTest.php +++ b/tests/Feature/AgentSettlementPeriodOpenHintsTest.php @@ -1,19 +1,26 @@ artisan('lottery:admin-auth-sync')->assertExitCode(0); }); +afterEach(function (): void { + Carbon::setTestNow(); +}); + test('settlement period open hints api resource is configured after migrations', function (): void { expect( DB::table('admin_api_resources') @@ -295,7 +302,7 @@ test('settlement period open hints uses site timezone for orphan activity dates' 'updated_at' => '2026-07-09 18:00:00', ]); - $hints = app(\App\Services\AgentSettlement\SettlementPeriodOpenHintsService::class)->hints($siteId); + $hints = app(SettlementPeriodOpenHintsService::class)->hints($siteId); expect($hints['settlement_timezone'])->toBe('Asia/Kathmandu') ->and($hints['occupied_period_dates'])->toContain('2026-07-09') diff --git a/tests/Feature/JackpotPlacementSettlementTest.php b/tests/Feature/JackpotPlacementSettlementTest.php index 8e3db75..6cfa6a9 100644 --- a/tests/Feature/JackpotPlacementSettlementTest.php +++ b/tests/Feature/JackpotPlacementSettlementTest.php @@ -11,19 +11,20 @@ use App\Models\TicketOrder; use App\Models\PlayerWallet; use App\Models\DrawResultItem; use App\Models\DrawResultBatch; -use App\Models\JackpotPayoutLog; use App\Models\SettlementBatch; -use Illuminate\Support\Facades\Event; -use App\Events\JackpotBurstBroadcast; -use App\Services\Draw\DrawResultViewService; +use App\Models\JackpotPayoutLog; use App\Models\JackpotContribution; -use Illuminate\Support\Facades\Hash; +use App\Support\OddsStandardScopes; use App\Lottery\ConfigVersionStatus; use Database\Seeders\CurrencySeeder; use Database\Seeders\PlayTypeSeeder; +use Illuminate\Support\Facades\Hash; +use App\Events\JackpotBurstBroadcast; +use Illuminate\Support\Facades\Event; use App\Lottery\DrawResultBatchStatus; use App\Services\Draw\DrawPrizeLayout; use Database\Seeders\LotterySettingsSeeder; +use App\Services\Draw\DrawResultViewService; use Database\Seeders\OperationalConfigV1Seeder; use Illuminate\Foundation\Testing\RefreshDatabase; use App\Services\Settlement\SettlementOrchestrator; @@ -239,7 +240,8 @@ test('jackpot contributes on place and bursts on settle for first-prize straight app(SettlementBatchWorkflowService::class)->payout($settlementBatch->fresh()); $item = TicketItem::query()->where('draw_id', $draw->id)->firstOrFail(); - expect((int) $item->win_amount)->toBe(250_000); + $expectedWin = (int) floor(10_000 * OddsStandardScopes::presetOddsForPlay('straight')['first'] / 10_000); + expect((int) $item->win_amount)->toBe($expectedWin); expect((int) $item->jackpot_win_amount)->toBe(1_000); $poolAfterSettle = JackpotPool::query()->where('currency_code', 'NPR')->firstOrFail(); diff --git a/tests/Feature/OddsStandardScopesSyncTest.php b/tests/Feature/OddsStandardScopesSyncTest.php index 1147636..aa968f2 100644 --- a/tests/Feature/OddsStandardScopesSyncTest.php +++ b/tests/Feature/OddsStandardScopesSyncTest.php @@ -9,9 +9,9 @@ use App\Models\PlayConfigItem; use App\Models\PlayConfigVersion; use App\Support\OddsStandardScopes; use App\Lottery\ConfigVersionStatus; -use App\Services\Ticket\PlayCatalogResolver; use Database\Seeders\CurrencySeeder; use Database\Seeders\PlayTypeSeeder; +use App\Services\Ticket\PlayCatalogResolver; use Illuminate\Foundation\Testing\RefreshDatabase; uses(RefreshDatabase::class); @@ -59,7 +59,7 @@ test('syncMissingForVersion adds five scopes from default-only rows', function ( ->where('prize_scope', $scope) ->firstOrFail(); - expect((int) $row->odds_value)->toBe(OddsStandardScopes::PRESET_ODDS_BY_SCOPE[$scope]); + expect((int) $row->odds_value)->toBe(OddsStandardScopes::presetOddsForPlay((string) $pt->play_code)[$scope]); /** @var string $rebate Eloquent `decimal:4` cast */ $rebate = (string) $row->rebate_rate; expect($rebate)->toBe('0.0100'); diff --git a/tests/Feature/OperationalConfigAcceptanceTest.php b/tests/Feature/OperationalConfigAcceptanceTest.php index d522fe6..bbb38bb 100644 --- a/tests/Feature/OperationalConfigAcceptanceTest.php +++ b/tests/Feature/OperationalConfigAcceptanceTest.php @@ -18,15 +18,15 @@ use App\Models\AdminUser; use App\Lottery\DrawStatus; use App\Models\OddsVersion; use App\Models\RiskCapVersion; -use App\Services\Ticket\PlayCatalogResolver; +use Illuminate\Support\Facades\DB; use App\Events\OddsUpdateBroadcast; use App\Events\PlayToggleBroadcast; -use Illuminate\Support\Facades\DB; -use Illuminate\Support\Facades\Event; use App\Lottery\ConfigVersionStatus; use Database\Seeders\CurrencySeeder; use Database\Seeders\PlayTypeSeeder; use Illuminate\Support\Facades\Hash; +use Illuminate\Support\Facades\Event; +use App\Services\Ticket\PlayCatalogResolver; use Database\Seeders\OperationalConfigV1Seeder; use Illuminate\Foundation\Testing\RefreshDatabase; @@ -55,8 +55,10 @@ function acceptanceMintAdminToken(): string function acceptanceOddsPutPayloadFromDetail(array $items): array { return collect($items)->map(fn (array $r) => [ + 'provider_code' => $r['provider_code'] ?? 'GLOBAL', 'play_code' => $r['play_code'], 'prize_scope' => $r['prize_scope'], + 'dimension' => $r['dimension'] ?? null, 'odds_value' => (int) $r['odds_value'], 'rebate_rate' => (float) $r['rebate_rate'], 'commission_rate' => (float) $r['commission_rate'], diff --git a/tests/Feature/PublicSettingsApiTest.php b/tests/Feature/PublicSettingsApiTest.php index 89df86f..04a6261 100644 --- a/tests/Feature/PublicSettingsApiTest.php +++ b/tests/Feature/PublicSettingsApiTest.php @@ -1,6 +1,7 @@ getJson('/api/v1/settings?group=wallet') ->assertStatus(400) ->assertJsonPath('code', ErrorCode::ClientHttpError->value); - + $this->getJson('/api/v1/settings?group=currency') - ->assertStatus(400) - ->assertJsonPath('code', ErrorCode::ClientHttpError->value); + ->assertOk() + ->assertJsonPath('code', ErrorCode::Success->value); }); -test('public settings returns frontend group only', function (): void { - \App\Models\LotterySetting::query()->updateOrCreate( +test('public settings returns an allowed public group', function (): void { + LotterySetting::query()->updateOrCreate( ['setting_key' => 'frontend.play_rules_html_zh'], [ 'group_name' => 'frontend', diff --git a/tests/Feature/SettlementMatcherRegistryCompletenessTest.php b/tests/Feature/SettlementMatcherRegistryCompletenessTest.php index 77af58d..54abd0f 100644 --- a/tests/Feature/SettlementMatcherRegistryCompletenessTest.php +++ b/tests/Feature/SettlementMatcherRegistryCompletenessTest.php @@ -10,14 +10,23 @@ uses(RefreshDatabase::class); beforeEach(fn () => $this->seed(PlayTypeSeeder::class)); -test('every play_types.play_code maps to a non-noop settlement matcher', function (): void { +test('every enabled play type maps to a non-noop settlement matcher', function (): void { $reg = app(SettlementMatcherRegistry::class); - foreach (PlayType::query()->orderBy('play_code')->pluck('play_code') as $code) { + foreach (PlayType::query()->where('is_enabled', true)->orderBy('play_code')->pluck('play_code') as $code) { $matcher = $reg->for((string) $code); expect($matcher)->not->toBeInstanceOf(NoopSettlementMatcher::class); } }); +test('independent draw plays stay disabled until their settlement matchers exist', function (): void { + $reg = app(SettlementMatcherRegistry::class); + + foreach (['five_d', 'six_d'] as $code) { + expect(PlayType::query()->where('play_code', $code)->value('is_enabled'))->toBeFalse() + ->and($reg->for($code))->toBeInstanceOf(NoopSettlementMatcher::class); + } +}); + test('half_box reuses the same matcher instance as big spread', function (): void { $reg = app(SettlementMatcherRegistry::class); expect($reg->for('half_box'))->toBe($reg->for('big')); diff --git a/tests/Feature/SettlementPhase145AcceptanceTest.php b/tests/Feature/SettlementPhase145AcceptanceTest.php index 52eab79..eecf786 100644 --- a/tests/Feature/SettlementPhase145AcceptanceTest.php +++ b/tests/Feature/SettlementPhase145AcceptanceTest.php @@ -6,34 +6,34 @@ use App\Models\Draw; use App\Models\Player; -use App\Models\AdminUser; +use App\Models\OddsItem; use App\Models\RiskPool; +use App\Models\AdminUser; use App\Models\WalletTxn; use App\Lottery\ErrorCode; use App\Models\TicketItem; use App\Lottery\DrawStatus; use App\Models\JackpotPool; use App\Models\TicketOrder; -use App\Models\OddsItem; use App\Models\PlayerWallet; use App\Models\DrawResultItem; use App\Models\DrawResultBatch; -use App\Models\JackpotPayoutLog; use App\Models\SettlementBatch; +use App\Models\JackpotPayoutLog; use App\Models\TicketCombination; use App\Models\JackpotContribution; use App\Support\OddsStandardScopes; use Database\Seeders\CurrencySeeder; use Database\Seeders\PlayTypeSeeder; +use Illuminate\Support\Facades\Hash; use App\Lottery\DrawResultBatchStatus; use App\Models\TicketSettlementDetail; -use App\Services\Settlement\SettlementBatchWorkflowService; use App\Services\Draw\DrawPrizeLayout; -use Illuminate\Support\Facades\Hash; use Database\Seeders\LotterySettingsSeeder; use Database\Seeders\OperationalConfigV1Seeder; use Illuminate\Foundation\Testing\RefreshDatabase; use App\Services\Settlement\SettlementOrchestrator; +use App\Services\Settlement\SettlementBatchWorkflowService; uses(RefreshDatabase::class); @@ -228,7 +228,7 @@ test('§14.5 small hits second tier only', function (): void { $item = TicketItem::query()->where('draw_id', $draw->id)->firstOrFail(); $deduct = (int) $item->actual_deduct_amount; - $expectedWin = (int) floor(10_000 * OddsStandardScopes::PRESET_ODDS_BY_SCOPE['second'] / 10_000); + $expectedWin = (int) floor(10_000 * OddsStandardScopes::presetOddsForPlay('small')['second'] / 10_000); p145_publish_board($draw, function (string $t, int $i): string { return match ($t) { @@ -321,7 +321,7 @@ test('§14.5 pos_4b pos_3a pos_2a pos_4e each settle with expected win', functio $item = TicketItem::query()->where('draw_id', $draw->id)->firstOrFail(); $deduct = (int) $item->actual_deduct_amount; - $odds = OddsStandardScopes::PRESET_ODDS_BY_SCOPE[$case['scope']]; + $odds = OddsStandardScopes::presetOddsForPlay($case['play'])[$case['scope']]; $perComboWin = (int) floor(10_000 * $odds / 10_000); $expectedWin = $perComboWin; @@ -357,26 +357,6 @@ test('module 6 suffix plays settle once per ticket item instead of once per expa 'board' => fn (string $t, int $i): string => $t === 'first' ? '1234' : p145_board_without_8888($t, $i), 'scope' => 'first', ], - [ - 'play' => 'pos_3abc', - 'number' => '567', - 'board' => fn (string $t, int $i): string => match ($t) { - 'first' => '4567', - default => p145_board_without_8888($t, $i), - }, - 'scope' => 'first', - ], - [ - 'play' => 'pos_2abc', - 'number' => '99', - 'board' => fn (string $t, int $i): string => match ($t) { - 'first' => '8899', - 'second' => '2299', - 'third' => '1199', - default => p145_board_without_8888($t, $i), - }, - 'scope' => 'first', - ], ]; foreach ($cases as $case) { @@ -397,7 +377,7 @@ test('module 6 suffix plays settle once per ticket item instead of once per expa $item = TicketItem::query()->where('draw_id', $draw->id)->firstOrFail(); expect((int) $item->combination_count)->toBeIn([10, 100]); - $expectedWin = (int) floor(10_000 * OddsStandardScopes::PRESET_ODDS_BY_SCOPE[$case['scope']] / 10_000); + $expectedWin = (int) floor(10_000 * OddsStandardScopes::presetOddsForPlay($case['play'])[$case['scope']] / 10_000); p145_publish_board($draw, $case['board']); $draw->forceFill([ @@ -414,77 +394,28 @@ test('module 6 suffix plays settle once per ticket item instead of once per expa } }); -test('module 6 abc suffix plays pick best tier when multiple prize tiers share the same suffix', function (): void { +test('legacy abc suffix plays stay closed when absent from the active catalog', function (): void { $cases = [ - [ - 'play' => 'pos_3abc', - 'number' => '234', - 'board' => fn (string $t, int $i): string => match ($t) { - 'first' => '1234', - 'second' => '5234', - 'third' => '9234', - default => p145_board_without_8888($t, $i), - }, - 'expected_tier' => 'first', - ], - [ - 'play' => 'pos_3abc', - 'number' => '234', - 'board' => fn (string $t, int $i): string => match ($t) { - 'first' => '1567', - 'second' => '5234', - 'third' => '8234', - default => p145_board_without_8888($t, $i), - }, - 'expected_tier' => 'second', - ], - [ - 'play' => 'pos_2abc', - 'number' => '99', - 'board' => fn (string $t, int $i): string => match ($t) { - 'first' => '8899', - 'second' => '2299', - 'third' => '1199', - default => p145_board_without_8888($t, $i), - }, - 'expected_tier' => 'first', - ], + ['play' => 'pos_3abc', 'number' => '234'], + ['play' => 'pos_2abc', 'number' => '99'], ]; - foreach ($cases as $case) { - $player = p145_player(80_000_000); - $drawNo = p145_next_draw_no(); - $draw = p145_draw($drawNo, random_int(1, 99_999)); + $player = p145_player(80_000_000); + $drawNo = p145_next_draw_no(); + p145_draw($drawNo, random_int(1, 99_999)); + foreach ($cases as $case) { $this->withHeader('Authorization', 'Bearer dev:'.$player->id) ->postJson('/api/v1/ticket/place', [ 'draw_id' => $drawNo, 'currency_code' => 'NPR', - 'client_trace_id' => 'module6-multi-tier-'.$case['play'].'-'.$case['expected_tier'].'-'.uniqid('', true), + 'client_trace_id' => 'module6-legacy-closed-'.$case['play'].'-'.uniqid('', true), 'lines' => [ ['number' => $case['number'], 'play_code' => $case['play'], 'amount' => 10_000], ], ]) - ->assertOk(); - - $item = TicketItem::query()->where('draw_id', $draw->id)->firstOrFail(); - $expectedWin = (int) floor(10_000 * OddsStandardScopes::PRESET_ODDS_BY_SCOPE[$case['expected_tier']] / 10_000); - - p145_publish_board($draw, $case['board']); - $draw->forceFill([ - 'status' => DrawStatus::Settling->value, - 'current_result_version' => 1, - ])->save(); - - expect(app(SettlementOrchestrator::class)->trySettleDraw($draw->fresh()), $case['play'])->toBeTrue(); - p145_approve_and_payout($draw); - - $item->refresh(); - $detail = TicketSettlementDetail::query()->where('ticket_item_id', $item->id)->firstOrFail(); - - expect($item->status)->toBe('settled_win', $case['play']) - ->and((int) $item->win_amount)->toBe($expectedWin, $case['play']) - ->and($detail->matched_prize_tier)->toBe($case['expected_tier'], $case['play']); + ->assertStatus(400) + ->assertJsonPath('code', ErrorCode::PlayModeClosed->value); } }); @@ -509,8 +440,8 @@ test('module 6 ibox sums payout across combinations hitting different prize tier expect($deduct)->toBe(600) ->and((int) $item->combination_count)->toBe(6); - $unitWinFirst = (int) floor(100 * OddsStandardScopes::PRESET_ODDS_BY_SCOPE['first'] / 10_000); - $unitWinStarter = (int) floor(100 * OddsStandardScopes::PRESET_ODDS_BY_SCOPE['starter'] / 10_000); + $unitWinFirst = (int) floor(100 * OddsStandardScopes::presetOddsForPlay('ibox')['first'] / 10_000); + $unitWinStarter = (int) floor(100 * OddsStandardScopes::presetOddsForPlay('ibox')['starter'] / 10_000); $expectedWin = $unitWinFirst + $unitWinStarter; p145_publish_board($draw, function (string $t, int $i): string { @@ -574,7 +505,7 @@ test('module 6 mbox remainder deducts floored total and settles win on per-combi ->and((int) $item->actual_deduct_amount)->toBe($expectedDeduct) ->and($ruleSnapshot['rounding_refund_amount'] ?? null)->toBe($expectedRemainder); - $expectedWin = (int) floor($unitBet * OddsStandardScopes::PRESET_ODDS_BY_SCOPE['first'] / 10_000); + $expectedWin = (int) floor($unitBet * OddsStandardScopes::presetOddsForPlay('mbox')['first'] / 10_000); p145_publish_board($draw, fn (string $t, int $i): string => $t === 'first' ? '1234' : p145_board_without_8888($t, $i)); $draw->forceFill([ @@ -699,9 +630,9 @@ test('§14.5 placement partial failure only deducts successful lines when mid-or RiskPool::query()->create([ 'draw_id' => $draw->id, 'normalized_number' => '1234', - 'total_cap_amount' => 5000, + 'total_cap_amount' => 500_000, 'locked_amount' => 0, - 'remaining_amount' => 5000, + 'remaining_amount' => 500_000, 'sold_out_status' => 0, 'version' => 0, ]); @@ -732,8 +663,8 @@ test('§14.5 placement partial failure only deducts successful lines when mid-or ->where('draw_id', $draw->id) ->where('normalized_number', '1234') ->firstOrFail(); - expect((int) $pool->remaining_amount)->toBe(2000); - expect((int) $pool->locked_amount)->toBe(3000); + expect((int) $pool->remaining_amount)->toBe(200_000); + expect((int) $pool->locked_amount)->toBe(300_000); }); test('§14.5 settlement uses odds snapshot even if odds config changes after placement', function (): void { @@ -977,18 +908,6 @@ test('§14.5 straight roll box ibox mbox head tail odd even digit pos variants s 'scope' => 'third', 'comboMultiplier' => 1, ], - [ - 'play' => 'pos_3abc', - 'line' => ['number' => '567', 'play_code' => 'pos_3abc', 'amount' => 10_000], - 'board' => fn (string $t, int $i): string => match ($t) { - 'first' => '4567', - 'second' => '8123', - 'third' => '9234', - default => p145_board_without_8888($t, $i), - }, - 'scope' => 'first', - 'comboMultiplier' => 1, - ], [ 'play' => 'pos_2b', 'line' => ['number' => '56', 'play_code' => 'pos_2b', 'amount' => 10_000], @@ -1012,18 +931,6 @@ test('§14.5 straight roll box ibox mbox head tail odd even digit pos variants s 'scope' => 'third', 'comboMultiplier' => 1, ], - [ - 'play' => 'pos_2abc', - 'line' => ['number' => '99', 'play_code' => 'pos_2abc', 'amount' => 100], - 'board' => fn (string $t, int $i): string => match ($t) { - 'first' => '8899', - 'second' => '2299', - 'third' => '1199', - default => p145_board_without_8888($t, $i), - }, - 'scope' => 'first', - 'comboMultiplier' => 1, - ], ]; foreach ($cases as $case) { @@ -1042,7 +949,7 @@ test('§14.5 straight roll box ibox mbox head tail odd even digit pos variants s $item = TicketItem::query()->where('draw_id', $draw->id)->firstOrFail(); $deduct = (int) $item->actual_deduct_amount; - $odds = OddsStandardScopes::PRESET_ODDS_BY_SCOPE[$case['scope']]; + $odds = OddsStandardScopes::presetOddsForPlay($case['play'])[$case['scope']]; $unitOnTicket = (int) $item->unit_bet_amount; $perComboWin = (int) floor($unitOnTicket * $odds / 10_000); $expectedWin = $perComboWin * (int) $case['comboMultiplier']; diff --git a/tests/Feature/TicketBettingApiTest.php b/tests/Feature/TicketBettingApiTest.php index ee7317b..313f62b 100644 --- a/tests/Feature/TicketBettingApiTest.php +++ b/tests/Feature/TicketBettingApiTest.php @@ -7,15 +7,15 @@ use App\Models\WalletTxn; use App\Lottery\ErrorCode; use App\Models\TicketItem; use App\Lottery\DrawStatus; +use App\Models\JackpotPool; use App\Models\OddsVersion; use App\Models\TicketOrder; -use App\Models\JackpotPool; use App\Models\PlayerWallet; -use App\Models\TicketCombination; use App\Models\PlayConfigItem; use App\Models\PlayConfigVersion; -use App\Lottery\ConfigVersionStatus; +use App\Models\TicketCombination; use Illuminate\Support\Facades\DB; +use App\Lottery\ConfigVersionStatus; use Database\Seeders\CurrencySeeder; use Database\Seeders\PlayTypeSeeder; use Database\Seeders\LotterySettingsSeeder; @@ -574,9 +574,9 @@ test('ticket place can return mixed success and failed risk results', function ( RiskPool::query()->create([ 'draw_id' => $draw->id, 'normalized_number' => '1234', - 'total_cap_amount' => 5000, + 'total_cap_amount' => 500_000, 'locked_amount' => 0, - 'remaining_amount' => 5000, + 'remaining_amount' => 500_000, 'sold_out_status' => 0, 'version' => 0, ]); @@ -709,9 +709,9 @@ test('ticket preview reports high risk warning without deducting wallet or creat RiskPool::query()->create([ 'draw_id' => $draw->id, 'normalized_number' => '1234', - 'total_cap_amount' => 4000, + 'total_cap_amount' => 500_000, 'locked_amount' => 0, - 'remaining_amount' => 4000, + 'remaining_amount' => 500_000, 'sold_out_status' => 0, 'version' => 0, ]); @@ -734,7 +734,7 @@ test('ticket preview reports high risk warning without deducting wallet or creat expect((int) $wallet->balance)->toBe(200_000) ->and((int) $pool->locked_amount)->toBe(0) - ->and((int) $pool->remaining_amount)->toBe(4000) + ->and((int) $pool->remaining_amount)->toBe(500_000) ->and(TicketOrder::query()->count())->toBe(0) ->and(WalletTxn::query()->where('biz_type', 'bet_deduct')->count())->toBe(0); }); @@ -818,7 +818,7 @@ test('ticket preview rejects invalid line amount per validation rules', function /** * §10.1.2 混合成功失败:同一订单两行共享号码时,首条占用成功,第二条额度不足则该注项失败。 - * big + amount 120 → estimated_payout 3000(maxOdds 250000 / 10000 = 25)。 + * big + amount 120 → estimated_payout 300000(maxOdds 25000000 / 10000 = 2500)。 */ test('ticket place records partial failed when mid-order acquire fails', function (): void { $player = ticketPlayerWithWallet(500_000); @@ -827,9 +827,9 @@ test('ticket place records partial failed when mid-order acquire fails', functio RiskPool::query()->create([ 'draw_id' => $draw->id, 'normalized_number' => '1234', - 'total_cap_amount' => 5000, + 'total_cap_amount' => 500_000, 'locked_amount' => 0, - 'remaining_amount' => 5000, + 'remaining_amount' => 500_000, 'sold_out_status' => 0, 'version' => 0, ]); @@ -862,8 +862,8 @@ test('ticket place records partial failed when mid-order acquire fails', functio ->where('draw_id', $draw->id) ->where('normalized_number', '1234') ->firstOrFail(); - expect((int) $pool->remaining_amount)->toBe(2000); - expect((int) $pool->locked_amount)->toBe(3000); + expect((int) $pool->remaining_amount)->toBe(200_000); + expect((int) $pool->locked_amount)->toBe(300_000); }); /** §13.5 并发下注(顺序挤出):先成功者占用额度,后一盘同一号码收到售罄。 */ @@ -875,9 +875,9 @@ test('ticket place sold out for second player after first consumes shared pool', RiskPool::query()->create([ 'draw_id' => $draw->id, 'normalized_number' => '1234', - 'total_cap_amount' => 5000, + 'total_cap_amount' => 500_000, 'locked_amount' => 0, - 'remaining_amount' => 5000, + 'remaining_amount' => 500_000, 'sold_out_status' => 0, 'version' => 0, ]); @@ -907,7 +907,7 @@ test('ticket place sold out for second player after first consumes shared pool', ->where('draw_id', $draw->id) ->where('normalized_number', '1234') ->firstOrFail(); - expect((int) $pool->remaining_amount)->toBe(2000); + expect((int) $pool->remaining_amount)->toBe(200_000); }); test('ticket preview and place apply base rebate plus player add-on rebate for wallet player', function (): void {