# Схема данных платформы

Опорный документ для фронтенда, моков и задания бэкенду. Зафиксировано 21.09.2026 по решениям из `local/docs/plan.md`
и разбору ролевой модели и реестра метрик. Файлы данных рядом, в `data/`. Контракт API в `api/contract.md`.

Принципы, из которых выведена схема:

- **Одна точка правды на сущность.** Права, определения метрик и список отчётов живут в одном месте каждое.
  Сейчас на halleg три разных перечня отчётов и 105+ определений метрик, разбросанных по страницам.
- **Права проверяет бэкенд, фронт только отражает.** Каталог, навигация, экспорт и данные читают один ответ о правах.
- **Идентификаторы стабильны и наши.** Слаги отчётов берём как есть с halleg (это история посещений и привычные ссылки).
  Идентификаторы метрик наши, читаемый snake_case. Совпадение с параметрами halleg не обязательство.
- **Состав не расширяем.** Ни новых метрик, ни новых экранов. Реестр описывает то, что есть.
- **Три языка на уровне данных.** Подписи в записях, не в разметке. Поля `name.en / name.uk / name.ru`.

## Сущности

### Department (отдел)

| Поле | Тип | Описание |
|---|---|---|
| `id` | string | `analytics`, `traders`, `support`, `affiliates`, `finance` |
| `name` | i18n | подписи на трёх языках |
| `owns_reports` | bool | отдел-владелец отчётов (пока только аналитика) |

Пользователь принадлежит ровно одному отделу. Отдел задаёт роль по умолчанию при переводе сотрудника.

### Role (роль)

| Поле | Тип | Описание |
|---|---|---|
| `id` | string | `trader_manager`, `support_teamlead`, ... |
| `department` | Department.id или null | роль принадлежит отделу; `viewer` без отдела |
| `name` | i18n | |
| `reports` | Report.slug[] или `"*"` | набор отчётов; `"*"` = все |
| `parametrized` | bool | отчёты роли получают текущего пользователя параметром на бэкенде (саппорт: «вижу только себя») |
| `can_manage_access` | bool | право выдавать роли |
| `can_edit_metrics` | bool | право менять определения в реестре метрик |

Правила:
- Права пользователя = объединение `reports` всех его ролей.
- Персональных исключений «этому человеку ещё вот этот отчёт» нет. Нестандартный набор = отдельная роль, и она видна в админке как аномалия.
- Права только на уровне отчёта целиком. Прав на строки и колонки в этой итерации нет. `parametrized` это не row-level в общем виде, а отчёт, который знает, кто его открыл.

### User (пользователь)

| Поле | Тип | Описание |
|---|---|---|
| `id`, `login`, `display_name`, `email` | string | `login` как на halleg (вида `vector_om4`) |
| `department` | Department.id | |
| `roles` | Role.id[] | одна или несколько |
| `status` | `active` / `blocked` | блокировка по CRM или руками |
| `blocked_reason`, `blocked_at` | string | источник блокировки: CRM-событие или логин админа |

События из CRM: увольнение (→ `blocked`), перевод между отделами (→ снять роли старого отдела, поставить роль по умолчанию нового).
Ручной рычаг «заблокировать сейчас» обязателен, задержка CRM неизвестна.

### Report (отчёт)

| Поле | Тип | Описание |
|---|---|---|
| `slug` | string | как на halleg: `trader-operational`, `churn`, ... |
| `name` | i18n | en из источника, uk/ru наш перевод |
| `group` | string | `Trader` / `Merchant` / `Partners` / `Retention` / `Other`, группировка навигации как на halleg |
| `department` | Department.id | отдел-владелец |
| `kind` | `report` / `reference` | `metrics-legend` это справочник, не отчёт |
| `admin_only` | bool | `threshold-analysis` |
| `listed_in` | {sidebar, direct_links, usage_stats} | в каких из трёх перечней halleg упомянут |
| `usage_7d` | {visits, users, is_new} или null | разовая выгрузка статистики |
| `copy` | {file, size_bytes} или null | есть ли сохранённая копия |
| `data_needs` | string[] | что отчёт запрашивает у halleg сейчас; список потребностей для бэкенда, не контракт |
| `migration_priority` | int | порядок переноса, по посещаемости |
| `metrics` | Metric.id[] | какие метрики показывает (выводится из реестра метрик, поле `reports` у метрики) |

Легенда отчёта не хранится отдельно: это выборка записей реестра метрик по `metrics` плюс `legend_notes` (подписи вкладок, фильтров, источника данных).

### Metric (метрика)

| Поле | Тип | Описание |
|---|---|---|
| `id` | string | `payin_volume`, `cr`, `score_scoreboard`, ... |
| `name` | i18n | |
| `group` | string | группа из канона (`Обсяги`, `Конверсія і автоматизація`, ...) или наша |
| `status` | `canonical` / `conflict` / `needs_review` | см. ниже |
| `canonical` | bool | есть в `metrics-legend` |
| `definition` | string | что измеряет, одним абзацем |
| `formula` | string | SQL-подобная запись |
| `unit` | string | INR, %, минуты, штуки, баллы |
| `synonyms` | string[] | все имена, под которыми метрика встречается в отчётах и у финансов. Поиск идёт по ним |
| `reports` | {report, label, description, pattern}[] | где используется и как там описана |
| `deviations` | {report, text, status}[] | чем расчёт в отчёте отличается от канона. `status: open` = не решено |
| `notes` | string[] | примечания канона: что не путать, что не использовать |
| `owner`, `verified_by` | User.id или null | автор определения и верифицировавший аналитик |
| `updated_at` | date | дата последней версии определения |
| `source` | string | откуда взято определение |

Статусы:
- `canonical` - есть в `metrics-legend`, определение каноническое. Отклонения в отчётах перечислены в `deviations`.
- `conflict` - несовместимые определения без канона. Пока только `Score`: заведён двумя метриками, объединять нельзя до решения арбитра.
- `needs_review` - в каноне нет, определение взято из кода отчёта как есть. Ждёт верификации аналитиком.

Правила:
- Метрика без `definition` в реестр не принимается. Метрика вне реестра не попадает в отчёт.
- Три представления одного реестра: справочник целиком, легенда отчёта (выборка по `reports`), подсказка у числа (одна запись).
- Отклонение от канона не скрывается: пользователь у числа видит, что здесь считается иначе и почему.
- Каждое изменение `definition` или `formula` это новая версия (см. MetricVersion). Старая не удаляется: отчёт за прошлый период должен ссылаться на формулу, по которой считался.

### MetricVersion (версия определения)

| Поле | Тип |
|---|---|
| `metric_id` | Metric.id |
| `version` | int |
| `definition`, `formula`, `unit` | как в Metric на момент версии |
| `changed_by` | User.id |
| `changed_at` | datetime |
| `reason` | string |

В моках версий нет, все метрики в версии 1. Формулы у саппорта меняются почти ежедневно, поэтому сущность заложена сразу.

### AccessLog и UsageEvent

Один middleware на бэкенде проверяет право на отчёт и пишет два события: `AccessGrant {user, role, report, granted_by, at}` при изменении прав
и `UsageEvent {user, report, at, filters?}` при каждом открытии. Из второго строится статистика использования, которой на halleg нет.
Раз весь трафик идёт через платформу, активность мимо неё не теряется.

## Связи

```
Department 1 ─── * Role 1 ─── * (Role × Report)
Department 1 ─── * User * ─── * Role
Report 1 ─── * (Metric.reports) ─── 1 Metric 1 ─── * MetricVersion
User 1 ─── * UsageEvent * ─── 1 Report
```

## Что в этой схеме заглушка

- Отделы и роли: до выгрузки «пользователь → роль → отчёты» с halleg. Роли выведены из паттернов, а не из данных.
- Привязка отчётов к отделам: предположение «всё аналитика, кроме Partners».
- `name.uk/ru` у отчётов и у неканонических метрик: наш перевод или пусто.
- Метрики со статусом `needs_review`: определения взяты из кода как есть, состав будет перебираться.
- Саппорт и аффилиаты: отчётов в реестре нет, только роли и отделы.

## Файлы

| Файл | Что | Как собран |
|---|---|---|
| `data/reports.json` | 19 отчётов | `tools/build_reports.py` из sidebar.js, макета night и копий страниц |
| `data/metrics.json` | реестр метрик + legend_notes по отчётам | `tools/build_metrics.py` из `_canon.json` и `_raw_descriptions.json` |
| `data/_canon.json` | 15 канонических метрик | `tools/parse_canon_legend.py` из `Source/Trader_Reporting.html` |
| `data/_raw_descriptions.json` | описания метрик из кода 11 отчётов | `tools/extract_metric_descriptions.py` |
| `data/departments.json`, `roles.json`, `users.json` | заглушки | руками, с пометкой источника каждой строки |
