feat(прогрев): разъезд API «Реклама на кандидатов» по площадкам {platform}
GET/PATCH /api/sales/warming/{platform}, GET .../firms, POST .../toggle
и .../assign заменяют общую /api/sales/ad-audience*; настройки и статус
площадки читаются из sales_ad_audience_platforms, членство фирмы —
по ch_yandex/ch_vk/ch_mts. Метод channels() (массовый мультивыбор)
убран — заменён точечным toggle. Неизвестная площадка → 404 через
platformOr404() в контроллере (не через route whereIn — иначе
непойманный сегмент проваливается в Route::fallback).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7,62 +7,77 @@ namespace App\Http\Controllers\Api\Sales;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\SalesAdAudienceFirm;
|
||||
use App\Models\SalesAdAudiencePhone;
|
||||
use App\Models\SalesAdAudiencePlatform;
|
||||
use App\Models\SalesAdAudienceState;
|
||||
use App\Models\SalesProspect;
|
||||
use App\Models\SalesUser;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
/**
|
||||
* Экран «Реклама на кандидатов» в кабинете начальника: состояние, рубильник,
|
||||
* список фирм на прогреве и назначение менеджера.
|
||||
* Экран «Реклама на кандидатов» в кабинете начальника — по площадкам
|
||||
* (yandex | vk | mts): состояние, рубильник, список фирм на прогреве,
|
||||
* переключатель членства фирмы и назначение менеджера.
|
||||
*
|
||||
* GET /api/sales/ad-audience — три числа + настройки (11 сроков) + метка синхронизации
|
||||
* PATCH /api/sales/ad-audience — рубильник (enabled) и сроки (days + 11 настраиваемых)
|
||||
* GET /api/sales/ad-audience/firms — список фирм на прогреве и в рекламе
|
||||
* POST /api/sales/ad-audience/firms/{id}/assign — начальник назначает менеджера, рождается карточка воронки
|
||||
* GET /api/sales/warming/{platform} — числа + настройки площадки + метка синхронизации
|
||||
* PATCH /api/sales/warming/{platform} — рубильник площадки (enabled) и сроки (days + 11 настраиваемых, пока общие)
|
||||
* GET /api/sales/warming/{platform}/firms — список фирм, подписанных именно на эту площадку
|
||||
* POST /api/sales/warming/{platform}/firms/{id}/toggle — включить/выключить фирму на площадке
|
||||
* POST /api/sales/warming/{platform}/firms/{id}/assign — начальник назначает менеджера, рождается карточка воронки
|
||||
* GET /api/sales/warming/mts/file — выгрузка номеров для ручной загрузки в МТС
|
||||
*
|
||||
* Все методы — ТОЛЬКО начальник (role=head), гейт зеркалит SalesManagersController.
|
||||
* Менеджер → 403.
|
||||
* Менеджер → 403. Неизвестная площадка (не yandex/vk/mts) → 404.
|
||||
*
|
||||
* Номера сюда не добавляются — только кнопкой «Отправить в рекламу» в поиске клиентов
|
||||
* (сервис-канал SalesIntegrationController::ingestAdAudience). Сам список не растёт.
|
||||
* Членство фирмы в площадке — булевы ch_yandex/ch_vk/ch_mts на sales_ad_audience_firms;
|
||||
* настройки и статус площадки — построчно в sales_ad_audience_platforms
|
||||
* (App\Models\SalesAdAudiencePlatform). days и 11 сроков воронки в этом шаге
|
||||
* остаются ОБЩИМИ на sales_ad_audience_state.
|
||||
*
|
||||
* Модели прибиты к соединению pgsql_supplier, поэтому маршрут работает независимо
|
||||
* от admin-db-соединения остального кабинета. SalesProspect живёт на дефолтном
|
||||
* соединении — JOIN между ними невозможен, сопоставление по prospect_id в PHP.
|
||||
*
|
||||
* План: docs/superpowers/plans/2026-07-19-reklamnaya-auditoriya-v2.md (Task 7)
|
||||
* План: docs/superpowers/plans/2026-07-19-reklamnaya-auditoriya-v2.md (Task 7);
|
||||
* разъезд на площадки — план трёх площадок (Task 4+5).
|
||||
*/
|
||||
class SalesAdAudienceController extends Controller
|
||||
{
|
||||
/**
|
||||
* GET /api/sales/ad-audience — состояние рекламной аудитории (только head).
|
||||
* GET /api/sales/warming/{platform} — состояние площадки (только head).
|
||||
*/
|
||||
public function show(Request $request): JsonResponse
|
||||
public function show(Request $request, string $platform): JsonResponse
|
||||
{
|
||||
if (($resp = $this->denyIfNotHead($request)) !== null) {
|
||||
return $resp;
|
||||
}
|
||||
|
||||
return response()->json(['data' => $this->state()]);
|
||||
$p = $this->platformOr404($platform);
|
||||
|
||||
return response()->json(['data' => $this->platformState($p)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* PATCH /api/sales/ad-audience — рубильник и срок хранения (только head).
|
||||
* PATCH /api/sales/warming/{platform} — рубильник площадки и срок хранения (только head).
|
||||
*
|
||||
* Срок меняется только для НОВЫХ номеров: у уже принятых expires_at
|
||||
* зафиксирован на момент приёма (иначе непонятно, когда что выйдет).
|
||||
* days и 11 настраиваемых сроков в этом шаге ОБЩИЕ на все площадки.
|
||||
*/
|
||||
public function update(Request $request): JsonResponse
|
||||
public function update(Request $request, string $platform): JsonResponse
|
||||
{
|
||||
if (($resp = $this->denyIfNotHead($request)) !== null) {
|
||||
return $resp;
|
||||
}
|
||||
|
||||
$p = $this->platformOr404($platform);
|
||||
|
||||
$rules = [
|
||||
'enabled' => ['sometimes', 'boolean'],
|
||||
'days' => ['sometimes', 'integer', 'between:1,365'],
|
||||
@@ -74,25 +89,36 @@ class SalesAdAudienceController extends Controller
|
||||
|
||||
$data = $request->validate($rules);
|
||||
|
||||
$state = SalesAdAudienceState::current();
|
||||
$state->fill($data);
|
||||
$state->updated_at = Carbon::now();
|
||||
$state->save();
|
||||
if (array_key_exists('enabled', $data)) {
|
||||
$p->update(['enabled' => (bool) $data['enabled'], 'updated_at' => Carbon::now()]);
|
||||
unset($data['enabled']);
|
||||
}
|
||||
|
||||
return response()->json(['data' => $this->state()]);
|
||||
if ($data !== []) {
|
||||
$state = SalesAdAudienceState::current();
|
||||
$state->fill($data);
|
||||
$state->updated_at = Carbon::now();
|
||||
$state->save();
|
||||
}
|
||||
|
||||
return response()->json(['data' => $this->platformState($p)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/sales/ad-audience/firms — список фирм на прогреве и в рекламе
|
||||
* (только head). Прогретые и ждущие менеджера — наверх списка.
|
||||
* GET /api/sales/warming/{platform}/firms — список фирм, подписанных именно
|
||||
* на эту площадку (только head). Прогретые и ждущие менеджера — наверх списка.
|
||||
*/
|
||||
public function firms(Request $request): JsonResponse
|
||||
public function firms(Request $request, string $platform): JsonResponse
|
||||
{
|
||||
if (($resp = $this->denyIfNotHead($request)) !== null) {
|
||||
return $resp;
|
||||
}
|
||||
|
||||
$this->platformOr404($platform);
|
||||
$column = $this->channelColumn($platform);
|
||||
|
||||
$firms = SalesAdAudienceFirm::query()
|
||||
->where($column, true)
|
||||
->with('phones')
|
||||
->orderByRaw('ready_at IS NULL')
|
||||
->orderByDesc('warmup_started_at')
|
||||
@@ -105,35 +131,34 @@ class SalesAdAudienceController extends Controller
|
||||
->keyBy('id');
|
||||
|
||||
return response()->json([
|
||||
'firms' => $firms->map(function (SalesAdAudienceFirm $firm) use ($prospects): array {
|
||||
$prospect = $firm->prospect_id !== null ? $prospects->get($firm->prospect_id) : null;
|
||||
$phone = $firm->phones->first();
|
||||
|
||||
return [
|
||||
'id' => $firm->id,
|
||||
'firm_name' => $firm->firm_name,
|
||||
'ch_yandex' => $firm->ch_yandex,
|
||||
'ch_vk' => $firm->ch_vk,
|
||||
'ch_mts' => $firm->ch_mts,
|
||||
'city' => $firm->city,
|
||||
'phones_count' => $firm->phones->count(),
|
||||
'state' => $phone === null ? 'stopped' : $phone->state,
|
||||
'stop_reason' => $firm->stop_reason,
|
||||
'resume_at' => $phone?->resume_at,
|
||||
'days_in_ads' => (int) $firm->warmup_started_at->diffInDays(now()),
|
||||
'prospect_id' => $firm->prospect_id,
|
||||
'stage' => $prospect?->stage,
|
||||
'assigned' => $firm->assigned_at !== null,
|
||||
];
|
||||
})->all(),
|
||||
'firms' => $firms->map(fn (SalesAdAudienceFirm $firm): array => $this->firmRow($firm, $prospects))->all(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/sales/ad-audience/firms/{firm}/assign — начальник решил, что
|
||||
* POST /api/sales/warming/{platform}/firms/{firm}/toggle — включить/выключить
|
||||
* фирму на площадке (только head).
|
||||
*/
|
||||
public function toggle(Request $request, string $platform, int $firm): JsonResponse
|
||||
{
|
||||
if (($resp = $this->denyIfNotHead($request)) !== null) {
|
||||
return $resp;
|
||||
}
|
||||
|
||||
$this->platformOr404($platform);
|
||||
|
||||
$data = $request->validate(['on' => ['required', 'boolean']]);
|
||||
|
||||
SalesAdAudienceFirm::whereKey($firm)->update([$this->channelColumn($platform) => (bool) $data['on']]);
|
||||
|
||||
return response()->json(['ok' => true]);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/sales/warming/{platform}/firms/{firm}/assign — начальник решил, что
|
||||
* человек созрел: заводим карточку в воронке из снимка фирмы (только head).
|
||||
*/
|
||||
public function assign(Request $request, int $firm): JsonResponse
|
||||
public function assign(Request $request, string $platform, int $firm): JsonResponse
|
||||
{
|
||||
if (($resp = $this->denyIfNotHead($request)) !== null) {
|
||||
return $resp;
|
||||
@@ -174,38 +199,7 @@ class SalesAdAudienceController extends Controller
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/sales/ad-audience/channels — массовая простановка площадок (только head).
|
||||
*
|
||||
* Три независимые галочки. Состав в Яндексе и ВК подтянется сам ближайшей
|
||||
* ночной заливкой — джобы фильтруют номера по этим полям. Для МТС
|
||||
* программного доступа нет: там начальник скачивает файл и грузит руками.
|
||||
*/
|
||||
public function channels(Request $request): JsonResponse
|
||||
{
|
||||
if (($resp = $this->denyIfNotHead($request)) !== null) {
|
||||
return $resp;
|
||||
}
|
||||
|
||||
/** @var array{firm_ids: list<int>, ch_yandex: bool, ch_vk: bool, ch_mts: bool} $data */
|
||||
$data = $request->validate([
|
||||
'firm_ids' => ['required', 'array', 'min:1'],
|
||||
'firm_ids.*' => ['required', 'integer'],
|
||||
'ch_yandex' => ['required', 'boolean'],
|
||||
'ch_vk' => ['required', 'boolean'],
|
||||
'ch_mts' => ['required', 'boolean'],
|
||||
]);
|
||||
|
||||
$updated = SalesAdAudienceFirm::whereIn('id', $data['firm_ids'])->update([
|
||||
'ch_yandex' => $data['ch_yandex'],
|
||||
'ch_vk' => $data['ch_vk'],
|
||||
'ch_mts' => $data['ch_mts'],
|
||||
]);
|
||||
|
||||
return response()->json(['updated' => $updated]);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/sales/ad-audience/mts-file — список номеров для загрузки в МТС (только head).
|
||||
* GET /api/sales/warming/mts/file — список номеров для загрузки в МТС (только head).
|
||||
*
|
||||
* У МТС Маркетолога нет программного доступа к рекламе в Telegram: их REST API
|
||||
* умеет только SMS. Поэтому портал отдаёт готовый файл, а начальник загружает
|
||||
@@ -245,39 +239,89 @@ class SalesAdAudienceController extends Controller
|
||||
// ── private ──────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Снимок состояния для экрана: настройки + три числа по живым номерам.
|
||||
* Строка площадки по коду или 404, если код не yandex/vk/mts.
|
||||
*/
|
||||
private function platformOr404(string $platform): SalesAdAudiencePlatform
|
||||
{
|
||||
abort_unless(in_array($platform, SalesAdAudiencePlatform::CODES, true), 404);
|
||||
|
||||
return SalesAdAudiencePlatform::forCode($platform);
|
||||
}
|
||||
|
||||
/**
|
||||
* Булева колонка членства фирмы в площадке на sales_ad_audience_firms.
|
||||
*/
|
||||
private function channelColumn(string $platform): string
|
||||
{
|
||||
return ['yandex' => 'ch_yandex', 'vk' => 'ch_vk', 'mts' => 'ch_mts'][$platform];
|
||||
}
|
||||
|
||||
/**
|
||||
* Снимок состояния площадки для экрана: настройки площадки + сроки воронки
|
||||
* (пока общие) + три числа по живым номерам подписанных на неё фирм.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function state(): array
|
||||
private function platformState(SalesAdAudiencePlatform $p): array
|
||||
{
|
||||
$state = SalesAdAudienceState::current();
|
||||
$now = Carbon::now();
|
||||
$column = $this->channelColumn($p->platform);
|
||||
|
||||
// «Живой» номер: ещё не убран из сегмента и срок не вышел.
|
||||
$live = static fn (): Builder => SalesAdAudiencePhone::query()
|
||||
// «Живой» номер: ещё не убран из сегмента, срок не вышел, фирма подписана на площадку.
|
||||
$live = fn (): Builder => SalesAdAudiencePhone::query()
|
||||
->whereNull('removed_at')
|
||||
->where('expires_at', '>', $now);
|
||||
->where('expires_at', '>', $now)
|
||||
->whereIn('firm_id', SalesAdAudienceFirm::query()->where($column, true)->select('id'));
|
||||
|
||||
return [
|
||||
'enabled' => $state->enabled,
|
||||
'platform' => $p->platform,
|
||||
'enabled' => $p->enabled,
|
||||
'min_phones' => $p->min_phones,
|
||||
'external_id' => $p->external_id,
|
||||
'last_synced_at' => $p->last_synced_at,
|
||||
'last_error' => $p->last_error,
|
||||
'status' => $p->status,
|
||||
'days' => $state->days,
|
||||
'durations' => $state->durations(),
|
||||
'segment_id' => $state->yandex_segment_id,
|
||||
'last_synced_at' => $state->last_synced_at,
|
||||
'last_error' => $state->last_error,
|
||||
'vk_status' => $state->vk_status,
|
||||
'vk_last_synced_at' => $state->vk_last_synced_at,
|
||||
'vk_last_error' => $state->vk_last_error,
|
||||
// В рекламе сейчас.
|
||||
'in_ads' => $live()->count(),
|
||||
// Выходят на этой неделе — срок кончается в ближайшие 7 дней.
|
||||
'expiring_week' => $live()->where('expires_at', '<', $now->copy()->addWeek())->count(),
|
||||
// Ждут отправки — приняты, но ночной джоб ещё не отдал их Яндексу.
|
||||
// Ждут отправки — приняты, но ночной джоб ещё не отдал их площадке.
|
||||
'waiting_sync' => $live()->whereNull('synced_at')->count(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Одна строка списка фирм на прогреве — как отдаём в JSON.
|
||||
*
|
||||
* @param Collection<int, SalesProspect> $prospects
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function firmRow(SalesAdAudienceFirm $firm, Collection $prospects): array
|
||||
{
|
||||
$prospect = $firm->prospect_id !== null ? $prospects->get($firm->prospect_id) : null;
|
||||
$phone = $firm->phones->first();
|
||||
|
||||
return [
|
||||
'id' => $firm->id,
|
||||
'firm_name' => $firm->firm_name,
|
||||
'ch_yandex' => $firm->ch_yandex,
|
||||
'ch_vk' => $firm->ch_vk,
|
||||
'ch_mts' => $firm->ch_mts,
|
||||
'city' => $firm->city,
|
||||
'phones_count' => $firm->phones->count(),
|
||||
'state' => $phone === null ? 'stopped' : $phone->state,
|
||||
'stop_reason' => $firm->stop_reason,
|
||||
'resume_at' => $phone?->resume_at,
|
||||
'days_in_ads' => (int) $firm->warmup_started_at->diffInDays(now()),
|
||||
'prospect_id' => $firm->prospect_id,
|
||||
'stage' => $prospect?->stage,
|
||||
'assigned' => $firm->assigned_at !== null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Гейт «только начальник». Возвращает 403-ответ, либо null если доступ есть.
|
||||
*/
|
||||
|
||||
+12
-9
@@ -303,16 +303,19 @@ Route::middleware(['admin-db', 'auth:sales', 'sales-portal'])->prefix('api/sales
|
||||
// Task 7.1a: заведение менеджеров начальником + список аккаунтов (только head).
|
||||
Route::get('/managers', [SalesManagersController::class, 'index']);
|
||||
Route::post('/managers', [SalesManagersController::class, 'store']);
|
||||
// Task 8: «Реклама на кандидатов» — состояние и рубильник (только head).
|
||||
Route::get('/ad-audience', [SalesAdAudienceController::class, 'show']);
|
||||
Route::patch('/ad-audience', [SalesAdAudienceController::class, 'update']);
|
||||
// Task 7 (план v2): список фирм на прогреве + назначение менеджера (только head).
|
||||
Route::get('/ad-audience/firms', [SalesAdAudienceController::class, 'firms']);
|
||||
Route::post('/ad-audience/firms/{firm}/assign', [SalesAdAudienceController::class, 'assign'])->whereNumber('firm');
|
||||
// Task 5 (план выбора площадки прогрева): массовая смена площадки (только head).
|
||||
Route::post('/ad-audience/channels', [SalesAdAudienceController::class, 'channels']);
|
||||
// Task 4+5 (план трёх площадок): «Реклама на кандидатов» по площадкам yandex|vk|mts
|
||||
// (только head). Неизвестную площадку контроллер отвергает сам (platformOr404 → 404
|
||||
// JSON) — без whereIn на маршруте, иначе непойманный сегмент проваливается
|
||||
// в Route::fallback (SPA welcome-view) вместо чистого 404-ответа API.
|
||||
Route::get('/warming/{platform}', [SalesAdAudienceController::class, 'show']);
|
||||
Route::patch('/warming/{platform}', [SalesAdAudienceController::class, 'update']);
|
||||
Route::get('/warming/{platform}/firms', [SalesAdAudienceController::class, 'firms']);
|
||||
Route::post('/warming/{platform}/firms/{firm}/toggle', [SalesAdAudienceController::class, 'toggle'])->whereNumber('firm');
|
||||
Route::post('/warming/{platform}/firms/{firm}/assign', [SalesAdAudienceController::class, 'assign'])->whereNumber('firm');
|
||||
// Task 3 (план трёх площадок): выгрузка списка номеров файлом для МТС (только head).
|
||||
Route::get('/ad-audience/mts-file', [SalesAdAudienceController::class, 'mtsFile']);
|
||||
// Регистрируем ДО /warming/{platform}/firms — иначе .../firms не перепутается,
|
||||
// но mts/file имеет другое число сегментов, поэтому порядок не критичен.
|
||||
Route::get('/warming/mts/file', [SalesAdAudienceController::class, 'mtsFile']);
|
||||
// Task 9 (план «Прогрев СМС»): список рассылок, предпросмотр цены, запуск (только head).
|
||||
Route::get('/sms/campaigns', [SalesSmsController::class, 'index']);
|
||||
Route::post('/sms/campaigns', [SalesSmsController::class, 'store']);
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Models\SalesAdAudienceFirm;
|
||||
use App\Models\SalesAdAudiencePlatform;
|
||||
use App\Models\SalesUser;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
use Tests\Concerns\SharesSupplierPdo;
|
||||
|
||||
/**
|
||||
* API «Реклама на кандидатов» по площадкам (Task 4+5 плана трёх площадок).
|
||||
*
|
||||
* Одна общая ручка /api/sales/ad-audience разъезжается на три:
|
||||
* /api/sales/warming/{yandex|vk|mts}. Настройки и статус — построчно
|
||||
* в sales_ad_audience_platforms; days/сроки воронки пока общие
|
||||
* (sales_ad_audience_state). Членство фирмы в площадке — булевы
|
||||
* ch_yandex/ch_vk/ch_mts на sales_ad_audience_firms.
|
||||
*
|
||||
* Аутентификация — как в AdAudienceScreenTest: SalesUser::create() + actingAs('sales')
|
||||
* (у SalesUser нет HasFactory/factory()).
|
||||
*/
|
||||
uses(DatabaseTransactions::class, SharesSupplierPdo::class);
|
||||
|
||||
function actingHead(): SalesUser
|
||||
{
|
||||
$head = SalesUser::create([
|
||||
'name' => 'Начальник '.uniqid(),
|
||||
'email' => 'warminghead'.uniqid().'@sales.local',
|
||||
'password' => bcrypt('secret'),
|
||||
'role' => 'head',
|
||||
'is_active' => true,
|
||||
]);
|
||||
test()->actingAs($head, 'sales');
|
||||
|
||||
return $head;
|
||||
}
|
||||
|
||||
it('отдаёт настройки именно запрошенной площадки', function () {
|
||||
actingHead();
|
||||
SalesAdAudiencePlatform::forCode('vk')->update(['status' => 'waiting_volume']);
|
||||
|
||||
test()->getJson('/api/sales/warming/vk')
|
||||
->assertOk()
|
||||
->assertJsonPath('data.platform', 'vk')
|
||||
->assertJsonPath('data.min_phones', 2000)
|
||||
->assertJsonPath('data.status', 'waiting_volume');
|
||||
});
|
||||
|
||||
it('неизвестную площадку отвергает', function () {
|
||||
actingHead();
|
||||
|
||||
test()->getJson('/api/sales/warming/facebook')->assertNotFound();
|
||||
});
|
||||
|
||||
it('рубильник пишется в строку площадки', function () {
|
||||
actingHead();
|
||||
|
||||
test()->patchJson('/api/sales/warming/vk', ['enabled' => true])->assertOk();
|
||||
|
||||
expect(SalesAdAudiencePlatform::forCode('vk')->enabled)->toBeTrue();
|
||||
});
|
||||
|
||||
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/warming/yandex')->assertForbidden();
|
||||
});
|
||||
|
||||
it('в список площадки попадают только её подписанные фирмы', function () {
|
||||
actingHead();
|
||||
|
||||
SalesAdAudienceFirm::create(['firm_name' => 'Только Яндекс', 'ch_yandex' => true]);
|
||||
SalesAdAudienceFirm::create(['firm_name' => 'Только ВК', 'ch_vk' => true]);
|
||||
|
||||
test()->getJson('/api/sales/warming/vk/firms')
|
||||
->assertOk()
|
||||
->assertJsonCount(1, 'firms')
|
||||
->assertJsonPath('firms.0.firm_name', 'Только ВК');
|
||||
});
|
||||
|
||||
it('переключатель добавляет и убирает фирму с площадки', function () {
|
||||
actingHead();
|
||||
|
||||
$firm = SalesAdAudienceFirm::create(['firm_name' => 'Плавающая', 'ch_vk' => false]);
|
||||
|
||||
test()->postJson("/api/sales/warming/vk/firms/{$firm->id}/toggle", ['on' => true])->assertOk();
|
||||
expect($firm->fresh()->ch_vk)->toBeTrue();
|
||||
|
||||
test()->postJson("/api/sales/warming/vk/firms/{$firm->id}/toggle", ['on' => false])->assertOk();
|
||||
expect($firm->fresh()->ch_vk)->toBeFalse();
|
||||
});
|
||||
Reference in New Issue
Block a user