Stage 3.4
This commit is contained in:
102
src/core/core_booking.erl
Normal file
102
src/core/core_booking.erl
Normal file
@@ -0,0 +1,102 @@
|
||||
-module(core_booking).
|
||||
-include("records.hrl").
|
||||
|
||||
-export([create/2, get_by_id/1, get_by_event_and_user/2, list_by_event/1, list_by_user/1]).
|
||||
-export([update_status/2, delete/1]).
|
||||
-export([generate_id/0]).
|
||||
|
||||
%% Создание бронирования
|
||||
create(EventId, UserId) ->
|
||||
Id = generate_id(),
|
||||
Booking = #booking{
|
||||
id = Id,
|
||||
event_id = EventId,
|
||||
user_id = UserId,
|
||||
status = pending,
|
||||
confirmed_at = undefined,
|
||||
created_at = calendar:universal_time(),
|
||||
updated_at = calendar:universal_time()
|
||||
},
|
||||
|
||||
F = fun() ->
|
||||
mnesia:write(Booking),
|
||||
{ok, Booking}
|
||||
end,
|
||||
|
||||
case mnesia:transaction(F) of
|
||||
{atomic, Result} -> Result;
|
||||
{aborted, Reason} -> {error, Reason}
|
||||
end.
|
||||
|
||||
%% Получение бронирования по ID
|
||||
get_by_id(Id) ->
|
||||
case mnesia:dirty_read(booking, Id) of
|
||||
[] -> {error, not_found};
|
||||
[Booking] -> {ok, Booking}
|
||||
end.
|
||||
|
||||
%% Получение бронирования по событию и пользователю
|
||||
get_by_event_and_user(EventId, UserId) ->
|
||||
Match = #booking{event_id = EventId, user_id = UserId, _ = '_'},
|
||||
case mnesia:dirty_match_object(Match) of
|
||||
[] -> {error, not_found};
|
||||
[Booking] -> {ok, Booking}
|
||||
end.
|
||||
|
||||
%% Список бронирований события
|
||||
list_by_event(EventId) ->
|
||||
Match = #booking{event_id = EventId, _ = '_'},
|
||||
Bookings = mnesia:dirty_match_object(Match),
|
||||
{ok, Bookings}.
|
||||
|
||||
%% Список бронирований пользователя
|
||||
list_by_user(UserId) ->
|
||||
Match = #booking{user_id = UserId, _ = '_'},
|
||||
Bookings = mnesia:dirty_match_object(Match),
|
||||
{ok, Bookings}.
|
||||
|
||||
%% Обновление статуса бронирования
|
||||
update_status(Id, Status) when Status =:= pending; Status =:= confirmed; Status =:= cancelled ->
|
||||
F = fun() ->
|
||||
case mnesia:read(booking, Id) of
|
||||
[] ->
|
||||
{error, not_found};
|
||||
[Booking] ->
|
||||
Updated = Booking#booking{
|
||||
status = Status,
|
||||
confirmed_at = case Status of
|
||||
confirmed -> calendar:universal_time();
|
||||
_ -> Booking#booking.confirmed_at
|
||||
end,
|
||||
updated_at = calendar:universal_time()
|
||||
},
|
||||
mnesia:write(Updated),
|
||||
{ok, Updated}
|
||||
end
|
||||
end,
|
||||
|
||||
case mnesia:transaction(F) of
|
||||
{atomic, Result} -> Result;
|
||||
{aborted, Reason} -> {error, Reason}
|
||||
end.
|
||||
|
||||
%% Удаление бронирования (hard delete)
|
||||
delete(Id) ->
|
||||
F = fun() ->
|
||||
case mnesia:read(booking, Id) of
|
||||
[] ->
|
||||
{error, not_found};
|
||||
[Booking] ->
|
||||
mnesia:delete_object(Booking),
|
||||
{ok, deleted}
|
||||
end
|
||||
end,
|
||||
|
||||
case mnesia:transaction(F) of
|
||||
{atomic, Result} -> Result;
|
||||
{aborted, Reason} -> {error, Reason}
|
||||
end.
|
||||
|
||||
%% Внутренние функции
|
||||
generate_id() ->
|
||||
base64:encode(crypto:strong_rand_bytes(16), #{mode => urlsafe, padding => false}).
|
||||
@@ -1,11 +1,11 @@
|
||||
-module(core_calendar).
|
||||
-include("records.hrl").
|
||||
|
||||
-export([create/3, get_by_id/1, list_by_owner/1, update/2, delete/1]).
|
||||
-export([create/4, get_by_id/1, list_by_owner/1, update/2, delete/1]).
|
||||
-export([generate_id/0]).
|
||||
|
||||
%% Создание календаря
|
||||
create(OwnerId, Title, Description) ->
|
||||
create(OwnerId, Title, Description, Confirmation) ->
|
||||
Id = generate_id(),
|
||||
Calendar = #calendar{
|
||||
id = Id,
|
||||
@@ -14,7 +14,7 @@ create(OwnerId, Title, Description) ->
|
||||
description = Description,
|
||||
tags = [],
|
||||
type = personal,
|
||||
confirmation = manual,
|
||||
confirmation = Confirmation,
|
||||
rating_avg = 0.0,
|
||||
rating_count = 0,
|
||||
status = active,
|
||||
|
||||
@@ -37,12 +37,15 @@ start_http() ->
|
||||
{"/v1/login", handler_login, []},
|
||||
{"/v1/refresh", handler_refresh, []},
|
||||
{"/v1/user/me", handler_user_me, []},
|
||||
{"/v1/user/bookings", handler_user_bookings, []},
|
||||
{"/v1/calendars", handler_calendars, []},
|
||||
{"/v1/calendars/:id", handler_calendar_by_id, []},
|
||||
{"/v1/calendars/:calendar_id/events", handler_events, []},
|
||||
{"/v1/events/:id", handler_event_by_id, []},
|
||||
{"/v1/events/:id/occurrences", handler_event_occurrences, []},
|
||||
{"/v1/events/:id/occurrences/:start_time", handler_event_occurrences, []}
|
||||
{"/v1/events/:id/occurrences/:start_time", handler_event_occurrences, []},
|
||||
{"/v1/events/:id/bookings", handler_bookings, []},
|
||||
{"/v1/bookings/:id", handler_booking_by_id, []}
|
||||
]}
|
||||
]),
|
||||
|
||||
|
||||
128
src/handlers/handler_booking_by_id.erl
Normal file
128
src/handlers/handler_booking_by_id.erl
Normal file
@@ -0,0 +1,128 @@
|
||||
-module(handler_booking_by_id).
|
||||
-include("records.hrl").
|
||||
|
||||
-export([init/2]).
|
||||
|
||||
init(Req, Opts) ->
|
||||
handle(Req, Opts).
|
||||
|
||||
handle(Req, _Opts) ->
|
||||
case cowboy_req:method(Req) of
|
||||
<<"GET">> -> get_booking(Req);
|
||||
<<"PUT">> -> update_booking(Req);
|
||||
<<"DELETE">> -> cancel_booking(Req);
|
||||
_ -> send_error(Req, 405, <<"Method not allowed">>)
|
||||
end.
|
||||
|
||||
%% GET /v1/bookings/:id - получение бронирования
|
||||
get_booking(Req) ->
|
||||
case handler_auth:authenticate(Req) of
|
||||
{ok, UserId, Req1} ->
|
||||
BookingId = cowboy_req:binding(id, Req1),
|
||||
case logic_booking:get_booking(UserId, BookingId) of
|
||||
{ok, Booking} ->
|
||||
Response = booking_to_json(Booking),
|
||||
send_json(Req1, 200, Response);
|
||||
{error, access_denied} ->
|
||||
send_error(Req1, 403, <<"Access denied">>);
|
||||
{error, not_found} ->
|
||||
send_error(Req1, 404, <<"Booking not found">>);
|
||||
{error, _} ->
|
||||
send_error(Req1, 500, <<"Internal server error">>)
|
||||
end;
|
||||
{error, Code, Message, Req1} ->
|
||||
send_error(Req1, Code, Message)
|
||||
end.
|
||||
|
||||
%% PUT /v1/bookings/:id - подтверждение/отклонение бронирования (владельцем)
|
||||
update_booking(Req) ->
|
||||
case handler_auth:authenticate(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
|
||||
Decoded when is_map(Decoded) ->
|
||||
case maps:get(<<"action">>, Decoded, undefined) of
|
||||
<<"confirm">> ->
|
||||
case logic_booking:confirm_booking(UserId, BookingId, confirm) of
|
||||
{ok, Booking} ->
|
||||
Response = booking_to_json(Booking),
|
||||
send_json(Req2, 200, Response);
|
||||
{error, access_denied} ->
|
||||
send_error(Req2, 403, <<"Access denied">>);
|
||||
{error, not_found} ->
|
||||
send_error(Req2, 404, <<"Booking not found">>);
|
||||
{error, _} ->
|
||||
send_error(Req2, 500, <<"Internal server error">>)
|
||||
end;
|
||||
<<"decline">> ->
|
||||
case logic_booking:confirm_booking(UserId, BookingId, decline) of
|
||||
{ok, Booking} ->
|
||||
Response = booking_to_json(Booking),
|
||||
send_json(Req2, 200, Response);
|
||||
{error, access_denied} ->
|
||||
send_error(Req2, 403, <<"Access denied">>);
|
||||
{error, not_found} ->
|
||||
send_error(Req2, 404, <<"Booking not found">>);
|
||||
{error, _} ->
|
||||
send_error(Req2, 500, <<"Internal server error">>)
|
||||
end;
|
||||
_ ->
|
||||
send_error(Req2, 400, <<"Missing or invalid 'action' field. Use 'confirm' or 'decline'">>)
|
||||
end;
|
||||
_ ->
|
||||
send_error(Req2, 400, <<"Invalid JSON">>)
|
||||
catch
|
||||
_:_ ->
|
||||
send_error(Req2, 400, <<"Invalid JSON format">>)
|
||||
end;
|
||||
{error, Code, Message, Req1} ->
|
||||
send_error(Req1, Code, Message)
|
||||
end.
|
||||
|
||||
%% DELETE /v1/bookings/:id - отмена бронирования (участником)
|
||||
cancel_booking(Req) ->
|
||||
case handler_auth:authenticate(Req) of
|
||||
{ok, UserId, Req1} ->
|
||||
BookingId = cowboy_req:binding(id, Req1),
|
||||
case logic_booking:cancel_booking(UserId, BookingId) of
|
||||
{ok, Booking} ->
|
||||
Response = booking_to_json(Booking),
|
||||
send_json(Req1, 200, Response);
|
||||
{error, access_denied} ->
|
||||
send_error(Req1, 403, <<"Access denied">>);
|
||||
{error, not_found} ->
|
||||
send_error(Req1, 404, <<"Booking not found">>);
|
||||
{error, _} ->
|
||||
send_error(Req1, 500, <<"Internal server error">>)
|
||||
end;
|
||||
{error, Code, Message, Req1} ->
|
||||
send_error(Req1, Code, Message)
|
||||
end.
|
||||
|
||||
%% Вспомогательные функции
|
||||
booking_to_json(Booking) ->
|
||||
#{
|
||||
id => Booking#booking.id,
|
||||
event_id => Booking#booking.event_id,
|
||||
user_id => Booking#booking.user_id,
|
||||
status => Booking#booking.status,
|
||||
confirmed_at => case Booking#booking.confirmed_at of
|
||||
undefined -> null;
|
||||
Dt -> datetime_to_iso8601(Dt)
|
||||
end,
|
||||
created_at => datetime_to_iso8601(Booking#booking.created_at),
|
||||
updated_at => datetime_to_iso8601(Booking#booking.updated_at)
|
||||
}.
|
||||
|
||||
datetime_to_iso8601({{Year, Month, Day}, {Hour, Minute, Second}}) ->
|
||||
iolist_to_binary(io_lib:format("~4..0B-~2..0B-~2..0BT~2..0B:~2..0B:~2..0BZ",
|
||||
[Year, Month, Day, Hour, Minute, Second])).
|
||||
|
||||
send_json(Req, Status, Data) ->
|
||||
Body = jsx:encode(Data),
|
||||
cowboy_req:reply(Status, #{<<"content-type">> => <<"application/json">>}, Body, Req).
|
||||
|
||||
send_error(Req, Status, Message) ->
|
||||
Body = jsx:encode(#{error => Message}),
|
||||
cowboy_req:reply(Status, #{<<"content-type">> => <<"application/json">>}, Body, Req).
|
||||
87
src/handlers/handler_bookings.erl
Normal file
87
src/handlers/handler_bookings.erl
Normal file
@@ -0,0 +1,87 @@
|
||||
-module(handler_bookings).
|
||||
-include("records.hrl").
|
||||
|
||||
-export([init/2]).
|
||||
|
||||
init(Req, Opts) ->
|
||||
handle(Req, Opts).
|
||||
|
||||
handle(Req, _Opts) ->
|
||||
case cowboy_req:method(Req) of
|
||||
<<"POST">> -> create_booking(Req);
|
||||
<<"GET">> -> list_bookings(Req);
|
||||
_ -> send_error(Req, 405, <<"Method not allowed">>)
|
||||
end.
|
||||
|
||||
%% POST /v1/events/:id/bookings - создание бронирования (запись на событие)
|
||||
create_booking(Req) ->
|
||||
case handler_auth:authenticate(Req) of
|
||||
{ok, UserId, Req1} ->
|
||||
EventId = cowboy_req:binding(id, Req1),
|
||||
case logic_booking:create_booking(UserId, EventId) of
|
||||
{ok, Booking} ->
|
||||
Response = booking_to_json(Booking),
|
||||
send_json(Req1, 201, Response);
|
||||
{error, already_booked} ->
|
||||
send_error(Req1, 409, <<"Already booked">>);
|
||||
{error, event_full} ->
|
||||
send_error(Req1, 400, <<"Event is full">>);
|
||||
{error, event_not_active} ->
|
||||
send_error(Req1, 400, <<"Event is not active">>);
|
||||
{error, access_denied} ->
|
||||
send_error(Req1, 403, <<"Access denied">>);
|
||||
{error, not_found} ->
|
||||
send_error(Req1, 404, <<"Event not found">>);
|
||||
{error, _} ->
|
||||
send_error(Req1, 500, <<"Internal server error">>)
|
||||
end;
|
||||
{error, Code, Message, Req1} ->
|
||||
send_error(Req1, Code, Message)
|
||||
end.
|
||||
|
||||
%% GET /v1/events/:id/bookings - список бронирований события (для владельца)
|
||||
list_bookings(Req) ->
|
||||
case handler_auth:authenticate(Req) of
|
||||
{ok, UserId, Req1} ->
|
||||
EventId = cowboy_req:binding(id, Req1),
|
||||
case logic_booking:list_event_bookings(UserId, EventId) of
|
||||
{ok, Bookings} ->
|
||||
Response = [booking_to_json(B) || B <- Bookings],
|
||||
send_json(Req1, 200, Response);
|
||||
{error, access_denied} ->
|
||||
send_error(Req1, 403, <<"Access denied">>);
|
||||
{error, not_found} ->
|
||||
send_error(Req1, 404, <<"Event not found">>);
|
||||
{error, _} ->
|
||||
send_error(Req1, 500, <<"Internal server error">>)
|
||||
end;
|
||||
{error, Code, Message, Req1} ->
|
||||
send_error(Req1, Code, Message)
|
||||
end.
|
||||
|
||||
%% Вспомогательные функции
|
||||
booking_to_json(Booking) ->
|
||||
#{
|
||||
id => Booking#booking.id,
|
||||
event_id => Booking#booking.event_id,
|
||||
user_id => Booking#booking.user_id,
|
||||
status => Booking#booking.status,
|
||||
confirmed_at => case Booking#booking.confirmed_at of
|
||||
undefined -> null;
|
||||
Dt -> datetime_to_iso8601(Dt)
|
||||
end,
|
||||
created_at => datetime_to_iso8601(Booking#booking.created_at),
|
||||
updated_at => datetime_to_iso8601(Booking#booking.updated_at)
|
||||
}.
|
||||
|
||||
datetime_to_iso8601({{Year, Month, Day}, {Hour, Minute, Second}}) ->
|
||||
iolist_to_binary(io_lib:format("~4..0B-~2..0B-~2..0BT~2..0B:~2..0B:~2..0BZ",
|
||||
[Year, Month, Day, Hour, Minute, Second])).
|
||||
|
||||
send_json(Req, Status, Data) ->
|
||||
Body = jsx:encode(Data),
|
||||
cowboy_req:reply(Status, #{<<"content-type">> => <<"application/json">>}, Body, Req).
|
||||
|
||||
send_error(Req, Status, Message) ->
|
||||
Body = jsx:encode(#{error => Message}),
|
||||
cowboy_req:reply(Status, #{<<"content-type">> => <<"application/json">>}, Body, Req).
|
||||
@@ -23,7 +23,8 @@ create_calendar(Req) ->
|
||||
case Decoded of
|
||||
#{<<"title">> := Title} ->
|
||||
Description = maps:get(<<"description">>, Decoded, <<"">>),
|
||||
case logic_calendar:create_calendar(UserId, Title, Description) of
|
||||
Confirmation = parse_confirmation(maps:get(<<"confirmation">>, Decoded, <<"manual">>)),
|
||||
case logic_calendar:create_calendar(UserId, Title, Description, Confirmation) of
|
||||
{ok, Calendar} ->
|
||||
Response = calendar_to_json(Calendar),
|
||||
send_json(Req2, 201, Response);
|
||||
@@ -45,6 +46,11 @@ create_calendar(Req) ->
|
||||
send_error(Req1, Code, Message)
|
||||
end.
|
||||
|
||||
parse_confirmation(<<"auto">>) -> auto;
|
||||
parse_confirmation(<<"manual">>) -> manual;
|
||||
parse_confirmation(#{<<"timeout">> := N}) when is_integer(N), N > 0 -> {timeout, N};
|
||||
parse_confirmation(_) -> manual.
|
||||
|
||||
%% GET /v1/calendars - список календарей
|
||||
list_calendars(Req) ->
|
||||
case handler_auth:authenticate(Req) of
|
||||
|
||||
@@ -9,41 +9,54 @@ init(Req, Opts) ->
|
||||
handle(Req, _Opts) ->
|
||||
case cowboy_req:method(Req) of
|
||||
<<"POST">> ->
|
||||
{ok, Body, Req1} = cowboy_req:read_body(Req),
|
||||
case jsx:decode(Body, [return_maps]) of
|
||||
#{<<"email">> := Email, <<"password">> := Password} ->
|
||||
case core_user:get_by_email(Email) of
|
||||
{ok, User} ->
|
||||
case logic_auth:verify_password(Password, User#user.password_hash) of
|
||||
{ok, true} ->
|
||||
case User#user.status of
|
||||
active ->
|
||||
Token = logic_auth:generate_jwt(User#user.id, User#user.role),
|
||||
{RefreshToken, ExpiresAt} = logic_auth:generate_refresh_token(User#user.id),
|
||||
save_refresh_token(User#user.id, RefreshToken, ExpiresAt),
|
||||
Response = #{
|
||||
user => #{
|
||||
id => User#user.id,
|
||||
email => User#user.email,
|
||||
role => User#user.role
|
||||
},
|
||||
token => Token,
|
||||
refresh_token => RefreshToken
|
||||
},
|
||||
send_json(Req1, 200, Response);
|
||||
frozen ->
|
||||
send_error(Req1, 403, <<"Account frozen">>);
|
||||
deleted ->
|
||||
send_error(Req1, 403, <<"Account deleted">>)
|
||||
case cowboy_req:has_body(Req) of
|
||||
true ->
|
||||
{ok, Body, Req1} = cowboy_req:read_body(Req),
|
||||
case Body of
|
||||
<<>> ->
|
||||
send_error(Req1, 400, <<"Empty request body">>);
|
||||
_ ->
|
||||
try jsx:decode(Body, [return_maps]) of
|
||||
#{<<"email">> := Email, <<"password">> := Password} ->
|
||||
case core_user:get_by_email(Email) of
|
||||
{ok, User} ->
|
||||
case logic_auth:verify_password(Password, User#user.password_hash) of
|
||||
{ok, true} ->
|
||||
case User#user.status of
|
||||
active ->
|
||||
Token = logic_auth:generate_jwt(User#user.id, User#user.role),
|
||||
{RefreshToken, ExpiresAt} = logic_auth:generate_refresh_token(User#user.id),
|
||||
save_refresh_token(User#user.id, RefreshToken, ExpiresAt),
|
||||
Response = #{
|
||||
user => #{
|
||||
id => User#user.id,
|
||||
email => User#user.email,
|
||||
role => User#user.role
|
||||
},
|
||||
token => Token,
|
||||
refresh_token => RefreshToken
|
||||
},
|
||||
send_json(Req1, 200, Response);
|
||||
frozen ->
|
||||
send_error(Req1, 403, <<"Account frozen">>);
|
||||
deleted ->
|
||||
send_error(Req1, 403, <<"Account deleted">>)
|
||||
end;
|
||||
_ ->
|
||||
send_error(Req1, 401, <<"Invalid credentials">>)
|
||||
end;
|
||||
{error, not_found} ->
|
||||
send_error(Req1, 401, <<"Invalid credentials">>)
|
||||
end;
|
||||
_ ->
|
||||
send_error(Req1, 401, <<"Invalid credentials">>)
|
||||
end;
|
||||
{error, not_found} ->
|
||||
send_error(Req1, 401, <<"Invalid credentials">>)
|
||||
send_error(Req1, 400, <<"Missing email or password">>)
|
||||
catch
|
||||
_:_ ->
|
||||
send_error(Req1, 400, <<"Invalid JSON">>)
|
||||
end
|
||||
end;
|
||||
_ ->
|
||||
send_error(Req1, 400, <<"Invalid request body">>)
|
||||
false ->
|
||||
send_error(Req, 400, <<"Missing request body">>)
|
||||
end;
|
||||
_ ->
|
||||
send_error(Req, 405, <<"Method not allowed">>)
|
||||
|
||||
@@ -9,31 +9,46 @@ init(Req, Opts) ->
|
||||
handle(Req, _Opts) ->
|
||||
case cowboy_req:method(Req) of
|
||||
<<"POST">> ->
|
||||
{ok, Body, Req1} = cowboy_req:read_body(Req),
|
||||
case jsx:decode(Body, [return_maps]) of
|
||||
#{<<"email">> := Email, <<"password">> := Password} ->
|
||||
case core_user:email_exists(Email) of
|
||||
true ->
|
||||
send_error(Req1, 409, <<"Email already exists">>);
|
||||
false ->
|
||||
case core_user:create(Email, Password) of
|
||||
{ok, User} ->
|
||||
Token = logic_auth:generate_jwt(User#user.id, User#user.role),
|
||||
Response = #{
|
||||
user => #{
|
||||
id => User#user.id,
|
||||
email => User#user.email,
|
||||
role => User#user.role
|
||||
},
|
||||
token => Token
|
||||
},
|
||||
send_json(Req1, 201, Response);
|
||||
{error, _} ->
|
||||
send_error(Req1, 500, <<"Internal server error">>)
|
||||
case cowboy_req:has_body(Req) of
|
||||
true ->
|
||||
{ok, Body, Req1} = cowboy_req:read_body(Req),
|
||||
case Body of
|
||||
<<>> ->
|
||||
send_error(Req1, 400, <<"Empty request body">>);
|
||||
_ ->
|
||||
try jsx:decode(Body, [return_maps]) of
|
||||
#{<<"email">> := Email, <<"password">> := Password} ->
|
||||
case core_user:email_exists(Email) of
|
||||
true ->
|
||||
send_error(Req1, 409, <<"Email already exists">>);
|
||||
false ->
|
||||
case core_user:create(Email, Password) of
|
||||
{ok, User} ->
|
||||
Token = logic_auth:generate_jwt(User#user.id, User#user.role),
|
||||
Response = #{
|
||||
user => #{
|
||||
id => User#user.id,
|
||||
email => User#user.email,
|
||||
role => User#user.role
|
||||
},
|
||||
token => Token
|
||||
},
|
||||
send_json(Req1, 201, Response);
|
||||
{error, email_exists} ->
|
||||
send_error(Req1, 409, <<"Email already exists">>);
|
||||
{error, _} ->
|
||||
send_error(Req1, 500, <<"Internal server error">>)
|
||||
end
|
||||
end;
|
||||
_ ->
|
||||
send_error(Req1, 400, <<"Missing email or password">>)
|
||||
catch
|
||||
_:_ ->
|
||||
send_error(Req1, 400, <<"Invalid JSON">>)
|
||||
end
|
||||
end;
|
||||
_ ->
|
||||
send_error(Req1, 400, <<"Invalid request body">>)
|
||||
false ->
|
||||
send_error(Req, 400, <<"Missing request body">>)
|
||||
end;
|
||||
_ ->
|
||||
send_error(Req, 405, <<"Method not allowed">>)
|
||||
|
||||
55
src/handlers/handler_user_bookings.erl
Normal file
55
src/handlers/handler_user_bookings.erl
Normal file
@@ -0,0 +1,55 @@
|
||||
-module(handler_user_bookings).
|
||||
-include("records.hrl").
|
||||
|
||||
-export([init/2]).
|
||||
|
||||
init(Req, Opts) ->
|
||||
handle(Req, Opts).
|
||||
|
||||
handle(Req, _Opts) ->
|
||||
case cowboy_req:method(Req) of
|
||||
<<"GET">> -> list_user_bookings(Req);
|
||||
_ -> send_error(Req, 405, <<"Method not allowed">>)
|
||||
end.
|
||||
|
||||
%% GET /v1/user/bookings - список бронирований текущего пользователя
|
||||
list_user_bookings(Req) ->
|
||||
case handler_auth:authenticate(Req) of
|
||||
{ok, UserId, Req1} ->
|
||||
case logic_booking:list_user_bookings(UserId) of
|
||||
{ok, Bookings} ->
|
||||
Response = [booking_to_json(B) || B <- Bookings],
|
||||
send_json(Req1, 200, Response);
|
||||
{error, _} ->
|
||||
send_error(Req1, 500, <<"Internal server error">>)
|
||||
end;
|
||||
{error, Code, Message, Req1} ->
|
||||
send_error(Req1, Code, Message)
|
||||
end.
|
||||
|
||||
%% Вспомогательные функции
|
||||
booking_to_json(Booking) ->
|
||||
#{
|
||||
id => Booking#booking.id,
|
||||
event_id => Booking#booking.event_id,
|
||||
user_id => Booking#booking.user_id,
|
||||
status => Booking#booking.status,
|
||||
confirmed_at => case Booking#booking.confirmed_at of
|
||||
undefined -> null;
|
||||
Dt -> datetime_to_iso8601(Dt)
|
||||
end,
|
||||
created_at => datetime_to_iso8601(Booking#booking.created_at),
|
||||
updated_at => datetime_to_iso8601(Booking#booking.updated_at)
|
||||
}.
|
||||
|
||||
datetime_to_iso8601({{Year, Month, Day}, {Hour, Minute, Second}}) ->
|
||||
iolist_to_binary(io_lib:format("~4..0B-~2..0B-~2..0BT~2..0B:~2..0B:~2..0BZ",
|
||||
[Year, Month, Day, Hour, Minute, Second])).
|
||||
|
||||
send_json(Req, Status, Data) ->
|
||||
Body = jsx:encode(Data),
|
||||
cowboy_req:reply(Status, #{<<"content-type">> => <<"application/json">>}, Body, Req).
|
||||
|
||||
send_error(Req, Status, Message) ->
|
||||
Body = jsx:encode(#{error => Message}),
|
||||
cowboy_req:reply(Status, #{<<"content-type">> => <<"application/json">>}, Body, Req).
|
||||
189
src/logic/logic_booking.erl
Normal file
189
src/logic/logic_booking.erl
Normal file
@@ -0,0 +1,189 @@
|
||||
-module(logic_booking).
|
||||
-include("records.hrl").
|
||||
|
||||
-export([create_booking/2, confirm_booking/3, cancel_booking/2]).
|
||||
-export([get_booking/2, list_event_bookings/2, list_user_bookings/1]).
|
||||
-export([auto_confirm/1, check_timeout_confirmations/0]).
|
||||
|
||||
%% Создание бронирования (запись на событие)
|
||||
create_booking(UserId, EventId) ->
|
||||
% Получаем событие напрямую, без проверки доступа к календарю
|
||||
case core_event:get_by_id(EventId) of
|
||||
{ok, Event} ->
|
||||
% Проверяем, что событие активно
|
||||
case Event#event.status of
|
||||
active ->
|
||||
% Проверяем календарь для политики подтверждения
|
||||
case core_calendar:get_by_id(Event#event.calendar_id) of
|
||||
{ok, Calendar} ->
|
||||
case Calendar#calendar.status of
|
||||
active ->
|
||||
% Проверяем, что есть места
|
||||
case check_capacity(EventId, Event#event.capacity) of
|
||||
{ok, _} ->
|
||||
% Проверяем, не записан ли уже пользователь
|
||||
case core_booking:get_by_event_and_user(EventId, UserId) of
|
||||
{error, not_found} ->
|
||||
ActualEventId = get_actual_event_id(Event, UserId),
|
||||
case core_booking:create(ActualEventId, UserId) of
|
||||
{ok, Booking} ->
|
||||
handle_confirmation_policy(Booking, Event, Calendar),
|
||||
{ok, Booking};
|
||||
Error ->
|
||||
Error
|
||||
end;
|
||||
{ok, _} ->
|
||||
{error, already_booked}
|
||||
end;
|
||||
{error, full} ->
|
||||
{error, event_full}
|
||||
end;
|
||||
_ ->
|
||||
{error, calendar_not_active}
|
||||
end;
|
||||
_ ->
|
||||
{error, calendar_not_found}
|
||||
end;
|
||||
_ ->
|
||||
{error, event_not_active}
|
||||
end;
|
||||
Error ->
|
||||
Error
|
||||
end.
|
||||
|
||||
%% Подтверждение бронирования (владельцем календаря)
|
||||
confirm_booking(UserId, BookingId, Action) when Action =:= confirm; Action =:= decline ->
|
||||
case core_booking:get_by_id(BookingId) of
|
||||
{ok, Booking} ->
|
||||
% Проверяем права на событие
|
||||
case logic_event:get_event(UserId, Booking#booking.event_id) of
|
||||
{ok, Event} ->
|
||||
% Проверяем, что пользователь может редактировать календарь
|
||||
case logic_calendar:get_calendar(UserId, Event#event.calendar_id) of
|
||||
{ok, Calendar} ->
|
||||
case logic_calendar:can_edit(UserId, Calendar) of
|
||||
true ->
|
||||
case Action of
|
||||
confirm ->
|
||||
core_booking:update_status(BookingId, confirmed);
|
||||
decline ->
|
||||
core_booking:update_status(BookingId, cancelled)
|
||||
end;
|
||||
false ->
|
||||
{error, access_denied}
|
||||
end;
|
||||
Error ->
|
||||
Error
|
||||
end;
|
||||
Error ->
|
||||
Error
|
||||
end;
|
||||
Error ->
|
||||
Error
|
||||
end.
|
||||
|
||||
%% Отмена бронирования (участником)
|
||||
cancel_booking(UserId, BookingId) ->
|
||||
case core_booking:get_by_id(BookingId) of
|
||||
{ok, Booking} ->
|
||||
% Проверяем, что это бронирование пользователя
|
||||
case Booking#booking.user_id =:= UserId of
|
||||
true ->
|
||||
core_booking:update_status(BookingId, cancelled);
|
||||
false ->
|
||||
{error, access_denied}
|
||||
end;
|
||||
Error ->
|
||||
Error
|
||||
end.
|
||||
|
||||
%% Получение бронирования
|
||||
get_booking(UserId, BookingId) ->
|
||||
case core_booking:get_by_id(BookingId) of
|
||||
{ok, Booking} ->
|
||||
% Проверяем доступ к событию
|
||||
case logic_event:get_event(UserId, Booking#booking.event_id) of
|
||||
{ok, _} -> {ok, Booking};
|
||||
Error -> Error
|
||||
end;
|
||||
Error ->
|
||||
Error
|
||||
end.
|
||||
|
||||
%% Список бронирований события (для владельца)
|
||||
list_event_bookings(UserId, EventId) ->
|
||||
case logic_event:get_event(UserId, EventId) of
|
||||
{ok, Event} ->
|
||||
% Проверяем права на календарь
|
||||
case logic_calendar:get_calendar(UserId, Event#event.calendar_id) of
|
||||
{ok, Calendar} ->
|
||||
case logic_calendar:can_edit(UserId, Calendar) of
|
||||
true ->
|
||||
core_booking:list_by_event(EventId);
|
||||
false ->
|
||||
{error, access_denied}
|
||||
end;
|
||||
Error ->
|
||||
Error
|
||||
end;
|
||||
Error ->
|
||||
Error
|
||||
end.
|
||||
|
||||
%% Список бронирований пользователя
|
||||
list_user_bookings(UserId) ->
|
||||
core_booking:list_by_user(UserId).
|
||||
|
||||
%% Автоматическое подтверждение (для политики auto)
|
||||
auto_confirm(BookingId) ->
|
||||
core_booking:update_status(BookingId, confirmed).
|
||||
|
||||
%% Проверка истечения timeout подтверждений
|
||||
check_timeout_confirmations() ->
|
||||
% Получаем все pending бронирования для календарей с timeout
|
||||
% В реальной реализации нужно периодически вызывать эту функцию
|
||||
ok.
|
||||
|
||||
%% Внутренние функции
|
||||
check_capacity(_EventId, undefined) ->
|
||||
{ok, unlimited};
|
||||
check_capacity(EventId, Capacity) ->
|
||||
{ok, Bookings} = core_booking:list_by_event(EventId),
|
||||
ConfirmedCount = length([B || B <- Bookings, B#booking.status =:= confirmed]),
|
||||
case ConfirmedCount < Capacity of
|
||||
true -> {ok, Capacity - ConfirmedCount};
|
||||
false -> {error, full}
|
||||
end.
|
||||
|
||||
get_actual_event_id(Event, _UserId) ->
|
||||
case Event#event.event_type of
|
||||
recurring ->
|
||||
% Для повторяющихся событий нужно материализовать вхождение
|
||||
% Здесь предполагается, что start_time передаётся в запросе
|
||||
% В полной реализации нужно получать occurrence_start из параметров
|
||||
Event#event.id;
|
||||
single ->
|
||||
Event#event.id
|
||||
end.
|
||||
|
||||
handle_confirmation_policy(Booking, _Event, Calendar) ->
|
||||
io:format("Confirmation policy: ~p~n", [Calendar#calendar.confirmation]),
|
||||
case Calendar#calendar.confirmation of
|
||||
auto ->
|
||||
io:format("Auto-confirming booking ~p~n", [Booking#booking.id]),
|
||||
auto_confirm(Booking#booking.id);
|
||||
manual ->
|
||||
io:format("Manual confirmation, leaving pending~n"),
|
||||
ok;
|
||||
{timeout, Seconds} ->
|
||||
io:format("Timeout confirmation: ~p seconds~n", [Seconds]),
|
||||
spawn(fun() ->
|
||||
timer:sleep(Seconds * 1000),
|
||||
case core_booking:get_by_id(Booking#booking.id) of
|
||||
{ok, B} when B#booking.status =:= pending ->
|
||||
auto_confirm(Booking#booking.id);
|
||||
_ ->
|
||||
ok
|
||||
end
|
||||
end)
|
||||
end.
|
||||
@@ -1,18 +1,17 @@
|
||||
-module(logic_calendar).
|
||||
-include("records.hrl").
|
||||
|
||||
-export([create_calendar/3, get_calendar/2, list_calendars/1,
|
||||
-export([create_calendar/4, get_calendar/2, list_calendars/1,
|
||||
update_calendar/3, delete_calendar/2]).
|
||||
-export([can_access/2, can_edit/2]).
|
||||
|
||||
%% Создание календаря
|
||||
create_calendar(UserId, Title, Description) ->
|
||||
% Проверка, что пользователь может создавать календари
|
||||
create_calendar(UserId, Title, Description, Confirmation) ->
|
||||
case core_user:get_by_id(UserId) of
|
||||
{ok, User} ->
|
||||
case User#user.status of
|
||||
active ->
|
||||
core_calendar:create(UserId, Title, Description);
|
||||
core_calendar:create(UserId, Title, Description, Confirmation);
|
||||
_ ->
|
||||
{error, user_inactive}
|
||||
end;
|
||||
|
||||
Reference in New Issue
Block a user