af53dfed41
Запуск кампании стал возобновляемым: номер каждой созданной в Яндексе сущности пишется на кампанию сразу, повторный вызов переиспользует созданное и заводит объявления только для баннеров без номера. Номер группы записывается лишь после успешной привязки аудитории — инвариант «есть номер группы, значит аудитория на ней висит». Обрыв связи больше не оставляет кампанию-сироту в кабинете. Двойной заморозки денег не было и раньше — AdWalletService::freeze идемпотентен по активному холду; закрыто тестом. Денежный код не тронут, выходов снятия заморозки по-прежнему четыре. Два замка: launch отказывает из любого статуса кроме draft и queued — раньше повтор откатывал статус и launched_at; update отказывает в правке параметров показа, как только у кампании есть yandex_campaign_id — раньше клиент мог поменять смету, цену и адрес сайта у работающей кампании, и портал молча расходился с Яндексом. Название менять по-прежнему можно. Права: GRANT SELECT, UPDATE на ad_campaign_banners роли crm_admin_user — без него админ-экран увидел бы тихий ноль. GRANT USAGE, SELECT на нумераторы семи рекламных таблиц роли crm_app_user — bigserial без USAGE даёт отказ на бою, а на dev невидим из-за суперпользователя. Корневая причина в db/02_grants.sql — ALTER DEFAULT PRIVILEGES без FOR ROLE crm_migrator — вынесена отдельным вопросом к владельцу. Миграция 2026_07_27_100000 получила гард на существование колонок под прод-порядок выката migrate --pretend --force плюс ручной psql. Журнал схемы v9.04 и v9.05, формулировка проверки в v9.04 уточнена честно. 229 из 229 тестов рекламы зелёные, squawk чист. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>