---
name: tbr-report-payment-never-landed
description: "ТБР: оплату экспертного отчёта (InvId 4M+) не ловил ни один из трёх путей — ветка броней ≥3M забирала всё; отчёт не был виден в кабинете вовсе"
metadata: 
  node_type: memory
  type: project
  originSessionId: d0c6c5aa-4d05-42e2-a1f9-034482b88603
  modified: 2026-08-05T12:05:42.022Z
---

**2026-08-01, «Торги без риска».** Продукт стоил 599 ₽ и **не мог быть куплен**: ни один из трёх путей подтверждения не видел счёт отчёта.

- Счёт отчёта = `WEB_REPORT_OFFSET + report_id` = **4 000 000+**.
- ResultURL в боте «Земли»: `if payment_id >= WEB_BOOKING_OFFSET` (**3 000 000**) забирал и 4M+, считал бронью, делал UPDATE по несуществующему `bookings.id` → `rowcount=0`, тишина.
- Страница возврата зовёт `/api/pay/verify`; тот при `inv >= 3M` искал `Booking` → `valid:false` → «оплата обрабатывается» навсегда.
- Поллер Robokassa обходил только таблицу `Booking`.
- `mark_report_paid` висела на `/api/report/pay/verify`, который **не звал никто** (грепом по фронту — ноль).

Доказательство, что путь никогда не отрабатывал: `lot_reports` — две записи, обе `free`, ноль `paid`.

**Починено во всех трёх местах:** ветка 4M+ в ResultURL ОБЯЗАТЕЛЬНО перед веткой броней; `/api/pay/verify` распознаёт счета отчётов и делегирует; поллер опрашивает неоплаченные отчёты и добивает генерацию для помеченных вебхуком. Проверено настоящей подписью по боевому вебхуку: `paid`, идемпотентно, чужая подпись → 400 без изменения состояния.

**Рядом — вторая половина той же дыры:** купленный отчёт **не был виден в кабинете**. `/dashboard` показывал только брони (продукт снят) и рендерил «У вас пока нет броней»; `/api/my-reports` существовал, но с фронта его не звал никто. Добавлен раздел «Ваши экспертные отчёты».

**Ловушка в данных (ПОЙМАЛА ЖИВОГО ТЕСТЕРА, чинено 05.08.2026):** `lot_reports.ai_report_id` хранит id **заказа** генерации на стороне «Земли» (`reports.id`), а НЕ готового документа (`ai_reports.id`). `/api/report/{id}/pdf` просил файл по id заказа — `/internal/ai-file` ждёт другое пространство id, файл не находился НИКОГДА. Симптом: отчёт давно собран (заказ 741 → документ 282), а в кабинете «Документ пока недоступен» третьи сутки. **Фикс:** id документа брать из `fetch_ai_report_result(order_id)["ai_report_id"]`, и только им звать `fetch_ai_pdf`. Заодно сорвавшаяся генерация (`status=failed`, как заказ 687) теперь возвращает списание на баланс — один раз, через `lot_reports.refunded_at`; раньше человек терял отчёт и не получал ничего.

**Урок:** «есть ai_report_id» ≠ «есть PDF», и одинаковый текст ошибки для «готов, но не отдаём» и «не собрался» прячет разницу на недели. Причину отказа доводить до пользователя.

Обобщение: продукт удалили (бронь), а его следы остались в трёх слоях — оферте, кабинете и диапазонах InvId. Проверять надо не «работает ли новый путь», а «кто ещё читает старую таблицу».

**05.08.2026 — нашлись ещё пять слоёв того же снятого продукта.** (1) Админка сайта и бота считала выручку и конверсию по броням → с 08.2026 показывала вечный ноль при живых продажах. (2) Кнопка админа «+ бонусная бронь» писала в `subscribers.free_bookings` — колонку, которую никто не списывает: начисляла в пустоту, списывается `lot_users.free_reports`. (3) Реф-награда называлась «бронями», хотя падала в отчёты. (4) Проверка «свежести» приглашённого смотрела только на оплаченные брони. (5) Бот обещал «пришлём, как будет готов» — воркер доставки существовал ТОЛЬКО для броней, отчёты не доезжали никогда. Мораль та же и сильнее: после снятия продукта грепать не имя таблицы, а **все места, где считались деньги и бонусы**.

**Отдельно: докстринг соврал.** `tbr/web/auth.py` писал «Яндекс/Mail OAuth — каркас готов, активируется после регистрации redirect-URI». Каркаса не было вовсе — только колонки `yandex_id`/`mail_id` в модели. Комментарий о готовности — не доказательство готовности, грепать сам код (было: 446 строк порта из zemlya-web).

См. [[zemlya_tbr_partner_scope]], [[tbr_overview]].
