Files
portal/app/config/obzvon.php
T
Дмитрий df8c4720cb feat обзвон: запись исхода мимо двери больше не проходит — пол в модели и обход кода
Голый шов. Правило Т84 «недозвон не затирает состоявшийся разговор» жило в двери
ItogPoNomeru, и восемь сторожей доказывали, что дверь работает. Но все восемь
ходили ЧЕРЕЗ дверь, а дверью можно не воспользоваться: запрета писать МИМО не
было ни одного. Итог по номеру сегодня не пишет никто, значит первым пишущим
будет следующая смена — она увидит у модели поле outcome раньше, чем найдёт
дверь, напишет в него напрямую, и правило рухнет молча. Дверь осталась бы целой
и никем не использованной.

Защита разделена по УСТРОЙСТВУ, а не по тексту, и белого списка «кому можно»
нет ни в одной половине — такой список сам стал бы дырой.

Пол в самой модели ловит любую запись через объект: присваивание полю, fill,
update у строки, save. Имя переменной он не читает вовсе — он стоит на самой
записи, поэтому главная слабость текстового поиска закрыта не текстом, а местом.
Проверяется смысл перехода, а не автор: дверь поблажек не имеет и проходит лишь
потому, что запрещённого перехода не делает.

Обход кода ловит то, чего пол не видит физически: массовую правку и запросы
построителем — Eloquent на них событий не шлёт. Запрет действует на всех без
исключений, включая саму дверь.

Раскладка исходов НЕ задвоена: обе половины читают одну настройку. Повторено
только применение правила — модели запрещено зависеть от служб, у слоя Model
в настройке слоёв разрешённых зависимостей ноль. Два места связаны сторожем,
который обходит все 64 пары исходов и требует одинакового ответа.

Чего не ловит НИ ОДНА из половин, названо в бумагах: правку прямым SQL мимо
портала, имя таблицы или столбца, собранное из кусков, и код вне app. Первое
лечится только замком в самой базе — это правка канона схемы и вопрос владельцу,
поэтому названо, а не сделано молча.

Сторожа показаны красными. Настоящая запись мимо двери положена в САМ обходимый
код, а не в песочницу: сторож назвал файл, строку 14 и само выражение. Снятие
пола дало три красных, включая связку — пара transferred и no_answer. Возврат
доказан слепком со снятием невидимых знаков конца строки.

Заодно подписи двух значений в настройке: владелец закрыл обе развилки решениями
Р120 и Р121 от 06.08.2026, оба совпали с поставленным. Значения не менялись,
переписаны только пояснения, и рядом с каждым названо то, чего решение НЕ
закрывает.

Прогоны: обзвон и сделки 442, зелёных 441, красных ноль. Мои сторожа 32 из 32.
Статанализ ноль, deptrac ноль нарушений.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 21:10:53 +03:00

117 lines
7.5 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
declare(strict_types=1);
/**
* Настройки обзвона — ОДНО место, где лежат решения, которые дороже всего
* переигрывать рассыпанными по коду (З-2.4, решения владельца Р112/Р113).
*
* 🔴 Зачем файл вообще. Восемь исходов Т21/Т65 живут В ОДНОМ столбце
* `obzvon_number_results.outcome`, и замок в базе пропускает любой из восьми.
* Значит «недозвон верхнюю строку не меняет» (Т84) базой НЕ охраняется: тот,
* кто запишет недозвон туда же, куда пишет остальные семь, затрёт удачный
* разговор молча — столбец один, значение разрешено, база примет. Единственное
* место, где эта разница вообще существует, — здесь.
*
* Кто это читает: App\Services\Obzvon\RaskladkaIskhodov (и только он).
*/
return [
/*
|--------------------------------------------------------------------------
| Раскладка восьми исходов на два рода (Р112)
|--------------------------------------------------------------------------
|
| «Про номер» — исход самого номера. Верхнюю строку карточки берёт
| ПОСЛЕДНИЙ ПО ВРЕМЕНИ из них, и каждый попадает в ленту разговоров.
|
| «Про попытку» — исход одной попытки набора. Верхнюю строку НЕ меняет,
| в ленту не попадает, сворачивается в счётчик «не дозвонились: N раз».
|
| 🔴 Ровно одно исключение, и оно нарочное: когда состоявшегося разговора
| НЕ БЫЛО НИ РАЗУ, верхняя строка обязана сказать «не дозвонились» — иначе
| двенадцать попыток дадут пустую карточку. Поэтому исход «про попытку»
| ставится как ЗАПАСНОЙ: пишется только поверх пустоты или самого себя и
| никогда поверх исхода «про номер». Правило исполнено в одном месте —
| App\Services\Obzvon\ItogPoNomeru::zapisat.
|
| Переиграть раскладку = переставить слаг из списка в список. Минута.
| Восемь слагов — канон в App\Models\ObzvonNumberResult::OUTCOMES и в
| CHECK-замке `chk_obzvon_number_results_outcome`; здесь только раскладка.
|
*/
'raskladka' => [
'pro_nomer' => [
'transferred', // переведено
'transfer_failed', // переведено неуспешно
'callback', // перезвонить, дата и время
'email_sent', // письмо клиенту отправлено
'not_target', // не целевой
'refused', // отказ
'aggression', // агрессия
],
'pro_popytku' => [
'no_answer', // недозвон
],
],
/*
|--------------------------------------------------------------------------
| Карточка сделки — два вопроса, ЗАКРЫТЫЕ владельцем 06.08.2026 (З-2.4)
|--------------------------------------------------------------------------
|
| 🟢 Оба значения ниже — не «пока так», а **решения владельца**: Р120 и Р121
| от 06.08.2026. Переспрашивать закрытое не нужно.
|
| Менять их можно, но только новым решением владельца: числа стоят в одном
| месте нарочно, чтобы правка стоила минуту, а не чтобы её делали походя.
|
*/
'kartochka' => [
/*
* Где кончается «хоть секунда разговора» (Т84а).
*
* 🟢 РЕШЕНИЕ ВЛАДЕЛЬЦА Р120 от 06.08.2026: разговором считается контакт
* от ОДНОЙ секунды. Закрыто, переспрашивать не нужно.
*
* Разговор попадает в ленту, когда трубку сняли (`answered`) И первое
* плечо длилось не меньше этого числа секунд.
*
* Развилки «0,4 секунды» не существовало вовсе: длительность в базе —
* целые секунды (`leg1_duration_sec INTEGER`). Владелец выбирал из
* «одна, три или пять» и отрезал три и пять по доводу: короче порога
* сброс сливается с недозвоном и становится невидимым, а сброс — это
* ОТВЕТ человека, тогда как недозвон — его отсутствие.
*
* 🪤 ЧЕГО РЕШЕНИЕ НЕ ЗАКРЫВАЕТ: при пороге в секунду лента длиннее, чем
* при любом другом, — короткие сбросы тоже строки, и сворачивать их
* никто не назначал; случай «соединилось и оборвалось до первого звука»
* ни разу не замерен на живых звонках.
*/
'minimalnaya_dlitelnost_sec' => 1,
/*
* Сколько ПРОШЛЫХ списков показывать в карточке (Р113).
*
* 🟢 РЕШЕНИЕ ВЛАДЕЛЬЦА Р121 от 06.08.2026: показываем ВСЕ прошлые
* списки. Закрыто, переспрашивать не нужно. Довод: Р113 требует, чтобы
* прошлое было ВИДНО, и прятать часть истории по своему усмотрению мы
* не вправе.
*
* Тот же номер новым списком заводит НОВЫЙ итог (замок
* `UNIQUE (tenant_id, campaign_id, phone)`), прежний остаётся историей.
*
* null — показывать все. Число N — показывать N самых свежих списков.
*
* 🪤 ЧЕГО РЕШЕНИЕ НЕ ЗАКРЫВАЕТ: карточка растёт без предела — у номера
* в десятке списков лента вытянется. И отдельно: итог привязан к
* СПИСКУ, а не к человеку, поэтому при смене владельца номера в
* карточке смешается история двух РАЗНЫХ людей. Это не обсуждали вовсе.
*/
'kampaniy_v_kartochke' => null,
],
];