- 在赔率数据表中新增 provider_code 字段,默认值为 GLOBAL - 赔率接口与服务中新增 provider_code 参数支持,实现开注商特定赔率加载 - 解析赔率集合时,根据 provider_code 优先级进行排序与筛选,保证特定开注商赔率优先 - 优化赔率唯一索引,新增 provider_code 列以防止不同开注商赔率重复冲突 - 调整赔率相关验证规则,支持开注商代码的合法性与可选性检查 - 玩法解析与出票相关逻辑改造,支持多开注商环境下的投注处理 - 管理界面赔率替换接口增加 provider_code 字段支持 - 同步和测试用例覆盖 provider_code 相关功能,验证开注商赔率隔离与优先切换 - PlayEffectiveCatalogController 支持按 provider_code 返回赔率目录数据 - TicketPlacementService 与 TicketPreviewService 修改为支持 provider_code 多开注商玩法解析 - 维护 OddsStandardScopes 同步逻辑,保证开注商维度佣金与赔率正确同步 - AdminConfigPresenter 增加了对应数据的 provider_code 与 dimension 输出字段 - SDK接口添加 odds_snapshot_json 中开注商字段,完善赔率快照信息 - 路由配置调整管理文档地址,无关改动细微修改
198 lines
6.8 KiB
PHP
198 lines
6.8 KiB
PHP
<?php
|
|
|
|
use App\Models\Currency;
|
|
use App\Models\OddsItem;
|
|
use App\Models\PlayType;
|
|
use App\Models\BetProvider;
|
|
use App\Models\OddsVersion;
|
|
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 Illuminate\Foundation\Testing\RefreshDatabase;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
test('syncMissingForVersion adds five scopes from default-only rows', function (): void {
|
|
$this->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');
|
|
});
|