统一通过 $amountModePlayCode 映射选择类型到正确的金额模式: - reverse / half_play / full_play 使用 ibox 模式(单注 × 组合数) - full_cover 使用 mbox 模式(总金额平摊) - straight 保持原有 play_code 规则
105 lines
3.7 KiB
PHP
105 lines
3.7 KiB
PHP
<?php
|
|
|
|
use App\Models\OddsItem;
|
|
use App\Models\PlayConfigItem;
|
|
use Illuminate\Support\Collection;
|
|
use App\Services\Ticket\PlayRuleEngine;
|
|
use App\Services\Ticket\NumberNormalizer;
|
|
use App\Exceptions\TicketOperationException;
|
|
|
|
function selectionTypeEngine(): PlayRuleEngine
|
|
{
|
|
return new PlayRuleEngine(new NumberNormalizer);
|
|
}
|
|
|
|
function selectionTypePlayConfig(int $dimension = 4): PlayConfigItem
|
|
{
|
|
return new PlayConfigItem([
|
|
'play_code' => 'big',
|
|
'dimension' => $dimension,
|
|
'min_bet_amount' => 1,
|
|
'max_bet_amount' => 1_000_000,
|
|
]);
|
|
}
|
|
|
|
/** @return Collection<int, OddsItem> */
|
|
function selectionTypeOdds(): Collection
|
|
{
|
|
return collect([
|
|
new OddsItem([
|
|
'provider_code' => 'GLOBAL',
|
|
'prize_scope' => 'first',
|
|
'dimension' => 4,
|
|
'odds_value' => 20_000,
|
|
'rebate_rate' => '0.0000',
|
|
'commission_rate' => '0.0000',
|
|
]),
|
|
]);
|
|
}
|
|
|
|
/** @return array<string, mixed> */
|
|
function evaluateSelectionType(string $selectionType, int $amount = 100, string $number = '1234'): array
|
|
{
|
|
return selectionTypeEngine()->evaluateLine(
|
|
[
|
|
'play_code' => 'big',
|
|
'number' => $number,
|
|
'amount' => $amount,
|
|
'selection_type' => $selectionType,
|
|
],
|
|
selectionTypePlayConfig(),
|
|
selectionTypeOdds(),
|
|
);
|
|
}
|
|
|
|
test('selection types expand 4D numbers and apply the correct stake semantics', function (): void {
|
|
$straight = evaluateSelectionType('straight');
|
|
$reverse = evaluateSelectionType('reverse');
|
|
$halfPlay = evaluateSelectionType('half_play');
|
|
$fullCover = evaluateSelectionType('full_cover', 2_400);
|
|
$fullPlay = evaluateSelectionType('full_play');
|
|
|
|
expect($straight['combination_count'])->toBe(1)
|
|
->and($straight['total_bet_amount'])->toBe(100)
|
|
->and($straight['combinations'][0]['number_4d'])->toBe('1234')
|
|
->and($reverse['combination_count'])->toBe(2)
|
|
->and($reverse['total_bet_amount'])->toBe(200)
|
|
->and(collect($reverse['combinations'])->pluck('number_4d')->all())->toBe(['1234', '4321'])
|
|
->and($halfPlay['combination_count'])->toBe(12)
|
|
->and($halfPlay['total_bet_amount'])->toBe(1_200)
|
|
->and($fullCover['combination_count'])->toBe(24)
|
|
->and($fullCover['unit_bet_amount'])->toBe(100)
|
|
->and($fullCover['total_bet_amount'])->toBe(2_400)
|
|
->and($fullPlay['combination_count'])->toBe(24)
|
|
->and($fullPlay['unit_bet_amount'])->toBe(100)
|
|
->and($fullPlay['total_bet_amount'])->toBe(2_400);
|
|
});
|
|
|
|
test('selection types use unique permutations when a number contains repeated digits', function (): void {
|
|
$fullPlay = evaluateSelectionType('full_play', 100, '1122');
|
|
$halfPlay = evaluateSelectionType('half_play', 100, '1232');
|
|
|
|
expect($fullPlay['combination_count'])->toBe(6)
|
|
->and($fullPlay['total_bet_amount'])->toBe(600)
|
|
->and($halfPlay['combination_count'])->toBe(4)
|
|
->and($halfPlay['total_bet_amount'])->toBe(400);
|
|
});
|
|
|
|
test('full cover rejects an amount that cannot be evenly divided across combinations', function (): void {
|
|
expect(fn (): array => evaluateSelectionType('full_cover', 2_399))
|
|
->toThrow(TicketOperationException::class, 'full_cover_amount_not_divisible');
|
|
});
|
|
|
|
test('selection types enforce their dimension availability', function (): void {
|
|
expect(fn (): array => selectionTypeEngine()->evaluateLine(
|
|
[
|
|
'play_code' => 'pos_3a',
|
|
'number' => '123',
|
|
'amount' => 100,
|
|
'selection_type' => 'half_play',
|
|
],
|
|
selectionTypePlayConfig(3),
|
|
selectionTypeOdds(),
|
|
))->toThrow(TicketOperationException::class, 'selection_type_not_allowed');
|
|
});
|