Implement commercial calendars: booking_open, specialists, expire without type downgrade.
Refs EventHub/EventHubBack#54
This commit is contained in:
+168
-86
@@ -4,39 +4,71 @@
|
||||
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]).
|
||||
list_event_bookings/1, list_event_bookings/2,
|
||||
process_timeout_bookings/0, cancel_pending_for_owner/1,
|
||||
cancel_pending_for_calendar/1]).
|
||||
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Создание бронирования со статусом `pending`.
|
||||
%%% @doc Создание бронирования с учётом commercial / confirmation / capacity.
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-spec create_booking(UserId :: binary(), EventId :: binary()) ->
|
||||
{ok, #booking{}} | {error, full | already_booked | not_found}.
|
||||
{ok, #booking{}} |
|
||||
{error, full | already_booked | not_found | personal_calendar |
|
||||
subscription_inactive | own_event | event_not_active | access_denied}.
|
||||
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}
|
||||
{ok, #event{status = active} = Event} ->
|
||||
case core_calendar:get_by_id(Event#event.calendar_id) of
|
||||
{ok, Calendar} ->
|
||||
create_on_calendar(UserId, Event, Calendar);
|
||||
{error, not_found} ->
|
||||
{error, not_found}
|
||||
end;
|
||||
{ok, _} ->
|
||||
{error, event_not_active};
|
||||
{error, not_found} ->
|
||||
{error, not_found}
|
||||
end.
|
||||
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Подтверждение бронирования (двухарная версия).
|
||||
%%% Только владелец календаря события или admin.
|
||||
%%% @end
|
||||
create_on_calendar(UserId, _Event, #calendar{owner_id = UserId}) ->
|
||||
{error, own_event};
|
||||
create_on_calendar(_UserId, _Event, #calendar{type = personal}) ->
|
||||
{error, personal_calendar};
|
||||
create_on_calendar(UserId, Event, #calendar{type = commercial} = Calendar) ->
|
||||
case logic_calendar:booking_open(Calendar) of
|
||||
false ->
|
||||
{error, subscription_inactive};
|
||||
true ->
|
||||
case active_booking(Event#event.id, UserId) of
|
||||
{ok, _} ->
|
||||
{error, already_booked};
|
||||
{error, not_found} ->
|
||||
case check_capacity(Event#event.id, Event#event.capacity) of
|
||||
{ok, _} ->
|
||||
Initial = initial_status(Calendar#calendar.confirmation),
|
||||
case core_booking:create(Event#event.id, UserId, Initial) of
|
||||
{ok, Booking} when Initial =:= confirmed ->
|
||||
Now = calendar:universal_time(),
|
||||
core_booking:update(Booking#booking.id,
|
||||
[{status, confirmed}, {confirmed_at, Now}]);
|
||||
Other ->
|
||||
Other
|
||||
end;
|
||||
{error, full} ->
|
||||
{error, full}
|
||||
end
|
||||
end
|
||||
end;
|
||||
create_on_calendar(_, _, _) ->
|
||||
{error, access_denied}.
|
||||
|
||||
initial_status(auto) -> confirmed;
|
||||
initial_status(_) -> pending.
|
||||
|
||||
%%%-------------------------------------------------------------------
|
||||
-spec confirm_booking(BookingId :: binary(), UserId :: binary()) ->
|
||||
{ok, #booking{}} | {error, not_found | access_denied}.
|
||||
{ok, #booking{}} | {error, not_found | access_denied | full}.
|
||||
confirm_booking(BookingId, UserId) ->
|
||||
case core_booking:get_by_id(BookingId) of
|
||||
{ok, Booking} ->
|
||||
@@ -44,8 +76,13 @@ confirm_booking(BookingId, UserId) ->
|
||||
true ->
|
||||
case Booking#booking.status of
|
||||
pending ->
|
||||
Now = calendar:universal_time(),
|
||||
core_booking:update(BookingId, [{status, confirmed}, {confirmed_at, Now}]);
|
||||
case event_capacity_ok(Booking#booking.event_id) of
|
||||
true ->
|
||||
Now = calendar:universal_time(),
|
||||
core_booking:update(BookingId, [{status, confirmed}, {confirmed_at, Now}]);
|
||||
false ->
|
||||
{error, full}
|
||||
end;
|
||||
_ ->
|
||||
{error, access_denied}
|
||||
end;
|
||||
@@ -55,13 +92,8 @@ confirm_booking(BookingId, UserId) ->
|
||||
Error -> Error
|
||||
end.
|
||||
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Подтверждение или отклонение бронирования (для обработчиков).
|
||||
%%% `decline` переводит pending-заявку в `cancelled`.
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-spec confirm_booking(UserId :: binary(), BookingId :: binary(), confirm | decline) ->
|
||||
{ok, #booking{}} | {error, not_found | access_denied}.
|
||||
{ok, #booking{}} | {error, not_found | access_denied | full}.
|
||||
confirm_booking(UserId, BookingId, confirm) ->
|
||||
confirm_booking(BookingId, UserId);
|
||||
confirm_booking(UserId, BookingId, decline) ->
|
||||
@@ -81,10 +113,6 @@ confirm_booking(UserId, BookingId, decline) ->
|
||||
Error -> Error
|
||||
end.
|
||||
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Отмена бронирования (двухарная версия).
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-spec cancel_booking(BookingId :: binary(), UserId :: binary()) ->
|
||||
{ok, #booking{}} | {error, not_found | access_denied}.
|
||||
cancel_booking(BookingId, UserId) ->
|
||||
@@ -102,19 +130,11 @@ cancel_booking(BookingId, UserId) ->
|
||||
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) ->
|
||||
@@ -122,15 +142,15 @@ get_booking(BookingId, UserId) ->
|
||||
{ok, Booking} ->
|
||||
case Booking#booking.user_id =:= UserId of
|
||||
true -> {ok, Booking};
|
||||
false -> {error, access_denied}
|
||||
false ->
|
||||
case can_manage_event_bookings(UserId, Booking#booking.event_id) of
|
||||
true -> {ok, Booking};
|
||||
{error, _} -> {error, access_denied}
|
||||
end
|
||||
end;
|
||||
Error -> Error
|
||||
end.
|
||||
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Список бронирований события.
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-spec list_bookings(EventId :: binary(), UserId :: binary()) ->
|
||||
{ok, [#booking{}]}.
|
||||
list_bookings(EventId, UserId) ->
|
||||
@@ -141,18 +161,10 @@ list_bookings(EventId, 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) ->
|
||||
@@ -165,28 +177,15 @@ delete_booking(BookingId, UserId) ->
|
||||
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) ->
|
||||
@@ -197,22 +196,82 @@ list_event_bookings(UserId, EventId) ->
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Список всех бронирований (административный).
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-spec list_bookings_admin() -> {ok, [#booking{}]}.
|
||||
list_bookings_admin() ->
|
||||
{ok, core_booking:list_all()}.
|
||||
|
||||
%%%===================================================================
|
||||
%%% ВНУТРЕННИЕ ФУНКЦИИ
|
||||
%%%===================================================================
|
||||
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Владелец календаря события или admin может управлять заявками.
|
||||
%%% @doc Авто-confirm/cancel по политике {timeout, N}.
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-spec process_timeout_bookings() -> ok.
|
||||
process_timeout_bookings() ->
|
||||
NowSec = calendar:datetime_to_gregorian_seconds(calendar:universal_time()),
|
||||
Pending = mnesia:dirty_match_object(#booking{status = pending, _ = '_'}),
|
||||
lists:foreach(fun(B) -> maybe_timeout(B, NowSec) end, Pending),
|
||||
ok.
|
||||
|
||||
maybe_timeout(#booking{id = Id, event_id = EventId, created_at = Created} = Booking, NowSec) ->
|
||||
case core_event:get_by_id(EventId) of
|
||||
{ok, Event} ->
|
||||
case core_calendar:get_by_id(Event#event.calendar_id) of
|
||||
{ok, #calendar{confirmation = {timeout, N}}} when is_integer(N), N > 0 ->
|
||||
CreatedSec = calendar:datetime_to_gregorian_seconds(Created),
|
||||
case NowSec - CreatedSec >= N of
|
||||
true ->
|
||||
case event_capacity_ok(EventId) of
|
||||
true ->
|
||||
Now = calendar:universal_time(),
|
||||
_ = core_booking:update(Id, [{status, confirmed}, {confirmed_at, Now}]);
|
||||
false ->
|
||||
_ = core_booking:update(Id, [{status, cancelled}])
|
||||
end;
|
||||
false ->
|
||||
ok
|
||||
end;
|
||||
_ ->
|
||||
ok
|
||||
end;
|
||||
_ ->
|
||||
ok
|
||||
end,
|
||||
Booking.
|
||||
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Отмена всех pending владельца (expire подписки).
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-spec cancel_pending_for_owner(UserId :: binary()) -> ok.
|
||||
cancel_pending_for_owner(UserId) ->
|
||||
case core_calendar:list_by_owner(UserId) of
|
||||
{ok, Cals} ->
|
||||
lists:foreach(fun(#calendar{id = Id, type = commercial}) ->
|
||||
cancel_pending_for_calendar(Id);
|
||||
(_) -> ok
|
||||
end, Cals);
|
||||
_ -> ok
|
||||
end.
|
||||
|
||||
-spec cancel_pending_for_calendar(CalendarId :: binary()) -> ok.
|
||||
cancel_pending_for_calendar(CalendarId) ->
|
||||
case core_event:list_by_calendar(CalendarId) of
|
||||
{ok, Events} ->
|
||||
lists:foreach(fun(#event{id = EventId}) ->
|
||||
{ok, Bookings} = core_booking:list_by_event(EventId),
|
||||
lists:foreach(fun
|
||||
(#booking{id = Bid, status = pending}) ->
|
||||
_ = core_booking:update(Bid, [{status, cancelled}]);
|
||||
(_) -> ok
|
||||
end, Bookings)
|
||||
end, Events);
|
||||
_ -> ok
|
||||
end,
|
||||
ok.
|
||||
|
||||
%%%===================================================================
|
||||
%%% INTERNAL
|
||||
%%%===================================================================
|
||||
|
||||
-spec can_manage_event_bookings(UserId :: binary(), EventId :: binary()) ->
|
||||
true | {error, not_found | access_denied}.
|
||||
can_manage_event_bookings(UserId, EventId) ->
|
||||
@@ -220,8 +279,14 @@ can_manage_event_bookings(UserId, EventId) ->
|
||||
{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
|
||||
OwnerOk = Calendar#calendar.owner_id =:= UserId,
|
||||
AdminOk = admin_utils:is_admin(UserId),
|
||||
SpecOk = is_binary(Event#event.specialist_id)
|
||||
andalso Event#event.specialist_id =/= <<>>
|
||||
andalso Event#event.specialist_id =:= UserId
|
||||
andalso core_calendar_specialist:is_active_specialist(
|
||||
Calendar#calendar.id, UserId),
|
||||
case OwnerOk orelse AdminOk orelse SpecOk of
|
||||
true -> true;
|
||||
false -> {error, access_denied}
|
||||
end;
|
||||
@@ -232,19 +297,36 @@ can_manage_event_bookings(UserId, EventId) ->
|
||||
{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};
|
||||
case occupied_slots(EventId) < Capacity of
|
||||
true -> {ok, Capacity - occupied_slots(EventId)};
|
||||
false -> {error, full}
|
||||
end.
|
||||
end.
|
||||
|
||||
event_capacity_ok(EventId) ->
|
||||
case core_event:get_by_id(EventId) of
|
||||
{ok, #event{capacity = Cap}} when Cap =:= undefined; Cap =:= 0 ->
|
||||
true;
|
||||
{ok, #event{capacity = Cap}} when is_integer(Cap) ->
|
||||
%% при confirm текущий pending уже в occupied — слот свой, ок если <= Cap
|
||||
occupied_slots(EventId) =< Cap;
|
||||
_ ->
|
||||
false
|
||||
end.
|
||||
|
||||
occupied_slots(EventId) ->
|
||||
{ok, Bookings} = core_booking:list_by_event(EventId),
|
||||
length([B || B <- Bookings,
|
||||
B#booking.status =:= pending orelse B#booking.status =:= confirmed]).
|
||||
|
||||
active_booking(EventId, UserId) ->
|
||||
Match = #booking{event_id = EventId, user_id = UserId, _ = '_'},
|
||||
case [B || B <- mnesia:dirty_match_object(Match),
|
||||
B#booking.status =:= pending orelse B#booking.status =:= confirmed] of
|
||||
[B | _] -> {ok, B};
|
||||
[] -> {error, not_found}
|
||||
end.
|
||||
|
||||
Reference in New Issue
Block a user