feat(22begh): schema extension — flats+6 / objects+30 / developers+4 / metro_top3

SQL migration 112: ADD COLUMN IF NOT EXISTS для trёх таблиц (idempotent).
Scraper: _norm_flat section_no, _norm_object metro/freeFlatsInfo/dev_group_name,
UPSERT SQL расширены с COALESCE ON CONFLICT для всех новых полей.
CAST(:metro_top3 AS jsonb) — canonical psycopg3 bind для jsonb.
Поля catalog scraper 22d проставлены NULL-заглушками для будущего COALESCE.

Closes #297
This commit is contained in:
lekss361 2026-05-17 18:19:32 +03:00
parent 16e3d5c86d
commit c67de26cd0
2 changed files with 377 additions and 8 deletions

View file

@ -186,15 +186,25 @@ def _extract_list(payload: dict[str, Any]) -> list[dict[str, Any]]:
def _norm_object(row: dict[str, Any], region_cd: int | None = None) -> dict[str, Any]:
"""Map naш.дом.рф /kn/object row → domrf_kn_objects column dict.
Real API field names:
Real API field names (confirmed via payload audit 2026-05-17, obj=65136):
objId, hobjId, developer{devId, shortName, fullName, groupName, companyGroup, devInn},
rpdRegionCd, objAddr, shortAddr, objCommercNm, objFloorMin, objFloorMax,
objElemLivingCnt, objSquareLiving, objReady100PercDt, objClass, latitude, longitude,
objProblemFlg, problemFlag, siteStatus, objGreenHouseFlg, objGuarantyEscrowFlg,
objStatus, aiDescription, freeFlatsInfo{priceMin, numberFlats}.
objStatus, aiDescription, freeFlatsInfo{priceMin, numberFlats},
residentialBuildings (section_count), rpdNum (project_declaration_num),
objPublDt (project_published_at), metro{name, time, line, color, colors, isWalk, id}.
Note: DOM.РФ /kn/object list endpoint never populates `objClass` in API responses
(field absent from payload). Class is extracted from `aiDescription` text as fallback.
22e fields NOT in kn-API list payload (remain NULL until catalog scraper 22d):
first_floor_type, elevators_passenger/cargo_count, parking_total_slots,
guest_parking_inside/outside_count, ceiling_height_m, finishing_variants_count,
has_free_planning, playground_kids/sport_count, has_bike_paths, trash_areas_count,
has_ramp, has_low_platforms, has_wheelchair_lift, flat_area_min/max,
price_max_rub, price_per_m2_min/max, parking_provision_pct, avg_flat_area_m2,
domrf_score_location/transport/infrastructure.
"""
dev = row.get("developer") if isinstance(row.get("developer"), dict) else {}
company_group = _g(dev, "companyGroup") if dev else None
@ -210,6 +220,34 @@ def _norm_object(row: dict[str, Any], region_cd: int | None = None) -> dict[str,
obj_id,
)
# Metro: kn-API returns a single metro object (nearest station).
# Stored as metro_top3 JSON array (single element) for future multi-station extension.
metro_raw = row.get("metro")
metro_nearest_name: str | None = None
metro_nearest_walk_minutes: int | None = None
metro_top3: list[dict[str, Any]] | None = None
if isinstance(metro_raw, dict) and metro_raw.get("name"):
metro_nearest_name = metro_raw.get("name")
raw_time = metro_raw.get("time")
if raw_time is not None:
try:
metro_nearest_walk_minutes = round(float(raw_time))
except (TypeError, ValueError):
pass
metro_top3 = [
{
"name": metro_raw.get("name"),
"time": raw_time,
"line": metro_raw.get("line"),
"color": metro_raw.get("color"),
"isWalk": metro_raw.get("isWalk"),
}
]
# freeFlatsInfo.priceMin — price of cheapest available flat in the object
free_info = row.get("freeFlatsInfo") if isinstance(row.get("freeFlatsInfo"), dict) else {}
price_min_rub = _g(free_info, "priceMin") if free_info else None
return {
"obj_id": _g(row, "objId", "obj_id", "id"),
"hobj_id": _g(row, "hobjId", "hobj_id"),
@ -235,6 +273,43 @@ def _norm_object(row: dict[str, Any], region_cd: int | None = None) -> dict[str,
"latitude": _g(row, "latitude"),
"longitude": _g(row, "longitude"),
"obj_status": _g(row, "objStatus"),
# 22e: fields mappable from kn-API list payload
"section_count": _g(row, "residentialBuildings"),
"project_declaration_num": _g(row, "rpdNum"),
"project_published_at": _to_date(_g(row, "objPublDt")),
"price_min_rub": price_min_rub,
"dev_group_name": _g(dev, "groupName"),
# 22e + 22h: metro (nearest station from kn-API, single object)
"metro_nearest_name": metro_nearest_name,
"metro_nearest_walk_minutes": metro_nearest_walk_minutes,
"metro_top3": json.dumps(metro_top3, ensure_ascii=False) if metro_top3 else None,
# 22e: fields NOT in kn-API list payload — filled by catalog scraper (22d)
"first_floor_type": None,
"elevators_passenger_count": None,
"elevators_cargo_count": None,
"parking_total_slots": None,
"guest_parking_inside_count": None,
"guest_parking_outside_count": None,
"ceiling_height_m": None,
"finishing_variants_count": None,
"has_free_planning": None,
"avg_flat_area_m2": None,
"playground_kids_count": None,
"playground_sport_count": None,
"has_bike_paths": None,
"trash_areas_count": None,
"has_ramp": None,
"has_low_platforms": None,
"has_wheelchair_lift": None,
"flat_area_min": None,
"flat_area_max": None,
"price_max_rub": None,
"price_per_m2_min": None,
"price_per_m2_max": None,
"parking_provision_pct": None,
"domrf_score_location": None,
"domrf_score_transport": None,
"domrf_score_infrastructure": None,
}
@ -308,6 +383,13 @@ def _norm_flat(row: dict[str, Any], region_cd: int | None) -> dict[str, Any]:
"city": _g(row, "city"),
"region_cd": region_cd,
"obj_name": _g(row, "objName") or _g(row.get("objInfo") or {}, "objCommercNm"),
# 22b: новые поля квартиры
"section_no": _g(row, "_entrance"), # entranceNumber injected by _flatten_table
"finishing_type": None, # заполнит catalog scraper 22d
"ceiling_height_m": None, # заполнит catalog scraper 22d
"key_handover_dt": None, # заполнит catalog scraper 22d
"catalog_updated_at": None, # заполнит catalog scraper 22d
"catalog_url_hash": None, # заполнит catalog scraper 22d
}
@ -321,14 +403,38 @@ UPSERT_OBJECT_SQL = text(
floor_min, floor_max, flat_count, square_living, ready_dt,
problem_flag, site_status, green_house, escrow,
obj_class, wall_type, energy_eff,
latitude, longitude, obj_status, snapshot_date
latitude, longitude, obj_status,
section_count, project_declaration_num, project_published_at,
price_min_rub, dev_group_name,
metro_nearest_name, metro_nearest_walk_minutes, metro_top3,
first_floor_type, elevators_passenger_count, elevators_cargo_count,
parking_total_slots, guest_parking_inside_count, guest_parking_outside_count,
ceiling_height_m, finishing_variants_count, has_free_planning, avg_flat_area_m2,
playground_kids_count, playground_sport_count, has_bike_paths, trash_areas_count,
has_ramp, has_low_platforms, has_wheelchair_lift,
flat_area_min, flat_area_max, price_max_rub, price_per_m2_min, price_per_m2_max,
parking_provision_pct, domrf_score_location, domrf_score_transport,
domrf_score_infrastructure,
snapshot_date
) VALUES (
:obj_id, :hobj_id, :comm_name, :addr, :short_addr, :region_cd,
:dev_id, :dev_name, :dev_inn,
:floor_min, :floor_max, :flat_count, :square_living, :ready_dt,
:problem_flag, :site_status, :green_house, :escrow,
:obj_class, :wall_type, :energy_eff,
:latitude, :longitude, :obj_status, :snapshot_date
:latitude, :longitude, :obj_status,
:section_count, :project_declaration_num, :project_published_at,
:price_min_rub, :dev_group_name,
:metro_nearest_name, :metro_nearest_walk_minutes, CAST(:metro_top3 AS jsonb),
:first_floor_type, :elevators_passenger_count, :elevators_cargo_count,
:parking_total_slots, :guest_parking_inside_count, :guest_parking_outside_count,
:ceiling_height_m, :finishing_variants_count, :has_free_planning, :avg_flat_area_m2,
:playground_kids_count, :playground_sport_count, :has_bike_paths, :trash_areas_count,
:has_ramp, :has_low_platforms, :has_wheelchair_lift,
:flat_area_min, :flat_area_max, :price_max_rub, :price_per_m2_min, :price_per_m2_max,
:parking_provision_pct, :domrf_score_location, :domrf_score_transport,
:domrf_score_infrastructure,
:snapshot_date
)
ON CONFLICT (obj_id, snapshot_date) DO UPDATE SET
comm_name = EXCLUDED.comm_name,
@ -345,7 +451,91 @@ UPSERT_OBJECT_SQL = text(
energy_eff = COALESCE(EXCLUDED.energy_eff, domrf_kn_objects.energy_eff),
latitude = EXCLUDED.latitude,
longitude = EXCLUDED.longitude,
obj_status = EXCLUDED.obj_status
obj_status = EXCLUDED.obj_status,
section_count = COALESCE(EXCLUDED.section_count, domrf_kn_objects.section_count),
project_declaration_num = COALESCE(
EXCLUDED.project_declaration_num, domrf_kn_objects.project_declaration_num
),
project_published_at = COALESCE(
EXCLUDED.project_published_at, domrf_kn_objects.project_published_at
),
price_min_rub = EXCLUDED.price_min_rub,
dev_group_name = COALESCE(EXCLUDED.dev_group_name, domrf_kn_objects.dev_group_name),
metro_nearest_name = COALESCE(
EXCLUDED.metro_nearest_name, domrf_kn_objects.metro_nearest_name
),
metro_nearest_walk_minutes = COALESCE(
EXCLUDED.metro_nearest_walk_minutes, domrf_kn_objects.metro_nearest_walk_minutes
),
metro_top3 = COALESCE(EXCLUDED.metro_top3, domrf_kn_objects.metro_top3),
first_floor_type = COALESCE(
EXCLUDED.first_floor_type, domrf_kn_objects.first_floor_type
),
elevators_passenger_count = COALESCE(
EXCLUDED.elevators_passenger_count, domrf_kn_objects.elevators_passenger_count
),
elevators_cargo_count = COALESCE(
EXCLUDED.elevators_cargo_count, domrf_kn_objects.elevators_cargo_count
),
parking_total_slots = COALESCE(
EXCLUDED.parking_total_slots, domrf_kn_objects.parking_total_slots
),
guest_parking_inside_count = COALESCE(
EXCLUDED.guest_parking_inside_count, domrf_kn_objects.guest_parking_inside_count
),
guest_parking_outside_count = COALESCE(
EXCLUDED.guest_parking_outside_count, domrf_kn_objects.guest_parking_outside_count
),
ceiling_height_m = COALESCE(
EXCLUDED.ceiling_height_m, domrf_kn_objects.ceiling_height_m
),
finishing_variants_count = COALESCE(
EXCLUDED.finishing_variants_count, domrf_kn_objects.finishing_variants_count
),
has_free_planning = COALESCE(
EXCLUDED.has_free_planning, domrf_kn_objects.has_free_planning
),
avg_flat_area_m2 = COALESCE(
EXCLUDED.avg_flat_area_m2, domrf_kn_objects.avg_flat_area_m2
),
playground_kids_count = COALESCE(
EXCLUDED.playground_kids_count, domrf_kn_objects.playground_kids_count
),
playground_sport_count = COALESCE(
EXCLUDED.playground_sport_count, domrf_kn_objects.playground_sport_count
),
has_bike_paths = COALESCE(EXCLUDED.has_bike_paths, domrf_kn_objects.has_bike_paths),
trash_areas_count = COALESCE(
EXCLUDED.trash_areas_count, domrf_kn_objects.trash_areas_count
),
has_ramp = COALESCE(EXCLUDED.has_ramp, domrf_kn_objects.has_ramp),
has_low_platforms = COALESCE(
EXCLUDED.has_low_platforms, domrf_kn_objects.has_low_platforms
),
has_wheelchair_lift = COALESCE(
EXCLUDED.has_wheelchair_lift, domrf_kn_objects.has_wheelchair_lift
),
flat_area_min = COALESCE(EXCLUDED.flat_area_min, domrf_kn_objects.flat_area_min),
flat_area_max = COALESCE(EXCLUDED.flat_area_max, domrf_kn_objects.flat_area_max),
price_max_rub = COALESCE(EXCLUDED.price_max_rub, domrf_kn_objects.price_max_rub),
price_per_m2_min = COALESCE(
EXCLUDED.price_per_m2_min, domrf_kn_objects.price_per_m2_min
),
price_per_m2_max = COALESCE(
EXCLUDED.price_per_m2_max, domrf_kn_objects.price_per_m2_max
),
parking_provision_pct = COALESCE(
EXCLUDED.parking_provision_pct, domrf_kn_objects.parking_provision_pct
),
domrf_score_location = COALESCE(
EXCLUDED.domrf_score_location, domrf_kn_objects.domrf_score_location
),
domrf_score_transport = COALESCE(
EXCLUDED.domrf_score_transport, domrf_kn_objects.domrf_score_transport
),
domrf_score_infrastructure = COALESCE(
EXCLUDED.domrf_score_infrastructure, domrf_kn_objects.domrf_score_infrastructure
)
"""
)
@ -354,11 +544,17 @@ UPSERT_FLAT_SQL = text(
INSERT INTO domrf_kn_flats (
id, ods_id, flat_type, flat_number, is_studio, total_area, living_area,
rooms, status, price_rub, price_per_m2, floor, num_floors, obj_id,
city, region_cd, obj_name, snapshot_date
city, region_cd, obj_name,
section_no, finishing_type, ceiling_height_m,
key_handover_dt, catalog_updated_at, catalog_url_hash,
snapshot_date
) VALUES (
:id, :ods_id, :flat_type, :flat_number, :is_studio, :total_area, :living_area,
:rooms, :status, :price_rub, :price_per_m2, :floor, :num_floors, :obj_id,
:city, :region_cd, :obj_name, :snapshot_date
:city, :region_cd, :obj_name,
:section_no, :finishing_type, :ceiling_height_m,
:key_handover_dt, :catalog_updated_at, :catalog_url_hash,
:snapshot_date
)
ON CONFLICT (id, snapshot_date) DO UPDATE SET
status = EXCLUDED.status,
@ -373,7 +569,19 @@ UPSERT_FLAT_SQL = text(
total_area = COALESCE(EXCLUDED.total_area, domrf_kn_flats.total_area),
rooms = COALESCE(EXCLUDED.rooms, domrf_kn_flats.rooms),
floor = COALESCE(EXCLUDED.floor, domrf_kn_flats.floor),
num_floors = COALESCE(EXCLUDED.num_floors, domrf_kn_flats.num_floors)
num_floors = COALESCE(EXCLUDED.num_floors, domrf_kn_flats.num_floors),
section_no = COALESCE(EXCLUDED.section_no, domrf_kn_flats.section_no),
finishing_type = COALESCE(EXCLUDED.finishing_type, domrf_kn_flats.finishing_type),
ceiling_height_m = COALESCE(
EXCLUDED.ceiling_height_m, domrf_kn_flats.ceiling_height_m
),
key_handover_dt = COALESCE(EXCLUDED.key_handover_dt, domrf_kn_flats.key_handover_dt),
catalog_updated_at = COALESCE(
EXCLUDED.catalog_updated_at, domrf_kn_flats.catalog_updated_at
),
catalog_url_hash = COALESCE(
EXCLUDED.catalog_url_hash, domrf_kn_flats.catalog_url_hash
)
"""
)

View file

@ -0,0 +1,161 @@
-- Migration 112: schema extension for issues #297 sub-tasks 22b + 22e + 22g + 22h
-- Adds 6 flat-level cols, 30 object-level cols (incl. metro_top3), 4 developer metrics.
--
-- Payload audit (obj=65136, 2026-05-17):
-- kn_api payload keys: objId, hobjId, objCommercNm, objAddr, shortAddr, rpdRegionCd,
-- developer{devId,devInn,orgForm,fullName,groupName,shortName,companyGroup},
-- rpdNum, objPublDt, objStatus, siteStatus, objFloorMax, objFloorMin, objPriceAVG,
-- problemFlag, rpdRegionCd, objSquareLiving, objElemLivingCnt, objGreenHouseFlg,
-- objGuarantyEscrowFlg, objReady100PercDt, hobjRenderPhotoUrl, aiDescription,
-- freeFlatsInfo{priceMin, numberFlats}, residentialBuildings,
-- metro{id, line, name, time, color, colors, isWalk}, latitude, longitude, pdId, buildType.
--
-- flat-level (portal/table): flatId, odsId, elemId, type, number, isStudio,
-- totalArea, livingArea, rooms, status, price, pricePerSquareMeter, numberFloors,
-- _objId(injected), _floor(injected), _entrance(injected as entranceNumber).
--
-- Fields that map NOW (from existing kn-API payload):
-- 22b: section_no (from _entrance/entranceNumber injected by _flatten_table)
-- 22e: section_count (residentialBuildings), project_declaration_num (rpdNum),
-- project_published_at (objPublDt), metro_nearest_name + metro_nearest_walk_minutes
-- (metro.name + metro.time), metro_top3 (metro wrapped as single-element array),
-- dev_group_name (developer.groupName), price_min_rub (freeFlatsInfo.priceMin)
--
-- Fields remaining NULL until catalog scraper 22d:
-- finishing_type, ceiling_height_m (flat), key_handover_dt, catalog_updated_at,
-- catalog_url_hash, first_floor_type, elevators_passenger_count, elevators_cargo_count,
-- parking_total_slots, guest_parking_inside_count, guest_parking_outside_count,
-- ceiling_height_m (obj), finishing_variants_count, has_free_planning,
-- playground_kids_count, playground_sport_count, has_bike_paths, trash_areas_count,
-- has_ramp, has_low_platforms, has_wheelchair_lift, flat_area_min, flat_area_max,
-- price_max_rub, price_per_m2_min, price_per_m2_max, parking_provision_pct,
-- avg_flat_area_m2, domrf_score_location, domrf_score_transport,
-- domrf_score_infrastructure, houses_building, houses_building_delayed,
-- houses_delivered, houses_delivered_delayed.
BEGIN;
-- ─────────────────────────────────────────────────────────────────────────────
-- 22b — domrf_kn_flats +6 columns
-- ─────────────────────────────────────────────────────────────────────────────
COMMENT ON TABLE domrf_kn_flats IS 'Per-flat catalogue from DOM.RF kn-API portal/table endpoint';
ALTER TABLE domrf_kn_flats
ADD COLUMN IF NOT EXISTS section_no int,
ADD COLUMN IF NOT EXISTS finishing_type text,
ADD COLUMN IF NOT EXISTS ceiling_height_m numeric(3,2),
ADD COLUMN IF NOT EXISTS key_handover_dt date,
ADD COLUMN IF NOT EXISTS catalog_updated_at date,
ADD COLUMN IF NOT EXISTS catalog_url_hash text;
COMMENT ON COLUMN domrf_kn_flats.section_no IS '22b: подъезд из entrance.entranceNumber (portal/table)';
COMMENT ON COLUMN domrf_kn_flats.finishing_type IS '22b/22d: тип отделки — заполнит catalog scraper (22d)';
COMMENT ON COLUMN domrf_kn_flats.ceiling_height_m IS '22b/22d: высота потолков — заполнит catalog scraper (22d)';
COMMENT ON COLUMN domrf_kn_flats.key_handover_dt IS '22b/22d: дата выдачи ключей — заполнит catalog scraper (22d)';
COMMENT ON COLUMN domrf_kn_flats.catalog_updated_at IS '22b/22d: дата обновления инфо на catalog-странице (22d)';
COMMENT ON COLUMN domrf_kn_flats.catalog_url_hash IS '22b/22d: hash-часть URL квартиры на каталоге dom.rf (22d)';
-- ─────────────────────────────────────────────────────────────────────────────
-- 22e + 22h — domrf_kn_objects +30 columns (incl. metro_top3)
-- ─────────────────────────────────────────────────────────────────────────────
COMMENT ON TABLE domrf_kn_objects IS 'Per-object (ЖК) catalogue from DOM.RF kn-API list endpoint';
-- Building specs (8)
ALTER TABLE domrf_kn_objects
ADD COLUMN IF NOT EXISTS first_floor_type text,
ADD COLUMN IF NOT EXISTS section_count int,
ADD COLUMN IF NOT EXISTS elevators_passenger_count int,
ADD COLUMN IF NOT EXISTS elevators_cargo_count int,
ADD COLUMN IF NOT EXISTS parking_total_slots int,
ADD COLUMN IF NOT EXISTS guest_parking_inside_count int,
ADD COLUMN IF NOT EXISTS guest_parking_outside_count int,
ADD COLUMN IF NOT EXISTS ceiling_height_m numeric(3,2);
-- Apartment summary (3)
ALTER TABLE domrf_kn_objects
ADD COLUMN IF NOT EXISTS finishing_variants_count int,
ADD COLUMN IF NOT EXISTS has_free_planning bool,
ADD COLUMN IF NOT EXISTS avg_flat_area_m2 numeric(5,1);
-- Yard infrastructure (4)
ALTER TABLE domrf_kn_objects
ADD COLUMN IF NOT EXISTS playground_kids_count int,
ADD COLUMN IF NOT EXISTS playground_sport_count int,
ADD COLUMN IF NOT EXISTS has_bike_paths bool,
ADD COLUMN IF NOT EXISTS trash_areas_count int;
-- Accessibility / OVZ (3)
ALTER TABLE domrf_kn_objects
ADD COLUMN IF NOT EXISTS has_ramp bool,
ADD COLUMN IF NOT EXISTS has_low_platforms bool,
ADD COLUMN IF NOT EXISTS has_wheelchair_lift bool;
-- Catalog / UI (9)
ALTER TABLE domrf_kn_objects
ADD COLUMN IF NOT EXISTS flat_area_min numeric(8,2),
ADD COLUMN IF NOT EXISTS flat_area_max numeric(8,2),
ADD COLUMN IF NOT EXISTS price_min_rub bigint,
ADD COLUMN IF NOT EXISTS price_max_rub bigint,
ADD COLUMN IF NOT EXISTS price_per_m2_min numeric(12,2),
ADD COLUMN IF NOT EXISTS price_per_m2_max numeric(12,2),
ADD COLUMN IF NOT EXISTS parking_provision_pct numeric(5,1),
ADD COLUMN IF NOT EXISTS project_published_at date,
ADD COLUMN IF NOT EXISTS project_declaration_num text;
-- Metro & scores (5) — 22h merged into 22e
ALTER TABLE domrf_kn_objects
ADD COLUMN IF NOT EXISTS metro_nearest_name text,
ADD COLUMN IF NOT EXISTS metro_nearest_walk_minutes int,
ADD COLUMN IF NOT EXISTS metro_top3 jsonb,
ADD COLUMN IF NOT EXISTS domrf_score_location int,
ADD COLUMN IF NOT EXISTS domrf_score_transport int,
ADD COLUMN IF NOT EXISTS domrf_score_infrastructure int;
-- Developer split (1)
ALTER TABLE domrf_kn_objects
ADD COLUMN IF NOT EXISTS dev_group_name text;
COMMENT ON COLUMN domrf_kn_objects.section_count
IS '22e: подъезды (residentialBuildings из kn-API)';
COMMENT ON COLUMN domrf_kn_objects.project_published_at
IS '22e: дата публикации проекта (objPublDt из kn-API)';
COMMENT ON COLUMN domrf_kn_objects.project_declaration_num
IS '22e: номер проектной декларации (rpdNum из kn-API)';
COMMENT ON COLUMN domrf_kn_objects.price_min_rub
IS '22e: минимальная цена (freeFlatsInfo.priceMin из kn-API)';
COMMENT ON COLUMN domrf_kn_objects.metro_nearest_name
IS '22h: ближайшая станция метро (metro.name из kn-API)';
COMMENT ON COLUMN domrf_kn_objects.metro_nearest_walk_minutes
IS '22h: время пешком до метро, мин (metro.time из kn-API, rounded)';
COMMENT ON COLUMN domrf_kn_objects.metro_top3
IS '22h: топ-3 ближайших станций [{name,time,line,color,isWalk}], сейчас 1 элемент';
COMMENT ON COLUMN domrf_kn_objects.dev_group_name
IS '22e: группа компаний (developer.groupName из kn-API)';
COMMENT ON COLUMN domrf_kn_objects.domrf_score_location
IS '22e: оценка расположения от DOM.RF (catalog 22d)';
COMMENT ON COLUMN domrf_kn_objects.domrf_score_transport
IS '22e: оценка транспорта от DOM.RF (catalog 22d)';
COMMENT ON COLUMN domrf_kn_objects.domrf_score_infrastructure
IS '22e: оценка инфраструктуры от DOM.RF (catalog 22d)';
-- ─────────────────────────────────────────────────────────────────────────────
-- 22g — domrf_developers +4 metrics
-- ─────────────────────────────────────────────────────────────────────────────
COMMENT ON TABLE domrf_developers IS 'Застройщики DOM.RF (companyGroup-level)';
ALTER TABLE domrf_developers
ADD COLUMN IF NOT EXISTS houses_building int,
ADD COLUMN IF NOT EXISTS houses_building_delayed int,
ADD COLUMN IF NOT EXISTS houses_delivered int,
ADD COLUMN IF NOT EXISTS houses_delivered_delayed int;
COMMENT ON COLUMN domrf_developers.houses_building
IS '22g: домов строится (из карточки застройщика, catalog 22d)';
COMMENT ON COLUMN domrf_developers.houses_building_delayed
IS '22g: строится с задержкой';
COMMENT ON COLUMN domrf_developers.houses_delivered
IS '22g: сдано домов';
COMMENT ON COLUMN domrf_developers.houses_delivered_delayed
IS '22g: сдано с задержкой';
COMMIT;