fix(event): store multiple recurrence cancel exceptions
Use bag table for recurrence_exception so canceling several WEEKLY occurrences keeps all exceptions (set key was only master_id). Refs EventHub/EventHubFront#42
This commit is contained in:
@@ -321,7 +321,9 @@ table_opts(calendar_follow) -> [{disc_copies, [node()]}, {attributes, record_inf
|
|||||||
table_opts(calendar_specialist) -> [{disc_copies, [node()]}, {attributes, record_info(fields, calendar_specialist)}];
|
table_opts(calendar_specialist) -> [{disc_copies, [node()]}, {attributes, record_info(fields, calendar_specialist)}];
|
||||||
table_opts(specialist_invite) -> [{disc_copies, [node()]}, {attributes, record_info(fields, specialist_invite)}];
|
table_opts(specialist_invite) -> [{disc_copies, [node()]}, {attributes, record_info(fields, specialist_invite)}];
|
||||||
table_opts(event) -> [{disc_copies, [node()]}, {attributes, record_info(fields, event)}];
|
table_opts(event) -> [{disc_copies, [node()]}, {attributes, record_info(fields, event)}];
|
||||||
table_opts(recurrence_exception) -> [{disc_copies, [node()]}, {attributes, record_info(fields, recurrence_exception)}];
|
%% bag: несколько cancel/reschedule на один master_id (ключ set = только master_id).
|
||||||
|
table_opts(recurrence_exception) ->
|
||||||
|
[{disc_copies, [node()]}, {type, bag}, {attributes, record_info(fields, recurrence_exception)}];
|
||||||
table_opts(booking) -> [{disc_copies, [node()]}, {attributes, record_info(fields, booking)}];
|
table_opts(booking) -> [{disc_copies, [node()]}, {attributes, record_info(fields, booking)}];
|
||||||
table_opts(review) -> [{disc_copies, [node()]}, {attributes, record_info(fields, review)}];
|
table_opts(review) -> [{disc_copies, [node()]}, {attributes, record_info(fields, review)}];
|
||||||
table_opts(review_vote) -> [{disc_copies, [node()]}, {attributes, record_info(fields, review_vote)}];
|
table_opts(review_vote) -> [{disc_copies, [node()]}, {attributes, record_info(fields, review_vote)}];
|
||||||
|
|||||||
@@ -27,7 +27,8 @@
|
|||||||
'20260719210000_review_vote',
|
'20260719210000_review_vote',
|
||||||
'20260720210000_calendar_follow',
|
'20260720210000_calendar_follow',
|
||||||
'20260722150000_calendar_specialist_id',
|
'20260722150000_calendar_specialist_id',
|
||||||
'20260722190000_specialist_invite'
|
'20260722190000_specialist_invite',
|
||||||
|
'20260730120000_recurrence_exception_bag'
|
||||||
]).
|
]).
|
||||||
|
|
||||||
%% ------------------------------
|
%% ------------------------------
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
%% @doc recurrence_exception: set → bag, чтобы хранить несколько cancel на один master.
|
||||||
|
-module('20260730120000_recurrence_exception_bag').
|
||||||
|
|
||||||
|
-export([up/0, down/0]).
|
||||||
|
|
||||||
|
-include("records.hrl").
|
||||||
|
|
||||||
|
up() ->
|
||||||
|
case lists:member(recurrence_exception, mnesia:system_info(tables)) of
|
||||||
|
false ->
|
||||||
|
case mnesia:create_table(recurrence_exception, [
|
||||||
|
{disc_copies, [node()]},
|
||||||
|
{type, bag},
|
||||||
|
{attributes, record_info(fields, recurrence_exception)}
|
||||||
|
]) of
|
||||||
|
{atomic, ok} -> ok;
|
||||||
|
{aborted, {already_exists, recurrence_exception}} -> ok;
|
||||||
|
{aborted, Reason} -> error({create_table_failed, Reason})
|
||||||
|
end;
|
||||||
|
true ->
|
||||||
|
case mnesia:table_info(recurrence_exception, type) of
|
||||||
|
bag ->
|
||||||
|
ok;
|
||||||
|
_ ->
|
||||||
|
Rows = mnesia:dirty_match_object(#recurrence_exception{_ = '_'}),
|
||||||
|
case mnesia:delete_table(recurrence_exception) of
|
||||||
|
{atomic, ok} -> ok;
|
||||||
|
{aborted, Reason1} -> error({delete_table_failed, Reason1})
|
||||||
|
end,
|
||||||
|
case mnesia:create_table(recurrence_exception, [
|
||||||
|
{disc_copies, [node()]},
|
||||||
|
{type, bag},
|
||||||
|
{attributes, record_info(fields, recurrence_exception)}
|
||||||
|
]) of
|
||||||
|
{atomic, ok} -> ok;
|
||||||
|
{aborted, Reason2} -> error({create_table_failed, Reason2})
|
||||||
|
end,
|
||||||
|
lists:foreach(fun(R) -> mnesia:dirty_write(R) end, Rows),
|
||||||
|
ok
|
||||||
|
end
|
||||||
|
end.
|
||||||
|
|
||||||
|
down() ->
|
||||||
|
%% Откат в set потерял бы все кроме одного exception на master — не делаем.
|
||||||
|
ok.
|
||||||
@@ -137,7 +137,7 @@ table_opts(specialist_invite) ->
|
|||||||
table_opts(event) ->
|
table_opts(event) ->
|
||||||
[{ram_copies, [node()]}, {attributes, record_info(fields, event)}];
|
[{ram_copies, [node()]}, {attributes, record_info(fields, event)}];
|
||||||
table_opts(recurrence_exception) ->
|
table_opts(recurrence_exception) ->
|
||||||
[{ram_copies, [node()]}, {attributes, record_info(fields, recurrence_exception)}];
|
[{ram_copies, [node()]}, {type, bag}, {attributes, record_info(fields, recurrence_exception)}];
|
||||||
table_opts(booking) ->
|
table_opts(booking) ->
|
||||||
[{ram_copies, [node()]}, {attributes, record_info(fields, booking)}];
|
[{ram_copies, [node()]}, {attributes, record_info(fields, booking)}];
|
||||||
table_opts(review) ->
|
table_opts(review) ->
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ logic_event_recurring_test_() ->
|
|||||||
{"Get occurrences test", fun test_get_occurrences/0},
|
{"Get occurrences test", fun test_get_occurrences/0},
|
||||||
{"Cancel occurrence test", fun test_cancel_occurrence/0},
|
{"Cancel occurrence test", fun test_cancel_occurrence/0},
|
||||||
{"Get occurrences with cancelled test", fun test_occurrences_with_cancelled/0},
|
{"Get occurrences with cancelled test", fun test_occurrences_with_cancelled/0},
|
||||||
|
{"Cancel multiple occurrences keeps all exceptions", fun test_cancel_multiple_occurrences/0},
|
||||||
{"Materialize for booking test", fun test_materialize_for_booking/0}
|
{"Materialize for booking test", fun test_materialize_for_booking/0}
|
||||||
]}.
|
]}.
|
||||||
|
|
||||||
@@ -117,6 +118,28 @@ test_occurrences_with_cancelled() ->
|
|||||||
Starts = [O || {virtual, O} <- Occurrences],
|
Starts = [O || {virtual, O} <- Occurrences],
|
||||||
?assertNot(lists:member(CancelTime, Starts)).
|
?assertNot(lists:member(CancelTime, Starts)).
|
||||||
|
|
||||||
|
test_cancel_multiple_occurrences() ->
|
||||||
|
{UserId, CalendarId} = create_test_user_and_calendar(),
|
||||||
|
StartTime = eh_test_support:future_start(),
|
||||||
|
RRule = #{<<"freq">> => <<"WEEKLY">>, <<"interval">> => 1, <<"count">> => 4},
|
||||||
|
|
||||||
|
{ok, Event} = logic_event:create_recurring_event(
|
||||||
|
UserId, CalendarId, <<"WeeklyFill">>, StartTime, 60, RRule
|
||||||
|
),
|
||||||
|
|
||||||
|
C1 = StartTime,
|
||||||
|
C2 = add_days(StartTime, 7),
|
||||||
|
C3 = add_days(StartTime, 14),
|
||||||
|
{ok, cancelled} = logic_event:cancel_occurrence(UserId, Event#event.id, C1),
|
||||||
|
{ok, cancelled} = logic_event:cancel_occurrence(UserId, Event#event.id, C2),
|
||||||
|
{ok, cancelled} = logic_event:cancel_occurrence(UserId, Event#event.id, C3),
|
||||||
|
|
||||||
|
RangeEnd = add_days(StartTime, 28),
|
||||||
|
{ok, Occurrences} = logic_event:get_occurrences(UserId, Event#event.id, RangeEnd),
|
||||||
|
Starts = [O || {virtual, O} <- Occurrences],
|
||||||
|
?assertEqual(1, length(Starts)),
|
||||||
|
?assertEqual([add_days(StartTime, 21)], Starts).
|
||||||
|
|
||||||
test_materialize_for_booking() ->
|
test_materialize_for_booking() ->
|
||||||
{UserId, CalendarId} = create_test_user_and_calendar(),
|
{UserId, CalendarId} = create_test_user_and_calendar(),
|
||||||
StartTime = eh_test_support:future_start(),
|
StartTime = eh_test_support:future_start(),
|
||||||
|
|||||||
Reference in New Issue
Block a user