PromotionRecordDisplayTest.php 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. <?php
  2. namespace Tests\Integration;
  3. use App\Http\Controllers\admin\Promotion;
  4. use App\Services\PromotionAdminService;
  5. use Illuminate\Config\Repository;
  6. use Illuminate\Database\Capsule\Manager;
  7. use Illuminate\Foundation\Application;
  8. use Illuminate\Http\Request;
  9. use Illuminate\Support\Facades\DB;
  10. use Illuminate\Support\Facades\Facade;
  11. use PHPUnit\Framework\TestCase;
  12. class PromotionRecordDisplayTest extends TestCase
  13. {
  14. private Application $app;
  15. protected function setUp(): void
  16. {
  17. Facade::clearResolvedInstances();
  18. $this->app = new Application(dirname(__DIR__, 2));
  19. $this->app->instance('config', new Repository(['app' => ['locale' => 'zh']]));
  20. $this->app->instance('log', new \Psr\Log\NullLogger());
  21. $translator = new \Illuminate\Translation\Translator(new \Illuminate\Translation\ArrayLoader(), 'zh');
  22. $this->app->instance('translator', $translator);
  23. $this->app->instance('validator', new \Illuminate\Validation\Factory($translator, $this->app));
  24. $this->app->instance(\Illuminate\Contracts\Routing\ResponseFactory::class, new \Tests\Support\JsonResponseFactory());
  25. Facade::setFacadeApplication($this->app);
  26. $db = new Manager($this->app);
  27. $db->addConnection(['driver' => 'sqlite', 'database' => ':memory:', 'prefix' => 'bot_']);
  28. $db->setAsGlobal(); $db->bootEloquent(); $this->app->instance('db', $db->getDatabaseManager());
  29. foreach ([
  30. 'users' => 'id INTEGER PRIMARY KEY,account TEXT',
  31. 'promotion_activities' => 'id INTEGER PRIMARY KEY,type INTEGER',
  32. 'promotion_accounts' => 'id INTEGER PRIMARY KEY,activity_id INTEGER,user_id INTEGER,round_no INTEGER,spins INTEGER,applied_at TEXT,application_snapshot TEXT,created_at TEXT,updated_at TEXT',
  33. 'promotion_rewards' => 'id INTEGER PRIMARY KEY,activity_id INTEGER,user_id INTEGER,account_id INTEGER,round_no INTEGER,amount TEXT,required_flow TEXT,status TEXT,credited_log_id INTEGER,rule_snapshot TEXT',
  34. 'promotion_coupons' => 'id INTEGER PRIMARY KEY,reward_id INTEGER,activity_id INTEGER,user_id INTEGER,member_id TEXT,role TEXT,inviter_user_id INTEGER,min_amount TEXT,discount TEXT,categories TEXT,status TEXT,valid_from TEXT,valid_until TEXT',
  35. ] as $table => $columns) DB::statement('CREATE TABLE bot_' . $table . ' (' . $columns . ')');
  36. foreach ([164, 165, 168, 169, 170, 171] as $id) DB::table('users')->insert(['id' => $id, 'account' => 'member' . $id]);
  37. DB::table('promotion_activities')->insert(['id' => 1, 'type' => 7]);
  38. DB::table('promotion_accounts')->insert(['id' => 11865, 'activity_id' => 1, 'user_id' => 164, 'round_no' => 1, 'spins' => 0,
  39. 'applied_at' => '2026-10-29 11:52:42', 'application_snapshot' => json_encode(['rules' => ['reward_amount' => '500']])]);
  40. DB::table('promotion_rewards')->insert(['id' => 3, 'activity_id' => 1, 'user_id' => 164, 'account_id' => 11865, 'round_no' => 1,
  41. 'amount' => '500.00', 'required_flow' => '0.00', 'status' => 'paid', 'credited_log_id' => 20800,
  42. 'rule_snapshot' => json_encode(['invite_count' => 3, 'reward_amount' => '500', 'qualified_invitees' => [168, 169, 170]])]);
  43. }
  44. protected function tearDown(): void
  45. {
  46. DB::disconnect(); Facade::clearResolvedInstances(); parent::tearDown();
  47. }
  48. private function records(string $kind, array $filters = []): array
  49. {
  50. $this->app->instance('request', Request::create('/admin/promotion/records', 'GET', $filters + ['activity_id' => 1, 'kind' => $kind]));
  51. $body = (new Promotion())->records(new PromotionAdminService())->getData(true);
  52. $this->assertSame(0, $body['code'], $body['msg']);
  53. return $body['data'];
  54. }
  55. private function coupon(int $id, int $user, string $role, string $min = '100', string $discount = '50', int $reward = 3, string $status = 'unused'): void
  56. {
  57. DB::table('promotion_coupons')->insert(['id' => $id, 'reward_id' => $reward, 'activity_id' => 1, 'user_id' => $user, 'member_id' => 'wallet-' . $user,
  58. 'role' => $role, 'inviter_user_id' => 164, 'min_amount' => $min, 'discount' => $discount, 'categories' => '["self:sport"]', 'status' => $status,
  59. 'valid_from' => '2000-01-01 00:00:00', 'valid_until' => '2001-01-01 00:00:00']);
  60. }
  61. public function test_legacy_cash_reward_exposes_the_three_qualified_members_without_inventing_coupon_awards(): void
  62. {
  63. DB::table('users')->where('id', 168)->update(['account' => '000168']);
  64. $before = DB::table('promotion_rewards')->first();
  65. $result = $this->records('rewards', ['user_id' => 164]);
  66. $row = $result['data'][0];
  67. $this->assertSame(1, $result['total']);
  68. $this->assertSame(3, $row['id']);
  69. $this->assertSame('cash', $row['reward_mode']);
  70. $this->assertSame(['id' => 164, 'account' => 'member164', 'coupons' => []], $row['inviter']);
  71. $this->assertSame([168, 169, 170], array_column($row['invitees'], 'id'));
  72. $this->assertSame(['000168', 'member169', 'member170'], array_column($row['invitees'], 'account'));
  73. $this->assertSame([[], [], []], array_column($row['invitees'], 'coupons'));
  74. $this->assertSame('500', $row['amount']);
  75. $this->assertSame((array)$before, (array)DB::table('promotion_rewards')->first());
  76. $this->assertSame(0, DB::table('promotion_coupons')->count());
  77. }
  78. public function test_coupon_reward_groups_actual_inviter_and_invitee_awards_including_used_and_expired_coupons(): void
  79. {
  80. DB::table('promotion_rewards')->where('id', 3)->update(['credited_log_id' => null, 'rule_snapshot' => json_encode([
  81. 'reward_mode' => 'coupon', 'qualified_invitees' => [168, 169, 170, 171], 'coupon_invitees' => [168, 169, 170],
  82. ])]);
  83. $this->coupon(1, 164, 'inviter'); $this->coupon(2, 164, 'inviter', status: 'used');
  84. $this->coupon(3, 164, 'inviter', '200', '100');
  85. foreach ([168, 169, 170] as $id) $this->coupon($id, $id, 'invitee', '100', '100');
  86. $this->coupon(999, 168, 'invitee', '999', '999', reward: 999); // Another reward must not bleed into this row.
  87. DB::table('users')->where('id', 169)->delete();
  88. DB::table('users')->where('id', 170)->update(['account' => '']);
  89. $before = DB::table('promotion_coupons')->orderBy('id')->get()->toJson();
  90. foreach (['rewards', 'accounts'] as $kind) {
  91. $row = $this->records($kind)['data'][0];
  92. $this->assertSame('coupon', $row['reward_mode']);
  93. $this->assertSame([['min_amount' => '100', 'discount' => '50', 'quantity' => 2], ['min_amount' => '200', 'discount' => '100', 'quantity' => 1]], $row['inviter']['coupons']);
  94. $this->assertSame([168, 169, 170], array_column($row['invitees'], 'id'));
  95. $this->assertSame(['member168', null, null], array_column($row['invitees'], 'account'));
  96. foreach ($row['invitees'] as $person) $this->assertSame([['min_amount' => '100', 'discount' => '100', 'quantity' => 1]], $person['coupons']);
  97. if ($kind === 'accounts') {
  98. $this->assertSame(3, $row['reward_id']);
  99. $this->assertSame('paid', $row['reward_status']);
  100. }
  101. }
  102. $this->assertSame($before, DB::table('promotion_coupons')->orderBy('id')->get()->toJson());
  103. }
  104. public function test_application_without_reward_has_no_award_list_and_uses_its_application_mode(): void
  105. {
  106. DB::table('promotion_accounts')->insert(['id' => 11866, 'activity_id' => 1, 'user_id' => 165,
  107. 'application_snapshot' => '{"rules":{"reward_mode":"coupon"}}']);
  108. $row = $this->records('accounts', ['user_id' => 165])['data'][0];
  109. $this->assertNull($row['reward_id']); $this->assertNull($row['reward_status']); $this->assertSame('coupon', $row['reward_mode']);
  110. $this->assertSame(['id' => 165, 'account' => 'member165', 'coupons' => []], $row['inviter']);
  111. $this->assertSame([], $row['invitees']);
  112. }
  113. public function test_pagination_still_counts_rewards_not_nested_people_and_does_not_mix_reward_snapshots(): void
  114. {
  115. DB::table('promotion_rewards')->insert(['id' => 4, 'activity_id' => 1, 'user_id' => 164, 'account_id' => 11865, 'round_no' => 2,
  116. 'amount' => '5', 'status' => 'paid', 'rule_snapshot' => '{"qualified_invitees":[171]}' ]);
  117. $first = $this->records('rewards', ['limit' => 1, 'page' => 1]);
  118. $second = $this->records('rewards', ['limit' => 1, 'page' => 2]);
  119. $this->assertSame(2, $first['total']); $this->assertSame(2, $second['total']);
  120. $this->assertSame([171], array_column($first['data'][0]['invitees'], 'id'));
  121. $this->assertSame([168, 169, 170], array_column($second['data'][0]['invitees'], 'id'));
  122. }
  123. public function test_cash_history_does_not_require_coupon_tables_and_bad_snapshot_does_not_break_the_list(): void
  124. {
  125. DB::statement('DROP TABLE bot_promotion_coupons');
  126. $this->assertCount(3, $this->records('rewards')['data'][0]['invitees']);
  127. DB::table('promotion_rewards')->update(['rule_snapshot' => 'invalid historical snapshot']);
  128. $row = $this->records('rewards')['data'][0];
  129. $this->assertSame('cash', $row['reward_mode']); $this->assertSame([], $row['invitees']);
  130. }
  131. public function test_coupon_record_list_also_contains_holder_and_inviter_accounts(): void
  132. {
  133. $this->coupon(1, 168, 'invitee');
  134. $row = $this->records('coupons', ['user_id' => 164])['data'][0];
  135. $this->assertSame(168, $row['user_id']); $this->assertSame('member168', $row['account']);
  136. $this->assertSame(164, $row['inviter_user_id']); $this->assertSame('member164', $row['inviter_account']);
  137. }
  138. public function test_unrelated_activity_types_keep_the_existing_record_shape(): void
  139. {
  140. DB::table('promotion_activities')->update(['type' => 9]);
  141. $row = $this->records('rewards')['data'][0];
  142. $this->assertArrayNotHasKey('inviter', $row); $this->assertArrayNotHasKey('invitees', $row);
  143. }
  144. }