feat: 支持开奖重开与风险池原子扣减,完善投注部分成功流程
This commit is contained in:
@@ -236,6 +236,24 @@ test('ticket place rejects closed draw', function (): void {
|
||||
->assertJsonPath('code', ErrorCode::DrawClosed->value);
|
||||
});
|
||||
|
||||
test('ticket preview and place reject open draw after server close time', function (): void {
|
||||
$player = ticketPlayerWithWallet();
|
||||
$draw = ticketOpenDraw();
|
||||
$draw->update(['close_time' => now()->subSecond()]);
|
||||
|
||||
$this->withHeader('Authorization', 'Bearer dev:'.$player->id)
|
||||
->postJson('/api/v1/ticket/preview', ticketPreviewPayload())
|
||||
->assertStatus(400)
|
||||
->assertJsonPath('code', ErrorCode::DrawClosed->value);
|
||||
|
||||
$this->withHeader('Authorization', 'Bearer dev:'.$player->id)
|
||||
->postJson('/api/v1/ticket/place', ticketPreviewPayload())
|
||||
->assertStatus(400)
|
||||
->assertJsonPath('code', ErrorCode::DrawClosed->value);
|
||||
|
||||
expect(TicketOrder::query()->count())->toBe(0);
|
||||
});
|
||||
|
||||
test('ticket place rejects insufficient balance', function (): void {
|
||||
$player = ticketPlayerWithWallet(1_000);
|
||||
ticketOpenDraw();
|
||||
@@ -324,6 +342,58 @@ test('ticket place rejects sold out risk pool', function (): void {
|
||||
expect(TicketOrder::query()->count())->toBe(0);
|
||||
});
|
||||
|
||||
test('ticket place can return mixed success and failed risk results', function (): void {
|
||||
$player = ticketPlayerWithWallet(500_000);
|
||||
$draw = ticketOpenDraw();
|
||||
|
||||
RiskPool::query()->create([
|
||||
'draw_id' => $draw->id,
|
||||
'normalized_number' => '1234',
|
||||
'total_cap_amount' => 5000,
|
||||
'locked_amount' => 0,
|
||||
'remaining_amount' => 5000,
|
||||
'sold_out_status' => 0,
|
||||
'version' => 0,
|
||||
]);
|
||||
|
||||
RiskPool::query()->create([
|
||||
'draw_id' => $draw->id,
|
||||
'normalized_number' => '5678',
|
||||
'total_cap_amount' => 100,
|
||||
'locked_amount' => 100,
|
||||
'remaining_amount' => 0,
|
||||
'sold_out_status' => 1,
|
||||
'version' => 1,
|
||||
]);
|
||||
|
||||
$this->withHeader('Authorization', 'Bearer dev:'.$player->id)
|
||||
->postJson('/api/v1/ticket/place', [
|
||||
'draw_id' => '20260511-001',
|
||||
'currency_code' => 'NPR',
|
||||
'client_trace_id' => 'trace-mixed-risk',
|
||||
'lines' => [
|
||||
['number' => '1234', 'play_code' => 'big', 'amount' => 120],
|
||||
['number' => '5678', 'play_code' => 'big', 'amount' => 120],
|
||||
],
|
||||
])
|
||||
->assertOk()
|
||||
->assertJsonPath('code', ErrorCode::Success->value)
|
||||
->assertJsonPath('data.summary.success_count', 1)
|
||||
->assertJsonPath('data.summary.failure_count', 1)
|
||||
->assertJsonPath('data.items.0.status', 'success')
|
||||
->assertJsonPath('data.items.1.status', 'failed')
|
||||
->assertJsonPath('data.items.1.fail_reason_code', (string) ErrorCode::RiskPoolSoldOut->value);
|
||||
|
||||
$order = TicketOrder::query()->firstOrFail();
|
||||
expect($order->status)->toBe('partial_failed')
|
||||
->and((int) $order->total_actual_deduct)->toBe(120)
|
||||
->and(TicketItem::query()->where('status', 'success')->count())->toBe(1)
|
||||
->and(TicketItem::query()->where('status', 'failed')->count())->toBe(1);
|
||||
|
||||
$wallet = PlayerWallet::query()->where('player_id', $player->id)->firstOrFail();
|
||||
expect((int) $wallet->balance)->toBe(500_000 - 120);
|
||||
});
|
||||
|
||||
test('ticket place rejects disabled play from active catalog', function (): void {
|
||||
$player = ticketPlayerWithWallet();
|
||||
ticketOpenDraw();
|
||||
@@ -424,10 +494,10 @@ test('ticket preview rejects invalid line amount per validation rules', function
|
||||
});
|
||||
|
||||
/**
|
||||
* §13.5 风险占用回滚:同一订单两行共享号码时,preview 各自通过;首条 acquire 后剩余不足则第二条失败,整单事务回滚。
|
||||
* §10.1.2 混合成功失败:同一订单两行共享号码时,首条占用成功,第二条额度不足则该注项失败。
|
||||
* big + amount 120 → estimated_payout 3000(maxOdds 250000 / 10000 = 25)。
|
||||
*/
|
||||
test('ticket place rolls back order wallet and risk locks when mid-order acquire fails', function (): void {
|
||||
test('ticket place records partial failed when mid-order acquire fails', function (): void {
|
||||
$player = ticketPlayerWithWallet(500_000);
|
||||
$draw = ticketOpenDraw();
|
||||
|
||||
@@ -453,21 +523,24 @@ test('ticket place rolls back order wallet and risk locks when mid-order acquire
|
||||
|
||||
$this->withHeader('Authorization', 'Bearer dev:'.$player->id)
|
||||
->postJson('/api/v1/ticket/place', $payload)
|
||||
->assertStatus(400)
|
||||
->assertJsonPath('code', ErrorCode::RiskPoolSoldOut->value);
|
||||
->assertOk()
|
||||
->assertJsonPath('code', ErrorCode::Success->value)
|
||||
->assertJsonPath('data.summary.success_count', 1)
|
||||
->assertJsonPath('data.summary.failure_count', 1);
|
||||
|
||||
expect(TicketOrder::query()->count())->toBe(0);
|
||||
expect(WalletTxn::query()->where('biz_type', 'bet_deduct')->count())->toBe(0);
|
||||
expect(TicketOrder::query()->count())->toBe(1);
|
||||
expect(TicketOrder::query()->firstOrFail()->status)->toBe('partial_failed');
|
||||
expect(WalletTxn::query()->where('biz_type', 'bet_deduct')->count())->toBe(1);
|
||||
|
||||
$wallet = PlayerWallet::query()->where('player_id', $player->id)->firstOrFail();
|
||||
expect((int) $wallet->balance)->toBe(500_000);
|
||||
expect((int) $wallet->balance)->toBe(500_000 - 120);
|
||||
|
||||
$pool = RiskPool::query()
|
||||
->where('draw_id', $draw->id)
|
||||
->where('normalized_number', '1234')
|
||||
->firstOrFail();
|
||||
expect((int) $pool->remaining_amount)->toBe(5000);
|
||||
expect((int) $pool->locked_amount)->toBe(0);
|
||||
expect((int) $pool->remaining_amount)->toBe(2000);
|
||||
expect((int) $pool->locked_amount)->toBe(3000);
|
||||
});
|
||||
|
||||
/** §13.5 并发下注(顺序挤出):先成功者占用额度,后一盘同一号码收到售罄。 */
|
||||
|
||||
Reference in New Issue
Block a user