fix(site-finder): align competitor flats_sold to objective_lots.contract_date (#1926) #1927

Merged
bot-backend merged 2 commits from fix/1926-units-sold-align into main 2026-06-26 18:33:52 +00:00
Collaborator

Root (audit #1871/#1926): competitor-блок считал flats_sold через domrf_kn_flats.status (LIKE '%прод%'/sold) — заполнен ~0.2% (99.8% NULL) → недосчёт ~1600 юнитов и расхождение с MarketMetrics, который считает через objective_lots.contract_date (100% для проданных).

Fix: новый _SOLD_COUNT_SQL считает sold через objective_lots.contract_date IS NOT NULL, мост domrf obj_id → objective_complex_mapping.domrf_obj_id → objective_complex_name == objective_lots.project_name (тот же 1:1 паттерн, что в _OBJECTIVE_PRICE_FALLBACK_SQL). Та же логика, что у MarketMetrics._STOCK_SQL → пути теперь согласованы. _AVG_PRICE_SQL стал price-only. psycopg v3 CAST. Без schema/codegen-изменений (flats_sold/sold_pct — существующие поля).

Tests: 217 passed / 4 skipped (DB-gated); + test_competitors_flats_sold_from_contract_date (120/200 → 60%); broader suite 1349 passed 0 failed. ruff clean.

Refs #1926, #1871

**Root (audit #1871/#1926):** competitor-блок считал `flats_sold` через `domrf_kn_flats.status` (`LIKE '%прод%'`/`sold`) — заполнен ~0.2% (99.8% NULL) → недосчёт ~1600 юнитов и расхождение с MarketMetrics, который считает через `objective_lots.contract_date` (100% для проданных). **Fix:** новый `_SOLD_COUNT_SQL` считает sold через `objective_lots.contract_date IS NOT NULL`, мост `domrf obj_id → objective_complex_mapping.domrf_obj_id → objective_complex_name == objective_lots.project_name` (тот же 1:1 паттерн, что в `_OBJECTIVE_PRICE_FALLBACK_SQL`). Та же логика, что у `MarketMetrics._STOCK_SQL` → пути теперь согласованы. `_AVG_PRICE_SQL` стал price-only. psycopg v3 CAST. Без schema/codegen-изменений (`flats_sold`/`sold_pct` — существующие поля). **Tests:** 217 passed / 4 skipped (DB-gated); + `test_competitors_flats_sold_from_contract_date` (120/200 → 60%); broader suite 1349 passed 0 failed. ruff clean. Refs #1926, #1871
bot-backend added 1 commit 2026-06-26 18:05:05 +00:00
fix(site-finder): align competitor flats_sold to objective_lots.contract_date (#1926)
All checks were successful
CI / changes (pull_request) Successful in 6s
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 1m50s
CI / backend-tests (pull_request) Successful in 11m17s
c336f91182
Audit #1871/#1926: competitor block computed flats_sold from
domrf_kn_flats.status (LIKE '%прод%'/'sold'), populated in only ~0.2% of
rows (99.8% NULL) -> massive undercount (~1600 units in reported case) and
divergence from MarketMetrics, which counts sold via objective_lots.contract_date
(100% populated for sold lots).

Fix: new _SOLD_COUNT_SQL counts sold lots from objective_lots.contract_date
IS NOT NULL, bridged domrf obj_id -> objective_complex_mapping.domrf_obj_id ->
objective_complex_mapping.objective_complex_name == objective_lots.project_name
(same 1:1 mapping pattern as _OBJECTIVE_PRICE_FALLBACK_SQL primary_price CTE).
This is the exact source MarketMetrics._STOCK_SQL uses, so the two paths agree.
Dropped the domrf_kn_flats.status count from _AVG_PRICE_SQL (price-only now).

No schema/Pydantic field change (flats_sold/sold_pct already existed) ->
codegen not needed. psycopg v3 CAST style preserved.

Tests: updated mock DB ordering (+ sold-count execute), added
test_competitors_flats_sold_from_contract_date (120/200 -> 60% sold) and
phantom-column EXPLAIN check for _SOLD_COUNT_SQL.
Light1YT added 1 commit 2026-06-26 18:22:06 +00:00
fix(site-finder): harden flats_sold sold-count SQL — fan-out, premise_kind, predicate (#1926)
All checks were successful
CI / changes (pull_request) Successful in 6s
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 1m51s
CI / backend-tests (pull_request) Successful in 11m6s
1a07af028a
Code-review #1927 found 2 MAJOR bugs in the initial fix:

1. JOIN fan-out: objective_complex_mapping UNIQUE is (objective_complex_name,
   objective_group), NOT domrf_obj_id (68_schema:321) — one obj_id can map to
   multiple rows (same name, different group), so COUNT(*) over cm JOIN ol
   double-counts lots (GROUP BY collapses output rows but not dups inside the
   aggregate). Fixed with COUNT(DISTINCT ol.objective_lot_id).

2. Missing premise_kind: MarketMetrics._STOCK_SQL filters premise_kind='квартира';
   the new SQL did not, so sold parking/storage/non-residential lots counted as
   sold flats (over-count). Added AND ol.premise_kind = :premise_kind.

3. Aligned the sold predicate to MarketMetrics exactly:
   is_sold IS TRUE OR contract_date IS NOT NULL OR LOWER(status)='продан'
   (Objective populates these inconsistently — OR mirrors _STOCK_SQL:285-289),
   not contract_date alone.

Tests: added static SQL-structure guards (fan-out-safe COUNT(DISTINCT),
premise_kind filter, full predicate) since mock-based API tests bypass real SQL
and cannot detect fan-out; the live-DB fan-out/EXPLAIN check lives in
test_phantom_columns (skipped without TEST_DATABASE_URL). Updated phantom EXPLAIN
to the new SQL.

Full suite: 3530 passed, 51 skipped, 0 failed. Ruff clean.
bot-backend merged commit 4846197d54 into main 2026-06-26 18:33:52 +00:00
bot-backend deleted branch fix/1926-units-sold-align 2026-06-26 18:33:52 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
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#1927
No description provided.