Files
EventHubBack/test/unit/logic_booking_tests.erl
T
aleksey 15ae8d4426 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
2026-08-14 20:59:38 +03:00

601 lines
25 KiB
Erlang
Executable File

-module(logic_booking_tests).
-include_lib("eunit/include/eunit.hrl").
-include("records.hrl").
-define(TABLES, [user, calendar, event, booking, admin, subscription, calendar_specialist,
recurrence_exception, notification]).
setup() ->
eh_test_support:start_mnesia(),
eh_test_support:ensure_tables(?TABLES),
ok.
cleanup(_) ->
eh_test_support:delete_tables(?TABLES),
eh_test_support:stop_mnesia(),
ok.
logic_booking_test_() ->
{foreach,
fun setup/0,
fun cleanup/1,
[
{"Create booking auto confirms", fun test_create_booking_auto/0},
{"Create booking manual pending", fun test_create_booking_pending/0},
{"Recurring booking requires occurrence_start", fun test_recurring_requires_occurrence/0},
{"Recurring booking materializes occurrence", fun test_recurring_books_occurrence/0},
{"Recurring booking rejects invalid occurrence", fun test_recurring_invalid_occurrence/0},
{"Recurring booking rejects cancelled occurrence", fun test_recurring_cancelled_occurrence/0},
{"Create booking personal denied", fun test_booking_personal_denied/0},
{"Create duplicate booking", fun test_create_duplicate_booking/0},
{"Create booking for missing event", fun test_booking_missing_event/0},
{"Create booking when event is full", fun test_booking_event_full/0},
{"Pending bookings fill capacity", fun test_pending_fills_capacity/0},
{"Confirm booking", fun test_confirm_booking/0},
{"Confirm booking as booker denied", fun test_confirm_booker_denied/0},
{"Confirm booking as stranger denied", fun test_confirm_stranger_denied/0},
{"Confirm booking as specialist", fun test_confirm_booking_specialist/0},
{"Confirm booking as admin", fun test_confirm_booking_admin/0},
{"Decline booking by owner", fun test_decline_booking/0},
{"Decline booking as booker denied", fun test_decline_booker_denied/0},
{"Confirm non-pending booking denied", fun test_confirm_non_pending/0},
{"Cancel booking by participant", fun test_cancel_booking/0},
{"Cancel booking access denied", fun test_cancel_access_denied/0},
{"List event bookings", fun test_list_event_bookings/0},
{"List event bookings as owner", fun test_list_event_bookings_owner/0},
{"List event bookings non-owner denied", fun test_list_event_bookings_non_owner/0},
{"List user bookings", fun test_list_user_bookings/0},
{"List booking requests as owner", fun test_list_booking_requests_owner/0},
{"List booking requests as specialist", fun test_list_booking_requests_specialist/0},
{"List booking requests stranger empty", fun test_list_booking_requests_stranger/0},
{"Recurring pending in owner inbox via instance", fun test_recurring_pending_in_inbox/0},
{"Studio bookings include confirmed after confirm", fun test_studio_bookings_after_confirm/0},
{"Past pending expires on list", fun test_past_pending_expires_on_list/0},
{"Past pending excluded from booking requests", fun test_past_pending_excluded_from_requests/0},
{"Past pending confirm denied", fun test_past_pending_confirm_denied/0},
{"Future pending still confirmable", fun test_future_pending_still_confirmable/0},
{"Process timeout expires past pending", fun test_process_timeout_expires_past/0},
{"Reminder sends once within lead window", fun test_reminder_within_window/0},
{"Reminder skipped outside lead window", fun test_reminder_outside_window/0},
{"Reminder skipped for pending", fun test_reminder_skips_pending/0}
]}.
%% Вспомогательные функции
create_test_user(Role) ->
UserId = base64:encode(crypto:strong_rand_bytes(16), #{mode => urlsafe, padding => false}),
User = #user{
id = UserId,
email = <<UserId/binary, "@test.com">>,
password_hash = <<"hash">>,
role = Role,
status = active,
created_at = calendar:universal_time(),
updated_at = calendar:universal_time()
},
mnesia:dirty_write(User),
UserId.
create_test_admin() ->
AdminId = base64:encode(crypto:strong_rand_bytes(16), #{mode => urlsafe, padding => false}),
Admin = eh_test_support:seed_admin(#{id => AdminId, email => <<AdminId/binary, "@admin.test">>}),
Admin#admin.id.
ensure_subscription(OwnerId) ->
{ok, _} = core_subscription:create(OwnerId, monthly, true),
ok.
create_test_calendar(OwnerId, Confirmation) ->
ensure_subscription(OwnerId),
{ok, Calendar} = core_calendar:create(OwnerId, <<"Test Calendar">>, <<"">>, Confirmation, commercial),
Calendar#calendar.id.
create_personal_calendar(OwnerId) ->
{ok, Calendar} = core_calendar:create(OwnerId, <<"Personal">>, <<"">>, manual, personal),
Calendar#calendar.id.
create_test_event(CalendarId) ->
StartTime = eh_test_support:future_start(),
{ok, Event} = core_event:create(CalendarId, <<"Test Event">>, StartTime, 60),
Event#event.id.
create_test_event_at(CalendarId, StartTime) ->
{ok, Event} = core_event:create(CalendarId, <<"Test Event">>, StartTime, 60),
Event#event.id.
past_start() ->
Sec = calendar:datetime_to_gregorian_seconds(calendar:universal_time()) - 3600,
calendar:gregorian_seconds_to_datetime(Sec).
add_days(DateTime, Days) ->
Sec = calendar:datetime_to_gregorian_seconds(DateTime) + Days * 86400,
calendar:gregorian_seconds_to_datetime(Sec).
create_recurring_event(CalendarId) ->
StartTime = eh_test_support:future_start(),
RRule = #{<<"freq">> => <<"WEEKLY">>, <<"interval">> => 1},
{ok, Event} = core_event:create_recurring(CalendarId, <<"Series">>, StartTime, 60, RRule),
{Event#event.id, StartTime}.
create_test_event_with_capacity(CalendarId, Capacity) ->
StartTime = eh_test_support:future_start(),
{ok, Event} = core_event:create(CalendarId, <<"Test Event">>, StartTime, 60),
{ok, Updated} = core_event:update(Event#event.id, [{capacity, Capacity}]),
Updated#event.id.
%% Тесты
test_create_booking_auto() ->
OwnerId = create_test_user(user),
ParticipantId = create_test_user(user),
CalendarId = create_test_calendar(OwnerId, auto),
EventId = create_test_event(CalendarId),
{ok, Booking} = logic_booking:create_booking(ParticipantId, EventId),
?assertEqual(confirmed, Booking#booking.status).
test_create_booking_pending() ->
OwnerId = create_test_user(user),
ParticipantId = create_test_user(user),
CalendarId = create_test_calendar(OwnerId, manual),
EventId = create_test_event(CalendarId),
{ok, Booking} = logic_booking:create_booking(ParticipantId, EventId),
?assertEqual(pending, Booking#booking.status),
{ok, Stored} = core_booking:get_by_id(Booking#booking.id),
?assertEqual(pending, Stored#booking.status).
test_recurring_requires_occurrence() ->
OwnerId = create_test_user(user),
ParticipantId = create_test_user(user),
CalendarId = create_test_calendar(OwnerId, auto),
{MasterId, _} = create_recurring_event(CalendarId),
{error, occurrence_start_required} = logic_booking:create_booking(ParticipantId, MasterId).
test_recurring_books_occurrence() ->
OwnerId = create_test_user(user),
ParticipantId = create_test_user(user),
CalendarId = create_test_calendar(OwnerId, auto),
{MasterId, StartTime} = create_recurring_event(CalendarId),
Occ = add_days(StartTime, 7),
{ok, Booking} = logic_booking:create_booking(ParticipantId, MasterId, Occ),
{ok, BookedEvent} = core_event:get_by_id(Booking#booking.event_id),
?assertEqual(true, BookedEvent#event.is_instance),
?assertEqual(MasterId, BookedEvent#event.master_id),
?assertEqual(Occ, BookedEvent#event.start_time),
?assertNotEqual(MasterId, Booking#booking.event_id).
test_recurring_invalid_occurrence() ->
OwnerId = create_test_user(user),
ParticipantId = create_test_user(user),
CalendarId = create_test_calendar(OwnerId, auto),
{MasterId, StartTime} = create_recurring_event(CalendarId),
Bad = add_days(StartTime, 1),
{error, invalid_occurrence} = logic_booking:create_booking(ParticipantId, MasterId, Bad).
test_recurring_cancelled_occurrence() ->
OwnerId = create_test_user(user),
ParticipantId = create_test_user(user),
CalendarId = create_test_calendar(OwnerId, auto),
{MasterId, StartTime} = create_recurring_event(CalendarId),
Occ = add_days(StartTime, 7),
mnesia:dirty_write(#recurrence_exception{
master_id = MasterId,
original_start = Occ,
action = cancel,
new_start = undefined
}),
{error, invalid_occurrence} = logic_booking:create_booking(ParticipantId, MasterId, Occ).
test_booking_personal_denied() ->
OwnerId = create_test_user(user),
ParticipantId = create_test_user(user),
CalendarId = create_personal_calendar(OwnerId),
EventId = create_test_event(CalendarId),
{error, personal_calendar} = logic_booking:create_booking(ParticipantId, EventId).
test_create_duplicate_booking() ->
OwnerId = create_test_user(user),
ParticipantId = create_test_user(user),
CalendarId = create_test_calendar(OwnerId, manual),
EventId = create_test_event(CalendarId),
{ok, _} = logic_booking:create_booking(ParticipantId, EventId),
{error, already_booked} = logic_booking:create_booking(ParticipantId, EventId).
test_booking_missing_event() ->
ParticipantId = create_test_user(user),
{error, not_found} = logic_booking:create_booking(ParticipantId, <<"nonexistent">>).
test_booking_event_full() ->
OwnerId = create_test_user(user),
Participant1Id = create_test_user(user),
Participant2Id = create_test_user(user),
CalendarId = create_test_calendar(OwnerId, manual),
EventId = create_test_event_with_capacity(CalendarId, 1),
{ok, B1} = logic_booking:create_booking(Participant1Id, EventId),
{ok, _} = logic_booking:confirm_booking(OwnerId, B1#booking.id, confirm),
{error, full} = logic_booking:create_booking(Participant2Id, EventId).
test_pending_fills_capacity() ->
OwnerId = create_test_user(user),
Participant1Id = create_test_user(user),
Participant2Id = create_test_user(user),
CalendarId = create_test_calendar(OwnerId, manual),
EventId = create_test_event_with_capacity(CalendarId, 1),
{ok, _} = logic_booking:create_booking(Participant1Id, EventId),
{error, full} = logic_booking:create_booking(Participant2Id, EventId).
test_confirm_booking() ->
OwnerId = create_test_user(user),
ParticipantId = create_test_user(user),
CalendarId = create_test_calendar(OwnerId, manual),
EventId = create_test_event(CalendarId),
{ok, Booking} = logic_booking:create_booking(ParticipantId, EventId),
{ok, Confirmed} = logic_booking:confirm_booking(OwnerId, Booking#booking.id, confirm),
?assertEqual(confirmed, Confirmed#booking.status).
test_confirm_booker_denied() ->
OwnerId = create_test_user(user),
ParticipantId = create_test_user(user),
CalendarId = create_test_calendar(OwnerId, manual),
EventId = create_test_event(CalendarId),
{ok, Booking} = logic_booking:create_booking(ParticipantId, EventId),
{error, access_denied} = logic_booking:confirm_booking(ParticipantId, Booking#booking.id, confirm).
test_confirm_stranger_denied() ->
OwnerId = create_test_user(user),
ParticipantId = create_test_user(user),
StrangerId = create_test_user(user),
CalendarId = create_test_calendar(OwnerId, manual),
EventId = create_test_event(CalendarId),
{ok, Booking} = logic_booking:create_booking(ParticipantId, EventId),
{error, access_denied} = logic_booking:confirm_booking(StrangerId, Booking#booking.id, confirm).
test_confirm_booking_specialist() ->
OwnerId = create_test_user(user),
SpecId = create_test_user(user),
ParticipantId = create_test_user(user),
CalendarId = create_test_calendar(OwnerId, manual),
{ok, _} = core_calendar_specialist:create(CalendarId, SpecId, <<"Spec">>, []),
EventId = create_test_event(CalendarId),
{ok, _} = core_event:update(EventId, [{specialist_id, SpecId}]),
{ok, Booking} = logic_booking:create_booking(ParticipantId, EventId),
{ok, Confirmed} = logic_booking:confirm_booking(SpecId, Booking#booking.id, confirm),
?assertEqual(confirmed, Confirmed#booking.status).
test_confirm_booking_admin() ->
OwnerId = create_test_user(user),
ParticipantId = create_test_user(user),
AdminId = create_test_admin(),
CalendarId = create_test_calendar(OwnerId, manual),
EventId = create_test_event(CalendarId),
{ok, Booking} = logic_booking:create_booking(ParticipantId, EventId),
{ok, Confirmed} = logic_booking:confirm_booking(AdminId, Booking#booking.id, confirm),
?assertEqual(confirmed, Confirmed#booking.status).
test_decline_booking() ->
OwnerId = create_test_user(user),
ParticipantId = create_test_user(user),
CalendarId = create_test_calendar(OwnerId, manual),
EventId = create_test_event(CalendarId),
{ok, Booking} = logic_booking:create_booking(ParticipantId, EventId),
{ok, Declined} = logic_booking:confirm_booking(OwnerId, Booking#booking.id, decline),
?assertEqual(cancelled, Declined#booking.status).
test_decline_booker_denied() ->
OwnerId = create_test_user(user),
ParticipantId = create_test_user(user),
CalendarId = create_test_calendar(OwnerId, manual),
EventId = create_test_event(CalendarId),
{ok, Booking} = logic_booking:create_booking(ParticipantId, EventId),
{error, access_denied} = logic_booking:confirm_booking(ParticipantId, Booking#booking.id, decline).
test_confirm_non_pending() ->
OwnerId = create_test_user(user),
ParticipantId = create_test_user(user),
CalendarId = create_test_calendar(OwnerId, manual),
EventId = create_test_event(CalendarId),
{ok, Booking} = logic_booking:create_booking(ParticipantId, EventId),
{ok, _} = logic_booking:confirm_booking(OwnerId, Booking#booking.id, confirm),
{error, access_denied} = logic_booking:confirm_booking(OwnerId, Booking#booking.id, confirm).
test_cancel_booking() ->
OwnerId = create_test_user(user),
ParticipantId = create_test_user(user),
CalendarId = create_test_calendar(OwnerId, manual),
EventId = create_test_event(CalendarId),
{ok, Booking} = logic_booking:create_booking(ParticipantId, EventId),
{ok, Cancelled} = logic_booking:cancel_booking(ParticipantId, Booking#booking.id, cancel),
?assertEqual(cancelled, Cancelled#booking.status).
test_cancel_access_denied() ->
OwnerId = create_test_user(user),
ParticipantId = create_test_user(user),
OtherId = create_test_user(user),
CalendarId = create_test_calendar(OwnerId, manual),
EventId = create_test_event(CalendarId),
{ok, Booking} = logic_booking:create_booking(ParticipantId, EventId),
{error, access_denied} = logic_booking:cancel_booking(OtherId, Booking#booking.id, cancel).
test_list_event_bookings() ->
OwnerId = create_test_user(user),
Participant1Id = create_test_user(user),
Participant2Id = create_test_user(user),
CalendarId = create_test_calendar(OwnerId, manual),
EventId = create_test_event(CalendarId),
{ok, _} = logic_booking:create_booking(Participant1Id, EventId),
{ok, _} = logic_booking:create_booking(Participant2Id, EventId),
{ok, Bookings} = logic_booking:list_event_bookings(EventId),
?assertEqual(2, length(Bookings)).
test_list_event_bookings_owner() ->
OwnerId = create_test_user(user),
Participant1Id = create_test_user(user),
Participant2Id = create_test_user(user),
CalendarId = create_test_calendar(OwnerId, manual),
EventId = create_test_event(CalendarId),
{ok, _} = logic_booking:create_booking(Participant1Id, EventId),
{ok, _} = logic_booking:create_booking(Participant2Id, EventId),
{ok, Bookings} = logic_booking:list_event_bookings(OwnerId, EventId),
?assertEqual(2, length(Bookings)).
test_list_event_bookings_non_owner() ->
OwnerId = create_test_user(user),
ParticipantId = create_test_user(user),
StrangerId = create_test_user(user),
CalendarId = create_test_calendar(OwnerId, manual),
EventId = create_test_event(CalendarId),
{ok, _} = logic_booking:create_booking(ParticipantId, EventId),
{error, access_denied} = logic_booking:list_event_bookings(StrangerId, EventId),
{error, access_denied} = logic_booking:list_event_bookings(ParticipantId, EventId).
test_list_user_bookings() ->
OwnerId = create_test_user(user),
ParticipantId = create_test_user(user),
CalendarId = create_test_calendar(OwnerId, manual),
EventId1 = create_test_event(CalendarId),
EventId2 = create_test_event(CalendarId),
{ok, _} = logic_booking:create_booking(ParticipantId, EventId1),
{ok, _} = logic_booking:create_booking(ParticipantId, EventId2),
{ok, Bookings} = logic_booking:list_user_bookings(ParticipantId),
?assertEqual(2, length(Bookings)).
test_list_booking_requests_owner() ->
OwnerId = create_test_user(user),
ParticipantId = create_test_user(user),
CalendarId = create_test_calendar(OwnerId, manual),
EventId = create_test_event(CalendarId),
{ok, Booking} = logic_booking:create_booking(ParticipantId, EventId),
{ok, Items} = logic_booking:list_user_booking_requests(OwnerId),
?assertEqual(1, length(Items)),
[{B, Event, Role}] = Items,
?assertEqual(Booking#booking.id, B#booking.id),
?assertEqual(EventId, Event#event.id),
?assertEqual(owner, Role),
%% participant inbox is separate — no manage requests for own booking
{ok, []} = logic_booking:list_user_booking_requests(ParticipantId).
test_list_booking_requests_specialist() ->
OwnerId = create_test_user(user),
SpecId = create_test_user(user),
ParticipantId = create_test_user(user),
CalendarId = create_test_calendar(OwnerId, manual),
{ok, _} = core_calendar_specialist:create(CalendarId, SpecId, <<"Spec">>, []),
SpecEventId = create_test_event(CalendarId),
OtherEventId = create_test_event(CalendarId),
{ok, _} = core_event:update(SpecEventId, [{specialist_id, SpecId}]),
{ok, SpecBooking} = logic_booking:create_booking(ParticipantId, SpecEventId),
OtherParticipant = create_test_user(user),
{ok, _} = logic_booking:create_booking(OtherParticipant, OtherEventId),
{ok, SpecItems} = logic_booking:list_user_booking_requests(SpecId),
?assertEqual(1, length(SpecItems)),
[{B, Event, Role}] = SpecItems,
?assertEqual(SpecBooking#booking.id, B#booking.id),
?assertEqual(SpecEventId, Event#event.id),
?assertEqual(specialist, Role),
%% owner sees both pending
{ok, OwnerItems} = logic_booking:list_user_booking_requests(OwnerId),
?assertEqual(2, length(OwnerItems)).
test_list_booking_requests_stranger() ->
OwnerId = create_test_user(user),
ParticipantId = create_test_user(user),
StrangerId = create_test_user(user),
CalendarId = create_test_calendar(OwnerId, manual),
EventId = create_test_event(CalendarId),
{ok, _} = logic_booking:create_booking(ParticipantId, EventId),
{ok, []} = logic_booking:list_user_booking_requests(StrangerId).
test_recurring_pending_in_inbox() ->
OwnerId = create_test_user(user),
ParticipantId = create_test_user(user),
CalendarId = create_test_calendar(OwnerId, manual),
{MasterId, StartTime} = create_recurring_event(CalendarId),
Occ = add_days(StartTime, 7),
{ok, Booking} = logic_booking:create_booking(ParticipantId, MasterId, Occ),
{ok, Items} = logic_booking:list_user_booking_requests(OwnerId),
?assertEqual(1, length(Items)),
[{B, Event, owner}] = Items,
?assertEqual(Booking#booking.id, B#booking.id),
?assertEqual(true, Event#event.is_instance),
?assertEqual(Occ, Event#event.start_time).
test_studio_bookings_after_confirm() ->
OwnerId = create_test_user(user),
ParticipantId = create_test_user(user),
CalendarId = create_test_calendar(OwnerId, manual),
EventId = create_test_event(CalendarId),
{ok, Booking} = logic_booking:create_booking(ParticipantId, EventId),
{ok, _} = logic_booking:confirm_booking(OwnerId, Booking#booking.id, confirm),
{ok, []} = logic_booking:list_user_booking_requests(OwnerId),
{ok, Studio} = logic_booking:list_user_studio_bookings(OwnerId),
?assertEqual(1, length(Studio)),
[{B, _Event, owner}] = Studio,
?assertEqual(confirmed, B#booking.status).
test_past_pending_expires_on_list() ->
OwnerId = create_test_user(user),
ParticipantId = create_test_user(user),
CalendarId = create_test_calendar(OwnerId, manual),
EventId = create_test_event_at(CalendarId, past_start()),
{ok, Booking} = core_booking:create(EventId, ParticipantId, pending),
{ok, [Listed]} = logic_booking:list_user_bookings(ParticipantId),
?assertEqual(Booking#booking.id, Listed#booking.id),
?assertEqual(expired, Listed#booking.status),
{ok, Stored} = core_booking:get_by_id(Booking#booking.id),
?assertEqual(expired, Stored#booking.status).
test_past_pending_excluded_from_requests() ->
OwnerId = create_test_user(user),
ParticipantId = create_test_user(user),
CalendarId = create_test_calendar(OwnerId, manual),
PastId = create_test_event_at(CalendarId, past_start()),
FutureId = create_test_event(CalendarId),
{ok, _} = core_booking:create(PastId, ParticipantId, pending),
{ok, FutureBooking} = logic_booking:create_booking(ParticipantId, FutureId),
{ok, Items} = logic_booking:list_user_booking_requests(OwnerId),
?assertEqual(1, length(Items)),
[{B, Event, owner}] = Items,
?assertEqual(FutureBooking#booking.id, B#booking.id),
?assertEqual(FutureId, Event#event.id).
test_past_pending_confirm_denied() ->
OwnerId = create_test_user(user),
ParticipantId = create_test_user(user),
CalendarId = create_test_calendar(OwnerId, manual),
EventId = create_test_event_at(CalendarId, past_start()),
{ok, Booking} = core_booking:create(EventId, ParticipantId, pending),
{error, expired} = logic_booking:confirm_booking(Booking#booking.id, OwnerId),
{ok, Stored} = core_booking:get_by_id(Booking#booking.id),
?assertEqual(expired, Stored#booking.status).
test_future_pending_still_confirmable() ->
OwnerId = create_test_user(user),
ParticipantId = create_test_user(user),
CalendarId = create_test_calendar(OwnerId, manual),
EventId = create_test_event(CalendarId),
{ok, Booking} = logic_booking:create_booking(ParticipantId, EventId),
{ok, Confirmed} = logic_booking:confirm_booking(Booking#booking.id, OwnerId),
?assertEqual(confirmed, Confirmed#booking.status).
test_process_timeout_expires_past() ->
OwnerId = create_test_user(user),
ParticipantId = create_test_user(user),
CalendarId = create_test_calendar(OwnerId, manual),
EventId = create_test_event_at(CalendarId, past_start()),
{ok, Booking} = core_booking:create(EventId, ParticipantId, pending),
ok = logic_booking:process_timeout_bookings(),
{ok, Stored} = core_booking:get_by_id(Booking#booking.id),
?assertEqual(expired, Stored#booking.status).
hours_from_now(Hours) ->
Sec = calendar:datetime_to_gregorian_seconds(calendar:universal_time())
+ Hours * 3600,
calendar:gregorian_seconds_to_datetime(Sec).
test_reminder_within_window() ->
application:set_env(eventhub, reminder_lead_hours, 24),
Self = self(),
application:set_env(eventhub, smtp_host, "smtp.test"),
application:set_env(eventhub, smtp_deliver,
fun(_From, To, Raw, _Opts) ->
Self ! {smtp, To, Raw},
{ok, mock}
end),
OwnerId = create_test_user(user),
ParticipantId = create_test_user(user),
CalendarId = create_test_calendar(OwnerId, auto),
EventId = create_test_event_at(CalendarId, hours_from_now(2)),
{ok, Booking} = logic_booking:create_booking(ParticipantId, EventId),
?assertEqual(confirmed, Booking#booking.status),
?assertEqual(false, Booking#booking.reminder_sent),
ok = logic_booking:process_reminders(),
receive
{smtp, To, Raw} ->
?assertEqual(<<ParticipantId/binary, "@test.com">>, To),
?assert(binary:match(Raw, <<"CalenTIQ">>) =/= nomatch),
?assert(binary:match(Raw, <<"/c/", CalendarId/binary, "/e/", EventId/binary>>)
=/= nomatch)
after 1000 ->
error(timeout_reminder_email)
end,
{ok, Stored} = core_booking:get_by_id(Booking#booking.id),
?assertEqual(true, Stored#booking.reminder_sent),
Notifs = core_notification:list_by_user(ParticipantId),
?assert(lists:any(fun(#notification{type = event_reminder}) -> true; (_) -> false end,
Notifs)),
%% second pass: no duplicate email
ok = logic_booking:process_reminders(),
receive
{smtp, _, _} -> error(duplicate_reminder)
after 200 ->
ok
end,
application:unset_env(eventhub, smtp_host),
application:unset_env(eventhub, smtp_deliver),
application:unset_env(eventhub, reminder_lead_hours).
test_reminder_outside_window() ->
application:set_env(eventhub, reminder_lead_hours, 24),
Self = self(),
application:set_env(eventhub, smtp_host, "smtp.test"),
application:set_env(eventhub, smtp_deliver,
fun(_From, To, Raw, _Opts) ->
Self ! {smtp, To, Raw},
{ok, mock}
end),
OwnerId = create_test_user(user),
ParticipantId = create_test_user(user),
CalendarId = create_test_calendar(OwnerId, auto),
EventId = create_test_event_at(CalendarId, hours_from_now(48)),
{ok, Booking} = logic_booking:create_booking(ParticipantId, EventId),
ok = logic_booking:process_reminders(),
receive
{smtp, _, _} -> error(unexpected_reminder)
after 200 ->
ok
end,
{ok, Stored} = core_booking:get_by_id(Booking#booking.id),
?assertEqual(false, Stored#booking.reminder_sent),
application:unset_env(eventhub, smtp_host),
application:unset_env(eventhub, smtp_deliver),
application:unset_env(eventhub, reminder_lead_hours).
test_reminder_skips_pending() ->
application:set_env(eventhub, reminder_lead_hours, 24),
OwnerId = create_test_user(user),
ParticipantId = create_test_user(user),
CalendarId = create_test_calendar(OwnerId, manual),
EventId = create_test_event_at(CalendarId, hours_from_now(2)),
{ok, Booking} = logic_booking:create_booking(ParticipantId, EventId),
?assertEqual(pending, Booking#booking.status),
ok = logic_booking:process_reminders(),
{ok, Stored} = core_booking:get_by_id(Booking#booking.id),
?assertEqual(false, Stored#booking.reminder_sent),
application:unset_env(eventhub, reminder_lead_hours).