Commit Graph

6 Commits

Author SHA1 Message Date
Дмитрий f4d1526b00 обзвон: фундамент клиентской половины — кампания в базе и четыре адреса портала под замком видимости 2026-08-08 22:43:45 +03:00
Дмитрий c277299794 колонка stop_requested_at: отметка «клиент попросил остановить рекламу»
Под кнопку «Остановить»: честно остановить показы можно только кнопкой «Завершить» в
кабинете МТС, её нажмёт робот по заданию портала, и кабинет предупреждает, что это
занимает до 30 минут. Всё это время порталу надо помнить, что просьба уже подана.

🔴 Отметка, а не новый статус: статус потянул бы переходы, подписи экрана и все места,
где статусы перечислены, а выигрыша для клиента нет. Кампания честно остаётся «Идут
показы» — показы правда идут.

NULL значим: не просили. Снимается обратно в NULL, если робот не смог нажать.

Канон таблицы — в db/schema_modules.sql, а не в db/schema.sql: первый только текст,
второй исполняется при сборке с нуля. Заодно в журнале схемы назван замеренный долг:
модульный канон отстал от базы на девять колонок со 02.08.2026, эта правка его не
закрывает и намеренно не увеличивает.

RLS не меняется: колонка на существующей таблице, политика задана миграцией создания,
табличный GRANT покрывает новые колонки.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 21:50:26 +03:00
Дмитрий 2b401298d8 feat: приём записей клиента для обучения робота обзвона — З-2.3
Клиент отдаёт нам записи СВОИХ холодных разговоров. Это голоса живых людей,
которые об этом не знают и согласия нам не давали. До этой правки принять их
было негде и нечем: положить так, чтобы клиент А не открыл запись клиента Б,
было некуда; срока жизни у записей не было; обращение к чужому голосу не
оставляло следа.

Что заведено:

- таблица obzvon_materialy_klienta — канон в db/schema_modules.sql раздел 24,
  журнал схемы v9.70. Изоляция клиентов защитой по строкам: ENABLE + FORCE,
  политика с USING и WITH CHECK. DELETE не выдан никому — строка остаётся
  носителем надписи «удалены по сроку хранения»;
- ДВА срока, а не один — решение владельца Р82: звук 30 дней, расшифровка
  шесть месяцев. Обе даты истечения NOT NULL: строка без даты означает голос,
  которого не найдёт ни одна чистка. Сами сроки — в настройках портала,
  не в коде;
- два следа удаления врозь: команда чистки из З-2.2 обязана уметь стереть
  звук, не тронув текст. Два замка не дают строке врать «стёрто», пока файл жив;
- закрытое хранилище obzvon_materialy: без ключа url и с serve=false, корень
  под storage/app/private. Публичной ссылки на чужой голос не появится даже
  по ошибке;
- служба MaterialyKlientaService: приём с отклонением чужого формата и битого
  файла, открытие с явной сверкой клиента поверх защиты по строкам, счёт
  записей без нижней границы — три берём, двадцать берём, ноль пускаем
  вторым путём;
- отказ MaterialNePrinyat отделяет наш сочинённый текст от дословного
  показания системы видимой границей. Настоящая причина не выбрасывается;
- след в журнале ПДн pd_processing_log готовым сервисом PdAuditLogger —
  на приём и на каждое открытие.

Стирает НЕ эта правка, а З-2.2, и она не построена. До её постройки материалы
не исчезнут — это известно и так задумано.

При накатке на боевой ОБЯЗАТЕЛЕН перезапуск db/03_service_bypass_policies.sql:
новой RLS-таблице мало политики и прав, иначе служебная роль молча правит
НОЛЬ строк.

Тронут один чужой сторож: SchemaDeltaTest, счётчик таблиц канона модулей
42 в 43. Так же его правили соседи на З-1.1 и З-1.3 — счётчик обязан расти
вместе с каноном, иначе прогон красный у всех.

Осталось открытым: от какой даты считать полгода — считаем от загрузки,
столбец recorded_at заведён под будущий ответ владельца.

24 сторожа, все 24 показаны красными двенадцатью ножами.
Полный прогон 4954 проверки, 4950 зелёных, 4 пропущено, красных 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 20:35:21 +03:00
Дмитрий 2f0ef8e868 feat,обзвон: тариф звонка переехал в базу — админ правит цену без программиста
Беда: правило цены звонка уже написано и работает, а самих двух цифр — за снятую
трубку и за начатую минуту — держать было негде и менять некому. Чтобы поправить
цену, нужен был программист и новая сборка.

Заведена таблица obzvon_tariffs: строка ровно одна на весь портал, замок
CHECK id = 1, деньги целыми копейками — те же единицы, что у строки звонка.
Ручка админки GET и PUT /api/admin/obzvon/tariff правит обе цифры разом:
половина новой пары с половиной старой давала бы тариф, которого никто не назначал.

🔴 Обе цифры назначены владельцем ВСЛЕПУЮ: сколько нам самим стоит звонок, ни разу
не измерено. Оговорка написана в четырёх местах вплотную к самим числам и уходит
в ответ ручки, чтобы её видел человек на экране, а не только программист в коде.

🔴 Смена тарифа НЕ пересчитывает вчерашние звонки: цена и снимок тарифа лежат в
самой строке звонка. Здесь только «сколько будет стоить следующий».

Мусор в цене не принимается: отрицательная, пустая, нечисловая и с третьим знаком
после точки — третий знак молча пропал бы копейкой. Правка оставляет след в
saas_admin_audit_log — кто, когда, с чего на что.

Канон схемы — db/schema_modules.sql раздел 23, журнал — запись v9.69.
Сторожа — app/tests/Feature/Obzvon/ObzvonTariffTest.php, все показаны красными.

План: docs/superpowers/plans/2026-08-04-obzvon-pod-klienta.md §З-1.3.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 13:42:34 +03:00
Дмитрий 30ca789507 feat,обзвон: место под звонок — таблица попыток и единственный итог по номеру
Задача З-1.1 плана «Обзвон под клиента». До этой правки фичи в портале не было
вовсе, и звонок было некуда положить: нечего списывать, нечего показать клиенту и
нечем ответить через год на вопрос «за что вы взяли с меня деньги в августе».

Заведены две таблицы, обе с защитой по клиентам ENABLE + FORCE:

- obzvon_calls — одна попытка набора. По номеру их бывает до двенадцати, и каждая
  своя строка со своим исходом. Держит два плеча врозь: робот с человеком и
  человек с менеджером после перевода, у каждого своя длительность и свой признак
  «состоялось». Держит два срока хранения и два следа удаления: звук месяц, текст
  три месяца, дальше живут сухие итоги.
- obzvon_number_results — единственный итог на всю работу с номером. Именно он
  стоит в карточке сделки, а не последняя попытка. Замок — UNIQUE по клиенту,
  кампании и номеру: двенадцать недозвонов дают ОДИН итог, а не двенадцать.

Расшифровка лежит в той же строке звонка. Отдельного хранилища под неё нет
намеренно — решение Р50 сняло бессрочную обезличенную расшифровку.

Имя своё, а не call_recordings из закомментированного чертежа: чертёж был про
телефонию вообще и не знает ни статуса обзвона, ни сроков со следом удаления, ни
«почему звонили», ни двух плеч, ни попыток с итогом. Чертёж не тронут.

Цена звонка запоминается в строке вместе со снимком тарифа, а не вычисляется из
действующего тарифа при показе: иначе смена тарифа задним числом перепишет
вчерашние счета. Образец — lead_charges.tier_no + price_per_lead_kopecks. Деньги
целыми копейками.

Строку звонка никто не удаляет: право DELETE не выдано ни одной роли. Вместе со
строкой ушли бы деньги, а спросить «за что вы взяли» клиент вправе и через год.

Канон — db/schema_modules.sql, новый раздел 23. Тело db/schema.sql таблиц не
получает: оно исполняется первой миграцией, и таблицы дельта-миграций в него
класть нельзя — это прямо запрещает сторож канона. В db/schema.sql добавлена
только пометка над закомментированным чертежом call_recordings: чертёж не
использован и не будет, модуль живёт в obzvon_*, и перечислено, чего чертёж не
знает. Чтобы следующая смена не воскресила его по недоразумению.

Журнал схемы — запись v9.68, номер свободен, столкновения нет.

Проверено: 21 сторож, в том числе изоляция клиентов живым запросом из-под роли
crm_app_user, а не глазами по миграции; служебная роль после перезапуска
db/03_service_bypass_policies.sql правит именно ту строку, а не «успешно ноль»;
откат миграции хвостов не оставляет; squawk с конфигом проекта чист; статанализ
по новым файлам ноль. Каждый сторож показан красным.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 11:01:19 +03:00
Дмитрий bffca85399 docs+fix: канон схемы догнал миграции — 39 таблиц описаны, сверка считает обе стороны
Канон знал 82 таблицы и 995 столбцов, а база — 121 и 1470: не описаны были
рекламный модуль, телеграм-рассылки и весь отдел продаж.

Главное, что выяснилось по дороге: db/schema.sql не документ, а исполняемый
файл — миграция 0001_01_01_000000_load_initial_schema заливает его целиком
первым шагом сборки. Перенести туда DDL модулей нельзя: те же таблицы создают
дельта-миграции, их 77 и 49 из них без стражей. Сборка с нуля падала дважды.
Поэтому канон разбит на два файла: schema.sql исполняется, новый
schema_modules.sql только описывает.

Проверено не глазами:
- сборка с нуля 148 из 148 DONE;
- столбцы 1470 против 1470, ноль расхождений по имени, типу, длине и NULL;
- все четыре счётчика расхождений инструмента — ноль;
- schema.sql отдельно исполняется без единой ошибки, ограничения 495=495,
  политики 64=64;
- статанализ 0.

Инструмент сверки усилен: считал только одну сторону и потому не видел
переименования jivo_chat_id в chat_id — старое имя жило в каноне месяц.
Теперь обе стороны, канон из нескольких файлов, понимание RENAME/DROP COLUMN
и DROP TABLE. Починен разбор переносов строки внутри определения столбца.

Сторож SchemaDeltaTest дополнен: файл модулей обязан быть на месте и описывать
39 таблиц, а тело schema.sql их не содержать. Принят вырезанием.

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 12:53:45 +03:00