| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161 |
- <?php
- namespace Tests\Integration;
- use App\Http\Controllers\api\Settle;
- use App\Models\BalanceLog;
- use App\Models\Bet;
- use App\Models\PcIssue;
- use Illuminate\Cache\ArrayStore;
- use Illuminate\Cache\Repository as CacheRepository;
- use Illuminate\Config\Repository as ConfigRepository;
- use Illuminate\Database\Capsule\Manager;
- use Illuminate\Database\Schema\Blueprint;
- use Illuminate\Foundation\Application;
- use Illuminate\Http\JsonResponse;
- use Illuminate\Http\Request;
- use Illuminate\Support\Facades\DB;
- use Illuminate\Support\Facades\Facade;
- use Illuminate\Translation\ArrayLoader;
- use Illuminate\Translation\Translator;
- use PHPUnit\Framework\TestCase;
- use Psr\Log\NullLogger;
- /** Uses real settlement code with an in-memory database and cache, without application providers. */
- class SettleLedgerTest extends TestCase
- {
- private Application $app;
- protected function setUp(): void
- {
- Facade::clearResolvedInstances();
- $this->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'));
- }
- }
|