create([ 'username' => 'guards_admin_'.uniqid(), 'name' => 'Guards Admin', 'email' => null, 'password' => Hash::make('secret-strong'), 'status' => 0, ]); grantSuperAdminRole($admin); return $admin->createToken('test', ['*'], now()->addDay())->plainTextToken; } test('cannot publish result batch while settlement batch is pending review', function (): void { $token = guardsAdminToken(); $draw = Draw::query()->create([ 'draw_no' => '20260526-guard-1', 'business_date' => '2026-05-26', 'sequence_no' => 1, 'status' => DrawStatus::Settling->value, 'start_time' => now()->subHour(), 'close_time' => now()->subMinutes(30), 'draw_time' => now()->subMinutes(20), 'cooling_end_time' => null, 'result_source' => 'manual', 'current_result_version' => 1, 'settle_version' => 1, 'is_reopened' => false, ]); $published = DrawResultBatch::query()->create([ 'draw_id' => $draw->id, 'result_version' => 1, 'source_type' => 'manual', 'rng_seed_hash' => null, 'raw_seed_encrypted' => null, 'status' => DrawResultBatchStatus::Published->value, 'created_by' => null, 'confirmed_by' => null, 'confirmed_at' => now(), ]); $pending = DrawResultBatch::query()->create([ 'draw_id' => $draw->id, 'result_version' => 2, 'source_type' => 'manual', 'rng_seed_hash' => null, 'raw_seed_encrypted' => null, 'status' => DrawResultBatchStatus::PendingReview->value, 'created_by' => null, 'confirmed_by' => null, 'confirmed_at' => null, ]); SettlementBatch::query()->create([ 'draw_id' => $draw->id, 'result_batch_id' => $published->id, 'settle_version' => 1, 'status' => SettlementBatchStatus::PendingReview->value, 'review_status' => 'pending', 'started_at' => now(), ]); $this->withHeader('Authorization', 'Bearer '.$token) ->postJson("/api/v1/admin/draws/{$draw->id}/result-batches/{$pending->id}/publish") ->assertStatus(409); }); test('cannot create second pending result batch for same draw', function (): void { $token = guardsAdminToken(); $draw = Draw::query()->create([ 'draw_no' => '20260526-guard-2', 'business_date' => '2026-05-26', 'sequence_no' => 2, 'status' => DrawStatus::Review->value, 'start_time' => now()->subHour(), 'close_time' => now()->subMinutes(30), 'draw_time' => now()->subMinutes(20), 'cooling_end_time' => null, 'result_source' => 'manual', 'current_result_version' => 0, 'settle_version' => 0, 'is_reopened' => false, ]); $items = []; foreach (DrawPrizeLayout::slots() as $i => $slot) { $items[] = [ 'prize_type' => $slot['prize_type'], 'prize_index' => $slot['prize_index'], 'number_4d' => str_pad((string) ($i + 1), 4, '0', STR_PAD_LEFT), ]; } $this->withHeader('Authorization', 'Bearer '.$token) ->postJson("/api/v1/admin/draws/{$draw->id}/result-batches", ['items' => $items]) ->assertOk(); $this->withHeader('Authorization', 'Bearer '.$token) ->postJson("/api/v1/admin/draws/{$draw->id}/result-batches", ['items' => $items]) ->assertStatus(409); }); test('admin cannot manually process transfer out pending reconcile order', function (): void { $token = guardsAdminToken(); $player = Player::query()->create([ 'site_code' => 'main', 'site_player_id' => 'out-manual-block', 'username' => null, 'nickname' => null, 'default_currency' => 'NPR', 'status' => 0, ]); TransferOrder::query()->create([ 'transfer_no' => 'TO_manual_block', 'player_id' => $player->id, 'direction' => 'out', 'currency_code' => 'NPR', 'amount' => 300, 'idempotent_key' => 'out-manual-block-key', 'status' => 'pending_reconcile', 'external_request_payload' => null, 'external_response_payload' => null, 'external_ref_no' => null, 'fail_reason' => 'main_site_timeout', 'finished_at' => null, ]); $this->withHeader('Authorization', 'Bearer '.$token) ->postJson('/api/v1/admin/wallet/transfer-orders/TO_manual_block/manually-process') ->assertStatus(422); expect(TransferOrder::query()->where('transfer_no', 'TO_manual_block')->value('status')) ->toBe('pending_reconcile'); }); test('admin cannot complete credit for main site timeout transfer in order', function (): void { $token = guardsAdminToken(); $player = Player::query()->create([ 'site_code' => 'main', 'site_player_id' => 'in-complete-block', 'username' => null, 'nickname' => null, 'default_currency' => 'NPR', 'status' => 0, ]); PlayerWallet::query()->create([ 'player_id' => $player->id, 'wallet_type' => 'lottery', 'currency_code' => 'NPR', 'balance' => 100, 'frozen_balance' => 0, 'status' => 0, 'version' => 0, ]); TransferOrder::query()->create([ 'transfer_no' => 'TI_complete_block', 'player_id' => $player->id, 'direction' => 'in', 'currency_code' => 'NPR', 'amount' => 500, 'idempotent_key' => 'in-complete-block-key', 'status' => 'pending_reconcile', 'external_request_payload' => null, 'external_response_payload' => null, 'external_ref_no' => null, 'fail_reason' => 'main_site_timeout', 'finished_at' => null, ]); $this->withHeader('Authorization', 'Bearer '.$token) ->postJson('/api/v1/admin/wallet/transfer-orders/TI_complete_block/complete-credit') ->assertStatus(422); expect((int) PlayerWallet::query()->where('player_id', $player->id)->value('balance'))->toBe(100); }); test('transfer order list hides manual process for out pending reconcile', function (): void { $token = guardsAdminToken(); $player = Player::query()->create([ 'site_code' => 'main', 'site_player_id' => 'list-flags', 'username' => null, 'nickname' => null, 'default_currency' => 'NPR', 'status' => 0, ]); TransferOrder::query()->create([ 'transfer_no' => 'TO_list_flags', 'player_id' => $player->id, 'direction' => 'out', 'currency_code' => 'NPR', 'amount' => 100, 'idempotent_key' => 'list-flags-key', 'status' => 'pending_reconcile', 'external_request_payload' => null, 'external_response_payload' => null, 'external_ref_no' => null, 'fail_reason' => 'main_site_timeout', 'finished_at' => null, ]); $items = $this->withHeader('Authorization', 'Bearer '.$token) ->getJson('/api/v1/admin/wallet/transfer-orders?player_id='.$player->id) ->assertOk() ->json('data.items'); $item = collect($items)->firstWhere('transfer_no', 'TO_list_flags'); expect($item)->not->toBeNull(); expect($item['can_reverse'])->toBeTrue() ->and($item['can_manually_process'])->toBeFalse() ->and($item['can_complete_credit'])->toBeFalse(); });