|
|
@@ -0,0 +1,158 @@
|
|
|
+<?php
|
|
|
+
|
|
|
+namespace Tests\Integration;
|
|
|
+
|
|
|
+use App\Http\Controllers\admin\Promotion;
|
|
|
+use App\Services\PromotionAdminService;
|
|
|
+use Illuminate\Config\Repository;
|
|
|
+use Illuminate\Database\Capsule\Manager;
|
|
|
+use Illuminate\Foundation\Application;
|
|
|
+use Illuminate\Http\Request;
|
|
|
+use Illuminate\Support\Facades\DB;
|
|
|
+use Illuminate\Support\Facades\Facade;
|
|
|
+use PHPUnit\Framework\TestCase;
|
|
|
+
|
|
|
+class PromotionRecordDisplayTest extends TestCase
|
|
|
+{
|
|
|
+ private Application $app;
|
|
|
+
|
|
|
+ protected function setUp(): void
|
|
|
+ {
|
|
|
+ Facade::clearResolvedInstances();
|
|
|
+ $this->app = new Application(dirname(__DIR__, 2));
|
|
|
+ $this->app->instance('config', new Repository(['app' => ['locale' => 'zh']]));
|
|
|
+ $this->app->instance('log', new \Psr\Log\NullLogger());
|
|
|
+ $translator = new \Illuminate\Translation\Translator(new \Illuminate\Translation\ArrayLoader(), 'zh');
|
|
|
+ $this->app->instance('translator', $translator);
|
|
|
+ $this->app->instance('validator', new \Illuminate\Validation\Factory($translator, $this->app));
|
|
|
+ $this->app->instance(\Illuminate\Contracts\Routing\ResponseFactory::class, new \Tests\Support\JsonResponseFactory());
|
|
|
+ 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());
|
|
|
+ foreach ([
|
|
|
+ 'users' => 'id INTEGER PRIMARY KEY,account TEXT',
|
|
|
+ 'promotion_activities' => 'id INTEGER PRIMARY KEY,type INTEGER',
|
|
|
+ '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',
|
|
|
+ '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',
|
|
|
+ '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',
|
|
|
+ ] as $table => $columns) DB::statement('CREATE TABLE bot_' . $table . ' (' . $columns . ')');
|
|
|
+ foreach ([164, 165, 168, 169, 170, 171] as $id) DB::table('users')->insert(['id' => $id, 'account' => 'member' . $id]);
|
|
|
+ DB::table('promotion_activities')->insert(['id' => 1, 'type' => 7]);
|
|
|
+ DB::table('promotion_accounts')->insert(['id' => 11865, 'activity_id' => 1, 'user_id' => 164, 'round_no' => 1, 'spins' => 0,
|
|
|
+ 'applied_at' => '2026-10-29 11:52:42', 'application_snapshot' => json_encode(['rules' => ['reward_amount' => '500']])]);
|
|
|
+ DB::table('promotion_rewards')->insert(['id' => 3, 'activity_id' => 1, 'user_id' => 164, 'account_id' => 11865, 'round_no' => 1,
|
|
|
+ 'amount' => '500.00', 'required_flow' => '0.00', 'status' => 'paid', 'credited_log_id' => 20800,
|
|
|
+ 'rule_snapshot' => json_encode(['invite_count' => 3, 'reward_amount' => '500', 'qualified_invitees' => [168, 169, 170]])]);
|
|
|
+ }
|
|
|
+
|
|
|
+ protected function tearDown(): void
|
|
|
+ {
|
|
|
+ DB::disconnect(); Facade::clearResolvedInstances(); parent::tearDown();
|
|
|
+ }
|
|
|
+
|
|
|
+ private function records(string $kind, array $filters = []): array
|
|
|
+ {
|
|
|
+ $this->app->instance('request', Request::create('/admin/promotion/records', 'GET', $filters + ['activity_id' => 1, 'kind' => $kind]));
|
|
|
+ $body = (new Promotion())->records(new PromotionAdminService())->getData(true);
|
|
|
+ $this->assertSame(0, $body['code'], $body['msg']);
|
|
|
+ return $body['data'];
|
|
|
+ }
|
|
|
+
|
|
|
+ private function coupon(int $id, int $user, string $role, string $min = '100', string $discount = '50', int $reward = 3, string $status = 'unused'): void
|
|
|
+ {
|
|
|
+ DB::table('promotion_coupons')->insert(['id' => $id, 'reward_id' => $reward, 'activity_id' => 1, 'user_id' => $user, 'member_id' => 'wallet-' . $user,
|
|
|
+ 'role' => $role, 'inviter_user_id' => 164, 'min_amount' => $min, 'discount' => $discount, 'categories' => '["self:sport"]', 'status' => $status,
|
|
|
+ 'valid_from' => '2000-01-01 00:00:00', 'valid_until' => '2001-01-01 00:00:00']);
|
|
|
+ }
|
|
|
+
|
|
|
+ public function test_legacy_cash_reward_exposes_the_three_qualified_members_without_inventing_coupon_awards(): void
|
|
|
+ {
|
|
|
+ DB::table('users')->where('id', 168)->update(['account' => '000168']);
|
|
|
+ $before = DB::table('promotion_rewards')->first();
|
|
|
+ $result = $this->records('rewards', ['user_id' => 164]);
|
|
|
+ $row = $result['data'][0];
|
|
|
+ $this->assertSame(1, $result['total']);
|
|
|
+ $this->assertSame(3, $row['id']);
|
|
|
+ $this->assertSame('cash', $row['reward_mode']);
|
|
|
+ $this->assertSame(['id' => 164, 'account' => 'member164', 'coupons' => []], $row['inviter']);
|
|
|
+ $this->assertSame([168, 169, 170], array_column($row['invitees'], 'id'));
|
|
|
+ $this->assertSame(['000168', 'member169', 'member170'], array_column($row['invitees'], 'account'));
|
|
|
+ $this->assertSame([[], [], []], array_column($row['invitees'], 'coupons'));
|
|
|
+ $this->assertSame('500', $row['amount']);
|
|
|
+ $this->assertSame((array)$before, (array)DB::table('promotion_rewards')->first());
|
|
|
+ $this->assertSame(0, DB::table('promotion_coupons')->count());
|
|
|
+ }
|
|
|
+
|
|
|
+ public function test_coupon_reward_groups_actual_inviter_and_invitee_awards_including_used_and_expired_coupons(): void
|
|
|
+ {
|
|
|
+ DB::table('promotion_rewards')->where('id', 3)->update(['credited_log_id' => null, 'rule_snapshot' => json_encode([
|
|
|
+ 'reward_mode' => 'coupon', 'qualified_invitees' => [168, 169, 170, 171], 'coupon_invitees' => [168, 169, 170],
|
|
|
+ ])]);
|
|
|
+ $this->coupon(1, 164, 'inviter'); $this->coupon(2, 164, 'inviter', status: 'used');
|
|
|
+ $this->coupon(3, 164, 'inviter', '200', '100');
|
|
|
+ foreach ([168, 169, 170] as $id) $this->coupon($id, $id, 'invitee', '100', '100');
|
|
|
+ $this->coupon(999, 168, 'invitee', '999', '999', reward: 999); // Another reward must not bleed into this row.
|
|
|
+ DB::table('users')->where('id', 169)->delete();
|
|
|
+ DB::table('users')->where('id', 170)->update(['account' => '']);
|
|
|
+ $before = DB::table('promotion_coupons')->orderBy('id')->get()->toJson();
|
|
|
+ foreach (['rewards', 'accounts'] as $kind) {
|
|
|
+ $row = $this->records($kind)['data'][0];
|
|
|
+ $this->assertSame('coupon', $row['reward_mode']);
|
|
|
+ $this->assertSame([['min_amount' => '100', 'discount' => '50', 'quantity' => 2], ['min_amount' => '200', 'discount' => '100', 'quantity' => 1]], $row['inviter']['coupons']);
|
|
|
+ $this->assertSame([168, 169, 170], array_column($row['invitees'], 'id'));
|
|
|
+ $this->assertSame(['member168', null, null], array_column($row['invitees'], 'account'));
|
|
|
+ foreach ($row['invitees'] as $person) $this->assertSame([['min_amount' => '100', 'discount' => '100', 'quantity' => 1]], $person['coupons']);
|
|
|
+ if ($kind === 'accounts') {
|
|
|
+ $this->assertSame(3, $row['reward_id']);
|
|
|
+ $this->assertSame('paid', $row['reward_status']);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ $this->assertSame($before, DB::table('promotion_coupons')->orderBy('id')->get()->toJson());
|
|
|
+ }
|
|
|
+
|
|
|
+ public function test_application_without_reward_has_no_award_list_and_uses_its_application_mode(): void
|
|
|
+ {
|
|
|
+ DB::table('promotion_accounts')->insert(['id' => 11866, 'activity_id' => 1, 'user_id' => 165,
|
|
|
+ 'application_snapshot' => '{"rules":{"reward_mode":"coupon"}}']);
|
|
|
+ $row = $this->records('accounts', ['user_id' => 165])['data'][0];
|
|
|
+ $this->assertNull($row['reward_id']); $this->assertNull($row['reward_status']); $this->assertSame('coupon', $row['reward_mode']);
|
|
|
+ $this->assertSame(['id' => 165, 'account' => 'member165', 'coupons' => []], $row['inviter']);
|
|
|
+ $this->assertSame([], $row['invitees']);
|
|
|
+ }
|
|
|
+
|
|
|
+ public function test_pagination_still_counts_rewards_not_nested_people_and_does_not_mix_reward_snapshots(): void
|
|
|
+ {
|
|
|
+ DB::table('promotion_rewards')->insert(['id' => 4, 'activity_id' => 1, 'user_id' => 164, 'account_id' => 11865, 'round_no' => 2,
|
|
|
+ 'amount' => '5', 'status' => 'paid', 'rule_snapshot' => '{"qualified_invitees":[171]}' ]);
|
|
|
+ $first = $this->records('rewards', ['limit' => 1, 'page' => 1]);
|
|
|
+ $second = $this->records('rewards', ['limit' => 1, 'page' => 2]);
|
|
|
+ $this->assertSame(2, $first['total']); $this->assertSame(2, $second['total']);
|
|
|
+ $this->assertSame([171], array_column($first['data'][0]['invitees'], 'id'));
|
|
|
+ $this->assertSame([168, 169, 170], array_column($second['data'][0]['invitees'], 'id'));
|
|
|
+ }
|
|
|
+
|
|
|
+ public function test_cash_history_does_not_require_coupon_tables_and_bad_snapshot_does_not_break_the_list(): void
|
|
|
+ {
|
|
|
+ DB::statement('DROP TABLE bot_promotion_coupons');
|
|
|
+ $this->assertCount(3, $this->records('rewards')['data'][0]['invitees']);
|
|
|
+ DB::table('promotion_rewards')->update(['rule_snapshot' => 'invalid historical snapshot']);
|
|
|
+ $row = $this->records('rewards')['data'][0];
|
|
|
+ $this->assertSame('cash', $row['reward_mode']); $this->assertSame([], $row['invitees']);
|
|
|
+ }
|
|
|
+
|
|
|
+ public function test_coupon_record_list_also_contains_holder_and_inviter_accounts(): void
|
|
|
+ {
|
|
|
+ $this->coupon(1, 168, 'invitee');
|
|
|
+ $row = $this->records('coupons', ['user_id' => 164])['data'][0];
|
|
|
+ $this->assertSame(168, $row['user_id']); $this->assertSame('member168', $row['account']);
|
|
|
+ $this->assertSame(164, $row['inviter_user_id']); $this->assertSame('member164', $row['inviter_account']);
|
|
|
+ }
|
|
|
+
|
|
|
+ public function test_unrelated_activity_types_keep_the_existing_record_shape(): void
|
|
|
+ {
|
|
|
+ DB::table('promotion_activities')->update(['type' => 9]);
|
|
|
+ $row = $this->records('rewards')['data'][0];
|
|
|
+ $this->assertArrayNotHasKey('inviter', $row); $this->assertArrayNotHasKey('invitees', $row);
|
|
|
+ }
|
|
|
+}
|