Expire past-pending bookings to expired status. Fixes EventHub/EventHubBack#60
This commit is contained in:
+1
-1
@@ -192,7 +192,7 @@
|
||||
id :: binary(),
|
||||
event_id :: binary(), % ссылка на конкретный экземпляр события
|
||||
user_id :: binary(),
|
||||
status :: pending | confirmed | cancelled,
|
||||
status :: pending | confirmed | cancelled | expired,
|
||||
notes :: binary(),
|
||||
reminder_sent :: boolean(),
|
||||
confirmed_at :: calendar:datetime(),
|
||||
|
||||
@@ -88,7 +88,7 @@ booking_schema() ->
|
||||
id => #{type => string},
|
||||
event_id => #{type => string},
|
||||
user_id => #{type => string},
|
||||
status => #{type => string, enum => [<<"pending">>, <<"confirmed">>, <<"cancelled">>]},
|
||||
status => #{type => string, enum => [<<"pending">>, <<"confirmed">>, <<"cancelled">>, <<"expired">>]},
|
||||
notes => #{type => string, nullable => true},
|
||||
reminder_sent => #{type => boolean},
|
||||
confirmed_at => #{type => string, format => <<"date-time">>, nullable => true},
|
||||
@@ -143,6 +143,10 @@ update_booking(Req) ->
|
||||
handler_utils:send_json(Req2, 200, booking_to_json(Booking));
|
||||
{error, access_denied} ->
|
||||
handler_utils:send_error(Req2, 403, <<"Access denied">>);
|
||||
{error, expired} ->
|
||||
handler_utils:send_error(Req2, 409, <<"Booking expired">>);
|
||||
{error, full} ->
|
||||
handler_utils:send_error(Req2, 409, <<"Event is full">>);
|
||||
{error, not_found} ->
|
||||
handler_utils:send_error(Req2, 404, <<"Booking not found">>);
|
||||
{error, _} ->
|
||||
|
||||
@@ -92,7 +92,7 @@ booking_schema() ->
|
||||
id => #{type => string},
|
||||
event_id => #{type => string},
|
||||
user_id => #{type => string},
|
||||
status => #{type => string, enum => [<<"pending">>, <<"confirmed">>, <<"cancelled">>]},
|
||||
status => #{type => string, enum => [<<"pending">>, <<"confirmed">>, <<"cancelled">>, <<"expired">>]},
|
||||
notes => #{type => string, nullable => true},
|
||||
reminder_sent => #{type => boolean},
|
||||
confirmed_at => #{type => string, format => <<"date-time">>, nullable => true},
|
||||
|
||||
@@ -45,7 +45,7 @@ booking_schema() ->
|
||||
id => #{type => string},
|
||||
event_id => #{type => string},
|
||||
user_id => #{type => string},
|
||||
status => #{type => string, enum => [<<"pending">>, <<"confirmed">>, <<"cancelled">>]},
|
||||
status => #{type => string, enum => [<<"pending">>, <<"confirmed">>, <<"cancelled">>, <<"expired">>]},
|
||||
notes => #{type => string, nullable => true},
|
||||
reminder_sent => #{type => boolean},
|
||||
confirmed_at => #{type => string, format => <<"date-time">>, nullable => true},
|
||||
|
||||
+104
-31
@@ -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) ->
|
||||
|
||||
@@ -42,7 +42,12 @@ logic_booking_test_() ->
|
||||
{"List user bookings", fun test_list_user_bookings/0},
|
||||
{"List booking requests as owner", fun test_list_booking_requests_owner/0},
|
||||
{"List booking requests as specialist", fun test_list_booking_requests_specialist/0},
|
||||
{"List booking requests stranger empty", fun test_list_booking_requests_stranger/0}
|
||||
{"List booking requests stranger empty", fun test_list_booking_requests_stranger/0},
|
||||
{"Past pending expires on list", fun test_past_pending_expires_on_list/0},
|
||||
{"Past pending excluded from booking requests", fun test_past_pending_excluded_from_requests/0},
|
||||
{"Past pending confirm denied", fun test_past_pending_confirm_denied/0},
|
||||
{"Future pending still confirmable", fun test_future_pending_still_confirmable/0},
|
||||
{"Process timeout expires past pending", fun test_process_timeout_expires_past/0}
|
||||
]}.
|
||||
|
||||
%% Вспомогательные функции
|
||||
@@ -83,6 +88,14 @@ create_test_event(CalendarId) ->
|
||||
{ok, Event} = core_event:create(CalendarId, <<"Test Event">>, StartTime, 60),
|
||||
Event#event.id.
|
||||
|
||||
create_test_event_at(CalendarId, StartTime) ->
|
||||
{ok, Event} = core_event:create(CalendarId, <<"Test Event">>, StartTime, 60),
|
||||
Event#event.id.
|
||||
|
||||
past_start() ->
|
||||
Sec = calendar:datetime_to_gregorian_seconds(calendar:universal_time()) - 3600,
|
||||
calendar:gregorian_seconds_to_datetime(Sec).
|
||||
|
||||
create_test_event_with_capacity(CalendarId, Capacity) ->
|
||||
StartTime = eh_test_support:future_start(),
|
||||
{ok, Event} = core_event:create(CalendarId, <<"Test Event">>, StartTime, 60),
|
||||
@@ -355,3 +368,58 @@ test_list_booking_requests_stranger() ->
|
||||
EventId = create_test_event(CalendarId),
|
||||
{ok, _} = logic_booking:create_booking(ParticipantId, EventId),
|
||||
{ok, []} = logic_booking:list_user_booking_requests(StrangerId).
|
||||
|
||||
test_past_pending_expires_on_list() ->
|
||||
OwnerId = create_test_user(user),
|
||||
ParticipantId = create_test_user(user),
|
||||
CalendarId = create_test_calendar(OwnerId, manual),
|
||||
EventId = create_test_event_at(CalendarId, past_start()),
|
||||
{ok, Booking} = core_booking:create(EventId, ParticipantId, pending),
|
||||
{ok, [Listed]} = logic_booking:list_user_bookings(ParticipantId),
|
||||
?assertEqual(Booking#booking.id, Listed#booking.id),
|
||||
?assertEqual(expired, Listed#booking.status),
|
||||
{ok, Stored} = core_booking:get_by_id(Booking#booking.id),
|
||||
?assertEqual(expired, Stored#booking.status).
|
||||
|
||||
test_past_pending_excluded_from_requests() ->
|
||||
OwnerId = create_test_user(user),
|
||||
ParticipantId = create_test_user(user),
|
||||
CalendarId = create_test_calendar(OwnerId, manual),
|
||||
PastId = create_test_event_at(CalendarId, past_start()),
|
||||
FutureId = create_test_event(CalendarId),
|
||||
{ok, _} = core_booking:create(PastId, ParticipantId, pending),
|
||||
{ok, FutureBooking} = logic_booking:create_booking(ParticipantId, FutureId),
|
||||
{ok, Items} = logic_booking:list_user_booking_requests(OwnerId),
|
||||
?assertEqual(1, length(Items)),
|
||||
[{B, Event, owner}] = Items,
|
||||
?assertEqual(FutureBooking#booking.id, B#booking.id),
|
||||
?assertEqual(FutureId, Event#event.id).
|
||||
|
||||
test_past_pending_confirm_denied() ->
|
||||
OwnerId = create_test_user(user),
|
||||
ParticipantId = create_test_user(user),
|
||||
CalendarId = create_test_calendar(OwnerId, manual),
|
||||
EventId = create_test_event_at(CalendarId, past_start()),
|
||||
{ok, Booking} = core_booking:create(EventId, ParticipantId, pending),
|
||||
{error, expired} = logic_booking:confirm_booking(Booking#booking.id, OwnerId),
|
||||
{ok, Stored} = core_booking:get_by_id(Booking#booking.id),
|
||||
?assertEqual(expired, Stored#booking.status).
|
||||
|
||||
test_future_pending_still_confirmable() ->
|
||||
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, Confirmed} = logic_booking:confirm_booking(Booking#booking.id, OwnerId),
|
||||
?assertEqual(confirmed, Confirmed#booking.status).
|
||||
|
||||
test_process_timeout_expires_past() ->
|
||||
OwnerId = create_test_user(user),
|
||||
ParticipantId = create_test_user(user),
|
||||
CalendarId = create_test_calendar(OwnerId, manual),
|
||||
EventId = create_test_event_at(CalendarId, past_start()),
|
||||
{ok, Booking} = core_booking:create(EventId, ParticipantId, pending),
|
||||
ok = logic_booking:process_timeout_bookings(),
|
||||
{ok, Stored} = core_booking:get_by_id(Booking#booking.id),
|
||||
?assertEqual(expired, Stored#booking.status).
|
||||
|
||||
Reference in New Issue
Block a user