Commit graph

31 commits

Author SHA1 Message Date
e332052d0e feat(tradein): DaData /clean/address backfill для houses (PR Q3)
Migration 070 — добавляет houses.house_fias_id + dadata_qc_geo / qc_house /
enriched_at для tracking. Скрипт scripts/backfill_houses_dadata.py
проходит по ~3.8k rows с NULL coords + ~8.6k с NULL cadnum daily 100/day
(DaData demo quota → ~124 дня walltime для полного прохода).

Per-row SAVEPOINT, resume-safe (фильтр dadata_enriched_at IS NULL),
qc_geo IN (0,1) → overwrite coord-payload, иначе только tracking.
--priority coords|cadnum|both, --dry-run, --limit N (default 100).

20 тестов покрывают happy path qc_geo=0, qc_geo=3 city-level skip,
DaData None response, per-row exception isolation, --dry-run, --limit,
--priority filter, и missing-creds fail-fast.
2026-05-27 19:43:07 +05:00
a7fa1a4ffa feat(tradein): DaData on-demand enrichment в estimate flow (PR Q1)
Добавляет 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) — обе используют тот же сервис.
2026-05-27 18:50:12 +05:00
577ddc0654 chore(tradein): drop spurious v_street_sales_vs_listings (#598)
All checks were successful
Deploy Trade-In / changes (push) Successful in 11s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-backend (push) Successful in 33s
Deploy Trade-In / deploy (push) Successful in 33s
2026-05-27 12:23:18 +00:00
39da0f0644 feat(tradein): sales-vs-listings endpoint (PR K, foundation #564) (#597)
All checks were successful
Deploy Trade-In / changes (push) Successful in 45s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-backend (push) Successful in 1m38s
Deploy Trade-In / deploy (push) Successful in 45s
2026-05-27 11:55:44 +00:00
02ad9a1f58 feat(tradein): Phase 1 of #582 — address mismatch audit infrastructure
Stratified 200-house Yandex reverse-geocode audit comparing our DB's
(address, lat/lon) pair against what Yandex.Карты returns for those
coordinates. Лежит фундамент данных для Phases 2-5 (canonical = Yandex
per user decision).

What ships:
- Migration 066: address_mismatch_audit table + FDW foreign table
  gendesign_ekb_districts_geom для stratified sampling.
- Sampling SQL: 8 EKB districts × 25 houses = 200.
- Driver scripts/audit_address_mismatch.py с двумя режимами:
  API (если YANDEX_GEOCODER_API_KEY есть) и Playwright (CAPTCHA-aware,
  персистентный контекст). Resumable по --batch.
- Report SQL: p50/p75/p95 distance, top-20 outliers, per-district breakdown.
- 19 unit-тестов: normalize, distance bind, API parser, resume idempotency,
  captcha → status=blocked.

Devops note: после деплоя миграции — выдать tradein_fdw_reader SELECT на
gendesign.public.ekb_districts_geom перед первым запуском audit.

Refs #582 (Phase 1 of 5). Phase 3 backfill заблокирован до Yandex API
key (#402).
2026-05-25 12:50:30 +05:00
33c4c84467 feat(tradein): этаж/этажность optional + best test presets по deal count (#558)
All checks were successful
Deploy Trade-In / changes (push) Successful in 5s
Deploy Trade-In / build-backend (push) Successful in 44s
Deploy Trade-In / build-frontend (push) Successful in 1m53s
Deploy Trade-In / deploy (push) Successful in 44s
2026-05-24 21:44:44 +00:00
b0d377c984 feat(tradein): Phase C — Avito IMV per-house backfill (SQL + script) (#534)
All checks were successful
Deploy Trade-In / changes (push) Successful in 5s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-backend (push) Successful in 25s
Deploy Trade-In / deploy (push) Successful in 36s
2026-05-24 14:50:31 +00:00
c6ab419100 feat(tradein-sql): bootstrap houses + link 98.8% of listings (Phase A+B) (#527)
All checks were successful
Deploy Trade-In / changes (push) Successful in 6s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-backend (push) Successful in 41s
Deploy Trade-In / deploy (push) Successful in 37s
2026-05-24 14:17:32 +00:00
bad23095cf fix(tradein/cian): sanitize valuation cache writes (047 UNIQUE + 026/044 cols) (#523)
All checks were successful
Deploy Trade-In / changes (push) Successful in 5s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-backend (push) Successful in 41s
Deploy Trade-In / deploy (push) Successful in 33s
2026-05-24 13:44:39 +00:00
e45ff47aa6 fix(tradein-sql): remove duplicate CREATE INDEX from 050 (regression from PR #500) (#515)
All checks were successful
Deploy Trade-In / changes (push) Successful in 6s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-backend (push) Successful in 26s
Deploy Trade-In / deploy (push) Successful in 35s
2026-05-24 12:16:27 +00:00
8fc309d9ad fix(tradein-avito): strip Emotion CSS from listings.address (#502)
Some checks failed
Deploy Trade-In / build-backend (push) Blocked by required conditions
Deploy Trade-In / build-frontend (push) Blocked by required conditions
Deploy Trade-In / deploy (push) Blocked by required conditions
Deploy Trade-In / changes (push) Has been cancelled
2026-05-24 10:52:50 +00:00
02b48b8d04 fix(tradein-sql): rename dup 030, add trade_in_estimates geom, drop dup indexes (#500)
All checks were successful
Deploy Trade-In / changes (push) Successful in 5s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-backend (push) Successful in 23s
Deploy Trade-In / deploy (push) Successful in 32s
Close 3 SQL schema findings from 2026-05-24 trade-in audit.

- Rename 030_listings_alter_yandex.sql -> 033_*.sql (resolve dup prefix with 030_avito_imv_cache_key_unique.sql).
- New 034_trade_in_estimates_geom.sql: PostGIS geom column + GIST index + backfill + BEFORE INSERT/UPDATE trigger.
- New 035_drop_duplicate_indexes.sql: drop listing_sources_listing_idx2 (dup of 028) and houses_geom_idx2 (dup of 009).

All idempotent. Deploy script uses ls|sort + no tracking table — rename is safe because original 030 already applied (IF NOT EXISTS no-op on re-run as 033).
2026-05-24 10:04:40 +00:00
94cf199276 feat(tradein): postgres_fdw live read of gendesign.cad_buildings (replaces snapshot) (#493)
Some checks failed
Deploy Trade-In / changes (push) Successful in 5s
Deploy / changes (push) Successful in 5s
Deploy Trade-In / build-frontend (push) Successful in 26s
Deploy Trade-In / build-backend (push) Successful in 47s
Deploy / build-frontend (push) Successful in 29s
Deploy / build-backend (push) Successful in 1m24s
Deploy Trade-In / deploy (push) Successful in 40s
Deploy / build-worker (push) Successful in 2m57s
Deploy / deploy (push) Failing after 37s
Replaces tradein.cad_buildings snapshot with live postgres_fdw foreign table reading gendesign.v_tradein_cad_buildings. Fixes /trade-in/api/v1/geocode/reverse 500 (Nominatim ban) and address_not_geocoded for cadastre addresses (e.g. Хохрякова 81).

Security (deep-review fixes):
- 100_tradein_fdw_role.sql: passwordless CREATE ROLE; password set by deploy.yml ALTER ROLE bootstrap reading GENDESIGN_FDW_PASSWORD from backend/.env.runtime (via psql :'pw' var → format %L — injection-safe).
- core/fdw.py: regex whitelist [A-Za-z0-9_-]{32,256} on password, ValueError without echoing value, try/rollback on commit.
- 060_postgres_fdw_extension.sql: connect_timeout='3' on FOREIGN SERVER + ALTER ADD/SET fallback.
- geocoder.py: _cadastral_forward_sync / _cadastral_reverse_sync wrapped in asyncio.to_thread.
- 100_*.sql: REVOKE ALL ON ALL TABLES/SEQUENCES/FUNCTIONS IN SCHEMA public; only GRANT SELECT on v_tradein_cad_buildings.
- pg_user_mappings query handles PUBLIC mapping (usename IS NULL).

Tests: 3 SQL-injection guards on ensure_fdw_user_mapping + rewritten cadastral suite.
2026-05-24 08:57:30 +00:00
e79c9cec88 feat(tradein): global scraper delay setting (applies across all scrapers) (#485)
All checks were successful
Deploy Trade-In / changes (push) Successful in 5s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / deploy (push) Successful in 23s
Deploy Trade-In / build-backend (push) Successful in 50s
Add global request delay floor + per-source delay management.

- 054: seed 'global'=0 + per-source rows (avito=7, cian=5, n1=5, domrf=5, rosreestr=5)
  using ON CONFLICT DO NOTHING; reuses existing 053 table from #484
- scraper_settings.py: get_scraper_delay() = max(per_source, global); _GLOBAL_KEY,
  refactored _get_setting_cached(); preserved yandex umbrella alias logic
- admin.py: GET /scraper-settings + PUT /scraper-settings/{source} (ge=0.0, le=60.0);
  cache invalidation on update; CAST(:d AS numeric) per psycopg v3
- avito/cian/n1 scrapers wired (yandex was wired by #484)
- 18 unit tests pass (10 original updated for max() + 8 new global delay tests)

Closes follow-up gap from #486 (Cian admin slider min=0 now accepted by API).
2026-05-23 15:43:28 +00:00
b21d7c7c85 feat(tradein): scraper_settings live-config + Yandex admin trigger endpoints (#484)
All checks were successful
Deploy Trade-In / changes (push) Successful in 4s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-backend (push) Successful in 39s
Deploy Trade-In / deploy (push) Successful in 32s
2026-05-23 15:28:34 +00:00
0202720096 feat(tradein): in-app scheduler — UI-managed schedule (replaces SSH crontab) (#482)
Some checks failed
Deploy Trade-In / changes (push) Successful in 5s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-backend (push) Successful in 36s
Deploy Trade-In / deploy (push) Has been cancelled
2026-05-23 14:54:25 +00:00
2914ff2ef2 feat(tradein): city sweep — auto ЕКБ pipeline (#477)
All checks were successful
Deploy Trade-In / build-frontend (push) Successful in 23s
Deploy Trade-In / deploy (push) Successful in 22s
Deploy Trade-In / changes (push) Successful in 5s
Deploy Trade-In / build-backend (push) Successful in 38s
Stage 4d: full city sweep ЕКБ (5 anchors × N pages + enrichment + cooperative cancel).

- avito.py fetch_around(pages=N) — pagination backward compat default=1
- scrape_pipeline.run_avito_city_sweep — anchors loop + heartbeat + cancel check
- scrape_runs.py utility (create/heartbeat/done/failed/cancelled/list_recent)
- admin.py 3 endpoints: start (BackgroundTasks) / runs / cancel
- migration 051 ADD COLUMN IF NOT EXISTS + DROP/ADD CHECK constraint (text+CHECK, не enum — safe в transaction)
- deploy/avito-city-sweep.sh с random delay 0-3h (anti-pattern)
- 16 offline tests

Deep review approved: migration idempotent, cancel priority correct (mark_done guarded by status='running'), no SQL injection, psycopg v3 compliant.
2026-05-23 14:38:07 +00:00
e46b7d778e feat(tradein): search matview + indexes (Phase 3.1) (#469)
All checks were successful
Deploy Trade-In / changes (push) Successful in 5s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / deploy (push) Successful in 22s
Deploy Trade-In / build-backend (push) Successful in 43s
Multi-Source Integration Phase 3.1. Foundation for /api/v1/search p95 < 100ms on 100K+ listings.

SQL 050_search_optimization.sql:
- CREATE EXTENSION IF NOT EXISTS pg_trgm
- ALTER listings ADD COLUMN tsv tsvector GENERATED ALWAYS AS (...) STORED
- 10 indexes (geom GIST, composite, gin_trgm, GIN tsv, partial kadastr, ...)
- CREATE MATERIALIZED VIEW listings_search_mv (per-listing aggregated across sources)
- 5 matview indexes (UNIQUE on listing_id for REFRESH CONCURRENTLY)

Celery task tradein.refresh_search_matview:
- REFRESH MATERIALIZED VIEW CONCURRENTLY listings_search_mv (psycopg v3)
- Logged duration via logger.info
- Graceful try/except ImportError fallback (no celery_app.py in tradein-mvp yet)
- Beat schedule snippet in docstring for future wiring

Fixups (deep-code-reviewer 2026-05-23):
- h.ratings_count → h.reviews_count (alias house_ratings_count preserved)
- Strip +psycopg dialect prefix from DSN for libpq compat

Refs Master Plan sec 5.1 + 6.5 + 10.1.
2026-05-23 14:01:18 +00:00
5e90797a9f feat(tradein): SQL 040-046 — matching schema delta (Phase 1.1) (#464)
Some checks failed
Deploy Trade-In / deploy (push) Blocked by required conditions
Deploy Trade-In / changes (push) Successful in 4s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-backend (push) Has been cancelled
2026-05-23 13:30:22 +00:00
0db71033b4 fix(tradein): 030 — UNIQUE constraint on avito_imv_evaluations.cache_key (unblocks PR #452) (#454)
All checks were successful
Deploy Trade-In / changes (push) Successful in 4s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / deploy (push) Successful in 20s
Deploy Trade-In / build-backend (push) Successful in 21s
Hotfix unblocking PR #452 (Stage 3 IMV estimator integration).

Adds 030_avito_imv_cache_key_unique.sql: creates UNIQUE INDEX imv_cache_key_uniq_idx
on avito_imv_evaluations(cache_key), allowing save_imv_evaluation's ON CONFLICT (cache_key)
DO UPDATE to resolve. Migration 018 only created a non-unique composite index, which would
have caused runtime error on first IMV cache write.

DELETE-USING dedup guards against historical duplicates (no-op on fresh table).
BEGIN/COMMIT atomic; IF NOT EXISTS idempotent; CONCURRENTLY unnecessary (table empty).

Lexicographic apply order with PR #453's 030_listings_alter_yandex.sql is safe — different
tables, no functional interaction.

Reviewed by deep-code-reviewer: APPROVE.
2026-05-23 13:04:21 +00:00
f452b132c9 feat(tradein): SQL migrations 030-032 — Yandex schema (listings/houses/history) (#453)
All checks were successful
Deploy Trade-In / changes (push) Successful in 5s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-backend (push) Successful in 23s
Deploy Trade-In / deploy (push) Successful in 19s
2026-05-23 13:00:54 +00:00
7fad898c7f feat(tradein): SQL migration 029 — extend matching/valuation/dynamics tables (PR #445 follow-up) (#448)
All checks were successful
Deploy Trade-In / changes (push) Successful in 4s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / deploy (push) Successful in 19s
Deploy Trade-In / build-backend (push) Successful in 38s
2026-05-23 12:35:03 +00:00
lekss361
1ad2565af1 feat(tradein): SQL migrations 019-028 — Cian schema + matching + cookies
- 019: ALTER listings (windows_view_type, wcs counts, ceiling_height,
       repair_type, views, living_area, bedrooms, balconies, loggias,
       description_minhash, cadastral_number, phones, bargain_allowed,
       sale_type, house_source/ext_id, metro_stations)
- 020: ALTER houses (BTI: series_name, entrances, flat_count, is_emergency,
       heat/gas/overlap types; Cian: cian_internal_house_id,
       management_company_id, transport_accessibility_rate,
       advantages/banks/builders/houses_by_turn jsonb + GIN indexes)
- 021: CREATE management_companies + FK to houses.management_company_id
- 022: CREATE agents table + agent_id_fk on listings
- 023: CREATE offer_price_history (per-listing price change timeline)
- 024: CREATE houses_price_dynamics (monthly price/sqm series)
- 025: CREATE house_reliability_checks (наш.дом.рф checks)
- 026: CREATE external_valuations (unified Avito IMV + Cian Valuation cache)
- 027: CREATE EXTENSION pgcrypto + cian_session_cookies (encrypted)
- 028: CREATE house_sources, listing_sources, house_address_aliases
       + address_fingerprint/canonical/merged_into alters

Refs: decisions/CianScraper_v1_Implementation_Plan.md Stage 1
      decisions/Schema_Cian_SERP_Inventory.md sec 16, 20.3, 26
      decisions/Cross_Source_Matching_Strategy.md sec 2
2026-05-23 15:14:49 +03:00
a2147872ed feat(tradein): SQL migrations 009-018 — houses + listings extension + IMV cache (#440)
All checks were successful
Deploy Trade-In / build-backend (push) Successful in 23s
Deploy Trade-In / deploy (push) Successful in 27s
Deploy Trade-In / changes (push) Successful in 5s
Deploy Trade-In / build-frontend (push) Has been skipped
2026-05-23 11:41:44 +00:00
TradeIn Deploy
48ccbdafc8 feat(tradein): CRM-поля в форме оценки (#395)
Доп. поля для трейд-ин менеджера: тип собственности, ипотека/
обременение, имя и телефон клиента. На расчёт оценки не влияют —
сохраняются в trade_in_estimates вместе с записью.

- data/sql/008_crm_fields.sql — 4 колонки в trade_in_estimates.
- TradeInEstimateInput + estimator INSERT — приём и сохранение.
- EstimateForm — секция «CRM — для менеджера» (4 поля).

Closes #395
2026-05-22 11:15:15 +05:00
TradeIn Deploy
5413cce953 feat(tradein): backend загрузки фото квартиры (#394)
Фото квартиры прикрепляются к оценке, хранятся в Postgres (bytea) —
без отдельного файлового тома, уезжают вместе с бэкапом БД.

- data/sql/007_estimate_photos.sql — таблица estimate_photos
  (FK на trade_in_estimates, ON DELETE CASCADE).
- POST /estimate/{id}/photos — multipart-загрузка (image/*, ≤10 МБ,
  ≤12 фото на оценку).
- GET  /estimate/{id}/photos — список метаданных.
- GET  /estimate/{id}/photos/{photo_id} — отдать содержимое.
- python-multipart добавлен в зависимости (FastAPI UploadFile).

Фронтенд (file-drop в форме) — отдельным PR.
2026-05-22 11:05:35 +05:00
TradeIn Deploy
9cc8ab96f7 feat(tradein): кадастр зданий как источник house_metadata (#393)
В gendesign-БД есть cad_buildings — ~36к зданий ЕКБ с годом постройки
и этажностью (Росреестр). Подключаем как ПРИОРИТЕТНЫЙ источник
обогащения house_metadata, OSM Overpass остаётся fallback-ом.

- data/sql/006_cad_buildings.sql — таблица cad_buildings в tradein-БД.
- deploy/import-cadastre.sh — ETL: ЕКБ-срез cad_buildings из
  gendesign-postgres → tradein (по образцу import-rosreestr.sh).
- house_metadata._cad_buildings_get — lookup по гео-близости (60 м),
  проверяется ПЕРВЫМ в get_house_metadata, до OSM.

cad_buildings — кадастр ЗДАНИЙ, не квартир: поле ручного ввода кадастра
(исходная идея #393) не делаем — гео-обогащение полезнее и работает
автоматически для каждой оценки.

Closes #393
2026-05-22 10:54:42 +05:00
TradeIn Deploy
4ea35d5740 feat(tradein): geocode-missing для deals (#4 завершение)
geocode-missing получил target=listings|deals. deals.geocode_tried_at
добавлен в миграцию 005. cron геокодит и сделки Росреестра по улицам.
2026-05-21 20:04:19 +03:00
TradeIn Deploy
609744df91 fix(tradein): чанковый геокодинг — cron больше не таймаутится (#1)
geocode_tried_at: после каждой попытки NOW(), повтор только через 7 дней.
Эндпоинт чанкуется (limit=100), отдаёт remaining. cron вызывает в цикле.
2026-05-21 19:43:12 +03:00
TradeIn Deploy
fb49c56b81 fix(tradein): убрать TRUNCATE из seed — деплой больше не wipe-ит данные
КРИТИЧНЫЙ БАГ: 003_seed_listings.sql содержал TRUNCATE listings,deals,
а deploy-tradein.yml гоняет весь data/sql/*.sql на каждом деплое →
каждый деплой стирал реальные данные парсеров и подсовывал 120 синтетики.

Удалён 003_seed_listings.sql. Вместо него 003_seed_deals.sql —
идемпотентный (INSERT ... WHERE NOT EXISTS), сидит ТОЛЬКО сделки и
ТОЛЬКО когда таблица deals пуста. listings больше не трогается seed-ом.
2026-05-21 13:19:24 +03:00
lekss361
02267d0306 feat: add tradein-mvp subproject (Trade-In Estimator под /trade-in)
- FastAPI backend: PostGIS estimator + 3 scrapers (Avito/Cian/Yandex)
- Next.js 15 frontend: tradein.html mockup design, basePath=/trade-in
- WeasyPrint PDF (Брусника-style 4-page report)
- Address autocomplete с typo-tolerance + 6 EKB presets
- Изолированный docker stack gendesign-tradein (отдельная postgres БД)
- Caddy inline routes: gendsgn.ru/trade-in/* и /trade-in/api/v1/*
- Forgejo Actions: .forgejo/workflows/deploy-tradein.yml (shell-based GHCR login)
- Триггер только по paths: tradein-mvp/** (не пересекается с deploy.yml)
- Образы: ghcr.io/lekss361/gendesign-tradein-{backend,frontend}:latest

Первый запуск на сервере (вручную, один раз):
  - создать /opt/gendesign/tradein-mvp/.env.runtime (postgres pwd, contact email)
  - docker network create gendesign_shared (если нет)
  - docker compose -p gendesign-tradein up -d
  - docker compose -p gendesign exec caddy caddy reload
2026-05-21 00:25:39 +03:00