feat: 拆分开奖与结算审核流程,新增手动结果录入、重开和派彩审批接口

This commit is contained in:
2026-05-16 18:01:06 +08:00
parent 83046b402d
commit 4f143c7cb1
38 changed files with 1992 additions and 170 deletions

View File

@@ -6,6 +6,7 @@
use App\Models\Draw;
use App\Models\Player;
use App\Models\AdminUser;
use App\Models\RiskPool;
use App\Models\WalletTxn;
use App\Lottery\ErrorCode;
@@ -17,6 +18,7 @@ use App\Models\PlayerWallet;
use App\Models\DrawResultItem;
use App\Models\DrawResultBatch;
use App\Models\JackpotPayoutLog;
use App\Models\SettlementBatch;
use App\Models\TicketCombination;
use App\Models\JackpotContribution;
use App\Support\OddsStandardScopes;
@@ -24,7 +26,9 @@ use Database\Seeders\CurrencySeeder;
use Database\Seeders\PlayTypeSeeder;
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;
@@ -135,6 +139,22 @@ function p145_board_without_8888(string $prizeType, int $prizeIndex): string
};
}
function p145_approve_and_payout(Draw $draw): void
{
$batch = SettlementBatch::query()->where('draw_id', $draw->id)->latest('id')->firstOrFail();
$admin = AdminUser::query()->create([
'username' => 'p145_settle_'.bin2hex(random_bytes(3)),
'name' => 'P145 Settlement',
'email' => null,
'password' => Hash::make('secret-strong'),
'status' => 0,
]);
grantSuperAdminRole($admin);
$workflow = app(SettlementBatchWorkflowService::class);
$workflow->approve($batch, $admin);
$workflow->payout($batch->fresh());
}
test('§14.5 big no-hit settles lose wallet unchanged except bet and no settle_payout txn', function (): void {
$player = p145_player();
$drawNo = p145_next_draw_no();
@@ -161,6 +181,7 @@ test('§14.5 big no-hit settles lose wallet unchanged except bet and no settle_p
])->save();
expect(app(SettlementOrchestrator::class)->trySettleDraw($draw->fresh()))->toBeTrue();
p145_approve_and_payout($draw);
$item->refresh();
expect($item->status)->toBe('settled_lose')
@@ -224,6 +245,7 @@ test('§14.5 small hits second tier only', function (): void {
])->save();
expect(app(SettlementOrchestrator::class)->trySettleDraw($draw->fresh()))->toBeTrue();
p145_approve_and_payout($draw);
$item->refresh();
expect($item->status)->toBe('settled_win')
@@ -300,11 +322,7 @@ test('§14.5 pos_4b pos_3a pos_2a pos_4e each settle with expected win', functio
$deduct = (int) $item->actual_deduct_amount;
$odds = OddsStandardScopes::PRESET_ODDS_BY_SCOPE[$case['scope']];
$perComboWin = (int) floor(10_000 * $odds / 10_000);
$comboCount = (int) $item->combination_count;
$expectedWin = match ($case['play']) {
'pos_3a', 'pos_2a' => $perComboWin * $comboCount,
default => $perComboWin,
};
$expectedWin = $perComboWin;
p145_publish_board($draw, $case['board']);
$draw->forceFill([
@@ -313,6 +331,7 @@ test('§14.5 pos_4b pos_3a pos_2a pos_4e each settle with expected win', functio
])->save();
expect(app(SettlementOrchestrator::class)->trySettleDraw($draw->fresh()))->toBeTrue();
p145_approve_and_payout($draw);
$item->refresh();
expect($item->status)->toBe('settled_win', $case['play'])
@@ -323,6 +342,77 @@ test('§14.5 pos_4b pos_3a pos_2a pos_4e each settle with expected win', functio
}
});
test('module 6 suffix plays settle once per ticket item instead of once per expanded prefix', function (): void {
$cases = [
[
'play' => 'pos_3a',
'number' => '234',
'board' => fn (string $t, int $i): string => $t === 'first' ? '1234' : p145_board_without_8888($t, $i),
'scope' => 'first',
],
[
'play' => 'pos_2a',
'number' => '34',
'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) {
$player = p145_player(80_000_000);
$drawNo = p145_next_draw_no();
$draw = p145_draw($drawNo, random_int(1, 99_999));
$this->withHeader('Authorization', 'Bearer dev:'.$player->id)
->postJson('/api/v1/ticket/place', [
'draw_id' => $drawNo,
'currency_code' => 'NPR',
'client_trace_id' => 'module6-suffix-'.$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();
expect((int) $item->combination_count)->toBeIn([10, 100]);
$expectedWin = (int) floor(10_000 * OddsStandardScopes::PRESET_ODDS_BY_SCOPE[$case['scope']] / 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();
expect($item->status)->toBe('settled_win', $case['play'])
->and((int) $item->win_amount)->toBe($expectedWin, $case['play']);
}
});
test('§14.5 jackpot contributes on place and stays in pool when no first-prize burst', function (): void {
JackpotPool::query()->create([
'currency_code' => 'NPR',
@@ -371,6 +461,7 @@ test('§14.5 jackpot contributes on place and stays in pool when no first-prize
])->save();
expect(app(SettlementOrchestrator::class)->trySettleDraw($draw->fresh()))->toBeTrue();
p145_approve_and_payout($draw);
expect(JackpotPayoutLog::query()->count())->toBe(0);
$poolAfter = JackpotPool::query()->where('currency_code', 'NPR')->firstOrFail();
@@ -532,7 +623,7 @@ test('§14.5 straight roll box ibox mbox head tail odd even digit pos variants s
default => p145_board_without_8888($t, $i),
},
'scope' => 'second',
'comboMultiplier' => 10,
'comboMultiplier' => 1,
],
[
'play' => 'pos_3c',
@@ -542,7 +633,7 @@ test('§14.5 straight roll box ibox mbox head tail odd even digit pos variants s
default => p145_board_without_8888($t, $i),
},
'scope' => 'third',
'comboMultiplier' => 10,
'comboMultiplier' => 1,
],
[
'play' => 'pos_3abc',
@@ -554,7 +645,7 @@ test('§14.5 straight roll box ibox mbox head tail odd even digit pos variants s
default => p145_board_without_8888($t, $i),
},
'scope' => 'first',
'comboMultiplier' => 10,
'comboMultiplier' => 1,
],
[
'play' => 'pos_2b',
@@ -565,7 +656,7 @@ test('§14.5 straight roll box ibox mbox head tail odd even digit pos variants s
default => p145_board_without_8888($t, $i),
},
'scope' => 'second',
'comboMultiplier' => 100,
'comboMultiplier' => 1,
],
[
'play' => 'pos_2c',
@@ -577,7 +668,7 @@ test('§14.5 straight roll box ibox mbox head tail odd even digit pos variants s
default => p145_board_without_8888($t, $i),
},
'scope' => 'third',
'comboMultiplier' => 100,
'comboMultiplier' => 1,
],
[
'play' => 'pos_2abc',
@@ -589,7 +680,7 @@ test('§14.5 straight roll box ibox mbox head tail odd even digit pos variants s
default => p145_board_without_8888($t, $i),
},
'scope' => 'first',
'comboMultiplier' => 100,
'comboMultiplier' => 1,
],
];
@@ -631,6 +722,7 @@ test('§14.5 straight roll box ibox mbox head tail odd even digit pos variants s
])->save();
expect(app(SettlementOrchestrator::class)->trySettleDraw($draw->fresh()), $case['play'])->toBeTrue();
p145_approve_and_payout($draw);
$item->refresh();
expect($item->status)->toBe('settled_win', $case['play'])
@@ -672,6 +764,7 @@ test('§14.6 ticket detail shows settlement tier after win', function (): void {
])->save();
app(SettlementOrchestrator::class)->trySettleDraw($draw->fresh());
p145_approve_and_payout($draw);
$this->withHeader('Authorization', 'Bearer dev:'.$player->id)
->getJson('/api/v1/ticket/items/'.$ticketNo)