|
|
@@ -928,22 +928,28 @@ class PromotionActivitiesTest extends TestCase
|
|
|
$this->assertSame(0, $admin->records(['activity_id' => $loss, 'type' => 7, 'kind' => 'rewards'])['total']);
|
|
|
}
|
|
|
|
|
|
- public function test_first_deposit_is_automatic_without_opt_in(): void
|
|
|
+ public function test_first_deposit_requires_opt_in_and_declining_consumes_the_first_deposit(): void
|
|
|
{
|
|
|
$id = $this->activity(8, ['min_deposit' => 100, 'reward_amount' => 25]);
|
|
|
- // 不需要勾选:没有任何选择记录的会员首笔到账也发奖,第二笔起不再算首充。
|
|
|
+ // 默认不勾选:没有记录选择的会员首笔到账不发奖,第二笔起也不再算首充。
|
|
|
$this->service->onDeposit($this->deposit(1, '100'));
|
|
|
+ $this->invalid(fn () => $this->join(1), '已有充值记录,不能再选择首充活动');
|
|
|
$this->service->onDeposit($this->deposit(1, '1000'));
|
|
|
- $this->assertSame(1, DB::table('promotion_rewards')->where('activity_id', $id)->where('user_id', 1)->count());
|
|
|
+ $this->assertSame(0, DB::table('promotion_rewards')->count());
|
|
|
$this->assertSame(1, DB::table('promotion_first_deposits')->where('user_id', 1)->count());
|
|
|
- // 旧前端保存过「不参加」也照样发奖。
|
|
|
- $this->assertTrue($this->join(2, '0'));
|
|
|
+ // 明确选择不参加后又改为参加:以首笔到账前最后一次选择为准。
|
|
|
+ $this->assertTrue($this->join(2, '0')); $this->assertTrue($this->join(2, true));
|
|
|
+ $this->assertSame(1, (int)DB::table('promotion_first_deposit_choices')->where('user_id', 2)->value('joined'));
|
|
|
+ $this->assertFalse($this->join(2, null));
|
|
|
$this->service->onDeposit($this->deposit(2, '100'));
|
|
|
$this->assertSame(1, DB::table('promotion_rewards')->where('activity_id', $id)->where('user_id', 2)->count());
|
|
|
- // 首笔没达到门槛就不发,后面的充值也不再补。
|
|
|
- $this->member(3);
|
|
|
- $this->service->onDeposit($this->deposit(3, '50')); $this->service->onDeposit($this->deposit(3, '500'));
|
|
|
+ // 首笔到账后选择不能再修改。
|
|
|
+ $this->assertFalse($this->join(2, 0));
|
|
|
+ $this->assertSame(1, (int)DB::table('promotion_first_deposit_choices')->where('user_id', 2)->value('joined'));
|
|
|
+ $this->member(3); $this->join(3, 0);
|
|
|
+ $this->service->onDeposit($this->deposit(3, '100')); $this->service->onDeposit($this->deposit(3, '500'));
|
|
|
$this->assertSame(0, DB::table('promotion_rewards')->where('user_id', 3)->count());
|
|
|
+ $this->invalid(fn () => \App\Support\FirstDepositChoice::parse('yes'), 'join_first_deposit只能为0或1');
|
|
|
}
|
|
|
|
|
|
public function test_first_deposit_restriction_is_one_setting_shared_by_every_campaign(): void
|
|
|
@@ -976,38 +982,110 @@ class PromotionActivitiesTest extends TestCase
|
|
|
$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_highest_tier_the_deposit_reaches(): void
|
|
|
{
|
|
|
- // 100 送 10、1000 送 50:充 1000 发 50,充 500 发 10,充 50 不发;高档份数用完发下一档。
|
|
|
+ // 100 送 10、1000 送 50:会员确认的是 100 送 10(第一个活动),充 1000 发 50,充 500 发 10,充 50 不发。
|
|
|
$low = $this->activity(8, ['min_deposit' => 100, 'reward_amount' => 10]);
|
|
|
- $high = $this->activity(8, ['min_deposit' => 1000, 'reward_amount' => 50, 'turnover_ratio' => 2]);
|
|
|
- $this->restrict(['self:lottery']);
|
|
|
+ $high = $this->activity(8, ['min_deposit' => 1000, 'reward_amount' => 50]);
|
|
|
+ $pick = function (int $member, int $activityId) {
|
|
|
+ $item = collect($this->service->listing($member, 'H5', 'zh', 8)['data'])->firstWhere('activity_id', $activityId);
|
|
|
+ $offer = FirstDepositChoice::offer($activityId, 'H5', $item['rules'], $item['start_time'], $item['end_time']);
|
|
|
+ $this->assertTrue(FirstDepositChoice::record(User::find($member), true, 'test', $offer));
|
|
|
+ };
|
|
|
+ $pick(1, $low);
|
|
|
$this->service->onDeposit($this->deposit(1, '1000'));
|
|
|
$reward = DB::table('promotion_rewards')->where('user_id', 1)->first();
|
|
|
- $this->assertSame([$high, '50.00', '100.00'], [(int) $reward->activity_id, number_format((float) $reward->amount, 2, '.', ''), number_format((float) $reward->required_flow, 2, '.', '')]);
|
|
|
- $this->assertSame(['self:lottery'], json_decode($reward->rule_snapshot, true)['forbidden_categories'], 'restriction set at credit time');
|
|
|
+ $this->assertSame([$high, '50.00'], [(int) $reward->activity_id, number_format((float) $reward->amount, 2, '.', '')]);
|
|
|
+ $this->assertArrayNotHasKey('platform', json_decode($reward->rule_snapshot, true));
|
|
|
|
|
|
+ $pick(2, $high);
|
|
|
$this->service->onDeposit($this->deposit(2, '500'));
|
|
|
- $this->assertSame($low, (int) DB::table('promotion_rewards')->where('user_id', 2)->value('activity_id'));
|
|
|
+ $this->assertSame($low, (int) DB::table('promotion_rewards')->where('user_id', 2)->value('activity_id'), 'falls back to the tier the deposit reaches');
|
|
|
|
|
|
- $this->member(3);
|
|
|
+ $this->member(3); $pick(3, $low);
|
|
|
$this->service->onDeposit($this->deposit(3, '50'));
|
|
|
$this->assertSame(0, DB::table('promotion_rewards')->where('user_id', 3)->count());
|
|
|
|
|
|
+ // 高档份数用完时发下一档。
|
|
|
DB::table('promotion_activities')->where('id', $high)->update(['reward_quota' => 1, 'rewarded_count' => 1]);
|
|
|
- $this->member(4);
|
|
|
+ $this->member(4); $pick(4, $low);
|
|
|
$this->service->onDeposit($this->deposit(4, '2000'));
|
|
|
$this->assertSame($low, (int) DB::table('promotion_rewards')->where('user_id', 4)->value('activity_id'));
|
|
|
+
|
|
|
+ // 只发布在别的端的活动不参与:H5 确认的会员不会拿到只发布在 PC 的高档。
|
|
|
+ DB::table('promotion_activities')->where('id', $high)->update(['reward_quota' => null, 'publish_platforms' => '["PC"]']);
|
|
|
+ $this->member(5); $pick(5, $low);
|
|
|
+ $this->service->onDeposit($this->deposit(5, '1000'));
|
|
|
+ $this->assertSame($low, (int) DB::table('promotion_rewards')->where('user_id', 5)->value('activity_id'));
|
|
|
+
|
|
|
+ // 上线前的选择没有记录端:按确认的活动发布的端判断。
|
|
|
+ DB::table('promotion_activities')->where('id', $high)->update(['publish_platforms' => '["PC","H5","APP"]']);
|
|
|
+ $this->member(6); $pick(6, $low);
|
|
|
+ $row = DB::table('promotion_first_deposit_choices')->where('user_id', 6);
|
|
|
+ $legacy = json_decode($row->value('rule_snapshot'), true); unset($legacy['platform']);
|
|
|
+ $row->update(['rule_snapshot' => json_encode($legacy)]);
|
|
|
+ $this->service->onDeposit($this->deposit(6, '1000'));
|
|
|
+ $this->assertSame($high, (int) DB::table('promotion_rewards')->where('user_id', 6)->value('activity_id'));
|
|
|
+ }
|
|
|
+
|
|
|
+ public function test_first_deposit_pays_the_confirmed_campaign_and_terms(): void
|
|
|
+ {
|
|
|
+ $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]]);
|
|
|
+ $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));
|
|
|
+ // 全局禁止大类变更也会让已展示的活动凭据失效。
|
|
|
+ $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);
|
|
|
+ $this->assertSame('25.00', json_decode($reward->rule_snapshot, true)['reward_amount']);
|
|
|
+ $this->assertSame([], json_decode($reward->rule_snapshot, true)['forbidden_categories']);
|
|
|
+ }
|
|
|
+
|
|
|
+ public function test_pending_order_cutover_is_source_specific_and_keeps_legacy_terms(): void
|
|
|
+ {
|
|
|
+ $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';
|
|
|
+ $migration->down();
|
|
|
+ $migration->up();
|
|
|
+ DB::table('payment_orders')->insert(['id' => 2]);
|
|
|
+ $migration->up();
|
|
|
+
|
|
|
+ $old = $this->deposit(1, '100', type: '三方充值');
|
|
|
+ $old->related_id = 1;
|
|
|
+ $old->save();
|
|
|
+ $this->service->onDeposit($old, 'payment_orders');
|
|
|
+ $new = $this->deposit(2, '100', type: '三方充值');
|
|
|
+ $new->related_id = 2;
|
|
|
+ $new->save();
|
|
|
+ $this->service->onDeposit($new, 'payment_orders');
|
|
|
+ $this->member(3);
|
|
|
+ $otherSource = $this->deposit(3, '100');
|
|
|
+ $otherSource->related_id = 1;
|
|
|
+ $otherSource->save();
|
|
|
+ $this->service->onDeposit($otherSource, 'recharges');
|
|
|
+
|
|
|
+ $this->assertSame([1], DB::table('promotion_rewards')->where('activity_id', $id)->pluck('user_id')->all());
|
|
|
+ $this->assertSame([], json_decode(DB::table('promotion_rewards')->value('rule_snapshot'), true)['forbidden_categories']);
|
|
|
}
|
|
|
|
|
|
public function test_legacy_campaign_params_cannot_forbid_every_category_any_more(): void
|