fix(core): harden settlement and wallet integration
This commit is contained in:
@@ -1,23 +1,24 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Draw;
|
||||
use App\Models\Player;
|
||||
use App\Models\AdminRole;
|
||||
use App\Models\AdminUser;
|
||||
use App\Models\Draw;
|
||||
use App\Models\DrawResultBatch;
|
||||
use App\Models\Player;
|
||||
use App\Models\SettlementBatch;
|
||||
use App\Models\TicketItem;
|
||||
use App\Lottery\DrawResultBatchStatus;
|
||||
use App\Lottery\DrawStatus;
|
||||
use App\Lottery\SettlementBatchStatus;
|
||||
use App\Services\AgentSettlement\AgentGameSettlementRecorder;
|
||||
use App\Services\AgentSettlement\GameSettlementReversalService;
|
||||
use App\Services\Settlement\SettlementBatchWorkflowService;
|
||||
use App\Services\Settlement\SettlementTickFinalizer;
|
||||
use App\Models\DrawResultBatch;
|
||||
use App\Models\SettlementBatch;
|
||||
use App\Support\PlayerFundingMode;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use App\Lottery\DrawResultBatchStatus;
|
||||
use App\Lottery\SettlementBatchStatus;
|
||||
use App\Services\Player\PlayerCreditService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use App\Services\Settlement\SettlementTickFinalizer;
|
||||
use App\Services\Settlement\SettlementBatchWorkflowService;
|
||||
use App\Services\AgentSettlement\AgentGameSettlementRecorder;
|
||||
use App\Services\AgentSettlement\GameSettlementReversalService;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
@@ -132,15 +133,63 @@ test('agent recorder allows re-settlement after share ledger reversal', function
|
||||
$item->setRelation('player', $player);
|
||||
|
||||
$recorder = app(AgentGameSettlementRecorder::class);
|
||||
$recorder->recordForTicketItem($item, 0, 'settled_lose');
|
||||
expect(countActiveShareLedgerRows($item->id))->toBe(1);
|
||||
DB::table('player_credit_accounts')->where('player_id', $player->id)->update(['used_credit' => 1]);
|
||||
|
||||
app(GameSettlementReversalService::class)->reverseTicketItem($item->fresh());
|
||||
expect(countActiveShareLedgerRows($item->id))->toBe(0);
|
||||
$recorder->recordForTicketItem($item, 0, 'settled_lose', 1);
|
||||
expect(countActiveShareLedgerRows($item->id))->toBe(1)
|
||||
->and((int) DB::table('player_credit_accounts')->where('player_id', $player->id)->value('used_credit'))->toBe(1);
|
||||
|
||||
app(GameSettlementReversalService::class)->reverseTicketItem($item->fresh(), 1);
|
||||
expect(countActiveShareLedgerRows($item->id))->toBe(0)
|
||||
->and((int) DB::table('player_credit_accounts')->where('player_id', $player->id)->value('used_credit'))->toBe(1);
|
||||
|
||||
$item->forceFill(['agent_settled_at' => null])->save();
|
||||
$recorder->recordForTicketItem($item->fresh(), 0, 'settled_lose');
|
||||
expect(countActiveShareLedgerRows($item->id))->toBe(1);
|
||||
$recorder->recordForTicketItem($item->fresh(), 0, 'settled_lose', 2);
|
||||
expect(countActiveShareLedgerRows($item->id))->toBe(1)
|
||||
->and((int) DB::table('player_credit_accounts')->where('player_id', $player->id)->value('used_credit'))->toBe(1)
|
||||
->and(DB::table('credit_ledger')
|
||||
->where('ref_type', 'ticket_item')
|
||||
->where('ref_id', $item->id)
|
||||
->where('reason', 'game_settlement_loss')
|
||||
->count())->toBe(2);
|
||||
|
||||
app(GameSettlementReversalService::class)->reverseTicketItem($item->fresh(), 2);
|
||||
expect(countActiveShareLedgerRows($item->id))->toBe(0)
|
||||
->and((int) DB::table('player_credit_accounts')->where('player_id', $player->id)->value('used_credit'))->toBe(1)
|
||||
->and(DB::table('credit_ledger')
|
||||
->where('ref_type', 'ticket_item')
|
||||
->where('ref_id', $item->id)
|
||||
->where('reason', 'game_settlement_reversal')
|
||||
->count())->toBe(2)
|
||||
->and(DB::table('credit_ledger')
|
||||
->where('ref_type', 'ticket_item')
|
||||
->where('ref_id', $item->id)
|
||||
->where('reason', 'bet_hold_restore')
|
||||
->count())->toBe(2);
|
||||
});
|
||||
|
||||
test('credit win re-settlement applies the same direction again for a new version', function (): void {
|
||||
$player = creditAgentPlayerForFixtures('win-re-settle');
|
||||
DB::table('player_credit_accounts')->where('player_id', $player->id)->update(['used_credit' => 10]);
|
||||
|
||||
$credit = app(PlayerCreditService::class);
|
||||
$credit->releaseBetHold($player, 100, 99001, 1);
|
||||
$credit->applySettledWin($player, 300, 99001, 1);
|
||||
expect((int) DB::table('player_credit_accounts')->where('player_id', $player->id)->value('used_credit'))->toBe(6);
|
||||
|
||||
$credit->reverseGameSettlement($player, -300, 99001, 1);
|
||||
$credit->restoreBetHoldAfterSettlementReversal($player, 100, 99001, 1);
|
||||
expect((int) DB::table('player_credit_accounts')->where('player_id', $player->id)->value('used_credit'))->toBe(10);
|
||||
|
||||
$credit->releaseBetHold($player, 100, 99001, 2);
|
||||
$credit->applySettledWin($player, 300, 99001, 2);
|
||||
|
||||
expect((int) DB::table('player_credit_accounts')->where('player_id', $player->id)->value('used_credit'))->toBe(6)
|
||||
->and(DB::table('credit_ledger')
|
||||
->where('ref_type', 'ticket_item')
|
||||
->where('ref_id', 99001)
|
||||
->where('reason', 'game_settlement_win')
|
||||
->count())->toBe(2);
|
||||
});
|
||||
|
||||
test('credit player payout skips lottery wallet settle_payout txn', function (): void {
|
||||
@@ -279,7 +328,7 @@ test('settlement report show rejects inaccessible settlement period', function (
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
test('settlement tick finalizer marks approved batch failed when payout throws', function (): void {
|
||||
test('settlement tick finalizer keeps approved batch retryable when payout throws', function (): void {
|
||||
$player = creditAgentPlayerForFixtures('tick-fail');
|
||||
$draw = Draw::query()->create([
|
||||
'draw_no' => 'FIX-FAIL-DRAW',
|
||||
@@ -363,7 +412,7 @@ test('settlement tick finalizer marks approved batch failed when payout throws',
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
DB::table('ticket_items')->insert([
|
||||
$orphanItemId = (int) DB::table('ticket_items')->insertGetId([
|
||||
'ticket_no' => 'T-FIX-FAIL-ORPHAN',
|
||||
'order_id' => $orderId,
|
||||
'player_id' => $player->id,
|
||||
@@ -394,6 +443,13 @@ test('settlement tick finalizer marks approved batch failed when payout throws',
|
||||
$result = app(SettlementTickFinalizer::class)->finalizePendingBatches();
|
||||
|
||||
expect($result['payout_failed'])->toBe(1)
|
||||
->and($batch->fresh()->status)->toBe(SettlementBatchStatus::Failed->value)
|
||||
->and($batch->fresh()->status)->toBe(SettlementBatchStatus::Approved->value)
|
||||
->and($batch->fresh()->review_remark)->toContain('auto_payout_failed');
|
||||
});
|
||||
|
||||
DB::table('ticket_items')->where('id', $orphanItemId)->delete();
|
||||
$retry = app(SettlementTickFinalizer::class)->finalizePendingBatches();
|
||||
|
||||
expect($retry)->toMatchArray(['approved' => 0, 'paid' => 1, 'payout_failed' => 0])
|
||||
->and($batch->fresh()->status)->toBe(SettlementBatchStatus::Paid->value)
|
||||
->and($draw->fresh()->status)->toBe(DrawStatus::Settled->value);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user