# SHARENTA — сверка кода и план реализации

**Дата сверки:** 24 сентября 2026  
**Источник:** дорожная карта `docs/product/SHARENTA_PRODUCT_ROADMAP.md`.  
**Статус:** волны 1–6 закрыты в коде (P1, P2-5 instant book, P2-6 saved search, prefs без SMTP). P3 не начат.

Цель плана: закрыть путь **найти → написать → договориться → арендовать**, опираясь на текущий контур (`applyContourAction`), без нового кабинета, мессенджера, PRO и платёжного провайдера.

---

## 1. Что подтверждено в коде

Утверждения дорожной карты, которые совпадают с репозиторием:

| Утверждение | Где в коде |
| ----------- | ---------- |
| Поиск по title/brand/model/specs/категории/цене/городу/датам | `src/lib/filter.ts`, `src/app/api/catalog/route.ts` |
| Антидаблбук, в том числе instant book | `canCreateBooking`, `BUSY_BOOKING_STATUSES`, `pg_advisory_xact_lock` |
| KYC обязателен для **заявки**, не для чата | `createBooking` → `identityOk`; `openConversation` только auth |
| Текст заявки — user-message в том же треде | `createBooking` |
| PDP: «Написать» + бронь | `src/app/listings/[id]/page.tsx` |
| Встречные даты | `proposeBookingDates` / `resolveProposedDates` |
| Instant book | `listing.instantBook`; `createBooking` → `documents` + deal docs |
| Новые пины | `listingPlaceFromInput`; город из `CITIES` + ручной пин |
| Календарь в create | `draft.unavailableDates` + `MonthCal` |
| Пустой поиск + saved search | `CityEmptyState`, `watchCity` query+фильтры |
| City watch notice | только `setListingStatus` → `active` |
| Prefs уведомлений | `notificationPrefs`, `auditChannelsFor`; SMTP/FCM нет |
| Похожие, views, repeat, избранное в шапке | PDP / analytics / bookings / app-shell |
| Тикеты с перепиской | `replyTicket` |
| Owner resubmit | rejected → moderation |
| Android | те же contour actions |
| PRO без entitlements | `buyPromotion` → `purchases` |

---

## 2. Где дорожная карта уже устарела или слишком жёсткая

Эти пункты **не надо делать заново**.

1. **`listing.verified` при публикации уже false в контуре.**  
   `publishListing` ставит `verified: false`, `available: false`, тест `publish cannot self-verify`.  
   В `publishDraft` (store) до сих пор уходит `verified: true` — сервер это перетирает. P1-7 сужается до: почистить store, скрыть/переименовать сорт distance, не путать бейдж юнита с KYC пользователя.

2. **Чат до KYC уже разрешён.**  
   Узкое место сценария A («нельзя даже написать») преувеличено для чата. Стоп только на `createBooking`. P1-1 закрывает мягкий шаг BlaBlaCar **без смены KYC-политики**.

3. **Города для пина уже есть.**  
   `CITIES` содержат `lat/lng` (Москва/СПб/Казань/Дубай/Стамбул). P1-3 не требует геокодера: город из списка + пин с дефолтом центра города.

4. **`unavailableDates` уже в модели и в `ListingPatch`.**  
   P1-4 — UI мастера + прокинуть массив в `publishListing`, не новая таблица.

5. **City watch уже есть — не плодить второй тип подписки в P1.**  
   P2-6 расширяет его query+фильтрами; сейчас notice уходит в момент `publishListing` (ещё moderation). Для сохранённого поиска срабатывать нужно на переход в `active`.

6. **Два статуса брони (`confirmed` vs `documents`)** остаются. P1-6 — копирайт/чеклист, не новая state-машина.

---

## 3. Решения владельца продукта, которые план принимает

Из обновлённой дорожной карты (не из §10 — там вопросы ещё висят):

| Тема | Решение в карте | Как делаем |
| ---- | --------------- | ---------- |
| Instant book | Оба режима | Флаг на объявлении после P1-4. По умолчанию выкл = живое «да» |
| Сохранённый поиск | Да, opt-in | После P1-5; notice после модерации в `active` |
| Каналы | Заложить in-app + email + browser push и типы | Модель предпочтений и UI настроек; **SMTP/FCM не подключать** в этой волне |
| PRO / оплата / want-ad / выплаты | Не в этой волне | Витрину не расширять; не прятать `/services` пока нет отдельного решения |
| Стек | Не переписывать | Тот же Next.js, контур, shadcn, Android догоняет те же экраны |

**Ещё не решено — в коде не трогаем политику, только обходим:**

- KYC до заявки остаётся как сейчас (`identityOk`). Чат — без KYC.
- Смысл бейджа «Проверен»: не ставим его новым юнитам; сиды не чистим массово.
- Follow владельца — не делаем (P3-10).
- Гео мира: только существующий список `CITIES` + ручной пин внутри города.
- Android P1 после веба, кроме P2-8.

Если нужно иное — остановить волну до ответа.

---

## 4. Что сознательно не входит в реализацию

- Новый кабинет / второй CRM / новый мессенджер  
- Платёжный провайдер, entitlements PRO, реферальные выплаты  
- Want-ad, комплекты, почасовка, мультиединицы, филиалы  
- OCR KYC, УКЭП, SMS  
- Kafka, микросервисы, CDN, отдельная аналитическая платформа  
- Перепись Android с нуля  
- Смена `BookingStatus` на третью параллельную машину (deal status не дублируем)

---

## 5. Волны реализации

Каждая волна: контур + UI веба + тесты `npm test` + браузерный сценарий. Android — отдельным хвостом волны, кроме 2.8.

### Волна 1 — связаться и понять статус

**P1-1. Чат = канал договорённости**

- PDP: рядом с «Забронировать» кнопка «Написать» → тот же `openConversation(ownerId, listingId)`, что иконка. Гость — login return-to как у брони. KYC не требовать.
- `createBooking`: если `message` не пустой — user-message в связанный тред (не только system). Тот же `convId`, что сейчас `c-${listingId}-${actorId}`.
- Экран брони: «Перейти в чат» уже есть — оставить один тред.

Файлы: `src/app/listings/[id]/page.tsx`, `src/lib/contour.ts` (`createBooking`), `src/lib/contour.test.ts`, Android `ListingScreen` / `Store.kt` по возможности сразу.

Готово: текст заявки виден владельцу в чате; с карточки открывается тот же тред.

**P1-6. Чеклист сделки без новой машины**

- На `/bookings/[id]` линейка шагов поверх текущих статусов: заявка → ответ → подписи → фото выдачи → аренда → фото возврата → отзыв.
- Кнопка «Подтвердить» в копирайте: «Принять и сформировать договор» (факт: статус станет `documents`).
- Одна видимая следующая кнопка у каждой стороны.

Файлы: `src/app/bookings/[id]/page.tsx`, при необходимости маленький `src/lib/deal-steps.ts` (чистая функция статус → шаг). Не трогать `resolveStatusChange`.

**P1-7. Честные бейджи (узкий скоуп)**

- Store: не слать `verified: true` в draft (контур и так false).
- Сорт «По расстоянию»: подпись «По городу / как в каталоге» или убрать, пока нет гео пользователя. Не считать GPS.
- Бейдж «Проверен» на карточке оставить только если `listing.verified` (сиды); копирайт не обещать KYC.

Не делать: автопостановку verified при approve модерации — пока не ответили на вопрос 10.

### Волна 2 — объявление там, где вещь, и когда свободна

**P1-3. Точка на карте**

- В мастере: select из `CITIES` (не свободный ввод мира).
- Дефолт `lat/lng` = центр выбранного города; карта OSM с перетаскиваемым пином (как `ListingsMapCanvas`).
- `district` не подставлять «Арбат», если город не Москва; пусто, пока пользователь не ввёл.
- `distanceKm` не показывать на PDP, если это заглушка (или всегда скрыть до живой гео).
- Edit listing: те же поля в `ListingPatch` (`city`, `district`, `lat`, `lng`).

Файлы: `src/lib/types.ts`, `src/lib/store.tsx` `publishDraft`, `src/lib/contour.ts` `publishListing`/`updateListing` (убрать `\|\| 55.752` как безусловный fallback, если город не Москва), `src/app/landlord/create/page.tsx`, `src/app/cabinet/listings/[id]/page.tsx`.

Тесты: Казань ≠ Арбат на карте; `listingInCity` не ломается.

**P1-4. Закрытые дни в мастере**

- На шаге проверки: `MonthCal` + `draft.unavailableDates`.
- Уже поддержано `publishListing.unavailableDates`.
- Бронь на закрытый день по-прежнему режется `listingFreeOn` / `canCreateBooking`.

Файлы: draft в store, create page. Не новый календарный продукт.

### Волна 3 — договориться о датах

**P1-2. Встречные даты**

Минимальная модель **без нового BookingStatus**:

```ts
// на Booking
proposedStartDate?: string
proposedEndDate?: string
proposedBy?: "owner" | "renter"
rejectNote?: string
```

Поведение:

- Пока `requested`, владелец: «Предложить другие даты» → overlap check тем же `canCreateBooking` (игнорировать текущую бронь в busy set) → поля proposed + сообщение в чат.
- Арендатор: принять → копировать в `startDate/endDate`, пересчитать `total`, очистить proposed; статус остаётся `requested`; владелец потом подтверждает как сейчас.
- Отказ: как сейчас `rejected` + опциональный `rejectNote` в чат.

Миграция Postgres: additive колонки в `bookings` (как `005_user_profile_kyc`). JSON-контур подхватит поля сам.

Файлы: types, relational-sql + новый migrate id, contour-tables map, `setBookingStatus` или отдельные actions `proposeBookingDates` / `acceptProposedDates`, UI booking detail, тесты overlap.

Не делать instant book в этой волне.

### Волна 4 — поиск не тупик

**P1-5 + транспорт-подкатегории**

- Пустая выдача поиска: CTA «сбросить фильтры», «все города», соседние города (`nearbyCities` как в `CityEmptyState`), watch города если пусто из-за города. Не want-ad.
- `TRANSPORT_SUB`: `href` с `setQuery`/`setFilters` (велосипеды → query «велосипед» + category transport), не голый `/search`.

Файлы: `src/app/search/page.tsx`, `src/app/categories/page.tsx` `TransportList`, reuse `CityEmptyState` или общие CTA.

### Волна 5 — возврат и скорость договорённости (P2)

Порядок как в дорожной карте:

| ID | Суть | Зацепка в коде | Не делать |
| -- | ---- | -------------- | --------- |
| P2-3 | «Запросить снова» на `completed` | `/bookings`, `readBookDates` | новый флоу брони |
| P2-1 | 4–8 похожих: та же категория+город, `active`, не себя | PDP, `applyFilters` / catalog | ML |
| P2-2 | инкремент `views` раз за сессию; лента recently viewed в кабинете (localStorage + опционально контур) | `listing.views` уже в SQL | отдельная аналитика |
| P2-4 | сердце/счётчик избранного в шапке | `/favorites` живой | коллекции, follow |
| P2-7 | 3–5 шаблонов в чате, вставка текста | `sendMessage` | бот |
| P2-8 | Android `referralStats` как веб или убрать MiniStat | `ReferralScreen.kt` | паритет всего Android |
| P2-9 | `ticketMessages[]` + ответ админа | tickets table additive | новый мессенджер |

**P2-5 Instant book** — после волны 2–3:

- `listing.instantBook?: boolean` в create/edit, по умолчанию false.
- В `createBooking`: если флаг и overlap ок и KYC ок → сразу тот же путь, что confirm (`documents` + generateDealDocuments).
- Тест: overlap не обходится.

**P2-6 Saved search** — после P1-5:

- Расширить `CityWatch` или рядом `SearchWatch { query, filters }` — один inbox notices.
- Срабатывать на `setListingStatus` → `active`, не на publish в moderation.
- Пользователь включает сам с пустого поиска.

### Волна 6 — модель уведомлений без доставки

Соответствует пункту 17 дорожной карты.

- Настройки в `/cabinet/settings`: типы (сообщение, заявка, статус сделки) × каналы (in-app всегда; email/browser push — чекбоксы, disabled «скоро» или сохранение флага).
- Писать предпочтение в контур (`notificationPrefs` на user).
- In-app как сейчас. Аудит `channel` уже есть.
- **Не** подключать SMTP, FCM, web-push ключи, пока нет отдельной задачи на доставку.

PRO / `/services` / want-ad / выплаты — стоп до отдельных решений.

---

## 6. Порядок и зависимости (сжато)

```
1.1 чат+текст заявки
1.6 чеклист статусов     ── параллельно 1.1
1.7 бейджи/сорт          ── параллельно 1.1
        │
2.3 пин на карте
2.4 календарь в create
        │
3.2 встречные даты  ← нужен чат и календарь
        │
4.5 пустой поиск + подкатегории транспорта
        │
5. P2-3 → P2-1 → P2-2 → P2-4 → P2-7 → P2-8 → P2-9
5. P2-5 instant  ← после 2.4
5. P2-6 saved search ← после 4.5; notice на active
        │
6. prefs уведомлений (без SMTP)
```

Существующие модули не заменять: `applyContourAction`, `canCreateBooking`, `useCatalogListings`, `CityEmptyState`, `DealWorkspace`, `watchCity`, dual CRM.

---

## 7. Тесты по волнам

Обязательный регресс каждой волны: `npm test` (уже есть contour/deal/catalog).

| Волна | Добавить |
| ----- | -------- |
| 1 | `createBooking` с message → user chat line; KYC не нужен для `openConversation` (уже так — зафиксировать assert) |
| 1 | `dealStep(status)` маппинг, не ломает `canOwnerAct` |
| 1 | publish → `verified === false` (уже есть) + store не шлёт true |
| 2 | listing город Казань, lat около 55.79 не 55.752; edit меняет пин |
| 2 | unavailableDates из draft блокируют бронь |
| 3 | propose dates overlap forbidden; accept обновляет span; reject пишет чат |
| 4 | пустой поиск возвращает CTA; transport sub ставит query |
| 5 | instant + overlap; saved search не триггерится на moderation; ticket reply права |
| 6 | prefs сохраняются, in-app всё ещё пишется |

Браузер (после каждой волны, не вместо unit):

- Арендатор без KYC: карточка → Написать → сообщение уходит.
- С KYC: заявка с текстом → чат содержит текст → владелец предлагает даты → принять → подтвердить → чеклист.
- Хозяин: создать в Казани с закрытым днём → карта не Арбат → бронь на закрытый день нельзя.

---

## 8. Критерий готовности пилота (когда остановиться и не расползаться)

Новый арендатор: находит юнит в выбранном городе → пишет владельцу **без KYC** или шлёт заявку **с KYC** → текст в чате → владелец предлагает другие даты или принимает → обе стороны видят один следующий шаг → фото выдачи/возврата → отзыв → «запросить снова».

Новый арендодатель: фото, цена, свой пин из `CITIES`, закрытые дни → модерация → юнит на карте там, где указал → входящая заявка с вариантом не только «нет».

Ломать нельзя: нет двойной брони, гость не читает чужие чаты, KYC-копирайт без OCR, арендные деньги не «принимаются», AI не выдумывает юниты.

---

## 9. Открытые вопросы, которые ещё могут сдвинуть план

Пока молчание = значения из §3.

1. Снять KYC с заявки (оставить только на выдачу)? Сейчас не снимаем.
2. Прятать `/services` до оплаты? Сейчас не прячем и не расширяем.
3. «Проверен» = KYC владельца или ручной бейдж юнита?
4. Делать P1 сразу на Android или только веб, кроме рефералки?

---

## 10. Предлагаемый старт

Первый коммит кода — **волна 1 (P1-1 + P1-6 + P1-7)**: мало схемы, сразу виден пользовательский путь «написать и понять статус».

Реализация не начата. Нужно подтверждение: начинать волну 1 или изменить порядок.
