|
|
@@ -0,0 +1,357 @@
|
|
|
+<?php
|
|
|
+
|
|
|
+namespace Tests\Integration;
|
|
|
+
|
|
|
+use App\Console\Commands\AutoGrantRewards;
|
|
|
+use App\Http\Controllers\admin\Backflow as BackflowController;
|
|
|
+use App\Http\Controllers\admin\Rebate as RebateController;
|
|
|
+use App\Http\Controllers\admin\RewardGrantSettings;
|
|
|
+use App\Models\Rebate;
|
|
|
+use App\Services\AutomaticRewardGrantService;
|
|
|
+use App\Services\BackflowService;
|
|
|
+use App\Services\RebateService;
|
|
|
+use App\Services\RewardGrantService;
|
|
|
+use App\Services\RewardGrantSettingsService;
|
|
|
+use Carbon\Carbon;
|
|
|
+use Illuminate\Config\Repository;
|
|
|
+use Illuminate\Database\Capsule\Manager;
|
|
|
+use Illuminate\Database\Schema\Blueprint;
|
|
|
+use Illuminate\Events\Dispatcher;
|
|
|
+use Illuminate\Foundation\Application;
|
|
|
+use Illuminate\Http\JsonResponse;
|
|
|
+use Illuminate\Http\Request;
|
|
|
+use Illuminate\Routing\Router;
|
|
|
+use Illuminate\Support\Facades\DB;
|
|
|
+use Illuminate\Support\Facades\Facade;
|
|
|
+use Illuminate\Support\Facades\Schema;
|
|
|
+use Illuminate\Translation\ArrayLoader;
|
|
|
+use Illuminate\Translation\Translator;
|
|
|
+use Illuminate\Validation\Factory;
|
|
|
+use PHPUnit\Framework\TestCase;
|
|
|
+use Symfony\Component\Console\Tester\CommandTester;
|
|
|
+
|
|
|
+/** Real payout controllers, command, wallets and ledgers; no project boot, .env or live database. */
|
|
|
+class AutomaticRewardGrantTest extends TestCase
|
|
|
+{
|
|
|
+ private Application $app;
|
|
|
+
|
|
|
+ protected function setUp(): void
|
|
|
+ {
|
|
|
+ parent::setUp();
|
|
|
+ Carbon::setTestNow(Carbon::parse('2026-10-25 14:30:00', 'Asia/Shanghai'));
|
|
|
+ Facade::clearResolvedInstances();
|
|
|
+ $this->app = new Application(dirname(__DIR__, 2));
|
|
|
+ $this->app->instance('config', new Repository(['app' => ['locale' => 'zh', 'timezone' => 'Asia/Shanghai']]));
|
|
|
+ Facade::setFacadeApplication($this->app);
|
|
|
+ $db = new Manager($this->app);
|
|
|
+ $db->addConnection(['driver' => 'sqlite', 'database' => ':memory:', 'prefix' => 'bot_']);
|
|
|
+ $db->setEventDispatcher(new Dispatcher($this->app));
|
|
|
+ $db->setAsGlobal(); $db->bootEloquent();
|
|
|
+ $this->app->instance('db', $db->getDatabaseManager());
|
|
|
+ $this->app->bind('db.schema', fn () => $db->schema());
|
|
|
+ $translator = new Translator(new ArrayLoader(), 'zh');
|
|
|
+ $this->app->instance('translator', $translator);
|
|
|
+ $this->app->instance('validator', new Factory($translator, $this->app));
|
|
|
+ $this->app->instance('log', new \Psr\Log\NullLogger());
|
|
|
+ $this->app->instance(\Illuminate\Contracts\Routing\ResponseFactory::class, new class {
|
|
|
+ public function json($data, $status = 200, $headers = [], $options = 0) {
|
|
|
+ return new JsonResponse($data, $status, $headers, $options);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ Request::macro('validate', function (array $rules) {
|
|
|
+ return app('validator')->make($this->all(), $rules)->validate();
|
|
|
+ });
|
|
|
+ Schema::create('config', function (Blueprint $t) { $t->id(); $t->string('field'); $t->string('val'); });
|
|
|
+ DB::table('config')->insert([
|
|
|
+ ['field' => 'rebate', 'val' => '0.005'],
|
|
|
+ ['field' => 'huishui_restriction', 'val' => '100'],
|
|
|
+ ['field' => 'huishui_percentage', 'val' => '0.05'],
|
|
|
+ ]);
|
|
|
+ Schema::create('wallets', function (Blueprint $t) {
|
|
|
+ $t->id(); $t->string('member_id'); $t->decimal('available_balance', 24, 10)->default(0); $t->timestamps();
|
|
|
+ });
|
|
|
+ DB::table('wallets')->insert(['member_id' => '1001', 'available_balance' => '20.00']);
|
|
|
+ Schema::create('balance_logs', function (Blueprint $t) {
|
|
|
+ $t->id(); $t->string('member_id'); $t->string('change_type'); $t->unsignedBigInteger('related_id');
|
|
|
+ foreach (['amount', 'before_balance', 'after_balance'] as $field) $t->decimal($field, 24, 10);
|
|
|
+ $t->string('remark'); $t->timestamps();
|
|
|
+ });
|
|
|
+ Schema::create('rebates', function (Blueprint $t) {
|
|
|
+ $t->id(); $t->string('member_id'); $t->string('date');
|
|
|
+ $t->decimal('betting_amount', 24, 2); $t->decimal('effective_betting_amount', 24, 2);
|
|
|
+ $t->decimal('rebate_ratio', 12, 4)->default(0); $t->decimal('amount', 24, 2)->default(0);
|
|
|
+ $t->integer('status')->default(0); $t->string('audited_by')->nullable(); $t->bigInteger('grant_time')->default(0); $t->timestamps();
|
|
|
+ });
|
|
|
+ Schema::create('backflow', function (Blueprint $t) {
|
|
|
+ $t->id(); $t->string('member_id'); $t->string('date');
|
|
|
+ foreach (['recharge_amount', 'withdrawal_amount', 'backflow_ratio', 'amount'] as $field) $t->decimal($field, 24, 4)->default(0);
|
|
|
+ $t->integer('status')->default(0); $t->bigInteger('grant_time')->default(0); $t->timestamps();
|
|
|
+ });
|
|
|
+ $this->migration()->up();
|
|
|
+ $this->request('/admin/rebate');
|
|
|
+ }
|
|
|
+
|
|
|
+ protected function tearDown(): void
|
|
|
+ {
|
|
|
+ Carbon::setTestNow(); DB::disconnect(); Facade::clearResolvedInstances();
|
|
|
+ parent::tearDown();
|
|
|
+ }
|
|
|
+
|
|
|
+ private function migration()
|
|
|
+ {
|
|
|
+ return require dirname(__DIR__, 2) . '/database/migrations/2026_10_25_180000_create_reward_grant_settings.php';
|
|
|
+ }
|
|
|
+
|
|
|
+ private function request(string $path, array $params = [], string $method = 'GET'): void
|
|
|
+ {
|
|
|
+ $request = Request::create($path, $method, $params);
|
|
|
+ $request->user = (object) ['id' => 1, 'username' => 'operator-one'];
|
|
|
+ $this->app->instance('request', $request);
|
|
|
+ }
|
|
|
+
|
|
|
+ private function rebate(array $values = []): int
|
|
|
+ {
|
|
|
+ return DB::table('rebates')->insertGetId($values + ['member_id' => '1001', 'date' => '2026-10-23',
|
|
|
+ 'betting_amount' => '1000.00', 'effective_betting_amount' => '800.00']);
|
|
|
+ }
|
|
|
+
|
|
|
+ private function backflow(array $values = []): int
|
|
|
+ {
|
|
|
+ return DB::table('backflow')->insertGetId($values + ['member_id' => '1001', 'date' => '2026-09',
|
|
|
+ 'recharge_amount' => '1000.00', 'withdrawal_amount' => '-300.00']);
|
|
|
+ }
|
|
|
+
|
|
|
+ private function enable(string $type, string $time = '14:30'): void
|
|
|
+ {
|
|
|
+ RewardGrantSettingsService::save($type, ['auto_enabled' => 1, 'grant_time' => $time]);
|
|
|
+ }
|
|
|
+
|
|
|
+ private function runRewards(): array { return (new AutomaticRewardGrantService())->run(); }
|
|
|
+
|
|
|
+ public function test_console_scheduler_registers_the_automatic_payout_command_every_minute_with_overlap_protection(): void
|
|
|
+ {
|
|
|
+ $this->app['config']->set('cache', ['default' => 'array', 'stores' => ['array' => ['driver' => 'array']]]);
|
|
|
+ $this->app->register(\Illuminate\Cache\CacheServiceProvider::class);
|
|
|
+ $schedule = new \Illuminate\Console\Scheduling\Schedule('Asia/Shanghai');
|
|
|
+ $kernel = new \App\Console\Kernel($this->app, new Dispatcher($this->app));
|
|
|
+ $method = new \ReflectionMethod($kernel, 'schedule');
|
|
|
+ $method->setAccessible(true);
|
|
|
+ $method->invoke($kernel, $schedule);
|
|
|
+ $events = array_values(array_filter($schedule->events(), fn ($event) => str_contains($event->command ?? '', 'rewards:auto-grant')));
|
|
|
+ $this->assertCount(1, $events);
|
|
|
+ $this->assertSame('* * * * *', $events[0]->expression);
|
|
|
+ $this->assertTrue($events[0]->withoutOverlapping);
|
|
|
+ $this->assertSame(60, $events[0]->expiresAt);
|
|
|
+ }
|
|
|
+
|
|
|
+ public function test_migration_defaults_to_manual_and_both_settings_routes_keep_admin_and_existing_grant_permissions(): void
|
|
|
+ {
|
|
|
+ $id = $this->rebate(); $this->backflow();
|
|
|
+ foreach (['rebate', 'backflow'] as $type) {
|
|
|
+ $this->assertSame(['type' => $type, 'auto_enabled' => 0, 'grant_time' => '03:00', 'timezone' => 'Asia/Shanghai'], RewardGrantSettingsService::get($type));
|
|
|
+ }
|
|
|
+ $this->assertSame(0, $this->runRewards()['rebate']['granted']);
|
|
|
+ $this->assertSame(0, DB::table('rebates')->where('id', $id)->value('status'));
|
|
|
+ $this->assertSame(0, DB::table('balance_logs')->count());
|
|
|
+ $router = new Router(new Dispatcher($this->app), $this->app);
|
|
|
+ $this->app->instance('router', $router);
|
|
|
+ $router->group(['prefix' => 'admin'], function () { require dirname(__DIR__, 2) . '/routes/admin.php'; });
|
|
|
+ foreach (['rebate' => 'submit', 'backflow' => 'grant'] as $type => $permission) {
|
|
|
+ foreach (['GET', 'POST'] as $method) {
|
|
|
+ $route = $router->getRoutes()->match(Request::create('/admin/' . $type . '/grant-settings', $method));
|
|
|
+ $this->assertContains('admin.jwt', $route->middleware());
|
|
|
+ $this->assertContains('check.button.uri', $route->middleware());
|
|
|
+ $this->assertSame('admin/' . $type . '/' . $permission, $route->defaults['permission_uri']);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ $this->migration()->down();
|
|
|
+ $this->assertFalse(Schema::hasTable('reward_grant_settings'));
|
|
|
+ }
|
|
|
+
|
|
|
+ public function test_settings_api_persists_independent_times_and_disabling_preserves_the_previous_time_without_paying(): void
|
|
|
+ {
|
|
|
+ $this->rebate();
|
|
|
+ $this->request('/admin/rebate/grant-settings', ['auto_enabled' => true, 'grant_time' => '14:30'], 'POST');
|
|
|
+ $body = (new RewardGrantSettings())->saveRebate()->getData(true);
|
|
|
+ $this->assertSame(0, $body['code']);
|
|
|
+ $this->assertSame(1, $body['data']['auto_enabled']);
|
|
|
+ $this->assertSame('14:30', $body['data']['grant_time']);
|
|
|
+ $this->assertSame(0, RewardGrantSettingsService::get('backflow')['auto_enabled']);
|
|
|
+ $this->request('/admin/backflow/grant-settings', ['auto_enabled' => 1, 'grant_time' => '20:15'], 'POST');
|
|
|
+ $this->assertSame(0, (new RewardGrantSettings())->saveBackflow()->getData(true)['code']);
|
|
|
+ $this->assertSame('20:15', RewardGrantSettingsService::get('backflow')['grant_time']);
|
|
|
+ $this->request('/admin/rebate/grant-settings', ['auto_enabled' => 0], 'POST');
|
|
|
+ $body = (new RewardGrantSettings())->saveRebate()->getData(true);
|
|
|
+ $this->assertSame(0, $body['data']['auto_enabled']);
|
|
|
+ $this->assertSame('14:30', $body['data']['grant_time']);
|
|
|
+ $this->assertSame(0, DB::table('balance_logs')->count());
|
|
|
+ $this->assertSame(0, (new RewardGrantSettings())->rebate()->getData(true)['code']);
|
|
|
+ }
|
|
|
+
|
|
|
+ /** @dataProvider invalidSettings */
|
|
|
+ public function test_invalid_settings_do_not_change_the_switch_or_time(array $values): void
|
|
|
+ {
|
|
|
+ $this->request('/admin/rebate/grant-settings', $values, 'POST');
|
|
|
+ $this->assertSame(-3, (new RewardGrantSettings())->saveRebate()->getData(true)['code']);
|
|
|
+ $this->assertSame(0, RewardGrantSettingsService::get('rebate')['auto_enabled']);
|
|
|
+ $this->assertSame('03:00', RewardGrantSettingsService::get('rebate')['grant_time']);
|
|
|
+ }
|
|
|
+
|
|
|
+ public static function invalidSettings(): array
|
|
|
+ {
|
|
|
+ return [[[]], [['auto_enabled' => 2]], [['auto_enabled' => 'auto']], [['auto_enabled' => 1]],
|
|
|
+ [['auto_enabled' => true]], [['auto_enabled' => 1, 'grant_time' => '']],
|
|
|
+ [['auto_enabled' => 1, 'grant_time' => '24:00']], [['auto_enabled' => 1, 'grant_time' => '9:00']],
|
|
|
+ [['auto_enabled' => 1, 'grant_time' => '12:60']], [['auto_enabled' => 0, 'grant_time' => ['03:00']]]];
|
|
|
+ }
|
|
|
+
|
|
|
+ public function test_exact_minute_pays_both_types_once_and_retries_do_not_duplicate_wallet_or_ledger(): void
|
|
|
+ {
|
|
|
+ $rebate = $this->rebate(); $backflow = $this->backflow();
|
|
|
+ $this->rebate(['date' => '2026-10-25']); $this->backflow(['date' => '2026-10']);
|
|
|
+ $this->rebate(['status' => 1]); $this->backflow(['status' => 1]);
|
|
|
+ $this->enable('rebate'); $this->enable('backflow');
|
|
|
+ Carbon::setTestNow(Carbon::parse('2026-10-25 14:29:59', 'Asia/Shanghai'));
|
|
|
+ $this->assertSame(0, $this->runRewards()['rebate']['granted']);
|
|
|
+ $this->assertSame(0, DB::table('balance_logs')->count());
|
|
|
+ Carbon::setTestNow(Carbon::parse('2026-10-25 14:30:00', 'Asia/Shanghai'));
|
|
|
+ $result = $this->runRewards();
|
|
|
+ $this->assertSame(1, $result['rebate']['granted']); $this->assertSame(1, $result['backflow']['granted']);
|
|
|
+ $this->assertEquals(59, DB::table('wallets')->value('available_balance'));
|
|
|
+ $this->assertEquals(4, DB::table('rebates')->where('id', $rebate)->value('amount'));
|
|
|
+ $this->assertEquals(35, DB::table('backflow')->where('id', $backflow)->value('amount'));
|
|
|
+ $this->assertSame('system:auto-grant', DB::table('rebates')->where('id', $rebate)->value('audited_by'));
|
|
|
+ $this->assertSame(now()->timestamp, DB::table('backflow')->where('id', $backflow)->value('grant_time'));
|
|
|
+ $this->assertSame(2, DB::table('balance_logs')->count());
|
|
|
+ $this->assertSame(0, $this->runRewards()['rebate']['granted']);
|
|
|
+ $this->assertSame(2, DB::table('balance_logs')->count());
|
|
|
+ $this->assertEquals(59, DB::table('wallets')->value('available_balance'));
|
|
|
+ }
|
|
|
+
|
|
|
+ public function test_scheduled_cutoffs_keep_new_york_days_and_shanghai_months_even_when_catching_up_later(): void
|
|
|
+ {
|
|
|
+ Carbon::setTestNow(Carbon::parse('2026-11-01 03:00:00', 'Asia/Shanghai'));
|
|
|
+ $old = $this->rebate(['date' => '2026-10-30']);
|
|
|
+ $notYetEndedAtSchedule = $this->rebate(['date' => '2026-10-31']);
|
|
|
+ $lastMonth = $this->backflow(['date' => '2026-10']);
|
|
|
+ $currentMonth = $this->backflow(['date' => '2026-11']);
|
|
|
+ $this->enable('rebate', '03:00'); $this->enable('backflow', '03:00');
|
|
|
+ // At 14:00 the New York day has changed, but this run still belongs to the 03:00 schedule.
|
|
|
+ Carbon::setTestNow(Carbon::parse('2026-11-01 14:00:00', 'Asia/Shanghai'));
|
|
|
+ $this->runRewards();
|
|
|
+ $this->assertSame(1, DB::table('rebates')->where('id', $old)->value('status'));
|
|
|
+ $this->assertSame(0, DB::table('rebates')->where('id', $notYetEndedAtSchedule)->value('status'));
|
|
|
+ $this->assertSame(1, DB::table('backflow')->where('id', $lastMonth)->value('status'));
|
|
|
+ $this->assertSame(0, DB::table('backflow')->where('id', $currentMonth)->value('status'));
|
|
|
+ Carbon::setTestNow(Carbon::parse('2026-11-02 02:59:59', 'Asia/Shanghai'));
|
|
|
+ $this->assertSame(0, $this->runRewards()['rebate']['granted']);
|
|
|
+ Carbon::setTestNow(Carbon::parse('2026-11-02 03:00:00', 'Asia/Shanghai'));
|
|
|
+ $this->assertSame(1, $this->runRewards()['rebate']['granted']);
|
|
|
+ $this->assertSame(1, DB::table('rebates')->where('id', $notYetEndedAtSchedule)->value('status'));
|
|
|
+ }
|
|
|
+
|
|
|
+ public function test_disabled_switch_stops_auto_but_both_manual_controllers_still_work_and_stale_calls_cannot_pay_twice(): void
|
|
|
+ {
|
|
|
+ $id = $this->rebate(); $backflow = $this->backflow();
|
|
|
+ $stale = Rebate::findOrFail($id);
|
|
|
+ $this->enable('rebate');
|
|
|
+ RewardGrantSettingsService::save('rebate', ['auto_enabled' => 0]);
|
|
|
+ $this->assertSame(0, $this->runRewards()['rebate']['granted']);
|
|
|
+ $this->request('/admin/rebate/submit', ['id' => $id], 'POST');
|
|
|
+ $this->assertSame(0, (new RebateController())->store()->getData(true)['code']);
|
|
|
+ $this->assertSame(-3, (new RebateController())->store()->getData(true)['code']);
|
|
|
+ $this->assertSame('operator-one', DB::table('rebates')->where('id', $id)->value('audited_by'));
|
|
|
+ $this->request('/admin/backflow/grant', ['id' => $backflow], 'POST');
|
|
|
+ $this->assertSame(0, (new BackflowController())->grant()->getData(true)['code']);
|
|
|
+ $this->assertSame(-3, (new BackflowController())->grant()->getData(true)['code']);
|
|
|
+ $this->enable('rebate'); $this->enable('backflow');
|
|
|
+ $this->runRewards();
|
|
|
+ $this->assertFalse(RewardGrantService::grant('rebate', $id, 'auto', true));
|
|
|
+ $this->assertSame(2, DB::table('balance_logs')->count());
|
|
|
+ try { RebateService::BibiReturn($stale, '999999'); $this->fail('A stale row must not pay again'); }
|
|
|
+ catch (\RuntimeException $e) { $this->assertSame(-3, $e->getCode()); }
|
|
|
+ $this->assertEquals(59, DB::table('wallets')->value('available_balance'));
|
|
|
+ }
|
|
|
+
|
|
|
+ public function test_payout_reads_fresh_effective_bet_and_zero_rewards_are_finalized_without_ledger_credits(): void
|
|
|
+ {
|
|
|
+ $id = $this->rebate(); $stale = Rebate::findOrFail($id);
|
|
|
+ DB::table('rebates')->where('id', $id)->update(['effective_betting_amount' => 100]);
|
|
|
+ RebateService::BibiReturn($stale, '800');
|
|
|
+ $this->assertEquals(0.5, DB::table('rebates')->where('id', $id)->value('amount'));
|
|
|
+ $this->rebate(['effective_betting_amount' => 0]);
|
|
|
+ $below = $this->backflow(['recharge_amount' => 50, 'withdrawal_amount' => 0]);
|
|
|
+ $this->enable('rebate'); $this->enable('backflow'); $this->runRewards();
|
|
|
+ $this->assertSame(1, DB::table('backflow')->where('id', $below)->value('status'));
|
|
|
+ $this->assertEquals(0, DB::table('backflow')->where('id', $below)->value('amount'));
|
|
|
+ $this->assertSame(1, DB::table('balance_logs')->count());
|
|
|
+ $this->assertEquals(20.5, DB::table('wallets')->value('available_balance'));
|
|
|
+ }
|
|
|
+
|
|
|
+ public function test_ledger_failure_rolls_back_wallet_and_reward_then_automatic_retry_credits_once(): void
|
|
|
+ {
|
|
|
+ $id = $this->rebate(); $this->enable('rebate');
|
|
|
+ DB::statement("CREATE TRIGGER reject_reward_ledger BEFORE INSERT ON bot_balance_logs BEGIN SELECT RAISE(ABORT, 'ledger unavailable'); END");
|
|
|
+ $this->assertSame(1, $this->runRewards()['rebate']['failed']);
|
|
|
+ $this->assertEquals(20, DB::table('wallets')->value('available_balance'));
|
|
|
+ $this->assertSame(0, DB::table('rebates')->where('id', $id)->value('status'));
|
|
|
+ $this->assertSame(0, DB::table('rebates')->where('id', $id)->value('grant_time'));
|
|
|
+ $this->assertSame(0, DB::table('balance_logs')->count());
|
|
|
+ DB::statement('DROP TRIGGER reject_reward_ledger');
|
|
|
+ $this->assertSame(1, $this->runRewards()['rebate']['granted']);
|
|
|
+ $this->assertEquals(24, DB::table('wallets')->value('available_balance'));
|
|
|
+ $this->assertSame(1, DB::table('balance_logs')->count());
|
|
|
+ }
|
|
|
+
|
|
|
+ public function test_status_save_failure_rolls_back_backflow_wallet_and_ledger_and_missing_wallet_does_not_stop_other_rows(): void
|
|
|
+ {
|
|
|
+ $id = $this->backflow(); $this->enable('backflow');
|
|
|
+ DB::statement("CREATE TRIGGER reject_backflow_status BEFORE UPDATE ON bot_backflow BEGIN SELECT RAISE(ABORT, 'reward unavailable'); END");
|
|
|
+ $this->assertSame(1, $this->runRewards()['backflow']['failed']);
|
|
|
+ $this->assertSame(0, DB::table('backflow')->where('id', $id)->value('status'));
|
|
|
+ $this->assertSame(0, DB::table('balance_logs')->count());
|
|
|
+ $this->assertEquals(20, DB::table('wallets')->value('available_balance'));
|
|
|
+ DB::statement('DROP TRIGGER reject_backflow_status');
|
|
|
+ $bad = $this->rebate(['member_id' => 'no-wallet']); $good = $this->rebate(); $this->enable('rebate');
|
|
|
+ $result = $this->runRewards();
|
|
|
+ $this->assertSame(1, $result['rebate']['failed']); $this->assertSame(1, $result['rebate']['granted']);
|
|
|
+ $this->assertSame(0, DB::table('rebates')->where('id', $bad)->value('status'));
|
|
|
+ $this->assertSame(1, DB::table('rebates')->where('id', $good)->value('status'));
|
|
|
+ $this->assertSame(1, $result['backflow']['granted']);
|
|
|
+ }
|
|
|
+
|
|
|
+ public function test_switch_and_time_are_rechecked_between_records_and_before_retrying(): void
|
|
|
+ {
|
|
|
+ $this->rebate(); $second = $this->rebate(); $this->enable('rebate');
|
|
|
+ $changed = false;
|
|
|
+ $this->app['db']->connection()->listen(function ($event) use (&$changed) {
|
|
|
+ if (!$changed && str_starts_with(strtolower($event->sql), 'update "bot_rebates"')) {
|
|
|
+ $changed = true;
|
|
|
+ RewardGrantSettingsService::save('rebate', ['auto_enabled' => 0]);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ $this->assertSame(1, $this->runRewards()['rebate']['granted']);
|
|
|
+ $this->assertSame(0, DB::table('rebates')->where('id', $second)->value('status'));
|
|
|
+ $this->assertSame(1, DB::table('balance_logs')->count());
|
|
|
+ $this->enable('rebate', '15:00');
|
|
|
+ $this->assertSame(0, $this->runRewards()['rebate']['granted']);
|
|
|
+ Carbon::setTestNow(Carbon::parse('2026-10-25 15:00:00', 'Asia/Shanghai'));
|
|
|
+ $this->assertSame(1, $this->runRewards()['rebate']['granted']);
|
|
|
+ }
|
|
|
+
|
|
|
+ public function test_chunked_processing_does_not_skip_rows_and_command_reports_failure_without_forcing_disabled_rewards(): void
|
|
|
+ {
|
|
|
+ for ($i = 0; $i < 105; ++$i) $this->rebate(['effective_betting_amount' => 0]);
|
|
|
+ $command = new AutoGrantRewards(); $command->setLaravel($this->app);
|
|
|
+ $tester = new CommandTester($command);
|
|
|
+ $tester->execute([]);
|
|
|
+ $this->assertSame(0, $tester->getStatusCode());
|
|
|
+ $this->assertSame(105, DB::table('rebates')->where('status', 0)->count());
|
|
|
+ $this->enable('rebate'); $tester->execute([]);
|
|
|
+ $this->assertSame(0, $tester->getStatusCode());
|
|
|
+ $this->assertStringContainsString('"granted":105', $tester->getDisplay());
|
|
|
+ $this->assertSame(105, DB::table('rebates')->where('status', 1)->count());
|
|
|
+ $this->rebate(['member_id' => 'missing']); $tester->execute([]);
|
|
|
+ $this->assertSame(1, $tester->getStatusCode());
|
|
|
+ }
|
|
|
+}
|