app = new Application(dirname(__DIR__, 2)); $this->app->instance('config', new ConfigRepository(['app' => ['locale' => 'zh', 'timezone' => 'Asia/Shanghai']])); $this->app->instance('translator', new Translator(new ArrayLoader(), 'zh')); Facade::setFacadeApplication($this->app); $db = new Manager($this->app); $db->addConnection(['driver' => 'sqlite', 'database' => ':memory:', 'prefix' => 'bot_']); $db->setAsGlobal(); $db->bootEloquent(); $this->app->instance('db', $db->getDatabaseManager()); $this->app->instance('cache', new CacheRepository(new ArrayStore())); $this->app->instance('log', new NullLogger()); $this->app->instance(\Illuminate\Contracts\Routing\ResponseFactory::class, new class { public function json($data, $status = 200, $headers = [], $options = 0): JsonResponse { return new JsonResponse($data, $status, $headers, $options); } }); foreach (['pc_issues', 'issues'] as $table) { $db->schema()->create($table, function (Blueprint $t) { $t->id(); $t->string('issue_no'); $t->string('winning_numbers'); $t->integer('status'); $t->dateTime('end_time'); $t->timestamps(); }); } $db->schema()->create('gameplay_rules', function (Blueprint $t) { $t->id(); $t->string('keywords'); $t->decimal('odds', 10, 2); $t->timestamps(); }); $db->schema()->create('bets', function (Blueprint $t) { $t->id(); $t->string('issue_no'); $t->string('keywords'); $t->decimal('amount', 20, 2); $t->decimal('odds', 10, 2); $t->integer('status'); $t->decimal('profit', 20, 2); $t->integer('issue_id'); $t->string('member_id'); $t->string('user_id'); $t->timestamps(); }); $db->schema()->create('wallets', function (Blueprint $t) { $t->id(); $t->string('member_id')->unique(); $t->decimal('available_balance', 20, 2); $t->timestamps(); }); $db->schema()->create('balance_logs', function (Blueprint $t) { $t->id(); $t->string('member_id'); $t->integer('type')->default(1); $t->decimal('amount', 20, 2); $t->decimal('before_balance', 20, 2); $t->decimal('after_balance', 20, 2); $t->string('change_type'); $t->integer('related_id'); $t->string('remark'); $t->timestamps(); }); } protected function tearDown(): void { DB::disconnect(); Facade::clearResolvedInstances(); parent::tearDown(); } public static function settlementGames(): array { return [ '极速28' => ['pc28', 'pc_issues', '极速28投注'], '加拿大28' => ['ca28', 'issues', '加拿大28投注'], ]; } /** @dataProvider settlementGames */ public function test_settlement_ledger_matches_wallet_and_accumulated_consumption(string $game, string $issueTable, string $betType): void { DB::table($issueTable)->insert([ 'issue_no' => '100001', 'winning_numbers' => '6,6,6', 'status' => PcIssue::STATUS_DRAW, 'end_time' => '2026-10-13 12:00:00', ]); DB::table('gameplay_rules')->insert(['keywords' => '大', 'odds' => '2.00']); DB::table('wallets')->insert(['member_id' => '1001', 'available_balance' => '1000.50']); // An existing real debit must accumulate with the injected debit rather than cancel it. DB::table('balance_logs')->insert([ 'member_id' => '1001', 'amount' => '-100.25', 'before_balance' => '1100.75', 'after_balance' => '1000.50', 'change_type' => $betType, 'related_id' => 999, 'remark' => 'existing bet', ]); $this->assertSame('100.25', BalanceLog::getTotalConsume('1001')); $request = Request::create('/api/pcissue/verify', 'POST', [ 'issue_no' => '100001', 'member_id' => '1001', 'keywords' => '大', 'amount' => '100.25', 'fake_time' => '2026-10-13 11:58:00', 'game' => $game, ]); $this->app->instance('request', $request); $result = (new Settle())->inject($request)->getData(true); $this->assertSame(200, $result['code'], $result['msg'] ?? ''); $this->assertSame('200.50', BalanceLog::getTotalConsume('1001')); $walletBalance = DB::table('wallets')->where('member_id', '1001')->value('available_balance'); $this->assertSame('1100.75', bcadd((string)$walletBalance, '0', 2)); $this->assertSame('1100.75', bcadd((string)$result['balance_after'], '0', 2)); $logs = DB::table('balance_logs')->where('related_id', $result['order_id'])->orderBy('id')->get(); $this->assertCount(2, $logs); $this->assertSame($betType, $logs[0]->change_type); $this->assertSame('-100.25', bcadd((string)$logs[0]->amount, '0', 2)); $this->assertSame('900.25', bcadd((string)$logs[0]->after_balance, '0', 2)); $this->assertSame('中奖', $logs[1]->change_type); $this->assertSame('200.50', bcadd((string)$logs[1]->amount, '0', 2)); $this->assertSame('1100.75', bcadd((string)$logs[1]->after_balance, '0', 2)); $this->assertSame(0, bccomp((string)$logs[0]->after_balance, (string)$logs[1]->before_balance, 2)); foreach ($logs as $log) { $this->assertSame(0, bccomp((string)$log->amount, bcsub((string)$log->after_balance, (string)$log->before_balance, 2), 2)); } $ledgerChange = bcadd((string)$logs[0]->amount, (string)$logs[1]->amount, 2); $this->assertSame($ledgerChange, bcsub((string)$walletBalance, '1000.50', 2)); $this->assertSame(Bet::STATUS_SETTLED, (int)DB::table('bets')->where('id', $result['order_id'])->value('status')); } }