From cec2fd7c4f90cf6219b683e2045d70b9674bf013 Mon Sep 17 00:00:00 2001 From: kang Date: Wed, 15 Jul 2026 12:02:10 +0800 Subject: [PATCH] =?UTF-8?q?fix(ticket):=20=E4=BF=AE=E6=AD=A3=20full=5Fplay?= =?UTF-8?q?/half=5Fplay/reverse=20=E6=8A=95=E6=B3=A8=E6=A8=A1=E5=BC=8F?= =?UTF-8?q?=E7=9A=84=E9=87=91=E9=A2=9D=E8=AE=A1=E7=AE=97=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 统一通过 $amountModePlayCode 映射选择类型到正确的金额模式: - reverse / half_play / full_play 使用 ibox 模式(单注 × 组合数) - full_cover 使用 mbox 模式(总金额平摊) - straight 保持原有 play_code 规则 --- app/Services/Ticket/PlayRuleEngine.php | 13 ++- .../Unit/PlayRuleEngineSelectionTypeTest.php | 104 ++++++++++++++++++ 2 files changed, 115 insertions(+), 2 deletions(-) create mode 100644 tests/Unit/PlayRuleEngineSelectionTypeTest.php diff --git a/app/Services/Ticket/PlayRuleEngine.php b/app/Services/Ticket/PlayRuleEngine.php index 7df4684..89b1db1 100644 --- a/app/Services/Ticket/PlayRuleEngine.php +++ b/app/Services/Ticket/PlayRuleEngine.php @@ -59,8 +59,17 @@ final class PlayRuleEngine throw new TicketOperationException('full_cover_amount_not_divisible', ErrorCode::BetInvalidPlayInput->value); } - $unitBetAmount = $this->resolveUnitBetAmount($selectionType === 'full_cover' ? 'mbox' : $playCode, $amount, $combinationCount); - $totalBetAmount = $this->resolveTotalBetAmount($selectionType === 'full_cover' ? 'mbox' : $playCode, $amount, $unitBetAmount, $combinationCount); + // 种类金额模式: + // - full_cover(全保)→ 总金额平摊(mbox) + // - reverse / half_play / full_play → 单注 × 组合数(ibox) + // - straight → 沿用 play_code 自身规则 + $amountModePlayCode = match ($selectionType) { + 'full_cover' => 'mbox', + 'full_play', 'reverse', 'half_play' => 'ibox', + default => $playCode, + }; + $unitBetAmount = $this->resolveUnitBetAmount($amountModePlayCode, $amount, $combinationCount); + $totalBetAmount = $this->resolveTotalBetAmount($amountModePlayCode, $amount, $unitBetAmount, $combinationCount); $dimensionInt = $this->toDimensionInt(is_string($dimension) ? $dimension : null, $playConfig); $primaryOdds = $this->pickPrimaryOdds($oddsItems); $rebateRate = (float) $primaryOdds->rebate_rate; diff --git a/tests/Unit/PlayRuleEngineSelectionTypeTest.php b/tests/Unit/PlayRuleEngineSelectionTypeTest.php new file mode 100644 index 0000000..5763000 --- /dev/null +++ b/tests/Unit/PlayRuleEngineSelectionTypeTest.php @@ -0,0 +1,104 @@ + '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'); +});