12274f9ed0
Prefs in user.preferences; push_subscription table; VAPID send on reminder/waitlist. Refs EventHub/EventHubBack#75
661 lines
23 KiB
Erlang
Executable File
661 lines
23 KiB
Erlang
Executable File
-module(logic_booking).
|
|
-include("records.hrl").
|
|
-export([create_booking/2, create_booking/3, confirm_booking/2, confirm_booking/3,
|
|
cancel_booking/2, cancel_booking/3, get_booking/2,
|
|
list_bookings/2, list_user_bookings/1, list_user_booking_requests/1,
|
|
list_user_studio_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, process_reminders/0, cancel_pending_for_owner/1,
|
|
cancel_pending_for_calendar/1, can_manage_event_bookings/2]).
|
|
|
|
%%%-------------------------------------------------------------------
|
|
%%% @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 |
|
|
occurrence_start_required | invalid_occurrence}.
|
|
create_booking(UserId, EventId) ->
|
|
create_booking(UserId, EventId, undefined).
|
|
|
|
-spec create_booking(UserId :: binary(), EventId :: binary(),
|
|
OccurrenceStart :: calendar:datetime() | undefined) ->
|
|
{ok, #booking{}} |
|
|
{error, full | already_booked | not_found | personal_calendar |
|
|
subscription_inactive | own_event | event_not_active | access_denied |
|
|
occurrence_start_required | invalid_occurrence}.
|
|
create_booking(UserId, EventId, OccurrenceStart) ->
|
|
case core_event:get_by_id(EventId) of
|
|
{ok, #event{status = active} = Event} ->
|
|
case resolve_bookable_event(Event, OccurrenceStart) of
|
|
{ok, BookEvent} ->
|
|
case core_calendar:get_by_id(BookEvent#event.calendar_id) of
|
|
{ok, Calendar} ->
|
|
create_on_calendar(UserId, BookEvent, Calendar);
|
|
{error, not_found} ->
|
|
{error, not_found}
|
|
end;
|
|
{error, _} = Err ->
|
|
Err
|
|
end;
|
|
{ok, _} ->
|
|
{error, event_not_active};
|
|
{error, not_found} ->
|
|
{error, not_found}
|
|
end.
|
|
|
|
resolve_bookable_event(#event{event_type = recurring}, undefined) ->
|
|
{error, occurrence_start_required};
|
|
resolve_bookable_event(#event{event_type = recurring} = Master, OccurrenceStart) ->
|
|
case logic_event:validate_occurrence(Master, OccurrenceStart) of
|
|
ok ->
|
|
logic_event:materialize_for_booking(
|
|
Master#event.id, OccurrenceStart, Master#event.specialist_id);
|
|
{error, _} = Err ->
|
|
Err
|
|
end;
|
|
resolve_bookable_event(Event, _OccurrenceStart) ->
|
|
{ok, Event}.
|
|
|
|
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 | expired}.
|
|
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 ensure_pending_actionable(Booking) of
|
|
{ok, _} ->
|
|
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, Reason} ->
|
|
{error, Reason}
|
|
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 | expired}.
|
|
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 ensure_pending_actionable(Booking) of
|
|
{ok, _} ->
|
|
case core_booking:update(BookingId, [{status, cancelled}]) of
|
|
{ok, _} = Ok ->
|
|
_ = logic_waitlist:maybe_promote(Booking#booking.event_id),
|
|
Ok;
|
|
Err ->
|
|
Err
|
|
end;
|
|
{error, Reason} ->
|
|
{error, Reason}
|
|
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};
|
|
expired ->
|
|
{ok, Booking};
|
|
_ ->
|
|
case Booking#booking.user_id =:= UserId of
|
|
true ->
|
|
case core_booking:update(BookingId, [{status, cancelled}]) of
|
|
{ok, _} = Ok ->
|
|
_ = logic_waitlist:maybe_promote(Booking#booking.event_id),
|
|
Ok;
|
|
Err ->
|
|
Err
|
|
end;
|
|
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, Booking0} ->
|
|
Booking = ensure_not_past_pending(Booking0),
|
|
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, [ensure_not_past_pending(B) || B <- Filtered]}.
|
|
|
|
-spec list_user_bookings(UserId :: binary()) -> {ok, [#booking{}]}.
|
|
list_user_bookings(UserId) ->
|
|
{ok, Bookings} = core_booking:list_by_user(UserId),
|
|
{ok, [ensure_not_past_pending(B) || B <- Bookings]}.
|
|
|
|
%%%-------------------------------------------------------------------
|
|
%%% @doc Pending bookings the user can confirm/decline as owner or specialist.
|
|
%%% Returns `{ok, [{Booking, Event, Role}]}` where Role is `owner` | `specialist`.
|
|
%%% @end
|
|
%%%-------------------------------------------------------------------
|
|
-spec list_user_booking_requests(UserId :: binary()) ->
|
|
{ok, [{#booking{}, #event{}, owner | specialist}]}.
|
|
list_user_booking_requests(UserId) ->
|
|
{OwnedEventIds, SpecOnly} = managed_event_ids(UserId),
|
|
OwnerItems = collect_pending(OwnedEventIds, owner),
|
|
SpecItems = collect_pending(SpecOnly, specialist),
|
|
{ok, sort_requests(OwnerItems ++ SpecItems)}.
|
|
|
|
-spec list_user_studio_bookings(UserId :: binary()) ->
|
|
{ok, [{#booking{}, #event{}, owner | specialist}]}.
|
|
list_user_studio_bookings(UserId) ->
|
|
{OwnedEventIds, SpecOnly} = managed_event_ids(UserId),
|
|
OwnerItems = collect_studio(OwnedEventIds, owner),
|
|
SpecItems = collect_studio(SpecOnly, specialist),
|
|
{ok, sort_requests(OwnerItems ++ SpecItems)}.
|
|
|
|
managed_event_ids(UserId) ->
|
|
OwnedEventIds = owned_event_ids(UserId),
|
|
SpecEventIds = specialist_event_ids(UserId),
|
|
OwnedSet = sets:from_list(OwnedEventIds),
|
|
SpecOnly = [E || E <- SpecEventIds, not sets:is_element(E, OwnedSet)],
|
|
{OwnedEventIds, SpecOnly}.
|
|
|
|
owned_event_ids(UserId) ->
|
|
case core_calendar:list_by_owner(UserId) of
|
|
{ok, Cals} ->
|
|
lists:flatmap(fun(#calendar{id = CalId}) ->
|
|
case core_event:list_active_including_instances(CalId) of
|
|
{ok, Events} -> [E#event.id || E <- Events];
|
|
_ -> []
|
|
end
|
|
end, Cals);
|
|
_ ->
|
|
[]
|
|
end.
|
|
|
|
specialist_event_ids(UserId) ->
|
|
Specs = [S || S <- core_calendar_specialist:list_by_user(UserId),
|
|
S#calendar_specialist.status =:= active],
|
|
lists:flatmap(fun(#calendar_specialist{calendar_id = CalId}) ->
|
|
case core_event:list_active_including_instances(CalId) of
|
|
{ok, Events} ->
|
|
[E#event.id || E <- Events,
|
|
is_binary(E#event.specialist_id),
|
|
E#event.specialist_id =/= <<>>,
|
|
E#event.specialist_id =:= UserId];
|
|
_ ->
|
|
[]
|
|
end
|
|
end, Specs).
|
|
|
|
collect_pending(EventIds, Role) ->
|
|
NowSec = calendar:datetime_to_gregorian_seconds(calendar:universal_time()),
|
|
lists:flatmap(fun(EventId) ->
|
|
case core_booking:list_by_event(EventId) of
|
|
{ok, Bookings} ->
|
|
case core_event:get_by_id(EventId) of
|
|
{ok, Event} ->
|
|
lists:filtermap(fun(B) ->
|
|
case B#booking.status of
|
|
pending ->
|
|
case event_started(Event, NowSec) of
|
|
true ->
|
|
_ = mark_expired(B#booking.id),
|
|
false;
|
|
false ->
|
|
{true, {B, Event, Role}}
|
|
end;
|
|
_ ->
|
|
false
|
|
end
|
|
end, Bookings);
|
|
_ ->
|
|
[]
|
|
end;
|
|
_ ->
|
|
[]
|
|
end
|
|
end, EventIds).
|
|
|
|
collect_studio(EventIds, Role) ->
|
|
NowSec = calendar:datetime_to_gregorian_seconds(calendar:universal_time()),
|
|
lists:flatmap(fun(EventId) ->
|
|
case core_booking:list_by_event(EventId) of
|
|
{ok, Bookings} ->
|
|
case core_event:get_by_id(EventId) of
|
|
{ok, Event} ->
|
|
lists:filtermap(fun(B) ->
|
|
case B#booking.status of
|
|
pending ->
|
|
case event_started(Event, NowSec) of
|
|
true ->
|
|
_ = mark_expired(B#booking.id),
|
|
false;
|
|
false ->
|
|
{true, {B, Event, Role}}
|
|
end;
|
|
confirmed ->
|
|
{true, {B, Event, Role}};
|
|
_ ->
|
|
false
|
|
end
|
|
end, Bookings);
|
|
_ ->
|
|
[]
|
|
end;
|
|
_ ->
|
|
[]
|
|
end
|
|
end, EventIds).
|
|
|
|
sort_requests(Items) ->
|
|
lists:sort(fun({A, _, _}, {B, _, _}) ->
|
|
A#booking.created_at >= B#booking.created_at
|
|
end, Items).
|
|
|
|
-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) ->
|
|
{ok, Bookings} = core_booking:list_by_event(EventId),
|
|
{ok, [ensure_not_past_pending(B) || B <- Bookings]}.
|
|
|
|
-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 ->
|
|
list_event_bookings(EventId);
|
|
{error, Reason} ->
|
|
{error, Reason}
|
|
end.
|
|
|
|
-spec list_bookings_admin() -> {ok, [#booking{}]}.
|
|
list_bookings_admin() ->
|
|
{ok, [ensure_not_past_pending(B) || B <- core_booking:list_all()]}.
|
|
|
|
%%%-------------------------------------------------------------------
|
|
%%% @doc Авто-confirm/cancel по политике {timeout, N}; past-pending → expired.
|
|
%%% @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.
|
|
|
|
%%%-------------------------------------------------------------------
|
|
%%% @doc Email + in-app reminder before event start (Back#70).
|
|
%%% Confirmed bookings with reminder_sent=false whose event starts within
|
|
%%% REMINDER_LEAD_HOURS (default 24). Flag set before send (once).
|
|
%%% @end
|
|
%%%-------------------------------------------------------------------
|
|
-spec process_reminders() -> ok.
|
|
process_reminders() ->
|
|
NowSec = calendar:datetime_to_gregorian_seconds(calendar:universal_time()),
|
|
LeadSec = reminder_lead_hours() * 3600,
|
|
Horizon = NowSec + LeadSec,
|
|
Candidates = mnesia:dirty_match_object(
|
|
#booking{status = confirmed, reminder_sent = false, _ = '_'}),
|
|
lists:foreach(fun(B) -> maybe_remind(B, NowSec, Horizon) end, Candidates),
|
|
ok.
|
|
|
|
maybe_remind(#booking{id = Id, event_id = EventId, user_id = UserId} = Booking,
|
|
NowSec, Horizon) ->
|
|
case core_event:get_by_id(EventId) of
|
|
{ok, #event{status = active, start_time = Start, title = Title,
|
|
calendar_id = CalId} = _Event} ->
|
|
StartSec = calendar:datetime_to_gregorian_seconds(Start),
|
|
case StartSec > NowSec andalso StartSec =< Horizon of
|
|
true ->
|
|
case core_booking:update(Id, [{reminder_sent, true}]) of
|
|
{ok, _} ->
|
|
send_reminder(UserId, Title, Start, CalId, EventId),
|
|
ok;
|
|
_ ->
|
|
ok
|
|
end;
|
|
false ->
|
|
ok
|
|
end;
|
|
_ ->
|
|
ok
|
|
end,
|
|
Booking.
|
|
|
|
send_reminder(UserId, Title, Start, CalId, EventId) ->
|
|
Path = <<"/c/", CalId/binary, "/e/", EventId/binary>>,
|
|
WhenText = format_when(Start),
|
|
case logic_notification_prefs:email_enabled(UserId) of
|
|
true ->
|
|
case core_user:get_by_id(UserId) of
|
|
{ok, #user{email = Email}} when is_binary(Email), Email =/= <<>> ->
|
|
_ = logic_email:send_booking_reminder(Email, Title, Start, Path);
|
|
_ ->
|
|
ok
|
|
end;
|
|
false ->
|
|
ok
|
|
end,
|
|
_ = logic_notification:notify_event_reminder(UserId, #{
|
|
event_id => EventId,
|
|
calendar_id => CalId,
|
|
title => Title,
|
|
start_time => Start,
|
|
when_text => WhenText
|
|
}),
|
|
_ = logic_web_push:send(
|
|
UserId,
|
|
<<"Напоминание о записи"/utf8>>,
|
|
iolist_to_binary([<<"Скоро: «"/utf8>>, Title, <<"» — "/utf8>>, WhenText]),
|
|
Path
|
|
),
|
|
ok.
|
|
|
|
format_when({{Y, Mo, D}, {H, Mi, _S}}) ->
|
|
iolist_to_binary(io_lib:format("~4..0B-~2..0B-~2..0B ~2..0B:~2..0B",
|
|
[Y, Mo, D, H, Mi])).
|
|
|
|
reminder_lead_hours() ->
|
|
case application:get_env(eventhub, reminder_lead_hours) of
|
|
{ok, N} when is_integer(N), N > 0 -> N;
|
|
_ ->
|
|
case os:getenv("REMINDER_LEAD_HOURS") of
|
|
false -> 24;
|
|
"" -> 24;
|
|
S ->
|
|
try list_to_integer(S) of
|
|
N when N > 0 -> N;
|
|
_ -> 24
|
|
catch
|
|
_:_ -> 24
|
|
end
|
|
end
|
|
end.
|
|
|
|
maybe_timeout(#booking{id = Id, event_id = EventId, created_at = Created} = Booking, NowSec) ->
|
|
case core_event:get_by_id(EventId) of
|
|
{ok, Event} ->
|
|
case event_started(Event, NowSec) of
|
|
true ->
|
|
_ = mark_expired(Id);
|
|
false ->
|
|
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 ->
|
|
case core_booking:update(Id, [{status, cancelled}]) of
|
|
{ok, _} ->
|
|
_ = logic_waitlist:maybe_promote(EventId);
|
|
_ ->
|
|
ok
|
|
end
|
|
end;
|
|
false ->
|
|
ok
|
|
end;
|
|
_ ->
|
|
ok
|
|
end
|
|
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
|
|
%%%===================================================================
|
|
|
|
%% @doc Pending after event start is no longer actionable → expired.
|
|
-spec ensure_pending_actionable(#booking{}) ->
|
|
{ok, #booking{}} | {error, access_denied | expired | not_found}.
|
|
ensure_pending_actionable(#booking{status = pending} = Booking) ->
|
|
case core_event:get_by_id(Booking#booking.event_id) of
|
|
{ok, Event} ->
|
|
NowSec = calendar:datetime_to_gregorian_seconds(calendar:universal_time()),
|
|
case event_started(Event, NowSec) of
|
|
true ->
|
|
_ = mark_expired(Booking#booking.id),
|
|
{error, expired};
|
|
false ->
|
|
{ok, Booking}
|
|
end;
|
|
{error, not_found} ->
|
|
{error, not_found}
|
|
end;
|
|
ensure_pending_actionable(_) ->
|
|
{error, access_denied}.
|
|
|
|
-spec ensure_not_past_pending(#booking{}) -> #booking{}.
|
|
ensure_not_past_pending(#booking{status = pending, id = Id, event_id = EventId} = B) ->
|
|
case core_event:get_by_id(EventId) of
|
|
{ok, Event} ->
|
|
NowSec = calendar:datetime_to_gregorian_seconds(calendar:universal_time()),
|
|
case event_started(Event, NowSec) of
|
|
true ->
|
|
case mark_expired(Id) of
|
|
{ok, Updated} -> Updated;
|
|
_ -> B#booking{status = expired}
|
|
end;
|
|
false ->
|
|
B
|
|
end;
|
|
_ ->
|
|
B
|
|
end;
|
|
ensure_not_past_pending(B) ->
|
|
B.
|
|
|
|
-spec mark_expired(binary()) -> {ok, #booking{}} | {error, term()}.
|
|
mark_expired(BookingId) ->
|
|
case core_booking:get_by_id(BookingId) of
|
|
{ok, #booking{event_id = EventId}} ->
|
|
case core_booking:update(BookingId, [{status, expired}]) of
|
|
{ok, _} = Ok ->
|
|
_ = logic_waitlist:maybe_promote(EventId),
|
|
Ok;
|
|
Err ->
|
|
Err
|
|
end;
|
|
{error, _} = Err ->
|
|
Err
|
|
end.
|
|
|
|
-spec event_started(#event{}, non_neg_integer()) -> boolean().
|
|
event_started(#event{start_time = Start}, NowSec) ->
|
|
StartSec = calendar:datetime_to_gregorian_seconds(Start),
|
|
NowSec >= StartSec.
|
|
|
|
-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.
|