feat(tradein): этаж/этажность optional + best test presets по deal count #558
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#558
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "feat/tradein-form-floor-optional-best-presets"
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?
UX-fix: floor/total_floors optional
Все 6 мест в
estimator.pyгдеpayload.floor/payload.total_floorsиспользуются уже имеютis not Noneguards (Tier H фильтр, IMV Avito, Cian Valuation, tier note, DB INSERT, response). Schema-only change достаточен — поменять Requiredintнаint | None = Field(default=None, ge=1, le=100).Trade-off при None: Tier H / IMV / Cian skip → оценка через Tier W (wider), confidence упадёт до medium/low. Но estimator работает.
Form changes
<input required>убран с этажа и этажности*→если знаешьhinttoPayload()теперь возвращаетnullпри пустом input (а не NaN)validate()пропускает поля если пустыеTS types
TradeInEstimateInput.floor: number | null,.total_floors: number | null.Test presets — top по deal_count
Заменены на 6 ЕКБ улиц с лучшим покрытием ДКП (из
rosreestr_dealsза 12 мес). Это первая итерация — следующий PR (follow-up) сделает balanced rooms/area diversity (студия/4-к и т.д.).Verification
ruff check— passpnpm type-check— zero errorsReverses ничего, дополняет PR #557 завершившего ДКП-roadmap.
Deep Code Review — verdict: REQUEST CHANGES
Summary
c60d394🔴 Critical (BLOCK)
tradein-mvp/backend/data/sql/001_trade_in_estimates.sql:L?— schema mismatch crash on submit без этажаDDL имеет:
ни одна alter-migration (
004,008,034...064) не делаетDROP NOT NULLдля этих колонок.Pydantic schema теперь принимает
None, форма шлётnull, ноestimator.pyделает 2 INSERT напрямую с этими значениями без guard:estimator.py:893-894(основной путь_estimate()):estimator.py:1800-1801(_empty_estimate()fallback):Repro: открыть форму → заполнить адрес/площадь/комнаты, оставить «Этаж»/«Всего этажей» пустыми → submit → 500 +
null value in column "floor" violates not-null constraint.Логика gating для IMV (L764), Cian Valuation (L811), Tier H (L1353) — все три проверяют
is not Noneи graceful skip ✅. Только INSERT'ы пропустили. PR body утверждает «все 6 мест ... уже имеют is not None guards» — это неверно, 2 INSERT'а из 6 без guard.Fix — нужна новая migration + (опционально) безопасный default:
🟠 High
tradein-mvp/backend/app/services/exporters/trade_in_pdf.py:264-265, L338— UX regression в PDFЕсли пользователь не заполнил — PDF покажет «0 / 0» что выглядит как баг. Заменить на «—»:
🟡 Medium
estimator.py:744— tier_note строит «-эт.» черезif payload.total_floors✅ работает (None falsy), но рядом line 745
if target_year— стилистически inconsistent с явнымis not Noneв gating блоках. Минор, не блокер.🟢 Low / Notes
number | null(не optional?) — это правильно, payload всегда содержит ключ.if (f.floor !== "")гарды + cross-validationfloor > total_floorsпропускается если любое поле пустое — корректно.year_built, неtotal_floors— не затронут.Test coverage gap 🧪
Нет ни одного test'а который проверяет flow с
floor=None/total_floors=None:test_estimator_cohort.py,test_estimator_imv_integration.pyи т.д. — все используют fixtures с заполненными floorVault cross-check 📚
Nothing in
decisions//code/modules/запрещает relaxing NOT NULL. Это первый раз когда estimator поддерживает partial input — желательно зафиксировать в vault через/vault-writedecision: «trade_in input floor/total_floors optional since PR #558».Recommended next steps
065_trade_in_estimates_floor_optional.sql(DROP NOT NULL для floor/total_floors).or "—"вместо, 0default.test_estimator_floor_optional.py.Risk / blast radius
Re-review verdict: APPROVE
All prior BLOCKER concerns resolved.
Fixed
065_trade_in_estimates_floor_optional.sqlдобавлена:BEGIN/COMMITтранзакцияALTER TABLE trade_in_estimates ALTER COLUMN floor DROP NOT NULL+ same дляtotal_floorsCOMMENT ON COLUMNобоих полей со ссылкой на PRtrade_in_pdf.py:264-265):input_snapshot.get("floor") or "—"корректно показывает прочерк вместо0для NULL значенийestimator.py:744):is not Noneguard (вместо truthy) — корректно обрабатываетNoneestimator.py:764-765, 811-812): уже фильтруютpayload.floor is not None and payload.total_floors is not Noneперед вызовом external servicesEstimateForm.tsx): валидация optional + label*→ "если знаешь" + hint про 5-10% price impact +requiredснятtest_estimator_floor_optional.pyпокрывает schema accept + tier-H tf_str guardCross-verified
_estimate()(:893-894) → NULL safe после migration_empty_estimate()(:1800-1801) → NULL safe после migrationfloor/total_floorscurrentlyNOT NULL(is_nullable: NO) — migration НЕ применена pre-merge, applied auto через deploy.yml после mergetoPayload(EstimateForm.tsx:81-82) посылаетnullдля пустого поля — backend Pydantic + DB NULL acceptNo regressions
Deploy note
065_*идёт после064_*— OKNotNullViolationMerging via squash.