-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, push_subscription, calendar_share]). 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}, {"Reminder skips email when prefs email=false", fun test_reminder_email_pref_off/0}, {"Reminder calls web_push when prefs push=true", fun test_reminder_web_push/0} ]}. %% Вспомогательные функции create_test_user(Role) -> UserId = base64:encode(crypto:strong_rand_bytes(16), #{mode => urlsafe, padding => false}), User = #user{ id = UserId, email = <>, 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 => <>}), 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(<>, 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). test_reminder_email_pref_off() -> 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), {ok, _} = logic_notification_prefs:put_prefs(ParticipantId, #{email => false, push => true}), CalendarId = create_test_calendar(OwnerId, auto), EventId = create_test_event_at(CalendarId, hours_from_now(2)), {ok, Booking} = logic_booking:create_booking(ParticipantId, EventId), ok = logic_booking:process_reminders(), receive {smtp, _, _} -> error(unexpected_email_when_pref_off) after 300 -> ok 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)), application:unset_env(eventhub, smtp_host), application:unset_env(eventhub, smtp_deliver), application:unset_env(eventhub, reminder_lead_hours). test_reminder_web_push() -> application:set_env(eventhub, reminder_lead_hours, 24), application:set_env(eventhub, vapid_public_key, <<"BKtestpublickey0123456789abcdef">>), application:set_env(eventhub, vapid_private_key, <<"testprivatekey0123456789abcdef">>), Self = self(), application:set_env(eventhub, web_push_deliver, fun(Ep, _P256, _Auth, Payload, _Pub, _Priv) -> Self ! {push, Ep, Payload}, ok end), OwnerId = create_test_user(user), ParticipantId = create_test_user(user), {ok, _} = logic_notification_prefs:put_prefs(ParticipantId, #{email => false, push => true}), {ok, _} = core_push_subscription:upsert( ParticipantId, <<"https://push.example/endpoint-reminder">>, <<"p256dh-test">>, <<"auth-test">>), CalendarId = create_test_calendar(OwnerId, auto), EventId = create_test_event_at(CalendarId, hours_from_now(2)), {ok, _} = logic_booking:create_booking(ParticipantId, EventId), ok = logic_booking:process_reminders(), receive {push, Ep, Payload} -> ?assertEqual(<<"https://push.example/endpoint-reminder">>, Ep), ?assert(binary:match(Payload, <<"Напоминание"/utf8>>) =/= nomatch) after 1000 -> error(timeout_web_push) end, {ok, _} = logic_notification_prefs:put_prefs(ParticipantId, #{email => false, push => false}), ok = logic_web_push:send(ParticipantId, <<"t">>, <<"b">>, <<"/x">>), receive {push, _, _} -> error(push_should_be_skipped) after 300 -> ok end, application:unset_env(eventhub, web_push_deliver), application:unset_env(eventhub, vapid_public_key), application:unset_env(eventhub, vapid_private_key), application:unset_env(eventhub, reminder_lead_hours).