All checks were successful
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / changes (push) Successful in 10s
Deploy Trade-In / test (push) Successful in 1m36s
Deploy Trade-In / build-backend (push) Successful in 54s
Deploy Trade-In / deploy (push) Successful in 48s
91 lines
3.2 KiB
Python
91 lines
3.2 KiB
Python
"""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",
|
||
}
|