feat(booking): studio journal and instance inbox. Refs EventHub/EventHubFront#60
This commit is contained in:
+12
-1
@@ -1,6 +1,7 @@
|
|||||||
-module(core_event).
|
-module(core_event).
|
||||||
-include("records.hrl").
|
-include("records.hrl").
|
||||||
-export([create/4, create_recurring/5, get_by_id/1, list_by_calendar/1, update/2, delete/1,
|
-export([create/4, create_recurring/5, get_by_id/1, list_by_calendar/1,
|
||||||
|
list_active_including_instances/1, update/2, delete/1,
|
||||||
materialize_occurrence/3]).
|
materialize_occurrence/3]).
|
||||||
-export([count_events/0, count_events_by_date/2]).
|
-export([count_events/0, count_events_by_date/2]).
|
||||||
-export([freeze/2, unfreeze/2]).
|
-export([freeze/2, unfreeze/2]).
|
||||||
@@ -189,6 +190,16 @@ list_by_calendar(CalendarId) ->
|
|||||||
E#event.status =:= active andalso E#event.is_instance =:= false],
|
E#event.status =:= active andalso E#event.is_instance =:= false],
|
||||||
{ok, Events}.
|
{ok, Events}.
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Active events including materialized occurrences (for booking inbox).
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-spec list_active_including_instances(CalendarId :: binary()) -> {ok, [#event{}]}.
|
||||||
|
list_active_including_instances(CalendarId) ->
|
||||||
|
Candidates = mnesia:dirty_index_read(event, CalendarId, #event.calendar_id),
|
||||||
|
Events = [E || E <- Candidates, E#event.status =:= active],
|
||||||
|
{ok, Events}.
|
||||||
|
|
||||||
%%%-------------------------------------------------------------------
|
%%%-------------------------------------------------------------------
|
||||||
%%% @doc Обновить поля события.
|
%%% @doc Обновить поля события.
|
||||||
%%% `Updates` – список пар `[{atom(), term()}]`.
|
%%% `Updates` – список пар `[{atom(), term()}]`.
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ start_http() ->
|
|||||||
{"/v1/user/me", handler_user_me, []},
|
{"/v1/user/me", handler_user_me, []},
|
||||||
{"/v1/user/bookings", handler_user_bookings, []},
|
{"/v1/user/bookings", handler_user_bookings, []},
|
||||||
{"/v1/user/booking-requests", handler_user_booking_requests, []},
|
{"/v1/user/booking-requests", handler_user_booking_requests, []},
|
||||||
|
{"/v1/user/studio-bookings", handler_user_studio_bookings, []},
|
||||||
{"/v1/user/reviews", handler_user_reviews, []},
|
{"/v1/user/reviews", handler_user_reviews, []},
|
||||||
{"/v1/user/following", handler_user_following, []},
|
{"/v1/user/following", handler_user_following, []},
|
||||||
{"/v1/user/specialist-invites", handler_specialist_invites, []},
|
{"/v1/user/specialist-invites", handler_specialist_invites, []},
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
-export([init/2]).
|
-export([init/2]).
|
||||||
-export([trails/0]).
|
-export([trails/0]).
|
||||||
|
-export([item_to_json/1]).
|
||||||
|
|
||||||
-include("records.hrl").
|
-include("records.hrl").
|
||||||
|
|
||||||
@@ -76,7 +77,7 @@ list_requests(Req) ->
|
|||||||
{ok, UserId, Req1} ->
|
{ok, UserId, Req1} ->
|
||||||
case logic_booking:list_user_booking_requests(UserId) of
|
case logic_booking:list_user_booking_requests(UserId) of
|
||||||
{ok, Items} ->
|
{ok, Items} ->
|
||||||
Response = [request_to_json(I) || I <- Items],
|
Response = [item_to_json(I) || I <- Items],
|
||||||
handler_utils:send_json(Req1, 200, Response);
|
handler_utils:send_json(Req1, 200, Response);
|
||||||
{error, _} ->
|
{error, _} ->
|
||||||
handler_utils:send_error(Req1, 500, <<"Internal server error">>)
|
handler_utils:send_error(Req1, 500, <<"Internal server error">>)
|
||||||
@@ -85,8 +86,8 @@ list_requests(Req) ->
|
|||||||
handler_utils:send_error(Req1, Code, Message)
|
handler_utils:send_error(Req1, Code, Message)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
-spec request_to_json({#booking{}, #event{}, owner | specialist}) -> map().
|
-spec item_to_json({#booking{}, #event{}, owner | specialist}) -> map().
|
||||||
request_to_json({Booking, Event, Role}) ->
|
item_to_json({Booking, Event, Role}) ->
|
||||||
CalendarTitle = case core_calendar:get_by_id(Event#event.calendar_id) of
|
CalendarTitle = case core_calendar:get_by_id(Event#event.calendar_id) of
|
||||||
{ok, Cal} -> Cal#calendar.title;
|
{ok, Cal} -> Cal#calendar.title;
|
||||||
_ -> null
|
_ -> null
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Confirmed + pending bookings on calendars the user owns or staffs.
|
||||||
|
%%% GET /v1/user/studio-bookings
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-module(handler_user_studio_bookings).
|
||||||
|
-behaviour(cowboy_handler).
|
||||||
|
|
||||||
|
-export([init/2]).
|
||||||
|
-export([trails/0]).
|
||||||
|
|
||||||
|
-include("records.hrl").
|
||||||
|
|
||||||
|
-spec init(cowboy_req:req(), any()) -> {ok, cowboy_req:req(), any()}.
|
||||||
|
init(Req, Opts) ->
|
||||||
|
handle(Req, Opts).
|
||||||
|
|
||||||
|
-spec trails() -> [map()].
|
||||||
|
trails() ->
|
||||||
|
[
|
||||||
|
#{
|
||||||
|
path => <<"/v1/user/studio-bookings">>,
|
||||||
|
method => <<"GET">>,
|
||||||
|
description => <<"Pending and confirmed bookings on owned/staffed calendars">>,
|
||||||
|
tags => [<<"Bookings">>],
|
||||||
|
responses => #{
|
||||||
|
200 => #{
|
||||||
|
description => <<"Array of enriched studio bookings">>,
|
||||||
|
content => #{<<"application/json">> => #{schema => #{
|
||||||
|
type => array,
|
||||||
|
items => #{type => object}
|
||||||
|
}}}
|
||||||
|
},
|
||||||
|
401 => #{description => <<"Unauthorized">>}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
].
|
||||||
|
|
||||||
|
-spec handle(cowboy_req:req(), any()) -> {ok, cowboy_req:req(), any()}.
|
||||||
|
handle(Req, _Opts) ->
|
||||||
|
case cowboy_req:method(Req) of
|
||||||
|
<<"GET">> -> list_studio(Req);
|
||||||
|
_ -> handler_utils:send_error(Req, 405, <<"Method not allowed">>)
|
||||||
|
end.
|
||||||
|
|
||||||
|
-spec list_studio(cowboy_req:req()) -> {ok, cowboy_req:req(), any()}.
|
||||||
|
list_studio(Req) ->
|
||||||
|
case handler_utils:auth_user(Req) of
|
||||||
|
{ok, UserId, Req1} ->
|
||||||
|
case logic_booking:list_user_studio_bookings(UserId) of
|
||||||
|
{ok, Items} ->
|
||||||
|
Response = [handler_user_booking_requests:item_to_json(I) || I <- Items],
|
||||||
|
handler_utils:send_json(Req1, 200, Response);
|
||||||
|
{error, _} ->
|
||||||
|
handler_utils:send_error(Req1, 500, <<"Internal server error">>)
|
||||||
|
end;
|
||||||
|
{error, Code, Message, Req1} ->
|
||||||
|
handler_utils:send_error(Req1, Code, Message)
|
||||||
|
end.
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
-export([create_booking/2, create_booking/3, confirm_booking/2, confirm_booking/3,
|
-export([create_booking/2, create_booking/3, confirm_booking/2, confirm_booking/3,
|
||||||
cancel_booking/2, cancel_booking/3, get_booking/2,
|
cancel_booking/2, cancel_booking/3, get_booking/2,
|
||||||
list_bookings/2, list_user_bookings/1, list_user_booking_requests/1,
|
list_bookings/2, list_user_bookings/1, list_user_booking_requests/1,
|
||||||
|
list_user_studio_bookings/1,
|
||||||
delete_booking/2,
|
delete_booking/2,
|
||||||
list_bookings_admin/0, get_booking_admin/1,
|
list_bookings_admin/0, get_booking_admin/1,
|
||||||
list_event_bookings/1, list_event_bookings/2,
|
list_event_bookings/1, list_event_bookings/2,
|
||||||
@@ -206,20 +207,31 @@ list_user_bookings(UserId) ->
|
|||||||
-spec list_user_booking_requests(UserId :: binary()) ->
|
-spec list_user_booking_requests(UserId :: binary()) ->
|
||||||
{ok, [{#booking{}, #event{}, owner | specialist}]}.
|
{ok, [{#booking{}, #event{}, owner | specialist}]}.
|
||||||
list_user_booking_requests(UserId) ->
|
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),
|
OwnedEventIds = owned_event_ids(UserId),
|
||||||
SpecEventIds = specialist_event_ids(UserId),
|
SpecEventIds = specialist_event_ids(UserId),
|
||||||
OwnedSet = sets:from_list(OwnedEventIds),
|
OwnedSet = sets:from_list(OwnedEventIds),
|
||||||
SpecOnly = [E || E <- SpecEventIds, not sets:is_element(E, OwnedSet)],
|
SpecOnly = [E || E <- SpecEventIds, not sets:is_element(E, OwnedSet)],
|
||||||
OwnerItems = collect_pending(OwnedEventIds, owner),
|
{OwnedEventIds, SpecOnly}.
|
||||||
SpecItems = collect_pending(SpecOnly, specialist),
|
|
||||||
Items = sort_requests(OwnerItems ++ SpecItems),
|
|
||||||
{ok, Items}.
|
|
||||||
|
|
||||||
owned_event_ids(UserId) ->
|
owned_event_ids(UserId) ->
|
||||||
case core_calendar:list_by_owner(UserId) of
|
case core_calendar:list_by_owner(UserId) of
|
||||||
{ok, Cals} ->
|
{ok, Cals} ->
|
||||||
lists:flatmap(fun(#calendar{id = CalId}) ->
|
lists:flatmap(fun(#calendar{id = CalId}) ->
|
||||||
case core_event:list_by_calendar(CalId) of
|
case core_event:list_active_including_instances(CalId) of
|
||||||
{ok, Events} -> [E#event.id || E <- Events];
|
{ok, Events} -> [E#event.id || E <- Events];
|
||||||
_ -> []
|
_ -> []
|
||||||
end
|
end
|
||||||
@@ -232,7 +244,7 @@ specialist_event_ids(UserId) ->
|
|||||||
Specs = [S || S <- core_calendar_specialist:list_by_user(UserId),
|
Specs = [S || S <- core_calendar_specialist:list_by_user(UserId),
|
||||||
S#calendar_specialist.status =:= active],
|
S#calendar_specialist.status =:= active],
|
||||||
lists:flatmap(fun(#calendar_specialist{calendar_id = CalId}) ->
|
lists:flatmap(fun(#calendar_specialist{calendar_id = CalId}) ->
|
||||||
case core_event:list_by_calendar(CalId) of
|
case core_event:list_active_including_instances(CalId) of
|
||||||
{ok, Events} ->
|
{ok, Events} ->
|
||||||
[E#event.id || E <- Events,
|
[E#event.id || E <- Events,
|
||||||
is_binary(E#event.specialist_id),
|
is_binary(E#event.specialist_id),
|
||||||
@@ -272,6 +284,37 @@ collect_pending(EventIds, Role) ->
|
|||||||
end
|
end
|
||||||
end, EventIds).
|
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) ->
|
sort_requests(Items) ->
|
||||||
lists:sort(fun({A, _, _}, {B, _, _}) ->
|
lists:sort(fun({A, _, _}, {B, _, _}) ->
|
||||||
A#booking.created_at >= B#booking.created_at
|
A#booking.created_at >= B#booking.created_at
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ user() ->
|
|||||||
handler_tickets,
|
handler_tickets,
|
||||||
handler_user_bookings,
|
handler_user_bookings,
|
||||||
handler_user_booking_requests,
|
handler_user_booking_requests,
|
||||||
|
handler_user_studio_bookings,
|
||||||
handler_user_following,
|
handler_user_following,
|
||||||
handler_user_me,
|
handler_user_me,
|
||||||
handler_user_reviews
|
handler_user_reviews
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ logic_booking_test_() ->
|
|||||||
{"List booking requests as owner", fun test_list_booking_requests_owner/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 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},
|
||||||
|
{"Recurring pending in owner inbox via instance", fun test_recurring_pending_in_inbox/0},
|
||||||
|
{"Studio bookings include confirmed after confirm", fun test_studio_bookings_after_confirm/0},
|
||||||
{"Past pending expires on list", fun test_past_pending_expires_on_list/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 excluded from booking requests", fun test_past_pending_excluded_from_requests/0},
|
||||||
{"Past pending confirm denied", fun test_past_pending_confirm_denied/0},
|
{"Past pending confirm denied", fun test_past_pending_confirm_denied/0},
|
||||||
@@ -426,6 +428,33 @@ test_list_booking_requests_stranger() ->
|
|||||||
{ok, _} = logic_booking:create_booking(ParticipantId, EventId),
|
{ok, _} = logic_booking:create_booking(ParticipantId, EventId),
|
||||||
{ok, []} = logic_booking:list_user_booking_requests(StrangerId).
|
{ok, []} = logic_booking:list_user_booking_requests(StrangerId).
|
||||||
|
|
||||||
|
test_recurring_pending_in_inbox() ->
|
||||||
|
OwnerId = create_test_user(user),
|
||||||
|
ParticipantId = create_test_user(user),
|
||||||
|
CalendarId = create_test_calendar(OwnerId, manual),
|
||||||
|
{MasterId, StartTime} = create_recurring_event(CalendarId),
|
||||||
|
Occ = add_days(StartTime, 7),
|
||||||
|
{ok, Booking} = logic_booking:create_booking(ParticipantId, MasterId, Occ),
|
||||||
|
{ok, Items} = logic_booking:list_user_booking_requests(OwnerId),
|
||||||
|
?assertEqual(1, length(Items)),
|
||||||
|
[{B, Event, owner}] = Items,
|
||||||
|
?assertEqual(Booking#booking.id, B#booking.id),
|
||||||
|
?assertEqual(true, Event#event.is_instance),
|
||||||
|
?assertEqual(Occ, Event#event.start_time).
|
||||||
|
|
||||||
|
test_studio_bookings_after_confirm() ->
|
||||||
|
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, _} = logic_booking:confirm_booking(OwnerId, Booking#booking.id, confirm),
|
||||||
|
{ok, []} = logic_booking:list_user_booking_requests(OwnerId),
|
||||||
|
{ok, Studio} = logic_booking:list_user_studio_bookings(OwnerId),
|
||||||
|
?assertEqual(1, length(Studio)),
|
||||||
|
[{B, _Event, owner}] = Studio,
|
||||||
|
?assertEqual(confirmed, B#booking.status).
|
||||||
|
|
||||||
test_past_pending_expires_on_list() ->
|
test_past_pending_expires_on_list() ->
|
||||||
OwnerId = create_test_user(user),
|
OwnerId = create_test_user(user),
|
||||||
ParticipantId = create_test_user(user),
|
ParticipantId = create_test_user(user),
|
||||||
|
|||||||
Reference in New Issue
Block a user