create(['balance_rub' => '100000.00']); DB::statement('SET app.current_tenant_id = '.$tenant->id); SystemSetting::updateOrCreate(['key' => 'autopodbor_price_search_rub'], ['value' => '500', 'type' => 'decimal']); $run = app(AutopodborRunService::class)->startSearch($tenant->id, 16, ['okna.ru'], [], true); expect($run->kind)->toBe('search')->and($run->status)->toBe('queued'); Queue::assertPushed(RunAutopodborSearchJob::class); }); it('startStudy на уже изученном конкуренте создаёт НОВЫЙ прогон (повторный сбор источников)', function () { Queue::fake(); $tenant = Tenant::factory()->create(['balance_rub' => '100000.00']); DB::statement('SET app.current_tenant_id = '.$tenant->id); SystemSetting::updateOrCreate(['key' => 'autopodbor_price_study_rub'], ['value' => '300', 'type' => 'decimal']); $oldRun = AutopodborRun::create(['tenant_id' => $tenant->id, 'kind' => 'study', 'status' => 'done', 'region_code' => 16, 'params' => []]); $comp = AutopodborCompetitor::create([ 'tenant_id' => $tenant->id, 'name' => 'Окна', 'dedup_key' => 'okna', 'box' => 'field', 'studied_at' => now(), 'study_run_id' => $oldRun->id, ]); $run = app(AutopodborRunService::class)->startStudy($tenant->id, $comp->id); // Раньше был жёсткий стоп «уже изучали → вернуть старый прогон». Теперь повтор разрешён. expect($run->id)->not->toBe($oldRun->id) ->and($run->kind)->toBe('study') ->and($run->status)->toBe('queued') ->and($run->competitor_id)->toBe($comp->id); Queue::assertPushed(RunAutopodborStudyJob::class); }); it('не стартует второй in-flight search того же tenant', function () { Queue::fake(); $tenant = Tenant::factory()->create(['balance_rub' => '100000.00']); DB::statement('SET app.current_tenant_id = '.$tenant->id); SystemSetting::updateOrCreate(['key' => 'autopodbor_price_search_rub'], ['value' => '500', 'type' => 'decimal']); $svc = app(AutopodborRunService::class); $svc->startSearch($tenant->id, 16, ['okna.ru'], [], true); expect(fn () => $svc->startSearch($tenant->id, 16, ['okna.ru'], [], true)) ->toThrow(RunInFlightException::class); }); it('гейтит по балансу: нехватка на цену search → InsufficientBalanceException', function () { Queue::fake(); $tenant = Tenant::factory()->create(['balance_rub' => '100.00']); DB::statement('SET app.current_tenant_id = '.$tenant->id); SystemSetting::updateOrCreate(['key' => 'autopodbor_price_search_rub'], ['value' => '500', 'type' => 'decimal']); expect(fn () => app(AutopodborRunService::class)->startSearch($tenant->id, 16, ['okna.ru'], [], true)) ->toThrow(InsufficientBalanceException::class); Queue::assertNotPushed(RunAutopodborSearchJob::class); }); it('startResolve бесплатный: стартует даже при нулевом балансе, ставит resolve-джобу', function () { Queue::fake(); $tenant = Tenant::factory()->create(['balance_rub' => '0.00']); DB::statement('SET app.current_tenant_id = '.$tenant->id); $run = app(AutopodborRunService::class)->startResolve($tenant->id, 'Окна Комфорт', 16); expect($run->kind)->toBe('resolve')->and($run->status)->toBe('queued'); Queue::assertPushed(RunAutopodborResolveJob::class); });