SettleLedgerTest.php 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157
  1. <?php
  2. namespace Tests\Integration;
  3. use App\Http\Controllers\api\Settle;
  4. use App\Models\BalanceLog;
  5. use App\Models\Bet;
  6. use App\Models\PcIssue;
  7. use Illuminate\Cache\ArrayStore;
  8. use Illuminate\Cache\Repository as CacheRepository;
  9. use Illuminate\Config\Repository as ConfigRepository;
  10. use Illuminate\Database\Capsule\Manager;
  11. use Illuminate\Database\Schema\Blueprint;
  12. use Illuminate\Foundation\Application;
  13. use Illuminate\Http\JsonResponse;
  14. use Illuminate\Http\Request;
  15. use Illuminate\Support\Facades\DB;
  16. use Illuminate\Support\Facades\Facade;
  17. use Illuminate\Translation\ArrayLoader;
  18. use Illuminate\Translation\Translator;
  19. use PHPUnit\Framework\Attributes\DataProvider;
  20. use PHPUnit\Framework\TestCase;
  21. use Psr\Log\NullLogger;
  22. /** Uses real settlement code with an in-memory database and cache, without application providers. */
  23. class SettleLedgerTest extends TestCase
  24. {
  25. private Application $app;
  26. protected function setUp(): void
  27. {
  28. Facade::clearResolvedInstances();
  29. $this->app = new Application(dirname(__DIR__, 2));
  30. $this->app->instance('config', new ConfigRepository(['app' => ['locale' => 'zh', 'timezone' => 'Asia/Shanghai']]));
  31. $this->app->instance('translator', new Translator(new ArrayLoader(), 'zh'));
  32. Facade::setFacadeApplication($this->app);
  33. $db = new Manager($this->app);
  34. $db->addConnection(['driver' => 'sqlite', 'database' => ':memory:', 'prefix' => 'bot_']);
  35. $db->setAsGlobal();
  36. $db->bootEloquent();
  37. $this->app->instance('db', $db->getDatabaseManager());
  38. $this->app->instance('cache', new CacheRepository(new ArrayStore()));
  39. $this->app->instance('log', new NullLogger());
  40. $this->app->instance(\Illuminate\Contracts\Routing\ResponseFactory::class, new \Tests\Support\JsonResponseFactory());
  41. foreach (['pc_issues', 'issues'] as $table) {
  42. $db->schema()->create($table, function (Blueprint $t) {
  43. $t->id();
  44. $t->string('issue_no');
  45. $t->string('winning_numbers');
  46. $t->integer('status');
  47. $t->dateTime('end_time');
  48. $t->timestamps();
  49. });
  50. }
  51. $db->schema()->create('gameplay_rules', function (Blueprint $t) {
  52. $t->id();
  53. $t->string('keywords');
  54. $t->decimal('odds', 10, 2);
  55. $t->timestamps();
  56. });
  57. $db->schema()->create('bets', function (Blueprint $t) {
  58. $t->id();
  59. $t->string('issue_no');
  60. $t->string('keywords');
  61. $t->decimal('amount', 20, 2);
  62. $t->decimal('odds', 10, 2);
  63. $t->integer('status');
  64. $t->decimal('profit', 20, 2);
  65. $t->integer('issue_id');
  66. $t->string('member_id');
  67. $t->string('user_id');
  68. $t->timestamps();
  69. });
  70. $db->schema()->create('wallets', function (Blueprint $t) {
  71. $t->id();
  72. $t->string('member_id')->unique();
  73. $t->decimal('available_balance', 20, 2);
  74. $t->timestamps();
  75. });
  76. $db->schema()->create('balance_logs', function (Blueprint $t) {
  77. $t->id();
  78. $t->string('member_id');
  79. $t->integer('type')->default(1);
  80. $t->decimal('amount', 20, 2);
  81. $t->decimal('before_balance', 20, 2);
  82. $t->decimal('after_balance', 20, 2);
  83. $t->string('change_type');
  84. $t->integer('related_id');
  85. $t->string('remark');
  86. $t->timestamps();
  87. });
  88. }
  89. protected function tearDown(): void
  90. {
  91. DB::disconnect();
  92. Facade::clearResolvedInstances();
  93. parent::tearDown();
  94. }
  95. public static function settlementGames(): array
  96. {
  97. return [
  98. '极速28' => ['pc28', 'pc_issues', '极速28投注'],
  99. '加拿大28' => ['ca28', 'issues', '加拿大28投注'],
  100. ];
  101. }
  102. #[DataProvider('settlementGames')]
  103. public function test_settlement_ledger_matches_wallet_and_accumulated_consumption(string $game, string $issueTable, string $betType): void
  104. {
  105. DB::table($issueTable)->insert([
  106. 'issue_no' => '100001', 'winning_numbers' => '6,6,6',
  107. 'status' => PcIssue::STATUS_DRAW, 'end_time' => '2026-10-13 12:00:00',
  108. ]);
  109. DB::table('gameplay_rules')->insert(['keywords' => '大', 'odds' => '2.00']);
  110. DB::table('wallets')->insert(['member_id' => '1001', 'available_balance' => '1000.50']);
  111. // An existing real debit must accumulate with the injected debit rather than cancel it.
  112. DB::table('balance_logs')->insert([
  113. 'member_id' => '1001', 'amount' => '-100.25',
  114. 'before_balance' => '1100.75', 'after_balance' => '1000.50',
  115. 'change_type' => $betType, 'related_id' => 999, 'remark' => 'existing bet',
  116. ]);
  117. $this->assertSame('100.25', BalanceLog::getTotalConsume('1001'));
  118. $request = Request::create('/api/pcissue/verify', 'POST', [
  119. 'issue_no' => '100001', 'member_id' => '1001', 'keywords' => '大',
  120. 'amount' => '100.25', 'fake_time' => '2026-10-13 11:58:00', 'game' => $game,
  121. ]);
  122. $this->app->instance('request', $request);
  123. $result = (new Settle())->inject($request)->getData(true);
  124. $this->assertSame(200, $result['code'], $result['msg'] ?? '');
  125. $this->assertSame('200.50', BalanceLog::getTotalConsume('1001'));
  126. $walletBalance = DB::table('wallets')->where('member_id', '1001')->value('available_balance');
  127. $this->assertSame('1100.75', bcadd((string)$walletBalance, '0', 2));
  128. $this->assertSame('1100.75', bcadd((string)$result['balance_after'], '0', 2));
  129. $logs = DB::table('balance_logs')->where('related_id', $result['order_id'])->orderBy('id')->get();
  130. $this->assertCount(2, $logs);
  131. $this->assertSame($betType, $logs[0]->change_type);
  132. $this->assertSame('-100.25', bcadd((string)$logs[0]->amount, '0', 2));
  133. $this->assertSame('900.25', bcadd((string)$logs[0]->after_balance, '0', 2));
  134. $this->assertSame('中奖', $logs[1]->change_type);
  135. $this->assertSame('200.50', bcadd((string)$logs[1]->amount, '0', 2));
  136. $this->assertSame('1100.75', bcadd((string)$logs[1]->after_balance, '0', 2));
  137. $this->assertSame(0, bccomp((string)$logs[0]->after_balance, (string)$logs[1]->before_balance, 2));
  138. foreach ($logs as $log) {
  139. $this->assertSame(0, bccomp((string)$log->amount, bcsub((string)$log->after_balance, (string)$log->before_balance, 2), 2));
  140. }
  141. $ledgerChange = bcadd((string)$logs[0]->amount, (string)$logs[1]->amount, 2);
  142. $this->assertSame($ledgerChange, bcsub((string)$walletBalance, '1000.50', 2));
  143. $this->assertSame(Bet::STATUS_SETTLED, (int)DB::table('bets')->where('id', $result['order_id'])->value('status'));
  144. }
  145. }