%%%------------------------------------------------------------------- %%% @doc Обработчик конкретного бронирования (клиентский API). %%% %%% GET – получить информацию о бронировании. %%% PUT – подтвердить или отклонить бронирование (владельцем). %%% DELETE – отменить бронирование (участником). %%% @end %%%------------------------------------------------------------------- -module(handler_booking_by_id). -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) -> case cowboy_req:method(Req) of <<"GET">> -> get_booking(Req); <<"PUT">> -> update_booking(Req); <<"DELETE">> -> cancel_booking(Req); _ -> handler_utils:send_error(Req, 405, <<"Method not allowed">>) end. %%% Swagger metadata -spec trails() -> [map()]. trails() -> BaseParams = [ #{ name => <<"id">>, in => <<"path">>, description => <<"Booking ID">>, required => true, schema => #{type => string} } ], [ #{ % GET by id path => <<"/v1/bookings/:id">>, method => <<"GET">>, description => <<"Get booking by ID">>, tags => [<<"Bookings">>], parameters => BaseParams, responses => #{ 200 => #{ description => <<"Booking details">>, content => #{<<"application/json">> => #{schema => booking_schema()}} }, 404 => #{description => <<"Booking not found">>} } }, #{ % PUT update (confirm/decline) path => <<"/v1/bookings/:id">>, method => <<"PUT">>, description => <<"Confirm or decline a booking (owner)">>, tags => [<<"Bookings">>], parameters => BaseParams, requestBody => #{ required => true, content => #{<<"application/json">> => #{schema => booking_update_schema()}} }, responses => #{ 200 => #{description => <<"Booking updated">>}, 400 => #{description => <<"Invalid action">>}, 404 => #{description => <<"Booking not found">>} } }, #{ % DELETE cancel path => <<"/v1/bookings/:id">>, method => <<"DELETE">>, description => <<"Cancel booking (participant)">>, tags => [<<"Bookings">>], parameters => BaseParams, responses => #{ 200 => #{description => <<"Booking cancelled">>}, 404 => #{description => <<"Booking not found">>} } } ]. booking_schema() -> #{ type => object, properties => #{ id => #{type => string}, event_id => #{type => string}, user_id => #{type => string}, status => #{type => string, enum => [<<"pending">>, <<"confirmed">>, <<"cancelled">>]}, notes => #{type => string, nullable => true}, reminder_sent => #{type => boolean}, confirmed_at => #{type => string, format => <<"date-time">>, nullable => true}, created_at => #{type => string, format => <<"date-time">>}, updated_at => #{type => string, format => <<"date-time">>} } }. booking_update_schema() -> #{ type => object, required => [<<"action">>], properties => #{ action => #{type => string, enum => [<<"confirm">>, <<"decline">>]} } }. %%% Internal functions %% @doc Получить бронирование по ID. -spec get_booking(cowboy_req:req()) -> {ok, binary(), cowboy_req:req()}. get_booking(Req) -> case handler_utils:auth_user(Req) of {ok, UserId, Req1} -> BookingId = cowboy_req:binding(id, Req1), case logic_booking:get_booking(UserId, BookingId) of {ok, Booking} -> handler_utils:send_json(Req1, 200, booking_to_json(Booking)); {error, access_denied} -> handler_utils:send_error(Req1, 403, <<"Access denied">>); {error, not_found} -> handler_utils:send_error(Req1, 404, <<"Booking not found">>); {error, _} -> handler_utils:send_error(Req1, 500, <<"Internal server error">>) end; {error, Code, Message, Req1} -> handler_utils:send_error(Req1, Code, Message) end. %% @doc Подтвердить или отклонить бронирование (владельцем). -spec update_booking(cowboy_req:req()) -> {ok, binary(), cowboy_req:req()}. update_booking(Req) -> case handler_utils:auth_user(Req) of {ok, UserId, Req1} -> BookingId = cowboy_req:binding(id, Req1), {ok, Body, Req2} = cowboy_req:read_body(Req1), try jsx:decode(Body, [return_maps]) of #{<<"action">> := Action} when Action =:= <<"confirm">>; Action =:= <<"decline">> -> ActionAtom = binary_to_existing_atom(Action, utf8), case logic_booking:confirm_booking(UserId, BookingId, ActionAtom) of {ok, Booking} -> handler_utils:send_json(Req2, 200, booking_to_json(Booking)); {error, access_denied} -> handler_utils:send_error(Req2, 403, <<"Access denied">>); {error, not_found} -> handler_utils:send_error(Req2, 404, <<"Booking not found">>); {error, _} -> handler_utils:send_error(Req2, 500, <<"Internal server error">>) end; _ -> handler_utils:send_error(Req2, 400, <<"Missing or invalid 'action' field. Use 'confirm' or 'decline'">>) catch _:_ -> handler_utils:send_error(Req2, 400, <<"Invalid JSON format">>) end; {error, Code, Message, Req1} -> handler_utils:send_error(Req1, Code, Message) end. %% @doc Отменить бронирование (участником). -spec cancel_booking(cowboy_req:req()) -> {ok, binary(), cowboy_req:req()}. cancel_booking(Req) -> case handler_utils:auth_user(Req) of {ok, UserId, Req1} -> BookingId = cowboy_req:binding(id, Req1), case logic_booking:cancel_booking(BookingId, UserId) of {ok, Booking} -> handler_utils:send_json(Req1, 200, booking_to_json(Booking)); {error, access_denied} -> handler_utils:send_error(Req1, 403, <<"Access denied">>); {error, not_found} -> handler_utils:send_error(Req1, 404, <<"Booking not found">>); {error, _} -> handler_utils:send_error(Req1, 500, <<"Internal server error">>) end; {error, Code, Message, Req1} -> handler_utils:send_error(Req1, Code, Message) end. %% @private Формирует JSON-представление записи #booking{}. %% Учитывает все поля из records.hrl. -spec booking_to_json(#booking{}) -> map(). booking_to_json(Booking) -> #{ id => Booking#booking.id, event_id => Booking#booking.event_id, user_id => Booking#booking.user_id, status => Booking#booking.status, notes => Booking#booking.notes, reminder_sent => Booking#booking.reminder_sent, confirmed_at => case Booking#booking.confirmed_at of undefined -> null; Dt -> handler_utils:datetime_to_iso8601(Dt) end, created_at => handler_utils:datetime_to_iso8601(Booking#booking.created_at), updated_at => handler_utils:datetime_to_iso8601(Booking#booking.updated_at) }.