Expire past-pending bookings to expired status. Fixes EventHub/EventHubBack#60
CI / test (push) Successful in 7m14s
CI / deploy-ift (push) Successful in 3m40s
CI / e2e-ift (push) Successful in 1m43s
CI / deploy-stage (push) Successful in 3m8s
CI / e2e-stage (push) Successful in 1m12s

This commit is contained in:
2026-07-27 16:45:30 +03:00
parent 1d9752bb69
commit edb7c20f70
6 changed files with 181 additions and 36 deletions
+104 -31
View File
@@ -69,14 +69,14 @@ initial_status(_) -> pending.
%%%-------------------------------------------------------------------
-spec confirm_booking(BookingId :: binary(), UserId :: binary()) ->
{ok, #booking{}} | {error, not_found | access_denied | full}.
{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 Booking#booking.status of
pending ->
case ensure_pending_actionable(Booking) of
{ok, _} ->
case event_capacity_ok(Booking#booking.event_id) of
true ->
Now = calendar:universal_time(),
@@ -84,8 +84,8 @@ confirm_booking(BookingId, UserId) ->
false ->
{error, full}
end;
_ ->
{error, access_denied}
{error, Reason} ->
{error, Reason}
end;
{error, Reason} ->
{error, Reason}
@@ -94,7 +94,7 @@ confirm_booking(BookingId, UserId) ->
end.
-spec confirm_booking(UserId :: binary(), BookingId :: binary(), confirm | decline) ->
{ok, #booking{}} | {error, not_found | access_denied | full}.
{ok, #booking{}} | {error, not_found | access_denied | full | expired}.
confirm_booking(UserId, BookingId, confirm) ->
confirm_booking(BookingId, UserId);
confirm_booking(UserId, BookingId, decline) ->
@@ -102,11 +102,11 @@ confirm_booking(UserId, BookingId, decline) ->
{ok, Booking} ->
case can_manage_event_bookings(UserId, Booking#booking.event_id) of
true ->
case Booking#booking.status of
pending ->
case ensure_pending_actionable(Booking) of
{ok, _} ->
core_booking:update(BookingId, [{status, cancelled}]);
_ ->
{error, access_denied}
{error, Reason} ->
{error, Reason}
end;
{error, Reason} ->
{error, Reason}
@@ -122,6 +122,8 @@ cancel_booking(BookingId, UserId) ->
case Booking#booking.status of
cancelled ->
{ok, Booking};
expired ->
{ok, Booking};
_ ->
case Booking#booking.user_id =:= UserId of
true -> core_booking:update(BookingId, [{status, cancelled}]);
@@ -140,7 +142,8 @@ cancel_booking(UserId, BookingId, cancel) ->
{ok, #booking{}} | {error, not_found | access_denied}.
get_booking(BookingId, UserId) ->
case core_booking:get_by_id(BookingId) of
{ok, Booking} ->
{ok, Booking0} ->
Booking = ensure_not_past_pending(Booking0),
case Booking#booking.user_id =:= UserId of
true -> {ok, Booking};
false ->
@@ -160,11 +163,12 @@ list_bookings(EventId, UserId) ->
true -> Bookings;
false -> [B || B <- Bookings, B#booking.user_id =:= UserId]
end,
{ok, Filtered}.
{ok, [ensure_not_past_pending(B) || B <- Filtered]}.
-spec list_user_bookings(UserId :: binary()) -> {ok, [#booking{}]}.
list_user_bookings(UserId) ->
core_booking:list_by_user(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.
@@ -212,12 +216,26 @@ specialist_event_ids(UserId) ->
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} ->
[{B, Event, Role} || B <- Bookings, B#booking.status =:= pending];
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;
@@ -250,24 +268,25 @@ get_booking_admin(BookingId) ->
-spec list_event_bookings(EventId :: binary()) -> {ok, [#booking{}]}.
list_event_bookings(EventId) ->
core_booking:list_by_event(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 ->
core_booking:list_by_event(EventId);
list_event_bookings(EventId);
{error, Reason} ->
{error, Reason}
end.
-spec list_bookings_admin() -> {ok, [#booking{}]}.
list_bookings_admin() ->
{ok, core_booking:list_all()}.
{ok, [ensure_not_past_pending(B) || B <- core_booking:list_all()]}.
%%%-------------------------------------------------------------------
%%% @doc Авто-confirm/cancel по политике {timeout, N}.
%%% @doc Авто-confirm/cancel по политике {timeout, N}; past-pending → expired.
%%% @end
%%%-------------------------------------------------------------------
-spec process_timeout_bookings() -> ok.
@@ -280,23 +299,28 @@ process_timeout_bookings() ->
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
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 ->
Now = calendar:universal_time(),
_ = core_booking:update(Id, [{status, confirmed}, {confirmed_at, Now}]);
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 ->
_ = core_booking:update(Id, [{status, cancelled}])
ok
end;
false ->
_ ->
ok
end;
_ ->
ok
end
end;
_ ->
ok
@@ -338,6 +362,55 @@ cancel_pending_for_calendar(CalendarId) ->
%%% 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) ->
core_booking:update(BookingId, [{status, expired}]).
-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) ->