%%%------------------------------------------------------------------- %%% @doc Обработчик вхождений повторяющегося события (клиентский API). %%% %%% GET – получить список вхождений события в заданном диапазоне. %%% DELETE – отменить конкретное вхождение. %%% @end %%%------------------------------------------------------------------- -module(handler_event_occurrences). -behaviour(cowboy_handler). -export([init/2]). -export([trails/0]). -include("records.hrl"). %%% cowboy_handler callback -spec init(cowboy_req:req(), any()) -> {ok, cowboy_req:req(), any()}. init(Req, Opts) -> handle(Req, Opts). %%% Swagger metadata -spec trails() -> [map()]. trails() -> [ #{ % GET occurrences path => <<"/v1/events/:id/occurrences">>, method => <<"GET">>, description => <<"Get event occurrences in a date range">>, tags => [<<"Events">>], parameters => [ #{ name => <<"id">>, in => <<"path">>, description => <<"Event ID">>, required => true, schema => #{type => string} }, #{ name => <<"from">>, in => <<"query">>, description => <<"Start datetime (ISO8601)">>, required => true, schema => #{type => string, format => <<"date-time">>} }, #{ name => <<"to">>, in => <<"query">>, description => <<"End datetime (ISO8601)">>, required => true, schema => #{type => string, format => <<"date-time">>} } ], responses => #{ 200 => #{ description => <<"Array of occurrences">>, content => #{<<"application/json">> => #{schema => #{ type => array, items => occurrence_schema() }}} }, 400 => #{description => <<"Missing or invalid parameters">>}, 403 => #{description => <<"Access denied">>}, 404 => #{description => <<"Event not found">>} } }, #{ % DELETE occurrence path => <<"/v1/events/:id/occurrences/:start_time">>, method => <<"DELETE">>, description => <<"Cancel a specific occurrence">>, tags => [<<"Events">>], parameters => [ #{ name => <<"id">>, in => <<"path">>, description => <<"Event ID">>, required => true, schema => #{type => string} }, #{ name => <<"start_time">>, in => <<"path">>, description => <<"Start time of the occurrence (ISO8601)">>, required => true, schema => #{type => string, format => <<"date-time">>} } ], responses => #{ 200 => #{description => <<"Occurrence cancelled">>}, 400 => #{description => <<"Missing or invalid parameters">>}, 403 => #{description => <<"Access denied">>}, 404 => #{description => <<"Event not found">>} } } ]. occurrence_schema() -> #{ type => object, properties => #{ start_time => #{type => string, format => <<"date-time">>}, is_virtual => #{type => boolean}, id => #{type => string, description => <<"Event ID (only for materialized occurrences)">>}, duration => #{type => integer, description => <<"Duration in minutes (only for materialized occurrences)">>}, specialist_id => #{type => string, description => <<"Specialist ID (only for materialized occurrences)">>}, status => #{type => string, enum => [<<"active">>, <<"cancelled">>, <<"completed">>], description => <<"Status (only for materialized occurrences)">>} } }. %%%=================================================================== %%% HTTP-методы %%%=================================================================== %% @private -spec handle(cowboy_req:req(), any()) -> {ok, cowboy_req:req(), any()}. handle(Req, _Opts) -> case cowboy_req:method(Req) of <<"GET">> -> get_occurrences(Req); <<"DELETE">> -> cancel_occurrence(Req); _ -> handler_utils:send_error(Req, 405, <<"Method not allowed">>) end. %% @doc GET /v1/events/:id/occurrences — получение вхождений повторяющегося события. -spec get_occurrences(cowboy_req:req()) -> {ok, binary(), cowboy_req:req()}. get_occurrences(Req) -> case handler_utils:auth_user(Req) of {ok, UserId, Req1} -> EventId = cowboy_req:binding(id, Req1), Qs = cowboy_req:parse_qs(Req1), From = proplists:get_value(<<"from">>, Qs, undefined), To = proplists:get_value(<<"to">>, Qs, undefined), case {From, To} of {undefined, _} -> handler_utils:send_error(Req1, 400, <<"Missing 'from' parameter">>); {_, undefined} -> handler_utils:send_error(Req1, 400, <<"Missing 'to' parameter">>); {FromStr, ToStr} -> case {handler_utils:parse_datetime(FromStr), handler_utils:parse_datetime(ToStr)} of {{ok, FromDt}, {ok, ToDt}} -> case logic_event:get_occurrences(UserId, EventId, ToDt) of {ok, Occurrences} -> Filtered = filter_from(Occurrences, FromDt), Response = occurrences_to_json(Filtered), handler_utils:send_json(Req1, 200, Response); {error, not_recurring} -> handler_utils:send_error(Req1, 400, <<"Event is not recurring">>); {error, access_denied} -> handler_utils:send_error(Req1, 403, <<"Access denied">>); {error, not_found} -> handler_utils:send_error(Req1, 404, <<"Event not found">>); {error, _} -> handler_utils:send_error(Req1, 500, <<"Internal server error">>) end; _ -> handler_utils:send_error(Req1, 400, <<"Invalid date format. Use ISO 8601">>) end end; {error, Code, Message, Req1} -> handler_utils:send_error(Req1, Code, Message) end. %% @doc DELETE /v1/events/:id/occurrences/:start_time — отмена вхождения. -spec cancel_occurrence(cowboy_req:req()) -> {ok, binary(), cowboy_req:req()}. cancel_occurrence(Req) -> case handler_utils:auth_user(Req) of {ok, UserId, Req1} -> EventId = cowboy_req:binding(id, Req1), StartTimeStr = cowboy_req:binding(start_time, Req1), case handler_utils:parse_datetime(StartTimeStr) of {ok, StartTime} -> case logic_event:cancel_occurrence(UserId, EventId, StartTime) of {ok, cancelled} -> handler_utils:send_json(Req1, 200, #{status => <<"cancelled">>}); {error, not_recurring} -> handler_utils:send_error(Req1, 400, <<"Event is not recurring">>); {error, access_denied} -> handler_utils:send_error(Req1, 403, <<"Access denied">>); {error, not_found} -> handler_utils:send_error(Req1, 404, <<"Event not found">>); {error, _} -> handler_utils:send_error(Req1, 500, <<"Internal server error">>) end; {error, _} -> handler_utils:send_error(Req1, 400, <<"Invalid start_time format">>) end; {error, Code, Message, Req1} -> handler_utils:send_error(Req1, Code, Message) end. %%%=================================================================== %%% Вспомогательные функции %%%=================================================================== %% @private Фильтрует вхождения, оставляя только те, что не раньше From. -spec filter_from(list(), calendar:datetime()) -> list(). filter_from(Occurrences, From) -> lists:filter( fun({virtual, Occ}) -> Occ >= From; ({materialized, Event}) -> Event#event.start_time >= From end, Occurrences). %% @private Преобразует список вхождений в JSON-представление. -spec occurrences_to_json(list()) -> list(). occurrences_to_json(Occurrences) -> lists:map(fun occurrence_to_json/1, Occurrences). %% @private Преобразует одно вхождение в JSON-карту. -spec occurrence_to_json({virtual, calendar:datetime()} | {materialized, #event{}}) -> map(). occurrence_to_json({virtual, Occ}) -> #{ start_time => handler_utils:datetime_to_iso8601(Occ), is_virtual => true }; occurrence_to_json({materialized, Event}) -> #{ id => Event#event.id, start_time => handler_utils:datetime_to_iso8601(Event#event.start_time), duration => Event#event.duration, specialist_id => Event#event.specialist_id, is_virtual => false, status => Event#event.status }.