|
|
@@ -55,6 +55,7 @@ class PromotionActivitiesTest extends TestCase
|
|
|
'user_login' => 'id INTEGER PRIMARY KEY AUTOINCREMENT,user_id TEXT,status INTEGER,platform TEXT,created_at TEXT',
|
|
|
'reporting_activity' => 'id INTEGER PRIMARY KEY AUTOINCREMENT,user_id INTEGER,session_hash TEXT,observed_at TEXT',
|
|
|
'share_levels' => 'id INTEGER PRIMARY KEY AUTOINCREMENT,level INTEGER UNIQUE,name TEXT',
|
|
|
+ 'config' => 'id INTEGER PRIMARY KEY AUTOINCREMENT,field TEXT,val TEXT',
|
|
|
'menus' => 'id INTEGER PRIMARY KEY AUTOINCREMENT,parent_id INTEGER,title TEXT,icon TEXT,uri TEXT,permission_name TEXT,sort INTEGER,status INTEGER,type INTEGER,created_at TEXT,updated_at TEXT',
|
|
|
] as $table => $columns) DB::statement('CREATE TABLE bot_' . $table . ' (' . $columns . ')');
|
|
|
foreach (['2026_10_13_120000_add_activity_eligibility_fields.php', '2026_10_14_120000_add_activity_reward_quotas.php', '2026_10_25_120000_add_game_category_to_activity_rewards.php', '2026_10_26_160000_create_promotion_activities.php', '2026_10_27_130000_initialize_fixed_promotion_activities.php', '2026_10_27_140000_add_promotion_activity_translations.php', '2026_11_03_120000_add_promotion_coupons.php', '2026_11_03_170000_repeat_loss_invite_rounds.php', '2026_11_04_120000_create_upgrade_invite_promotion.php'] as $migration) (require dirname(__DIR__, 2) . '/database/migrations/' . $migration)->up();
|
|
|
@@ -99,6 +100,12 @@ class PromotionActivitiesTest extends TestCase
|
|
|
return FirstDepositChoice::record(User::find($id), $joined, 'test', $offer);
|
|
|
}
|
|
|
|
|
|
+ /** 全部首充活动共用的禁止投注大类。 */
|
|
|
+ private function restrict($categories): array
|
|
|
+ {
|
|
|
+ return (new PromotionAdminService())->saveFirstDepositRestriction(['categories' => $categories]);
|
|
|
+ }
|
|
|
+
|
|
|
private function deposit(int $id, string $amount, ?string $time = null, string $type = '充值'): BalanceLog
|
|
|
{
|
|
|
return BalanceLog::create(['member_id' => 'm' . $id, 'type' => 1, 'amount' => $amount, 'change_type' => $type, 'created_at' => $time ?? PromotionFacts::now()]);
|
|
|
@@ -782,7 +789,6 @@ class PromotionActivitiesTest extends TestCase
|
|
|
public function test_real_chain_credit_hook_awards_first_deposit_once_and_rolls_back_a_failed_award(): void
|
|
|
{
|
|
|
foreach ([
|
|
|
- 'config' => 'id INTEGER PRIMARY KEY AUTOINCREMENT,field TEXT,val TEXT',
|
|
|
'level' => 'id INTEGER PRIMARY KEY,level INTEGER,recharge TEXT',
|
|
|
'recharge_channel_group' => 'id INTEGER PRIMARY KEY,activity_type TEXT,deleted_at TEXT',
|
|
|
'backflow' => 'id INTEGER PRIMARY KEY AUTOINCREMENT,date TEXT,member_id TEXT,recharge_amount TEXT DEFAULT "0",withdrawal_amount TEXT DEFAULT "0",backflow_ratio TEXT,amount TEXT,created_at TEXT,updated_at TEXT',
|
|
|
@@ -831,7 +837,6 @@ class PromotionActivitiesTest extends TestCase
|
|
|
public function test_manual_payment_approval_keeps_first_deposit_bonus_instead_of_overwriting_it_with_principal(): void
|
|
|
{
|
|
|
foreach ([
|
|
|
- 'config' => 'id INTEGER PRIMARY KEY AUTOINCREMENT,field TEXT,val TEXT',
|
|
|
'level' => 'id INTEGER PRIMARY KEY,level INTEGER,recharge TEXT',
|
|
|
'recharge_channel_group' => 'id INTEGER PRIMARY KEY,activity_type TEXT,deleted_at TEXT',
|
|
|
'backflow' => 'id INTEGER PRIMARY KEY AUTOINCREMENT,date TEXT,member_id TEXT,recharge_amount TEXT DEFAULT "0",withdrawal_amount TEXT DEFAULT "0",backflow_ratio TEXT,amount TEXT,created_at TEXT,updated_at TEXT',
|
|
|
@@ -947,34 +952,45 @@ class PromotionActivitiesTest extends TestCase
|
|
|
$this->invalid(fn () => \App\Support\FirstDepositChoice::parse('yes'), 'join_first_deposit只能为0或1');
|
|
|
}
|
|
|
|
|
|
- public function test_first_deposit_forbidden_categories_use_commission_categories_and_show_in_admin_list(): void
|
|
|
+ public function test_first_deposit_restriction_is_one_setting_shared_by_every_campaign(): void
|
|
|
{
|
|
|
$admin = new PromotionAdminService();
|
|
|
$options = $admin->options();
|
|
|
- $this->assertSame(array_keys(\App\Services\Sharing\ShareDefaults::commissionCategories()), array_column($options['forbidden_categories'], 'value'));
|
|
|
- $this->assertSame([], $options['defaults'][8]['forbidden_categories']);
|
|
|
- $this->assertNotContains('third:3', array_column($options['forbidden_categories'], 'value'));
|
|
|
- $id = $this->activity(8, ['min_deposit' => 100, 'reward_amount' => 25, 'forbidden_categories' => ['self:sport', 'third:2', 'third:1']]);
|
|
|
- $detail = $admin->detail($id);
|
|
|
- $this->assertSame(['third:1', 'third:2', 'self:sport'], $detail['params']['forbidden_categories']);
|
|
|
- $this->assertSame([['value' => 'third:1', 'label' => '真人'], ['value' => 'third:2', 'label' => '电子'], ['value' => 'self:sport', 'label' => 'F1体育']], $detail['forbidden_categories']);
|
|
|
- $row = $admin->listing(['type' => 8])['data'][0];
|
|
|
- $this->assertSame(['真人', '电子', 'F1体育'], array_column($row['forbidden_categories'], 'label'));
|
|
|
- // 局部编辑其他规则时保留禁止大类,传空数组清空。
|
|
|
- $base = ['id' => $id, 'type' => 8, 'status' => 1, 'translations' => $this->texts()];
|
|
|
- $this->assertSame(['third:1', 'third:2', 'self:sport'], $admin->save($base + ['params' => ['reward_amount' => 30]])['params']['forbidden_categories']);
|
|
|
- foreach ([['third:3'], ['third:1', 'third:1'], 'third:1', ['x' => 'third:1'], [4]] as $bad) {
|
|
|
- $this->invalid(fn () => $admin->save($base + ['params' => ['forbidden_categories' => $bad]]), 'forbidden_categories参数错误');
|
|
|
+ $this->assertArrayNotHasKey('forbidden_categories', $options);
|
|
|
+ $this->assertArrayNotHasKey('forbidden_categories', $options['defaults'][8]);
|
|
|
+ $setting = $admin->firstDepositRestriction();
|
|
|
+ $this->assertSame([], $setting['categories']);
|
|
|
+ $this->assertSame(array_keys(\App\Services\Sharing\ShareDefaults::commissionCategories()), array_column($setting['options'], 'value'));
|
|
|
+ $this->assertNotContains('third:3', array_column($setting['options'], 'value'));
|
|
|
+ $this->assertSame(['third:1', 'third:2', 'self:sport'], $this->restrict(['self:sport', 'third:2', 'third:1'])['categories']);
|
|
|
+ $this->assertSame(['third:1', 'third:2', 'self:sport'], $admin->firstDepositRestriction()['categories']);
|
|
|
+ foreach ([['third:3'], ['third:1', 'third:1'], 'third:1', ['x' => 'third:1'], [4]] as $bad) $this->invalid(fn () => $this->restrict($bad), 'categories参数错误');
|
|
|
+ $this->invalid(fn () => $this->restrict(array_keys(\App\Services\Sharing\ShareDefaults::commissionCategories())), '至少保留一个可投注的游戏大类');
|
|
|
+ $this->invalid(fn () => $admin->saveFirstDepositRestriction([]), '请传categories');
|
|
|
+ $this->assertSame(['third:1', 'third:2', 'self:sport'], $admin->firstDepositRestriction()['categories']);
|
|
|
+
|
|
|
+ // 活动里不再保存、返回禁止大类;旧前端仍传该参数时忽略。
|
|
|
+ $first = $this->activity(8, ['min_deposit' => 100, 'reward_amount' => 25, 'forbidden_categories' => ['third:5']]);
|
|
|
+ $second = $this->activity(8, ['min_deposit' => 50, 'reward_amount' => 10]);
|
|
|
+ $this->assertArrayNotHasKey('forbidden_categories', json_decode(DB::table('promotion_activities')->where('id', $first)->value('params'), true));
|
|
|
+ DB::table('promotion_activities')->where('id', $second)->update(['params' => json_encode(['min_deposit' => '50.00', 'reward_amount' => '10.00', 'turnover_ratio' => '0.00', 'forbidden_categories' => ['third:5']])]);
|
|
|
+ foreach ([$admin->detail($first), $admin->detail($second), $admin->listing(['type' => 8])['data'][0]] as $row) {
|
|
|
+ $this->assertArrayNotHasKey('forbidden_categories', $row);
|
|
|
+ $this->assertArrayNotHasKey('forbidden_categories', $row['params']);
|
|
|
}
|
|
|
- $this->assertSame([], $admin->save($base + ['params' => ['forbidden_categories' => []]])['forbidden_categories']);
|
|
|
- // 旧配置没有该键时按不限制返回。
|
|
|
- DB::table('promotion_activities')->where('id', $id)->update(['params' => json_encode(['min_deposit' => '100.00', 'reward_amount' => '25.00', 'turnover_ratio' => '0.00'])]);
|
|
|
- $this->assertSame([], $admin->detail($id)['params']['forbidden_categories']);
|
|
|
- // 发奖规则快照带上禁止大类,供会员端下注时判断。
|
|
|
- $admin->save($base + ['params' => ['forbidden_categories' => ['third:5'], 'turnover_ratio' => 2]]);
|
|
|
- $this->join(1); $this->service->onDeposit($this->deposit(1, '100'));
|
|
|
+ $saved = $admin->save(['id' => $second, 'type' => 8, 'status' => 1, 'translations' => $this->texts(), 'params' => ['reward_amount' => 12]]);
|
|
|
+ $this->assertArrayNotHasKey('forbidden_categories', json_decode(DB::table('promotion_activities')->where('id', $second)->value('params'), true));
|
|
|
+ $this->assertSame('12.00', $saved['params']['reward_amount']);
|
|
|
+
|
|
|
+ // 每个首充活动都使用同一设置;发奖快照记录会员确认时的设置。
|
|
|
+ foreach ($this->service->listing(1, 'H5', 'zh', 8)['data'] as $item) $this->assertSame(['third:1', 'third:2', 'self:sport'], $item['rules']['forbidden_categories']);
|
|
|
+ $admin->save(['id' => $first, 'type' => 8, 'status' => 1, 'translations' => $this->texts(), 'sort' => 9, 'params' => ['turnover_ratio' => 2]]);
|
|
|
+ $this->join(1);
|
|
|
+ $this->restrict(['third:5']);
|
|
|
+ $this->service->onDeposit($this->deposit(1, '100'));
|
|
|
$snapshot = json_decode(DB::table('promotion_rewards')->where('user_id', 1)->value('rule_snapshot'), true);
|
|
|
- $this->assertSame(['third:5'], $snapshot['forbidden_categories']);
|
|
|
+ $this->assertSame(['third:1', 'third:2', 'self:sport'], $snapshot['forbidden_categories']);
|
|
|
+ foreach ($this->service->listing(2, 'H5', 'zh', 8)['data'] as $item) $this->assertSame(['third:5'], $item['rules']['forbidden_categories']);
|
|
|
}
|
|
|
|
|
|
public function test_first_deposit_pays_the_confirmed_campaign_and_terms(): void
|
|
|
@@ -982,14 +998,14 @@ class PromotionActivitiesTest extends TestCase
|
|
|
$admin = new PromotionAdminService();
|
|
|
$base = ['type' => 8, 'status' => 1, 'translations' => $this->texts()];
|
|
|
$h5 = $admin->save($base + ['sort' => 1, 'publish_platforms' => ['H5'], 'params' => ['reward_amount' => 25, 'turnover_ratio' => 2]])['id'];
|
|
|
- $admin->save($base + ['sort' => 2, 'publish_platforms' => ['PC'], 'params' => [
|
|
|
- 'reward_amount' => 80, 'turnover_ratio' => 2, 'forbidden_categories' => ['self:lottery'],
|
|
|
- ]]);
|
|
|
+ $admin->save($base + ['sort' => 2, 'publish_platforms' => ['PC'], 'params' => ['reward_amount' => 80, 'turnover_ratio' => 2]]);
|
|
|
$shown = $this->service->listing(1, 'H5', 'zh', 8)['data'][0];
|
|
|
$offer = FirstDepositChoice::offer($shown['activity_id'], 'H5', $shown['rules'], null, null);
|
|
|
$this->assertTrue(FirstDepositChoice::record(User::find(1), true, 'test', $offer));
|
|
|
- $admin->save($base + ['id' => $h5, 'params' => ['reward_amount' => 50, 'forbidden_categories' => ['self:sport']]]);
|
|
|
+ // 全局禁止大类变更也会让已展示的活动凭据失效。
|
|
|
+ $this->restrict(['self:sport']);
|
|
|
$this->invalid(fn () => FirstDepositChoice::record(User::find(2), true, 'test', $offer), '首充活动已变更,请刷新后重新确认');
|
|
|
+ $admin->save($base + ['id' => $h5, 'params' => ['reward_amount' => 50]]);
|
|
|
$this->service->onDeposit($this->deposit(1, '100'));
|
|
|
$reward = DB::table('promotion_rewards')->where('user_id', 1)->first();
|
|
|
$this->assertSame($h5, (int) $reward->activity_id);
|
|
|
@@ -999,7 +1015,7 @@ class PromotionActivitiesTest extends TestCase
|
|
|
|
|
|
public function test_pending_order_cutover_is_source_specific_and_keeps_legacy_terms(): void
|
|
|
{
|
|
|
- $id = $this->activity(8, ['reward_amount' => 25, 'turnover_ratio' => 2, 'forbidden_categories' => ['self:lottery']]);
|
|
|
+ $id = $this->activity(8, ['reward_amount' => 25, 'turnover_ratio' => 2]); $this->restrict(['self:lottery']);
|
|
|
DB::statement('CREATE TABLE bot_payment_orders (id INTEGER PRIMARY KEY)');
|
|
|
DB::table('payment_orders')->insert(['id' => 1]);
|
|
|
$migration = require dirname(__DIR__, 2) . '/database/migrations/2026_11_12_130000_bind_first_deposit_choices.php';
|
|
|
@@ -1026,22 +1042,20 @@ class PromotionActivitiesTest extends TestCase
|
|
|
$this->assertSame([], json_decode(DB::table('promotion_rewards')->value('rule_snapshot'), true)['forbidden_categories']);
|
|
|
}
|
|
|
|
|
|
- public function test_first_deposit_with_turnover_cannot_forbid_every_category(): void
|
|
|
+ public function test_legacy_campaign_params_cannot_forbid_every_category_any_more(): void
|
|
|
{
|
|
|
$all = array_keys(\App\Services\Sharing\ShareDefaults::commissionCategories());
|
|
|
- $rules = ['turnover_ratio' => 2, 'forbidden_categories' => $all];
|
|
|
- $error = '首充奖励有打码量时,至少保留一个可投注的游戏大类';
|
|
|
- $this->invalid(fn () => $this->activity(8, $rules), $error);
|
|
|
$draft = $this->activity(8, [], 0);
|
|
|
- DB::table('promotion_activities')->where('id', $draft)->update(['params' => json_encode($rules)]);
|
|
|
- $this->invalid(fn () => (new PromotionAdminService())->status($draft, 1), $error);
|
|
|
+ DB::table('promotion_activities')->where('id', $draft)->update(['params' => json_encode(['turnover_ratio' => 2, 'forbidden_categories' => $all])]);
|
|
|
+ (new PromotionAdminService())->status($draft, 1);
|
|
|
+ $this->assertSame([], $this->service->listing(1, 'H5', 'zh', 8)['data'][0]['rules']['forbidden_categories']);
|
|
|
}
|
|
|
|
|
|
public function test_telegram_choice_is_bound_to_the_member_and_its_restriction_lifts_after_turnover(): void
|
|
|
{
|
|
|
\Illuminate\Support\Facades\Cache::swap(new \Illuminate\Cache\Repository(new \Illuminate\Cache\ArrayStore()));
|
|
|
- $activity = $this->activity(8, ['reward_amount' => 25, 'turnover_ratio' => 2, 'forbidden_categories' => ['self:sport']]);
|
|
|
- $lotteryForbidden = $this->activity(8, ['reward_amount' => 25, 'turnover_ratio' => 2, 'forbidden_categories' => ['self:lottery']]);
|
|
|
+ $activity = $this->activity(8, ['reward_amount' => 25, 'turnover_ratio' => 2]);
|
|
|
+ $this->restrict(['self:sport']);
|
|
|
$member = User::find(1);
|
|
|
$reply = TelegramFirstDeposit::menu($member);
|
|
|
$button = json_decode($reply['reply_markup'], true)['inline_keyboard'][0][0]['callback_data'];
|
|
|
@@ -1052,11 +1066,30 @@ class PromotionActivitiesTest extends TestCase
|
|
|
$this->service->onDeposit($this->deposit(1, '100'));
|
|
|
$this->assertFalse($this->service->lotteryRestricted('m1'));
|
|
|
|
|
|
+ // 禁投 F1彩票 时 Telegram 没有可参加的首充活动,网页端仍可参加。
|
|
|
+ $this->restrict(['self:lottery']);
|
|
|
+ $this->member(3);
|
|
|
+ $this->assertSame(1, count(json_decode(TelegramFirstDeposit::menu(User::find(3))['reply_markup'], true)['inline_keyboard']));
|
|
|
$this->assertTrue($this->join(2));
|
|
|
- $this->assertSame($lotteryForbidden, (int) FirstDepositChoice::selection(2)->activity_id);
|
|
|
+ $this->assertSame($activity, (int) FirstDepositChoice::selection(2)->activity_id);
|
|
|
$this->service->onDeposit($this->deposit(2, '100'));
|
|
|
$this->assertTrue($this->service->lotteryRestricted('m2'));
|
|
|
$this->bet(2, '50', '-50', PromotionFacts::now());
|
|
|
$this->assertFalse($this->service->lotteryRestricted('m2'));
|
|
|
}
|
|
|
+
|
|
|
+ public function test_admin_http_reads_and_saves_the_shared_first_deposit_restriction(): void
|
|
|
+ {
|
|
|
+ $controller = new \App\Http\Controllers\admin\Promotion(); $admin = new PromotionAdminService();
|
|
|
+ $this->app->instance('request', Request::create('/admin/promotion/first-deposit-restriction', 'POST', ['categories' => ['self:lottery', 'third:2']]));
|
|
|
+ $saved = $controller->firstDepositRestriction($admin)->getData(true);
|
|
|
+ $this->assertSame(0, $saved['code']); $this->assertSame(['third:2', 'self:lottery'], $saved['data']['categories']);
|
|
|
+ $this->app->instance('request', Request::create('/admin/promotion/first-deposit-restriction', 'POST', ['categories' => ['third:3']]));
|
|
|
+ $failed = $controller->firstDepositRestriction($admin)->getData(true);
|
|
|
+ $this->assertSame(-3, $failed['code']); $this->assertStringContainsString('categories参数错误', $failed['msg']);
|
|
|
+ $this->app->instance('request', Request::create('/admin/promotion/first-deposit-restriction', 'GET'));
|
|
|
+ $read = $controller->firstDepositRestriction($admin)->getData(true);
|
|
|
+ $this->assertSame(['third:2', 'self:lottery'], $read['data']['categories']);
|
|
|
+ $this->assertCount(8, $read['data']['options']);
|
|
|
+ }
|
|
|
}
|