feat(tradein): GET /street-deals — ДКП-сделки Росреестра по улице target адреса #555
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#555
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "feat/tradein-street-deals-backend"
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?
What
Новый endpoint
GET /api/v1/trade-in/street-dealsвозвращает ДКП-сделки Росреестра, матчящиеся по улице целевого адреса + rooms + area BETWEEN target ± 15%.Питает frontend секцию «По вашей улице» (PR-C-front идёт следом).
Why per-street (не per-house)
Open dataset Росреестра агрегирует адреса до УЛИЦЫ, без номера дома. Проверено:
tradein.deals.addressдля rosreestr ='Екатеринбург, Космонавтов'(890 сделок),'Екатеринбург, 8 Марта'(690), etc. См. Fix_Rosreestr_Dkp_Filter_May24 для constraints.Использует чистые ДКП-сделки после PR-A (#549). До merge PR-A endpoint вернул бы mixed ДКП+ДДУ-первичку с искажённой median — теперь корректно.
API
Empty response (count=0, deals=[]) при street is None или нет совпадений — frontend gracefully скроет секцию.
Changes (4 файла)
schemas/trade_in.py— добавленStreetDealsResponseпослеSellTimeSensitivityResponseservices/estimator.py— publicextract_street_name(full_address) -> str | Noneс двумя regex (_TRAILING_HOUSE_RE,_STREET_PREFIX_RE), переиспользует существующий_extract_short_addrи_STREET_START_REapi/v1/trade_in.py—GET /street-dealsendpoint с inline SQL (text + bind params per psycopg3 rules); reuse_deal_to_analogи_percentileиз estimatortests/test_street_deals_endpoint.py— NEW, 5 тестов: 3× extract_street_name parsing, empty case, aggregationVerification
ruff check— All checks passedpytest— 5/5 новых + 14/14 existing estimator/_extract_short_addr тестов = 19/19 passПри тесте на prod после merge:
curl 'http://localhost:8002/api/v1/trade-in/street-deals?address=...&area_m2=55&rooms=2'должен вернуть ~5-30 сделок по большинству ЕКБ-улиц.Refs: PR-C-back из 4-PR roadmap (PR-C-front следом). Fix_Rosreestr_Dkp_Filter_May24 для контекста.
Deep Code Review — verdict: APPROVE
Files: 4 (P1: api/trade_in.py, schemas/trade_in.py, services/estimator.py; P2: tests/test_street_deals_endpoint.py) · +398 / -1
Checked SHA:
23aebea812(head matches prompt).Security
street_pattern=%+ street +%is bound via psycopg, not concatenated into SQL text.%/_) instreet_namewould only widen the SELECT (read-only). Low risk — see suggestions.%rfor user input — safe.Correctness
extract_street_nameregex passes for the 5 listed test cases including«ул. 8 Марта». Traced:_TRAILING_HOUSE_REstrips, 18, then_STREET_PREFIX_REstripsул.→8 Марта.source = 'rosreestr').period_fromusesdays*30approximation; SQL uses'N months'::interval— minor drift, only affects the echoedperiod_fromfield. Not a real bug.:rooms/:period_months— correct psycopg v3 pattern._empty()is defined inside the handler but never called — dead code (theif not street_namebranch inlines its own empty response). Minor cleanup.extract_street_namedoes not stripд. Nform (e.g.«ул. Малышева, д. 1»→«Малышева, д. 1»). All explicit test cases use, N. Real-data addresses commonly useд. N. Low impact: only widens ILIKE pattern, doesn't break results._TRAILING_HOUSE_RE = r",\s*\d+.*$"requires comma before digits. Addresses like«Космонавтов 12»(no comma) keep the number → wider ILIKE. Low impact.Performance
deals.address— no functional index. Acceptable for one-off endpoint call; deals table is partitioned and thesource = 'rosreestr'predicate narrows aggressively.LIMITon the main query (existing_fetch_dealsuses LIMIT 30). For very common streets (Космонавтов ≈ 890 rows) the full scan + sort is fine; considerLIMIT 1000cap before percentile math if it grows.period_months/area_tolerance— a user can passperiod_months=10000to force a long scan. RecommendQuery(12, ge=1, le=60)and bounds onarea_tolerance.Project conventions
list[dict]) · Annotated[Depends].db: Annotated[..., Depends(get_db)] = None, # type: ignore[assignment]— needed because required-no-default params precede defaulted ones. Works at runtime; cleaner would beQuery()annotations onperiod_months/area_tolerance, or movedblast.Architecture & maintenance
from app.services.estimator import …— likely to avoid circular import. Fine._deal_to_analog,_percentile,_extract_short_addr. Good DRY.extract_street_nameis public (no underscore) — appropriate for cross-module use.Tests
extract_street_namecover 5 positive + 2 edge cases.MagicMockfor the DB session — no real SQL execution, but the SQL string is canonical with_fetch_dealsand the parametrization is identical.Cross-file impact
extract_street_nameis a new symbol — no existing callers affected.StreetDealsResponse— no breaking change. Frontend consumer (PR-C-front) follows.Vault cross-check
[[Fix_Rosreestr_Dkp_Filter_May24]]— consistent with the deals invariant from PR #549 (merged:596842d).Recommended follow-ups (non-blocking)
Query(ge=…, le=…)bounds onperiod_months,area_tolerance,rooms,area_m2._empty()helper or use it consistently._TRAILING_HOUSE_REto handle, д.\s*\d+and bare\s+\d+forms.LIMIT 1000cap on the main query as a safety bound.Complexity / blast radius
Auto-merging (squash + branch delete).