Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2567585333 | |||
| 520af23cc6 | |||
| 8a5f254c2a |
+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, []},
|
||||||
|
|||||||
@@ -45,12 +45,29 @@ trails() ->
|
|||||||
schema => #{type => string}
|
schema => #{type => string}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
requestBody => #{
|
||||||
|
required => false,
|
||||||
|
content => #{
|
||||||
|
<<"application/json">> => #{
|
||||||
|
schema => #{
|
||||||
|
type => object,
|
||||||
|
properties => #{
|
||||||
|
occurrence_start => #{
|
||||||
|
type => string,
|
||||||
|
format => <<"date-time">>,
|
||||||
|
description => <<"Required for recurring masters">>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
responses => #{
|
responses => #{
|
||||||
201 => #{
|
201 => #{
|
||||||
description => <<"Booking created">>,
|
description => <<"Booking created">>,
|
||||||
content => #{<<"application/json">> => #{schema => BookingSchema}}
|
content => #{<<"application/json">> => #{schema => BookingSchema}}
|
||||||
},
|
},
|
||||||
400 => #{description => <<"Event is full or not active">>},
|
400 => #{description => <<"Event is full, not active, or occurrence_start missing/invalid">>},
|
||||||
403 => #{description => <<"Access denied">>},
|
403 => #{description => <<"Access denied">>},
|
||||||
404 => #{description => <<"Event not found">>},
|
404 => #{description => <<"Event not found">>},
|
||||||
409 => #{description => <<"Already booked">>}
|
409 => #{description => <<"Already booked">>}
|
||||||
@@ -111,29 +128,39 @@ create_booking(Req) ->
|
|||||||
case handler_utils:auth_user(Req) of
|
case handler_utils:auth_user(Req) of
|
||||||
{ok, UserId, Req1} ->
|
{ok, UserId, Req1} ->
|
||||||
EventId = cowboy_req:binding(id, Req1),
|
EventId = cowboy_req:binding(id, Req1),
|
||||||
case logic_booking:create_booking(UserId, EventId) of
|
{ok, Body, Req2} = cowboy_req:read_body(Req1),
|
||||||
{ok, Booking} ->
|
case parse_occurrence_start(Body) of
|
||||||
handler_utils:send_json(Req1, 201, booking_to_json(Booking));
|
{error, invalid_occurrence} ->
|
||||||
{error, already_booked} ->
|
handler_utils:send_error(Req2, 400, <<"Invalid occurrence_start">>);
|
||||||
handler_utils:send_error(Req1, 409, <<"Already booked">>);
|
{ok, OccurrenceStart} ->
|
||||||
{error, full} ->
|
case logic_booking:create_booking(UserId, EventId, OccurrenceStart) of
|
||||||
handler_utils:send_error(Req1, 400, <<"Event is full">>);
|
{ok, Booking} ->
|
||||||
{error, event_full} ->
|
handler_utils:send_json(Req2, 201, booking_to_json(Booking));
|
||||||
handler_utils:send_error(Req1, 400, <<"Event is full">>);
|
{error, already_booked} ->
|
||||||
{error, event_not_active} ->
|
handler_utils:send_error(Req2, 409, <<"Already booked">>);
|
||||||
handler_utils:send_error(Req1, 400, <<"Event is not active">>);
|
{error, full} ->
|
||||||
{error, personal_calendar} ->
|
handler_utils:send_error(Req2, 400, <<"Event is full">>);
|
||||||
handler_utils:send_error(Req1, 403, <<"personal_calendar">>);
|
{error, event_full} ->
|
||||||
{error, subscription_inactive} ->
|
handler_utils:send_error(Req2, 400, <<"Event is full">>);
|
||||||
handler_utils:send_error(Req1, 403, <<"subscription_inactive">>);
|
{error, event_not_active} ->
|
||||||
{error, own_event} ->
|
handler_utils:send_error(Req2, 400, <<"Event is not active">>);
|
||||||
handler_utils:send_error(Req1, 403, <<"Cannot book own event">>);
|
{error, occurrence_start_required} ->
|
||||||
{error, access_denied} ->
|
handler_utils:send_error(Req2, 400, <<"occurrence_start required">>);
|
||||||
handler_utils:send_error(Req1, 403, <<"Access denied">>);
|
{error, invalid_occurrence} ->
|
||||||
{error, not_found} ->
|
handler_utils:send_error(Req2, 400, <<"Invalid occurrence_start">>);
|
||||||
handler_utils:send_error(Req1, 404, <<"Event not found">>);
|
{error, personal_calendar} ->
|
||||||
{error, _} ->
|
handler_utils:send_error(Req2, 403, <<"personal_calendar">>);
|
||||||
handler_utils:send_error(Req1, 500, <<"Internal server error">>)
|
{error, subscription_inactive} ->
|
||||||
|
handler_utils:send_error(Req2, 403, <<"subscription_inactive">>);
|
||||||
|
{error, own_event} ->
|
||||||
|
handler_utils:send_error(Req2, 403, <<"Cannot book own event">>);
|
||||||
|
{error, access_denied} ->
|
||||||
|
handler_utils:send_error(Req2, 403, <<"Access denied">>);
|
||||||
|
{error, not_found} ->
|
||||||
|
handler_utils:send_error(Req2, 404, <<"Event not found">>);
|
||||||
|
{error, _} ->
|
||||||
|
handler_utils:send_error(Req2, 500, <<"Internal server error">>)
|
||||||
|
end
|
||||||
end;
|
end;
|
||||||
{error, Code, Message, Req1} ->
|
{error, Code, Message, Req1} ->
|
||||||
handler_utils:send_error(Req1, Code, Message)
|
handler_utils:send_error(Req1, Code, Message)
|
||||||
@@ -167,4 +194,28 @@ list_bookings(Req) ->
|
|||||||
%% @private Формирует JSON-представление записи #booking{}.
|
%% @private Формирует JSON-представление записи #booking{}.
|
||||||
-spec booking_to_json(#booking{}) -> map().
|
-spec booking_to_json(#booking{}) -> map().
|
||||||
booking_to_json(Booking) ->
|
booking_to_json(Booking) ->
|
||||||
handler_utils:booking_to_json(Booking).
|
handler_utils:booking_to_json(Booking).
|
||||||
|
|
||||||
|
-spec parse_occurrence_start(binary()) ->
|
||||||
|
{ok, calendar:datetime() | undefined} | {error, invalid_occurrence}.
|
||||||
|
parse_occurrence_start(<<>>) ->
|
||||||
|
{ok, undefined};
|
||||||
|
parse_occurrence_start(Body) ->
|
||||||
|
try jsx:decode(Body, [return_maps]) of
|
||||||
|
Map when is_map(Map) ->
|
||||||
|
case maps:get(<<"occurrence_start">>, Map, undefined) of
|
||||||
|
undefined ->
|
||||||
|
{ok, undefined};
|
||||||
|
Iso when is_binary(Iso) ->
|
||||||
|
case handler_utils:parse_datetime(Iso) of
|
||||||
|
{ok, Dt} -> {ok, Dt};
|
||||||
|
{error, _} -> {error, invalid_occurrence}
|
||||||
|
end;
|
||||||
|
_ ->
|
||||||
|
{error, invalid_occurrence}
|
||||||
|
end;
|
||||||
|
_ ->
|
||||||
|
{ok, undefined}
|
||||||
|
catch
|
||||||
|
_:_ -> {error, invalid_occurrence}
|
||||||
|
end.
|
||||||
@@ -115,13 +115,13 @@ calendar_update_schema() ->
|
|||||||
%%% Internal functions
|
%%% Internal functions
|
||||||
|
|
||||||
get_calendar(Req) ->
|
get_calendar(Req) ->
|
||||||
case handler_utils:auth_user(Req) of
|
case handler_utils:auth_user_optional(Req) of
|
||||||
{ok, UserId, Req1} ->
|
{ok, UserId, Req1} ->
|
||||||
CalendarId = cowboy_req:binding(id, Req1),
|
CalendarId = cowboy_req:binding(id, Req1),
|
||||||
case logic_calendar:get_calendar(UserId, CalendarId) of
|
case logic_calendar:get_calendar(UserId, CalendarId) of
|
||||||
{ok, Calendar} ->
|
{ok, Calendar} ->
|
||||||
Json0 = handler_utils:calendar_to_json(Calendar),
|
Json0 = handler_utils:calendar_to_json(Calendar),
|
||||||
Following = logic_calendar_follow:is_following(UserId, CalendarId),
|
Following = UserId =/= <<>> andalso logic_calendar_follow:is_following(UserId, CalendarId),
|
||||||
handler_utils:send_json(Req1, 200, Json0#{following => Following});
|
handler_utils:send_json(Req1, 200, Json0#{following => Following});
|
||||||
{error, access_denied} ->
|
{error, access_denied} ->
|
||||||
handler_utils:send_error(Req1, 403, <<"Access denied">>);
|
handler_utils:send_error(Req1, 403, <<"Access denied">>);
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ has_user_binding(Req) ->
|
|||||||
cowboy_req:binding(user_id, Req) =/= undefined.
|
cowboy_req:binding(user_id, Req) =/= undefined.
|
||||||
|
|
||||||
list_specialists(Req) ->
|
list_specialists(Req) ->
|
||||||
case handler_utils:auth_user(Req) of
|
case handler_utils:auth_user_optional(Req) of
|
||||||
{ok, UserId, Req1} ->
|
{ok, UserId, Req1} ->
|
||||||
CalendarId = cowboy_req:binding(id, Req1),
|
CalendarId = cowboy_req:binding(id, Req1),
|
||||||
case logic_calendar_specialist:list(UserId, CalendarId) of
|
case logic_calendar_specialist:list(UserId, CalendarId) of
|
||||||
|
|||||||
@@ -225,7 +225,7 @@ create_event(Req) ->
|
|||||||
%% @doc GET /v1/calendars/:calendar_id/events — список событий.
|
%% @doc GET /v1/calendars/:calendar_id/events — список событий.
|
||||||
-spec list_events(cowboy_req:req()) -> {ok, binary(), cowboy_req:req()}.
|
-spec list_events(cowboy_req:req()) -> {ok, binary(), cowboy_req:req()}.
|
||||||
list_events(Req) ->
|
list_events(Req) ->
|
||||||
case handler_utils:auth_user(Req) of
|
case handler_utils:auth_user_optional(Req) of
|
||||||
{ok, UserId, Req1} ->
|
{ok, UserId, Req1} ->
|
||||||
CalendarId = cowboy_req:binding(calendar_id, Req1),
|
CalendarId = cowboy_req:binding(calendar_id, Req1),
|
||||||
Qs = cowboy_req:parse_qs(Req1),
|
Qs = cowboy_req:parse_qs(Req1),
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ handle(Req, _Opts) ->
|
|||||||
%% @doc GET /v1/search — полнотекстовый поиск с фильтрами.
|
%% @doc GET /v1/search — полнотекстовый поиск с фильтрами.
|
||||||
-spec search(cowboy_req:req()) -> {ok, binary(), cowboy_req:req()}.
|
-spec search(cowboy_req:req()) -> {ok, binary(), cowboy_req:req()}.
|
||||||
search(Req) ->
|
search(Req) ->
|
||||||
case handler_utils:auth_user(Req) of
|
case handler_utils:auth_user_optional(Req) of
|
||||||
{ok, UserId, Req1} ->
|
{ok, UserId, Req1} ->
|
||||||
Qs = cowboy_req:parse_qs(Req1),
|
Qs = cowboy_req:parse_qs(Req1),
|
||||||
Type = proplists:get_value(<<"type">>, Qs, undefined),
|
Type = proplists:get_value(<<"type">>, Qs, undefined),
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
-export([
|
-export([
|
||||||
auth_admin/1,
|
auth_admin/1,
|
||||||
auth_user/1,
|
auth_user/1,
|
||||||
|
auth_user_optional/1,
|
||||||
send_json/3,
|
send_json/3,
|
||||||
send_json/4,
|
send_json/4,
|
||||||
send_error/3,
|
send_error/3,
|
||||||
@@ -75,6 +76,17 @@ is_superadmin(Req) ->
|
|||||||
auth_user(Req) ->
|
auth_user(Req) ->
|
||||||
handler_auth:authenticate(Req).
|
handler_auth:authenticate(Req).
|
||||||
|
|
||||||
|
%% @doc Как auth_user/1, но без заголовка Authorization — гость (`<<>>`).
|
||||||
|
%% Невалидный Bearer по-прежнему 401.
|
||||||
|
-spec auth_user_optional(cowboy_req:req()) ->
|
||||||
|
{ok, binary(), cowboy_req:req()} | {error, integer(), binary(), cowboy_req:req()}.
|
||||||
|
auth_user_optional(Req) ->
|
||||||
|
case cowboy_req:header(<<"authorization">>, Req) of
|
||||||
|
undefined -> {ok, <<>>, Req};
|
||||||
|
<<>> -> {ok, <<>>, Req};
|
||||||
|
_ -> auth_user(Req)
|
||||||
|
end.
|
||||||
|
|
||||||
%%%===================================================================
|
%%%===================================================================
|
||||||
%%% HTTP‑ответы
|
%%% HTTP‑ответы
|
||||||
%%%===================================================================
|
%%%===================================================================
|
||||||
|
|||||||
+84
-13
@@ -1,8 +1,9 @@
|
|||||||
-module(logic_booking).
|
-module(logic_booking).
|
||||||
-include("records.hrl").
|
-include("records.hrl").
|
||||||
-export([create_booking/2, 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,
|
||||||
@@ -16,15 +17,30 @@
|
|||||||
-spec create_booking(UserId :: binary(), EventId :: binary()) ->
|
-spec create_booking(UserId :: binary(), EventId :: binary()) ->
|
||||||
{ok, #booking{}} |
|
{ok, #booking{}} |
|
||||||
{error, full | already_booked | not_found | personal_calendar |
|
{error, full | already_booked | not_found | personal_calendar |
|
||||||
subscription_inactive | own_event | event_not_active | access_denied}.
|
subscription_inactive | own_event | event_not_active | access_denied |
|
||||||
|
occurrence_start_required | invalid_occurrence}.
|
||||||
create_booking(UserId, EventId) ->
|
create_booking(UserId, EventId) ->
|
||||||
|
create_booking(UserId, EventId, undefined).
|
||||||
|
|
||||||
|
-spec create_booking(UserId :: binary(), EventId :: binary(),
|
||||||
|
OccurrenceStart :: calendar:datetime() | undefined) ->
|
||||||
|
{ok, #booking{}} |
|
||||||
|
{error, full | already_booked | not_found | personal_calendar |
|
||||||
|
subscription_inactive | own_event | event_not_active | access_denied |
|
||||||
|
occurrence_start_required | invalid_occurrence}.
|
||||||
|
create_booking(UserId, EventId, OccurrenceStart) ->
|
||||||
case core_event:get_by_id(EventId) of
|
case core_event:get_by_id(EventId) of
|
||||||
{ok, #event{status = active} = Event} ->
|
{ok, #event{status = active} = Event} ->
|
||||||
case core_calendar:get_by_id(Event#event.calendar_id) of
|
case resolve_bookable_event(Event, OccurrenceStart) of
|
||||||
{ok, Calendar} ->
|
{ok, BookEvent} ->
|
||||||
create_on_calendar(UserId, Event, Calendar);
|
case core_calendar:get_by_id(BookEvent#event.calendar_id) of
|
||||||
{error, not_found} ->
|
{ok, Calendar} ->
|
||||||
{error, not_found}
|
create_on_calendar(UserId, BookEvent, Calendar);
|
||||||
|
{error, not_found} ->
|
||||||
|
{error, not_found}
|
||||||
|
end;
|
||||||
|
{error, _} = Err ->
|
||||||
|
Err
|
||||||
end;
|
end;
|
||||||
{ok, _} ->
|
{ok, _} ->
|
||||||
{error, event_not_active};
|
{error, event_not_active};
|
||||||
@@ -32,6 +48,19 @@ create_booking(UserId, EventId) ->
|
|||||||
{error, not_found}
|
{error, not_found}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
|
resolve_bookable_event(#event{event_type = recurring}, undefined) ->
|
||||||
|
{error, occurrence_start_required};
|
||||||
|
resolve_bookable_event(#event{event_type = recurring} = Master, OccurrenceStart) ->
|
||||||
|
case logic_event:validate_occurrence(Master, OccurrenceStart) of
|
||||||
|
ok ->
|
||||||
|
logic_event:materialize_for_booking(
|
||||||
|
Master#event.id, OccurrenceStart, Master#event.specialist_id);
|
||||||
|
{error, _} = Err ->
|
||||||
|
Err
|
||||||
|
end;
|
||||||
|
resolve_bookable_event(Event, _OccurrenceStart) ->
|
||||||
|
{ok, Event}.
|
||||||
|
|
||||||
create_on_calendar(UserId, _Event, #calendar{owner_id = UserId}) ->
|
create_on_calendar(UserId, _Event, #calendar{owner_id = UserId}) ->
|
||||||
{error, own_event};
|
{error, own_event};
|
||||||
create_on_calendar(_UserId, _Event, #calendar{type = personal}) ->
|
create_on_calendar(_UserId, _Event, #calendar{type = personal}) ->
|
||||||
@@ -178,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
|
||||||
@@ -204,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),
|
||||||
@@ -244,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
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
-export([create_event/5, create_event/6, create_recurring_event/6, create_recurring_event/7,
|
-export([create_event/5, create_event/6, create_recurring_event/6, create_recurring_event/7,
|
||||||
get_event/2, list_events/2, update_event/3, delete_event/2]).
|
get_event/2, list_events/2, update_event/3, delete_event/2]).
|
||||||
-export([validate_event_time/1, validate_event_time/2, get_occurrences/3, cancel_occurrence/3]).
|
-export([validate_event_time/1, validate_event_time/2, get_occurrences/3, cancel_occurrence/3]).
|
||||||
-export([materialize_for_booking/3]).
|
-export([materialize_for_booking/3, validate_occurrence/2]).
|
||||||
-export([list_all_events/1, get_event_admin/1, update_event_admin/2, delete_event_admin/1]).
|
-export([list_all_events/1, get_event_admin/1, update_event_admin/2, delete_event_admin/1]).
|
||||||
-export([search_events/1]).
|
-export([search_events/1]).
|
||||||
|
|
||||||
@@ -147,6 +147,28 @@ cancel_occurrence(UserId, MasterId, OccurrenceStart) ->
|
|||||||
materialize_for_booking(MasterId, OccurrenceStart, SpecialistId) ->
|
materialize_for_booking(MasterId, OccurrenceStart, SpecialistId) ->
|
||||||
core_event:materialize_occurrence(MasterId, OccurrenceStart, SpecialistId).
|
core_event:materialize_occurrence(MasterId, OccurrenceStart, SpecialistId).
|
||||||
|
|
||||||
|
%% Проверка, что OccurrenceStart — неотменённое вхождение серии.
|
||||||
|
validate_occurrence(#event{event_type = recurring} = Event, OccurrenceStart) ->
|
||||||
|
try
|
||||||
|
Decoded = jsx:decode(Event#event.recurrence_rule, [return_maps]),
|
||||||
|
RRuleMap = case Decoded of
|
||||||
|
Map when is_map(Map) -> Map;
|
||||||
|
_ -> #{}
|
||||||
|
end,
|
||||||
|
{ok, ParsedRule} = logic_recurrence:parse_rrule(RRuleMap),
|
||||||
|
Occurrences = logic_recurrence:generate_occurrences(
|
||||||
|
Event#event.start_time, ParsedRule, OccurrenceStart),
|
||||||
|
Valid = filter_cancelled(Occurrences, get_exceptions(Event#event.id)),
|
||||||
|
case lists:member(OccurrenceStart, Valid) of
|
||||||
|
true -> ok;
|
||||||
|
false -> {error, invalid_occurrence}
|
||||||
|
end
|
||||||
|
catch
|
||||||
|
_:_ -> {error, invalid_occurrence}
|
||||||
|
end;
|
||||||
|
validate_occurrence(_, _) ->
|
||||||
|
{error, not_recurring}.
|
||||||
|
|
||||||
%% Получение события с проверкой доступа
|
%% Получение события с проверкой доступа
|
||||||
get_event(UserId, EventId) ->
|
get_event(UserId, EventId) ->
|
||||||
case core_event:get_by_id(EventId) of
|
case core_event:get_by_id(EventId) of
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ test() ->
|
|||||||
test_cancel_booking(ParticipantToken, EventId),
|
test_cancel_booking(ParticipantToken, EventId),
|
||||||
test_duplicate_booking(ParticipantToken, EventId),
|
test_duplicate_booking(ParticipantToken, EventId),
|
||||||
test_booking_unauthorized(EventId),
|
test_booking_unauthorized(EventId),
|
||||||
|
test_recurring_booking(OwnerToken, ParticipantToken),
|
||||||
|
|
||||||
ct:pal("=== All user bookings tests passed ==="),
|
ct:pal("=== All user bookings tests passed ==="),
|
||||||
ok.
|
ok.
|
||||||
@@ -116,4 +117,30 @@ test_booking_unauthorized(EventId) ->
|
|||||||
Path = <<"/v1/events/", EventId/binary, "/bookings">>,
|
Path = <<"/v1/events/", EventId/binary, "/bookings">>,
|
||||||
Resp = api_test_runner:client_request(post, Path, <<>>, <<"{}">>),
|
Resp = api_test_runner:client_request(post, Path, <<>>, <<"{}">>),
|
||||||
?assertMatch({ok, 401, _, _}, Resp),
|
?assertMatch({ok, 401, _, _}, Resp),
|
||||||
ct:pal(" OK: got 401").
|
ct:pal(" OK: got 401").
|
||||||
|
|
||||||
|
%% @doc Серия: без occurrence_start — 400; с датой вхождения — 201 на instance.
|
||||||
|
-spec test_recurring_booking(binary(), binary()) -> ok.
|
||||||
|
test_recurring_booking(OwnerToken, ParticipantToken) ->
|
||||||
|
ct:pal(" TEST: Recurring booking occurrence_start"),
|
||||||
|
CalId = api_test_runner:create_calendar(OwnerToken, #{
|
||||||
|
title => <<"SeriesBooking">>,
|
||||||
|
type => <<"commercial">>,
|
||||||
|
confirmation => <<"auto">>
|
||||||
|
}),
|
||||||
|
Start = api_test_runner:future_date(),
|
||||||
|
StartIso = api_test_runner:iso8601_utc(Start),
|
||||||
|
#{<<"id">> := MasterId} = api_test_runner:client_post(
|
||||||
|
<<"/v1/calendars/", CalId/binary, "/events">>, OwnerToken,
|
||||||
|
#{title => <<"Weekly slot">>,
|
||||||
|
start_time => StartIso,
|
||||||
|
duration => 60,
|
||||||
|
recurrence => #{freq => <<"WEEKLY">>, interval => 1}}),
|
||||||
|
Path = <<"/v1/events/", MasterId/binary, "/bookings">>,
|
||||||
|
{ok, 400, _, _} = api_test_runner:client_request(post, Path, ParticipantToken, <<"{}">>),
|
||||||
|
OccSec = calendar:datetime_to_gregorian_seconds(Start) + 7 * 86400,
|
||||||
|
OccIso = api_test_runner:iso8601_utc(calendar:gregorian_seconds_to_datetime(OccSec)),
|
||||||
|
#{<<"id">> := _Bid, <<"event_id">> := InstId} =
|
||||||
|
api_test_runner:client_post(Path, ParticipantToken, #{occurrence_start => OccIso}),
|
||||||
|
?assertNotEqual(MasterId, InstId),
|
||||||
|
ct:pal(" OK: booked instance ~s", [InstId]).
|
||||||
@@ -19,7 +19,7 @@ test() ->
|
|||||||
CalId = api_test_runner:create_calendar(Token, #{title => <<"TestCal">>}),
|
CalId = api_test_runner:create_calendar(Token, #{title => <<"TestCal">>}),
|
||||||
|
|
||||||
test_get_calendar(Token, CalId),
|
test_get_calendar(Token, CalId),
|
||||||
test_get_calendar_unauthorized(CalId),
|
test_get_calendar_guest(Token, CalId),
|
||||||
test_get_calendar_not_found(Token),
|
test_get_calendar_not_found(Token),
|
||||||
test_update_calendar(Token, CalId),
|
test_update_calendar(Token, CalId),
|
||||||
test_update_calendar_settings(Token, CalId),
|
test_update_calendar_settings(Token, CalId),
|
||||||
@@ -39,12 +39,15 @@ test_get_calendar(Token, CalId) ->
|
|||||||
?assert(maps:is_key(<<"title">>, Cal)),
|
?assert(maps:is_key(<<"title">>, Cal)),
|
||||||
ct:pal(" OK: ~s", [maps:get(<<"title">>, Cal)]).
|
ct:pal(" OK: ~s", [maps:get(<<"title">>, Cal)]).
|
||||||
|
|
||||||
test_get_calendar_unauthorized(CalId) ->
|
test_get_calendar_guest(Token, CalId) ->
|
||||||
ct:pal(" TEST: Get calendar without token (401)"),
|
ct:pal(" TEST: Guest GET commercial calendar (200), personal (403)"),
|
||||||
Path = <<"/v1/calendars/", CalId/binary>>,
|
Path = <<"/v1/calendars/", CalId/binary>>,
|
||||||
Resp = api_test_runner:client_request(get, Path, <<>>),
|
{ok, 200, _, Body} = api_test_runner:client_request(get, Path, <<>>),
|
||||||
?assertMatch({ok, 401, _, _}, Resp),
|
#{<<"id">> := CalId} = jsx:decode(list_to_binary(Body), [return_maps]),
|
||||||
ct:pal(" OK: got 401").
|
PersonalId = api_test_runner:existing_personal_calendar_id(Token),
|
||||||
|
PPath = <<"/v1/calendars/", PersonalId/binary>>,
|
||||||
|
{ok, 403, _, _} = api_test_runner:client_request(get, PPath, <<>>),
|
||||||
|
ct:pal(" OK: guest commercial 200, personal 403").
|
||||||
|
|
||||||
test_get_calendar_not_found(Token) ->
|
test_get_calendar_not_found(Token) ->
|
||||||
ct:pal(" TEST: Get non-existent calendar (404)"),
|
ct:pal(" TEST: Get non-existent calendar (404)"),
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
%%% - поиск с фильтрацией по датам (from/to)
|
%%% - поиск с фильтрацией по датам (from/to)
|
||||||
%%% - геопоиск (lat, lon, radius)
|
%%% - геопоиск (lat, lon, radius)
|
||||||
%%% - пагинацию результатов
|
%%% - пагинацию результатов
|
||||||
%%% - ошибку 401 без токена
|
%%% - публичный поиск без токена (200)
|
||||||
%%% @end
|
%%% @end
|
||||||
%%%-------------------------------------------------------------------
|
%%%-------------------------------------------------------------------
|
||||||
-module(user_search_tests).
|
-module(user_search_tests).
|
||||||
@@ -120,7 +120,9 @@ test_search_pagination(Token) ->
|
|||||||
ct:pal(" OK").
|
ct:pal(" OK").
|
||||||
|
|
||||||
test_search_unauthorized() ->
|
test_search_unauthorized() ->
|
||||||
ct:pal(" TEST: Search without token"),
|
ct:pal(" TEST: Search without token (public)"),
|
||||||
Resp = api_test_runner:client_request(get, <<"/v1/search?q=test">>, <<>>),
|
Resp = api_test_runner:client_request(get, <<"/v1/search?q=test">>, <<>>),
|
||||||
?assertMatch({ok, 401, _, _}, Resp),
|
{ok, 200, _, Body} = Resp,
|
||||||
ct:pal(" OK: got 401").
|
Decoded = jsx:decode(list_to_binary(Body), [return_maps]),
|
||||||
|
?assert(is_map(Decoded)),
|
||||||
|
ct:pal(" OK: guest search 200").
|
||||||
@@ -2,7 +2,8 @@
|
|||||||
-include_lib("eunit/include/eunit.hrl").
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
-include("records.hrl").
|
-include("records.hrl").
|
||||||
|
|
||||||
-define(TABLES, [user, calendar, event, booking, admin, subscription, calendar_specialist]).
|
-define(TABLES, [user, calendar, event, booking, admin, subscription, calendar_specialist,
|
||||||
|
recurrence_exception]).
|
||||||
|
|
||||||
setup() ->
|
setup() ->
|
||||||
eh_test_support:start_mnesia(),
|
eh_test_support:start_mnesia(),
|
||||||
@@ -21,6 +22,10 @@ logic_booking_test_() ->
|
|||||||
[
|
[
|
||||||
{"Create booking auto confirms", fun test_create_booking_auto/0},
|
{"Create booking auto confirms", fun test_create_booking_auto/0},
|
||||||
{"Create booking manual pending", fun test_create_booking_pending/0},
|
{"Create booking manual pending", fun test_create_booking_pending/0},
|
||||||
|
{"Recurring booking requires occurrence_start", fun test_recurring_requires_occurrence/0},
|
||||||
|
{"Recurring booking materializes occurrence", fun test_recurring_books_occurrence/0},
|
||||||
|
{"Recurring booking rejects invalid occurrence", fun test_recurring_invalid_occurrence/0},
|
||||||
|
{"Recurring booking rejects cancelled occurrence", fun test_recurring_cancelled_occurrence/0},
|
||||||
{"Create booking personal denied", fun test_booking_personal_denied/0},
|
{"Create booking personal denied", fun test_booking_personal_denied/0},
|
||||||
{"Create duplicate booking", fun test_create_duplicate_booking/0},
|
{"Create duplicate booking", fun test_create_duplicate_booking/0},
|
||||||
{"Create booking for missing event", fun test_booking_missing_event/0},
|
{"Create booking for missing event", fun test_booking_missing_event/0},
|
||||||
@@ -43,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},
|
||||||
@@ -96,6 +103,16 @@ past_start() ->
|
|||||||
Sec = calendar:datetime_to_gregorian_seconds(calendar:universal_time()) - 3600,
|
Sec = calendar:datetime_to_gregorian_seconds(calendar:universal_time()) - 3600,
|
||||||
calendar:gregorian_seconds_to_datetime(Sec).
|
calendar:gregorian_seconds_to_datetime(Sec).
|
||||||
|
|
||||||
|
add_days(DateTime, Days) ->
|
||||||
|
Sec = calendar:datetime_to_gregorian_seconds(DateTime) + Days * 86400,
|
||||||
|
calendar:gregorian_seconds_to_datetime(Sec).
|
||||||
|
|
||||||
|
create_recurring_event(CalendarId) ->
|
||||||
|
StartTime = eh_test_support:future_start(),
|
||||||
|
RRule = #{<<"freq">> => <<"WEEKLY">>, <<"interval">> => 1},
|
||||||
|
{ok, Event} = core_event:create_recurring(CalendarId, <<"Series">>, StartTime, 60, RRule),
|
||||||
|
{Event#event.id, StartTime}.
|
||||||
|
|
||||||
create_test_event_with_capacity(CalendarId, Capacity) ->
|
create_test_event_with_capacity(CalendarId, Capacity) ->
|
||||||
StartTime = eh_test_support:future_start(),
|
StartTime = eh_test_support:future_start(),
|
||||||
{ok, Event} = core_event:create(CalendarId, <<"Test Event">>, StartTime, 60),
|
{ok, Event} = core_event:create(CalendarId, <<"Test Event">>, StartTime, 60),
|
||||||
@@ -124,6 +141,48 @@ test_create_booking_pending() ->
|
|||||||
{ok, Stored} = core_booking:get_by_id(Booking#booking.id),
|
{ok, Stored} = core_booking:get_by_id(Booking#booking.id),
|
||||||
?assertEqual(pending, Stored#booking.status).
|
?assertEqual(pending, Stored#booking.status).
|
||||||
|
|
||||||
|
test_recurring_requires_occurrence() ->
|
||||||
|
OwnerId = create_test_user(user),
|
||||||
|
ParticipantId = create_test_user(user),
|
||||||
|
CalendarId = create_test_calendar(OwnerId, auto),
|
||||||
|
{MasterId, _} = create_recurring_event(CalendarId),
|
||||||
|
{error, occurrence_start_required} = logic_booking:create_booking(ParticipantId, MasterId).
|
||||||
|
|
||||||
|
test_recurring_books_occurrence() ->
|
||||||
|
OwnerId = create_test_user(user),
|
||||||
|
ParticipantId = create_test_user(user),
|
||||||
|
CalendarId = create_test_calendar(OwnerId, auto),
|
||||||
|
{MasterId, StartTime} = create_recurring_event(CalendarId),
|
||||||
|
Occ = add_days(StartTime, 7),
|
||||||
|
{ok, Booking} = logic_booking:create_booking(ParticipantId, MasterId, Occ),
|
||||||
|
{ok, BookedEvent} = core_event:get_by_id(Booking#booking.event_id),
|
||||||
|
?assertEqual(true, BookedEvent#event.is_instance),
|
||||||
|
?assertEqual(MasterId, BookedEvent#event.master_id),
|
||||||
|
?assertEqual(Occ, BookedEvent#event.start_time),
|
||||||
|
?assertNotEqual(MasterId, Booking#booking.event_id).
|
||||||
|
|
||||||
|
test_recurring_invalid_occurrence() ->
|
||||||
|
OwnerId = create_test_user(user),
|
||||||
|
ParticipantId = create_test_user(user),
|
||||||
|
CalendarId = create_test_calendar(OwnerId, auto),
|
||||||
|
{MasterId, StartTime} = create_recurring_event(CalendarId),
|
||||||
|
Bad = add_days(StartTime, 1),
|
||||||
|
{error, invalid_occurrence} = logic_booking:create_booking(ParticipantId, MasterId, Bad).
|
||||||
|
|
||||||
|
test_recurring_cancelled_occurrence() ->
|
||||||
|
OwnerId = create_test_user(user),
|
||||||
|
ParticipantId = create_test_user(user),
|
||||||
|
CalendarId = create_test_calendar(OwnerId, auto),
|
||||||
|
{MasterId, StartTime} = create_recurring_event(CalendarId),
|
||||||
|
Occ = add_days(StartTime, 7),
|
||||||
|
mnesia:dirty_write(#recurrence_exception{
|
||||||
|
master_id = MasterId,
|
||||||
|
original_start = Occ,
|
||||||
|
action = cancel,
|
||||||
|
new_start = undefined
|
||||||
|
}),
|
||||||
|
{error, invalid_occurrence} = logic_booking:create_booking(ParticipantId, MasterId, Occ).
|
||||||
|
|
||||||
test_booking_personal_denied() ->
|
test_booking_personal_denied() ->
|
||||||
OwnerId = create_test_user(user),
|
OwnerId = create_test_user(user),
|
||||||
ParticipantId = create_test_user(user),
|
ParticipantId = create_test_user(user),
|
||||||
@@ -369,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