ТЕХНИЧЕСКОЕ ЗАДАНИЕ (ТЗ) НА ПЛАТФОРМУ EVENTHUB
Версия: 1.1 (расширенная с гибридной моделью повторяющихся событий)

1. ЦЕЛИ И НАЗНАЧЕНИЕ
EventHub — платформа для управления событиями с поддержкой календарей, записи участников (включая специалистов), гибкого подтверждения, рейтингов, отзывов, модерации, встроенного баг-трекера и платной подписки. Целевая аудитория: владельцы календарей (бизнес), участники (клиенты), администраторы.

2. ФУНКЦИОНАЛЬНЫЕ ТРЕБОВАНИЯ

2.1. Календари
- CRUD календаря (название, описание, теги, владелец)
- Расшаривание по ссылке (публичная/приватная)
- Приглашение пользователей с правами "запись" или "администрирование"
- Типы календарей: personal (бесплатный, без записи), commercial (платный, запись клиентов, специалисты)
- Гибкое подтверждение заявок: auto (автоматически), manual (вручную), timeout (авто через N секунд)
- Теги календаря, рейтинг (средняя оценка, количество голосов)

2.2. События (расширенная версия с повторяющимися событиями)

2.2.1. Типы событий и модель хранения
События делятся на два типа:
- single — одиночное событие, имеет конкретные дату и время начала, длительность.
- recurring — повторяющееся событие (серия), определяется мастер-записью и правилом повторения.

Для эффективного хранения и быстрого поиска применяется гибридная модель с отложенной материализацией вхождений:
- Каждая повторяющаяся серия представлена одной мастер-записью в таблице events с полем event_type = recurring и полем recurrence_rule, содержащим правило повторения в формате, совместимом с iCalendar RRULE (или упрощённый JSON-представление: freq, interval, byday, until и т.д.).
- Конкретные вхождения (экземпляры) серии материализуются в таблице events только при необходимости:
  * когда на данное вхождение записался хотя бы один участник;
  * когда администратор вручную редактирует это вхождение (перенос времени, отмена, смена специалиста);
  * (опционально) при наступлении даты вхождения для архивных целей.
- Материализованное вхождение имеет:
  * event_type = single (или специальный флаг is_instance = true),
  * ссылку master_id на родительскую мастер-запись,
  * собственные start_time и duration, которые могут отличаться от вычисленных по правилу (например, при переносе конкретного дня).
- Отменённые вхождения из серии (исключения) хранятся в отдельной таблице recurrence_exceptions с указанием master_id, original_start_time и action = cancel | reschedule.

2.2.2. Правила генерации вхождений при поиске
При поиске событий на заданный интервал дат (например, /v1/events?from=...&to=...) система:
1. Выбирает все одиночные события (event_type = single), попадающие в интервал.
2. Выбирает все активные мастер-записи (event_type = recurring), у которых:
   - дата первого вхождения ≤ конец интервала,
   - правило повторения допускает вхождения внутри интервала (учёт until или ограничения по количеству повторений).
3. Для каждой мастер-записи генерирует список вхождений в пределах запрошенного интервала (используя встроенную функцию разбора RRULE или собственный алгоритм).
4. Из сгенерированного списка исключаются вхождения, присутствующие в таблице recurrence_exceptions с действием cancel.
5. Если для какого-либо вхождения уже существует материализованная запись в таблице events (по совпадению master_id и start_time), то используются данные материализованного экземпляра (специалист, длительность, статус) вместо вычисленных по шаблону.
6. Итоговый список сортируется по времени начала и возвращается с пагинацией.

Ограничение: глубина генерации вхождений для одного запроса ограничена 1000 элементов; для длительных серий применяется пагинация по датам.

2.2.3. Материализация при записи участника
При попытке записаться на вхождение повторяющегося события (POST /v1/events/:event_id/join):
- Если event_id ссылается на мастер-запись (event_type = recurring), то в теле запроса обязательно передаётся occurrence_start — конкретное время вхождения, на которое производится запись.
- Система в одной транзакции Mnesia:
  * Проверяет, существует ли материализованное вхождение с master_id = EventId и start_time = OccurrenceStart.
  * Если нет — создаёт новую запись в таблице events (тип single, master_id = EventId), копируя общие атрибуты мастер-записи (название, описание, календарь, специалист).
  * Создаёт бронирование (booking), привязанное к идентификатору материализованного вхождения.
- Дальнейшие действия по подтверждению заявки выполняются уже с материализованным экземпляром.

2.2.4. Изменение и удаление серий
- При редактировании мастер-записи (изменение названия, описания, правила повторения) обновляется только сама мастер-запись. Уже существующие материализованные вхождения остаются неизменными (сохраняют старые значения атрибутов). Это поведение может быть изменено администратором через специальный флаг «применить ко всем будущим вхождениям».
- При удалении мастер-записи все связанные материализованные вхождения также помечаются удалёнными (или удаляются каскадно), а бронирования на будущие вхождения аннулируются с уведомлением участников.

2.2.5. Структура записей (records.hrl)
```erlang
%% Событие (может быть мастером или экземпляром)
-record(event, {
    id              :: binary(),
    calendar_id     :: binary(),
    title           :: binary(),
    description     :: binary(),
    event_type      :: single | recurring,
    start_time      :: calendar:datetime(),
    duration        :: integer(),          %% минуты
    recurrence_rule :: binary() | undefined,
    master_id       :: binary() | undefined,
    is_instance     :: boolean(),          %% true для материализованных вхождений
    specialist_id   :: binary() | undefined,
    location        :: #location{} | undefined,
    tags            :: [binary()],
    status          :: active | cancelled | completed,
    created_at      :: calendar:datetime(),
    updated_at      :: calendar:datetime()
}).

%% Исключение из повторений
-record(recurrence_exception, {
    master_id       :: binary(),
    original_start  :: calendar:datetime(),
    action          :: cancel | reschedule,
    new_start       :: calendar:datetime() | undefined
}).

2.2.6. Требования к реализации

Разбор и генерация RRULE должны быть реализованы в отдельном модуле logic_recurrence без внешних зависимостей (чистый Erlang).

Все операции с материализацией и записью участников должны выполняться в транзакциях Mnesia для обеспечения консистентности.

Для ускорения поиска материализованных вхождений по мастеру и дате создаётся составной индекс в Mnesia: {master_id, start_time} (через mnesia:add_table_index/2 или хранение в ets с ключом {master_id, start_time}).

2.3. Запись участников и подтверждение

Запись через календарь

Подтверждение согласно политике календаря

Уведомления участника и владельца

Привязка события к специалисту (отдельная запись в календаре специалиста)

2.4. Отзывы и рейтинги

Только участники событий могут оставлять отзывы

Оценка 1-5, текстовый комментарий

Отзывы на событие или на календарь

Модерация отзывов (администраторы могут скрывать)

2.5. Поиск и фильтрация

По тексту, по тегам, по гео-позиции (радиус), по дате/времени

2.6. Расширенные возможности

Экспорт в Google Calendar (через OAuth2)

Экспорт в Apple Calendar (ICS-файл)

Геокодирование (адрес -> координаты) через внешний API (заглушка / Nominatim)

2.7. Модерация и безопасность

Жалобы на календари, события, отзывы

Автоматическая модерация по ключевым словам и по порогу жалоб

Ручная заморозка / разморозка администратором

Бан-лист слов

Аудит действий администраторов

2.8. Баг-трекер (автоматический)

При ошибке создаётся тикет, группировка по хэшу ошибки

Учёт количества повторений

Уведомление пользователя при создании и при закрытии тикета

Доступ только у администраторов

2.9. Платная подписка

Личное использование бесплатно (personal)

Коммерческое использование платно (commercial)

Пробный период 30 дней

Планы подписки: 1, 3, 6, 12 месяцев

Заглушка платежного шлюза (для тестирования)

Автоматическое истечение подписки

2.10. Административная панель

Отдельный HTTPS-сервер (порт 8445) и отдельный WSS (порт 8446)

Управление календарями, событиями, отзывами, жалобами, тикетами, бан-словами

Статистика, информация о нодах кластера

WebSocket-уведомления администраторам

2.11. Real-time уведомления (WebSocket)

Пользователи: подписка на календарь, получение обновлений

Администраторы: подписка на глобальные уведомления (жалобы, авто-заморозки)

НЕФУНКЦИОНАЛЬНЫЕ ТРЕБОВАНИЯ

3.1. Производительность и масштабирование

100 000+ пользователей

Горизонтальное масштабирование (увеличение нод)

Mnesia с дисковыми копиями на нескольких нодах

Отдельные ноды для исторических данных (disc_only_copies)

Пагинация всех списков

3.2. Надёжность

Супервизорное дерево OTP

Let it crash – быстрый перезапуск процессов

Автоматическое восстановление после падения ноды (Mnesia)

3.3. Безопасность

JWT с ролью (user / admin)

HTTPS / WSS (самоподписанный сертификат для dev, реальный для prod)

Argon2 для хеширования паролей (erlang-argon2)

OAuth2 для Google (опционально)

Refresh token (запланирован)

3.4. Наблюдаемость

Healthcheck эндпоинт

Логирование (JSON, ротация)

Prometheus метрики (запланированы)

Аудит действий админов

3.5. CI/CD

Drone CI (или GitLab CI / GitHub Actions)

EUnit + Common Test + Tsung

Сборка релиза

Docker-образ

Горячее обновление (rolling upgrade) кластера (3+ нод)

Миграции Mnesia

СТЕК ТЕХНОЛОГИЙ (С ВЕРСИЯМИ)

Бэкенд: Erlang/OTP 28.2

Сборка: rebar3 3.27.0

HTTP/WebSocket: Cowboy 2.10.0

JSON: jsx 3.1.0

JWT: jwerl 1.1.0

Хеширование паролей: erlang-argon2 1.0.0

Тестирование: meck 0.9.2, gun 2.0.0

Нагрузочное тестирование: Tsung 1.8.0

CI/CD: Drone 2.0+

Контейнеризация: Docker 20.10+

База данных: Mnesia (встроенная, распределённая)

ИЕРАРХИЧЕСКАЯ СТРУКТУРА КОДА

src/
├── infra/ # инфраструктура (приложение, супервизоры, аутентификация, подписки)
├── core/ # слой доступа к данным (DAO) и модели
├── logic/ # бизнес-логика (независимая от HTTP/WS)
├── services/ # внешние сервисы (заглушки/интеграции)
└── handlers/ # обработчики HTTP/WebSocket

Правила:

Модули handlers/ не содержат бизнес-логики.

Модули logic/ не знают о HTTP/WS.

Модули core/ (DAO) работают только с Mnesia.

Модули services/ могут быть заменены на реальные реализации без изменения остального кода.

Заголовочный файл include/records.hrl содержит все записи таблиц Mnesia.

ОСНОВНЫЕ API (КРАТКО)

Пользовательские (порт 8080):

POST /v1/register

POST /v1/login

GET /v1/user/me

POST /v1/calendars + CRUD

POST /v1/calendars/:id/events + CRUD

POST /v1/events/:id/join

POST /v1/events/:id/confirm/:user_id

POST /v1/reviews

POST /v1/reports

POST /v1/subscription/activate

POST /v1/events/:id/export (Google)

GET /v1/events/:id/ical (Apple)

GET /health

WebSocket (порт 8081): ws://localhost:8081/ws?token=...

Административные (порт 8445):

GET /admin/stats/overview

GET /admin/nodes

GET /admin/calendars, PUT /admin/calendars/:id/freeze, /unfreeze

GET /admin/events, PUT /admin/events/:id/freeze

GET /admin/reviews, PUT /admin/reviews/:id/hide

GET /admin/reports, PUT /admin/reports/:id/status

GET /admin/tickets, PUT /admin/tickets/:id/status

GET /admin/banned_words, POST, DELETE

Административный WebSocket (порт 8446): wss://localhost:8446/admin/ws?token=...

ВЕРСИОНИРОВАНИЕ И СТАТУС
Текущая версия: 1.1 (MVP, альфа, включает гибридную модель повторяющихся событий)

ОГРАНИЧЕНИЯ И ДОПУЩЕНИЯ

В разработке используются самоподписанные SSL-сертификаты.

Для production требуется реальный сертификат и настройка OAuth2 для Google.

Заглушки внешних сервисов должны быть заменены перед запуском.

ТРЕБОВАНИЯ К ОКРУЖЕНИЮ

Erlang/OTP 28.2

rebar3 3.27.0

openssl

Docker (опционально)

Drone (опционально, для CI/CD)