$tenantId, 'name' => $name, 'origin' => 'manual', 'dedup_key' => Str::slug($name).'-'.Str::random(6), 'box' => 'field', ]); } it('ставит пакет: N queued-прогонов с общим batch_id, распорядитель стартует ровно первого', 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']); $c1 = batchCompetitor($tenant->id, 'Окна А'); $c2 = batchCompetitor($tenant->id, 'Окна Б'); $c3 = batchCompetitor($tenant->id, 'Окна В'); $result = app(AutopodborRunService::class)->startStudyBatch($tenant->id, [$c1->id, $c2->id, $c3->id]); expect($result->queued)->toBe(3) ->and($result->batch_id)->not->toBeNull(); $runs = AutopodborRun::where('batch_id', $result->batch_id)->get(); expect($runs)->toHaveCount(3) ->and($runs->pluck('status')->unique()->values()->all())->toBe(['queued']) ->and($runs->pluck('kind')->unique()->values()->all())->toBe(['study']); // Джобы не диспатчатся напрямую из сервиса — распорядитель стартует РОВНО первого. Queue::assertPushed(RunAutopodborStudyJob::class, 1); }); it('нормализация: дубли, чужие конкуренты и уже-в-очереди отсеиваются', function () { Queue::fake(); $tenant = Tenant::factory()->create(['balance_rub' => '100000.00']); $other = 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']); $c1 = batchCompetitor($tenant->id, 'Окна А'); $c2 = batchCompetitor($tenant->id, 'Окна Б'); $foreign = batchCompetitor($other->id, 'Чужой'); $busy = batchCompetitor($tenant->id, 'Занятый'); // У «занятого» уже есть queued study-прогон → должен быть отсеян. AutopodborRun::create([ 'tenant_id' => $tenant->id, 'kind' => 'study', 'status' => 'queued', 'competitor_id' => $busy->id, 'region_code' => 1, 'params' => [], ]); $result = app(AutopodborRunService::class)->startStudyBatch( $tenant->id, [$c1->id, $c1->id, $c2->id, $foreign->id, $busy->id], ); expect($result->queued)->toBe(2); $runs = AutopodborRun::where('batch_id', $result->batch_id)->get(); expect($runs)->toHaveCount(2) ->and($runs->pluck('competitor_id')->sort()->values()->all()) ->toBe(collect([$c1->id, $c2->id])->sort()->values()->all()); }); it('нет баланса на весь пакет → BatchBudgetException, ни одного прогона не создано', function () { Queue::fake(); $tenant = Tenant::factory()->create(['balance_rub' => '100.00', 'delivered_in_month' => 0]); DB::statement('SET app.current_tenant_id = '.$tenant->id); SystemSetting::updateOrCreate(['key' => 'autopodbor_price_study_rub'], ['value' => '300', 'type' => 'decimal']); // Активный проект-обязательство + сетка тарифов — гейт учитывает резерв проектов. Project::factory()->create(['tenant_id' => $tenant->id, 'is_active' => true, 'daily_limit_target' => 10]); PricingTier::factory()->create([ 'tier_no' => 1, 'leads_in_tier' => null, 'price_per_lead_kopecks' => 3000, 'is_active' => true, 'effective_from' => now()->toDateString(), ]); $c1 = batchCompetitor($tenant->id, 'Окна А'); $c2 = batchCompetitor($tenant->id, 'Окна Б'); expect(fn () => app(AutopodborRunService::class)->startStudyBatch($tenant->id, [$c1->id, $c2->id])) ->toThrow(BatchBudgetException::class); expect(AutopodborRun::where('tenant_id', $tenant->id)->where('kind', 'study')->count())->toBe(0); Queue::assertNotPushed(RunAutopodborStudyJob::class); });