feat(specialists): owner is non-deletable specialist on commercial create
CI / test (push) Successful in 8m35s
CI / deploy-ift (push) Successful in 21m2s
CI / e2e-ift (push) Successful in 2m3s
CI / deploy-stage (push) Successful in 2m9s
CI / e2e-stage (push) Successful in 1m16s

Bootstrap calendar_specialist for owner (active); DELETE owner → 403;
toggle via status. Fixes solo assign-to-slot. Refs EventHub/EventHubBack#66
This commit is contained in:
2026-08-01 12:18:36 +03:00
parent ba1649f344
commit 0e4e886cd4
6 changed files with 149 additions and 6 deletions
@@ -187,6 +187,8 @@ remove_specialist(Req) ->
handler_utils:send_json(Req1, 200, #{status => <<"deleted">>});
{error, not_found} ->
handler_utils:send_error(Req1, 404, <<"Not found">>);
{error, owner_specialist_protected} ->
handler_utils:send_error(Req1, 403, <<"Owner specialist cannot be removed">>);
{error, access_denied} ->
handler_utils:send_error(Req1, 403, <<"Access denied">>);
{error, not_commercial} ->
+18 -2
View File
@@ -46,7 +46,18 @@ create_calendar(UserId, Title, Description, Confirmation, Type) ->
case Result of
{ok, Cal} ->
logic_automoderation:apply_after_save(calendar, Cal#calendar.id, Action, Words),
core_calendar:get_by_id(Cal#calendar.id);
case core_calendar:get_by_id(Cal#calendar.id) of
{ok, Cal2} = Ok ->
case Cal2#calendar.type of
commercial ->
_ = logic_calendar_specialist:ensure_owner_specialist(Cal2);
_ ->
ok
end,
Ok;
Error ->
Error
end;
Error ->
Error
end
@@ -118,7 +129,12 @@ 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}]),
case core_calendar:update(Id, [{type, commercial}]) of
{ok, Updated} ->
_ = logic_calendar_specialist:ensure_owner_specialist(Updated);
_ ->
ok
end,
ok;
false ->
_ = core_calendar:delete(Id),
+32 -2
View File
@@ -5,7 +5,7 @@
-module(logic_calendar_specialist).
-include("records.hrl").
-export([list/2, add/5, update/4, remove/3, to_json/1]).
-export([list/2, add/5, update/4, remove/3, to_json/1, ensure_owner_specialist/1]).
-spec list(ActorId :: binary(), CalendarId :: binary()) ->
{ok, [#calendar_specialist{}]} | {error, not_found | access_denied}.
@@ -47,14 +47,34 @@ update(OwnerId, CalendarId, UserId, Updates) ->
end.
-spec remove(OwnerId :: binary(), CalendarId :: binary(), UserId :: binary()) ->
ok | {error, not_found | access_denied | not_commercial | term()}.
ok | {error, not_found | access_denied | not_commercial |
owner_specialist_protected | term()}.
remove(OwnerId, CalendarId, UserId) ->
case require_owner_commercial(OwnerId, CalendarId) of
{ok, #calendar{owner_id = CalOwnerId}} when UserId =:= CalOwnerId ->
{error, owner_specialist_protected};
{ok, _} ->
core_calendar_specialist:delete(CalendarId, UserId);
Error -> Error
end.
%% @doc Idempotent: commercial calendar always has a specialist row for owner.
%% Default status=active; name from nickname (else email). No-op if already present.
-spec ensure_owner_specialist(#calendar{}) -> ok | {error, term()}.
ensure_owner_specialist(#calendar{id = CalId, owner_id = OwnerId, type = commercial}) ->
case core_calendar_specialist:get_by_calendar_and_user(CalId, OwnerId) of
{ok, _} ->
ok;
{error, not_found} ->
case core_calendar_specialist:create(CalId, OwnerId, owner_display_name(OwnerId), []) of
{ok, _} -> ok;
{error, already_exists} -> ok;
{error, _} = Err -> Err
end
end;
ensure_owner_specialist(_) ->
ok.
-spec to_json(#calendar_specialist{}) -> map().
to_json(S) ->
#{
@@ -82,3 +102,13 @@ require_owner_commercial(OwnerId, CalendarId) ->
{error, access_denied};
Error -> Error
end.
owner_display_name(OwnerId) ->
case core_user:get_by_id(OwnerId) of
{ok, #user{nickname = Nick}} when is_binary(Nick), Nick =/= <<>> ->
Nick;
{ok, #user{email = Email}} when is_binary(Email), Email =/= <<>> ->
Email;
_ ->
<<"Owner">>
end.