seed(CurrencySeeder::class); $this->seed(PlayTypeSeeder::class); $this->seed(OperationalConfigV1Seeder::class); $this->seed(LotterySettingsSeeder::class); }); function ticketItemsPlayer(): Player { $uniq = bin2hex(random_bytes(4)); $player = Player::query()->create([ 'site_code' => 'test', 'site_player_id' => 'items-p-'.$uniq, 'username' => 'ti_'.$uniq, 'nickname' => null, 'default_currency' => 'NPR', 'status' => 0, ]); PlayerWallet::query()->create([ 'player_id' => $player->id, 'wallet_type' => 'lottery', 'currency_code' => 'NPR', 'balance' => 5_000_000, 'frozen_balance' => 0, 'status' => 0, 'version' => 0, ]); return $player; } function ticketItemsPublishAndSettle(Draw $draw, string $firstNumber): void { $batch = DrawResultBatch::query()->create([ 'draw_id' => $draw->id, 'result_version' => 1, 'source_type' => 'rng', 'rng_seed_hash' => 'items-'.(string) $draw->draw_no, 'raw_seed_encrypted' => null, 'status' => DrawResultBatchStatus::Published->value, 'created_by' => null, 'confirmed_by' => null, 'confirmed_at' => now(), ]); foreach (DrawPrizeLayout::slots() as $slot) { $num = $slot['prize_type'] === 'first' ? $firstNumber : '5678'; DrawResultItem::query()->create([ 'draw_id' => $draw->id, 'result_batch_id' => $batch->id, 'prize_type' => $slot['prize_type'], 'prize_index' => $slot['prize_index'], 'number_4d' => $num, 'suffix_3d' => substr($num, -3), 'suffix_2d' => substr($num, -2), 'head_digit' => (int) substr($num, 0, 1), 'tail_digit' => (int) substr($num, 3, 1), ]); } $draw->forceFill([ 'status' => DrawStatus::Settling->value, 'current_result_version' => 1, ])->save(); expect(app(SettlementOrchestrator::class)->trySettleDraw($draw->fresh()))->toBeTrue(); $admin = AdminUser::query()->create([ 'username' => 'ticket_items_settle_'.bin2hex(random_bytes(3)), 'name' => 'Ticket Items Settle', 'email' => null, 'password' => Hash::make('secret-strong'), 'status' => 0, ]); grantSuperAdminRole($admin); $settlementBatch = SettlementBatch::query()->where('draw_id', $draw->id)->firstOrFail(); app(SettlementBatchWorkflowService::class)->approve($settlementBatch, $admin); app(SettlementBatchWorkflowService::class)->payout($settlementBatch->fresh()); } test('jackpot summary is public', function (): void { JackpotPool::query()->create([ 'currency_code' => 'NPR', 'current_amount' => 1_234_000, 'contribution_rate' => '0.0100', 'trigger_threshold' => 0, 'payout_rate' => '0.5000', 'force_trigger_draw_gap' => 0, 'min_bet_amount' => 0, 'status' => 1, 'last_trigger_draw_id' => null, ]); $this->getJson('/api/v1/jackpot/summary?currency_code=NPR') ->assertOk() ->assertJsonPath('data.enabled', true) ->assertJsonPath('data.current_amount_minor', 1_234_000); }); test('ticket items index returns placed ticket for player', function (): void { $player = ticketItemsPlayer(); $draw = Draw::query()->create([ 'draw_no' => '20260511-777', 'business_date' => '2026-05-11', 'sequence_no' => 777, 'status' => DrawStatus::Open->value, 'start_time' => now()->subMinutes(2), 'close_time' => now()->addMinutes(5), 'draw_time' => now()->addMinutes(6), 'cooling_end_time' => null, 'result_source' => null, 'current_result_version' => 0, 'settle_version' => 0, 'is_reopened' => false, ]); $this->withHeader('Authorization', 'Bearer dev:'.$player->id) ->postJson('/api/v1/ticket/place', [ 'draw_id' => '20260511-777', 'currency_code' => 'NPR', 'client_trace_id' => 'items-trace-1', 'lines' => [ ['number' => '1234', 'play_code' => 'big', 'amount' => 10_000], ], ]) ->assertOk(); $this->withHeader('Authorization', 'Bearer dev:'.$player->id) ->getJson('/api/v1/ticket/items') ->assertOk() ->assertJsonPath('data.total', 1) ->assertJsonPath('data.items.0.draw_no', '20260511-777') ->assertJsonPath('data.items.0.play_code', 'big'); $ticketNo = $this->withHeader('Authorization', 'Bearer dev:'.$player->id) ->getJson('/api/v1/ticket/items') ->json('data.items.0.ticket_no'); $this->withHeader('Authorization', 'Bearer dev:'.$player->id) ->getJson('/api/v1/ticket/items/'.$ticketNo) ->assertOk() ->assertJsonPath('data.ticket_no', $ticketNo) ->assertJsonPath('data.combinations.0.number_4d', '1234'); $this->withHeader('Authorization', 'Bearer dev:'.$player->id) ->getJson('/api/v1/ticket/items?draw_no='.urlencode('20260511-777')) ->assertOk() ->assertJsonPath('data.total', 1); $this->withHeader('Authorization', 'Bearer dev:'.$player->id) ->getJson('/api/v1/ticket/items?draw_no='.urlencode('20260511-000')) ->assertOk() ->assertJsonPath('data.total', 0); }); test('ticket items index filters by status number and date range', function (): void { $player = ticketItemsPlayer(); $draw1 = Draw::query()->create([ 'draw_no' => '20260511-779', 'business_date' => '2026-05-11', 'sequence_no' => 779, 'status' => DrawStatus::Open->value, 'start_time' => now()->subMinutes(2), 'close_time' => now()->addMinutes(5), 'draw_time' => now()->addMinutes(6), 'cooling_end_time' => null, 'result_source' => null, 'current_result_version' => 0, 'settle_version' => 0, 'is_reopened' => false, ]); $draw2 = Draw::query()->create([ 'draw_no' => '20260512-780', 'business_date' => '2026-05-12', 'sequence_no' => 780, 'status' => DrawStatus::Open->value, 'start_time' => now()->subMinutes(2), 'close_time' => now()->addMinutes(5), 'draw_time' => now()->addMinutes(6), 'cooling_end_time' => null, 'result_source' => null, 'current_result_version' => 0, 'settle_version' => 0, 'is_reopened' => false, ]); $this->withHeader('Authorization', 'Bearer dev:'.$player->id) ->postJson('/api/v1/ticket/place', [ 'draw_id' => $draw1->draw_no, 'currency_code' => 'NPR', 'client_trace_id' => 'items-filter-1', 'lines' => [ ['number' => '1234', 'play_code' => 'big', 'amount' => 10_000], ], ]) ->assertOk(); $this->withHeader('Authorization', 'Bearer dev:'.$player->id) ->postJson('/api/v1/ticket/place', [ 'draw_id' => $draw2->draw_no, 'currency_code' => 'NPR', 'client_trace_id' => 'items-filter-2', 'lines' => [ ['number' => '4321', 'play_code' => 'big', 'amount' => 10_000], ], ]) ->assertOk(); TicketOrder::query()->where('draw_id', $draw1->id)->update([ 'created_at' => '2026-05-01 10:00:00', 'updated_at' => '2026-05-01 10:00:00', ]); TicketOrder::query()->where('draw_id', $draw2->id)->update([ 'created_at' => '2026-05-10 10:00:00', 'updated_at' => '2026-05-10 10:00:00', ]); ticketItemsPublishAndSettle($draw2, '4321'); $this->withHeader('Authorization', 'Bearer dev:'.$player->id) ->getJson('/api/v1/ticket/items?status[]=settled_win') ->assertOk() ->assertJsonPath('data.total', 1) ->assertJsonPath('data.items.0.draw_no', '20260512-780'); $this->withHeader('Authorization', 'Bearer dev:'.$player->id) ->getJson('/api/v1/ticket/items?number=1234') ->assertOk() ->assertJsonPath('data.total', 1) ->assertJsonPath('data.items.0.original_number', '1234'); $this->withHeader('Authorization', 'Bearer dev:'.$player->id) ->getJson('/api/v1/ticket/items?start_date=2026-05-09&end_date=2026-05-11') ->assertOk() ->assertJsonPath('data.total', 1) ->assertJsonPath('data.items.0.draw_no', '20260512-780'); }); test('ticket item show returns match result and timeline', function (): void { $player = ticketItemsPlayer(); $draw = Draw::query()->create([ 'draw_no' => '20260513-781', 'business_date' => '2026-05-13', 'sequence_no' => 781, 'status' => DrawStatus::Open->value, 'start_time' => now()->subMinutes(2), 'close_time' => now()->addMinutes(5), 'draw_time' => now()->addMinutes(6), 'cooling_end_time' => null, 'result_source' => null, 'current_result_version' => 0, 'settle_version' => 0, 'is_reopened' => false, ]); $this->withHeader('Authorization', 'Bearer dev:'.$player->id) ->postJson('/api/v1/ticket/place', [ 'draw_id' => $draw->draw_no, 'currency_code' => 'NPR', 'client_trace_id' => 'items-detail-1', 'lines' => [ ['number' => '1234', 'play_code' => 'big', 'amount' => 10_000], ], ]) ->assertOk(); ticketItemsPublishAndSettle($draw, '1234'); $ticketNo = \App\Models\TicketItem::query()->where('draw_id', $draw->id)->value('ticket_no'); $this->withHeader('Authorization', 'Bearer dev:'.$player->id) ->getJson('/api/v1/ticket/items/'.$ticketNo) ->assertOk() ->assertJsonPath('data.match_result.matched', true) ->assertJsonPath('data.match_result.matched_prize_tier', 'first') ->assertJsonPath('data.timeline.0.code', 'placed') ->assertJsonPath('data.timeline.1.code', 'deducted') ->assertJsonPath('data.timeline.2.code', 'draw_published') ->assertJsonPath('data.timeline.3.code', 'settlement_started') ->assertJsonPath('data.timeline.4.code', 'settled'); }); test('my-match returns hit numbers when draw published', function (): void { $uniq = bin2hex(random_bytes(4)); $player = Player::query()->create([ 'site_code' => 'test', 'site_player_id' => 'match-p-'.$uniq, 'username' => 'tm_'.$uniq, 'nickname' => null, 'default_currency' => 'NPR', 'status' => 0, ]); PlayerWallet::query()->create([ 'player_id' => $player->id, 'wallet_type' => 'lottery', 'currency_code' => 'NPR', 'balance' => 5_000_000, 'frozen_balance' => 0, 'status' => 0, 'version' => 0, ]); $draw = Draw::query()->create([ 'draw_no' => '20260511-778', 'business_date' => '2026-05-11', 'sequence_no' => 778, 'status' => DrawStatus::Open->value, 'start_time' => now()->subMinutes(2), 'close_time' => now()->addMinutes(5), 'draw_time' => now()->addMinutes(6), 'cooling_end_time' => null, 'result_source' => null, 'current_result_version' => 0, 'settle_version' => 0, 'is_reopened' => false, ]); $this->withHeader('Authorization', 'Bearer dev:'.$player->id) ->postJson('/api/v1/ticket/place', [ 'draw_id' => '20260511-778', 'currency_code' => 'NPR', 'client_trace_id' => 'match-trace-1', 'lines' => [ ['number' => '1234', 'play_code' => 'big', 'amount' => 10_000], ], ]) ->assertOk(); $batch = DrawResultBatch::query()->create([ 'draw_id' => $draw->id, 'result_version' => 1, 'source_type' => 'rng', 'rng_seed_hash' => 'test', 'raw_seed_encrypted' => null, 'status' => DrawResultBatchStatus::Published->value, 'created_by' => null, 'confirmed_by' => null, 'confirmed_at' => now(), ]); foreach (DrawPrizeLayout::slots() as $slot) { $num = $slot['prize_type'] === 'first' ? '1234' : '5678'; DrawResultItem::query()->create([ 'draw_id' => $draw->id, 'result_batch_id' => $batch->id, 'prize_type' => $slot['prize_type'], 'prize_index' => $slot['prize_index'], 'number_4d' => $num, 'suffix_3d' => substr($num, -3), 'suffix_2d' => substr($num, -2), 'head_digit' => (int) substr($num, 0, 1), 'tail_digit' => (int) substr($num, 3, 1), ]); } $draw->forceFill([ 'status' => DrawStatus::Cooldown->value, 'current_result_version' => 1, ])->save(); $this->withHeader('Authorization', 'Bearer dev:'.$player->id) ->getJson('/api/v1/ticket/draws/20260511-778/my-match') ->assertOk() ->assertJsonPath('data.has_bets', true) ->assertJsonPath('data.hit_numbers_4d', ['1234']); });