feat(trade-in): TI-2 PDF export 4-page + frontend enable button #319
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#319
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "feat/ti-2-pdf"
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
Wave C финал issue #314 TradeIn MVP. Backend WeasyPrint 4-page PDF + frontend кнопка «Скачать PDF» enabled.
Files
backend/app/services/exporters/trade_in_pdf.py(NEW, ~250 lines) —generate_trade_in_pdf(estimate, input_snapshot) -> bytes. 4-page WeasyPrint HTML+CSS: Cover / Listings / Actual Deals / Trade-in Cost stub. Pattern mirrorslayout_tz_pdf.py(Cyrillic-ready).backend/app/api/v1/trade_in.py(EXTENDED) — newGET /api/v1/trade-in/estimate/{estimate_id}/pdfendpoint:application/pdf+Content-Disposition: attachmentCAST(:id AS uuid)— psycopg v3 compliantfrontend/src/components/trade-in/EstimateResult.tsx(MODIFIED) — disabled<button>+ tooltip «Coming in TI-2» replaced with active<a href download>→${NEXT_PUBLIC_API_URL}/api/v1/trade-in/estimate/${estimate.estimate_id}/pdffixes/Fix_TradeIn_MVP_TI2_PDF_May17.md— design notes + pitfalls4 PDF pages
Compliance
CAST(:id AS uuid)(no::type)Test plan
POST /api/v1/trade-in/estimate→ takeestimate_idGET /api/v1/trade-in/estimate/{id}/pdf→ downloads 4-page PDF/trade-in→ submit form → click «Скачать PDF» → file downloadsCloses #314 (Wave C — TI-2/4 last sub-tasks before TI-5 smoke). Depends on TI-1 #316, TI-3 #317 (both merged).
- Add backend/app/services/exporters/trade_in_pdf.py: 4-page WeasyPrint PDF (Cover / Listings / Deals / Trade-in cost stub) following layout_tz_pdf.py pattern - Extend trade_in.py: GET /api/v1/trade-in/estimate/{id}/pdf endpoint (404 not found, 410 expired TTL, application/pdf attachment) - Enable EstimateResult.tsx PDF button: replace disabled button with active anchor tag using NEXT_PUBLIC_API_URL + estimate_id Closes #314 (TI-2 + TI-4 sub-tasks)Deep Code Review — verdict ✅ APPROVE
Files: 3 (P1: 2 backend, P2: 1 frontend) · +529/-42 · ~12 min
Critical checks passed
CAST(:id AS uuid)used everywhere, no:x::type(backend/app/api/v1/trade_in.py:330)_html.escape()applied toinput_snapshot.address(trade_in_pdf.py:130) andlot.addressin analog rows (trade_in_pdf.py:54). Numeric fields (area_m2: float,rooms: int,floor: int) are typed → no injection vectorphoto_urlis NOT embedded into PDF — analog table has only 6 columns (address / area / rooms / floor / price / date), images skipped. No remote URL fetched during PDF renderif row.expires_at.replace(tzinfo=UTC) < datetime.now(tz=UTC): 410(trade_in.py:347) — intentionally differs from TI-1get_estimate(which mixed 404+expired into single 404). 410 Gone is correct semantic for expired-but-existed-onceestimate.estimate_idis TS-typedUUIDfromAggregatedEstimateinterface, interpolated after fixed prefix${NEXT_PUBLIC_API_URL ?? ""}/api/v1/trade-in/estimate/— nojavascript:injection possiblef'attachment; filename="trade-in-{estimate_id}.pdf"'— estimate_id isUUIDpath-param (FastAPI validates), no header injectionlayout_tz_pdf.py(same_html.escape, same WeasyPrint flow, same Cyrillic-ready font stackHelvetica, Arial)AnalogLotfields used (listing_date,days_on_market,floor,total_floors,address,area_m2,rooms,price_rub) matchbackend/app/schemas/trade_in.pydata/sql/115_trade_in_estimates.sql(TI-1 schema)get_estimateandestimateendpoints — no auth, UUID is unguessableLow / nits (non-blocking, future polish)
row.expires_at.replace(tzinfo=UTC)(trade_in.py:347) is redundant —timestamptzalready returns TZ-aware UTC datetime. No-op, safe but verbose.lot.rooms if lot.rooms else 'С'(trade_in_pdf.py:58) —rooms == 0(студия) renders as "С" which is intentional (studio shorthand). OK.Cross-file impact
GET /trade-in/estimate/{id}/pdf— new, no existing callers affected<a href download>replaces disabled<button>cleanly — no other consumers of the old disabled elementgenerate_trade_in_pdfimport added correctly to module top (TI-1 had strayfrom fastapi import HTTPExceptioninside function — this PR cleans that up by moving to top-level imports ✅)Risk / blast radius
Merging via deep-code-reviewer (any-scope policy 2026-05-16).