Implement commercial calendars: booking_open, specialists, expire without type downgrade.
Refs EventHub/EventHubBack#54
This commit is contained in:
+168
-86
@@ -4,39 +4,71 @@
|
||||
cancel_booking/2, cancel_booking/3, get_booking/2,
|
||||
list_bookings/2, list_user_bookings/1, delete_booking/2,
|
||||
list_bookings_admin/0, get_booking_admin/1,
|
||||
list_event_bookings/1, list_event_bookings/2]).
|
||||
list_event_bookings/1, list_event_bookings/2,
|
||||
process_timeout_bookings/0, cancel_pending_for_owner/1,
|
||||
cancel_pending_for_calendar/1]).
|
||||
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Создание бронирования со статусом `pending`.
|
||||
%%% @doc Создание бронирования с учётом commercial / confirmation / capacity.
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-spec create_booking(UserId :: binary(), EventId :: binary()) ->
|
||||
{ok, #booking{}} | {error, full | already_booked | not_found}.
|
||||
{ok, #booking{}} |
|
||||
{error, full | already_booked | not_found | personal_calendar |
|
||||
subscription_inactive | own_event | event_not_active | access_denied}.
|
||||
create_booking(UserId, EventId) ->
|
||||
case core_event:get_by_id(EventId) of
|
||||
{ok, Event} ->
|
||||
case check_capacity(EventId, Event#event.capacity) of
|
||||
{ok, _} ->
|
||||
case core_booking:get_by_event_and_user(EventId, UserId) of
|
||||
{error, not_found} ->
|
||||
core_booking:create(EventId, UserId, pending);
|
||||
{ok, _} ->
|
||||
{error, already_booked}
|
||||
end;
|
||||
{error, full} ->
|
||||
{error, full}
|
||||
{ok, #event{status = active} = Event} ->
|
||||
case core_calendar:get_by_id(Event#event.calendar_id) of
|
||||
{ok, Calendar} ->
|
||||
create_on_calendar(UserId, Event, Calendar);
|
||||
{error, not_found} ->
|
||||
{error, not_found}
|
||||
end;
|
||||
{ok, _} ->
|
||||
{error, event_not_active};
|
||||
{error, not_found} ->
|
||||
{error, not_found}
|
||||
end.
|
||||
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Подтверждение бронирования (двухарная версия).
|
||||
%%% Только владелец календаря события или admin.
|
||||
%%% @end
|
||||
create_on_calendar(UserId, _Event, #calendar{owner_id = UserId}) ->
|
||||
{error, own_event};
|
||||
create_on_calendar(_UserId, _Event, #calendar{type = personal}) ->
|
||||
{error, personal_calendar};
|
||||
create_on_calendar(UserId, Event, #calendar{type = commercial} = Calendar) ->
|
||||
case logic_calendar:booking_open(Calendar) of
|
||||
false ->
|
||||
{error, subscription_inactive};
|
||||
true ->
|
||||
case active_booking(Event#event.id, UserId) of
|
||||
{ok, _} ->
|
||||
{error, already_booked};
|
||||
{error, not_found} ->
|
||||
case check_capacity(Event#event.id, Event#event.capacity) of
|
||||
{ok, _} ->
|
||||
Initial = initial_status(Calendar#calendar.confirmation),
|
||||
case core_booking:create(Event#event.id, UserId, Initial) of
|
||||
{ok, Booking} when Initial =:= confirmed ->
|
||||
Now = calendar:universal_time(),
|
||||
core_booking:update(Booking#booking.id,
|
||||
[{status, confirmed}, {confirmed_at, Now}]);
|
||||
Other ->
|
||||
Other
|
||||
end;
|
||||
{error, full} ->
|
||||
{error, full}
|
||||
end
|
||||
end
|
||||
end;
|
||||
create_on_calendar(_, _, _) ->
|
||||
{error, access_denied}.
|
||||
|
||||
initial_status(auto) -> confirmed;
|
||||
initial_status(_) -> pending.
|
||||
|
||||
%%%-------------------------------------------------------------------
|
||||
-spec confirm_booking(BookingId :: binary(), UserId :: binary()) ->
|
||||
{ok, #booking{}} | {error, not_found | access_denied}.
|
||||
{ok, #booking{}} | {error, not_found | access_denied | full}.
|
||||
confirm_booking(BookingId, UserId) ->
|
||||
case core_booking:get_by_id(BookingId) of
|
||||
{ok, Booking} ->
|
||||
@@ -44,8 +76,13 @@ confirm_booking(BookingId, UserId) ->
|
||||
true ->
|
||||
case Booking#booking.status of
|
||||
pending ->
|
||||
Now = calendar:universal_time(),
|
||||
core_booking:update(BookingId, [{status, confirmed}, {confirmed_at, Now}]);
|
||||
case event_capacity_ok(Booking#booking.event_id) of
|
||||
true ->
|
||||
Now = calendar:universal_time(),
|
||||
core_booking:update(BookingId, [{status, confirmed}, {confirmed_at, Now}]);
|
||||
false ->
|
||||
{error, full}
|
||||
end;
|
||||
_ ->
|
||||
{error, access_denied}
|
||||
end;
|
||||
@@ -55,13 +92,8 @@ confirm_booking(BookingId, UserId) ->
|
||||
Error -> Error
|
||||
end.
|
||||
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Подтверждение или отклонение бронирования (для обработчиков).
|
||||
%%% `decline` переводит pending-заявку в `cancelled`.
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-spec confirm_booking(UserId :: binary(), BookingId :: binary(), confirm | decline) ->
|
||||
{ok, #booking{}} | {error, not_found | access_denied}.
|
||||
{ok, #booking{}} | {error, not_found | access_denied | full}.
|
||||
confirm_booking(UserId, BookingId, confirm) ->
|
||||
confirm_booking(BookingId, UserId);
|
||||
confirm_booking(UserId, BookingId, decline) ->
|
||||
@@ -81,10 +113,6 @@ confirm_booking(UserId, BookingId, decline) ->
|
||||
Error -> Error
|
||||
end.
|
||||
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Отмена бронирования (двухарная версия).
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-spec cancel_booking(BookingId :: binary(), UserId :: binary()) ->
|
||||
{ok, #booking{}} | {error, not_found | access_denied}.
|
||||
cancel_booking(BookingId, UserId) ->
|
||||
@@ -102,19 +130,11 @@ cancel_booking(BookingId, UserId) ->
|
||||
Error -> Error
|
||||
end.
|
||||
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Отмена бронирования (трёхарная версия для обработчиков).
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-spec cancel_booking(UserId :: binary(), BookingId :: binary(), cancel) ->
|
||||
{ok, #booking{}} | {error, not_found | access_denied}.
|
||||
cancel_booking(UserId, BookingId, cancel) ->
|
||||
cancel_booking(BookingId, UserId).
|
||||
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Получение бронирования по ID.
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-spec get_booking(BookingId :: binary(), UserId :: binary()) ->
|
||||
{ok, #booking{}} | {error, not_found | access_denied}.
|
||||
get_booking(BookingId, UserId) ->
|
||||
@@ -122,15 +142,15 @@ get_booking(BookingId, UserId) ->
|
||||
{ok, Booking} ->
|
||||
case Booking#booking.user_id =:= UserId of
|
||||
true -> {ok, Booking};
|
||||
false -> {error, access_denied}
|
||||
false ->
|
||||
case can_manage_event_bookings(UserId, Booking#booking.event_id) of
|
||||
true -> {ok, Booking};
|
||||
{error, _} -> {error, access_denied}
|
||||
end
|
||||
end;
|
||||
Error -> Error
|
||||
end.
|
||||
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Список бронирований события.
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-spec list_bookings(EventId :: binary(), UserId :: binary()) ->
|
||||
{ok, [#booking{}]}.
|
||||
list_bookings(EventId, UserId) ->
|
||||
@@ -141,18 +161,10 @@ list_bookings(EventId, UserId) ->
|
||||
end,
|
||||
{ok, Filtered}.
|
||||
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Список бронирований пользователя.
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-spec list_user_bookings(UserId :: binary()) -> {ok, [#booking{}]}.
|
||||
list_user_bookings(UserId) ->
|
||||
core_booking:list_by_user(UserId).
|
||||
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Удаление бронирования (только владелец).
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-spec delete_booking(BookingId :: binary(), UserId :: binary()) ->
|
||||
ok | {error, not_found | access_denied}.
|
||||
delete_booking(BookingId, UserId) ->
|
||||
@@ -165,28 +177,15 @@ delete_booking(BookingId, UserId) ->
|
||||
Error -> Error
|
||||
end.
|
||||
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Административное получение бронирования.
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-spec get_booking_admin(BookingId :: binary()) ->
|
||||
{ok, #booking{}} | {error, not_found}.
|
||||
get_booking_admin(BookingId) ->
|
||||
core_booking:get_by_id(BookingId).
|
||||
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Список бронирований события (административный / внутренний).
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-spec list_event_bookings(EventId :: binary()) -> {ok, [#booking{}]}.
|
||||
list_event_bookings(EventId) ->
|
||||
core_booking:list_by_event(EventId).
|
||||
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Список бронирований события для владельца календаря (или admin).
|
||||
%%% Возвращает полный список заявок без фильтрации по участнику.
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-spec list_event_bookings(UserId :: binary(), EventId :: binary()) ->
|
||||
{ok, [#booking{}]} | {error, not_found | access_denied}.
|
||||
list_event_bookings(UserId, EventId) ->
|
||||
@@ -197,22 +196,82 @@ list_event_bookings(UserId, EventId) ->
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Список всех бронирований (административный).
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-spec list_bookings_admin() -> {ok, [#booking{}]}.
|
||||
list_bookings_admin() ->
|
||||
{ok, core_booking:list_all()}.
|
||||
|
||||
%%%===================================================================
|
||||
%%% ВНУТРЕННИЕ ФУНКЦИИ
|
||||
%%%===================================================================
|
||||
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Владелец календаря события или admin может управлять заявками.
|
||||
%%% @doc Авто-confirm/cancel по политике {timeout, N}.
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-spec process_timeout_bookings() -> ok.
|
||||
process_timeout_bookings() ->
|
||||
NowSec = calendar:datetime_to_gregorian_seconds(calendar:universal_time()),
|
||||
Pending = mnesia:dirty_match_object(#booking{status = pending, _ = '_'}),
|
||||
lists:foreach(fun(B) -> maybe_timeout(B, NowSec) end, Pending),
|
||||
ok.
|
||||
|
||||
maybe_timeout(#booking{id = Id, event_id = EventId, created_at = Created} = Booking, NowSec) ->
|
||||
case core_event:get_by_id(EventId) of
|
||||
{ok, Event} ->
|
||||
case core_calendar:get_by_id(Event#event.calendar_id) of
|
||||
{ok, #calendar{confirmation = {timeout, N}}} when is_integer(N), N > 0 ->
|
||||
CreatedSec = calendar:datetime_to_gregorian_seconds(Created),
|
||||
case NowSec - CreatedSec >= N of
|
||||
true ->
|
||||
case event_capacity_ok(EventId) of
|
||||
true ->
|
||||
Now = calendar:universal_time(),
|
||||
_ = core_booking:update(Id, [{status, confirmed}, {confirmed_at, Now}]);
|
||||
false ->
|
||||
_ = core_booking:update(Id, [{status, cancelled}])
|
||||
end;
|
||||
false ->
|
||||
ok
|
||||
end;
|
||||
_ ->
|
||||
ok
|
||||
end;
|
||||
_ ->
|
||||
ok
|
||||
end,
|
||||
Booking.
|
||||
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Отмена всех pending владельца (expire подписки).
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-spec cancel_pending_for_owner(UserId :: binary()) -> ok.
|
||||
cancel_pending_for_owner(UserId) ->
|
||||
case core_calendar:list_by_owner(UserId) of
|
||||
{ok, Cals} ->
|
||||
lists:foreach(fun(#calendar{id = Id, type = commercial}) ->
|
||||
cancel_pending_for_calendar(Id);
|
||||
(_) -> ok
|
||||
end, Cals);
|
||||
_ -> ok
|
||||
end.
|
||||
|
||||
-spec cancel_pending_for_calendar(CalendarId :: binary()) -> ok.
|
||||
cancel_pending_for_calendar(CalendarId) ->
|
||||
case core_event:list_by_calendar(CalendarId) of
|
||||
{ok, Events} ->
|
||||
lists:foreach(fun(#event{id = EventId}) ->
|
||||
{ok, Bookings} = core_booking:list_by_event(EventId),
|
||||
lists:foreach(fun
|
||||
(#booking{id = Bid, status = pending}) ->
|
||||
_ = core_booking:update(Bid, [{status, cancelled}]);
|
||||
(_) -> ok
|
||||
end, Bookings)
|
||||
end, Events);
|
||||
_ -> ok
|
||||
end,
|
||||
ok.
|
||||
|
||||
%%%===================================================================
|
||||
%%% INTERNAL
|
||||
%%%===================================================================
|
||||
|
||||
-spec can_manage_event_bookings(UserId :: binary(), EventId :: binary()) ->
|
||||
true | {error, not_found | access_denied}.
|
||||
can_manage_event_bookings(UserId, EventId) ->
|
||||
@@ -220,8 +279,14 @@ can_manage_event_bookings(UserId, EventId) ->
|
||||
{ok, Event} ->
|
||||
case core_calendar:get_by_id(Event#event.calendar_id) of
|
||||
{ok, Calendar} ->
|
||||
case Calendar#calendar.owner_id =:= UserId
|
||||
orelse admin_utils:is_admin(UserId) of
|
||||
OwnerOk = Calendar#calendar.owner_id =:= UserId,
|
||||
AdminOk = admin_utils:is_admin(UserId),
|
||||
SpecOk = is_binary(Event#event.specialist_id)
|
||||
andalso Event#event.specialist_id =/= <<>>
|
||||
andalso Event#event.specialist_id =:= UserId
|
||||
andalso core_calendar_specialist:is_active_specialist(
|
||||
Calendar#calendar.id, UserId),
|
||||
case OwnerOk orelse AdminOk orelse SpecOk of
|
||||
true -> true;
|
||||
false -> {error, access_denied}
|
||||
end;
|
||||
@@ -232,19 +297,36 @@ can_manage_event_bookings(UserId, EventId) ->
|
||||
{error, not_found}
|
||||
end.
|
||||
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Проверка вместимости события.
|
||||
%%% `undefined` и `0` означают неограниченную вместимость.
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-spec check_capacity(EventId :: binary(), Capacity :: integer() | undefined) ->
|
||||
{ok, integer() | unlimited} | {error, full}.
|
||||
check_capacity(_EventId, undefined) -> {ok, unlimited};
|
||||
check_capacity(_EventId, 0) -> {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};
|
||||
case occupied_slots(EventId) < Capacity of
|
||||
true -> {ok, Capacity - occupied_slots(EventId)};
|
||||
false -> {error, full}
|
||||
end.
|
||||
end.
|
||||
|
||||
event_capacity_ok(EventId) ->
|
||||
case core_event:get_by_id(EventId) of
|
||||
{ok, #event{capacity = Cap}} when Cap =:= undefined; Cap =:= 0 ->
|
||||
true;
|
||||
{ok, #event{capacity = Cap}} when is_integer(Cap) ->
|
||||
%% при confirm текущий pending уже в occupied — слот свой, ок если <= Cap
|
||||
occupied_slots(EventId) =< Cap;
|
||||
_ ->
|
||||
false
|
||||
end.
|
||||
|
||||
occupied_slots(EventId) ->
|
||||
{ok, Bookings} = core_booking:list_by_event(EventId),
|
||||
length([B || B <- Bookings,
|
||||
B#booking.status =:= pending orelse B#booking.status =:= confirmed]).
|
||||
|
||||
active_booking(EventId, UserId) ->
|
||||
Match = #booking{event_id = EventId, user_id = UserId, _ = '_'},
|
||||
case [B || B <- mnesia:dirty_match_object(Match),
|
||||
B#booking.status =:= pending orelse B#booking.status =:= confirmed] of
|
||||
[B | _] -> {ok, B};
|
||||
[] -> {error, not_found}
|
||||
end.
|
||||
|
||||
Regular → Executable
+84
-20
@@ -3,7 +3,7 @@
|
||||
|
||||
-export([create_calendar/3, create_calendar/4, get_calendar/2, list_calendars/1,
|
||||
update_calendar/3, delete_calendar/2, ensure_default_calendar/1]).
|
||||
-export([can_access/2, can_edit/2]).
|
||||
-export([can_access/2, can_edit/2, booking_open/1]).
|
||||
-export([admin_list_all/0, admin_get_by_id/1, admin_update/2, admin_delete/1]).
|
||||
|
||||
%% Создание календаря с политикой по умолчанию (manual)
|
||||
@@ -110,23 +110,11 @@ update_calendar(UserId, CalendarId, Updates) ->
|
||||
case can_edit(UserId, Calendar) of
|
||||
true ->
|
||||
ValidUpdates = validate_updates(Updates),
|
||||
case content_fields(ValidUpdates, [title, description]) of
|
||||
{[], []} ->
|
||||
core_calendar:update(CalendarId, ValidUpdates);
|
||||
{Fields, Texts} ->
|
||||
case logic_automoderation:evaluate_texts(Texts) of
|
||||
{reject, Words} ->
|
||||
{error, {content_banned, Words}};
|
||||
{ok, Action, OutTexts, Words} ->
|
||||
FinalUpdates = apply_text_results(ValidUpdates, Fields, OutTexts),
|
||||
case core_calendar:update(CalendarId, FinalUpdates) of
|
||||
{ok, _Updated} ->
|
||||
logic_automoderation:apply_after_save(calendar, CalendarId, Action, Words),
|
||||
core_calendar:get_by_id(CalendarId);
|
||||
Error ->
|
||||
Error
|
||||
end
|
||||
end
|
||||
case gate_type_change(UserId, Calendar, ValidUpdates) of
|
||||
{error, _} = Err ->
|
||||
Err;
|
||||
{ok, ValidUpdates2} ->
|
||||
apply_calendar_update(CalendarId, Calendar, ValidUpdates2)
|
||||
end;
|
||||
false ->
|
||||
{error, access_denied}
|
||||
@@ -135,6 +123,66 @@ update_calendar(UserId, CalendarId, Updates) ->
|
||||
Error
|
||||
end.
|
||||
|
||||
apply_calendar_update(CalendarId, Calendar, ValidUpdates) ->
|
||||
case content_fields(ValidUpdates, [title, description]) of
|
||||
{[], []} ->
|
||||
case core_calendar:update(CalendarId, ValidUpdates) of
|
||||
{ok, Updated} = Ok ->
|
||||
maybe_cancel_pending_on_personal(Calendar, Updated),
|
||||
Ok;
|
||||
Error ->
|
||||
Error
|
||||
end;
|
||||
{Fields, Texts} ->
|
||||
case logic_automoderation:evaluate_texts(Texts) of
|
||||
{reject, Words} ->
|
||||
{error, {content_banned, Words}};
|
||||
{ok, Action, OutTexts, Words} ->
|
||||
FinalUpdates = apply_text_results(ValidUpdates, Fields, OutTexts),
|
||||
case core_calendar:update(CalendarId, FinalUpdates) of
|
||||
{ok, Updated} ->
|
||||
logic_automoderation:apply_after_save(calendar, CalendarId, Action, Words),
|
||||
maybe_cancel_pending_on_personal(Calendar, Updated),
|
||||
core_calendar:get_by_id(CalendarId);
|
||||
Error ->
|
||||
Error
|
||||
end
|
||||
end
|
||||
end.
|
||||
|
||||
gate_type_change(UserId, #calendar{type = personal}, Updates) ->
|
||||
case lists:keyfind(type, 1, Updates) of
|
||||
{type, commercial} ->
|
||||
case logic_subscription:can_create_commercial_calendar(UserId) of
|
||||
true -> {ok, Updates};
|
||||
false -> {error, subscription_required}
|
||||
end;
|
||||
_ ->
|
||||
{ok, Updates}
|
||||
end;
|
||||
gate_type_change(_UserId, _Calendar, Updates) ->
|
||||
{ok, Updates}.
|
||||
|
||||
maybe_cancel_pending_on_personal(#calendar{type = commercial},
|
||||
#calendar{type = personal, id = Id}) ->
|
||||
logic_booking:cancel_pending_for_calendar(Id);
|
||||
maybe_cancel_pending_on_personal(_, _) ->
|
||||
ok.
|
||||
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Можно ли принимать новые booking на календарь.
|
||||
%%% commercial + active + active subscription владельца.
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-spec booking_open(#calendar{}) -> boolean().
|
||||
booking_open(#calendar{type = commercial, status = active, owner_id = OwnerId}) ->
|
||||
case logic_subscription:check_user_subscription(OwnerId) of
|
||||
{ok, active, _} -> true;
|
||||
_ -> false
|
||||
end;
|
||||
booking_open(_) ->
|
||||
false.
|
||||
|
||||
content_fields(Updates, Fields) ->
|
||||
lists:foldl(fun(F, {Fs, Ts}) ->
|
||||
case lists:keyfind(F, 1, Updates) of
|
||||
@@ -179,12 +227,28 @@ can_edit(_, _) ->
|
||||
|
||||
%% Валидация полей обновления
|
||||
validate_updates(Updates) ->
|
||||
lists:filter(fun validate_update/1, Updates).
|
||||
lists:filtermap(fun(U) ->
|
||||
case normalize_update(U) of
|
||||
false -> false;
|
||||
Norm ->
|
||||
case validate_update(Norm) of
|
||||
true -> {true, Norm};
|
||||
false -> false
|
||||
end
|
||||
end
|
||||
end, Updates).
|
||||
|
||||
normalize_update({type, <<"personal">>}) -> {type, personal};
|
||||
normalize_update({type, <<"commercial">>}) -> {type, commercial};
|
||||
normalize_update({type, personal}) -> {type, personal};
|
||||
normalize_update({type, commercial}) -> {type, commercial};
|
||||
normalize_update(Other) -> Other.
|
||||
|
||||
validate_update({title, Value}) when is_binary(Value) -> true;
|
||||
validate_update({description, Value}) when is_binary(Value) -> true;
|
||||
validate_update({tags, Value}) when is_list(Value) -> true;
|
||||
validate_update({type, Value}) when Value =:= personal; Value =:= commercial -> true;
|
||||
validate_update({type, personal}) -> true;
|
||||
validate_update({type, commercial}) -> true;
|
||||
validate_update({confirmation, Value}) ->
|
||||
case Value of
|
||||
auto -> true;
|
||||
|
||||
Executable
+84
@@ -0,0 +1,84 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Логика специалистов commercial-календаря.
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(logic_calendar_specialist).
|
||||
-include("records.hrl").
|
||||
|
||||
-export([list/2, add/5, update/4, remove/3, to_json/1]).
|
||||
|
||||
-spec list(ActorId :: binary(), CalendarId :: binary()) ->
|
||||
{ok, [#calendar_specialist{}]} | {error, not_found | access_denied}.
|
||||
list(ActorId, CalendarId) ->
|
||||
case core_calendar:get_by_id(CalendarId) of
|
||||
{ok, Cal} ->
|
||||
case logic_calendar:can_access(ActorId, Cal) of
|
||||
true -> {ok, core_calendar_specialist:list_by_calendar(CalendarId)};
|
||||
false -> {error, access_denied}
|
||||
end;
|
||||
Error -> Error
|
||||
end.
|
||||
|
||||
-spec add(OwnerId :: binary(), CalendarId :: binary(), UserId :: binary(),
|
||||
Name :: binary(), Specs :: [binary()]) ->
|
||||
{ok, #calendar_specialist{}} |
|
||||
{error, not_found | access_denied | not_commercial | user_not_found |
|
||||
already_exists | term()}.
|
||||
add(OwnerId, CalendarId, UserId, Name, Specs) ->
|
||||
case require_owner_commercial(OwnerId, CalendarId) of
|
||||
{ok, _} ->
|
||||
case core_user:get_by_id(UserId) of
|
||||
{ok, _} ->
|
||||
core_calendar_specialist:create(CalendarId, UserId, Name, Specs);
|
||||
{error, _} ->
|
||||
{error, user_not_found}
|
||||
end;
|
||||
Error -> Error
|
||||
end.
|
||||
|
||||
-spec update(OwnerId :: binary(), CalendarId :: binary(), UserId :: binary(),
|
||||
Updates :: [{atom(), term()}]) ->
|
||||
{ok, #calendar_specialist{}} | {error, not_found | access_denied | not_commercial | term()}.
|
||||
update(OwnerId, CalendarId, UserId, Updates) ->
|
||||
case require_owner_commercial(OwnerId, CalendarId) of
|
||||
{ok, _} ->
|
||||
core_calendar_specialist:update(CalendarId, UserId, Updates);
|
||||
Error -> Error
|
||||
end.
|
||||
|
||||
-spec remove(OwnerId :: binary(), CalendarId :: binary(), UserId :: binary()) ->
|
||||
ok | {error, not_found | access_denied | not_commercial | term()}.
|
||||
remove(OwnerId, CalendarId, UserId) ->
|
||||
case require_owner_commercial(OwnerId, CalendarId) of
|
||||
{ok, _} ->
|
||||
core_calendar_specialist:delete(CalendarId, UserId);
|
||||
Error -> Error
|
||||
end.
|
||||
|
||||
-spec to_json(#calendar_specialist{}) -> map().
|
||||
to_json(S) ->
|
||||
#{
|
||||
id => S#calendar_specialist.id,
|
||||
calendar_id => S#calendar_specialist.calendar_id,
|
||||
user_id => S#calendar_specialist.user_id,
|
||||
name => S#calendar_specialist.name,
|
||||
specialization => S#calendar_specialist.specialization,
|
||||
status => S#calendar_specialist.status,
|
||||
added_at => handler_utils:datetime_to_iso8601(S#calendar_specialist.added_at),
|
||||
updated_at => handler_utils:datetime_to_iso8601(S#calendar_specialist.updated_at)
|
||||
}.
|
||||
|
||||
%%%===================================================================
|
||||
%%% Internal
|
||||
%%%===================================================================
|
||||
|
||||
require_owner_commercial(OwnerId, CalendarId) ->
|
||||
case core_calendar:get_by_id(CalendarId) of
|
||||
{ok, #calendar{owner_id = OwnerId, type = commercial, status = active} = Cal} ->
|
||||
{ok, Cal};
|
||||
{ok, #calendar{owner_id = OwnerId, type = personal}} ->
|
||||
{error, not_commercial};
|
||||
{ok, _} ->
|
||||
{error, access_denied};
|
||||
Error -> Error
|
||||
end.
|
||||
Regular → Executable
+52
-26
@@ -176,33 +176,38 @@ update_event(UserId, EventId, Updates) ->
|
||||
{ok, Calendar} ->
|
||||
case logic_calendar:can_edit(UserId, Calendar) of
|
||||
true ->
|
||||
ValidUpdates = validate_updates(Updates, UserId),
|
||||
Title = proplists:get_value(title, ValidUpdates, Event#event.title),
|
||||
Desc = proplists:get_value(description, ValidUpdates, Event#event.description),
|
||||
case logic_automoderation:evaluate_texts([Title, Desc]) of
|
||||
{reject, Words} ->
|
||||
{error, {content_banned, Words}};
|
||||
{ok, Action, [Title2, Desc2], Words} ->
|
||||
Final0 = case lists:keymember(title, 1, ValidUpdates) of
|
||||
true -> lists:keystore(title, 1, ValidUpdates, {title, Title2});
|
||||
false -> ValidUpdates
|
||||
end,
|
||||
Final = case lists:keymember(description, 1, Final0) orelse Desc2 =/= Event#event.description of
|
||||
true when Action =:= censor ->
|
||||
lists:keystore(description, 1, Final0, {description, Desc2});
|
||||
true ->
|
||||
case lists:keymember(description, 1, Final0) of
|
||||
true -> lists:keystore(description, 1, Final0, {description, Desc2});
|
||||
case validate_specialist_update(Calendar, Updates) of
|
||||
{error, _} = E ->
|
||||
E;
|
||||
ok ->
|
||||
ValidUpdates = validate_updates(Updates, UserId),
|
||||
Title = proplists:get_value(title, ValidUpdates, Event#event.title),
|
||||
Desc = proplists:get_value(description, ValidUpdates, Event#event.description),
|
||||
case logic_automoderation:evaluate_texts([Title, Desc]) of
|
||||
{reject, Words} ->
|
||||
{error, {content_banned, Words}};
|
||||
{ok, Action, [Title2, Desc2], Words} ->
|
||||
Final0 = case lists:keymember(title, 1, ValidUpdates) of
|
||||
true -> lists:keystore(title, 1, ValidUpdates, {title, Title2});
|
||||
false -> ValidUpdates
|
||||
end,
|
||||
Final = case lists:keymember(description, 1, Final0) orelse Desc2 =/= Event#event.description of
|
||||
true when Action =:= censor ->
|
||||
lists:keystore(description, 1, Final0, {description, Desc2});
|
||||
true ->
|
||||
case lists:keymember(description, 1, Final0) of
|
||||
true -> lists:keystore(description, 1, Final0, {description, Desc2});
|
||||
false -> Final0
|
||||
end;
|
||||
false -> Final0
|
||||
end;
|
||||
false -> Final0
|
||||
end,
|
||||
case core_event:update(EventId, Final) of
|
||||
{ok, _} ->
|
||||
logic_automoderation:apply_after_save(event, EventId, Action, Words),
|
||||
core_event:get_by_id(EventId);
|
||||
Error ->
|
||||
Error
|
||||
end,
|
||||
case core_event:update(EventId, Final) of
|
||||
{ok, _} ->
|
||||
logic_automoderation:apply_after_save(event, EventId, Action, Words),
|
||||
core_event:get_by_id(EventId);
|
||||
Error ->
|
||||
Error
|
||||
end
|
||||
end
|
||||
end;
|
||||
false ->
|
||||
@@ -378,6 +383,8 @@ validate_update({start_time, Value}, UserId) ->
|
||||
end;
|
||||
validate_update({duration, Value}, _) when is_integer(Value), Value > 0 -> true;
|
||||
validate_update({specialist_id, Value}, _) when is_binary(Value) -> true;
|
||||
validate_update({specialist_id, null}, _) -> true;
|
||||
validate_update({specialist_id, undefined}, _) -> true;
|
||||
validate_update({location, Value}, _) ->
|
||||
case Value of
|
||||
#location{} -> true;
|
||||
@@ -389,6 +396,25 @@ validate_update({online_link, Value}, _) when is_binary(Value) -> true;
|
||||
validate_update({status, Value}, _) when is_atom(Value) -> true;
|
||||
validate_update(_, _) -> false.
|
||||
|
||||
validate_specialist_update(Calendar, Updates) ->
|
||||
case lists:keyfind(specialist_id, 1, Updates) of
|
||||
false -> ok;
|
||||
{specialist_id, null} -> ok;
|
||||
{specialist_id, undefined} -> ok;
|
||||
{specialist_id, <<>>} -> ok;
|
||||
{specialist_id, SpecId} when is_binary(SpecId) ->
|
||||
case Calendar#calendar.type of
|
||||
commercial ->
|
||||
case core_calendar_specialist:is_active_specialist(Calendar#calendar.id, SpecId) of
|
||||
true -> ok;
|
||||
false -> {error, invalid_specialist}
|
||||
end;
|
||||
_ ->
|
||||
{error, invalid_specialist}
|
||||
end;
|
||||
_ -> ok
|
||||
end.
|
||||
|
||||
get_exceptions(MasterId) ->
|
||||
Match = #recurrence_exception{master_id = MasterId, _ = '_'},
|
||||
mnesia:dirty_match_object(Match).
|
||||
|
||||
+13
-11
@@ -163,23 +163,24 @@ filter_accessible_events(Events, UserId) ->
|
||||
lists:filter(fun(Event) ->
|
||||
case core_calendar:get_by_id(Event#event.calendar_id) of
|
||||
{ok, Calendar} ->
|
||||
CanAccess = logic_calendar:can_access(UserId, Calendar),
|
||||
case CanAccess of
|
||||
false ->
|
||||
%% io:format("Access denied for user ~p to calendar ~p (type: ~p, owner: ~p, status: ~p)~n",
|
||||
%% [UserId, Calendar#calendar.id, Calendar#calendar.type,
|
||||
%% Calendar#calendar.owner_id, Calendar#calendar.status]);
|
||||
false;
|
||||
true -> ok
|
||||
end,
|
||||
CanAccess;
|
||||
logic_calendar:can_access(UserId, Calendar)
|
||||
andalso calendar_discoverable(Calendar);
|
||||
_ -> false
|
||||
end
|
||||
end, Events).
|
||||
|
||||
-spec filter_accessible_calendars([#calendar{}], binary()) -> [#calendar{}].
|
||||
filter_accessible_calendars(Calendars, UserId) ->
|
||||
lists:filter(fun(Calendar) -> logic_calendar:can_access(UserId, Calendar) end, Calendars).
|
||||
lists:filter(fun(Calendar) ->
|
||||
logic_calendar:can_access(UserId, Calendar)
|
||||
andalso calendar_discoverable(Calendar)
|
||||
end, Calendars).
|
||||
|
||||
%% Restricted commercial не показываем в search/discovery (deep-link остаётся).
|
||||
calendar_discoverable(#calendar{type = commercial} = C) ->
|
||||
logic_calendar:booking_open(C);
|
||||
calendar_discoverable(_) ->
|
||||
true.
|
||||
|
||||
%% ============ Применение фильтров ============
|
||||
|
||||
@@ -357,6 +358,7 @@ format_calendar(Calendar) ->
|
||||
title => Calendar#calendar.title,
|
||||
description => Calendar#calendar.description,
|
||||
type => Calendar#calendar.type,
|
||||
booking_open => logic_calendar:booking_open(Calendar),
|
||||
tags => Calendar#calendar.tags,
|
||||
rating_avg => Calendar#calendar.rating_avg,
|
||||
rating_count => Calendar#calendar.rating_count,
|
||||
|
||||
Reference in New Issue
Block a user