feat: 拆分开奖与结算审核流程,新增手动结果录入、重开和派彩审批接口
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user