# Приёмка liderra.ru — ПУНКТ №22: Отчёты (4 типа, очередь, скачивание, квота, изоляция)

> **Для исполнителя:** формат PR1 (одобрен 20.06). Каждый шаг = `🔧 Код-факт (file:line)` · `🎬 Действие` · `📋 ОТЧЁТ (было→ожидали→стало + 📸/📝)` · `❌ Если не так`. Техника «под капотом»; владельцу — человеческие карточки.

**Цель:** показать, что раздел **Отчёты** работает: клиент заказывает отчёт (несколько типов, в разных форматах), он готовится в фоне (статус виден), скачивается по **защищённой ссылке** (живёт сутки), числа в отчёте = тому, что в базе; нельзя заказать слишком много сразу (квота); клиент видит **только свои** отчёты.

**Что снимаем глазами:** раздел **Отчёты** (список, статусы, кнопка скачать), скачанный файл. Живые 📸.

**Источник сценария:** свод проверок №22 (RPT-TYPES/QUEUE/DOWNLOAD/ISO); R3b «онбординг/UI».

---

## 🔧 Код-факты (подтверждено чтением 20.06)

- **4 типа отчётов** — провайдеры: Биллинг-сводка, Выгрузка сделок, Сводка по менеджерам, Сводка по источникам — `ReportJob::TYPES` [ReportJob.php:44](../../../app/app/Models/ReportJob.php#L44); **форматы** `csv/xlsx/json/pdf` — [:51](../../../app/app/Models/ReportJob.php#L51).
- **Очередь и статусы** — заказ создаёт задачу `pending`, фоновая `GenerateReportJob` ведёт `pending → processing → done|failed`, кладёт файл — [GenerateReportJob.php:21](../../../app/app/Jobs/GenerateReportJob.php#L21), [:90](../../../app/app/Jobs/GenerateReportJob.php#L90); на dev выполняется сразу, на проде — фоновым воркером — [ReportJobController.php:174-176](../../../app/app/Http/Controllers/Api/ReportJobController.php#L174-L176).
- **Квота** — нельзя держать больше N **активных** (pending+processing) задач; превышение → отказ `_quota` — [:144-151](../../../app/app/Http/Controllers/Api/ReportJobController.php#L144-L151), [ReportJob::ACTIVE_STATUSES :40](../../../app/app/Models/ReportJob.php#L40).
- **Скачивание по защищённой ссылке 24ч** — `temporarySignedUrl(..., addHours(24))` (подпись = разовый пропуск, без логина) — [:403-413](../../../app/app/Http/Controllers/Api/ReportJobController.php#L403-L413), сам download под `signed`-middleware — [:360-364](../../../app/app/Http/Controllers/Api/ReportJobController.php#L360-L364).
- **Изоляция** — все запросы `where('tenant_id', …)` + RLS `SET LOCAL` — [:65](../../../app/app/Http/Controllers/Api/ReportJobController.php#L65), [:106-110](../../../app/app/Http/Controllers/Api/ReportJobController.php#L106-L110).
- **Сбой формата (например PDF)** — ловится в фоновой задаче → статус `failed` + лог, **без падения системы** — [GenerateReportJob.php:102](../../../app/app/Jobs/GenerateReportJob.php#L102). (Штатность PDF→failed — проверить на прогоне.)

---

## Шаг 22-1 — Заказать отчёт → готовится в очереди → готов

**🔧 Код:** [store :124-176](../../../app/app/Http/Controllers/Api/ReportJobController.php#L124-L176).
**🎬 Действие (лок):** в Отчётах заказать «Выгрузку сделок» в формате xlsx.

**📋 ОТЧЁТ:**
| Что | Было | Ожидалось | Стало (факт) | Чем подтверждаем |
|---|---|---|---|---|
| Задача отчёта | нет | создана, статус «готовится» → «готов» | _(слот прогона)_ | 📸 Отчёты (строка со статусом) |
| Файл | — | сформирован | _(слот)_ | 📸 кнопка «Скачать» активна |

**💡 Что внутри:** клиент заказывает отчёт — портал ставит его в очередь и готовит в фоне, чтобы не заставлять ждать у экрана. Видно статус: «готовится» → «готов». Когда готов — появляется кнопка скачать.

**❌ Если не так:** задача зависла в «готовится» / упала без причины → разобрать (воркер/провайдер).

---

## Шаг 22-2 — Скачать по защищённой ссылке (живёт сутки); числа = базе

**🔧 Код:** [download :360-392](../../../app/app/Http/Controllers/Api/ReportJobController.php#L360-L392); ссылка 24ч — [:413](../../../app/app/Http/Controllers/Api/ReportJobController.php#L413).
**🎬 Действие (лок):** скачать готовый отчёт; сверить числа с разделом Сделки/Биллинг.

**📋 ОТЧЁТ:**
| Что | Было | Ожидалось | Стало (факт) | Чем подтверждаем |
|---|---|---|---|---|
| Скачивание | — | файл скачивается по защищённой ссылке | _(слот)_ | 📸 файл скачан |
| Числа в отчёте | — | = базе (сделки/деньги совпадают) | _(слот)_ | 📸 файл + сверка с экраном |
| Срок ссылки | — | действует 24 часа | _(слот)_ | 📝 ссылка с подписью (внутр.) |

**💡 Что внутри:** скачать отчёт можно по особой ссылке-пропуску, которая действует сутки — этого хватает, чтобы забрать файл, и потом она перестаёт работать (чтобы ссылка не «утекла» навсегда). Числа в отчёте — ровно те же, что в кабинете.

**❌ Если не так:** числа в отчёте ≠ базе / ссылка не открывается → дефект.

---

## Шаг 22-3 — Квота: слишком много заказов сразу не дают

**🔧 Код:** [store quota :144-151](../../../app/app/Http/Controllers/Api/ReportJobController.php#L144-L151).
**🎬 Действие (лок):** заказать отчётов больше квоты, не дожидаясь готовности предыдущих.

**📋 ОТЧЁТ:**
| Что | Было | Ожидалось | Стало (факт) | Чем подтверждаем |
|---|---|---|---|---|
| Лишний заказ | — | **отказ по квоте** (дождитесь готовности) | _(слот)_ | 📸 Отчёты (сообщение о квоте) |

**💡 Что внутри:** чтобы один клиент не завалил систему сотней одновременных заказов, действует ограничение: одновременно «в работе» может быть лишь несколько отчётов. Лишние портал просит подождать. Готовые не считаются.

**❌ Если не так:** квота не срабатывает → риск перегрузки.

---

## Шаг 22-4 — Изоляция: только свои отчёты

**🔧 Код:** [index :65](../../../app/app/Http/Controllers/Api/ReportJobController.php#L65), [show :106-110](../../../app/app/Http/Controllers/Api/ReportJobController.php#L106-L110).
**🎬 Действие (лок):** под клиентом B убедиться, что не видит/не качает отчёты клиента A.

**📋 ОТЧЁТ:**
| Что | Было | Ожидалось | Стало (факт) | Чем подтверждаем |
|---|---|---|---|---|
| Список отчётов | — | только свои | _(слот)_ | 📸 Отчёты B (нет чужих) |
| Чужой отчёт по ссылке | — | не доступен | _(слот)_ | 📝 404/нет доступа |

**❌ Если не так:** виден/качается чужой отчёт → утечка (связка с №16).

---

## Шаг 22-5 — Сбой формата (PDF) завершается штатно

**🔧 Код:** [GenerateReportJob :102](../../../app/app/Jobs/GenerateReportJob.php#L102).
**🎬 Действие (лок):** заказать отчёт в формате PDF.

**📋 ОТЧЁТ:**
| Что | Было | Ожидалось | Стало (факт) | Чем подтверждаем |
|---|---|---|---|---|
| PDF-отчёт | — | если PDF не поддержан — статус **failed** штатно (система не падает), можно повторить | _(слот прогона)_ | 📸 Отчёты (статус failed) |

**💡 Что внутри:** если какой-то формат не получается сформировать, портал не «зависает» и не падает — он спокойно помечает отчёт как «не удалось», и клиент может попробовать другой формат или повторить.

**❌ Если не так:** PDF роняет систему/задачу намертво → дефект (должно быть мягкое «failed»).

---

## Verification №22 (выход из пункта)
- [ ] 📸 Отчёт заказывается, проходит «готовится → готов».
- [ ] 📸 Скачивание по защищённой ссылке (24ч); числа = базе.
- [ ] 📸 Квота: лишние заказы отклоняются.
- [ ] 📸 Изоляция: только свои отчёты.
- [ ] 📸 PDF (или иной сбойный формат) → «failed» штатно, без падения.

## 📸 vs 📝 в этом пункте
- **📸 Скриншот:** Отчёты (список/статусы/квота/скачать), скачанный файл.
- **📝 Текст:** срок подписи ссылки (24ч), принадлежность тенанту — текстом.

## Грабли
- На dev отчёт готовится сразу (sync); на проде — фоновым воркером, статус идёт через «готовится».
- PDF→failed — заявлено сводом как «штатно»; **проверить файлом** (не утверждать без прогона).
- Ссылка скачивания — разовый пропуск на 24ч; через сутки недоступна (это правильно).
