'big', 'dimension' => $dimension, 'min_bet_amount' => 1, 'max_bet_amount' => 1_000_000, ]); } /** @return Collection */ 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 */ 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'); });