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").
|
||||
|
||||
-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(),
|
||||
Specs :: [binary()]) ->
|
||||
@@ -51,6 +51,10 @@ get_by_calendar_and_user(CalendarId, UserId) ->
|
||||
list_by_calendar(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()}]) ->
|
||||
{ok, #calendar_specialist{}} | {error, not_found | term()}.
|
||||
update(CalendarId, UserId, Updates) ->
|
||||
|
||||
@@ -91,6 +91,7 @@ start_http() ->
|
||||
{"/v1/refresh", handler_refresh, []},
|
||||
{"/v1/user/me", handler_user_me, []},
|
||||
{"/v1/user/bookings", handler_user_bookings, []},
|
||||
{"/v1/user/booking-requests", handler_user_booking_requests, []},
|
||||
{"/v1/user/reviews", handler_user_reviews, []},
|
||||
{"/v1/user/following", handler_user_following, []},
|
||||
{"/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").
|
||||
-export([create_booking/2, confirm_booking/2, confirm_booking/3,
|
||||
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_event_bookings/1, list_event_bookings/2,
|
||||
process_timeout_bookings/0, cancel_pending_for_owner/1,
|
||||
@@ -165,6 +166,71 @@ list_bookings(EventId, UserId) ->
|
||||
list_user_bookings(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()) ->
|
||||
ok | {error, not_found | access_denied}.
|
||||
delete_booking(BookingId, UserId) ->
|
||||
|
||||
@@ -325,10 +325,12 @@ format_event(Event) ->
|
||||
#location{address = Addr, lat = Lat, lon = Lon} ->
|
||||
#{address => Addr, lat => Lat, lon => Lon}
|
||||
end,
|
||||
CalendarTitle = case core_calendar:get_by_id(Event#event.calendar_id) of
|
||||
{ok, Cal} -> Cal#calendar.title;
|
||||
_ -> null
|
||||
end,
|
||||
{CalendarTitle, ImageUrl} = case core_calendar:get_by_id(Event#event.calendar_id) of
|
||||
{ok, #calendar{title = T, image_url = <<>>}} -> {T, null};
|
||||
{ok, #calendar{title = T, image_url = undefined}} -> {T, null};
|
||||
{ok, #calendar{title = T, image_url = Url}} -> {T, Url};
|
||||
_ -> {null, null}
|
||||
end,
|
||||
#{
|
||||
id => Event#event.id,
|
||||
calendar_id => Event#event.calendar_id,
|
||||
@@ -343,7 +345,8 @@ format_event(Event) ->
|
||||
capacity => Event#event.capacity,
|
||||
rating_avg => Event#event.rating_avg,
|
||||
rating_count => Event#event.rating_count,
|
||||
status => Event#event.status
|
||||
status => Event#event.status,
|
||||
image_url => ImageUrl
|
||||
}.
|
||||
|
||||
-spec format_calendars([#calendar{}]) -> [map()].
|
||||
@@ -359,6 +362,11 @@ format_calendar(Calendar) ->
|
||||
description => Calendar#calendar.description,
|
||||
type => Calendar#calendar.type,
|
||||
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,
|
||||
rating_avg => Calendar#calendar.rating_avg,
|
||||
rating_count => Calendar#calendar.rating_count,
|
||||
|
||||
@@ -94,6 +94,7 @@ user() ->
|
||||
handler_ticket_by_id,
|
||||
handler_tickets,
|
||||
handler_user_bookings,
|
||||
handler_user_booking_requests,
|
||||
handler_user_following,
|
||||
handler_user_me,
|
||||
handler_user_reviews
|
||||
|
||||
@@ -39,7 +39,10 @@ logic_booking_test_() ->
|
||||
{"List event bookings", fun test_list_event_bookings/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 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),
|
||||
?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},
|
||||
{"Combined search", fun test_combined_search/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},
|
||||
{"Pagination", fun test_pagination/0},
|
||||
{"Sorting", fun test_sorting/0},
|
||||
@@ -175,6 +176,14 @@ test_search_calendars() ->
|
||||
{Total2, _} = calendars_from(logic_search:search(<<"calendar">>, <<"Calendar">>, OwnerId, #{})),
|
||||
?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() ->
|
||||
OwnerId = create_test_user(user),
|
||||
CalendarId = create_test_calendar(OwnerId, personal, []),
|
||||
|
||||
Reference in New Issue
Block a user