-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.