feat(analyze): add market_price из mv_quarter_price_per_m2 (#33 PR B) #210
No reviewers
Labels
No labels
admin
analytics
auth
automation
bug
business
chore
ci
compliance
data
data-moat
docs
duplicate
dx
enhancement
Fable 5 ревью
feedback/max
generative
GG-форсайт
needs-discussion
needs-human
observability
pause-bots
performance
priority/p0
priority/p1
priority/p2
priority/p3
scope/backend
scope/db
scope/devops
scope/frontend
scope/qa
scrapers
security
site-finder
stage/1
stage/2
status/blocked
status/done
status/needs-analysis
status/needs-fix
status/qa
status/ready
status/review
status/wip
tech-debt
tradein
ux
week ревью 1
wontfix
вторичка
ИРД
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: lekss361/gendesign#210
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "feat/33-analyze-quarter-price"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Issue #33 PR B — analyze integration с
mv_quarter_price_per_m2(созданным в PR #209).Backend changes
POST /analyzeresponse расширенmarket_price: Или fallback при отсутствии данных:MarketPrice(BaseModel)вschemas/parcel.pyдля документации типовdistrict.median_price_per_m2kept as-is для backward-compat — frontend Phase B переключится отдельным PRMV stats (verified live на проде после PR #209 deploy)
mv_quarter_price_per_m2✅quarter_cad_numberlookupTests
backend/tests/api/v1/test_analyze_market_price.py(NEW, 3 mock tests):deals_count=0, source=no_dataTest plan
curl POST /analyzeна test cad →market_price.medianfinite +source=quarter_mvCloses #33 (full close — Phase A merged in #209, Phase B here completes acceptance "/analyze возвращает quarter price").
Phase C (Celery beat weekly refresh) — отдельный issue в follow-up.
POST /analyze returns market_price из mv_quarter_price_per_m2 (52K quarters). Fields: p25/median/p75/mean/median_{6,12,24}m/deals_count/last_deal_date. source='quarter_mv' либо 'no_data'. district.median_price_per_m2 kept для backward-compat. MarketPrice BaseModel в schemas. 3 mock tests.Deep Code Review — verdict
Summary
parcels.py+51,schemas/parcel.py+22 · P3 test: +263)614c951· baseefa66a9f· mergeable=truebackend/app/api/v1/**+backend/app/schemas/**) — needs human merge.🔴 Critical
None.
🟠 High
None.
🟡 Medium (желательно fix — отдельным follow-up PR можно)
DRY: дубль
_derive_quarter_cad—parcels.py:1804-1806inline реализуетИдентичная функция уже существует:
backend/app/services/cadastre/bulk_harvest.py:1182-1187(_derive_quarter_cad(cad_num: str) -> str) и используется в 5+ местах bulk_harvest. Дополнительноservices/site_finder/cadastre_fetch.py:45,61дублирует тот же split-by-colon. Fix: либоfrom app.services.cadastre.bulk_harvest import _derive_quarter_cad(но_-prefix намекает на private), либо вынести вapp/services/utils/cad.pyединым хелпером (рекомендую) — отдельным refactor PR после merge.Test coverage пропускает NULL sub-window —
test_market_price_found_in_mvвсегда даётmedian_6m=Decimal(...). На проде verified чтоmedian_6m/median_12mчасто = NULL (нет сделок в 6m окне) — этот ровно тот edge-case что PR обрабатывает (if mp_row["median_6m"] is not None else None), и он не покрыт тестом. Добавь 4-й тест сmv_data["median_6m"] = None, mv_data["median_12m"] = None→ assertmp["median_6m"] is None.🟢 Low / nits (можно проигнорировать)
MarketPrice(BaseModel)объявлена вschemas/parcel.pyно не используется какresponse_model(endpoint возвращает untypeddict[str, Any]). Per PR body это intentional — schema живёт для документации. OK для текущего scope, но при codegen OpenAPI типы не появятся у фронта автоматически → frontend Phase C прийдётся прописать TS-тип руками.test_market_price_invalid_cad_returns_404docstring "404 expected" но assertstatus_code in (202, 404)— расхождение между описанием и проверкой. Нит._cad_parts/_quarter_cadлокальные_-prefixed имена внутри функции — style nit, обычно_для module-private. Не блокер.Cross-file impact analysis
district.median_price_per_m2оставлен intact (parcels.py:1846) → frontendLandTab/MarketTabне сломается; Phase C переключение отдельным PR (declared в body).db.execute(...)call-sites вanalyze_parcelпересчитаны — тестовыйresponses[]индекс 13 (market_price после success_recommendation) совпадает с фактическим порядком в коде.mv_quarter_price_per_m2пока нет (grepпоbackend/чист) — single-consumer, blast radius минимален.market_price(grepfrontend/чист) — добавление поля aдитивно, никакого TS regen не нужно для этого PR.Vault cross-check
domains/rosreestr/MV_Quarter_Price_Per_M2_May16.md— schema vault entry создана в PR #209, актуальна.domains/api/endpoints/Endpoint_Parcel_Analyze.md— упоминаниеmarket_priceуже добавлено (verified via simple_search).limitations/Limit_District_Median_Null.md+limitations/Limit_Trend_Always_Capped_OPEN.md— обе ссылаются на #33 как решение, consistent с этим PR.Performance findings
Live
EXPLAINна MV (через MCP postgres):quarter_cad_number(PK отdata/sql/95_mv_quarter_price.sql)/analyze(negligible относительно остальных query).median_6m=NULLlegitimately — fallback логика правильная.Conventions ✅
psycopg2import).grep -nE ':[a-z_]+::[a-z]' parcels.py→ 0 hits.:q), no f-string injection.logger.warning(...)неprint(...).Literalimport present вschemas/parcel.py:2(from typing import Any, Literal).Anyimport present вparcels.py:6.with db.begin_nested():) защищает outer tx — matchesBug_Pzz_Loader_Missing_Savepointlesson.Literal["quarter_mv", "no_data"]дляsource— type-safe enum.median_6m/12m/24mкорректно разбит).Positive observations
{"deals_count": 0, "source": "no_data"}гарантирует no-500 при missing data — graceful degradation.int(mp_row["deals_count"])безif not None else— корректно, MV всегда возвращаетdeals_count >= 3(HAVING фильтр), NULL невозможен.last_deal_date.isoformat()правильно сериализуетdt.dateдля JSON.Decimal → floatcast в endpoint — test проверяет черезpytest.approx.district.median_price_per_m2сохранено).Recommended next steps
_derive_quarter_cad→app/services/utils/cad.pyи dedupe 3 call-sites (bulk_harvest.py,cadastre_fetch.py, нового —parcels.py).curl POST /analyzeна cad из66:41:*→ проверитьmarket_price.sourcevalue. На проде EKB-quarter может legitimately датьno_dataесли в нём нет 3+ ДДУ за 24mo, это OK.MarketPriceруками (codegen не подхватит — schema не привязана к response_model) + рендер вMarketTab/OverviewTab.Complexity / blast radius score
Auto-merge decision
PR diff touches
backend/app/api/v1/parcels.py+backend/app/schemas/parcel.py→ scope BLOCKED per.claude/agents/deep-code-reviewer.mdAuto-merge policy. Approved but needs human merge.