Commit Graph

4 Commits

Author SHA1 Message Date
Дмитрий dd894a5224 fix(реклама показы): заморозка тает при списании и снимается на всех 4 выходах кампании
Заморозка ставилась один раз при запуске на всю смету показов и не снималась
нигде — AdWalletService::release не вызывался ни одной строкой приложения.

Главное следствие было блокирующим: charge уменьшал balance_rub, но не трогал
frozen_rub, поэтому одни и те же рубли считались дважды. Свободный остаток
balance − frozen уходил в минус, а AdWalletGate::isSolvent вызывается сразу
после списания в ChargeCampaignSpendJob — клиент объявлялся неплатёжеспособным
после первого же суточного списания, и AdStopAll глушил все его кампании.
Кампания умерла бы после первого дня показов даже при полном кошельке.

Что сделано:
- charge уменьшает активный холд на списанную сумму, холд закрывается при нуле;
- release стал идемпотентным — отсутствие кошелька или холда больше не ошибка;
- выход 1 completed — CampaignImpressionCharger возвращает остаток резерва;
- выход 2 rejected — SyncCampaignModerationJob возвращает резерв целиком;
- выход 3 stopped_no_funds — PauseCampaignsOnAdStop снимает резерв;
- выход 4 paused — контроллер снимает резерв, resume морозит остаток сметы
  до обращения к Директу и отдаёт 409 с понятным текстом при нехватке денег.

Удаление кампании выходом не является — destroy разрешён только для черновика,
а черновик ещё не заморожен.

Решение по паузе согласовано с владельцем 27.07.2026: на паузе деньги свободны.

Тесты: рекламный модуль 156/156. Переписан сценарий одного существующего теста
ChargeCampaignSpendJobTest — нехватку денег теперь создаёт резерв ВТОРОЙ
кампании, так как прежняя постановка опиралась на двойной счёт и стала
недостижимой; проверяемое требование сохранено.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 09:44:12 +03:00
Дмитрий 3b55c96a85 feat(реклама показы): суточное списание по ПОКАЗАМ через счётчик показов вместо кликов
Задача 8a. Джоб ChargeCampaignSpendJob переключён с модели за клики и наценки
30% делением на модель за показы и наценку 40% вычитанием через готовый
CampaignImpressionCharger. YandexDirectClient.getCampaignSpend заменён на
getCampaignImpressions за всё время кампании ALL_TIME, счётчик сам считает
дельту от уже списанного и идемпотентен по external_key yandex-imp.

Всё за рубильником yandex_direct.enabled ВЫКЛ, на боевых деньгах ничего не
крутится. Реклама-модуль 190/190 зелёный. Отчёт показов помечен «проверить
при go-live» ReportName без даты, ALL_TIME, формат TSV.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-27 00:38:18 +03:00
Дмитрий 7e0b8e53aa fix(реклама): джобы Директа перечисляют кампании через pgsql_supplier (BYPASSRLS) + SET LOCAL для списаний — иначе на проде RLS даёт 0 строк (молчаливый сбой)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-25 00:07:45 +03:00
Дмитрий 620cc2eeb1 feat(реклама): ChargeCampaignSpendJob — суточное списание расхода Директа (Cost×1.3) 2026-07-24 23:27:50 +03:00