%%%------------------------------------------------------------------- %%% @doc Приглашения специалистов commercial-календаря (Spec §2.1.2). %%% @end %%%------------------------------------------------------------------- -module(logic_specialist_invite). -include("records.hrl"). -export([ create/4, list_outgoing/2, cancel/3, list_incoming/1, accept/2, decline/2, accept_by_token/2, to_json/1 ]). -define(INVITE_TTL_DAYS, 7). -spec create(OwnerId :: binary(), CalendarId :: binary(), Target :: map(), Opts :: map()) -> {ok, #specialist_invite{}} | {error, not_found | access_denied | not_commercial | subscription_inactive | user_not_found | already_specialist | already_pending | bad_request | term()}. create(OwnerId, CalendarId, Target, Opts) -> case require_owner_can_invite(OwnerId, CalendarId) of {ok, _Cal} -> case resolve_target(Target) of {error, _} = E -> E; {ok, UserId, Email} -> case already_active_specialist(CalendarId, UserId) of true -> {error, already_specialist}; false -> case core_specialist_invite:find_pending(CalendarId, UserId, Email) of [_ | _] -> {error, already_pending}; [] -> do_create(OwnerId, CalendarId, UserId, Email, Opts) end end end; Error -> Error end. -spec list_outgoing(OwnerId :: binary(), CalendarId :: binary()) -> {ok, [#specialist_invite{}]} | {error, not_found | access_denied | not_commercial}. list_outgoing(OwnerId, CalendarId) -> case require_owner_commercial(OwnerId, CalendarId) of {ok, _} -> List = [maybe_expire(I) || I <- core_specialist_invite:list_by_calendar(CalendarId)], {ok, List}; Error -> Error end. -spec cancel(OwnerId :: binary(), CalendarId :: binary(), InviteId :: binary()) -> {ok, #specialist_invite{}} | {error, not_found | access_denied | not_commercial | not_pending | term()}. cancel(OwnerId, CalendarId, InviteId) -> case require_owner_commercial(OwnerId, CalendarId) of {ok, _} -> case core_specialist_invite:get_by_id(InviteId) of {ok, #specialist_invite{calendar_id = CalendarId, status = pending} = Inv} -> Inv2 = maybe_expire(Inv), case Inv2#specialist_invite.status of pending -> core_specialist_invite:update_status(InviteId, cancelled); _ -> {error, not_pending} end; {ok, #specialist_invite{calendar_id = CalendarId}} -> {error, not_pending}; {ok, _} -> {error, not_found}; {error, _} = E -> E end; Error -> Error end. -spec list_incoming(UserId :: binary()) -> {ok, [#specialist_invite{}]}. list_incoming(UserId) -> case core_user:get_by_id(UserId) of {ok, #user{email = Email}} -> ByUser = core_specialist_invite:list_by_invitee(UserId), ByEmail = case Email of <<>> -> []; _ -> core_specialist_invite:list_by_email(Email) end, Merged = lists:ukeysort(1, [{I#specialist_invite.id, maybe_expire(I)} || I <- ByUser ++ ByEmail]), {ok, [I || {_, I} <- Merged]}; {error, _} -> {ok, [maybe_expire(I) || I <- core_specialist_invite:list_by_invitee(UserId)]} end. -spec accept(UserId :: binary(), InviteId :: binary()) -> {ok, #specialist_invite{}, #calendar_specialist{}} | {error, not_found | access_denied | not_pending | expired | term()}. accept(UserId, InviteId) -> case core_specialist_invite:get_by_id(InviteId) of {ok, Inv0} -> Inv = maybe_expire(Inv0), case Inv#specialist_invite.status of pending -> case can_accept(UserId, Inv) of true -> finalize_accept(UserId, Inv); false -> {error, access_denied} end; expired -> {error, expired}; _ -> {error, not_pending} end; {error, _} = E -> E end. -spec decline(UserId :: binary(), InviteId :: binary()) -> {ok, #specialist_invite{}} | {error, not_found | access_denied | not_pending | expired | term()}. decline(UserId, InviteId) -> case core_specialist_invite:get_by_id(InviteId) of {ok, Inv0} -> Inv = maybe_expire(Inv0), case Inv#specialist_invite.status of pending -> case can_accept(UserId, Inv) of true -> core_specialist_invite:update_status(InviteId, declined); false -> {error, access_denied} end; expired -> {error, expired}; _ -> {error, not_pending} end; {error, _} = E -> E end. -spec accept_by_token(UserId :: binary(), Token :: binary()) -> {ok, #specialist_invite{}, #calendar_specialist{}} | {error, not_found | access_denied | not_pending | expired | term()}. accept_by_token(UserId, Token) -> case core_specialist_invite:get_by_token(Token) of {ok, #specialist_invite{id = Id}} -> accept(UserId, Id); {error, _} = E -> E end. -spec to_json(#specialist_invite{}) -> map(). to_json(I) -> #{ id => I#specialist_invite.id, calendar_id => I#specialist_invite.calendar_id, inviter_id => I#specialist_invite.inviter_id, invitee_user_id => null_if_empty(I#specialist_invite.invitee_user_id), invitee_email => null_if_empty(I#specialist_invite.invitee_email), name => I#specialist_invite.name, specialization => I#specialist_invite.specialization, status => I#specialist_invite.status, created_at => handler_utils:datetime_to_iso8601(I#specialist_invite.created_at), expires_at => handler_utils:datetime_to_iso8601(I#specialist_invite.expires_at) }. %%%=================================================================== %%% Internal %%%=================================================================== do_create(OwnerId, CalendarId, UserId, Email, Opts) -> Now = calendar:universal_time(), Expires = add_days(Now, ?INVITE_TTL_DAYS), Name = maps:get(name, Opts, <<>>), Specs = maps:get(specialization, Opts, []), Rec = #specialist_invite{ id = infra_utils:generate_id(16), calendar_id = CalendarId, inviter_id = OwnerId, invitee_user_id = UserId, invitee_email = Email, name = Name, specialization = Specs, status = pending, token = infra_utils:generate_id(32), created_at = Now, expires_at = Expires }, case core_specialist_invite:create(Rec) of {ok, Created} -> notify_invite(Created), {ok, Created}; Error -> Error end. notify_invite(#specialist_invite{invitee_user_id = UserId, calendar_id = CalId, token = Token} = Inv) when UserId =/= <<>> -> Title = <<"Приглашение стать специалистом">>, Body = <<"Вас пригласили в календарь ", CalId/binary>>, _ = core_notification:create(UserId, specialist_invite, Title, Body), logic_notification:notify_specialist_invite(UserId, Inv), maybe_email(Inv, Token); notify_invite(#specialist_invite{token = Token} = Inv) -> maybe_email(Inv, Token). maybe_email(#specialist_invite{invitee_email = Email, token = Token}, _) when Email =/= <<>> -> logic_email:send_specialist_invite(Email, Token); maybe_email(#specialist_invite{invitee_user_id = UserId, token = Token}, _) when UserId =/= <<>> -> case core_user:get_by_id(UserId) of {ok, #user{email = Email}} when Email =/= <<>> -> logic_email:send_specialist_invite(Email, Token); _ -> ok end; maybe_email(_, _) -> ok. finalize_accept(UserId, #specialist_invite{ id = InviteId, calendar_id = CalendarId, name = Name, specialization = Specs, invitee_user_id = PrevUser}) -> SpecRes = case core_calendar_specialist:create(CalendarId, UserId, Name, Specs) of {ok, S} -> {ok, S}; {error, already_exists} -> core_calendar_specialist:update(CalendarId, UserId, [{status, active}, {name, Name}, {specialization, Specs}]) end, case SpecRes of {ok, Spec} -> case core_specialist_invite:update_status(InviteId, accepted) of {ok, Inv2} -> Inv3 = case PrevUser of <<>> -> Filled = Inv2#specialist_invite{invitee_user_id = UserId}, ok = mnesia:dirty_write(Filled), Filled; _ -> Inv2 end, {ok, Inv3, Spec}; Error -> Error end; Error -> Error end. can_accept(UserId, #specialist_invite{invitee_user_id = UserId}) when UserId =/= <<>> -> true; can_accept(UserId, #specialist_invite{invitee_user_id = <<>>, invitee_email = Email}) when Email =/= <<>> -> case core_user:get_by_id(UserId) of {ok, #user{email = Email}} -> true; _ -> false end; can_accept(_, _) -> false. already_active_specialist(<<>>, _) -> false; already_active_specialist(CalendarId, UserId) when UserId =/= <<>> -> core_calendar_specialist:is_active_specialist(CalendarId, UserId); already_active_specialist(_, _) -> false. resolve_target(#{user_id := UserId}) when is_binary(UserId), UserId =/= <<>> -> case core_user:get_by_id(UserId) of {ok, #user{status = active, email = Email}} -> {ok, UserId, Email}; {ok, #user{status = _}} -> {error, user_not_found}; {error, _} -> {error, user_not_found} end; resolve_target(#{email := Email0}) when is_binary(Email0), Email0 =/= <<>> -> Email = string:trim(Email0), case find_user_by_email(Email) of {ok, #user{id = UserId, status = active}} -> {ok, UserId, Email}; {ok, #user{status = _}} -> {ok, <<>>, Email}; {error, not_found} -> {ok, <<>>, Email} end; resolve_target(_) -> {error, bad_request}. find_user_by_email(Email) -> case core_user:get_by_email(Email) of {ok, _} = Ok -> Ok; {error, not_found} -> Lower = string:lowercase(Email), case Lower =:= Email of true -> {error, not_found}; false -> core_user:get_by_email(Lower) end end. require_owner_can_invite(OwnerId, CalendarId) -> case require_owner_commercial(OwnerId, CalendarId) of {ok, Cal} -> case logic_calendar:booking_open(Cal) of true -> {ok, Cal}; false -> {error, subscription_inactive} end; Error -> Error end. 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. maybe_expire(#specialist_invite{status = pending, expires_at = Exp, id = Id} = Inv) -> case Exp < calendar:universal_time() of true -> _ = core_specialist_invite:update_status(Id, expired), Inv#specialist_invite{status = expired}; false -> Inv end; maybe_expire(Inv) -> Inv. add_days(DateTime, Days) -> Sec = calendar:datetime_to_gregorian_seconds(DateTime) + Days * 86400, calendar:gregorian_seconds_to_datetime(Sec). null_if_empty(<<>>) -> null; null_if_empty(V) -> V.