fix(tradein/domclick): sale_type перестаёт быть третьим словарём (#2674) #2696

Merged
bot-backend merged 1 commit from fix/2674-sale-type-dialects into main 2026-08-06 06:11:46 +00:00
Collaborator

Замер прода 2026-08-06

SELECT source, sale_type, count(*) FROM listings GROUP BY 1,2:

source значения
avito free 2976 · assignment 46 · NULL 45020
cian free 14800 · fz214 4673 · alternative 1327 · dupt 409 · investment 64 · dzhsk 3 · pdkp 1 · NULL 463
domklik «Свободная продажа» 5546 · «Альтернатива» 553 · «Первичная продажа» 2 · NULL 493
n1 NULL 382 (колонку не пишет)
yandex NULL 16721 (колонку не пишет)

6101 русская строка против 24 299 английских. WHERE sale_type = 'free' не видит ни одной строки Домклика; GROUP BY sale_type разносит одну и ту же свободную продажу на две корзины.

Что аудит не досчитал: читателей у колонки НОЛЬ

Grep по всему репозиторию (2026-08-06) даёт только писателейproviders/{avito,cian,domclick}, scraper_kit/base.py, scripts/ingest_domclick_jsonl.py, scripts/domclick_local_runner.py — и DDL. Ни одного фильтра, агрегата, поля схемы API или элемента фронтенда. Единственное упоминание в app/ — комментарий в config.py, а v_data_quality тянет колонку транзитом через SELECT *.

Поэтому «канон» целиком здесь не вводится: выбирать между cian-токеном dupt и avito-токеном assignment (409 против 46 строк, оба = переуступка права требования) без единого читателя — монетка, и любой выбор потом придётся переигрывать под настоящего потребителя. Чинится ровно то, что сломано объективно: один писатель кладёт сырую русскую фразу там, где два других уже кладут английский токен.

Проверка смысла ДО маппинга (требование эпика)

cross-tab sale_type × listing_segment на проде — это одна ось, структура сделки:

  • domklik «Свободная продажа»/«Альтернатива» — 100% vtorichka, те же два полюса, что cian free (8545 vtorichka) / alternative (1185 vtorichka);
  • хвост cian fz214/dupt/investment/dzhsk/pdkp — 5089 строк из 5107 в novostroyki, т.е. ПОДмножество той же оси (какой договор на первичке), а не другой признак.

Конфликта смыслов нет → склейка free/alternative безопасна. Это и есть тот самый случай, где нормализация возможна; будь оси разными, вывод был бы обратный.

Что сделано

  1. canon_sale_type() в providers/domclick/detail.py — на единой границе записи (save_detail_enrichment), а не в парсере: тот же dataclass пишут три производителя (парсер карточки, ingest_domclick_jsonl.py, domclick_local_runner.py — именно он налил 6101 строку 2026-07-18). Незнакомое → None, как у avito._map_lower: COALESCE в UPDATE не затрёт существующее.
  2. Миграция 220 — backfill Свободная продажаfree (5546) + Альтернативаalternative (553) = 6099 строк, счётчики сняты на проде ДО применения. Идемпотентна (повторный прогон — 0 строк).
  3. COMMENT ON COLUMN listings.sale_type — фактический словарь по источникам, несведённая пара dupt/assignment и предупреждение сверяться с реальным GROUP BY перед фильтром.

«Первичная продажа» (2 строки) осознанно НЕ маппится: у cian первичка закодирована типом ДОГОВОРА (fz214/dzhsk/pdkp/dupt), а маркетинговая метка Домклика договор не называет — угадывать нельзя. Строки остаются как есть, факт зафиксирован в COMMENT.

Test plan

  • canon_sale_type — параметризованный тест на КАЖДОЕ фактическое значение прода (+ регистр/пробелы, неизвестное, пустое, None).
  • save_detail_enrichment пишет free, а не русскую фразу; для неотображаемого значения — None под защитой COALESCE.
  • tests/scrapers/test_domclick_detail.py + test_migrations_manifest.py зелёные (55 passed).
  • После деплоя: SELECT source, sale_type, count(*) FROM listings WHERE source='domklik' GROUP BY 1,2 → ожидается free 5546, alternative 553, «Первичная продажа» 2, русских Свободная продажа/Альтернатива — ноль.

Refs #2674

## Замер прода 2026-08-06 `SELECT source, sale_type, count(*) FROM listings GROUP BY 1,2`: | source | значения | |---|---| | avito | `free` 2976 · `assignment` 46 · NULL 45020 | | cian | `free` 14800 · `fz214` 4673 · `alternative` 1327 · `dupt` 409 · `investment` 64 · `dzhsk` 3 · `pdkp` 1 · NULL 463 | | domklik | «Свободная продажа» 5546 · «Альтернатива» 553 · «Первичная продажа» 2 · NULL 493 | | n1 | NULL 382 (колонку не пишет) | | yandex | NULL 16721 (колонку не пишет) | **6101 русская строка против 24 299 английских.** `WHERE sale_type = 'free'` не видит ни одной строки Домклика; `GROUP BY sale_type` разносит одну и ту же свободную продажу на две корзины. ## Что аудит не досчитал: читателей у колонки НОЛЬ Grep по всему репозиторию (2026-08-06) даёт только **писателей** — `providers/{avito,cian,domclick}`, `scraper_kit/base.py`, `scripts/ingest_domclick_jsonl.py`, `scripts/domclick_local_runner.py` — и DDL. Ни одного фильтра, агрегата, поля схемы API или элемента фронтенда. Единственное упоминание в `app/` — комментарий в `config.py`, а `v_data_quality` тянет колонку транзитом через `SELECT *`. Поэтому «канон» целиком здесь **не** вводится: выбирать между cian-токеном `dupt` и avito-токеном `assignment` (409 против 46 строк, оба = переуступка права требования) без единого читателя — монетка, и любой выбор потом придётся переигрывать под настоящего потребителя. Чинится ровно то, что сломано объективно: **один писатель кладёт сырую русскую фразу там, где два других уже кладут английский токен**. ## Проверка смысла ДО маппинга (требование эпика) cross-tab `sale_type × listing_segment` на проде — это одна ось, структура сделки: - domklik «Свободная продажа»/«Альтернатива» — 100% `vtorichka`, те же два полюса, что cian `free` (8545 vtorichka) / `alternative` (1185 vtorichka); - хвост cian `fz214`/`dupt`/`investment`/`dzhsk`/`pdkp` — 5089 строк из 5107 в `novostroyki`, т.е. ПОДмножество той же оси (какой договор на первичке), а не другой признак. Конфликта смыслов нет → склейка `free`/`alternative` безопасна. Это и есть тот самый случай, где нормализация возможна; будь оси разными, вывод был бы обратный. ## Что сделано 1. `canon_sale_type()` в `providers/domclick/detail.py` — на **единой границе записи** (`save_detail_enrichment`), а не в парсере: тот же dataclass пишут три производителя (парсер карточки, `ingest_domclick_jsonl.py`, `domclick_local_runner.py` — именно он налил 6101 строку 2026-07-18). Незнакомое → `None`, как у `avito._map_lower`: `COALESCE` в UPDATE не затрёт существующее. 2. Миграция **220** — backfill `Свободная продажа` → `free` (5546) + `Альтернатива` → `alternative` (553) = **6099 строк**, счётчики сняты на проде ДО применения. Идемпотентна (повторный прогон — 0 строк). 3. `COMMENT ON COLUMN listings.sale_type` — фактический словарь по источникам, несведённая пара `dupt`/`assignment` и предупреждение сверяться с реальным `GROUP BY` перед фильтром. **«Первичная продажа» (2 строки) осознанно НЕ маппится**: у cian первичка закодирована типом ДОГОВОРА (`fz214`/`dzhsk`/`pdkp`/`dupt`), а маркетинговая метка Домклика договор не называет — угадывать нельзя. Строки остаются как есть, факт зафиксирован в COMMENT. ## Test plan - [x] `canon_sale_type` — параметризованный тест на КАЖДОЕ фактическое значение прода (+ регистр/пробелы, неизвестное, пустое, None). - [x] `save_detail_enrichment` пишет `free`, а не русскую фразу; для неотображаемого значения — `None` под защитой `COALESCE`. - [x] `tests/scrapers/test_domclick_detail.py` + `test_migrations_manifest.py` зелёные (55 passed). - [ ] После деплоя: `SELECT source, sale_type, count(*) FROM listings WHERE source='domklik' GROUP BY 1,2` → ожидается `free` 5546, `alternative` 553, «Первичная продажа» 2, русских `Свободная продажа`/`Альтернатива` — ноль. Refs #2674
bot-backend added 1 commit 2026-08-06 05:48:12 +00:00
fix(tradein/domclick): sale_type перестаёт быть третьим словарём (#2674)
All checks were successful
CI / changes (pull_request) Successful in 9s
CI Trade-In / changes (pull_request) Successful in 9s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 3m0s
e22e708031
Замер прода 2026-08-06 (SELECT source, sale_type, count(*) FROM listings):
  avito   : free 2976 · assignment 46 · NULL 45020
  cian    : free 14800 · fz214 4673 · alternative 1327 · dupt 409 ·
            investment 64 · dzhsk 3 · pdkp 1 · NULL 463
  domklik : «Свободная продажа» 5546 · «Альтернатива» 553 ·
            «Первичная продажа» 2 · NULL 493
  n1/yandex: колонку не пишут вовсе (382 и 16721 NULL)
Итого 6101 русская строка против 24 299 английских: `WHERE sale_type='free'`
не видит ни одного Домклика, `GROUP BY sale_type` разносит одну и ту же
свободную продажу на две корзины.

ВАЖНОЕ ОГРАНИЧЕНИЕ, найденное по ходу: читателей у колонки НОЛЬ. Grep по
всему репозиторию даёт только писателей (providers/{avito,cian,domclick},
base.py, scripts/ingest_domclick_jsonl.py) и DDL — ни фильтра, ни агрегата,
ни поля схемы API, ни элемента фронтенда. Поэтому канон целиком тут НЕ
вводится: выбирать между cian-токеном dupt и avito-токеном assignment
(409 против 46 строк, оба = переуступка) без единого читателя — монетка.
Чинится ровно объективно сломанное: один писатель клал сырую русскую фразу
там, где два других уже кладут английский токен.

Проверка смысла перед маппингом (cross-tab sale_type × listing_segment):
это одна ось — структура сделки. Русские значения Домклика на 100%
vtorichka и совпадают с полюсами cian free/alternative; хвост cian
fz214/dupt/investment/dzhsk/pdkp — 5089 из 5107 строк novostroyki, т.е.
ПОДмножество той же оси (какой договор на первичке), а не другой признак.
Конфликта смыслов нет, склейка free/alternative безопасна.

canon_sale_type стоит на ЕДИНОЙ границе записи (save_detail_enrichment), а
не в парсере: в listings пишут три производителя одного dataclass'а — парсер
карточки, ingest_domclick_jsonl.py и domclick_local_runner.py (именно он
налил 6101 русскую строку 2026-07-18).

«Первичная продажа» (2 строки) осознанно НЕ маппится: у cian первичка
закодирована типом ДОГОВОРА, а маркетинговая метка Домклика договор не
называет — угадывать нельзя. Строки остаются как есть, факт зафиксирован в
COMMENT ON COLUMN вместе со всем фактическим словарём по источникам.

Миграция 220 (backfill 5546 + 553 = 6099 строк, счётчики сняты на проде ДО
применения; повторный прогон — 0 строк).

Refs #2674
bot-backend merged commit 58dcd76d2e into main 2026-08-06 06:11:46 +00:00
bot-backend deleted branch fix/2674-sale-type-dialects 2026-08-06 06:11:46 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#2696
No description provided.