feat(tradein): DaData on-demand enrichment в estimate flow (PR Q1) #604

Merged
Light1YT merged 1 commit from feat/tradein-dadata-enrichment into main 2026-05-27 13:56:14 +00:00
Owner

Summary

Интегрирует DaData /clean/address в estimate flow для on-demand enrichment целевого адреса — добавляет canonical-форму, kadastr_num (ДОМА), house_fias_id, ближайшие станции метро в response эстимейта.

Это foundation для будущего T0_per_house tier matching сделок Росреестра (см. PR M / #564 Phase 3): сейчас все rosreestr deals = T1_per_street потому что open dataset не отдаёт kadastr_num. С DaData enrichment у каждого estimate появляется canonical cadnum таргета — можно matchить deals по кадастру если они будут.

Изменения

File LOC Type
backend/app/services/dadata.py +214 new
backend/tests/services/test_dadata.py +364 new (20 unit-тестов)
backend/data/sql/069_trade_in_estimates_dadata_fields.sql +39 new migration
backend/app/services/estimator.py +33 mod (интеграция)
backend/app/schemas/trade_in.py +10 mod (AggregatedEstimate fields)
backend/app/core/config.py +8 mod (DADATA_API_TOKEN/SECRET)
.env.example +9 mod
DEPLOY.md +5 mod

8 файлов, +682 LOC.

Архитектура

  • clean_address(address)DadataAddressResult | None (slots-based frozen dataclass). Возвращает None gracefully при: отсутствии creds / коротком адресе / network fail / 4xx/5xx / пустом response / address-not-recognized.
  • Интеграция в estimator.estimate_quality: вызывается один раз после geocode(), перед house-metadata enrichment. Результат пишется в trade_in_estimates теми же 6 новыми nullable-колонками в основной INSERT (без отдельной транзакции).
  • Pydantic settings читают DADATA_API_TOKEN + DADATA_API_SECRET (auto env-var derivation).
  • Если хоть один из ENV пустой → service tихо disabled, estimator продолжает работу.

Quota

  • Demo tier: 100 req/день для /clean/address. Хватит на low-traffic prod (~1 estimate в час) и все наши тесты.
  • Suggestions API (10k/день free) будет использован в PR Q2 (frontend).
  • На 100+ /estimate в день DaData молча вернёт None, estimator продолжит без enrichment — никаких сбоев flow.

Тесты

tests/services/test_dadata.py — 20 passed in 0.12s

Покрыто:

  • happy path, отсутствие creds, короткий адрес
  • timeout / network error
  • HTTP 429 / 401 / 403 / 500 / 502 / 503
  • пустой массив, dict вместо list, non-dict первый элемент
  • partial payload (нет cadnum) → success с NULL
  • coerce строковых qc-кодов в int
  • проверка request body + headers

Zero real DaData API calls — все mocked через httpx.MockTransport. Существующие estimator-тесты (cian/cohort/floor/imv/yandex 46 тестов) ⇒ без регрессий.

Migration

069 — добавляет 6 nullable columns в trade_in_estimates (canonical_address, house_cadnum, house_fias_id, dadata_qc_geo, dadata_qc_house, dadata_metro jsonb). Идемпотентна (IF NOT EXISTS).

ENV

DADATA_API_TOKEN=<token>
DADATA_API_SECRET=<secret>

Хранится в vault meta/00_credentials.md. Перед merge нужно добавить на VPS:

ssh gendesign 'cat >> /opt/gendesign/backend/.env.runtime <<EOF
DADATA_API_TOKEN=...
DADATA_API_SECRET=...
EOF
cd /opt/gendesign && docker compose -p gendesign -f docker-compose.prod.yml restart tradein-backend'

Follow-ups

  • PR Q2 (frontend): Yandex Suggest → DaData Suggestions (10k/день free)
  • PR Q3 (backfill): 339 NULL-coords houses через DaData /clean (4 дня walltime на demo)
  • Caching enrichment в geocode_cache или отдельной таблице (90-day TTL) — экономия quota
  • Использование canonical_address в _fetch_deals / _fetch_analogs ILIKE-фильтрах вместо raw payload.address
  • T0_per_house tier matching когда / если ЕГРН прямой enrichment станет доступен
## Summary Интегрирует **DaData /clean/address** в estimate flow для on-demand enrichment целевого адреса — добавляет canonical-форму, `kadastr_num` (ДОМА), `house_fias_id`, ближайшие станции метро в response эстимейта. Это **foundation** для будущего T0_per_house tier matching сделок Росреестра (см. PR M / #564 Phase 3): сейчас все rosreestr deals = T1_per_street потому что open dataset не отдаёт `kadastr_num`. С DaData enrichment у каждого estimate появляется canonical cadnum таргета — можно matchить deals по кадастру если они будут. ## Изменения | File | LOC | Type | |---|---|---| | `backend/app/services/dadata.py` | +214 | new | | `backend/tests/services/test_dadata.py` | +364 | new (20 unit-тестов) | | `backend/data/sql/069_trade_in_estimates_dadata_fields.sql` | +39 | new migration | | `backend/app/services/estimator.py` | +33 | mod (интеграция) | | `backend/app/schemas/trade_in.py` | +10 | mod (AggregatedEstimate fields) | | `backend/app/core/config.py` | +8 | mod (DADATA_API_TOKEN/SECRET) | | `.env.example` | +9 | mod | | `DEPLOY.md` | +5 | mod | **8 файлов, +682 LOC.** ## Архитектура - `clean_address(address)` → `DadataAddressResult | None` (slots-based frozen dataclass). Возвращает `None` gracefully при: отсутствии creds / коротком адресе / network fail / 4xx/5xx / пустом response / address-not-recognized. - Интеграция в `estimator.estimate_quality`: вызывается один раз после `geocode()`, перед house-metadata enrichment. Результат пишется в `trade_in_estimates` теми же 6 новыми nullable-колонками в основной INSERT (без отдельной транзакции). - Pydantic settings читают `DADATA_API_TOKEN` + `DADATA_API_SECRET` (auto env-var derivation). - Если хоть один из ENV пустой → service tихо disabled, estimator продолжает работу. ## Quota - **Demo tier: 100 req/день** для `/clean/address`. Хватит на low-traffic prod (~1 estimate в час) и все наши тесты. - Suggestions API (10k/день free) будет использован в PR Q2 (frontend). - На 100+ /estimate в день DaData молча вернёт `None`, estimator продолжит без enrichment — никаких сбоев flow. ## Тесты ``` tests/services/test_dadata.py — 20 passed in 0.12s ``` Покрыто: - happy path, отсутствие creds, короткий адрес - timeout / network error - HTTP 429 / 401 / 403 / 500 / 502 / 503 - пустой массив, dict вместо list, non-dict первый элемент - partial payload (нет cadnum) → success с NULL - coerce строковых qc-кодов в int - проверка request body + headers **Zero real DaData API calls** — все mocked через `httpx.MockTransport`. Существующие estimator-тесты (cian/cohort/floor/imv/yandex 46 тестов) ⇒ без регрессий. ## Migration 069 — добавляет 6 nullable columns в `trade_in_estimates` (`canonical_address`, `house_cadnum`, `house_fias_id`, `dadata_qc_geo`, `dadata_qc_house`, `dadata_metro` jsonb). Идемпотентна (`IF NOT EXISTS`). ## ENV ``` DADATA_API_TOKEN=<token> DADATA_API_SECRET=<secret> ``` Хранится в vault `meta/00_credentials.md`. Перед merge нужно добавить на VPS: ```bash ssh gendesign 'cat >> /opt/gendesign/backend/.env.runtime <<EOF DADATA_API_TOKEN=... DADATA_API_SECRET=... EOF cd /opt/gendesign && docker compose -p gendesign -f docker-compose.prod.yml restart tradein-backend' ``` ## Follow-ups - **PR Q2** (frontend): Yandex Suggest → DaData Suggestions (10k/день free) - **PR Q3** (backfill): 339 NULL-coords houses через DaData /clean (4 дня walltime на demo) - Caching enrichment в `geocode_cache` или отдельной таблице (90-day TTL) — экономия quota - Использование canonical_address в `_fetch_deals` / `_fetch_analogs` ILIKE-фильтрах вместо raw payload.address - T0_per_house tier matching когда / если ЕГРН прямой enrichment станет доступен
Light1YT added 1 commit 2026-05-27 13:52:51 +00:00
Добавляет app/services/dadata.py — async client для DaData /clean/address,
обогащающий target адрес canonical-формой, kadastr_num, ФИАС, координатами
и ближайшим метро. Migration 069 расширяет trade_in_estimates 6 nullable
колонками; estimator.estimate_quality вызывает service после geocode и
сохраняет результат в основной INSERT. AggregatedEstimate отдаёт
canonical_address / house_cadnum / house_fias_id / metro_nearest наружу.

Service graceful: пустые credentials / quota 429 / 5xx / network fail → None,
estimator продолжает работу. Demo tier 100 req/день — хватит для low-traffic
prod и тестов. ENV: DADATA_API_TOKEN, DADATA_API_SECRET.

Tests: 20 unit-тестов через httpx.MockTransport, zero real DaData calls.
Покрыто: happy path, отсутствие creds, короткий адрес, timeout/network,
HTTP 429/401/403/500/502/503, пустой массив, malformed shape, partial
payload, coerce строковых qc-кодов в int, проверка request body+headers.

Закладывает базу для PR Q2 (frontend DaData Suggest, 10k/день free) и
PR Q3 (backfill 339 NULL-coords houses) — обе используют тот же сервис.
Light1YT merged commit 0b31431b99 into main 2026-05-27 13:56:14 +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#604
No description provided.