feat(calendar): single Default personal with guards and backfill
Ensure title Default; reject second personal and delete/type-change of the sole personal; migrate extras to commercial or soft-delete. Refs EventHub/EventHubBack#64
This commit is contained in:
@@ -152,6 +152,8 @@ update_calendar(Req) ->
|
||||
handler_utils:send_error(Req2, 403, <<"Access denied">>);
|
||||
{error, subscription_required} ->
|
||||
handler_utils:send_error(Req2, 402, <<"Subscription required for commercial calendar">>);
|
||||
{error, default_calendar} ->
|
||||
handler_utils:send_error(Req2, 403, <<"default_calendar">>);
|
||||
{error, not_found} ->
|
||||
handler_utils:send_error(Req2, 404, <<"Calendar not found">>);
|
||||
{error, {invalid_settings, Key}} when is_binary(Key) ->
|
||||
@@ -176,6 +178,8 @@ delete_calendar(Req) ->
|
||||
case logic_calendar:delete_calendar(UserId, CalendarId) of
|
||||
{ok, _} ->
|
||||
handler_utils:send_json(Req1, 200, #{status => <<"deleted">>});
|
||||
{error, default_calendar} ->
|
||||
handler_utils:send_error(Req1, 403, <<"default_calendar">>);
|
||||
{error, access_denied} ->
|
||||
handler_utils:send_error(Req1, 403, <<"Access denied">>);
|
||||
{error, not_found} ->
|
||||
|
||||
@@ -140,6 +140,8 @@ create_calendar(Req) ->
|
||||
handler_utils:send_json(Req2, 201, Response);
|
||||
{error, subscription_required} ->
|
||||
handler_utils:send_error(Req2, 402, <<"Subscription required for commercial calendar">>);
|
||||
{error, personal_exists} ->
|
||||
handler_utils:send_error(Req2, 409, <<"personal_exists">>);
|
||||
{error, user_inactive} ->
|
||||
handler_utils:send_error(Req2, 403, <<"User account is not active">>);
|
||||
{error, {content_banned, _Words}} ->
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
-include("records.hrl").
|
||||
|
||||
-export([create_calendar/3, create_calendar/4, create_calendar/5, get_calendar/2, list_calendars/1,
|
||||
update_calendar/3, delete_calendar/2, ensure_default_calendar/1]).
|
||||
update_calendar/3, delete_calendar/2, ensure_default_calendar/1, backfill_single_personal/0]).
|
||||
-export([can_access/2, can_edit/2, booking_open/1]).
|
||||
-export([normalize_settings/1]).
|
||||
-export([admin_list_all/0, admin_get_by_id/1, admin_update/2, admin_delete/1]).
|
||||
|
||||
-define(DEFAULT_PERSONAL_TITLE, <<"Default">>).
|
||||
|
||||
%% Создание календаря с политикой по умолчанию (manual)
|
||||
create_calendar(UserId, Title, Description) ->
|
||||
create_calendar(UserId, Title, Description, manual).
|
||||
@@ -34,7 +36,12 @@ create_calendar(UserId, Title, Description, Confirmation, Type) ->
|
||||
{error, subscription_required}
|
||||
end;
|
||||
personal ->
|
||||
core_calendar:create(UserId, Title2, Desc2, Confirmation, Type)
|
||||
case has_active_personal_calendar(UserId) of
|
||||
true ->
|
||||
{error, personal_exists};
|
||||
false ->
|
||||
core_calendar:create(UserId, Title2, Desc2, Confirmation, Type)
|
||||
end
|
||||
end,
|
||||
case Result of
|
||||
{ok, Cal} ->
|
||||
@@ -51,42 +58,91 @@ create_calendar(UserId, Title, Description, Confirmation, Type) ->
|
||||
{error, user_not_found}
|
||||
end.
|
||||
|
||||
%% @doc Создаёт дефолтный personal-календарь после активации пользователя.
|
||||
%% Идемпотентно: если у владельца уже есть active personal — ok.
|
||||
%% @doc Единственный personal владельца: title Default; лишние → commercial|delete.
|
||||
%% Вызывается после verify; идемпотентно.
|
||||
-spec ensure_default_calendar(UserId :: binary()) -> ok | {error, term()}.
|
||||
ensure_default_calendar(UserId) ->
|
||||
case has_active_personal_calendar(UserId) of
|
||||
true ->
|
||||
ok;
|
||||
false ->
|
||||
normalize_owner_personals(UserId).
|
||||
|
||||
%% @doc Backfill всех владельцев: ≤1 personal с title Default.
|
||||
-spec backfill_single_personal() -> ok.
|
||||
backfill_single_personal() ->
|
||||
Owners = lists:usort([
|
||||
C#calendar.owner_id
|
||||
|| C <- core_calendar:list_all(),
|
||||
C#calendar.status =:= active,
|
||||
C#calendar.type =:= personal
|
||||
]),
|
||||
lists:foreach(fun(OwnerId) -> _ = normalize_owner_personals(OwnerId) end, Owners),
|
||||
ok.
|
||||
|
||||
-spec normalize_owner_personals(UserId :: binary()) -> ok | {error, term()}.
|
||||
normalize_owner_personals(UserId) ->
|
||||
Personals = active_personals(UserId),
|
||||
case Personals of
|
||||
[] ->
|
||||
case core_user:get_by_id(UserId) of
|
||||
{ok, User} ->
|
||||
Title = default_calendar_title(User),
|
||||
case create_calendar(UserId, Title, <<>>, manual, personal) of
|
||||
{ok, #user{status = active}} ->
|
||||
case create_calendar(UserId, ?DEFAULT_PERSONAL_TITLE, <<>>, manual, personal) of
|
||||
{ok, _} -> ok;
|
||||
Error -> Error
|
||||
end;
|
||||
{ok, _} ->
|
||||
{error, user_inactive};
|
||||
Error ->
|
||||
Error
|
||||
end
|
||||
end;
|
||||
[Keep | Rest] ->
|
||||
_ = core_calendar:update(Keep#calendar.id, [{title, ?DEFAULT_PERSONAL_TITLE}]),
|
||||
lists:foreach(fun convert_or_delete_extra_personal/1, Rest),
|
||||
ok
|
||||
end.
|
||||
|
||||
active_personals(UserId) ->
|
||||
case core_calendar:list_by_owner(UserId) of
|
||||
{ok, Calendars} ->
|
||||
lists:sort(
|
||||
fun(#calendar{created_at = A, id = IdA}, #calendar{created_at = B, id = IdB}) ->
|
||||
{A, IdA} =< {B, IdB}
|
||||
end,
|
||||
[C || C <- Calendars, C#calendar.type =:= personal]
|
||||
);
|
||||
_ ->
|
||||
[]
|
||||
end.
|
||||
|
||||
has_active_personal_calendar(UserId) ->
|
||||
case core_calendar:list_by_owner(UserId) of
|
||||
{ok, Calendars} ->
|
||||
lists:any(
|
||||
fun(#calendar{type = personal}) -> true;
|
||||
(_) -> false
|
||||
end,
|
||||
Calendars);
|
||||
_ ->
|
||||
false
|
||||
end.
|
||||
active_personals(UserId) =/= [].
|
||||
|
||||
default_calendar_title(#user{nickname = Nick}) when is_binary(Nick), byte_size(Nick) > 0 ->
|
||||
Nick;
|
||||
default_calendar_title(_) ->
|
||||
<<"Мой календарь">>.
|
||||
convert_or_delete_extra_personal(#calendar{id = Id} = Cal) ->
|
||||
case calendar_has_content(Id) of
|
||||
true ->
|
||||
%% System backfill: bypass subscription gate.
|
||||
_ = core_calendar:update(Id, [{type, commercial}]),
|
||||
ok;
|
||||
false ->
|
||||
_ = core_calendar:delete(Id),
|
||||
ok
|
||||
end,
|
||||
Cal.
|
||||
|
||||
calendar_has_content(CalendarId) ->
|
||||
Events =
|
||||
try
|
||||
case core_event:list_by_calendar(CalendarId) of
|
||||
{ok, Es} -> Es;
|
||||
_ -> []
|
||||
end
|
||||
catch
|
||||
_:_ -> []
|
||||
end,
|
||||
Specs =
|
||||
try
|
||||
core_calendar_specialist:list_by_calendar(CalendarId)
|
||||
catch
|
||||
_:_ -> []
|
||||
end,
|
||||
Events =/= [] orelse Specs =/= [].
|
||||
|
||||
%% Получение календаря с проверкой доступа
|
||||
get_calendar(UserId, CalendarId) ->
|
||||
@@ -155,13 +211,11 @@ apply_calendar_update(CalendarId, Calendar, ValidUpdates) ->
|
||||
end
|
||||
end.
|
||||
|
||||
gate_type_change(UserId, #calendar{type = personal}, Updates) ->
|
||||
%% Единственный personal нельзя превратить в commercial (дневник пользователя).
|
||||
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;
|
||||
{error, default_calendar};
|
||||
_ ->
|
||||
{ok, Updates}
|
||||
end;
|
||||
@@ -200,9 +254,11 @@ apply_text_results(Updates, [], []) -> Updates;
|
||||
apply_text_results(Updates, [F | Fs], [T | Ts]) ->
|
||||
apply_text_results(lists:keystore(F, 1, Updates, {F, T}), Fs, Ts).
|
||||
|
||||
%% Удаление календаря
|
||||
%% Удаление календаря (единственный personal удалять нельзя)
|
||||
delete_calendar(UserId, CalendarId) ->
|
||||
case core_calendar:get_by_id(CalendarId) of
|
||||
{ok, #calendar{type = personal}} ->
|
||||
{error, default_calendar};
|
||||
{ok, Calendar} ->
|
||||
case can_edit(UserId, Calendar) of
|
||||
true ->
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
%% @doc Backfill: ≤1 personal per owner, title Default; extras → commercial | delete.
|
||||
-module('20260730200000_single_default_personal').
|
||||
|
||||
-export([up/0, down/0]).
|
||||
|
||||
up() ->
|
||||
logic_calendar:backfill_single_personal(),
|
||||
ok.
|
||||
|
||||
down() ->
|
||||
ok.
|
||||
Reference in New Issue
Block a user