test(config): align current odds and settlement contracts

This commit is contained in:
wchino
2026-07-21 23:41:18 +08:00
parent 457d2cc9e5
commit 150c3e7ebd
11 changed files with 93 additions and 163 deletions

View File

@@ -6,34 +6,34 @@
use App\Models\Draw;
use App\Models\Player;
use App\Models\AdminUser;
use App\Models\OddsItem;
use App\Models\RiskPool;
use App\Models\AdminUser;
use App\Models\WalletTxn;
use App\Lottery\ErrorCode;
use App\Models\TicketItem;
use App\Lottery\DrawStatus;
use App\Models\JackpotPool;
use App\Models\TicketOrder;
use App\Models\OddsItem;
use App\Models\PlayerWallet;
use App\Models\DrawResultItem;
use App\Models\DrawResultBatch;
use App\Models\JackpotPayoutLog;
use App\Models\SettlementBatch;
use App\Models\JackpotPayoutLog;
use App\Models\TicketCombination;
use App\Models\JackpotContribution;
use App\Support\OddsStandardScopes;
use Database\Seeders\CurrencySeeder;
use Database\Seeders\PlayTypeSeeder;
use Illuminate\Support\Facades\Hash;
use App\Lottery\DrawResultBatchStatus;
use App\Models\TicketSettlementDetail;
use App\Services\Settlement\SettlementBatchWorkflowService;
use App\Services\Draw\DrawPrizeLayout;
use Illuminate\Support\Facades\Hash;
use Database\Seeders\LotterySettingsSeeder;
use Database\Seeders\OperationalConfigV1Seeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use App\Services\Settlement\SettlementOrchestrator;
use App\Services\Settlement\SettlementBatchWorkflowService;
uses(RefreshDatabase::class);
@@ -228,7 +228,7 @@ test('§14.5 small hits second tier only', function (): void {
$item = TicketItem::query()->where('draw_id', $draw->id)->firstOrFail();
$deduct = (int) $item->actual_deduct_amount;
$expectedWin = (int) floor(10_000 * OddsStandardScopes::PRESET_ODDS_BY_SCOPE['second'] / 10_000);
$expectedWin = (int) floor(10_000 * OddsStandardScopes::presetOddsForPlay('small')['second'] / 10_000);
p145_publish_board($draw, function (string $t, int $i): string {
return match ($t) {
@@ -321,7 +321,7 @@ test('§14.5 pos_4b pos_3a pos_2a pos_4e each settle with expected win', functio
$item = TicketItem::query()->where('draw_id', $draw->id)->firstOrFail();
$deduct = (int) $item->actual_deduct_amount;
$odds = OddsStandardScopes::PRESET_ODDS_BY_SCOPE[$case['scope']];
$odds = OddsStandardScopes::presetOddsForPlay($case['play'])[$case['scope']];
$perComboWin = (int) floor(10_000 * $odds / 10_000);
$expectedWin = $perComboWin;
@@ -357,26 +357,6 @@ test('module 6 suffix plays settle once per ticket item instead of once per expa
'board' => fn (string $t, int $i): string => $t === 'first' ? '1234' : p145_board_without_8888($t, $i),
'scope' => 'first',
],
[
'play' => 'pos_3abc',
'number' => '567',
'board' => fn (string $t, int $i): string => match ($t) {
'first' => '4567',
default => p145_board_without_8888($t, $i),
},
'scope' => 'first',
],
[
'play' => 'pos_2abc',
'number' => '99',
'board' => fn (string $t, int $i): string => match ($t) {
'first' => '8899',
'second' => '2299',
'third' => '1199',
default => p145_board_without_8888($t, $i),
},
'scope' => 'first',
],
];
foreach ($cases as $case) {
@@ -397,7 +377,7 @@ test('module 6 suffix plays settle once per ticket item instead of once per expa
$item = TicketItem::query()->where('draw_id', $draw->id)->firstOrFail();
expect((int) $item->combination_count)->toBeIn([10, 100]);
$expectedWin = (int) floor(10_000 * OddsStandardScopes::PRESET_ODDS_BY_SCOPE[$case['scope']] / 10_000);
$expectedWin = (int) floor(10_000 * OddsStandardScopes::presetOddsForPlay($case['play'])[$case['scope']] / 10_000);
p145_publish_board($draw, $case['board']);
$draw->forceFill([
@@ -414,77 +394,28 @@ test('module 6 suffix plays settle once per ticket item instead of once per expa
}
});
test('module 6 abc suffix plays pick best tier when multiple prize tiers share the same suffix', function (): void {
test('legacy abc suffix plays stay closed when absent from the active catalog', function (): void {
$cases = [
[
'play' => 'pos_3abc',
'number' => '234',
'board' => fn (string $t, int $i): string => match ($t) {
'first' => '1234',
'second' => '5234',
'third' => '9234',
default => p145_board_without_8888($t, $i),
},
'expected_tier' => 'first',
],
[
'play' => 'pos_3abc',
'number' => '234',
'board' => fn (string $t, int $i): string => match ($t) {
'first' => '1567',
'second' => '5234',
'third' => '8234',
default => p145_board_without_8888($t, $i),
},
'expected_tier' => 'second',
],
[
'play' => 'pos_2abc',
'number' => '99',
'board' => fn (string $t, int $i): string => match ($t) {
'first' => '8899',
'second' => '2299',
'third' => '1199',
default => p145_board_without_8888($t, $i),
},
'expected_tier' => 'first',
],
['play' => 'pos_3abc', 'number' => '234'],
['play' => 'pos_2abc', 'number' => '99'],
];
foreach ($cases as $case) {
$player = p145_player(80_000_000);
$drawNo = p145_next_draw_no();
$draw = p145_draw($drawNo, random_int(1, 99_999));
$player = p145_player(80_000_000);
$drawNo = p145_next_draw_no();
p145_draw($drawNo, random_int(1, 99_999));
foreach ($cases as $case) {
$this->withHeader('Authorization', 'Bearer dev:'.$player->id)
->postJson('/api/v1/ticket/place', [
'draw_id' => $drawNo,
'currency_code' => 'NPR',
'client_trace_id' => 'module6-multi-tier-'.$case['play'].'-'.$case['expected_tier'].'-'.uniqid('', true),
'client_trace_id' => 'module6-legacy-closed-'.$case['play'].'-'.uniqid('', true),
'lines' => [
['number' => $case['number'], 'play_code' => $case['play'], 'amount' => 10_000],
],
])
->assertOk();
$item = TicketItem::query()->where('draw_id', $draw->id)->firstOrFail();
$expectedWin = (int) floor(10_000 * OddsStandardScopes::PRESET_ODDS_BY_SCOPE[$case['expected_tier']] / 10_000);
p145_publish_board($draw, $case['board']);
$draw->forceFill([
'status' => DrawStatus::Settling->value,
'current_result_version' => 1,
])->save();
expect(app(SettlementOrchestrator::class)->trySettleDraw($draw->fresh()), $case['play'])->toBeTrue();
p145_approve_and_payout($draw);
$item->refresh();
$detail = TicketSettlementDetail::query()->where('ticket_item_id', $item->id)->firstOrFail();
expect($item->status)->toBe('settled_win', $case['play'])
->and((int) $item->win_amount)->toBe($expectedWin, $case['play'])
->and($detail->matched_prize_tier)->toBe($case['expected_tier'], $case['play']);
->assertStatus(400)
->assertJsonPath('code', ErrorCode::PlayModeClosed->value);
}
});
@@ -509,8 +440,8 @@ test('module 6 ibox sums payout across combinations hitting different prize tier
expect($deduct)->toBe(600)
->and((int) $item->combination_count)->toBe(6);
$unitWinFirst = (int) floor(100 * OddsStandardScopes::PRESET_ODDS_BY_SCOPE['first'] / 10_000);
$unitWinStarter = (int) floor(100 * OddsStandardScopes::PRESET_ODDS_BY_SCOPE['starter'] / 10_000);
$unitWinFirst = (int) floor(100 * OddsStandardScopes::presetOddsForPlay('ibox')['first'] / 10_000);
$unitWinStarter = (int) floor(100 * OddsStandardScopes::presetOddsForPlay('ibox')['starter'] / 10_000);
$expectedWin = $unitWinFirst + $unitWinStarter;
p145_publish_board($draw, function (string $t, int $i): string {
@@ -574,7 +505,7 @@ test('module 6 mbox remainder deducts floored total and settles win on per-combi
->and((int) $item->actual_deduct_amount)->toBe($expectedDeduct)
->and($ruleSnapshot['rounding_refund_amount'] ?? null)->toBe($expectedRemainder);
$expectedWin = (int) floor($unitBet * OddsStandardScopes::PRESET_ODDS_BY_SCOPE['first'] / 10_000);
$expectedWin = (int) floor($unitBet * OddsStandardScopes::presetOddsForPlay('mbox')['first'] / 10_000);
p145_publish_board($draw, fn (string $t, int $i): string => $t === 'first' ? '1234' : p145_board_without_8888($t, $i));
$draw->forceFill([
@@ -699,9 +630,9 @@ test('§14.5 placement partial failure only deducts successful lines when mid-or
RiskPool::query()->create([
'draw_id' => $draw->id,
'normalized_number' => '1234',
'total_cap_amount' => 5000,
'total_cap_amount' => 500_000,
'locked_amount' => 0,
'remaining_amount' => 5000,
'remaining_amount' => 500_000,
'sold_out_status' => 0,
'version' => 0,
]);
@@ -732,8 +663,8 @@ test('§14.5 placement partial failure only deducts successful lines when mid-or
->where('draw_id', $draw->id)
->where('normalized_number', '1234')
->firstOrFail();
expect((int) $pool->remaining_amount)->toBe(2000);
expect((int) $pool->locked_amount)->toBe(3000);
expect((int) $pool->remaining_amount)->toBe(200_000);
expect((int) $pool->locked_amount)->toBe(300_000);
});
test('§14.5 settlement uses odds snapshot even if odds config changes after placement', function (): void {
@@ -977,18 +908,6 @@ test('§14.5 straight roll box ibox mbox head tail odd even digit pos variants s
'scope' => 'third',
'comboMultiplier' => 1,
],
[
'play' => 'pos_3abc',
'line' => ['number' => '567', 'play_code' => 'pos_3abc', 'amount' => 10_000],
'board' => fn (string $t, int $i): string => match ($t) {
'first' => '4567',
'second' => '8123',
'third' => '9234',
default => p145_board_without_8888($t, $i),
},
'scope' => 'first',
'comboMultiplier' => 1,
],
[
'play' => 'pos_2b',
'line' => ['number' => '56', 'play_code' => 'pos_2b', 'amount' => 10_000],
@@ -1012,18 +931,6 @@ test('§14.5 straight roll box ibox mbox head tail odd even digit pos variants s
'scope' => 'third',
'comboMultiplier' => 1,
],
[
'play' => 'pos_2abc',
'line' => ['number' => '99', 'play_code' => 'pos_2abc', 'amount' => 100],
'board' => fn (string $t, int $i): string => match ($t) {
'first' => '8899',
'second' => '2299',
'third' => '1199',
default => p145_board_without_8888($t, $i),
},
'scope' => 'first',
'comboMultiplier' => 1,
],
];
foreach ($cases as $case) {
@@ -1042,7 +949,7 @@ test('§14.5 straight roll box ibox mbox head tail odd even digit pos variants s
$item = TicketItem::query()->where('draw_id', $draw->id)->firstOrFail();
$deduct = (int) $item->actual_deduct_amount;
$odds = OddsStandardScopes::PRESET_ODDS_BY_SCOPE[$case['scope']];
$odds = OddsStandardScopes::presetOddsForPlay($case['play'])[$case['scope']];
$unitOnTicket = (int) $item->unit_bet_amount;
$perComboWin = (int) floor($unitOnTicket * $odds / 10_000);
$expectedWin = $perComboWin * (int) $case['comboMultiplier'];