feat(sf-b3): POST /pilot/request — lead gen + pilot_requests table (Telegram TODO) #330

Merged
lekss361 merged 2 commits from feat/sf-b3-pilot-leads into main 2026-05-17 21:14:29 +00:00
Owner

Summary

SF Wave 1 / Group B / sub-task B3 из SiteFinder Backend Migration Plan May17.

Files

  • backend/app/api/v1/pilot.py — NEW router с POST /api/v1/pilot/request
  • backend/app/main.pyapp.include_router(pilot.router, prefix="/api/v1/pilot", tags=["pilot"])
  • data/sql/118_pilot_requests.sql — NEW migration: pilot_requests table + index + comment

Schema

CREATE TABLE pilot_requests (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  name text NOT NULL, phone text, email text, company text, message text,
  source text, user_agent text,
  created_at timestamptz DEFAULT NOW(),
  notified_at timestamptz
);

API

POST /api/v1/pilot/request принимает {name, phone?, email?, company?, message?, source?} → INSERT → returns {id, created_at, status: "received"}.

Telegram notification — TODO (нужны TELEGRAM_BOT_TOKEN + TELEGRAM_CHAT_ID creds, добавится follow-up).

Test plan

  • Migration applies cleanly
  • POST /api/v1/pilot/request {"name": "Test", "source": "landing"} → 200 + id
  • Row inserted в pilot_requests

Part of plan vault code/patterns/SiteFinder_Backend_Migration_Plan_May17.md §B3.

## Summary SF Wave 1 / Group B / sub-task **B3** из SiteFinder Backend Migration Plan May17. ## Files - `backend/app/api/v1/pilot.py` — NEW router с `POST /api/v1/pilot/request` - `backend/app/main.py` — `app.include_router(pilot.router, prefix="/api/v1/pilot", tags=["pilot"])` - `data/sql/118_pilot_requests.sql` — NEW migration: `pilot_requests` table + index + comment ## Schema ```sql CREATE TABLE pilot_requests ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), name text NOT NULL, phone text, email text, company text, message text, source text, user_agent text, created_at timestamptz DEFAULT NOW(), notified_at timestamptz ); ``` ## API `POST /api/v1/pilot/request` принимает `{name, phone?, email?, company?, message?, source?}` → INSERT → returns `{id, created_at, status: "received"}`. Telegram notification — TODO (нужны `TELEGRAM_BOT_TOKEN` + `TELEGRAM_CHAT_ID` creds, добавится follow-up). ## Test plan - [ ] Migration applies cleanly - [ ] `POST /api/v1/pilot/request` `{"name": "Test", "source": "landing"}` → 200 + id - [ ] Row inserted в `pilot_requests` Part of plan vault `code/patterns/SiteFinder_Backend_Migration_Plan_May17.md` §B3.
lekss361 added 1 commit 2026-05-17 21:02:38 +00:00
Добавляет endpoint для приёма заявок на пилот (lead-gen).
INSERT в pilot_requests, response {id, created_at, status}.
Telegram-уведомление — TODO (creds не настроены, #307 SF-B3).

Migration: data/sql/118_pilot_requests.sql
Author
Owner

Deep Code Review — APPROVE

Migration data/sql/118_pilot_requests.sql

  • BEGIN/COMMIT wrapper, CREATE TABLE IF NOT EXISTS, CREATE INDEX IF NOT EXISTS — idempotent
  • gen_random_uuid() — PG 13+ core function, не требует pgcrypto (уже используется в 91_engineering_networks_191fz.sql и 115_trade_in_estimates.sql на prod без CREATE EXTENSION — empirical confirmation)
  • Index (created_at DESC) подходит под обычные admin list-queries
  • COMMENT ON TABLE present, нет DROP/TRUNCATE
  • Filename 118 — следующий после 117 ✓

Endpoint backend/app/api/v1/pilot.py

  • Pydantic v2: name min_length=2, max_length=200, phone max_length=50, company max_length=200, message max_length=2000 — DoS payload caps уже на месте
  • EmailStr (validated) + Literal["landing","analyze_page","other"] для source — strong validation
  • SQL: text(...) + bind params, нет injection
  • CAST(id AS text) — psycopg v3 / SQLAlchemy 2.0 convention compliant (никаких ::text)
  • Sync Session + .mappings().one() + db.commit() matches get_db() semantics
  • logger.info(...), нет print, нет requests, нет import psycopg2, нет hardcoded creds
  • Telegram TODO в docstring — нет половинчатого commented-out кода

main.py wiring

  • prefix="/api/v1/pilot" + router /request/api/v1/pilot/request

Cross-file impact

Self-contained NEW router + NEW table + 2 line registration. Нет callers для обновления, frontend types не затронуты.

Non-blocking follow-ups

  • 🟡 MINOR: rate-limit на unauth public POST — platform-wide concern (не только pilot), follow-up. Mitigated тем что Pydantic max_length ограничивает payload size.
  • 🟡 MINOR: user_agent text не имеет max_length — теоретически browser может прислать длинный UA. Маловероятно abuse, но max_length=512 был бы дешёвой страховкой. Не блокирующее.

Risk

  • Risk: Low
  • Reversibility: easy (DROP TABLE pilot_requests + revert router include)
  • Merge window: anytime

Merging.

## Deep Code Review — APPROVE ### Migration `data/sql/118_pilot_requests.sql` - `BEGIN/COMMIT` wrapper, `CREATE TABLE IF NOT EXISTS`, `CREATE INDEX IF NOT EXISTS` — idempotent - `gen_random_uuid()` — PG 13+ core function, не требует `pgcrypto` (уже используется в `91_engineering_networks_191fz.sql` и `115_trade_in_estimates.sql` на prod без `CREATE EXTENSION` — empirical confirmation) - Index `(created_at DESC)` подходит под обычные admin list-queries - `COMMENT ON TABLE` present, нет DROP/TRUNCATE - Filename `118` — следующий после 117 ✓ ### Endpoint `backend/app/api/v1/pilot.py` - Pydantic v2: `name` `min_length=2, max_length=200`, `phone max_length=50`, `company max_length=200`, `message max_length=2000` — DoS payload caps уже на месте - `EmailStr` (validated) + `Literal["landing","analyze_page","other"]` для `source` — strong validation - SQL: `text(...)` + bind params, нет injection - `CAST(id AS text)` — psycopg v3 / SQLAlchemy 2.0 convention compliant (никаких `::text`) - Sync `Session` + `.mappings().one()` + `db.commit()` matches `get_db()` semantics - `logger.info(...)`, нет `print`, нет `requests`, нет `import psycopg2`, нет hardcoded creds - Telegram TODO в docstring — нет половинчатого commented-out кода ### main.py wiring - `prefix="/api/v1/pilot"` + router `/request` → `/api/v1/pilot/request` ✓ ### Cross-file impact Self-contained NEW router + NEW table + 2 line registration. Нет callers для обновления, frontend types не затронуты. ### Non-blocking follow-ups - 🟡 MINOR: rate-limit на unauth public POST — platform-wide concern (не только pilot), follow-up. Mitigated тем что Pydantic `max_length` ограничивает payload size. - 🟡 MINOR: `user_agent` text не имеет `max_length` — теоретически browser может прислать длинный UA. Маловероятно abuse, но `max_length=512` был бы дешёвой страховкой. Не блокирующее. ### Risk - Risk: **Low** - Reversibility: easy (DROP TABLE pilot_requests + revert router include) - Merge window: anytime Merging.
Author
Owner

⚠️ Merge blocked: conflict with main after PR #329 merge.

Both #329 and #330 add app.include_router(...) in backend/app/main.py → conflict in the router registration block.

update_pr_branch returned 409: merge failed because of conflict. Conflict resolution needed by author — backend/app/main.py should keep BOTH include_router(users.router, ...) and include_router(pilot.router, ...) lines.

Code review verdict stands as APPROVE — will retry merge automatically on next polling iteration once branch is updated.

⚠️ **Merge blocked: conflict with main after PR #329 merge.** Both #329 and #330 add `app.include_router(...)` in `backend/app/main.py` → conflict in the router registration block. `update_pr_branch` returned `409: merge failed because of conflict`. Conflict resolution needed by author — `backend/app/main.py` should keep BOTH `include_router(users.router, ...)` and `include_router(pilot.router, ...)` lines. Code review verdict stands as ✅ **APPROVE** — will retry merge automatically on next polling iteration once branch is updated.
lekss361 added 1 commit 2026-05-17 21:08:55 +00:00
lekss361 merged commit dbeed80b26 into main 2026-05-17 21:14:29 +00:00
Sign in to join this conversation.
No reviewers
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#330
No description provided.