feat(email): send booking reminders before event start.

Job process_reminders in subscription_worker; REMINDER_LEAD_HOURS window;
CalenTIQ email + in-app event_reminder; idempotent reminder_sent.
Refs EventHub/EventHubBack#70
This commit is contained in:
2026-08-14 20:59:38 +03:00
parent 347a645933
commit 15ae8d4426
9 changed files with 245 additions and 6 deletions
+79 -1
View File
@@ -7,7 +7,7 @@
delete_booking/2,
list_bookings_admin/0, get_booking_admin/1,
list_event_bookings/1, list_event_bookings/2,
process_timeout_bookings/0, cancel_pending_for_owner/1,
process_timeout_bookings/0, process_reminders/0, cancel_pending_for_owner/1,
cancel_pending_for_calendar/1]).
%%%-------------------------------------------------------------------
@@ -367,6 +367,84 @@ process_timeout_bookings() ->
lists:foreach(fun(B) -> maybe_timeout(B, NowSec) end, Pending),
ok.
%%%-------------------------------------------------------------------
%%% @doc Email + in-app reminder before event start (Back#70).
%%% Confirmed bookings with reminder_sent=false whose event starts within
%%% REMINDER_LEAD_HOURS (default 24). Flag set before send (once).
%%% @end
%%%-------------------------------------------------------------------
-spec process_reminders() -> ok.
process_reminders() ->
NowSec = calendar:datetime_to_gregorian_seconds(calendar:universal_time()),
LeadSec = reminder_lead_hours() * 3600,
Horizon = NowSec + LeadSec,
Candidates = mnesia:dirty_match_object(
#booking{status = confirmed, reminder_sent = false, _ = '_'}),
lists:foreach(fun(B) -> maybe_remind(B, NowSec, Horizon) end, Candidates),
ok.
maybe_remind(#booking{id = Id, event_id = EventId, user_id = UserId} = Booking,
NowSec, Horizon) ->
case core_event:get_by_id(EventId) of
{ok, #event{status = active, start_time = Start, title = Title,
calendar_id = CalId} = _Event} ->
StartSec = calendar:datetime_to_gregorian_seconds(Start),
case StartSec > NowSec andalso StartSec =< Horizon of
true ->
case core_booking:update(Id, [{reminder_sent, true}]) of
{ok, _} ->
send_reminder(UserId, Title, Start, CalId, EventId),
ok;
_ ->
ok
end;
false ->
ok
end;
_ ->
ok
end,
Booking.
send_reminder(UserId, Title, Start, CalId, EventId) ->
Path = <<"/c/", CalId/binary, "/e/", EventId/binary>>,
WhenText = format_when(Start),
case core_user:get_by_id(UserId) of
{ok, #user{email = Email}} when is_binary(Email), Email =/= <<>> ->
_ = logic_email:send_booking_reminder(Email, Title, Start, Path);
_ ->
ok
end,
_ = logic_notification:notify_event_reminder(UserId, #{
event_id => EventId,
calendar_id => CalId,
title => Title,
start_time => Start,
when_text => WhenText
}),
ok.
format_when({{Y, Mo, D}, {H, Mi, _S}}) ->
iolist_to_binary(io_lib:format("~4..0B-~2..0B-~2..0B ~2..0B:~2..0B",
[Y, Mo, D, H, Mi])).
reminder_lead_hours() ->
case application:get_env(eventhub, reminder_lead_hours) of
{ok, N} when is_integer(N), N > 0 -> N;
_ ->
case os:getenv("REMINDER_LEAD_HOURS") of
false -> 24;
"" -> 24;
S ->
try list_to_integer(S) of
N when N > 0 -> N;
_ -> 24
catch
_:_ -> 24
end
end
end.
maybe_timeout(#booking{id = Id, event_id = EventId, created_at = Created} = Booking, NowSec) ->
case core_event:get_by_id(EventId) of
{ok, Event} ->