artisan('lottery:admin-auth-sync')->assertExitCode(0); }); test('period close from share ledger matches design doc example 12', function (): void { $siteId = (int) DB::table('admin_sites')->where('is_default', true)->value('id'); $siteCode = (string) DB::table('admin_sites')->where('id', $siteId)->value('code'); $rootId = (int) DB::table('agent_nodes')->where('admin_site_id', $siteId)->where('depth', 0)->value('id'); $service = app(AgentNodeService::class); $super = \App\Models\AdminUser::query()->create([ 'username' => 'e2e_super', 'name' => 'E2E', 'email' => null, 'password' => \Illuminate\Support\Facades\Hash::make('secret-strong'), 'status' => 0, ]); grantSuperAdminRole($super); $a = $service->createChild($super, agentChildPayload([ 'parent_id' => $rootId, 'code' => 'A', 'name' => 'A', 'username' => 'e2e_a', 'total_share_rate' => 60, 'credit_limit' => 500000, ])); $b = $service->createChild($super, agentChildPayload([ 'parent_id' => $a->id, 'code' => 'B', 'name' => 'B', 'username' => 'e2e_b', 'total_share_rate' => 40, 'credit_limit' => 200000, ])); $c = $service->createChild($super, agentChildPayload([ 'parent_id' => $b->id, 'code' => 'C', 'name' => 'C', 'username' => 'e2e_c', 'total_share_rate' => 25, 'credit_limit' => 100000, ])); $player = Player::query()->create([ 'site_code' => $siteCode, 'agent_node_id' => $c->id, 'site_player_id' => 'e2e-p1', 'username' => 'e2eplayer', 'nickname' => null, 'default_currency' => 'NPR', 'status' => 0, ]); $draw = \App\Models\Draw::query()->create([ 'draw_no' => 'E2E-AG-001', 'business_date' => now()->toDateString(), 'sequence_no' => 99, 'status' => \App\Lottery\DrawStatus::Open->value, 'start_time' => null, 'close_time' => null, 'draw_time' => null, 'cooling_end_time' => null, 'result_source' => null, 'current_result_version' => 0, 'settle_version' => 0, 'is_reopened' => false, ]); $orderId = (int) DB::table('ticket_orders')->insertGetId([ 'order_no' => 'ORD-E2E-AG-1', 'player_id' => $player->id, 'draw_id' => $draw->id, 'currency_code' => 'NPR', 'total_bet_amount' => 10000, 'total_rebate_amount' => 0, 'total_actual_deduct' => 10000, 'total_estimated_payout' => 0, 'status' => 'confirmed', 'submit_source' => 'h5', 'client_trace_id' => null, 'created_at' => now(), 'updated_at' => now(), ]); $ticketItemId = (int) DB::table('ticket_items')->insertGetId([ 'ticket_no' => 'T-E2E-AG-1', 'order_id' => $orderId, 'player_id' => $player->id, 'draw_id' => $draw->id, 'original_number' => null, 'normalized_number' => '1234', 'play_code' => 'big', 'dimension' => 2, 'digit_slot' => null, 'bet_mode' => null, 'unit_bet_amount' => 10000, 'total_bet_amount' => 10000, 'rebate_rate_snapshot' => 0, 'commission_rate_snapshot' => 0, 'actual_deduct_amount' => 10000, 'odds_snapshot_json' => null, 'rule_snapshot_json' => null, 'combination_count' => 1, 'estimated_max_payout' => 0, 'risk_locked_amount' => 0, 'status' => 'settled_lose', 'fail_reason_code' => null, 'fail_reason_text' => null, 'win_amount' => 0, 'jackpot_win_amount' => 0, 'settled_at' => null, 'created_at' => now(), 'updated_at' => now(), ]); $settledAt = now(); $periodId = (int) DB::table('settlement_periods')->insertGetId([ 'admin_site_id' => $siteId, 'period_start' => $settledAt->copy()->subDay(), 'period_end' => $settledAt->copy()->addDay(), 'status' => 'open', 'created_at' => now(), 'updated_at' => now(), ]); DB::table('share_ledger')->insert([ 'ticket_item_id' => $ticketItemId, 'player_id' => $player->id, 'agent_node_id' => $c->id, 'agent_path' => json_encode([$a->id, $b->id, $c->id]), 'share_snapshot' => json_encode([ 'total_shares' => ['C' => 25, 'B' => 40, 'A' => 60], 'actual_shares' => ['C' => 25, 'B' => 15, 'A' => 20, 'platform' => 40], 'chain_codes' => ['C', 'B', 'A'], 'agent_path' => [$a->id, $b->id, $c->id], ]), 'game_win_loss' => DesignDocExample12::GAME_WIN_LOSS, 'basic_rebate' => DesignDocExample12::BASIC_REBATE, 'shared_net_win_loss' => DesignDocExample12::SHARED_NET_WIN_LOSS, 'allocations_json' => json_encode([]), 'settled_at' => $settledAt, 'created_at' => $settledAt, 'updated_at' => $settledAt, ]); DB::table('rebate_records')->insert([ [ 'player_id' => $player->id, 'ticket_item_id' => $ticketItemId, 'game_type' => '*', 'valid_bet_amount' => 10000, 'rebate_rate' => 0.005, 'rebate_amount' => DesignDocExample12::BASIC_REBATE, 'rebate_type' => 'basic', 'owner_agent_id' => $c->id, 'status' => 'accrued', 'created_at' => $settledAt, 'updated_at' => $settledAt, ], [ 'player_id' => $player->id, 'ticket_item_id' => $ticketItemId, 'game_type' => '*', 'valid_bet_amount' => 10000, 'rebate_rate' => 0.002, 'rebate_amount' => DesignDocExample12::EXTRA_REBATE_BY_C, 'rebate_type' => 'extra', 'owner_agent_id' => $c->id, 'status' => 'accrued', 'created_at' => $settledAt, 'updated_at' => $settledAt, ], ]); $close = app(AgentSettlementPeriodCloseService::class)->closePeriod($periodId); $playerBill = DB::table('settlement_bills') ->where('settlement_period_id', $periodId) ->where('bill_type', 'player') ->where('owner_id', $player->id) ->first(); expect($playerBill)->not->toBeNull(); expect((int) $playerBill->net_amount)->toBe((int) DesignDocExample12::PLAYER_NET_SETTLEMENT); $edgeCtoB = DB::table('settlement_bills') ->where('settlement_period_id', $periodId) ->where('meta_json', 'like', '%C_to_B%') ->value('net_amount'); expect((int) $edgeCtoB)->toBe((int) round(DesignDocExample12::TIER_C_TO_B)); expect($close['rebate_dispatched'])->toBe(2); expect($close['rebate_allocations'])->toBeGreaterThan(0); expect(DB::table('rebate_records')->where('status', 'in_bill')->count())->toBe(2); expect(DB::table('rebate_allocations')->where('settlement_bill_id', $playerBill->id)->count()) ->toBeGreaterThan(0); });