§9.x forecast metrics filter objective_lots.district (informal micro-districts)
but /analyze resolves the official admin district from ekb_districts → admin
name matched 0 rows → silent empty forecast (market_metrics('Кировский')→0).
Add a curated micro→admin alias table seeding all 35 micro values to their
primary admin (8 EKB admins). Curated not spatial: complex_id join hits only
~28% of rows and complexes is multi-city, so ST_Contains scatters micros
uniformly. 4 cross-city values (Н.Тагил/Каменск) contained in one noted bucket.
Coverage: 8/8 admins resolve, 35/35 mapped, 0 unmapped. Idempotent + additive
(CREATE TABLE IF NOT EXISTS + INSERT ON CONFLICT DO NOTHING); applied to prod
+ registered in _schema_migrations. Resolver/fallback documented in header;
market_metrics wiring is Step 2 (separate PR). Refs #969#949.
After #129 fixed quarter_cad_number, the on-demand НСПД ingest of a real
not-pre-loaded parcel hit the NEXT cad_parcels NOT NULL column: source
(NotNullViolation) → geometry transaction rolled back → POST /analyze still
stuck. Repro 66:41:0610029:83 (job 57: quarter derived by #129, source null).
Migration 130: extend the #129 BEFORE INSERT/UPDATE trigger function to also
default source→'nspd' when NULL (column DEFAULT can't help — INSERT binds
explicit NULL; trigger overrides it). Explicit sources ("search"/"wms_grid_walk")
respected — only NULL is filled. cad_parcels NOT NULL surface now fully covered
(cad_num/quarter[#129]/category_id[literal]/source[#130]/fetched_at+updated_at[NOW]).
Dry-run-verified on prod: null quarter+source → 66:41:0610029 + nspd, INSERT ok.
PROD bug: on-demand НСПД ingest (bulk_harvest.upsert_parcel, for parcels NOT
pre-loaded) intermittently inserts quarter_cad_number=NULL at runtime →
NotNullViolation → geometry never persists → POST /analyze for a real new parcel
stuck "Геометрия загружается из НСПД…" forever (re-enqueues each call). Repro:
66:41:0610029:83 (real НСПД parcel; failing row had valid geom + props, only
quarter_cad_number null). Code derives quarter_cad but NULL still reaches the
INSERT via some runtime path (grid-walk).
Migration 129: BEFORE INSERT OR UPDATE trigger on cad_parcels — when
quarter_cad_number IS NULL and cad_num has >=3 colon-segments, derive it
(first 3 segments: 66:41:0610029:83 → 66:41:0610029). Fixes ALL write paths at
the DB level (quarter_cad_number is a pure derivation of cad_num). <3-segment
cad_num intentionally left NULL → NOT NULL still rejects genuine garbage (no
masking). Coexists with m.92 set_updated_at trigger. Idempotent.
Dry-run-verified on prod catalog: null-quarter insert → derived 66:41:0610029;
garbage <3seg still rejected. Verification = re-fetch 0610029:83 post-deploy.
REOPENED. L3 future-supply rows are computed per (district_name, dev_group_name)
but dev_group_name was never a key column — only embedded in method text. With
complex_id/obj_class NULL for L3, every dev_group of a district collapsed to one
upsert key → ~95.6% loss. Ground-truth (Академический, prod): should be 13,808
units / 15 dev_groups / 54 objects; only 1 row / 607 units survived.
Migration 128: ADD COLUMN supply_layers.dev_group_name TEXT + rebuild
uq_supply_layers_logical to (layer, district_name, complex_id, obj_class,
dev_group_name, source, snapshot_date) NULLS NOT DISTINCT (L1/L2 dev_group_name
NULL stays transparent → their dedup unchanged; L3 distinct groups no longer
collapse). Dry-run-verified vs prod catalog (applies clean, ROLLBACK clean).
Worker: SupplyLayerRow gains dev_group_name (L1/L2=None, L3=group); _UPSERT_SQL
adds it to INSERT/VALUES (CAST(:dev_group_name AS text)) + ON CONFLICT (key col,
not in DO UPDATE SET). Service+worker regression tests assert same-district/
different-dev_group → distinct keys (no collapse). 234 supply tests pass.
Deploy applies migration before container restart; collapsed data self-heals on
next supply_layers_refresh. Verification = prod re-measure post-deploy.
Refs #970
Migration 127: durable, versioned, re-openable persistence for Site Finder
analysis runs (the §22 API result + forecast/recommendation/report snapshot).
One row = one completed analysis of a parcel (frozen result jsonb =
SiteFinderReport.as_dict() #987, schema_version for versioned re-open).
Append-mostly (history/versioning — never overwrites prior runs → TABLE not MV).
Replaces the last_analysis_date=None placeholder (parcels.py:1026,1092):
"date of last analysis" + "current analysis of parcel" now come from
v_analysis_runs_latest (DISTINCT ON cad_num, max created_at, deterministic
id-DESC tiebreak — mirrors v_supply_layers_latest m.125).
created_by = X-Authenticated-User identity, nullable TEXT, NO FK (no users
table; precedent m.90/m.119; background/Celery runs have no author). result/
params/segment as JSONB (report shape evolves); district/confidence/status
denormalized out for filter/sort. status + confidence CHECK-guarded.
Idempotent (CREATE TABLE/INDEX IF NOT EXISTS + CREATE OR REPLACE VIEW).
Dry-run-verified vs prod catalog (DDL applies clean in rolled-back txn;
DISTINCT ON latest-wins confirmed). Foundation for #994 persist + #992
contract. Closes#993. Refs #961.
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.
Добавляет endpoint для приёма заявок на пилот (lead-gen).
INSERT в pilot_requests, response {id, created_at, status}.
Telegram-уведомление — TODO (creds не настроены, #307 SF-B3).
Migration: data/sql/118_pilot_requests.sql
Add _extract_obj_class_from_ai() with regex patterns for Комфорт/Бизнес/Премиум/Элит/Стандарт.
_norm_object() uses it as fallback; objClass from API still takes priority if ever returned.
Add backfill SQL 105 to retroactively populate existing rows from raw JSON payloads.
Coverage: ~1806/4548 rows will be filled; remainder NULL is expected (Нежилое/no AI desc).
- v_bucket_success_score HAVING >= 30 → >= 15 so sparse districts
(Кировский etc.) are no longer silently excluded from the block
- Backend: SUCCESS_REC_MIN_DEALS=15, SUCCESS_REC_STRONG_DEALS=30;
data_confidence='weak' (15-29 deals) or 'strong' (≥30) in response
- TS type ParcelSuccessRecommendation: add data_confidence literal
- SuccessRecommendationBlock: amber badge for weak, all hex → CSS tokens,
emoji star removed (ui-ux.md), empty-state text min 30→15
Audit 2026-05-17: objective_corpus_room_month содержал активные сделки
для Малевич (98/12m) и Ньютон парк (30/12m), но записей в
objective_complex_mapping не было → mv_layout_velocity пропускал оба
проекта в velocity calculations.
Добавлены маппинги:
- Малевич → domrf_obj_id 64701 (Строящиеся, 244кв, сдача 2027-09)
- Ньютон парк → domrf_obj_id 47390 (Сданные, 411кв, крупнейший corpus)
ON CONFLICT DO NOTHING — идемпотентно. REFRESH mv_layout_velocity
выполняется вручную post-deploy (runbooks/Refresh_MV_Layout_Velocity.md).
Closes (epic part) #271 item 4
Add SQL migration 100_user_weight_profiles_default_seed.sql with system
presets Эконом/Комфорт/Бизнес (user_id='__system__'). Migration is
idempotent via ON CONFLICT DO UPDATE.
Backend:
- weight_profiles.py: add SYSTEM_USER_ID constant + list_profiles_with_system()
- admin_weight_profiles.py: add include_system query param to GET list endpoint
Tests: 3 new tests covering include_system flag and service sentinel behaviour.
PR #222 deploy failed на 99_nspd_entities_denorm.sql:
ERROR: syntax error at or near "а"
LINE 2: '3-сегментный квартал последнего harvest'а. Используется...
Unescaped apostrophe в COMMENT string closes literal early.
Fix: harvest'а → harvest''а (SQL escape).
Других unescaped апострофов в SQL литералах нет (другие 3 — в SQL комментариях
'--' , безопасно).