fix(реклама): вернуть GET /api/sales/ad-audience/firms для модуля «Прогрев СМС»

Разъезд «Рекламы на кандидатов» на площадки (yandex|vk|mts) снёс общий
маршрут /ad-audience/firms — фирмо-пикер SalesSmsView.vue его звал
и получал 404. Восстановлен как платформо-независимый allFirms():
та же выборка firms(), но без where(channel, true).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Дмитрий
2026-07-21 11:39:15 +03:00
parent 25144a3cce
commit b7dc3f9b08
3 changed files with 60 additions and 0 deletions
@@ -29,6 +29,8 @@ use Symfony\Component\HttpFoundation\StreamedResponse;
* POST /api/sales/warming/{platform}/firms/{id}/toggle включить/выключить фирму на площадке
* POST /api/sales/warming/{platform}/firms/{id}/assign начальник назначает менеджера, рождается карточка воронки
* GET /api/sales/warming/mts/file выгрузка номеров для ручной загрузки в МТС
* GET /api/sales/ad-audience/firms ВСЕ фирмы прогрева, без фильтра площадки
* (для модуля «Прогрев СМС», см. allFirms())
*
* Все методы ТОЛЬКО начальник (role=head), гейт зеркалит SalesManagersController.
* Менеджер 403. Неизвестная площадка (не yandex/vk/mts) 404.
@@ -236,6 +238,35 @@ class SalesAdAudienceController extends Controller
);
}
/**
* GET /api/sales/ad-audience/firms ВСЕ фирмы прогрева без фильтра площадки.
*
* Платформенный список firms({platform}); этот метод отдаёт полный состав
* (используется модулем «Прогрев СМС», у которого своя логика отбора).
*/
public function allFirms(Request $request): JsonResponse
{
if (($resp = $this->denyIfNotHead($request)) !== null) {
return $resp;
}
$firms = SalesAdAudienceFirm::query()
->with('phones')
->orderByRaw('ready_at IS NULL')
->orderByDesc('warmup_started_at')
->limit(500)
->get();
$prospects = SalesProspect::query()
->whereIn('id', $firms->pluck('prospect_id')->filter()->all())
->get()
->keyBy('id');
return response()->json([
'firms' => $firms->map(fn (SalesAdAudienceFirm $firm): array => $this->firmRow($firm, $prospects))->all(),
]);
}
// ── private ──────────────────────────────────────────────────────────────
/**
+4
View File
@@ -316,6 +316,10 @@ Route::middleware(['admin-db', 'auth:sales', 'sales-portal'])->prefix('api/sales
// Регистрируем ДО /warming/{platform}/firms — иначе .../firms не перепутается,
// но mts/file имеет другое число сегментов, поэтому порядок не критичен.
Route::get('/warming/mts/file', [SalesAdAudienceController::class, 'mtsFile']);
// Регрессия: старая /ad-audience/firms была снесена разъездом на площадки, но модуль
// «Прогрев СМС» (SalesSmsView) зовёт именно её для фирмо-пикера — восстановлена как
// платформо-независимый список ВСЕХ фирм прогрева (allFirms(), НЕ путать с firms()).
Route::get('/ad-audience/firms', [SalesAdAudienceController::class, 'allFirms']);
// Task 9 (план «Прогрев СМС»): список рассылок, предпросмотр цены, запуск (только head).
Route::get('/sms/campaigns', [SalesSmsController::class, 'index']);
Route::post('/sms/campaigns', [SalesSmsController::class, 'store']);
@@ -97,3 +97,28 @@ it('переключатель добавляет и убирает фирму
test()->postJson("/api/sales/warming/vk/firms/{$firm->id}/toggle", ['on' => false])->assertOk();
expect($firm->fresh()->ch_vk)->toBeFalse();
});
it('отдаёт ВСЕ фирмы прогрева, без фильтра площадки (для модуля СМС)', function () {
actingHead();
SalesAdAudienceFirm::create(['firm_name' => 'Только Яндекс', 'ch_yandex' => true]);
SalesAdAudienceFirm::create(['firm_name' => 'Только ВК', 'ch_vk' => true]);
SalesAdAudienceFirm::create(['firm_name' => 'Нигде', 'ch_yandex' => false, 'ch_vk' => false, 'ch_mts' => false]);
test()->getJson('/api/sales/ad-audience/firms')
->assertOk()
->assertJsonCount(3, 'firms');
});
it('список всех фирм закрыт для менеджера', function () {
$m = SalesUser::create([
'name' => 'Менеджер '.uniqid(),
'email' => 'warmingmgr'.uniqid().'@sales.local',
'password' => bcrypt('secret'),
'role' => 'manager',
'is_active' => true,
]);
test()->actingAs($m, 'sales');
test()->getJson('/api/sales/ad-audience/firms')->assertForbidden();
});