seed(CurrencySeeder::class); $this->seed(PlayTypeSeeder::class); $currency = Currency::query()->where('is_bettable', true)->where('is_enabled', true)->orderBy('code')->firstOrFail(); $version = OddsVersion::query()->create([ 'version_no' => 1, 'status' => ConfigVersionStatus::Active->value, 'effective_at' => now(), 'updated_by' => null, 'reason' => 'test', ]); foreach (PlayType::query()->orderBy('play_code')->get() as $pt) { OddsItem::query()->create([ 'version_id' => $version->id, 'provider_code' => 'GLOBAL', 'play_code' => $pt->play_code, 'prize_scope' => 'default', 'odds_value' => 19_500, 'rebate_rate' => 0.01, 'commission_rate' => 0, 'currency_code' => $currency->code, 'extra_config_json' => null, ]); } OddsStandardScopes::syncMissingForVersion($version); $playCount = PlayType::query()->count(); expect(OddsItem::query()->where('version_id', $version->id)->count())->toBe( $playCount * (1 + count(OddsStandardScopes::SCOPE_KEYS)), ); foreach (PlayType::query()->get() as $pt) { foreach (OddsStandardScopes::SCOPE_KEYS as $scope) { $row = OddsItem::query() ->where('version_id', $version->id) ->where('play_code', $pt->play_code) ->where('currency_code', $currency->code) ->where('prize_scope', $scope) ->firstOrFail(); expect((int) $row->odds_value)->toBe(OddsStandardScopes::PRESET_ODDS_BY_SCOPE[$scope]); /** @var string $rebate Eloquent `decimal:4` cast */ $rebate = (string) $row->rebate_rate; expect($rebate)->toBe('0.0100'); } } }); test('syncMissingForVersion keeps provider-specific odds rows separate', function (): void { $this->seed(CurrencySeeder::class); $this->seed(PlayTypeSeeder::class); $currency = Currency::query()->where('is_bettable', true)->where('is_enabled', true)->orderBy('code')->firstOrFail(); $play = PlayType::query()->orderBy('play_code')->firstOrFail(); BetProvider::query()->create([ 'code' => 'MY', 'name' => 'Malaysia', 'is_enabled' => true, 'sort_order' => 2, ]); $version = OddsVersion::query()->create([ 'version_no' => 1, 'status' => ConfigVersionStatus::Active->value, 'effective_at' => now(), 'updated_by' => null, 'reason' => 'provider test', ]); OddsItem::query()->create([ 'version_id' => $version->id, 'provider_code' => 'MY', 'play_code' => $play->play_code, 'prize_scope' => 'first', 'dimension' => $play->dimension, 'odds_value' => 88_000, 'rebate_rate' => 0, 'commission_rate' => 0, 'currency_code' => $currency->code, 'extra_config_json' => null, ]); OddsStandardScopes::syncMissingForVersion($version); foreach (OddsStandardScopes::SCOPE_KEYS as $scope) { $this->assertDatabaseHas('odds_items', [ 'version_id' => $version->id, 'provider_code' => 'MY', 'play_code' => $play->play_code, 'currency_code' => $currency->code, 'prize_scope' => $scope, ]); } }); test('play catalog resolver uses provider odds and falls back to global scopes', function (): void { $this->seed(CurrencySeeder::class); $this->seed(PlayTypeSeeder::class); $currency = Currency::query()->where('is_bettable', true)->where('is_enabled', true)->orderBy('code')->firstOrFail(); $play = PlayType::query()->where('play_code', 'big')->firstOrFail(); BetProvider::query()->create([ 'code' => 'MY', 'name' => 'Malaysia', 'is_enabled' => true, 'sort_order' => 2, ]); $playVersion = PlayConfigVersion::query()->create([ 'version_no' => 1, 'status' => ConfigVersionStatus::Active->value, 'effective_at' => now(), 'updated_by' => null, 'reason' => 'play', ]); PlayConfigItem::query()->create([ 'version_id' => $playVersion->id, 'play_code' => $play->play_code, 'category' => $play->category, 'dimension' => $play->dimension, 'bet_mode' => $play->bet_mode, 'display_name' => $play->display_name ?? $play->play_code, 'is_enabled' => true, 'min_bet_amount' => 100, 'max_bet_amount' => 1_000_000, 'display_order' => $play->sort_order, 'supports_multi_number' => $play->supports_multi_number, 'reserved_rule_json' => null, ]); $oddsVersion = OddsVersion::query()->create([ 'version_no' => 1, 'status' => ConfigVersionStatus::Active->value, 'effective_at' => now(), 'updated_by' => null, 'reason' => 'odds', ]); foreach (OddsStandardScopes::SCOPE_KEYS as $scope) { OddsItem::query()->create([ 'version_id' => $oddsVersion->id, 'provider_code' => 'GLOBAL', 'play_code' => $play->play_code, 'prize_scope' => $scope, 'dimension' => $play->dimension, 'odds_value' => 10_000, 'rebate_rate' => 0, 'commission_rate' => 0, 'currency_code' => $currency->code, 'extra_config_json' => null, ]); } OddsItem::query()->create([ 'version_id' => $oddsVersion->id, 'provider_code' => 'MY', 'play_code' => $play->play_code, 'prize_scope' => 'first', 'dimension' => $play->dimension, 'odds_value' => 99_000, 'rebate_rate' => 0, 'commission_rate' => 0, 'currency_code' => $currency->code, 'extra_config_json' => null, ]); $resolved = app(PlayCatalogResolver::class)->resolve($play->play_code, $currency->code, 'MY'); $first = $resolved['odds_items']->firstWhere('prize_scope', 'first'); $second = $resolved['odds_items']->firstWhere('prize_scope', 'second'); expect((int) $first->odds_value)->toBe(99_000) ->and((string) $first->provider_code)->toBe('MY') ->and((int) $second->odds_value)->toBe(10_000) ->and((string) $second->provider_code)->toBe('GLOBAL'); });