gendesign/tradein-mvp/backend/app/api/v1/lead.py
bot-backend ada9bb8125
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 9s
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 1m48s
feat(tradein/lead): POST /api/v1/trade-in/lead — persist contact leads (#2376)
Contact capture (phone + consent) from estimate result / landing page.
New trade_in_leads table (migration 172), estimate_id FK validated
before insert (404 if unknown). Notification (Telegram/email)
intentionally out of scope — no existing SMTP/Telegram integration
anywhere in the codebase (same gap as pilot.py's TODO).
2026-07-04 10:01:53 +03:00

91 lines
3.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Trade-in lead capture endpoint (issue #2376, sub-issue родителя #1971).
POST /api/v1/trade-in/lead — контактная заявка с результата оценки (или лендинга):
телефон + явное согласие на обработку персональных данных. Persist в
trade_in_leads. Notification (Telegram/email) — вне scope: нет существующей
SMTP/Telegram интеграции в коде (подтверждено при разборе issue), только
persist + log; `notified_at` в таблице зарезервирован под будущую доставку.
"""
from __future__ import annotations
import logging
from typing import Annotated, Any, Literal
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, Request
from pydantic import BaseModel, Field
from sqlalchemy import text
from sqlalchemy.orm import Session
from app.core.db import get_db
logger = logging.getLogger(__name__)
router = APIRouter()
# Простая маска телефона (RU/международная): опциональный "+", цифры/пробелы/
# скобки/дефисы/точки, 5-32 символа. Полная нормализация в E.164 — вне scope MVP,
# см. #2376 DoD ("простая regex, не EmailStr-подобное").
_PHONE_PATTERN = r"^[+]?[\d\s().-]{5,32}$"
class TradeInLeadInput(BaseModel):
phone: str = Field(min_length=5, max_length=32, pattern=_PHONE_PATTERN)
estimate_id: UUID | None = None
consent: Literal[True]
source: Literal["result", "landing"] = "result"
@router.post("/lead")
async def create_trade_in_lead(
payload: TradeInLeadInput,
request: Request,
db: Annotated[Session, Depends(get_db)],
) -> dict[str, Any]:
"""Сохраняет лид (телефон + согласие) в trade_in_leads."""
if payload.estimate_id is not None:
exists = db.execute(
text("SELECT 1 FROM trade_in_estimates WHERE id = CAST(:id AS uuid)"),
{"id": str(payload.estimate_id)},
).fetchone()
if exists is None:
raise HTTPException(status_code=404, detail="estimate not found")
user_agent = request.headers.get("user-agent")
row = (
db.execute(
text(
"""
INSERT INTO trade_in_leads (estimate_id, phone, consent, source, user_agent)
VALUES (CAST(:estimate_id AS uuid), :phone, :consent, :source, :user_agent)
RETURNING CAST(id AS text), created_at
"""
),
{
"estimate_id": str(payload.estimate_id) if payload.estimate_id else None,
"phone": payload.phone,
"consent": payload.consent,
"source": payload.source,
"user_agent": user_agent,
},
)
.mappings()
.one()
)
db.commit()
logger.info(
"trade_in_lead saved id=%s estimate_id=%s source=%s",
row["id"],
payload.estimate_id,
payload.source,
)
return {
"id": row["id"],
"created_at": row["created_at"].isoformat(),
"status": "received",
}