diff --git a/src/handlers/handler_booking_by_id.erl b/src/handlers/handler_booking_by_id.erl old mode 100644 new mode 100755 index 10eb941..c23e1db --- a/src/handlers/handler_booking_by_id.erl +++ b/src/handlers/handler_booking_by_id.erl @@ -64,6 +64,7 @@ trails() -> responses => #{ 200 => #{description => <<"Booking updated">>}, 400 => #{description => <<"Invalid action">>}, + 403 => #{description => <<"Access denied">>}, 404 => #{description => <<"Booking not found">>} } }, diff --git a/src/logic/logic_booking.erl b/src/logic/logic_booking.erl old mode 100644 new mode 100755 index 45b5896..c7a7dbe --- a/src/logic/logic_booking.erl +++ b/src/logic/logic_booking.erl @@ -32,31 +32,54 @@ create_booking(UserId, EventId) -> %%%------------------------------------------------------------------- %%% @doc Подтверждение бронирования (двухарная версия). +%%% Только владелец календаря события или admin. %%% @end %%%------------------------------------------------------------------- -spec confirm_booking(BookingId :: binary(), UserId :: binary()) -> {ok, #booking{}} | {error, not_found | access_denied}. -confirm_booking(BookingId, _UserId) -> +confirm_booking(BookingId, UserId) -> case core_booking:get_by_id(BookingId) of {ok, Booking} -> - case Booking#booking.status of - pending -> - Now = calendar:universal_time(), - core_booking:update(BookingId, [{status, confirmed}, {confirmed_at, Now}]); - _ -> - {error, access_denied} + 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 Подтверждение бронирования (трёхарная версия для обработчиков). +%%% @doc Подтверждение или отклонение бронирования (для обработчиков). +%%% `decline` переводит pending-заявку в `cancelled`. %%% @end %%%------------------------------------------------------------------- --spec confirm_booking(UserId :: binary(), BookingId :: binary(), confirm) -> +-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(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 Отмена бронирования (двухарная версия). @@ -167,20 +190,11 @@ list_event_bookings(EventId) -> -spec list_event_bookings(UserId :: binary(), EventId :: binary()) -> {ok, [#booking{}]} | {error, not_found | access_denied}. list_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 admin_utils:is_admin(UserId) - orelse Calendar#calendar.owner_id =:= UserId of - true -> core_booking:list_by_event(EventId); - false -> {error, access_denied} - end; - {error, not_found} -> - {error, not_found} - end; - {error, not_found} -> - {error, not_found} + case can_manage_event_bookings(UserId, EventId) of + true -> + core_booking:list_by_event(EventId); + {error, Reason} -> + {error, Reason} end. %%%------------------------------------------------------------------- @@ -195,6 +209,29 @@ list_bookings_admin() -> %%% ВНУТРЕННИЕ ФУНКЦИИ %%%=================================================================== +%%%------------------------------------------------------------------- +%%% @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` означают неограниченную вместимость. diff --git a/test/unit/booking_integration_tests.erl b/test/unit/booking_integration_tests.erl old mode 100644 new mode 100755 index dd4a94e..c50ed58 --- a/test/unit/booking_integration_tests.erl +++ b/test/unit/booking_integration_tests.erl @@ -2,7 +2,7 @@ -include_lib("eunit/include/eunit.hrl"). -include("records.hrl"). --define(TABLES, [user, calendar, event, booking]). +-define(TABLES, [user, calendar, event, booking, admin]). setup() -> eh_test_support:start_mnesia(), diff --git a/test/unit/logic_booking_tests.erl b/test/unit/logic_booking_tests.erl old mode 100644 new mode 100755 index 39b7416..2f030b0 --- a/test/unit/logic_booking_tests.erl +++ b/test/unit/logic_booking_tests.erl @@ -25,6 +25,11 @@ logic_booking_test_() -> {"Create booking when event is full", fun test_booking_event_full/0}, {"Pending bookings do not fill capacity", fun test_pending_does_not_fill/0}, {"Confirm booking", fun test_confirm_booking/0}, + {"Confirm booking as booker denied", fun test_confirm_booker_denied/0}, + {"Confirm booking as stranger denied", fun test_confirm_stranger_denied/0}, + {"Confirm booking as admin", fun test_confirm_booking_admin/0}, + {"Decline booking by owner", fun test_decline_booking/0}, + {"Decline booking as booker denied", fun test_decline_booker_denied/0}, {"Confirm non-pending booking denied", fun test_confirm_non_pending/0}, {"Cancel booking by participant", fun test_cancel_booking/0}, {"Cancel booking access denied", fun test_cancel_access_denied/0}, @@ -49,6 +54,11 @@ create_test_user(Role) -> mnesia:dirty_write(User), UserId. +create_test_admin() -> + AdminId = base64:encode(crypto:strong_rand_bytes(16), #{mode => urlsafe, padding => false}), + Admin = eh_test_support:seed_admin(#{id => AdminId, email => <>}), + Admin#admin.id. + create_test_calendar(OwnerId, Confirmation) -> {ok, Calendar} = core_calendar:create(OwnerId, <<"Test Calendar">>, <<"">>, Confirmation), Calendar#calendar.id. @@ -98,7 +108,7 @@ test_booking_event_full() -> EventId = create_test_event_with_capacity(CalendarId, 1), {ok, B1} = logic_booking:create_booking(Participant1Id, EventId), - {ok, _} = logic_booking:confirm_booking(Participant1Id, B1#booking.id, confirm), + {ok, _} = logic_booking:confirm_booking(OwnerId, B1#booking.id, confirm), {error, full} = logic_booking:create_booking(Participant2Id, EventId). test_pending_does_not_fill() -> @@ -122,6 +132,55 @@ test_confirm_booking() -> {ok, Confirmed} = logic_booking:confirm_booking(OwnerId, Booking#booking.id, confirm), ?assertEqual(confirmed, Confirmed#booking.status). +test_confirm_booker_denied() -> + OwnerId = create_test_user(user), + ParticipantId = create_test_user(user), + CalendarId = create_test_calendar(OwnerId, manual), + EventId = create_test_event(CalendarId), + + {ok, Booking} = logic_booking:create_booking(ParticipantId, EventId), + {error, access_denied} = logic_booking:confirm_booking(ParticipantId, Booking#booking.id, confirm). + +test_confirm_stranger_denied() -> + OwnerId = create_test_user(user), + ParticipantId = create_test_user(user), + StrangerId = create_test_user(user), + CalendarId = create_test_calendar(OwnerId, manual), + EventId = create_test_event(CalendarId), + + {ok, Booking} = logic_booking:create_booking(ParticipantId, EventId), + {error, access_denied} = logic_booking:confirm_booking(StrangerId, Booking#booking.id, confirm). + +test_confirm_booking_admin() -> + OwnerId = create_test_user(user), + ParticipantId = create_test_user(user), + AdminId = create_test_admin(), + CalendarId = create_test_calendar(OwnerId, manual), + EventId = create_test_event(CalendarId), + + {ok, Booking} = logic_booking:create_booking(ParticipantId, EventId), + {ok, Confirmed} = logic_booking:confirm_booking(AdminId, Booking#booking.id, confirm), + ?assertEqual(confirmed, Confirmed#booking.status). + +test_decline_booking() -> + OwnerId = create_test_user(user), + ParticipantId = create_test_user(user), + CalendarId = create_test_calendar(OwnerId, manual), + EventId = create_test_event(CalendarId), + + {ok, Booking} = logic_booking:create_booking(ParticipantId, EventId), + {ok, Declined} = logic_booking:confirm_booking(OwnerId, Booking#booking.id, decline), + ?assertEqual(cancelled, Declined#booking.status). + +test_decline_booker_denied() -> + OwnerId = create_test_user(user), + ParticipantId = create_test_user(user), + CalendarId = create_test_calendar(OwnerId, manual), + EventId = create_test_event(CalendarId), + + {ok, Booking} = logic_booking:create_booking(ParticipantId, EventId), + {error, access_denied} = logic_booking:confirm_booking(ParticipantId, Booking#booking.id, decline). + test_confirm_non_pending() -> OwnerId = create_test_user(user), ParticipantId = create_test_user(user),