Files
vidconf/docs/architecture/adr/003-conference-invitees.md

6.4 KiB
Raw Permalink Blame History

ADR-003. Модель приглашённых участников конференции (conference_invitees)

Статус: принято.

Контекст

Продукту нужен состав приглашённых участников конференции: зарегистрированные пользователи (user_id) и внешние по произвольному email. Организатор обязан всегда быть в составе и быть неудаляемым. Уже существует таблица conference_participants — это ФАКТИЧЕСКИЕ участники сеанса (кто реально был, окна присутствия, единица атрибуции фраз, ADR-002); смешивать сущности нельзя.

Решение

  1. Новая таблица conference_invitees — приглашённые НА КОНФЕРЕНЦИЮ (не на сеанс):
    • id UUID PK, conference_id FK conferences ON DELETE CASCADE NOT NULL;
    • user_id FK users ON DELETE CASCADE NULL — зарегистрированный;
    • email VARCHAR(255) NULL — внешний (хранится в lower-case);
    • CHECK ((user_id IS NOT NULL)::int + (email IS NOT NULL)::int = 1) — ровно одна identity (тот же приём, что в conference_participants);
    • частичные UNIQUE: (conference_id, user_id) WHERE user_id IS NOT NULL и (conference_id, email) WHERE email IS NOT NULL — без дублей;
    • created_at.
  2. Организатор в таблице НЕ хранится: он выводится из conferences.owner_id и всегда добавляется в состав на уровне API/рассылки. Инвариант «организатор всегда в составе и неудаляем» обеспечен конструктивно — удалить его из состава невозможно в принципе, рассинхронизация при смене владельца исключена. Попытка добавить владельца в invitees (по user_id или его email) молча дедуплицируется на записи.
  3. Состав задаётся списком целиком (PUT-семантика поля participants в create/update конференции): backend вычисляет diff, отсутствие поля — «не менять». Права на изменение состава = права на изменение конференции.
  4. Связь с фактическими участниками сеанса — аналитическая, по join без FK: зарегистрированный — conference_participants.user_id = invitees.user_id; внешний — lower(guest_access.email) = invitees.email (если приглашённый вошёл гостем и указал тот же email). FK не вводим: гость может войти с другим email или не войти вовсе — жёсткая связь ложна по природе данных.
  5. Рассылка приглашений (.ics METHOD:REQUEST): получатели = организатор + invitees (email пользователя или внешний email); для закреплённых по-прежнему добавляются участники прошлых сеансов (workers/tasks/invitations.py, дедуп по lower(email)).
  6. Видимость приглашённого в списках. Приглашённый видит конференцию в GET /conferences/my и GET /conferences/calendar наравне с владельцем — строка попадает в выборку, если owner_id == user.id ИЛИ существует conference_invitees этой конференции с user_id == user.id ИЛИ с lower(email) == lower(email пользователя) (внешнее приглашение на адрес, под которым человек впоследствии зарегистрировался). Критерии показа (закреплённая — безусловно; разовая — status=scheduled и scheduled_at в будущем) не меняются, только круг «чей» конференция. GET /conferences/{id} аналогично открыт приглашённому (иначе ховер-карточка/детальная страница получали бы 403); is_owner в ответе для приглашённого — false, organizer_name — имя фактического владельца. Права на PATCH/DELETE это расширение НЕ затрагивает — по-прежнему только владелец/администратор.

Последствия

  • (+) Чистое разделение «приглашён» / «фактически был»; пайплайн атрибуции фраз (ADR-002) не затронут.
  • (+) Инвариант организатора не требует триггеров и проверок целостности.
  • () Внешний приглашённый не связывается с гостевым входом надёжно (только эвристика по email) — принято как ограничение модели.
  • () Списки состава в ответах API требуют дозагрузки (selectinload) — следить за N+1 в /my и /calendar; показ приглашённому (п. 6) добавляет туда же EXISTS-подзапрос по conference_invitees и точечный запрос имени реального владельца на каждую НЕ свою строку списка — список короткий (закреплённые + предстоящие), нагрузка признана приемлемой.
  • Календарь и «Мои конференции» — выборка «владелец ИЛИ приглашённый» (п. 6); до 2026-07-20 показывались только конференции владельца — приглашённый видел состав лишь через уведомление/.ics, не через списки приложения.