[ '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.07–31.07 (2 дня) → «cannot be less than 600 rub.» = 2 × 300 // период 30.07–06.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 !== '', )), ], ];