Files
EventHubBack/src/logic/logic_booking.erl
T
aleksey fdb08eb453
CI / test (push) Successful in 6m49s
CI / deploy-ift (push) Successful in 2m28s
CI / e2e-ift (push) Successful in 1m26s
CI / deploy-stage (push) Successful in 1m57s
CI / e2e-stage (push) Successful in 1m49s
fix: require calendar owner or admin for booking confirm/decline. Fixes EventHub/EventHubBack#53
2026-07-22 10:53:01 +03:00

250 lines
10 KiB
Erlang
Executable File

-module(logic_booking).
-include("records.hrl").
-export([create_booking/2, confirm_booking/2, confirm_booking/3,
cancel_booking/2, cancel_booking/3, get_booking/2,
list_bookings/2, list_user_bookings/1, delete_booking/2,
list_bookings_admin/0, get_booking_admin/1,
list_event_bookings/1, list_event_bookings/2]).
%%%-------------------------------------------------------------------
%%% @doc Создание бронирования со статусом `pending`.
%%% @end
%%%-------------------------------------------------------------------
-spec create_booking(UserId :: binary(), EventId :: binary()) ->
{ok, #booking{}} | {error, full | already_booked | not_found}.
create_booking(UserId, EventId) ->
case core_event:get_by_id(EventId) of
{ok, Event} ->
case check_capacity(EventId, Event#event.capacity) of
{ok, _} ->
case core_booking:get_by_event_and_user(EventId, UserId) of
{error, not_found} ->
core_booking:create(EventId, UserId, pending);
{ok, _} ->
{error, already_booked}
end;
{error, full} ->
{error, full}
end;
{error, not_found} ->
{error, not_found}
end.
%%%-------------------------------------------------------------------
%%% @doc Подтверждение бронирования (двухарная версия).
%%% Только владелец календаря события или admin.
%%% @end
%%%-------------------------------------------------------------------
-spec confirm_booking(BookingId :: binary(), UserId :: binary()) ->
{ok, #booking{}} | {error, not_found | access_denied}.
confirm_booking(BookingId, UserId) ->
case core_booking:get_by_id(BookingId) of
{ok, Booking} ->
case can_manage_event_bookings(UserId, Booking#booking.event_id) of
true ->
case Booking#booking.status of
pending ->
Now = calendar:universal_time(),
core_booking:update(BookingId, [{status, confirmed}, {confirmed_at, Now}]);
_ ->
{error, access_denied}
end;
{error, Reason} ->
{error, Reason}
end;
Error -> Error
end.
%%%-------------------------------------------------------------------
%%% @doc Подтверждение или отклонение бронирования (для обработчиков).
%%% `decline` переводит pending-заявку в `cancelled`.
%%% @end
%%%-------------------------------------------------------------------
-spec confirm_booking(UserId :: binary(), BookingId :: binary(), confirm | decline) ->
{ok, #booking{}} | {error, not_found | access_denied}.
confirm_booking(UserId, BookingId, confirm) ->
confirm_booking(BookingId, UserId);
confirm_booking(UserId, BookingId, decline) ->
case core_booking:get_by_id(BookingId) of
{ok, Booking} ->
case can_manage_event_bookings(UserId, Booking#booking.event_id) of
true ->
case Booking#booking.status of
pending ->
core_booking:update(BookingId, [{status, cancelled}]);
_ ->
{error, access_denied}
end;
{error, Reason} ->
{error, Reason}
end;
Error -> Error
end.
%%%-------------------------------------------------------------------
%%% @doc Отмена бронирования (двухарная версия).
%%% @end
%%%-------------------------------------------------------------------
-spec cancel_booking(BookingId :: binary(), UserId :: binary()) ->
{ok, #booking{}} | {error, not_found | access_denied}.
cancel_booking(BookingId, UserId) ->
case core_booking:get_by_id(BookingId) of
{ok, Booking} ->
case Booking#booking.status of
cancelled ->
{ok, Booking};
_ ->
case Booking#booking.user_id =:= UserId of
true -> core_booking:update(BookingId, [{status, cancelled}]);
false -> {error, access_denied}
end
end;
Error -> Error
end.
%%%-------------------------------------------------------------------
%%% @doc Отмена бронирования (трёхарная версия для обработчиков).
%%% @end
%%%-------------------------------------------------------------------
-spec cancel_booking(UserId :: binary(), BookingId :: binary(), cancel) ->
{ok, #booking{}} | {error, not_found | access_denied}.
cancel_booking(UserId, BookingId, cancel) ->
cancel_booking(BookingId, UserId).
%%%-------------------------------------------------------------------
%%% @doc Получение бронирования по ID.
%%% @end
%%%-------------------------------------------------------------------
-spec get_booking(BookingId :: binary(), UserId :: binary()) ->
{ok, #booking{}} | {error, not_found | access_denied}.
get_booking(BookingId, UserId) ->
case core_booking:get_by_id(BookingId) of
{ok, Booking} ->
case Booking#booking.user_id =:= UserId of
true -> {ok, Booking};
false -> {error, access_denied}
end;
Error -> Error
end.
%%%-------------------------------------------------------------------
%%% @doc Список бронирований события.
%%% @end
%%%-------------------------------------------------------------------
-spec list_bookings(EventId :: binary(), UserId :: binary()) ->
{ok, [#booking{}]}.
list_bookings(EventId, UserId) ->
{ok, Bookings} = core_booking:list_by_event(EventId),
Filtered = case admin_utils:is_admin(UserId) of
true -> Bookings;
false -> [B || B <- Bookings, B#booking.user_id =:= UserId]
end,
{ok, Filtered}.
%%%-------------------------------------------------------------------
%%% @doc Список бронирований пользователя.
%%% @end
%%%-------------------------------------------------------------------
-spec list_user_bookings(UserId :: binary()) -> {ok, [#booking{}]}.
list_user_bookings(UserId) ->
core_booking:list_by_user(UserId).
%%%-------------------------------------------------------------------
%%% @doc Удаление бронирования (только владелец).
%%% @end
%%%-------------------------------------------------------------------
-spec delete_booking(BookingId :: binary(), UserId :: binary()) ->
ok | {error, not_found | access_denied}.
delete_booking(BookingId, UserId) ->
case core_booking:get_by_id(BookingId) of
{ok, Booking} ->
case Booking#booking.user_id =:= UserId of
true -> core_booking:delete(BookingId);
false -> {error, access_denied}
end;
Error -> Error
end.
%%%-------------------------------------------------------------------
%%% @doc Административное получение бронирования.
%%% @end
%%%-------------------------------------------------------------------
-spec get_booking_admin(BookingId :: binary()) ->
{ok, #booking{}} | {error, not_found}.
get_booking_admin(BookingId) ->
core_booking:get_by_id(BookingId).
%%%-------------------------------------------------------------------
%%% @doc Список бронирований события (административный / внутренний).
%%% @end
%%%-------------------------------------------------------------------
-spec list_event_bookings(EventId :: binary()) -> {ok, [#booking{}]}.
list_event_bookings(EventId) ->
core_booking:list_by_event(EventId).
%%%-------------------------------------------------------------------
%%% @doc Список бронирований события для владельца календаря (или admin).
%%% Возвращает полный список заявок без фильтрации по участнику.
%%% @end
%%%-------------------------------------------------------------------
-spec list_event_bookings(UserId :: binary(), EventId :: binary()) ->
{ok, [#booking{}]} | {error, not_found | access_denied}.
list_event_bookings(UserId, EventId) ->
case can_manage_event_bookings(UserId, EventId) of
true ->
core_booking:list_by_event(EventId);
{error, Reason} ->
{error, Reason}
end.
%%%-------------------------------------------------------------------
%%% @doc Список всех бронирований (административный).
%%% @end
%%%-------------------------------------------------------------------
-spec list_bookings_admin() -> {ok, [#booking{}]}.
list_bookings_admin() ->
{ok, core_booking:list_all()}.
%%%===================================================================
%%% ВНУТРЕННИЕ ФУНКЦИИ
%%%===================================================================
%%%-------------------------------------------------------------------
%%% @doc Владелец календаря события или admin может управлять заявками.
%%% @end
%%%-------------------------------------------------------------------
-spec can_manage_event_bookings(UserId :: binary(), EventId :: binary()) ->
true | {error, not_found | access_denied}.
can_manage_event_bookings(UserId, EventId) ->
case core_event:get_by_id(EventId) of
{ok, Event} ->
case core_calendar:get_by_id(Event#event.calendar_id) of
{ok, Calendar} ->
case Calendar#calendar.owner_id =:= UserId
orelse admin_utils:is_admin(UserId) of
true -> true;
false -> {error, access_denied}
end;
{error, not_found} ->
{error, not_found}
end;
{error, not_found} ->
{error, not_found}
end.
%%%-------------------------------------------------------------------
%%% @doc Проверка вместимости события.
%%% `undefined` и `0` означают неограниченную вместимость.
%%% @end
%%%-------------------------------------------------------------------
-spec check_capacity(EventId :: binary(), Capacity :: integer() | undefined) ->
{ok, integer() | unlimited} | {error, full}.
check_capacity(_EventId, undefined) -> {ok, unlimited};
check_capacity(_EventId, 0) -> {ok, unlimited};
check_capacity(EventId, Capacity) ->
{ok, Bookings} = core_booking:list_by_event(EventId),
ConfirmedCount = length([B || B <- Bookings, B#booking.status =:= confirmed]),
case ConfirmedCount < Capacity of
true -> {ok, Capacity - ConfirmedCount};
false -> {error, full}
end.