Files
portal/app/config/services.php
T
Дмитрий df70a78e17 merge: ветка Билайна и Мотива сведена с главной — 347 коммитов, один затык
Затык был ровно один — routes/web.php: главная добавила ручку суточного итога
сторожей с машины робота, ветка добавила приёмник статусов Билайна. Ручки
независимые, оставлены обе.

Попутно поправлено примечание в config/services.php: оно говорило «заявка подана,
срок до десяти рабочих дней», а имя `Liderra.ru` было ОДОБРЕНО обеими сетями
10.08.2026 в тот же день — проверено глазами в кабинете. Примечание, которое
врёт, хуже отсутствующего: следующая смена прочла бы его и не стала поднимать
выключатель.

Замер на сведённом коде:
- модуль СМС 575/575, 7151 проверка (опора ветки была 548 — главная принесла 27);
- фронтенд 2205 из 2210. Пять падений — ЧУЖИЕ и приехали из главной:
  KoshelyokZakladkiPoKanalam.spec.ts, экран AdWalletMoneyColumn.vue спотыкается
  об отсутствующее поле frozen_breakdown. Доказано отпечатками: и тест, и экран
  байт в байт совпадают с gitea/main, моя правка их не касалась;
- Pint и PHPStan — чисто.
2026-08-10 21:39:33 +03:00

592 lines
47 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
return [
/*
|--------------------------------------------------------------------------
| Third Party Services
|--------------------------------------------------------------------------
|
| This file is for storing the credentials for third party services such
| as Mailgun, Postmark, AWS and more. This file provides the de facto
| location for this type of information, allowing packages to have
| a conventional file to locate the various service credentials.
|
*/
// Канал робота-грузчика креативов в веб-кабинет Яндекса. Свой токен, отдельный
// от клиентских и от токена «Поиск → Портал». Пусто → канал закрыт.
'creative_robot' => [
'token' => env('CREATIVE_ROBOT_TOKEN', ''),
],
// Служебный канал «Телеграм-робот кабинета МТС → Портал». Пусто → канал закрыт (401).
'tg_robot' => [
'token' => env('TG_ROBOT_TOKEN', ''),
],
// Рекламная аудитория кандидатов (Яндекс.Аудитории). Токен — OAuth Яндекс ID,
// выдаётся разово на аккаунт, где живёт сегмент. Пусто → синхронизация не идёт,
// джоб пишет причину в last_error и молчит.
'yandex_audience' => [
'token' => env('YANDEX_AUDIENCE_TOKEN'),
'segment_name' => env('YANDEX_AUDIENCE_SEGMENT_NAME', 'Лидерра — кандидаты'),
],
// Яндекс.Директ (B1) — рекламный кошелёк. ПЕСОЧНИЦА по умолчанию (В8): боевой —
// сменой env на api.direct.yandex.com после одобрения заявки. Денежный рубильник
// enabled: пока выкл — реальные вызовы Директа не идут (как enabled у прогрева).
'yandex_direct' => [
'base_url' => env('YANDEX_DIRECT_BASE_URL', 'https://api-sandbox.direct.yandex.com'),
'token' => env('YANDEX_DIRECT_TOKEN'),
'enabled' => env('YANDEX_DIRECT_ENABLED', false),
// Регионы показа по умолчанию (вся Россия = 225).
'region_ids' => [225],
// Предохранитель: SpendLimit в Яндексе = яндекс-бюджет × множитель (бэкстоп
// от перерасхода, НЕ клиентская цена).
'spend_limit_guard_multiplier' => (float) env('YANDEX_DIRECT_SPEND_GUARD', 1.2),
// Минимальный бюджет кампании за период, ниже которого Директ её просто не берёт.
// Порог НЕ постоянный — он растёт с длиной периода: 300 ₽ за каждый календарный
// день, считая оба края. Два живых отказа 30.07.2026 дали обе точки:
// период 30.0731.07 (2 дня) → «cannot be less than 600 rub.» = 2 × 300
// период 30.0706.08 (8 дней) → «cannot be less than 2400 rub.» = 8 × 300
// Первый отказ мы приняли за постоянный порог — и проверка пропускала любую
// кампанию длиннее двух дней прямо в тот же английский отказ Яндекса.
// В настройке, а не в коде: Яндекс может поменять ставку.
'min_spend_limit_rub_per_day' => (string) env('YANDEX_DIRECT_MIN_SPEND_RUB_PER_DAY', '300'),
],
// Рекламная аудитория кандидатов в ВК (Task 8, 21.07.2026). Программный доступ
// запрошен 19.07, ждём проверки реквизитов — до получения токена джоб пишет
// vk_status=no_access и не делает ни одного обращения к ВК.
'vk_ads' => [
'token' => env('VK_ADS_TOKEN', ''),
],
'postmark' => [
'key' => env('POSTMARK_API_KEY'),
],
'resend' => [
'key' => env('RESEND_API_KEY'),
],
'ses' => [
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
],
'slack' => [
'notifications' => [
'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'),
'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'),
],
],
// Капча самозаписи (G1/SP1). driver=null → NullCaptchaVerifier (dev/test).
// Реальный Yandex SmartCaptcha подключается позже (SP3/ops).
'captcha' => [
'driver' => env('CAPTCHA_DRIVER', 'null'),
'fake_passes' => filter_var(env('CAPTCHA_FAKE_PASSES', true), FILTER_VALIDATE_BOOL),
'yandex_server_key' => env('YANDEX_SMARTCAPTCHA_SERVER_KEY'),
'yandex_validate_url' => env('YANDEX_SMARTCAPTCHA_VALIDATE_URL', 'https://smartcaptcha.cloud.yandex.ru/validate'),
],
'supplier' => [
'login' => env('SUPPLIER_LOGIN'),
'password' => env('SUPPLIER_PASSWORD'),
'portal_url' => env('SUPPLIER_PORTAL_URL', 'https://crm.bp-gr.ru'),
'alert_email' => env('SUPPLIER_ALERT_EMAIL', 'ops@liderra.ru'),
// Плитка балансов (28.06): у кабинета поставщика НЕ денежный баланс, а остаток
// НОМЕРОВ («Баланс ГЦК» в выпадашке). Деньги = номера × number_price_rub (20 ₽/шт,
// подтверждено владельцем 28.06). avg_lead_price_rub=0 → расход/день неизвестен.
'number_price_rub' => (float) env('SUPPLIER_NUMBER_PRICE_RUB', 20),
'avg_lead_price_rub' => (float) env('SUPPLIER_AVG_LEAD_PRICE_RUB', 0),
'red_floor_rub' => (int) env('SUPPLIER_RED_FLOOR_RUB', 5000),
'amber_floor_rub' => (int) env('SUPPLIER_AMBER_FLOOR_RUB', 15000),
'topup_url' => env('SUPPLIER_TOPUP_URL', env('SUPPLIER_PORTAL_URL', 'https://crm.bp-gr.ru')),
// Рубильник ежедневной чистки доп-каналов (B6/B8/…). По умолчанию ВКЛ.
// Аварийное отключение: SUPPLIER_PRUNE_EXTRA_CHANNELS_ENABLED=false + рестарт воркеров.
'prune_extra_channels' => filter_var(env('SUPPLIER_PRUNE_EXTRA_CHANNELS_ENABLED', true), FILTER_VALIDATE_BOOL),
],
// DaData phone cleaner — резолв региона лида по телефону (lead region resolution).
// Ключи → YC Lockbox на проде; на dev/staging — .env. enabled=false до раскатки.
'dadata' => [
'api_key' => env('DADATA_API_KEY'),
'secret' => env('DADATA_SECRET'),
'timeout_ms' => (int) env('DADATA_TIMEOUT_MS', 2000),
'retries' => (int) env('DADATA_RETRIES', 1),
'daily_cap_rub' => (int) env('DADATA_DAILY_CAP_RUB', 10000),
'call_cost_kopecks' => (int) env('DADATA_CALL_COST_KOPECKS', 60), // ≈0.60 ₽/вызов, откалибровать по тарифу
'enabled' => filter_var(env('LEAD_REGION_RESOLVER_ENABLED', false), FILTER_VALIDATE_BOOL),
'cache_ttl_days' => (int) env('PHONE_REGION_CACHE_TTL_DAYS', 30),
// G1/SP2: подтяжка организации по ИНН (suggestions findById/party). Тот же api_key
// (Token), secret не нужен. Default false → NullPartyLookup (dev/тесты не ходят в сеть).
'party_enabled' => filter_var(env('DADATA_PARTY_ENABLED', false), FILTER_VALIDATE_BOOL),
// Плитка балансов (28.06): чтение баланса профиля + пороги светофора + ссылка пополнения.
'balance_url' => env('DADATA_BALANCE_URL', 'https://dadata.ru/api/v2/profile/balance'),
'red_floor_rub' => (int) env('DADATA_RED_FLOOR_RUB', 500),
'amber_floor_rub' => (int) env('DADATA_AMBER_FLOOR_RUB', 2000),
'topup_url' => env('DADATA_TOPUP_URL', 'https://dadata.ru/profile/#billing'),
],
// СМС-центр (smsc.ru) — HLR-проверка номеров в Поиске клиентов (жив/несуществует).
// Баланс на плитке присмотра. Логин/пароль — те же, что у службы поиска.
'smsc' => [
'login' => env('SMSC_LOGIN'),
'password' => env('SMSC_PASSWORD'),
'balance_url' => env('SMSC_BALANCE_URL', 'https://smsc.ru/sys/balance.php'),
'red_floor_rub' => (int) env('SMSC_RED_FLOOR_RUB', 300),
'amber_floor_rub' => (int) env('SMSC_AMBER_FLOOR_RUB', 1000),
'topup_url' => env('SMSC_TOPUP_URL', 'https://smsc.ru/oplata/'),
// Своё согласованное имя отправителя универсального канала. Подставляется,
// когда своего имени у клиента для сети получателя нет: имя согласовывает
// ОПЕРАТОР, а универсал ходит во все сети сразу (см. ClientSmsSenderResolver).
// Пусто — уйдёт без имени, как было до 05.08.2026.
'naming' => env('SMSC_NAMING', ''),
],
// AITUNNEL (OpenAI-совместимый) для автоподбора: эмбеддинги (похожесть конкурента на
// профиль клиента) + LLM-классификатор «поставщик услуги или площадка-агрегатор?».
// Ключ — только в .env/окружении, НИКОГДА в гит. Пусто → движок отдаёт сырой список
// (без % и без авто-отсева площадок).
'aitunnel' => [
'key' => env('AITUNNEL_API_KEY'),
'base_url' => env('AITUNNEL_BASE_URL', 'https://api.aitunnel.ru/v1'),
'embed_model' => env('AITUNNEL_EMBED_MODEL', 'text-embedding-3-small'),
'chat_model' => env('AITUNNEL_CHAT_MODEL', 'gpt-4o-mini'),
// Модель отсева агрегаторов (вариант А — точнее, чтобы не терять живых конкурентов вроде
// «ГорЗайм»). Пусто → фолбэк на chat_model. Пример: AITUNNEL_AGGREGATOR_MODEL=gpt-4o.
'aggregator_model' => env('AITUNNEL_AGGREGATOR_MODEL'),
// ИИ-вызовы (анализ рубрик, эмбеддинги) — с ЗАПАСОМ: латентность модели плавает
// (один и тот же запрос то 8с, то 30-40с), а сбор асинхронный и клиент ждёт — режем
// медленный-но-настоящий ответ таймаутом ХУЖЕ, чем ждём. Потому большие таймауты.
'timeout_sec' => (int) env('AITUNNEL_TIMEOUT_SEC', 120),
// Батч отсева агрегаторов: список фирм режем на порции по aggregator_batch_size (иначе
// один запрос на ~100 фирм долгий и рискует не успеть) + отдельный долгий таймаут на порцию.
'aggregator_batch_size' => (int) env('AITUNNEL_AGGREGATOR_BATCH_SIZE', 40),
'aggregator_timeout_sec' => (int) env('AITUNNEL_AGGREGATOR_TIMEOUT_SEC', 240),
// Канал В — генератор имён (ZAFIKSIROVANO §0-БИС): одна рассуждающая модель sonar-reasoning-pro,
// в реальном прогоне ~30-70 с/вызов и плавает вверх; 2 прохода. Большой таймаут с запасом —
// лучше дождаться, чем прислать пусто (сбор асинхронный, клиент всё равно ждёт результат).
'research_model' => env('AITUNNEL_RESEARCH_MODEL', 'sonar-reasoning-pro'),
'research_timeout_sec' => (int) env('AITUNNEL_RESEARCH_TIMEOUT_SEC', 300),
// Плитка балансов (16.07): ручка баланса `/v1/aitunnel/balance` (найдена в доках
// aitunnel, отдаёт {"balance":N} в рублях) + пороги светофора + ссылка пополнения.
'balance_url' => env('AITUNNEL_BALANCE_URL', 'https://api.aitunnel.ru/v1/aitunnel/balance'),
'red_floor_rub' => (int) env('AITUNNEL_RED_FLOOR_RUB', 500),
'amber_floor_rub' => (int) env('AITUNNEL_AMBER_FLOOR_RUB', 2000),
'topup_url' => env('AITUNNEL_TOPUP_URL', 'https://aitunnel.ru/'),
],
// EXA — поиск САЙТА по имени для канала В (федералы): у федерала нет карточки в 2ГИС/Яндексе
// на регион, а нам нужен сайт (ZAFIKSIROVANO / §11.5). Ключ только в .env. Пусто → канал В без сайтов.
'exa' => [
'key' => env('EXA_API_KEY'),
'base_url' => env('EXA_BASE_URL', 'https://api.exa.ai'),
'timeout_sec' => (int) env('EXA_TIMEOUT_SEC', 90),
// Сколько запросов сайта-федерала слать ОДНОВРЕМЕННО (пул EXA). Лимит EXA — на КЛЮЧ,
// общий на всех клиентов, поэтому бережно; глобальный потолок между клиентами — очередь.
'concurrency' => (int) env('EXA_CONCURRENCY', 5),
// Прокси для EXA (Guzzle proxy option). EXA за Cloudflare режет российские IP, поэтому
// федеральный канал ходит наружу через зарубежный выход. Формат socks5h://host:port или
// http://host:port. Пусто → напрямую.
'proxy' => env('EXA_PROXY'),
// У EXA нет программной ручки баланса (биллинг только в кабинете) → плитка «жив/нет».
'balance_url' => env('EXA_BALANCE_URL'),
'red_floor_rub' => (int) env('EXA_RED_FLOOR_RUB', 500),
'amber_floor_rub' => (int) env('EXA_AMBER_FLOOR_RUB', 2000),
'topup_url' => env('EXA_TOPUP_URL', 'https://dashboard.exa.ai/'),
// Плитка балансов (16.07): нейтральный адрес для проверки живости SSH-тоннеля
// (запрос через exa.proxy на него). Не EXA — чтобы отделить «тоннель жив» от «EXA жив».
'tunnel_check_url' => env('EXA_TUNNEL_CHECK_URL', 'https://api.ipify.org'),
// «Пополнить» для EXA-тоннеля: за ним платный зарубежный VPN (клиент Happ). Провайдер
// подписки задаётся владельцем — если платишь не через Happ, переопредели EXA_TUNNEL_TOPUP_URL.
'tunnel_topup_url' => env('EXA_TUNNEL_TOPUP_URL', 'https://happ-proxy.com/'),
],
// Плитка балансов (28.06): Yandex Cloud биллинг (серверы + Managed PG ~18к/мес).
// OAuth владельца (interim) → IAM-токен → billing API. SA billing-reader создан,
// миграция на него — follow-up (только источник токена сменится). console_billing_url
// + billing_account_id строят прямую ссылку «Пополнить» в дашборде.
'yandex_cloud' => [
'oauth_token' => env('YC_OAUTH_TOKEN'),
'billing_account_id' => env('YC_BILLING_ACCOUNT_ID'),
'iam_url' => env('YC_IAM_URL', 'https://iam.api.cloud.yandex.net/iam/v1/tokens'),
'billing_url' => env('YC_BILLING_URL', 'https://billing.api.cloud.yandex.net/billing/v1/billingAccounts'),
'console_billing_url' => env('YC_CONSOLE_BILLING_URL', 'https://console.yandex.cloud/billing/accounts'),
'daily_spend_rub' => (int) env('YC_DAILY_SPEND_RUB', 600), // оценка ~18к/мес; откалибровать
'red_floor_rub' => (int) env('YC_RED_FLOOR_RUB', 1000),
'amber_floor_rub' => (int) env('YC_AMBER_FLOOR_RUB', 5000),
],
// G7-A: клиентская «Помощь».
'support' => [
'email' => env('SUPPORT_EMAIL', 'support@liderra.ru'),
],
// Яндекс.Метрика. Два счётчика:
// counter_id — кабинет (110494416), историческая аналитика кабинета.
// path_counter_id — «полный путь» (110476275), тот же счётчик, что на лендинге
// liderra.ru. Грузится на входе/регистрации/публичных страницах
// и в кабинете, чтобы Вебвизор видел цепочку от клика по рекламе
// до действий в кабинете одним человеком, а не двумя.
// Пусто = соответствующий счётчик не грузится.
// Админка и портал продаж Метрику не грузят вообще — см. router/index.ts.
'metrika' => [
'counter_id' => env('METRIKA_COUNTER_ID'),
'path_counter_id' => env('METRIKA_PATH_COUNTER_ID'),
],
// ИИ-бот техподдержки в СВОЁМ окошке чата на сайте (спека 2026-07-13-own-chat-widget-design;
// до 07.07.2026 жил внутри чата Jivo — эта интеграция закрыта, см. docs/CHANGELOG).
// tours_enabled — «экскурсии»: ссылка «Показать на портале» в ответе бота. Раньше вели
// не по теме (совпало одно случайное слово) и были выключены. 13.07.2026 правило
// переписано: экскурсия только на ВОПРОС, слово которого попало в заголовок статьи
// или её синонимы, и только клиенту ИЗ КАБИНЕТА — гостю с лендинга там нечего смотреть.
'bot' => [
'tours_enabled' => env('BOT_TOURS_ENABLED', true),
// Показ примера на ЛЕНДИНГЕ для гостя (не вошедшего клиента): раздел «Как это
// работает» с якорями по темам (задача 2026-07-13). Гость не может открыть
// экскурсию внутри кабинета — ему нечего там показывать, но разбор на лендинге
// доступен без входа. Какие статьи имеют разбор — config/guides.php (связь по слагу).
'landing_url' => env('LANDING_URL', 'https://liderra.ru'),
// Мозг бота: yandex (данные не покидают РФ) | aitunnel (зарубежные модели через
// российский шлюз; ПДн вырезаются PersonalDataMask до отправки).
'llm' => env('BOT_LLM', 'yandex'),
'aitunnel_model' => env('BOT_AITUNNEL_MODEL', 'gpt-4o-mini'),
'timeout_seconds' => env('BOT_LLM_TIMEOUT', 10),
// Счётчики чата должны считать АТОМАРНО: файловый кэш под параллельной нагрузкой
// теряет до 74% прибавок (проверено), и защита от накрутки счёта становится дырявой.
// Redis (INCR) атомарен. В тестах — 'array' (один процесс, гонки не бывает).
'limiter_store' => env('BOT_LIMITER_STORE', 'redis'),
],
// YandexGPT Lite (Yandex Cloud Foundation Models) — мозг бота (решение 8 протокола).
'yandexgpt' => [
'api_key' => env('YANDEX_GPT_API_KEY', ''),
'folder_id' => env('YANDEX_GPT_FOLDER_ID', ''),
'model' => env('YANDEX_GPT_MODEL', 'yandexgpt-lite/latest'),
'endpoint' => env('YANDEX_GPT_ENDPOINT', 'https://llm.api.cloud.yandex.net/foundationModels/v1/completion'),
'timeout_seconds' => 8,
],
// Учёт посетителей: офлайн-базы гео (DB-IP Lite). Файлов нет → город не определяется,
// учёт продолжает работать. Обновление баз — раз в месяц, вручную.
'geoip' => [
'city_db' => env('GEOIP_CITY_DB', storage_path('geoip/dbip-city-lite.mmdb')),
'asn_db' => env('GEOIP_ASN_DB', storage_path('geoip/dbip-asn-lite.mmdb')),
],
// Платёжный шлюз ЮKassa. webhook_ip_allowlist — CSV IP/CIDR из env (defense-in-depth
// на /api/webhook/payment). Пусто → fail-open (поток не ломается). На проде заполнить
// опубликованными ЮKassa подсетями: 185.71.76.0/27,185.71.77.0/27,77.75.153.0/25,
// 77.75.154.128/25,77.75.156.11,77.75.156.35,2a02:5180::/32.
'yookassa' => [
'shop_id' => env('YOOKASSA_SHOP_ID'),
'secret_key' => env('YOOKASSA_SECRET_KEY'),
'api_url' => env('YOOKASSA_API_URL', 'https://api.yookassa.ru/v3'),
'webhook_ip_allowlist' => array_values(array_filter(array_map(
'trim',
explode(',', (string) env('YOOKASSA_WEBHOOK_IPS', '')),
))),
],
// Автоподбор шаг 2: сервис обхода антибота справочников (2ГИС/Яндекс) — xfetch.ru.
// Ключ — ТОЛЬКО в .env (gitignored), в код/гит не попадает. Без ключа загрузчик
// молча возвращает пусто (поток не падает).
'xfetch' => [
'key' => env('XFETCH_API_KEY'),
// Несколько ключей через запятую (round-robin) — суммарная параллельность = ключи × concurrency.
// Пусто → берётся одиночный XFETCH_API_KEY. Ключи — ТОЛЬКО в .env, в код/гит не попадают.
'keys' => array_values(array_filter(
array_map('trim', explode(',', (string) env('XFETCH_API_KEYS', ''))),
static fn (string $v): bool => $v !== '',
)),
'endpoint' => env('XFETCH_ENDPOINT', 'https://xf4.ru/fetch'),
// Параллельность НА КЛЮЧ. Эмпирический предел xf4.ru — ~3 одновременных на ключ
// (выше идут 429 Too Many Requests; замер 01.07.2026). Меняется без деплоя через .env.
'concurrency' => (int) env('XFETCH_CONCURRENCY', 3),
'balance_url' => env('XFETCH_BALANCE_URL'),
'red_floor_rub' => (int) env('XFETCH_RED_FLOOR_RUB', 300),
'amber_floor_rub' => (int) env('XFETCH_AMBER_FLOOR_RUB', 1000),
'topup_url' => env('XFETCH_TOPUP_URL', 'https://xf4.ru/'),
],
// Автоподбор self-render: свой рендер-сервис на виртуалке (замена xf4, за флагом autopodbor.self_render).
// Секрет портал↔виртуалка и адрес — ТОЛЬКО в .env. Без них SelfRenderClient молча пусто (деградация).
'self_render' => [
// 🔴 Р126: адрес обязан быть `https://`. Тайный ключ едет ЗАГОЛОВКОМ X-Render-Key в
// каждом запросе — по `http://` он поехал бы открытым текстом и молча. Адрес без
// шифрования портал не принимает: дверь закрыта, поиск отдаёт пусто и говорит об этом
// в журнал. Исключение ровно одно — разработчик на своём компьютере, и оно НЕ
// настраивается отсюда: тумблера «можно без шифра» тут нет намеренно, такой тумблер и
// был бы той самой опечаткой. Как устроено — см. `DverRendera::svoyaMashina()`.
'endpoint' => env('SELF_RENDER_ENDPOINT', ''),
'key' => env('SELF_RENDER_KEY', ''),
// 🔴 Р126. Куда клиенту ВООБЩЕ разрешено ходить с тайным ключом. Опечатка в
// SELF_RENDER_ENDPOINT увела бы запрос в чужой интернет — и тайный ключ уехал бы чужому
// человеку, молча и навсегда. Хост не из этого списка = дверь закрыта, запрос не
// отправлен, ключ никуда не ушёл. Пустой список = закрыто ВСЁ (fail-closed).
//
// 🔴 Умолчание НЕ пустое намеренно: на боевом этой строки в .env нет, а «Поиск клиентов»
// работает и зарабатывает прямо сейчас. Пустое умолчание закрыло бы его в момент выката.
// Взято по замеру ПИЛОТ.md: боевая виртуалка-рендер `https://51-250-1-97.sslip.io/render`
// (51.250.1.97). Набор тот же, что уже принят владельцем для OBZVON_ALLOWED_HOSTS —
// это ОДНА И ТА ЖЕ машина.
'allowed_hosts' => array_values(array_filter(
array_map('trim', explode(',', (string) env(
'SELF_RENDER_ALLOWED_HOSTS',
'51.250.1.97,51-250-1-97.sslip.io,127.0.0.1,localhost',
))),
static fn (string $v): bool => $v !== '',
)),
// Параллельность запросов к виртуалке (браузерная параллельность — внутри виртуалки, §12).
'concurrency' => (int) env('SELF_RENDER_CONCURRENCY', 8),
// Плитка балансов (16.07): URL health-check виртуалки. Пусто → пингуем endpoint.
'health_url' => env('SELF_RENDER_HEALTH_URL'),
],
// Плитка балансов (17.07): прокси автоподбора (Proxy.Market, мобильный Tele2). «Баланс» = срок
// действия. Живость проверяет виртуалка-рендер (у неё логин/пароль прокси) через /proxy-check.
// Ключ виртуалки берём из services.self_render.key. Срок — руками в .env при продлении.
//
// 🔴 Р126, третья дверь (решение владельца 07.08.2026). Ключ тут ТОТ ЖЕ, что у рендера, а
// адрес — ДРУГОЙ (PROXY_MARKET_CHECK_URL). Поэтому проба проходит ту же дверь `DverRendera`:
// только https:// и только хост из `self_render.allowed_hosts` — списка своего у неё нет
// намеренно, список один на ключ (два списка на один ключ разъехались бы). Дверь закрыта =
// плитка СЕРАЯ («не знаем»), а не красная («прокси кончились»), — красным врать нельзя.
'proxy_market' => [
'check_url' => env('PROXY_MARKET_CHECK_URL'), // https://51-250-1-97.sslip.io/proxy-check
'expires_at' => env('PROXY_MARKET_EXPIRES_AT'), // 2026-09-03 (продлён 05.08.2026)
'amber_days' => (int) env('PROXY_MARKET_AMBER_DAYS', 7),
'topup_url' => env('PROXY_MARKET_TOPUP_URL', 'https://proxy.market/'),
],
// Куда слать алерты о недоступности/исходе денег внешних сервисов.
'monitoring' => [
'alert_email' => env('MONITORING_ALERT_EMAIL', env('SUPPLIER_ALERT_EMAIL', 'ops@liderra.ru')),
],
// Баланс Яндекс 360 (почта) — РУЧНОЙ ввод (см. Yandex360BalanceProvider). Пороги
// светофора — как у dadata; topup_url — страница «Оплата и тариф» в кабинете Яндекс 360.
'yandex360' => [
'red_floor_rub' => (int) env('YANDEX360_RED_FLOOR_RUB', 500),
'amber_floor_rub' => (int) env('YANDEX360_AMBER_FLOOR_RUB', 2000),
'topup_url' => env('YANDEX360_TOPUP_URL', 'https://admin.yandex.ru/products'),
],
// Плитка балансов (16.07): служба поиска клиентов отдела продаж (/opt/sales-finder, :8200
// за nginx /finder/). Только живость (2ГИС-пробив; денежного баланса нет). Пусто → не проверяем.
// Дефолт ПУСТОЙ намеренно: реальный URL задаётся только в прод-.env (SALES_FINDER_BASE_URL),
// иначе dev/тесты стучались бы в боевой /finder/. Прод-адрес — https://lk.liderra.ru/finder/.
'sales_finder' => [
'base_url' => env('SALES_FINDER_BASE_URL', ''),
'health_path' => env('SALES_FINDER_HEALTH_PATH', ''),
],
// Плитка балансов (16.07): Keyso (keys.so) — разведка рекламы, зависимость службы
// «Поиск клиентов» (/opt/sales-finder держит токен). Портал только проверяет живость
// хоста (пинг api.keys.so, <500 = жив), денег/токена не нужно.
'keyso' => [
'base_url' => env('KEYSO_BASE_URL', 'https://api.keys.so'),
'topup_url' => env('KEYSO_TOPUP_URL', 'https://www.keys.so/ru/tarif'),
],
'sms' => [
// Песочница: провайдеры не вызываются, отправка только помечается в журнале.
'sandbox' => (bool) env('SMS_SANDBOX', true),
// Наша организация в бланке согласия клиента (кому клиент даёт согласие
// зарегистрировать имя). Согласуется с владельцем — юридический текст.
'consent_our_entity' => env('SMS_CONSENT_OUR_ENTITY', 'сервис «Лидерра» (liderra.ru)'),
// Резерв канала: точный оператор отказал → попробовать универсала.
// По умолчанию ВЫКЛ (решение владельца 23.07.2026) — включить одной строкой.
'failover_to_universal' => filter_var(env('SMS_FAILOVER_TO_UNIVERSAL', false), FILTER_VALIDATE_BOOL),
// Канал МТС = «МТС Маркетолог, Рассылки по своей базе PRO» (омни-адаптер
// api.mts.ru). naming — согласованное имя отправителя (MTSM_Test/liderra.ru);
// connect_to — мост (host:port SSH-туннеля, напр. 127.0.0.1:18443), пусто → напрямую.
'mts' => [
'token' => env('SMS_MTS_TOKEN'),
'enabled' => (bool) env('SMS_MTS_ENABLED', false),
'base_url' => env('SMS_MTS_BASE_URL', 'https://api.mts.ru/client-omni-adapter_production/1.0.2'),
'naming' => env('SMS_MTS_NAMING', ''),
'connect_to' => env('SMS_MTS_CONNECT_TO', ''),
// Обратный сигнал о судьбе (строка листа 5.2): МТС сам шлёт отчёт на
// /api/webhook/mts-delivery/{secret}. Секрет короче 32 знаков или пустой =
// приёмник ЗАКРЫТ (судьбу и без него доберёт опрос каждые 10 минут).
// Список адресов отправителя — через запятую, можно подсетями; пуст =
// не проверяем (адреса МТС пока неизвестны, В-243).
'webhook_secret' => env('SMS_MTS_WEBHOOK_SECRET', ''),
'webhook_ips' => env('SMS_MTS_WEBHOOK_IPS', ''),
],
// Канал Билайна = платформа прямых A2P-рассылок a2p-sms.beeline.ru,
// метод JSON (адрес обязан оканчиваться на /rest). Вход двух видов:
// • логин с паролем от кабинета рассылок — работает сразу;
// • постоянный ключ (X-ApiKey) — надёжнее, заводится в кабинете в
// «Карточка пользователя → API токены» и показывается ОДИН раз.
// Ключ задан → логин с паролем не передаются вовсе (иначе платформа откажет).
// naming — согласованное имя отправителя (Liderra.ru).
// 🔑 Билайн здесь агрегатор: доставляет в девять сетей, но имя отправителя
// согласуется с КАЖДЫМ оператором отдельно (код отказа 20230 называет,
// у кого именно не одобрено). Пока имя согласовано только с Билайном —
// serves в реестре ниже держим ['beeline'].
'beeline' => [
'login' => env('SMS_BEELINE_LOGIN'),
'password' => env('SMS_BEELINE_PASSWORD'),
'token' => env('SMS_BEELINE_TOKEN'),
'enabled' => (bool) env('SMS_BEELINE_ENABLED', false),
'base_url' => env('SMS_BEELINE_BASE_URL', 'https://a2p-sms-https.beeline.ru/proto/http/rest'),
'naming' => env('SMS_BEELINE_NAMING', ''),
// Обратный звонок о судьбе (строка листа 5.2): платформа сама зовёт
// /api/webhook/beeline-delivery/{secret} методом POST, поля ORDID,
// CNRID, RESCOUNT, STATUS, FINALTIME. Адрес заявляется в кабинете:
// Настройки → Дополнительно → «Приём статусов отправленных сообщений».
// Секрет короче 32 знаков или пустой = приёмник ЗАКРЫТ наглухо.
// 🪤 Повторяет ли платформа неудавшийся звонок — в её описании НЕ
// сказано ни слова; считаем худшее, пропущенный доберёт только опрос.
// Адреса отправителя — через запятую, можно подсетями; пуст = не
// проверяем (адреса платформы нам неизвестны, выдумывать нельзя).
'webhook_secret' => env('SMS_BEELINE_WEBHOOK_SECRET', ''),
'webhook_ips' => env('SMS_BEELINE_WEBHOOK_IPS', ''),
],
// Канал Теле2 = «SMS-Таргет» (target.t2.ru), HTTP API v2, вход по логину и
// паролю (Basic Auth) из раздела кабинета «Профиль → API-рассылки».
// naming — согласованное имя отправителя (Liderra).
// 🪤 t2 пускает к API только с ЗАЯВЛЕННЫХ адресов: адрес боевого сервера
// надо назвать в настройках подключения, иначе 403 wrong_ip.
'tele2' => [
'login' => env('SMS_T2_LOGIN'),
'password' => env('SMS_T2_PASSWORD'),
'enabled' => (bool) env('SMS_T2_ENABLED', false),
'base_url' => env('SMS_T2_BASE_URL', 'https://target.t2.ru/api/v2'),
'naming' => env('SMS_T2_NAMING', ''),
// Обратный звонок о судьбе (строка листа 5.2): t2 сам зовёт
// /api/webhook/t2-delivery/{secret}?id=&status=&parts=. Секрет короче
// 32 знаков или пустой = приёмник ЗАКРЫТ. 🪤 t2 ответа не проверяет и
// повторов НЕ делает — пропущенный звонок доберёт только опрос.
// Адреса отправителя — через запятую; пуст = не проверяем (адреса t2
// нам неизвестны, выдумывать нельзя).
'webhook_secret' => env('SMS_T2_WEBHOOK_SECRET', ''),
'webhook_ips' => env('SMS_T2_WEBHOOK_IPS', ''),
],
// Декларативный реестр живых каналов. Новый оператор = запись здесь
// + ключ в .env + (если API другой) файл-провайдер + арм в makeSmsProvider().
// Цены — в копейках за сегмент, ключ '*' = единая цена канала; меняются
// через .env без выката.
'providers' => [
'smsc' => [
'enabled' => (bool) env('SMS_SMSC_ENABLED', false),
'serves' => ['*'],
'price_kopecks' => ['*' => (int) env('SMS_SMSC_PRICE_KOP', 0)],
],
'mts' => [
'enabled' => (bool) env('SMS_MTS_ENABLED', false),
'serves' => ['mts'],
'price_kopecks' => ['*' => (int) env('SMS_MTS_PRICE_KOP', 0)],
],
'tele2' => [
'enabled' => (bool) env('SMS_T2_ENABLED', false),
'serves' => ['tele2'],
'price_kopecks' => ['*' => (int) env('SMS_T2_PRICE_KOP', 0)],
],
// Билайн умеет девять сетей, но имя отправителя согласовано пока
// только с двумя — с ним самим и с Мотивом. Расширять serves можно
// лишь после того, как имя одобрит очередной оператор, иначе будет
// отказ 20230.
//
// 🔴 Мотив достался в нагрузку: в форме заявки на имя его галка
// прибита намертво рядом с билайновской, снять нельзя. Возить его
// разрешает ОТДЕЛЬНЫЙ выключатель.
// Имя `Liderra.ru` ОДОБРЕНО обеими сетями 10.08.2026 — проверено
// глазами в кабинете: зелёные галочки у Билайна и у Мотива, у
// остальных семи сетей серые минусы. То есть выключатель поднимать
// МОЖНО. Умолчание всё равно оставлено ВЫКЛЮЧЕННЫМ намеренно: новая
// рабочая папка не должна начинать возить в чужую сеть сама по себе,
// это осознанное действие через .env.
// 🪤 Отправка в сеть, где имя НЕ одобрено, вернёт 20230 — отказ
// ОКОНЧАТЕЛЬНЫЙ, повтора нет, сообщение умрёт молча для клиента.
'beeline' => [
'enabled' => (bool) env('SMS_BEELINE_ENABLED', false),
'serves' => filter_var(env('SMS_BEELINE_SERVES_MOTIV', false), FILTER_VALIDATE_BOOL)
? ['beeline', 'motiv']
: ['beeline'],
// Цена у Мотива СВОЯ: 6,24 ₽ без НДС по прайсу Билайна × 1,23464
// (НДС 22% + надбавка тарифа 1,2%) = 7,70 ₽ против 5,86 ₽ своим.
// Пакетов на Мотив у Билайна нет вовсе — дешевле не станет.
// Ключ '*' остаётся ценой канала для всех прочих сетей.
'price_kopecks' => [
'motiv' => (int) env('SMS_BEELINE_PRICE_MOTIV_KOP', 770),
'*' => (int) env('SMS_BEELINE_PRICE_KOP', 0),
],
],
],
],
// ─────────────────────────────────────────────────────────────────────────
// Обзвон: шов «портал зовёт голосового робота» (З-0.3, решение владельца Р84).
//
// 🔴 Робот живёт на ОТДЕЛЬНОЙ машине и делит железо с боевым рендером «Поиска
// клиентов» — это живые деньги живых клиентов. Поэтому предел одновременных
// заданий и пределы времени стоят здесь С САМОГО НАЧАЛА, а не «когда-нибудь
// потом»: голос не имеет права уронить чужой боевой рендер.
//
// Адрес и тайный ключ — ТОЛЬКО в .env. Пусто = дверь закрыта: {@see ObzvonClient}
// наружу не ходит, звонка точно не было, и он это говорит вслух (мягкая
// деградация — та же, что у self_render без ключа).
// ─────────────────────────────────────────────────────────────────────────
'obzvon' => [
// БАЗОВЫЙ адрес приёмника заданий на машине робота (без хвоста ручки):
// клиент сам добавит /pozvonit, /sostoyanie, /ostanovit. Сам приёмник —
// задача З-0.6, его ещё нет; портальная половина от этого не зависит.
//
// 🔴 Р123: адрес обязан быть `https://`. Тайный ключ едет ЗАГОЛОВКОМ в
// каждом запросе — по `http://` он поехал бы открытым текстом и молча.
// Адрес без шифрования портал не принимает: дверь закрыта, звонка нет.
// Исключение ровно одно — разработчик на своём компьютере, и оно НЕ
// настраивается отсюда: тумблера «можно без шифра» тут нет намеренно,
// такой тумблер и был бы той самой опечаткой. Как устроено исключение —
// см. `ObzvonClient::svoyaMashina()`.
'endpoint' => env('OBZVON_ENDPOINT', ''),
'key' => env('OBZVON_KEY', ''),
// Сколько заданий разрешено держать у робота ОДНОВРЕМЕННО.
// 🔴 Предел общий на машину, а не «по числу в памяти работника очереди»:
// он взят именованными замками кэша (см. ObzvonClient), поэтому два и
// более работников очереди в РАЗНЫХ процессах делят одни и те же места.
// Чего он НЕ ограничивает — см. шапку ObzvonClient, там сказано прямо.
'concurrency' => (int) env('OBZVON_CONCURRENCY', 4),
// Сколько СЕКУНД лишнее задание согласно ждать свободного места. Дождалось —
// пошло; не дождалось — исход «робот занят»: наружу не ходили, звонка точно
// не было, повторить позже безопасно. 0 = не ждать вовсе (так гоняем тесты).
'wait_seconds' => (int) env('OBZVON_WAIT_SECONDS', 10),
// Два предела времени, а не один — и это не перестраховка.
// connect_timeout — дозвониться до машины робота;
// timeout — ВЕСЬ обмен целиком, вместе с чтением ответа.
// Второй нужен ровно против случая «робот отдаёт ответ по байту бесконечно»:
// соединение установлено, дальше чтение висит вечно и держит работника
// очереди. Один только connect_timeout такой случай НЕ ловит.
'connect_timeout' => (int) env('OBZVON_CONNECT_TIMEOUT', 5),
'timeout' => (int) env('OBZVON_TIMEOUT', 15),
// 🔴 Куда клиенту ВООБЩЕ разрешено ходить с тайным ключом. Опечатка в
// OBZVON_ENDPOINT уводит запрос в чужой интернет — и тайный ключ уезжает
// чужому человеку, молча и навсегда. Хост не из этого списка = дверь
// закрыта, запрос не отправлен, ключ никуда не ушёл.
// Пустой список = закрыто ВСЁ (fail-closed, а не «раз пусто — значит можно»).
// Переезд робота на другой адрес — осознанная правка .env в двух местах.
'allowed_hosts' => array_values(array_filter(
array_map('trim', explode(',', (string) env(
'OBZVON_ALLOWED_HOSTS',
'51.250.1.97,51-250-1-97.sslip.io,127.0.0.1,localhost',
))),
static fn (string $v): bool => $v !== '',
)),
],
];