333 lines
12 KiB
Erlang
Executable File
333 lines
12 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,
|
|
process_timeout_bookings/0, cancel_pending_for_owner/1,
|
|
cancel_pending_for_calendar/1]).
|
|
|
|
%%%-------------------------------------------------------------------
|
|
%%% @doc Создание бронирования с учётом commercial / confirmation / capacity.
|
|
%%% @end
|
|
%%%-------------------------------------------------------------------
|
|
-spec create_booking(UserId :: binary(), EventId :: binary()) ->
|
|
{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{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.
|
|
|
|
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 | full}.
|
|
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 ->
|
|
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;
|
|
{error, Reason} ->
|
|
{error, Reason}
|
|
end;
|
|
Error -> Error
|
|
end.
|
|
|
|
-spec confirm_booking(UserId :: binary(), BookingId :: binary(), confirm | decline) ->
|
|
{ok, #booking{}} | {error, not_found | access_denied | full}.
|
|
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.
|
|
|
|
-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.
|
|
|
|
-spec cancel_booking(UserId :: binary(), BookingId :: binary(), cancel) ->
|
|
{ok, #booking{}} | {error, not_found | access_denied}.
|
|
cancel_booking(UserId, BookingId, cancel) ->
|
|
cancel_booking(BookingId, UserId).
|
|
|
|
-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 ->
|
|
case can_manage_event_bookings(UserId, Booking#booking.event_id) of
|
|
true -> {ok, Booking};
|
|
{error, _} -> {error, access_denied}
|
|
end
|
|
end;
|
|
Error -> Error
|
|
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}.
|
|
|
|
-spec list_user_bookings(UserId :: binary()) -> {ok, [#booking{}]}.
|
|
list_user_bookings(UserId) ->
|
|
core_booking:list_by_user(UserId).
|
|
|
|
-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.
|
|
|
|
-spec get_booking_admin(BookingId :: binary()) ->
|
|
{ok, #booking{}} | {error, not_found}.
|
|
get_booking_admin(BookingId) ->
|
|
core_booking:get_by_id(BookingId).
|
|
|
|
-spec list_event_bookings(EventId :: binary()) -> {ok, [#booking{}]}.
|
|
list_event_bookings(EventId) ->
|
|
core_booking:list_by_event(EventId).
|
|
|
|
-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.
|
|
|
|
-spec list_bookings_admin() -> {ok, [#booking{}]}.
|
|
list_bookings_admin() ->
|
|
{ok, core_booking:list_all()}.
|
|
|
|
%%%-------------------------------------------------------------------
|
|
%%% @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) ->
|
|
case core_event:get_by_id(EventId) of
|
|
{ok, Event} ->
|
|
case core_calendar:get_by_id(Event#event.calendar_id) of
|
|
{ok, Calendar} ->
|
|
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;
|
|
{error, not_found} ->
|
|
{error, not_found}
|
|
end;
|
|
{error, not_found} ->
|
|
{error, not_found}
|
|
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) ->
|
|
case occupied_slots(EventId) < Capacity of
|
|
true -> {ok, Capacity - occupied_slots(EventId)};
|
|
false -> {error, full}
|
|
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.
|