AdminBonusLogTest.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. <?php
  2. namespace Tests\Integration;
  3. use App\Http\Controllers\admin\Balance;
  4. use Illuminate\Config\Repository;
  5. use Illuminate\Database\Capsule\Manager;
  6. use Illuminate\Foundation\Application;
  7. use Illuminate\Http\Request;
  8. use Illuminate\Support\Facades\DB;
  9. use Illuminate\Support\Facades\Facade;
  10. use PHPUnit\Framework\TestCase;
  11. class AdminBonusLogTest extends TestCase
  12. {
  13. private Application $app;
  14. protected function setUp(): void
  15. {
  16. Facade::clearResolvedInstances();
  17. $this->app = new Application(dirname(__DIR__, 2));
  18. $this->app->instance('config', new Repository(['app' => ['locale' => 'zh']]));
  19. $this->app->instance('log', new \Psr\Log\NullLogger());
  20. $translator = new \Illuminate\Translation\Translator(new \Illuminate\Translation\ArrayLoader(), 'zh');
  21. $this->app->instance('translator', $translator);
  22. $this->app->instance('validator', new \Illuminate\Validation\Factory($translator, $this->app));
  23. $this->app->instance(\Illuminate\Contracts\Routing\ResponseFactory::class, new \Tests\Support\JsonResponseFactory());
  24. Facade::setFacadeApplication($this->app);
  25. $db = new Manager($this->app);
  26. $db->addConnection(['driver' => 'sqlite', 'database' => ':memory:', 'prefix' => 'bot_']);
  27. $db->setAsGlobal(); $db->bootEloquent(); $this->app->instance('db', $db->getDatabaseManager());
  28. $this->app->bind('db.schema', fn () => $db->schema());
  29. foreach ([
  30. 'users' => 'id INTEGER PRIMARY KEY,member_id TEXT,account TEXT,username TEXT,first_name TEXT,admin_note TEXT,status INTEGER,phone TEXT,visitor_id TEXT,register_ip TEXT,"from" INTEGER',
  31. 'balance_logs' => 'id INTEGER PRIMARY KEY,member_id TEXT,type INTEGER,change_type TEXT,amount DECIMAL(30,10),before_balance TEXT,after_balance TEXT,related_id INTEGER,remark TEXT,frozen_status INTEGER,created_at TEXT,updated_at TEXT',
  32. 'promotion_activities' => 'id INTEGER PRIMARY KEY,type INTEGER',
  33. 'promotion_rewards' => 'id INTEGER PRIMARY KEY,activity_id INTEGER,credited_log_id INTEGER',
  34. 'recharge_bonus_rewards' => 'id INTEGER PRIMARY KEY,credited_log_id INTEGER',
  35. ] as $table => $columns) DB::statement('CREATE TABLE bot_' . $table . ' (' . $columns . ')');
  36. DB::table('users')->insert(['id' => 1, 'member_id' => 'm1', 'account' => 'alice', 'from' => 1]);
  37. foreach ([[1,2,'余额宝利息','2'],[2,1,'余额宝利息','3'],[3,1,'优惠活动','666'],[4,1,'优惠活动','500'],[5,1,'流水解冻','2'],[6,2,'流水解冻','-2'],[7,1,'充值','100'],[8,1,'即充即送','5']] as [$id,$type,$name,$amount]) {
  38. DB::table('balance_logs')->insert(['id'=>$id,'member_id'=>'m1','type'=>$type,'change_type'=>$name,'amount'=>$amount,'created_at'=>'2026-11-05 12:00:00']);
  39. }
  40. DB::table('promotion_activities')->insert([['id'=>9,'type'=>9],['id'=>7,'type'=>7]]);
  41. DB::table('promotion_rewards')->insert([['id'=>1,'activity_id'=>9,'credited_log_id'=>3],['id'=>2,'activity_id'=>7,'credited_log_id'=>4]]);
  42. Request::macro('validate', function (array $rules) { return app('validator')->make($this->all(), $rules)->validate(); });
  43. }
  44. protected function tearDown(): void
  45. {
  46. DB::disconnect(); Facade::clearResolvedInstances(); parent::tearDown();
  47. }
  48. private function records(array $filters = []): array
  49. {
  50. $this->app->instance('request', Request::create('/admin/balance/log', 'GET', $filters));
  51. $body = (new Balance())->log()->getData(true);
  52. $this->assertSame(0, $body['code'], $body['msg']);
  53. return $body['data'];
  54. }
  55. public function test_bonus_includes_available_and_legacy_rewards_without_counting_transfers(): void
  56. {
  57. $data=$this->records(['is_fancai'=>1,'type'=>2,'limit'=>2]);
  58. $this->assertSame(5,$data['total']);
  59. $this->assertSame('1176.00',$data['total_amount']);
  60. $this->assertCount(2,$data['data']);
  61. $this->assertSame([8,4],array_column($data['data'],'id'));
  62. $this->assertSame('净亏邀请',$data['data'][1]['change_type']);
  63. $this->assertArrayNotHasKey('activity_types', $data);
  64. foreach (['reward_source', 'activity_type', 'activity_id'] as $field) $this->assertArrayNotHasKey($field, $data['data'][1]);
  65. $this->assertSame('1176.00',$this->records(['is_fancai'=>1,'page'=>2,'limit'=>2])['total_amount']);
  66. }
  67. public function test_activity_and_member_filters_apply_to_counts_and_sum(): void
  68. {
  69. $data=$this->records(['is_fancai'=>1,'change_type'=>'转盘','account'=>'lic']);
  70. $this->assertSame(1,$data['total']);$this->assertSame('666.00',$data['total_amount']);
  71. $this->assertSame('转盘',$data['data'][0]['change_type']);
  72. $this->assertSame(0,$this->records(['is_fancai'=>1,'account'=>'bob'])['total']);
  73. $this->assertSame(2,$this->records(['is_fancai'=>1,'change_type'=>'余额宝利息'])['total']);
  74. }
  75. public function test_bonus_amount_sorts_numerically_before_pagination(): void
  76. {
  77. $filters = ['is_fancai' => 1, 'account' => 'lic', 'sort_field' => 'amount', 'limit' => 2];
  78. foreach ([['asc', 1, [1, 2]], ['asc', 2, [8, 4]], ['desc', 1, [3, 4]]] as [$direction, $page, $ids]) {
  79. $data = $this->records($filters + ['sort_order' => $direction, 'page' => $page]);
  80. $this->assertSame($ids, array_column($data['data'], 'id'));
  81. $this->assertSame(5, $data['total']);
  82. $this->assertSame('1176.00', $data['total_amount']);
  83. }
  84. }
  85. public function test_rebate_and_backflow_grants_are_bonus_types(): void
  86. {
  87. DB::table('balance_logs')->insert([
  88. ['id'=>20,'member_id'=>'m1','type'=>1,'change_type'=>'笔笔返','amount'=>'12','created_at'=>'2026-11-05 12:00:00'],
  89. ['id'=>21,'member_id'=>'m1','type'=>1,'change_type'=>'回水','amount'=>'30','created_at'=>'2026-11-05 12:00:00'],
  90. ]);
  91. $all = $this->records(['is_fancai'=>1]);
  92. $this->assertContains('笔笔返', $all['change_types']);
  93. $this->assertContains('回水', $all['change_types']);
  94. $this->assertSame(7, $all['total']);
  95. foreach (['笔笔返' => [20, '12.00'], '回水' => [21, '30.00']] as $type => [$id, $sum]) {
  96. $data = $this->records(['is_fancai'=>1, 'change_type'=>$type]);
  97. $this->assertSame([$id], array_column($data['data'], 'id'));
  98. $this->assertSame($sum, $data['total_amount']);
  99. $this->assertSame($type, $data['data'][0]['change_type']);
  100. }
  101. }
  102. public function test_regular_balance_type_filter_remains_effective(): void
  103. {
  104. $this->assertSame(2,$this->records(['type'=>2])['total']);
  105. $this->assertSame(8,$this->records()['total']);
  106. $ordinary = $this->records(['change_type'=>'优惠活动']);
  107. $this->assertSame(2, $ordinary['total']);
  108. $this->assertSame(['优惠活动'], array_values(array_unique(array_column($ordinary['data'], 'change_type'))));
  109. }
  110. private function log(int $id, string $type, string $amount = '10'): void
  111. {
  112. DB::table('balance_logs')->insert(['id'=>$id, 'member_id'=>'m1', 'type'=>1, 'change_type'=>$type,
  113. 'amount'=>$amount, 'before_balance'=>'100.00', 'after_balance'=>bcadd('100', $amount, 2),
  114. 'related_id'=>1, 'created_at'=>'2026-11-05 12:00:00']);
  115. }
  116. public function test_each_dropdown_value_filters_exactly_the_type_returned_in_rows(): void
  117. {
  118. DB::table('promotion_activities')->insert([['id'=>8,'type'=>8],['id'=>10,'type'=>10]]);
  119. foreach ([80=>8, 100=>10] as $log => $activity) {
  120. $this->log($log, '优惠活动');
  121. DB::table('promotion_rewards')->insert(['activity_id'=>$activity,'credited_log_id'=>$log]);
  122. }
  123. $this->log(101, '优惠活动', '6');
  124. DB::table('recharge_bonus_rewards')->insert(['credited_log_id'=>101]);
  125. $this->log(102, '优惠活动', '4'); // Same related_id as a promotion reward; it is not that reward's log.
  126. $this->log(103, '老用户回归');
  127. $this->log(104, '注册赠送');
  128. $this->log(105, '充值返现');
  129. $this->log(106, '笔笔返');
  130. $this->log(107, '回水');
  131. // Unclaimed/coupon rewards and zero-valued channel rewards have no credited log.
  132. DB::table('promotion_rewards')->insert(['activity_id'=>9,'credited_log_id'=>null]);
  133. DB::table('recharge_bonus_rewards')->insert(['credited_log_id'=>null]);
  134. $options = $this->records(['is_fancai'=>1])['change_types'];
  135. $this->assertContains('转盘', $options);
  136. $this->assertContains('净亏邀请', $options);
  137. $this->assertContains('首充奖励', $options);
  138. $this->assertContains('邀请升级奖励', $options);
  139. foreach ($options as $type) {
  140. $filtered = $this->records(['is_fancai'=>1, 'change_type'=>$type, 'limit'=>100]);
  141. $this->assertGreaterThan(0, $filtered['total'], $type);
  142. $this->assertSame([$type], array_values(array_unique(array_column($filtered['data'], 'change_type'))));
  143. }
  144. $generic = $this->records(['is_fancai'=>1, 'change_type'=>'优惠活动']);
  145. $this->assertSame([102], array_column($generic['data'], 'id'));
  146. $this->assertSame('4.00', $generic['total_amount']);
  147. $channel = $this->records(['is_fancai'=>1, 'change_type'=>'充值渠道赠送']);
  148. $this->assertSame([101], array_column($channel['data'], 'id'));
  149. $this->assertSame('6.00', $channel['total_amount']);
  150. $this->assertSame('优惠活动', DB::table('balance_logs')->where('id', 3)->value('change_type'));
  151. }
  152. public function test_source_filter_pagination_totals_and_money_fields_remain_consistent(): void
  153. {
  154. $this->log(200, '优惠活动', '20');
  155. DB::table('promotion_rewards')->insert(['activity_id'=>9,'credited_log_id'=>200]);
  156. foreach ([1=>200, 2=>3] as $page=>$id) {
  157. $data = $this->records(['is_fancai'=>1, 'change_type'=>'转盘', 'limit'=>1, 'page'=>$page]);
  158. $this->assertSame(2, $data['total']);
  159. $this->assertSame('686.00', $data['total_amount']);
  160. $this->assertSame([$id], array_column($data['data'], 'id'));
  161. }
  162. $row = $this->records(['is_fancai'=>1, 'change_type'=>'转盘', 'limit'=>1])['data'][0];
  163. $this->assertSame('100.00', $row['before_balance']);
  164. $this->assertSame('20', (string) $row['amount']);
  165. $this->assertSame('120.00', $row['after_balance']);
  166. }
  167. public function test_legacy_schema_keeps_generic_rewards_readable_and_specific_filters_empty(): void
  168. {
  169. DB::statement('DROP TABLE bot_promotion_rewards');
  170. DB::statement('DROP TABLE bot_promotion_activities');
  171. DB::statement('DROP TABLE bot_recharge_bonus_rewards');
  172. $generic = $this->records(['is_fancai'=>1, 'change_type'=>'优惠活动']);
  173. $this->assertSame(2, $generic['total']);
  174. $this->assertSame('1166.00', $generic['total_amount']);
  175. foreach (['转盘', '净亏邀请', '充值渠道赠送'] as $type) {
  176. $this->assertSame(0, $this->records(['is_fancai'=>1, 'change_type'=>$type])['total']);
  177. }
  178. }
  179. }