Веб-отчёт и лендинг с #3342 не показывают названия площадок (канон publicLabel в
frontend/src/lib/source-registry.ts), а клиентский PDF по тому же /estimate/{id}
печатал Avito / Циан / «Домклик · Сбер» / Я.Недвижимость / Этажи в пилюлях
источников, «подтверждают Росреестр, ДомКлик…» в советах и «на Циан, Авито,
Я.Недвижимости» в тарифах. Один клиент — два документа с разной нормой, и юр-риск,
ради которого всё делалось, в PDF оставался открытым.
- `_SOURCE_DISPLAY_NAMES` → публичные лейблы 1:1 с реестром фронта: avito → «Источник 1»,
cian → 2, yandex → 3, domklik → 4, etazhi → 5, rosreestr → «Росреестр»; fallback для
незнакомого id — «Другой источник», а не `source.title()` (сырой id — та же утечка).
- Алиасы (avito_imv, cian_valuation, yandex_valuation, domclick, etagi) канонизируются
ДО выбора цвета и лейбла (`_canonical_source`), а списки источников на страницах
объявлений и сделок дедуплицируются до среза [:5] (`_public_sources`): иначе
`sources_used` = listing ∪ valuation давал «Источник 1, Источник 1, Источник 2,
Источник 2, Источник 4» с серой точкой у алиасов и вытеснял yandex.
- Цвета пилюль не тронуты: цвет — опознаватель источника, как на вебе.
- Тексты: «Росреестр, сделки площадок и продажи агентств», «на основных площадках
объявлений» (двойник offer-rates.ts).
- Гейт `tests/test_pdf_public_source_labels.py`: видимый текст страниц (без тегов и
атрибутов, href на домены площадок законны) не содержит названий площадок и сырых
id, по одному кейсу на имя; дедуп алиасов; ветка совета с процентом.
Не тронуто: ссылки на объявления (avito.ru/domclick.ru) — отдельное решение;
`_QUALITY_SOURCE_SLOTS` — только счётчик, имена не рендерит.
Мина: purge_expired_trade_in_data (сейчас enabled=false) удаляет строки
WHERE expires_at < NOW() AND created_by IS NULL — это ровно популяция
будущих платящих физлиц (владелец продаёт отчёт за 150 руб., отчёт должен
жить год на нашей стороне, а не 24ч). Первый прогон после запуска продаж
безвозвратно снёс бы оплаченное.
Делается ДО платёжного кода, которого в этом PR нет:
- migration 234: колонка trade_in_estimates.retain_until (NULL = неоплачено,
бэкенд-бита-в-бит не меняется) + частичный индекс под purge-предикат.
- config.py: trade_in_paid_retention_days=365 (ENV) — единственный источник
"12 месяцев" для будущей оферты/экрана/SQL продления.
- Единый гейт чтения ESTIMATE_READABLE_SQL + estimate_readable() — раньше
SQL-фильтр (404) и Python-проверка (410) в trade_in.py уже разошлись по
тексту ответа; текст "estimate expired (24h TTL)" убран (стал бы ложью при
годовом хранении).
- purge_expired_trade_in_data: retain_until IS NULL (не < NOW() — оплаченное
не удаляем в принципе) + NOT EXISTS(payments) как независимая страховка +
pre-flight, который считает оплаченных кандидатов и падает в mark_failed
ДО первого батча при ненулевом результате.
- PDF: "Ссылка доступна до …" только при retain_until IS NOT NULL;
"ДЕЙСТВИТЕЛЕН ДО" (expires_at, актуальность расчёта) не тронут.
- Фронт: retain_until прокинут в mapper (validUntil остаётся на expires_at).
- privacy-страница: убрано устаревшее "механизма удаления нет" (неправда
после #2547), добавлен срок 12 месяцев для оплаченных отчётов.
Ни строчки платёжного кода. expires_at, trade_in_estimate_retention_hours,
_DELETE_EXPIRED_LEADS_SQL не тронуты.