# zemlya — подготовка к запуску (2026-05-19)

## Главное

Сайт **zbrcheck.ru** готов к запуску **сегодня-завтра** (2026-05-20). Реф-программа целиком (договор + документ юр.статуса + админ-апрув), Web Push, карта-пикер с поиском адреса, OAuth (Telegram/Яндекс/Mail.ru), REF<users.id> без зависимости от TG, антифрод, живая анимация pending как в боте, партнёрская программа с deep-link — всё на проде, full_audit 0/0 на 25 страницах. Backend 85 routes. Перед live: `ROBOKASSA_TEST_MODE=0`. SMTP отложен по решению юзера, VK ID не подключаем. **Свежий handoff: `/tmp/zw_state_pre_compact_6.md`**.

## Что добавлено / изменено

### Функционал (новое)
1. **Ввод адреса/координат вместо КН** на `/dashboard/new`:
   - 3 таба: 📋 КН / 📍 Адрес / 🗺️ Координаты
   - Бот endpoint `POST /internal/resolve-cadastral` (localhost-only, порт 8443)
   - Web `POST /api/lookup/cadastral` проксирует к боту
   - Использует `bot.services.coordinate_resolver` (адрес via NSPD v1 API, координаты via NSPD by point)
2. **Browser Notifications** на `/reports/[id]`:
   - Запрашивает `Notification.permission` при заходе на pending
   - Показывает уведомление при `completed` (один раз через `notifiedRef`)
   - Клик → focus + reload отчёта
   - Работает пока вкладка открыта (включая background)
3. **Ссылка на карту НСПД** в табе КН: `https://nspd.gov.ru/map` в новом окне
4. **3 формата реф-кода** на `/referrals`:
   - Telegram-ссылка
   - Web-ссылка `https://zbrcheck.ru/r/REF<tid>`
   - REF-код `REF<tid>` для ввода в `/promo`
5. **Bug-фикс**: `BOT_USERNAME=ZBRCheck` в .env указывал на канал. Теперь `_bot_username()` берёт `TELEGRAM_BOT_USERNAME=ZemlyaBezRiskaCheck_bot`

### Нейминг (sync с ботом)
- `PRO-отчёт` → `Pro-отчёт` везде на сайте (как в `/lk` бота)
- BalanceCard на /dashboard:
  - `PRO-отчёты` → `Pro-отчёты`
  - `Бесплатные` → `Бесплатные проверки`
  - `Реф-баланс` → `Реф. баланс`
- HubButton: `Мои отчёты` → `История отчётов` (как в боте)
- `/admin/leads`, /admin nav: `Лиды` → **`Заявки`** (бот говорит «Заявки продавцов/инвесторов»)
- Английские dropdown options в админке переведены: `completed`→`Завершено`, `pending`→`В обработке`, и т.д.
- Lookup-карты в таблицах: `STATUS_LABEL`, `MODE_LABEL`, `ACCESS_LABEL`, `PAYMENT_TYPE_LABEL`

### Удалено (подписки больше не нужны)
- Поле «Дней подписки» в форме создания промокода
- Колонка «Дней» (subscription_days) в таблице промокодов
- Stat «Подписка» в /admin/users/[id]
- Option «Подписка» в фильтре /admin/payments
- В /admin home desc: убрано «AI/подписку» → «Pro/AI-отчёты»

### Описание тарифов (по требованию юзера)
**Free и Pro идентичны кроме «Оценка цены»:**
- Free: 1 источник (Avito)
- Pro: 3 источника (Avito + Яндекс Недвижимость + ЦИАН)
- AI: всё то же + PDF от ИИ с вердиктами

Убраны упоминания «10-16 страниц» из всех описаний AI (8 мест: главная COMPARISON/TIERS, /pricing TierCard, /help table+TierBlock).

### Оферта
`/login` — мелкая ссылка под формой: «Регистрируясь на сервисе, вы соглашаетесь с **публичной офертой** и **политикой обработки ПД**».

### Серверный онбординг
- `users.web_onboarded_at TIMESTAMP NULL` (миграция + бэкфилл 102 существующим)
- `/api/auth/me.web_onboarded` поле
- `POST /api/auth/mark_onboarded`
- Модал онбординга показывается **только один раз ever** (не зависит от устройства/incognito)

### Блок G: Реф-ссылки независимы от TG (2026-05-20, поздний вечер)

Цель: OAuth-юзер должен мочь приглашать друзей без обязательной привязки TG.

**Решение**: переход с `REF<telegram_id>` на `REF<users.id>` для генерации новых ссылок. Все вход-точки парсера работают с обоими форматами через `_resolve_inviter()` (новый → users.id, legacy → telegram_id).

**Изменения**:
- `backend/app/routes/referrals.py`:
  - `_parse_ref_code`: `REF\d{1,15}` (раньше требовало 4..15 — слишком жёстко для users.id, которые могут быть от 1 цифры).
  - Новая функция `_resolve_inviter(conn, parsed_id)` — двух-режимный поиск (id → telegram_id legacy).
  - `referrals_summary`: ref_code = `REF<bot_user_id>` для всех. web_invite_link = `/r/REF<bot_user_id>` для всех. TG-deeplink `t.me/bot?start=ref_<tg_id>` — только если `telegram_id > 0`.
  - `capture`, `pending`, `activate_pending`: используют `_resolve_inviter`; self-ref проверка по `users.id` (не по telegram_id).
- `backend/app/routes/promo.py`:
  - `_redeem_referral_code_sql(invited_uid, code)`: сигнатура изменена с `invited_telegram_id` на `invited_uid` (всегда положительный).
  - Резолв инвайтера: сначала по `users.id`, затем по `telegram_id` (legacy fallback).
  - Вызов в `redeem()`: передаём `claims.bot_user_id` вместо `claims.telegram_id`.
- `frontend/app/referrals/page.tsx`: убран блокирующий баннер «Привяжите TG чтобы пригласить». Теперь:
  - Если есть TG-deeplink — показываем все 3 формата (web-ссылка, REF-код, TG-deeplink).
  - Если нет TG-deeplink — показываем 2 формата (web-ссылка, REF-код) + мягкий tooltip «Хотите TG-deeplink? Привяжите TG → /dashboard/account». Не блокирует.

**Совместимость**: старые ссылки `REF<telegram_id>` (которые юзеры могли уже скопировать) продолжают работать через legacy-фолбэк в `_resolve_inviter`.

**Эффект**: OAuth-юзер получает рабочую реф-ссылку **сразу после регистрации**. Привязка TG больше не нужна для реф-программы — становится **опциональным удобством** (для TG-deeplink). Сайт полностью независим от бота.

---

### Аудит F: OAuth-юзеры и финальное parity (2026-05-20, поздно)

**Цель: убрать сломанные сценарии для юзеров, логинящихся через Яндекс/Mail (без TG-привязки).**

#### Что работает «из коробки» для OAuth-юзеров (благодаря `users.telegram_id = -users.id` хранится в БД):
- Free/Pro/AI отчёты (`_consume_free_report/paid/ai` ищет по telegram_id, находит OAuth-юзера).
- Оплата (Robokassa Shp_user = отрицательное, бот при complete payment делает `get_user(tid)` — находит).
- Push-уведомления (через bot_user_id).
- Web-документ юр.статуса в реф-программе (через bot_user_id).
- Просмотр баланса, отчётов, фидбека, промокодов.
- Общие и личные (по target_user_id=bot_user_id) промокоды.

#### Что было сломано и починено в этой подсессии:
- **Реф-программа для OAuth без TG**: `_parse_ref_code` принимает только `REF\d{4,15}` → отрицательный код невалиден. Backend `referrals_summary` теперь:
  - Если `telegram_id > 0` → возвращает invite_link / ref_code / web_invite_link нормально.
  - Если `telegram_id < 0` → пустые строки + новый флаг `needs_tg_for_invite=True`.
- **Frontend `/referrals` MainTab**: если `needs_tg_for_invite=True`, рендерит баннер «Привяжите Telegram чтобы пригласить друзей» с кнопкой → `/dashboard/account`. Реф-баланс и приём приглашений работают независимо.
- **Онбординг для OAuth-юзеров**: в `oauth.py:_upsert_oauth_user` убрал `web_onboarded_at=now()` при INSERT — модал онбординга теперь покажется при первом заходе на `/dashboard` (как у TG-юзеров).
- **Новая страница `/dashboard/account`**: показывает текущие способы входа. Для OAuth-юзеров (`telegram_id <= 0`) — TelegramLoginButton-виджет с onSuccess → POST `/api/auth/telegram/link`.
- **Новый endpoint `POST /api/auth/telegram/link`** (в `routes/auth.py`):
  - Принимает TelegramLoginPayload, верифицирует подпись виджета.
  - Проверяет: другой TG не привязан → 409.
  - Если в users есть параллельная TG-запись с этим tid → **мерджит**: суммирует балансы (paid/ai/referral_balance/total_earned/withdrawable), переназначает `user_id` в reports / payments / promo_code_redemptions / referrals (inviter+invited), удаляет дубликат.
  - Обновляет `users.telegram_id, username, full_name` в OAuth-аккаунте.
  - Перевыдаёт JWT с положительным `tid`. Возвращает `MeResponse`.

#### Финальная parity бот vs сайт (полное):
| Бот (`lk_*` callback) | Сайт | Статус |
|---|---|---|
| lk_buy + lk_buy_reports + lk_buy_ai_reports | `/buy` | ✅ |
| lk_example (пример PDF) | `/` блок «📑 Разделы AI-отчёта» | ✅ |
| lk_history | `/reports` | ✅ |
| lk_plans | `/pricing` + `/` | ✅ |
| lk_promo | `/promo` | ✅ |
| lk_referrals + ref_copy_link/promo + history | `/referrals` (5 табов) | ✅ полный flow |
| lk_report_batch + lk_report_single | `/dashboard/new` (КН/Адрес/Координаты/Карта) | ✅ |
| TG-команды `/start /help /kn` | UI кнопки | ✅ |
| Голосовой ввод КН | — | ⏭️ не нужен |

**Файлы изменены в подсессии F (с бэкапами):**
- backend: `app/routes/{oauth,referrals,auth}.py`
- frontend: `app/referrals/page.tsx`, `lib/api.ts`, **новая `/dashboard/account/page.tsx`**

**Тесты:** full_audit → 0/0 на 23 страницах × 2 viewport, backend импортируется с 84 routes.

---

### OAuth-вход (2026-05-20)

- **Активные провайдеры на `/login`**: Telegram (рекомендуемый, через widget) + Яндекс + Mail.ru. VK ID — решили НЕ подключать (плашка убрана из UI 2026-05-20). Email-логин (login/password) тоже не делаем.
- БД: `users.yandex_id`, `users.vk_id`, `users.mail_id` (все TEXT NULL UNIQUE через partial index). Колонки VK/Mail оставлены на будущее — место в БД не стоит.
- Backend: `app/routes/oauth.py` (новый) — endpoints `/api/auth/{yandex,mail}/start` и `/callback`. State HMAC-подписан JWT_SECRET, в HttpOnly+Secure+SameSite=Lax cookie на 10 минут. После успешного callback'а — find-or-create в users по `<provider>_id`, выдача JWT через `_set_session_cookie`, redirect на `/dashboard`.
- `app/routes/oauth.py` использует `python-dotenv.load_dotenv()` для чтения `YANDEX_OAUTH_CLIENT_ID/SECRET` и `MAIL_OAUTH_CLIENT_ID/SECRET` (pydantic-settings читает .env только в свой класс Settings, не кладёт в `os.environ`).
- Для OAuth-юзера без TG-привязки: `users.telegram_id = -users.id` (отрицательный placeholder, не пересекается с реальными положительными TG-id). При логине JWT-claim `tid` = тот же отрицательный = -bot_user_id. Когда юзер привяжет TG — `telegram_id` обновится на положительный.
- **`.env`**: `YANDEX_OAUTH_CLIENT_ID=6c2a6ca030124b55a00b45021598d4df`, `MAIL_OAUTH_CLIENT_ID=019e44a8be2f73f39cb695ab832c7145` (secrets юзер может перегенерить кнопкой ↻ в консолях провайдеров — потом просто заменим в `.env` и kickstart).
- **Frontend `/login`**: убрал `DisabledMethod` (плашки «Скоро»), grid с `grid-cols-2` (Яндекс + Mail.ru). Telegram-widget сверху как «Рекомендуемый».
- **Email-уведомления (SMTP)** — реализованы структурно (mailer.py + worker + UI-виджет на dashboard) **но временно отключены** (нет credentials). Юзер решил отложить решение про конкретного провайдера. SendPulse/Brevo не подошли (иностранные), Sender.ru как кандидат на будущее. Включается одним добавлением `SMTP_HOST/PORT/USERNAME/PASSWORD/FROM_EMAIL/FROM_NAME` в backend `.env` + kickstart — без правок кода.

### Самодостаточность сайта (2026-05-20)

Цель: «сайт и бот связаны только балансом и админкой, можно работать с любым независимо».

**Блок A — Реф-программа целиком (договор + документ + админ-подтверждение)**
- `bot/services/robokassa_server.py`: новые `/internal/contract/{build,open,accept}` и `/internal/admin/referral/{pending,confirm,reject}`. Localhost-only. Переиспользуют `build_user_contract_pdf`, `accept_referral_contract_v2` (таймер 90с), `confirm_referral_status`, `reject_referral_status`.
- БД миграция: `users.referral_status_doc_web_path TEXT NULL`.
- Web backend `routes/referrals.py`: расширил `ReferralSummary` (contract_accepted_at, contract_number, contract_revoked, status_doc_uploaded, needs_contract, needs_status_doc, rejection_reason). Добавил endpoints `/api/referrals/{contract.pdf,contract/open,contract/accept,status_doc(POST/GET)}` и admin `/api/referrals/admin/{pending,pii/<tg>,status_doc/<tg>,approve/<tg>,reject/<tg>}`.
- Документ статуса хранится локально в `data/referral_docs_web/<user_id>_<ts>.<ext>` (PDF/JPG/PNG, до 10 МБ).
- Frontend `/referrals`: добавил **2 новых таба** «Договор» (preview PDF в iframe + таймер 90с + чекбокс согласия + кнопка «Согласен») и «Документ статуса» (file upload + статус подтверждения + причина отказа если был).
- Frontend `/admin/referrals`: добавил секцию «📑 Очередь подтверждения юр.статусов» со счётчиком и ссылками на детали.
- Frontend `/admin/referrals/[telegram_id]`: новая страница — полная PII (расшифрованная Fernet), кнопка «Скачать документ», кнопки **Подтвердить** / **Отказать** (с обязательной причиной).
- Бэкап: `*.BACKUP-pre-contract-flow-20260520-*`.

**Блок B — Web Push (после закрытия вкладки)**
- Установил `pywebpush` + `apscheduler` в backend venv.
- VAPID ключи: private в `backend/data/vapid/private.pem` (mode 0600), public раздаётся через `/api/push/vapid_public_key`.
- БД: таблица `push_subscriptions(user_id, endpoint UNIQUE, p256dh, auth, failed_count)` + `reports.push_sent_at TIMESTAMP`.
- Backend `routes/push.py`: `subscribe`, `unsubscribe`, `status`, `test`, `vapid_public_key`.
- Worker `app/workers/push_worker.py`: APScheduler-job, тикает каждые 30с, ищет completed-отчёты без push_sent_at, шлёт push на все подписки юзера, помечает. Dead подписки (404/410) автоматом удаляются.
- Frontend `public/sw.js`: Service Worker слушает push event, показывает Notification, клик ведёт на `/reports/<id>` (через `clients.matchAll` + navigate).
- Frontend `lib/pushClient.ts`: `ensurePushSubscription()` — graceful chain permission → SW register → subscribe(VAPID) → POST. Вызывается на `/reports/[id]` после permission granted + кнопкой на дашборде.

**Блок C — Email уведомления (готовая структура; SMTP подключится через .env)**
- БД: `users.email TEXT`, `users.email_notify_enabled DEFAULT 1`, `reports.email_sent_at TIMESTAMP`.
- `app/services/mailer.py`: SMTP-обёртка с STARTTLS/SSL. Если `SMTP_HOST` не задан — `send_email()` возвращает False БЕЗ exception (graceful no-op). HTML+text шаблоны: `send_report_ready`, `send_payment_succeeded`, `send_payout_sent`.
- Backend `routes/account.py`: `GET/POST /api/account/email` (email + notify_enabled + smtp_configured флаг для UI).
- Worker `push_worker.py` обновлён — в одном тике делает push И email параллельно. `email_sent_at` помечается даже если SMTP off (чтобы не зацикливать).
- Frontend dashboard: виджет «🔔 Уведомления о готовности отчёта» — email + кнопка «Включить браузерные».
- **Чтобы включить email**: положить в backend `.env` SMTP_HOST/PORT/USERNAME/PASSWORD/FROM_EMAIL/FROM_NAME → kickstart backend. Юзер выбрал Brevo (300 emails/день free).

**Блок D — Полноценный карта-пикер**
- Установил `leaflet` + `@types/leaflet` через npm.
- `components/MapPicker.tsx` (dynamic SSR-safe): OSM basemap + НСПД WMS overlay (`https://nspd.gov.ru/api/aeggis/v3/cadastral_parcels/wms` layer `36048`, прозрачность 70%). Клик → маркер + callback `onPick(lat, lon)`.
- `/dashboard/new`: добавил 4-й таб «🗺️ Карта» (Координаты переименован в «📌»). При клике карта вызывает `lookupByCoords` → автоматический resolve КН.

**Файлы** (все с бэкапами `.BACKUP-pre-*-20260520-*`):
- Bot: `bot/services/robokassa_server.py`
- Backend: `app/main.py`, `app/routes/{referrals,push,account}.py`, `app/services/mailer.py`, `app/workers/push_worker.py`
- Frontend: `app/referrals/page.tsx`, `app/admin/referrals/page.tsx`, `app/admin/referrals/[telegram_id]/page.tsx`, `app/dashboard/page.tsx`, `app/dashboard/new/page.tsx`, `app/reports/[id]/page.tsx`, `components/MapPicker.tsx`, `public/sw.js`, `lib/{api.ts,pushClient.ts}`

**Тесты**: full_audit 0/0 на 23 страницах × 2 viewport. Backend поднимается с 79 роутами.

---

### Доводка тарифов и реф-программы (2026-05-20, утро)
- **ReportCard** — убран блок «Диапазон» (`price_min/price_max` из НСПД). В боте этого давно нет, на сайте отображалось рудиментарно.
- **Главная (`app/page.tsx`)** — TIERS Free: убран дубликат «Кадастровый номер…», добавлена «Цена за сотку». Из TIERS Free и Pro убраны has:false-строки «Инфраструктура, коммуникации, транспорт» и «Юр. выводы и PDF» — теперь Free/Pro карточки на мобилке вообще не упоминают эти разделы (только AI). Таблица COMPARISON оставлена — там «—» по этим строкам корректно.
- **/pricing** — Free расширен: «Кадастровая стоимость», «Цена за сотку», «Сделки Росреестра по кварталу».
- **/help** — мобильный TierBlock Free: добавлена «Цена за сотку». В Section 4 реф-программы статусы: `ФЛ / самозанятый / ИП / ООО` (было без ООО).
- **Реф-программа `ooo` (как в боте)**:
  - Backend `LEGAL_STATUSES = {individual, self_employed, ip, ooo}`, валидация `ogrn_required_for_ooo`.
  - Frontend `LEGAL_LABELS.ooo = "ООО"`, новая ветка `needsOoo` → блок «Реквизиты ООО» с обязательными ОГРН (13 цифр), КПП (9 цифр), юр.адресом. ИП — только ОГРНИП + юр.адрес (раньше форма ИП тащила «ОГРН (если ООО)» внутрь себя — это убрано).
  - Сетка статусов: `sm:grid-cols-2 lg:grid-cols-4`.
- Все 6 файлов с бэкапом `.BACKUP-pre-ooo-tariffs-20260520-092237`. Билд фронта 0 ошибок, full_audit 0/0 на 23×2.

### 2026-05-20 (поздний вечер): блоки H/I/J/K + полировки

**H. QA-аудит сайта**
- Антифрод: rate-limits на всех POST endpoints (от 5/hr до 120/min); antibot middleware (score-based 429 при 3+ признаках); CORS (zbrcheck.ru + www + localhost, duckdns убран); JWT HttpOnly+Secure+SameSite=Lax; self-ref защита через users.id; freshness check; anti-replay промокодов; CSRF state для OAuth; `/internal/*` боты — localhost-only + опц shared token.
- UX-проверка: уже залогинен→/login→/dashboard, free_limit→429, Pro/AI нет баланса→Robokassa, payment success/fail чёткие пути с кнопками восстановления.
- Smoke: создал synthetic OAuth user (id=118, tid=-118), все endpoints (me/reports/referrals/account/push) → 200. Удалил после.

**I. UX-полировка**
- `bot_account_required: войдите через Telegram` в `reports.py:278` → **`account_not_found: сессия повреждена, перелогиньтесь`** (корректно для всех типов входа).
- `free_limit_exhausted` на `/dashboard/new` теперь рендерится как **жёлтый блок с 3 кнопками**: «Заказать Pro · 599 ₽» (setChosen pro), «Заказать AI · 1 200 ₽» (setChosen ai), «Купить пакет со скидкой →» (/buy).
- **Return-to redirect после оплаты пакета**: на `/buy` `useEffect` сохраняет `document.referrer.pathname` в `localStorage.zw_pay_return_to`. На `/payment/success` при `kind=package` редиректит туда (валидируем что path / без // и не /login/payment/buy). Для `kind=order` — без изменений на `/reports/{order_id}`.

**J. Живая анимация на pending-странице (как в боте)**
- `globals.css`: 3 keyframe — `animate-search-sweep` (лупа раскачивается ±6px и ±12°), `animate-progress-slide` (indeterminate бегущая полоска), `animate-dots` (анимированное «... »). Чистый CSS, GPU-only, prefers-reduced-motion уважается.
- Новый компонент `components/PendingProgress.tsx`:
  - Time-based simulation: 1Hz setInterval, считаем elapsed от `created_at`
  - 3 шага для free/pro (~40 сек), 5 шагов для AI (~3-5 мин — точно как в боте)
  - Иконки ⏳ → ▶️ → ✅, текущий шаг подсвечен синим бордером
  - Прогресс-бар с градиентом + бегущий shimmer
  - Плато на 95-99% если процесс затянулся (не показываем 100% до фактического completion)
- Подключено в `/reports/[id]` PendingView (старый удалён). В ReportDataView AI section — те же `animate-dots` + `animate-progress-slide` для мини-индикатора.

**Карта-пикер: дополнения**
- На `/dashboard/new` режим «🗺️ Карта»: добавлен **поиск адреса через Photon** (komoot.io, бесплатный OSM-геокодер без API-ключа). Nominatim тестировался — забанен с серверного UA, переключились на Photon. Триггер — Enter / кнопка «Найти» (не auto-search, fair-use).
- Sort приоритет: РФ→СНГ→др. Клик по результату → `flyTo(lat, lon, 17)` + маркер. КН резолвится только по клику на участок через `/api/lookup/cadastral`.
- **Убран attributionControl с карты** (наезжал на UI) → мелкая подпись под картой в JSX: «Карта: © OpenStreetMap · © НСПД (Росреестр)» с ссылкой на лицензию.
- **Фикс PNG-иконок маркера** — Next.js не резолвит пути из `node_modules` после сборки (показывались «?» вместо пинов). Скопировал PNG в `frontend/public/leaflet/{marker-icon, marker-icon-2x, marker-shadow}.png`, в MapPicker прописал `L.Icon.Default.mergeOptions({iconUrl: "/leaflet/...", ...})`.

**Email-блок убран с dashboard**
- Юзер решил отложить SMTP-настройку. Виджет «🔔 Уведомления о готовности отчёта» на `/dashboard` упрощён до одной колонки — только Web Push (включить/granted/denied/unsupported). Левый блок email с полем ввода и предупреждением убран.
- В `/dashboard/account` инфо-блок про привязку TG: «Web Push + Email + в Telegram-бот» → «Уведомления … будут приходить и в Telegram-бот (помимо браузера)».
- Backend (mailer.py, account.py, push_worker.py с email-частью) **оставлен** — включается одним добавлением `SMTP_HOST/PORT/USERNAME/PASSWORD/FROM_EMAIL/FROM_NAME` в `.env` + kickstart, без правок кода.

**K. Партнёрская программа (B2B)**

Подход: партнёр = инвайтер с удобной интеграцией. Оплата всегда через нас. Партнёр получает 5% реф-комиссии автоматически. **Никакого API-биллинга, балансов партнёров, webhook'ов** — всё через существующую реф-программу.

- **`/partners`** (новый публичный лендинг): Hero «Зарабатывайте 5%», карточки услуг, интерактивный калькулятор «N клиентов/мес → заработок», 4 шага «Как стать партнёром», условия + ссылка на оферту, 4 целевые аудитории.
- **`/r/[code]?kn=...`** — deep-link с pre-filled КН:
  - При заходе сохраняем КН в `localStorage.zw_intent` (TTL 1ч, валидируем формат)
  - UI: зелёный блок «📍 Партнёр уже подобрал участок: <КН>»
  - После регистрации `/dashboard` находит intent → редирект на `/dashboard/new?kn=...&tariff=...` с предзаполненной формой
- **`GET /api/v1/services`** (новый публичный endpoint без auth, лимит 120/min): JSON с услугами, пакетами, шаблоном deep-link, partner_program info (5%, 5000₽ min). Партнёры кэшируют у себя.
- **`/partners/integration`** — техническая документация: формат deep-link, готовые сниппеты (TG-бот aiogram, HTML, JS), описание API, FAQ.
- Любой зарегистрированный юзер — автоматически партнёр (через существующую реф-программу).

**Финальное состояние на запуск:**
- Backend 85 routes, full_audit 0/0 на 25 страницах × 2 viewport
- 40/40 задач сессии закрыты (A1-A8, B, C, D, E1-E6, F1-F7, G1-G3, H1-H5, I1-I3, J, K1-K4)
- VK ID НЕ подключаем (решение юзера)
- SMTP отложен (решение юзера)

---

## Что отложено на post-launch

| Что | Почему |
|---|---|
| Email уведомления (SMTP) | Юзер решил отложить. Структура backend готова, включается 6 строками в `.env` + kickstart, без правок кода |
| VK ID OAuth | Юзер решил НЕ подключать (UI убран). При желании — backend-паттерн как у Yandex/Mail |
| Перегенерация Yandex/Mail Client Secrets | Юзер должен сделать в консолях (засветились в скриншотах) → прислать → я подменяю в `.env` |
| Уборка duckdns DNS | Юзер удаляет DNS у регистратора duckdns когда удобно |

## Перед запуском

1. ✅ Юзер уже обновил Robokassa cabinet URLs на pay.zbrcheck.ru
2. ⏰ **Через 24-48ч**: убрать `zemlyaweb.duckdns.org` из CORS (уже не там), плюс убрать DNS у duckdns при желании
3. ⏰ **Перед live**: переключить `ROBOKASSA_TEST_MODE=0` в web `.env`
4. Тест-flow: TG-login → dashboard → /dashboard/new (КН/Адрес/Координаты) → оплата → отчёт → notification

## Связанные memory-файлы
- `zemlya_web_overview.md` — обзор проекта (структура, env, JWT)
- `zemlya_domain_zbrcheck_migration.md` — миграция домена (там напоминание про duckdns)
- `zemlya_audit_2026-05-19.md` — функциональный аудит

## Технические якоря
- SSH mac mini: `ssh -i ~/.ssh/id_ed25519 cebers_mini@192.168.0.107`
- VPS root: `sshpass -p '7pln1sZKoI' ssh root@193.108.113.35`
- Web backend: `/Users/cebers_mini/Services/zemlya-web/backend` (Python 3.12)
- Web frontend: `/Users/cebers_mini/Services/zemlya-web/frontend` (Next.js 14)
- Bot: `/Users/cebers_mini/Services/zemlya-bot` (Python 3.9)
- БД shared: `/Users/cebers_mini/Services/zemlya-bot/data/db/zemlya_bot.db`
- Admin owner: telegram_id 1005954706 (CeBers227); active admins also include osminog (78293831); inactive Cebers_dev (8002719406) — фильтр охватывает всех admin_users независимо от is_active
- Real revenue: 2999₽ (только AY_Popov 440755555) после фильтра is_test + admins
- Frontend build PATH: `export PATH=/opt/homebrew/bin:/opt/homebrew/opt/node@22/bin:$PATH`
- full_audit.py — 0/0 на 23 страницах × 2 viewport
