Files
lotteryLaravel/tests/Unit/PlayRuleEngineSelectionTypeTest.php
kang cec2fd7c4f
Some checks failed
lotterLaravel CI / test (push) Has been cancelled
lotterLaravel E2E / e2e-api (push) Has been cancelled
fix(ticket): 修正 full_play/half_play/reverse 投注模式的金额计算逻辑
统一通过 $amountModePlayCode 映射选择类型到正确的金额模式:
- reverse / half_play / full_play 使用 ibox 模式(单注 × 组合数)
- full_cover 使用 mbox 模式(总金额平摊)
- straight 保持原有 play_code 规则
2026-07-15 12:02:10 +08:00

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');
});