fix(ticket): 修正 full_play/half_play/reverse 投注模式的金额计算逻辑
统一通过 $amountModePlayCode 映射选择类型到正确的金额模式: - reverse / half_play / full_play 使用 ibox 模式(单注 × 组合数) - full_cover 使用 mbox 模式(总金额平摊) - straight 保持原有 play_code 规则
This commit is contained in:
104
tests/Unit/PlayRuleEngineSelectionTypeTest.php
Normal file
104
tests/Unit/PlayRuleEngineSelectionTypeTest.php
Normal file
@@ -0,0 +1,104 @@
|
||||
<?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');
|
||||
});
|
||||
Reference in New Issue
Block a user