backend: POST /api/v1/trade-in/lead + persist (parent #1971) #2376

Open
opened 2026-07-04 06:32:33 +00:00 by lekss361 · 0 comments
Owner

Parent: #1971

Контекст

mailto-стаб в tradein-mvp/frontend/src/components/trade-in/HeroTransparency.tsx:162-198 (leadHref/leadEnabled) — единственная точка лид-CTA в кодовой базе. Нужен реальный backend перед заменой frontend (sub-issue — заведён отдельно, блокируется этим issue).

Референс-паттерн — backend/app/api/v1/pilot.py (POST /api/v1/pilot/request): та же структура (Pydantic input, INSERT...RETURNING, logger.info, notification — TODO). Копировать конвенцию 1-в-1.

Scope

  1. SQL migration tradein-mvp/backend/data/sql/172_trade_in_leads.sql:

    • CREATE TABLE IF NOT EXISTS trade_in_leads (id uuid PRIMARY KEY DEFAULT gen_random_uuid(), estimate_id uuid REFERENCES trade_in_estimates(id) ON DELETE SET NULL, phone text NOT NULL, consent boolean NOT NULL, source text, user_agent text, created_at timestamptz NOT NULL DEFAULT NOW(), notified_at timestamptz)
    • CHECK (consent IS TRUE) — defense-in-depth (основная валидация на API-уровне)
    • CREATE INDEX IF NOT EXISTS trade_in_leads_created_idx ON trade_in_leads (created_at DESC)
    • notified_at — колонка-заготовка под будущий notifier, миграция под него позже не нужна (паттерн pilot_requests)
    • BEGIN/COMMIT, idempotent (см. .claude/rules/sql.md)
  2. Endpoint POST /api/v1/trade-in/lead:

    • Pydantic input: phone: str (простая regex-валидация, НЕ EmailStr-подобный пакет — см. комментарий в pilot.py про email-validator ImportError), estimate_id: UUID | None, consent: Literal[True] (Pydantic сам вернёт 422 при false/отсутствии), source: Literal["result", "landing"] = "result"
    • Файл: новый tradein-mvp/backend/app/api/v1/lead.py (предпочтительно — trade_in.py уже 1747+ строк) + include_router(lead.router, prefix="/api/v1/trade-in", tags=["trade-in"]) в main.py. Альтернатива — функция прямо в trade_in.py, на усмотрение backend-engineer.
    • INSERT + RETURNING id/created_at, db.commit(), logger.info("trade_in_lead saved id=%s source=%s", ...)
    • psycopg v3 conventions: CAST(:x AS type), никаких f-string в SQL
  3. Notification — НЕ строить. Grep подтвердил: ни SMTP, ни Telegram интеграции нет нигде в коде (ни backend/app, ни tradein-mvp/backend/app) — тот же гэп, что у pilot.py (TODO с прошлого спринта). Просто persist + structured log. notified_at резервируется под follow-up.

Acceptance

ssh gendesign
docker exec tradein-backend curl -s -X POST localhost:8000/api/v1/trade-in/lead \
  -H "Content-Type: application/json" \
  -d '{"phone":"+79001234567","consent":true,"estimate_id":"<real-uuid>","source":"result"}'
# → 200/201 + id/created_at

docker exec tradein-postgres psql -U <user> -d tradein -c "SELECT * FROM trade_in_leads ORDER BY created_at DESC LIMIT 1"
# → строка присутствует

# consent:false → ожидаем 422

Не входит (см. комментарий на #1971)

  • Email/Telegram notification (нет существующей интеграции — отдельный future scope)
  • Аналитика воронки (conversion)
  • CRM-интеграция за пределами persist в trade_in_leads

Референсы для делегирования

  • backend/app/api/v1/pilot.py — эталонный паттерн
  • backend/data/sql/118_pilot_requests.sql — эталонная миграция
  • tradein-mvp/backend/data/sql/171_scrape_schedules_seed_geoportal_coords_backfill.sql — последний файл, новый = 172_trade_in_leads.sql
  • tradein-mvp/backend/app/api/v1/trade_in.py (router), tradein-mvp/backend/app/main.py (include_router)
  • tradein-mvp/backend/data/sql/001_trade_in_estimates.sql — FK-таргет trade_in_estimates(id uuid)
Parent: #1971 ## Контекст mailto-стаб в `tradein-mvp/frontend/src/components/trade-in/HeroTransparency.tsx:162-198` (leadHref/leadEnabled) — единственная точка лид-CTA в кодовой базе. Нужен реальный backend перед заменой frontend (sub-issue — заведён отдельно, блокируется этим issue). Референс-паттерн — `backend/app/api/v1/pilot.py` (POST /api/v1/pilot/request): та же структура (Pydantic input, INSERT...RETURNING, logger.info, notification — TODO). Копировать конвенцию 1-в-1. ## Scope 1. **SQL migration** `tradein-mvp/backend/data/sql/172_trade_in_leads.sql`: - `CREATE TABLE IF NOT EXISTS trade_in_leads (id uuid PRIMARY KEY DEFAULT gen_random_uuid(), estimate_id uuid REFERENCES trade_in_estimates(id) ON DELETE SET NULL, phone text NOT NULL, consent boolean NOT NULL, source text, user_agent text, created_at timestamptz NOT NULL DEFAULT NOW(), notified_at timestamptz)` - `CHECK (consent IS TRUE)` — defense-in-depth (основная валидация на API-уровне) - `CREATE INDEX IF NOT EXISTS trade_in_leads_created_idx ON trade_in_leads (created_at DESC)` - `notified_at` — колонка-заготовка под будущий notifier, миграция под него позже не нужна (паттерн `pilot_requests`) - BEGIN/COMMIT, idempotent (см. `.claude/rules/sql.md`) 2. **Endpoint** `POST /api/v1/trade-in/lead`: - Pydantic input: `phone: str` (простая regex-валидация, НЕ EmailStr-подобный пакет — см. комментарий в pilot.py про email-validator ImportError), `estimate_id: UUID | None`, `consent: Literal[True]` (Pydantic сам вернёт 422 при `false`/отсутствии), `source: Literal["result", "landing"] = "result"` - Файл: новый `tradein-mvp/backend/app/api/v1/lead.py` (предпочтительно — `trade_in.py` уже 1747+ строк) + `include_router(lead.router, prefix="/api/v1/trade-in", tags=["trade-in"])` в `main.py`. Альтернатива — функция прямо в `trade_in.py`, на усмотрение backend-engineer. - INSERT + RETURNING id/created_at, `db.commit()`, `logger.info("trade_in_lead saved id=%s source=%s", ...)` - psycopg v3 conventions: `CAST(:x AS type)`, никаких f-string в SQL 3. **Notification — НЕ строить.** Grep подтвердил: ни SMTP, ни Telegram интеграции нет нигде в коде (ни `backend/app`, ни `tradein-mvp/backend/app`) — тот же гэп, что у `pilot.py` (TODO с прошлого спринта). Просто persist + structured log. `notified_at` резервируется под follow-up. ## Acceptance ``` ssh gendesign docker exec tradein-backend curl -s -X POST localhost:8000/api/v1/trade-in/lead \ -H "Content-Type: application/json" \ -d '{"phone":"+79001234567","consent":true,"estimate_id":"<real-uuid>","source":"result"}' # → 200/201 + id/created_at docker exec tradein-postgres psql -U <user> -d tradein -c "SELECT * FROM trade_in_leads ORDER BY created_at DESC LIMIT 1" # → строка присутствует # consent:false → ожидаем 422 ``` ## Не входит (см. комментарий на #1971) - Email/Telegram notification (нет существующей интеграции — отдельный future scope) - Аналитика воронки (conversion) - CRM-интеграция за пределами persist в trade_in_leads ## Референсы для делегирования - `backend/app/api/v1/pilot.py` — эталонный паттерн - `backend/data/sql/118_pilot_requests.sql` — эталонная миграция - `tradein-mvp/backend/data/sql/171_scrape_schedules_seed_geoportal_coords_backfill.sql` — последний файл, новый = `172_trade_in_leads.sql` - `tradein-mvp/backend/app/api/v1/trade_in.py` (router), `tradein-mvp/backend/app/main.py` (include_router) - `tradein-mvp/backend/data/sql/001_trade_in_estimates.sql` — FK-таргет `trade_in_estimates(id uuid)`
lekss361 added the
enhancement
scope/backend
scope/db
tradein
labels 2026-07-04 10:23:51 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#2376
No description provided.