feat: GET /v1/user/booking-requests + search image_url. Fixes EventHub/EventHubBack#59
This commit is contained in:
@@ -6,7 +6,7 @@
|
|||||||
-include("records.hrl").
|
-include("records.hrl").
|
||||||
|
|
||||||
-export([create/4, get_by_calendar_and_user/2, list_by_calendar/1,
|
-export([create/4, get_by_calendar_and_user/2, list_by_calendar/1,
|
||||||
update/3, delete/2, is_active_specialist/2]).
|
list_by_user/1, update/3, delete/2, is_active_specialist/2]).
|
||||||
|
|
||||||
-spec create(CalendarId :: binary(), UserId :: binary(), Name :: binary(),
|
-spec create(CalendarId :: binary(), UserId :: binary(), Name :: binary(),
|
||||||
Specs :: [binary()]) ->
|
Specs :: [binary()]) ->
|
||||||
@@ -51,6 +51,10 @@ get_by_calendar_and_user(CalendarId, UserId) ->
|
|||||||
list_by_calendar(CalendarId) ->
|
list_by_calendar(CalendarId) ->
|
||||||
mnesia:dirty_match_object(#calendar_specialist{calendar_id = CalendarId, _ = '_'}).
|
mnesia:dirty_match_object(#calendar_specialist{calendar_id = CalendarId, _ = '_'}).
|
||||||
|
|
||||||
|
-spec list_by_user(UserId :: binary()) -> [#calendar_specialist{}].
|
||||||
|
list_by_user(UserId) ->
|
||||||
|
mnesia:dirty_match_object(#calendar_specialist{user_id = UserId, _ = '_'}).
|
||||||
|
|
||||||
-spec update(CalendarId :: binary(), UserId :: binary(), Updates :: [{atom(), term()}]) ->
|
-spec update(CalendarId :: binary(), UserId :: binary(), Updates :: [{atom(), term()}]) ->
|
||||||
{ok, #calendar_specialist{}} | {error, not_found | term()}.
|
{ok, #calendar_specialist{}} | {error, not_found | term()}.
|
||||||
update(CalendarId, UserId, Updates) ->
|
update(CalendarId, UserId, Updates) ->
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ start_http() ->
|
|||||||
{"/v1/refresh", handler_refresh, []},
|
{"/v1/refresh", handler_refresh, []},
|
||||||
{"/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/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, []},
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @doc Pending booking requests for the current user (owner / specialist inbox).
|
||||||
|
%%% GET /v1/user/booking-requests
|
||||||
|
%%% @end
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
-module(handler_user_booking_requests).
|
||||||
|
-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/booking-requests">>,
|
||||||
|
method => <<"GET">>,
|
||||||
|
description => <<"List pending booking requests the user can confirm/decline">>,
|
||||||
|
tags => [<<"Bookings">>],
|
||||||
|
responses => #{
|
||||||
|
200 => #{
|
||||||
|
description => <<"Array of enriched booking requests">>,
|
||||||
|
content => #{<<"application/json">> => #{schema => #{
|
||||||
|
type => array,
|
||||||
|
items => request_schema()
|
||||||
|
}}}
|
||||||
|
},
|
||||||
|
401 => #{description => <<"Unauthorized">>}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
].
|
||||||
|
|
||||||
|
request_schema() ->
|
||||||
|
#{
|
||||||
|
type => object,
|
||||||
|
properties => #{
|
||||||
|
id => #{type => string},
|
||||||
|
event_id => #{type => string},
|
||||||
|
user_id => #{type => string},
|
||||||
|
status => #{type => string, enum => [<<"pending">>]},
|
||||||
|
role => #{type => string, enum => [<<"owner">>, <<"specialist">>]},
|
||||||
|
user_nickname => #{type => string, nullable => true},
|
||||||
|
user_email => #{type => string, nullable => true},
|
||||||
|
created_at => #{type => string, format => <<"date-time">>},
|
||||||
|
event => #{
|
||||||
|
type => object,
|
||||||
|
properties => #{
|
||||||
|
id => #{type => string},
|
||||||
|
calendar_id => #{type => string},
|
||||||
|
calendar_title => #{type => string, nullable => true},
|
||||||
|
title => #{type => string},
|
||||||
|
start_time => #{type => string, format => <<"date-time">>},
|
||||||
|
duration => #{type => integer},
|
||||||
|
specialist_id => #{type => string, nullable => true}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.
|
||||||
|
|
||||||
|
-spec handle(cowboy_req:req(), any()) -> {ok, cowboy_req:req(), any()}.
|
||||||
|
handle(Req, _Opts) ->
|
||||||
|
case cowboy_req:method(Req) of
|
||||||
|
<<"GET">> -> list_requests(Req);
|
||||||
|
_ -> handler_utils:send_error(Req, 405, <<"Method not allowed">>)
|
||||||
|
end.
|
||||||
|
|
||||||
|
-spec list_requests(cowboy_req:req()) -> {ok, cowboy_req:req(), any()}.
|
||||||
|
list_requests(Req) ->
|
||||||
|
case handler_utils:auth_user(Req) of
|
||||||
|
{ok, UserId, Req1} ->
|
||||||
|
case logic_booking:list_user_booking_requests(UserId) of
|
||||||
|
{ok, Items} ->
|
||||||
|
Response = [request_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.
|
||||||
|
|
||||||
|
-spec request_to_json({#booking{}, #event{}, owner | specialist}) -> map().
|
||||||
|
request_to_json({Booking, Event, Role}) ->
|
||||||
|
CalendarTitle = case core_calendar:get_by_id(Event#event.calendar_id) of
|
||||||
|
{ok, Cal} -> Cal#calendar.title;
|
||||||
|
_ -> null
|
||||||
|
end,
|
||||||
|
SpecialistId = case Event#event.specialist_id of
|
||||||
|
<<>> -> null;
|
||||||
|
undefined -> null;
|
||||||
|
Sid -> Sid
|
||||||
|
end,
|
||||||
|
Base = handler_utils:booking_to_json(Booking),
|
||||||
|
EventMap = #{
|
||||||
|
id => Event#event.id,
|
||||||
|
calendar_id => Event#event.calendar_id,
|
||||||
|
calendar_title => CalendarTitle,
|
||||||
|
title => Event#event.title,
|
||||||
|
start_time => handler_utils:datetime_to_iso8601(Event#event.start_time),
|
||||||
|
duration => Event#event.duration,
|
||||||
|
specialist_id => SpecialistId
|
||||||
|
},
|
||||||
|
Base#{
|
||||||
|
role => Role,
|
||||||
|
event => EventMap
|
||||||
|
}.
|
||||||
@@ -2,7 +2,8 @@
|
|||||||
-include("records.hrl").
|
-include("records.hrl").
|
||||||
-export([create_booking/2, confirm_booking/2, confirm_booking/3,
|
-export([create_booking/2, 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, delete_booking/2,
|
list_bookings/2, list_user_bookings/1, list_user_booking_requests/1,
|
||||||
|
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,
|
||||||
process_timeout_bookings/0, cancel_pending_for_owner/1,
|
process_timeout_bookings/0, cancel_pending_for_owner/1,
|
||||||
@@ -165,6 +166,71 @@ list_bookings(EventId, UserId) ->
|
|||||||
list_user_bookings(UserId) ->
|
list_user_bookings(UserId) ->
|
||||||
core_booking:list_by_user(UserId).
|
core_booking:list_by_user(UserId).
|
||||||
|
|
||||||
|
%%%-------------------------------------------------------------------
|
||||||
|
%%% @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 = owned_event_ids(UserId),
|
||||||
|
SpecEventIds = specialist_event_ids(UserId),
|
||||||
|
OwnedSet = sets:from_list(OwnedEventIds),
|
||||||
|
SpecOnly = [E || E <- SpecEventIds, not sets:is_element(E, OwnedSet)],
|
||||||
|
OwnerItems = collect_pending(OwnedEventIds, owner),
|
||||||
|
SpecItems = collect_pending(SpecOnly, specialist),
|
||||||
|
Items = sort_requests(OwnerItems ++ SpecItems),
|
||||||
|
{ok, Items}.
|
||||||
|
|
||||||
|
owned_event_ids(UserId) ->
|
||||||
|
case core_calendar:list_by_owner(UserId) of
|
||||||
|
{ok, Cals} ->
|
||||||
|
lists:flatmap(fun(#calendar{id = CalId}) ->
|
||||||
|
case core_event:list_by_calendar(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_by_calendar(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) ->
|
||||||
|
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];
|
||||||
|
_ ->
|
||||||
|
[]
|
||||||
|
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()) ->
|
-spec delete_booking(BookingId :: binary(), UserId :: binary()) ->
|
||||||
ok | {error, not_found | access_denied}.
|
ok | {error, not_found | access_denied}.
|
||||||
delete_booking(BookingId, UserId) ->
|
delete_booking(BookingId, UserId) ->
|
||||||
|
|||||||
@@ -325,10 +325,12 @@ format_event(Event) ->
|
|||||||
#location{address = Addr, lat = Lat, lon = Lon} ->
|
#location{address = Addr, lat = Lat, lon = Lon} ->
|
||||||
#{address => Addr, lat => Lat, lon => Lon}
|
#{address => Addr, lat => Lat, lon => Lon}
|
||||||
end,
|
end,
|
||||||
CalendarTitle = case core_calendar:get_by_id(Event#event.calendar_id) of
|
{CalendarTitle, ImageUrl} = case core_calendar:get_by_id(Event#event.calendar_id) of
|
||||||
{ok, Cal} -> Cal#calendar.title;
|
{ok, #calendar{title = T, image_url = <<>>}} -> {T, null};
|
||||||
_ -> null
|
{ok, #calendar{title = T, image_url = undefined}} -> {T, null};
|
||||||
end,
|
{ok, #calendar{title = T, image_url = Url}} -> {T, Url};
|
||||||
|
_ -> {null, null}
|
||||||
|
end,
|
||||||
#{
|
#{
|
||||||
id => Event#event.id,
|
id => Event#event.id,
|
||||||
calendar_id => Event#event.calendar_id,
|
calendar_id => Event#event.calendar_id,
|
||||||
@@ -343,7 +345,8 @@ format_event(Event) ->
|
|||||||
capacity => Event#event.capacity,
|
capacity => Event#event.capacity,
|
||||||
rating_avg => Event#event.rating_avg,
|
rating_avg => Event#event.rating_avg,
|
||||||
rating_count => Event#event.rating_count,
|
rating_count => Event#event.rating_count,
|
||||||
status => Event#event.status
|
status => Event#event.status,
|
||||||
|
image_url => ImageUrl
|
||||||
}.
|
}.
|
||||||
|
|
||||||
-spec format_calendars([#calendar{}]) -> [map()].
|
-spec format_calendars([#calendar{}]) -> [map()].
|
||||||
@@ -359,6 +362,11 @@ format_calendar(Calendar) ->
|
|||||||
description => Calendar#calendar.description,
|
description => Calendar#calendar.description,
|
||||||
type => Calendar#calendar.type,
|
type => Calendar#calendar.type,
|
||||||
booking_open => logic_calendar:booking_open(Calendar),
|
booking_open => logic_calendar:booking_open(Calendar),
|
||||||
|
image_url => case Calendar#calendar.image_url of
|
||||||
|
<<>> -> null;
|
||||||
|
undefined -> null;
|
||||||
|
Url -> Url
|
||||||
|
end,
|
||||||
tags => Calendar#calendar.tags,
|
tags => Calendar#calendar.tags,
|
||||||
rating_avg => Calendar#calendar.rating_avg,
|
rating_avg => Calendar#calendar.rating_avg,
|
||||||
rating_count => Calendar#calendar.rating_count,
|
rating_count => Calendar#calendar.rating_count,
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ user() ->
|
|||||||
handler_ticket_by_id,
|
handler_ticket_by_id,
|
||||||
handler_tickets,
|
handler_tickets,
|
||||||
handler_user_bookings,
|
handler_user_bookings,
|
||||||
|
handler_user_booking_requests,
|
||||||
handler_user_following,
|
handler_user_following,
|
||||||
handler_user_me,
|
handler_user_me,
|
||||||
handler_user_reviews
|
handler_user_reviews
|
||||||
|
|||||||
@@ -39,7 +39,10 @@ logic_booking_test_() ->
|
|||||||
{"List event bookings", fun test_list_event_bookings/0},
|
{"List event bookings", fun test_list_event_bookings/0},
|
||||||
{"List event bookings as owner", fun test_list_event_bookings_owner/0},
|
{"List event bookings as owner", fun test_list_event_bookings_owner/0},
|
||||||
{"List event bookings non-owner denied", fun test_list_event_bookings_non_owner/0},
|
{"List event bookings non-owner denied", fun test_list_event_bookings_non_owner/0},
|
||||||
{"List user bookings", fun test_list_user_bookings/0}
|
{"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}
|
||||||
]}.
|
]}.
|
||||||
|
|
||||||
%% Вспомогательные функции
|
%% Вспомогательные функции
|
||||||
@@ -301,3 +304,54 @@ test_list_user_bookings() ->
|
|||||||
|
|
||||||
{ok, Bookings} = logic_booking:list_user_bookings(ParticipantId),
|
{ok, Bookings} = logic_booking:list_user_bookings(ParticipantId),
|
||||||
?assertEqual(2, length(Bookings)).
|
?assertEqual(2, length(Bookings)).
|
||||||
|
|
||||||
|
test_list_booking_requests_owner() ->
|
||||||
|
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, Items} = logic_booking:list_user_booking_requests(OwnerId),
|
||||||
|
?assertEqual(1, length(Items)),
|
||||||
|
[{B, Event, Role}] = Items,
|
||||||
|
?assertEqual(Booking#booking.id, B#booking.id),
|
||||||
|
?assertEqual(EventId, Event#event.id),
|
||||||
|
?assertEqual(owner, Role),
|
||||||
|
|
||||||
|
%% participant inbox is separate — no manage requests for own booking
|
||||||
|
{ok, []} = logic_booking:list_user_booking_requests(ParticipantId).
|
||||||
|
|
||||||
|
test_list_booking_requests_specialist() ->
|
||||||
|
OwnerId = create_test_user(user),
|
||||||
|
SpecId = create_test_user(user),
|
||||||
|
ParticipantId = create_test_user(user),
|
||||||
|
CalendarId = create_test_calendar(OwnerId, manual),
|
||||||
|
{ok, _} = core_calendar_specialist:create(CalendarId, SpecId, <<"Spec">>, []),
|
||||||
|
SpecEventId = create_test_event(CalendarId),
|
||||||
|
OtherEventId = create_test_event(CalendarId),
|
||||||
|
{ok, _} = core_event:update(SpecEventId, [{specialist_id, SpecId}]),
|
||||||
|
|
||||||
|
{ok, SpecBooking} = logic_booking:create_booking(ParticipantId, SpecEventId),
|
||||||
|
OtherParticipant = create_test_user(user),
|
||||||
|
{ok, _} = logic_booking:create_booking(OtherParticipant, OtherEventId),
|
||||||
|
|
||||||
|
{ok, SpecItems} = logic_booking:list_user_booking_requests(SpecId),
|
||||||
|
?assertEqual(1, length(SpecItems)),
|
||||||
|
[{B, Event, Role}] = SpecItems,
|
||||||
|
?assertEqual(SpecBooking#booking.id, B#booking.id),
|
||||||
|
?assertEqual(SpecEventId, Event#event.id),
|
||||||
|
?assertEqual(specialist, Role),
|
||||||
|
|
||||||
|
%% owner sees both pending
|
||||||
|
{ok, OwnerItems} = logic_booking:list_user_booking_requests(OwnerId),
|
||||||
|
?assertEqual(2, length(OwnerItems)).
|
||||||
|
|
||||||
|
test_list_booking_requests_stranger() ->
|
||||||
|
OwnerId = create_test_user(user),
|
||||||
|
ParticipantId = create_test_user(user),
|
||||||
|
StrangerId = create_test_user(user),
|
||||||
|
CalendarId = create_test_calendar(OwnerId, manual),
|
||||||
|
EventId = create_test_event(CalendarId),
|
||||||
|
{ok, _} = logic_booking:create_booking(ParticipantId, EventId),
|
||||||
|
{ok, []} = logic_booking:list_user_booking_requests(StrangerId).
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ logic_search_test_() ->
|
|||||||
{"Search events by location", fun test_search_events_by_location/0},
|
{"Search events by location", fun test_search_events_by_location/0},
|
||||||
{"Combined search", fun test_combined_search/0},
|
{"Combined search", fun test_combined_search/0},
|
||||||
{"Search calendars", fun test_search_calendars/0},
|
{"Search calendars", fun test_search_calendars/0},
|
||||||
|
{"Search calendars include image_url", fun test_search_calendars_image_url/0},
|
||||||
{"Search all", fun test_search_all/0},
|
{"Search all", fun test_search_all/0},
|
||||||
{"Pagination", fun test_pagination/0},
|
{"Pagination", fun test_pagination/0},
|
||||||
{"Sorting", fun test_sorting/0},
|
{"Sorting", fun test_sorting/0},
|
||||||
@@ -175,6 +176,14 @@ test_search_calendars() ->
|
|||||||
{Total2, _} = calendars_from(logic_search:search(<<"calendar">>, <<"Calendar">>, OwnerId, #{})),
|
{Total2, _} = calendars_from(logic_search:search(<<"calendar">>, <<"Calendar">>, OwnerId, #{})),
|
||||||
?assertEqual(2, Total2).
|
?assertEqual(2, Total2).
|
||||||
|
|
||||||
|
test_search_calendars_image_url() ->
|
||||||
|
OwnerId = create_test_user(user),
|
||||||
|
CalendarId = create_test_calendar(OwnerId, commercial, [<<"cover">>]),
|
||||||
|
{ok, _} = core_calendar:update(CalendarId, [{image_url, <<"https://cdn.example/cal.jpg">>}]),
|
||||||
|
{_, Cals} = calendars_from(logic_search:search(<<"calendar">>, <<"Calendar">>, OwnerId, #{})),
|
||||||
|
[Hit | _] = [C || C <- Cals, maps:get(id, C) =:= CalendarId],
|
||||||
|
?assertEqual(<<"https://cdn.example/cal.jpg">>, maps:get(image_url, Hit)).
|
||||||
|
|
||||||
test_search_all() ->
|
test_search_all() ->
|
||||||
OwnerId = create_test_user(user),
|
OwnerId = create_test_user(user),
|
||||||
CalendarId = create_test_calendar(OwnerId, personal, []),
|
CalendarId = create_test_calendar(OwnerId, personal, []),
|
||||||
|
|||||||
Reference in New Issue
Block a user