create([ 'username' => 'settlement_manager', 'name' => 'Settlement Manager', 'email' => null, 'password' => Hash::make('secret-strong'), 'status' => 0, ]); $role = AdminRole::query()->create([ 'slug' => 'settlement_manager_role', 'name' => 'Settlement Manager Role', ]); $role->syncLegacyPermissionSlugs(['prd.payout.manage']); $admin->roles()->sync([ (int) $role->id => [ 'site_id' => AdminUser::defaultAdminSiteId(), 'granted_at' => now(), ], ]); return $admin->createToken('test', ['*'], now()->addDay())->plainTextToken; } test('admin can apply settlement payout adjustment for paid batch and audit it', function (): void { $token = settlementPayoutManagerToken(); $managerId = (int) AdminUser::query()->where('username', 'settlement_manager')->value('id'); $draw = Draw::query()->create([ 'draw_no' => '20260603-001', 'business_date' => '2026-06-03', 'sequence_no' => 1, 'status' => DrawStatus::Settled->value, 'start_time' => now()->subHour(), 'close_time' => now()->subMinutes(30), 'draw_time' => now()->subMinutes(20), 'cooling_end_time' => now()->subMinutes(10), 'result_source' => 'manual', 'current_result_version' => 1, 'settle_version' => 1, 'is_reopened' => false, ]); $player = Player::query()->create([ 'site_code' => 'main', 'site_player_id' => 'settlement-adjust-player', 'username' => 'settle_player', 'nickname' => null, 'default_currency' => 'NPR', 'status' => 0, ]); $wallet = PlayerWallet::query()->create([ 'player_id' => $player->id, 'wallet_type' => 'lottery', 'currency_code' => 'NPR', 'balance' => 1_000, 'frozen_balance' => 0, 'status' => 0, 'version' => 0, ]); $order = TicketOrder::query()->create([ 'order_no' => 'TO-SETTLE-ADJUST-1', 'player_id' => $player->id, 'draw_id' => $draw->id, 'currency_code' => 'NPR', 'total_bet_amount' => 100, 'total_rebate_amount' => 0, 'total_actual_deduct' => 100, 'total_estimated_payout' => 500, 'status' => 'settled', 'submit_source' => 'h5', 'client_trace_id' => 'settlement-adjust-trace', ]); $item = TicketItem::query()->create([ 'ticket_no' => 'TK-SETTLE-ADJUST-1', 'order_id' => $order->id, 'player_id' => $player->id, 'draw_id' => $draw->id, 'original_number' => '1234', 'normalized_number' => '1234', 'play_code' => 'big', 'dimension' => 4, 'digit_slot' => null, 'bet_mode' => 'straight', 'unit_bet_amount' => 100, 'total_bet_amount' => 100, 'rebate_rate_snapshot' => 0, 'commission_rate_snapshot' => 0, 'actual_deduct_amount' => 100, 'odds_snapshot_json' => [], 'rule_snapshot_json' => [], 'combination_count' => 1, 'estimated_max_payout' => 500, 'risk_locked_amount' => 0, 'status' => 'settled_win', 'fail_reason_code' => null, 'fail_reason_text' => null, 'win_amount' => 500, 'jackpot_win_amount' => 0, 'settled_at' => now()->subMinutes(5), ]); $resultBatch = DrawResultBatch::query()->create([ 'draw_id' => $draw->id, 'result_version' => 1, 'source_type' => 'manual', 'rng_seed_hash' => null, 'raw_seed_encrypted' => null, 'status' => 'published', 'created_by' => $managerId, 'confirmed_by' => $managerId, 'confirmed_at' => now()->subMinutes(9), ]); $batch = SettlementBatch::query()->create([ 'draw_id' => $draw->id, 'result_batch_id' => $resultBatch->id, 'settle_version' => 1, 'status' => 'paid', 'total_ticket_count' => 1, 'total_win_count' => 1, 'total_payout_amount' => 500, 'total_jackpot_payout_amount' => 0, 'review_status' => 'approved', 'reviewed_by' => $managerId, 'reviewed_at' => now()->subMinutes(6), 'review_remark' => 'approved', 'paid_at' => now()->subMinutes(5), 'started_at' => now()->subMinutes(8), 'finished_at' => now()->subMinutes(7), ]); TicketSettlementDetail::query()->create([ 'settlement_batch_id' => $batch->id, 'ticket_item_id' => $item->id, 'matched_prize_tier' => '1st', 'win_amount' => 500, 'jackpot_allocation_amount' => 0, 'match_detail_json' => ['numbers' => ['1234']], ]); $this->withHeader('Authorization', 'Bearer '.$token) ->postJson("/api/v1/admin/settlement-batches/{$batch->id}/adjustments", [ 'player_id' => $player->id, 'amount_delta' => 120, 'reason' => 'manual payout correction', ]) ->assertOk() ->assertJsonPath('data.batch_id', $batch->id) ->assertJsonPath('data.player_id', $player->id) ->assertJsonPath('data.amount_delta', 120) ->assertJsonPath('data.direction', 'credit'); $wallet->refresh(); expect((int) $wallet->balance)->toBe(1_120) ->and(WalletTxn::query()->where('biz_type', 'settlement_adjustment')->count())->toBe(1) ->and(AuditLog::query()->where('module_code', 'settlement')->where('action_code', 'payout_adjustment')->count())->toBe(1); }); test('admin settlement payout adjustment rejects player outside batch', function (): void { $token = settlementPayoutManagerToken(); $managerId = (int) AdminUser::query()->where('username', 'settlement_manager')->value('id'); $draw = Draw::query()->create([ 'draw_no' => '20260603-002', 'business_date' => '2026-06-03', 'sequence_no' => 2, 'status' => DrawStatus::Settled->value, 'start_time' => now()->subHour(), 'close_time' => now()->subMinutes(30), 'draw_time' => now()->subMinutes(20), 'cooling_end_time' => now()->subMinutes(10), 'result_source' => 'manual', 'current_result_version' => 1, 'settle_version' => 1, 'is_reopened' => false, ]); $resultBatch = DrawResultBatch::query()->create([ 'draw_id' => $draw->id, 'result_version' => 1, 'source_type' => 'manual', 'rng_seed_hash' => null, 'raw_seed_encrypted' => null, 'status' => 'published', 'created_by' => $managerId, 'confirmed_by' => $managerId, 'confirmed_at' => now()->subMinutes(9), ]); $batch = SettlementBatch::query()->create([ 'draw_id' => $draw->id, 'result_batch_id' => $resultBatch->id, 'settle_version' => 1, 'status' => 'paid', 'total_ticket_count' => 0, 'total_win_count' => 0, 'total_payout_amount' => 0, 'total_jackpot_payout_amount' => 0, 'review_status' => 'approved', 'reviewed_by' => $managerId, 'reviewed_at' => now()->subMinutes(6), 'review_remark' => 'approved', 'paid_at' => now()->subMinutes(5), 'started_at' => now()->subMinutes(8), 'finished_at' => now()->subMinutes(7), ]); $player = Player::query()->create([ 'site_code' => 'main', 'site_player_id' => 'not-in-batch', 'username' => null, 'nickname' => null, 'default_currency' => 'NPR', 'status' => 0, ]); $this->withHeader('Authorization', 'Bearer '.$token) ->postJson("/api/v1/admin/settlement-batches/{$batch->id}/adjustments", [ 'player_id' => $player->id, 'amount_delta' => 80, 'reason' => 'should reject', ]) ->assertStatus(422); expect(WalletTxn::query()->where('biz_type', 'settlement_adjustment')->count())->toBe(0); });