Compare commits
No commits in common. "7c715c9249a7342953a0c1799702b8167da3db92" and "e716252081f1c142988fa18b5fde028d29fa3c59" have entirely different histories.
7c715c9249
...
e716252081
7 changed files with 2 additions and 136 deletions
|
|
@ -75,9 +75,7 @@ def get_estimate(
|
||||||
analogs, actual_deals, sources_used, data_freshness_minutes,
|
analogs, actual_deals, sources_used, data_freshness_minutes,
|
||||||
expires_at, address, lat, lon,
|
expires_at, address, lat, lon,
|
||||||
area_m2, rooms, floor, total_floors,
|
area_m2, rooms, floor, total_floors,
|
||||||
year_built, house_type, repair_state, has_balcony,
|
year_built, house_type, repair_state, has_balcony
|
||||||
canonical_address, house_cadnum, house_fias_id,
|
|
||||||
dadata_qc_geo, dadata_metro
|
|
||||||
FROM trade_in_estimates
|
FROM trade_in_estimates
|
||||||
WHERE id = CAST(:id AS uuid)
|
WHERE id = CAST(:id AS uuid)
|
||||||
AND expires_at > NOW()
|
AND expires_at > NOW()
|
||||||
|
|
@ -89,17 +87,12 @@ def get_estimate(
|
||||||
if row is None:
|
if row is None:
|
||||||
raise HTTPException(status_code=404, detail="estimate not found or expired")
|
raise HTTPException(status_code=404, detail="estimate not found or expired")
|
||||||
|
|
||||||
from app.services.estimator import _qc_geo_to_precision
|
|
||||||
|
|
||||||
analogs = [AnalogLot(**a) for a in (row.analogs or [])]
|
analogs = [AnalogLot(**a) for a in (row.analogs or [])]
|
||||||
actual_deals = [AnalogLot(**a) for a in (row.actual_deals or [])]
|
actual_deals = [AnalogLot(**a) for a in (row.actual_deals or [])]
|
||||||
|
|
||||||
# ВАЖНО: возвращаем ПОЛНЫЙ набор полей. Раньше эндпоинт отдавал огрызок
|
# ВАЖНО: возвращаем ПОЛНЫЙ набор полей. Раньше эндпоинт отдавал огрызок
|
||||||
# без sources_used / confidence_explanation / координат — и при открытии
|
# без sources_used / confidence_explanation / координат — и при открытии
|
||||||
# оценки по ссылке (?id=) карточка источников пустела до «0/7».
|
# оценки по ссылке (?id=) карточка источников пустела до «0/7».
|
||||||
# DaData-обогащёнка (canonical/cadnum/fias/precision/metro) тоже
|
|
||||||
# rehydrate'ится из row — иначе бейдж точности + метро не показывались
|
|
||||||
# на shared-link reopen / в PDF.
|
|
||||||
return AggregatedEstimate(
|
return AggregatedEstimate(
|
||||||
estimate_id=row.id,
|
estimate_id=row.id,
|
||||||
median_price_rub=row.median_price,
|
median_price_rub=row.median_price,
|
||||||
|
|
@ -126,11 +119,6 @@ def get_estimate(
|
||||||
house_type=row.house_type,
|
house_type=row.house_type,
|
||||||
repair_state=row.repair_state,
|
repair_state=row.repair_state,
|
||||||
has_balcony=row.has_balcony,
|
has_balcony=row.has_balcony,
|
||||||
canonical_address=row.canonical_address,
|
|
||||||
house_cadnum=row.house_cadnum,
|
|
||||||
house_fias_id=row.house_fias_id,
|
|
||||||
address_precision=_qc_geo_to_precision(row.dadata_qc_geo),
|
|
||||||
metro_nearest=(row.dadata_metro or []),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -154,9 +142,7 @@ def estimate_pdf(
|
||||||
analogs, actual_deals, sources_used, data_freshness_minutes,
|
analogs, actual_deals, sources_used, data_freshness_minutes,
|
||||||
expires_at,
|
expires_at,
|
||||||
address, lat, lon, area_m2, rooms, floor, total_floors,
|
address, lat, lon, area_m2, rooms, floor, total_floors,
|
||||||
year_built, house_type, repair_state, has_balcony,
|
year_built, house_type, repair_state, has_balcony
|
||||||
canonical_address, house_cadnum, house_fias_id,
|
|
||||||
dadata_qc_geo, dadata_metro
|
|
||||||
FROM trade_in_estimates
|
FROM trade_in_estimates
|
||||||
WHERE id = CAST(:id AS uuid)
|
WHERE id = CAST(:id AS uuid)
|
||||||
"""
|
"""
|
||||||
|
|
@ -170,8 +156,6 @@ def estimate_pdf(
|
||||||
if row.expires_at.replace(tzinfo=UTC) < datetime.now(tz=UTC):
|
if row.expires_at.replace(tzinfo=UTC) < datetime.now(tz=UTC):
|
||||||
raise HTTPException(status_code=410, detail="estimate expired (24h TTL)")
|
raise HTTPException(status_code=410, detail="estimate expired (24h TTL)")
|
||||||
|
|
||||||
from app.services.estimator import _qc_geo_to_precision
|
|
||||||
|
|
||||||
analogs = [AnalogLot(**a) for a in (row.analogs or [])]
|
analogs = [AnalogLot(**a) for a in (row.analogs or [])]
|
||||||
actual_deals = [AnalogLot(**a) for a in (row.actual_deals or [])]
|
actual_deals = [AnalogLot(**a) for a in (row.actual_deals or [])]
|
||||||
|
|
||||||
|
|
@ -193,11 +177,6 @@ def estimate_pdf(
|
||||||
target_lon=row.lon,
|
target_lon=row.lon,
|
||||||
sources_used=row.sources_used or [],
|
sources_used=row.sources_used or [],
|
||||||
data_freshness_minutes=row.data_freshness_minutes,
|
data_freshness_minutes=row.data_freshness_minutes,
|
||||||
canonical_address=row.canonical_address,
|
|
||||||
house_cadnum=row.house_cadnum,
|
|
||||||
house_fias_id=row.house_fias_id,
|
|
||||||
address_precision=_qc_geo_to_precision(row.dadata_qc_geo),
|
|
||||||
metro_nearest=(row.dadata_metro or []),
|
|
||||||
)
|
)
|
||||||
input_snapshot = {
|
input_snapshot = {
|
||||||
"address": row.address,
|
"address": row.address,
|
||||||
|
|
|
||||||
|
|
@ -104,11 +104,6 @@ class AggregatedEstimate(BaseModel):
|
||||||
house_cadnum: str | None = None
|
house_cadnum: str | None = None
|
||||||
house_fias_id: str | None = None
|
house_fias_id: str | None = None
|
||||||
metro_nearest: list[dict] = Field(default_factory=list)
|
metro_nearest: list[dict] = Field(default_factory=list)
|
||||||
# address_precision — точность гео-привязки адреса (из DaData qc_geo):
|
|
||||||
# «house» (qc_geo=0, дом точно), «street» (qc_geo=1, до улицы),
|
|
||||||
# «approximate» (qc_geo≥2: населённый пункт/город/регион/не распознан).
|
|
||||||
# None если DaData не отрабатывала (адрес не геокодирован) / credentials не заданы.
|
|
||||||
address_precision: Literal["house", "street", "approximate"] | None = None
|
|
||||||
# ── Параметры оценённой квартиры — нужны, чтобы восстановить карточку
|
# ── Параметры оценённой квартиры — нужны, чтобы восстановить карточку
|
||||||
# при открытии оценки по ссылке (?id=), когда формы-инпута уже нет ──
|
# при открытии оценки по ссылке (?id=), когда формы-инпута уже нет ──
|
||||||
area_m2: float | None = None
|
area_m2: float | None = None
|
||||||
|
|
|
||||||
|
|
@ -1071,21 +1071,9 @@ async def estimate_quality(payload: TradeInEstimateInput, db: Session) -> Aggreg
|
||||||
house_cadnum=dadata.house_cadnum if dadata else None,
|
house_cadnum=dadata.house_cadnum if dadata else None,
|
||||||
house_fias_id=dadata.house_fias_id if dadata else None,
|
house_fias_id=dadata.house_fias_id if dadata else None,
|
||||||
metro_nearest=(dadata.metro if dadata and dadata.metro else []),
|
metro_nearest=(dadata.metro if dadata and dadata.metro else []),
|
||||||
address_precision=_qc_geo_to_precision(dadata.qc_geo if dadata else None),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _qc_geo_to_precision(qc_geo: int | None) -> str | None:
|
|
||||||
# DaData qc_geo: 0=exact(house), 1=street, 2=settlement, 3=city, 4=region, 5=unknown
|
|
||||||
if qc_geo is None:
|
|
||||||
return None
|
|
||||||
if qc_geo == 0:
|
|
||||||
return "house"
|
|
||||||
if qc_geo == 1:
|
|
||||||
return "street"
|
|
||||||
return "approximate"
|
|
||||||
|
|
||||||
|
|
||||||
def _estimate_days_on_market(
|
def _estimate_days_on_market(
|
||||||
listings: list[dict[str, Any]], deals: list[dict[str, Any]]
|
listings: list[dict[str, Any]], deals: list[dict[str, Any]]
|
||||||
) -> int | None:
|
) -> int | None:
|
||||||
|
|
@ -1965,6 +1953,4 @@ def _empty_estimate(
|
||||||
actual_deals=[],
|
actual_deals=[],
|
||||||
expires_at=expires_at,
|
expires_at=expires_at,
|
||||||
cian_valuation=None,
|
cian_valuation=None,
|
||||||
# Адрес не геокодирован (DaData не отрабатывала) → точность неизвестна.
|
|
||||||
address_precision=None,
|
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
"""Unit tests for `app.services.estimator._qc_geo_to_precision`.
|
|
||||||
|
|
||||||
Pure-function mapping DaData qc_geo → address-precision indicator:
|
|
||||||
- 0 → "house" (дом распознан точно)
|
|
||||||
- 1 → "street" (до улицы)
|
|
||||||
- 2/3/4/5 → "approximate" (населённый пункт / город / регион / не распознан)
|
|
||||||
- None → None (DaData не отрабатывала)
|
|
||||||
|
|
||||||
Чистый unit-тест: без БД и сети.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
from unittest.mock import MagicMock
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
# DATABASE_URL required by config before any app import (см. test_dadata.py).
|
|
||||||
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost:5432/test")
|
|
||||||
|
|
||||||
# WeasyPrint stub — not installed in CI без GTK (consistent с test_dadata.py).
|
|
||||||
_wp_mock = MagicMock()
|
|
||||||
sys.modules.setdefault("weasyprint", _wp_mock)
|
|
||||||
|
|
||||||
from app.services.estimator import _qc_geo_to_precision # noqa: E402
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
("qc_geo", "expected"),
|
|
||||||
[
|
|
||||||
(0, "house"),
|
|
||||||
(1, "street"),
|
|
||||||
(2, "approximate"),
|
|
||||||
(3, "approximate"),
|
|
||||||
(4, "approximate"),
|
|
||||||
(5, "approximate"),
|
|
||||||
(None, None),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
def test_qc_geo_to_precision(qc_geo: int | None, expected: str | None) -> None:
|
|
||||||
assert _qc_geo_to_precision(qc_geo) == expected
|
|
||||||
|
|
@ -59,16 +59,6 @@ function isMockAddress(estimate: AggregatedEstimate): boolean {
|
||||||
export function HeroSummary({ estimate, input, onResubmit, isResubmitting = false }: Props) {
|
export function HeroSummary({ estimate, input, onResubmit, isResubmitting = false }: Props) {
|
||||||
const cv = calcCv(estimate);
|
const cv = calcCv(estimate);
|
||||||
const conf = CONF_LABELS[estimate.confidence] ?? CONF_LABELS.low;
|
const conf = CONF_LABELS[estimate.confidence] ?? CONF_LABELS.low;
|
||||||
|
|
||||||
// Бейдж точности гео-привязки адреса (DaData qc_geo). null/undefined → ничего не рисуем.
|
|
||||||
const precisionBadge =
|
|
||||||
estimate.address_precision === "house"
|
|
||||||
? { text: "адрес: до дома", className: "precision-badge precision-house" }
|
|
||||||
: estimate.address_precision === "street"
|
|
||||||
? { text: "адрес: до улицы", className: "precision-badge precision-street" }
|
|
||||||
: estimate.address_precision === "approximate"
|
|
||||||
? { text: "адрес: приблизительно", className: "precision-badge precision-approx" }
|
|
||||||
: null;
|
|
||||||
const m = estimate.median_price_rub;
|
const m = estimate.median_price_rub;
|
||||||
const lo = estimate.range_low_rub;
|
const lo = estimate.range_low_rub;
|
||||||
const hi = estimate.range_high_rub;
|
const hi = estimate.range_high_rub;
|
||||||
|
|
@ -144,11 +134,6 @@ export function HeroSummary({ estimate, input, onResubmit, isResubmitting = fals
|
||||||
</div>
|
</div>
|
||||||
<div style={{ marginTop: 4 }}>
|
<div style={{ marginTop: 4 }}>
|
||||||
Достоверность · <b style={{ color: conf.color }}>{conf.txt}</b> · CV {cv.toFixed(1)}%
|
Достоверность · <b style={{ color: conf.color }}>{conf.txt}</b> · CV {cv.toFixed(1)}%
|
||||||
{precisionBadge && (
|
|
||||||
<span className={precisionBadge.className} title="Точность определения адреса">
|
|
||||||
{precisionBadge.text}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1714,31 +1714,3 @@
|
||||||
background: var(--bg-card-alt);
|
background: var(--bg-card-alt);
|
||||||
color: var(--fg-tertiary);
|
color: var(--fg-tertiary);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Address precision badge (DaData qc_geo) ─────────────────────────────── */
|
|
||||||
.precision-badge {
|
|
||||||
display: inline-block;
|
|
||||||
margin-left: 6px;
|
|
||||||
padding: 1px 6px;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 10px;
|
|
||||||
font-weight: 600;
|
|
||||||
letter-spacing: 0.04em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
.precision-house {
|
|
||||||
/* qc_geo=0 — дом определён точно (high confidence) */
|
|
||||||
background: var(--success-soft);
|
|
||||||
color: var(--success);
|
|
||||||
}
|
|
||||||
.precision-street {
|
|
||||||
/* qc_geo=1 — до улицы (neutral, как tier-t1) */
|
|
||||||
background: var(--bg-card-alt);
|
|
||||||
color: var(--fg-tertiary);
|
|
||||||
}
|
|
||||||
.precision-approx {
|
|
||||||
/* qc_geo≥2 — приблизительно. Amber-токены под «Адрес распознан неточно» disclaimer */
|
|
||||||
background: var(--accent-2-soft);
|
|
||||||
color: var(--accent-2);
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,6 @@ export type RepairState = "needs_repair" | "standard" | "good" | "excellent";
|
||||||
|
|
||||||
export type ConfidenceLevel = "low" | "medium" | "high";
|
export type ConfidenceLevel = "low" | "medium" | "high";
|
||||||
|
|
||||||
// Точность гео-привязки адреса (из DaData qc_geo): house=0, street=1, approximate≥2.
|
|
||||||
export type AddressPrecision = "house" | "street" | "approximate";
|
|
||||||
|
|
||||||
export interface TradeInEstimateInput {
|
export interface TradeInEstimateInput {
|
||||||
address: string; // min 3, max 500
|
address: string; // min 3, max 500
|
||||||
area_m2: number; // 10 < x < 500
|
area_m2: number; // 10 < x < 500
|
||||||
|
|
@ -81,11 +78,6 @@ export interface AggregatedEstimate {
|
||||||
sources_used: string[]; // ['avito', 'cian', 'rosreestr']
|
sources_used: string[]; // ['avito', 'cian', 'rosreestr']
|
||||||
data_freshness_minutes: number | null; // «обновлено N минут назад»
|
data_freshness_minutes: number | null; // «обновлено N минут назад»
|
||||||
est_days_on_market: number | null; // прогноз срока продажи
|
est_days_on_market: number | null; // прогноз срока продажи
|
||||||
// address_precision — точность гео-привязки адреса (из DaData qc_geo):
|
|
||||||
// «house» (qc_geo=0, дом точно), «street» (qc_geo=1, до улицы),
|
|
||||||
// «approximate» (qc_geo≥2: населённый пункт/город/регион/не распознан).
|
|
||||||
// null если DaData не отрабатывала (адрес не геокодирован) / credentials не заданы.
|
|
||||||
address_precision?: AddressPrecision | null;
|
|
||||||
// ── Параметры оценённой квартиры (для восстановления карточки по ?id=) ──
|
// ── Параметры оценённой квартиры (для восстановления карточки по ?id=) ──
|
||||||
area_m2: number | null;
|
area_m2: number | null;
|
||||||
rooms: number | null;
|
rooms: number | null;
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue