P0: GET /v1/events/:id/bookings отдаёт 500 — владелец не видит заявки #51

Closed
opened 2026-07-21 00:21:31 +03:00 by cursor-ai · 5 comments
Owner

Проблема

На stage GET /v1/events/:id/bookings возвращает HTTP 500. В UI владелец события видит «Нет заявок» и не может подтвердить/отклонить pending-запись кнопками.

Причина: handler_bookings:list_bookings/1 вызывает logic_booking:list_event_bookings(UserId, EventId) (arity 2), тогда как в logic_booking экспортирован только list_event_bookings/1.

Воспроизведение (browser QA, 2026-07-20/21):

  1. Пользователь B записывается на событие коммерческого календаря владельца A → status pending.
  2. Владелец A открывает карточку события → блок «Заявки» пустой («Нет заявок»).
  3. API: GET /v1/events/T75zm7qXFeISw9yREnJ0tA/bookings → 500; PUT /v1/bookings/:id с action=confirm от владельца при этом работает (200).

Влияние

Владелец не может подтверждать записи через UI. Обход только через прямой API. Ломает сценарий «запись → подтверждение → отзыв».

Ожидаемый результат

Владелец получает список заявок события и может confirm/decline в карточке события.

Критерии приёмки

  • GET /v1/events/:id/bookings для владельца возвращает 200 и список заявок (в т.ч. pending)
  • Чужой пользователь не получает чужие заявки (403/пустой по правилам доступа)
  • В UI владельца появляются кнопки подтверждения/отклонения
  • Есть/обновлён CT или unit-тест на list event bookings для владельца

Файлы (подсказка)

  • src/handlers/handler_bookings.erl (вызов list_event_bookings/2)
  • src/logic/logic_booking.erl (list_event_bookings/1, list_bookings/2)

Приоритет

P0

## Проблема На stage `GET /v1/events/:id/bookings` возвращает HTTP 500. В UI владелец события видит «Нет заявок» и не может подтвердить/отклонить pending-запись кнопками. Причина: `handler_bookings:list_bookings/1` вызывает `logic_booking:list_event_bookings(UserId, EventId)` (arity 2), тогда как в `logic_booking` экспортирован только `list_event_bookings/1`. Воспроизведение (browser QA, 2026-07-20/21): 1. Пользователь B записывается на событие коммерческого календаря владельца A → status `pending`. 2. Владелец A открывает карточку события → блок «Заявки» пустой («Нет заявок»). 3. API: `GET /v1/events/T75zm7qXFeISw9yREnJ0tA/bookings` → 500; `PUT /v1/bookings/:id` с `action=confirm` от владельца при этом работает (200). ## Влияние Владелец не может подтверждать записи через UI. Обход только через прямой API. Ломает сценарий «запись → подтверждение → отзыв». ## Ожидаемый результат Владелец получает список заявок события и может confirm/decline в карточке события. ## Критерии приёмки - [ ] `GET /v1/events/:id/bookings` для владельца возвращает 200 и список заявок (в т.ч. pending) - [ ] Чужой пользователь не получает чужие заявки (403/пустой по правилам доступа) - [ ] В UI владельца появляются кнопки подтверждения/отклонения - [ ] Есть/обновлён CT или unit-тест на list event bookings для владельца ## Файлы (подсказка) - `src/handlers/handler_bookings.erl` (вызов `list_event_bookings/2`) - `src/logic/logic_booking.erl` (`list_event_bookings/1`, `list_bookings/2`) ## Приоритет P0
cursor-ai added the Bug label 2026-07-21 00:21:31 +03:00
cursor-ai self-assigned this 2026-07-21 19:41:43 +03:00
Author
Owner

Беру задачу в работу из Cursor.

Сначала предложу варианты решения с trade-offs; код не меняю до вашего выбора варианта.

Беру задачу в работу из Cursor. Сначала предложу варианты решения с trade-offs; код не меняю до вашего выбора варианта.
Author
Owner

Анализ

Причина 500: handler_bookings:list_bookings/1 вызывает logic_booking:list_event_bookings(UserId, EventId) (arity 2), а в logic_booking экспортирована только list_event_bookings/1 (без проверки владельца). Существующая list_bookings/2 фильтрует заявки участника (свои), а не список владельца — для UI владельца не подходит.

Варианты решения

Вариант 1 (рекомендуемый)

Добавить list_event_bookings/2 (UserId, EventId):

  • событие не найдено → {error, not_found};
  • пользователь не владелец календаря события (и не admin) → {error, access_denied};
  • иначе → полный список заявок события.
    Оставить list_event_bookings/1 для admin/internal. Handler уже готов к этому контракту. Добавить unit + API/CT тест на 200/403.

Плюсы: точечный фикс, правильный ACL, минимальный риск.
Минусы: появляется ещё одна функция рядом с list_bookings/2 (нужно ясно развести имена/доки).

Вариант 2

Проверку владельца сделать в handler, затем вызывать list_event_bookings/1.

Плюсы: меньше изменений в logic.
Минусы: ACL утекает в handler; хуже переиспользование и тестирование.

Вариант 3

Переиспользовать/переписать list_bookings/2 под семантику «владелец видит все заявки».

Плюсы: меньше экспортов.
Минусы: ломает текущую семантику (участник видит только свои); риск регрессий у других вызовов.

Жду выбор варианта перед правками кода.

## Анализ Причина 500: handler_bookings:list_bookings/1 вызывает logic_booking:list_event_bookings(UserId, EventId) (arity 2), а в logic_booking экспортирована только list_event_bookings/1 (без проверки владельца). Существующая list_bookings/2 фильтрует заявки участника (свои), а не список владельца — для UI владельца не подходит. ## Варианты решения ### Вариант 1 (рекомендуемый) Добавить list_event_bookings/2 (UserId, EventId): - событие не найдено → {error, not_found}; - пользователь не владелец календаря события (и не admin) → {error, access_denied}; - иначе → полный список заявок события. Оставить list_event_bookings/1 для admin/internal. Handler уже готов к этому контракту. Добавить unit + API/CT тест на 200/403. **Плюсы:** точечный фикс, правильный ACL, минимальный риск. **Минусы:** появляется ещё одна функция рядом с list_bookings/2 (нужно ясно развести имена/доки). ### Вариант 2 Проверку владельца сделать в handler, затем вызывать list_event_bookings/1. **Плюсы:** меньше изменений в logic. **Минусы:** ACL утекает в handler; хуже переиспользование и тестирование. ### Вариант 3 Переиспользовать/переписать list_bookings/2 под семантику «владелец видит все заявки». **Плюсы:** меньше экспортов. **Минусы:** ломает текущую семантику (участник видит только свои); риск регрессий у других вызовов. Жду выбор варианта перед правками кода.
Author
Owner

Реализую утверждённый вариант 1: добавляю logic_booking:list_event_bookings/2 с проверкой владельца календаря (admin ok), возвращающий полный список заявок; list_event_bookings/1 оставляю для admin/internal; правлю вызов в handler; добавляю тесты 200 owner / 403 non-owner. list_bookings/2 не трогаю.

Реализую утверждённый вариант 1: добавляю logic_booking:list_event_bookings/2 с проверкой владельца календаря (admin ok), возвращающий полный список заявок; list_event_bookings/1 оставляю для admin/internal; правлю вызов в handler; добавляю тесты 200 owner / 403 non-owner. list_bookings/2 не трогаю.
Author
Owner

Сделано (вариант 1)

Добавлена logic_booking:list_event_bookings/2: проверка владельца календаря (или admin), полный список заявок. list_event_bookings/1 без изменений (admin/internal). list_bookings/2 не трогали. Handler уже вызывал /2 — теперь функция существует.

Файлы

  • src/logic/logic_booking.erl — list_event_bookings/2
  • est/unit/logic_booking_tests.erl — owner 200 / non-owner 403 (+ таблица dmin в setup)

Тесты

WSL: make eunit-module MODULE=logic_booking_tests — All 13 tests passed.

Issue оставляю открытой до подтверждения коммита/закрытия.

## Сделано (вариант 1) Добавлена logic_booking:list_event_bookings/2: проверка владельца календаря (или admin), полный список заявок. list_event_bookings/1 без изменений (admin/internal). list_bookings/2 не трогали. Handler уже вызывал /2 — теперь функция существует. ### Файлы - src/logic/logic_booking.erl — list_event_bookings/2 - est/unit/logic_booking_tests.erl — owner 200 / non-owner 403 (+ таблица dmin в setup) ### Тесты WSL: make eunit-module MODULE=logic_booking_tests — **All 13 tests passed**. Issue оставляю открытой до подтверждения коммита/закрытия.
Author
Owner

Зафиксировано в 5ac2321: list_event_bookings/2 с ACL владельца + тесты. Задеплоено на remote master.

Зафиксировано в `5ac2321`: `list_event_bookings/2` с ACL владельца + тесты. Задеплоено на remote master.
Sign in to join this conversation.