Implement commercial calendars: booking_open, specialists, expire without type downgrade.
CI / test (push) Successful in 6m44s
CI / deploy-ift (push) Successful in 4m2s
CI / e2e-ift (push) Successful in 1m25s
CI / deploy-stage (push) Successful in 2m25s
CI / e2e-stage (push) Successful in 1m12s

Refs EventHub/EventHubBack#54
This commit is contained in:
2026-07-22 16:12:52 +03:00
parent fdb08eb453
commit af5f506866
35 changed files with 978 additions and 207 deletions
+5 -2
View File
@@ -24,8 +24,11 @@ test() ->
ct:pal("=== Admin Moderation Tests ==="),
Token = api_test_runner:get_admin_token(),
UserToken = api_test_runner:get_user_token(),
% Создаём календарь и событие
CalId = api_test_runner:create_calendar(UserToken, #{title => <<"ModTestCal">>}),
% Создаём commercial-календарь и событие (бронь только на commercial)
CalId = api_test_runner:create_calendar(UserToken, #{
title => <<"ModTestCal">>,
type => <<"commercial">>,
confirmation => <<"manual">>}),
EventId = api_test_runner:create_event(UserToken, CalId, #{
title => <<"Event to moderate">>,
start_time => api_test_runner:future_date(),
+5 -2
View File
@@ -28,8 +28,11 @@ test() ->
ct:pal("=== Admin Reviews Tests ==="),
Token = api_test_runner:get_admin_token(),
UserToken = api_test_runner:get_user_token(),
%% Создаём тестовые данные: календарь, событие
CalId = api_test_runner:create_calendar(UserToken, #{title => <<"ReviewsTestCal">>}),
%% Создаём тестовые данные: commercial-календарь (бронь только на нём), событие
CalId = api_test_runner:create_calendar(UserToken, #{
title => <<"ReviewsTestCal">>,
type => <<"commercial">>,
confirmation => <<"manual">>}),
EventId = api_test_runner:create_event(UserToken, CalId, #{
title => <<"Event for review testing">>,
start_time => api_test_runner:future_date(),
+10 -5
View File
@@ -40,22 +40,27 @@ test() ->
% Создаём тестовые данные для ненулевой статистики
UserToken = api_test_runner:get_user_token(),
CalId = api_test_runner:create_calendar(UserToken, #{title => <<"StatsCal">>}),
ParticipantEmail = api_test_runner:unique_email(<<"statspart">>),
ParticipantToken = api_test_runner:register_and_login(ParticipantEmail, <<"pass">>),
CalId = api_test_runner:create_calendar(UserToken, #{
title => <<"StatsCal">>,
type => <<"commercial">>,
confirmation => <<"manual">>}),
EventId = api_test_runner:create_event(UserToken, CalId, #{
title => <<"Stats Event">>,
start_time => api_test_runner:future_date(),
duration => 60
}),
% Бронируем и подтверждаем, чтобы можно было оставить отзыв
% Бронируем участником (не владельцем) и подтверждаем, чтобы можно было оставить отзыв
#{<<"id">> := BookingId} = api_test_runner:client_post(
<<"/v1/events/", EventId/binary, "/bookings">>, UserToken, #{}),
<<"/v1/events/", EventId/binary, "/bookings">>, ParticipantToken, #{}),
api_test_runner:client_put(<<"/v1/bookings/", BookingId/binary>>, UserToken,
#{action => <<"confirm">>}),
% Оставляем отзыв
api_test_runner:client_post(<<"/v1/reviews">>, UserToken,
api_test_runner:client_post(<<"/v1/reviews">>, ParticipantToken,
#{target_type => <<"event">>, target_id => EventId, rating => 5, comment => <<"Great!">>}),
% Жалоба
api_test_runner:client_post(<<"/v1/reports">>, UserToken,
api_test_runner:client_post(<<"/v1/reports">>, ParticipantToken,
#{<<"target_type">> => <<"event">>, <<"target_id">> => EventId, <<"reason">> => <<"Test">>}),
% Подписка
SubUserToken = api_test_runner:get_user_token(),
+2 -1
View File
@@ -91,7 +91,8 @@ test_report_on_review(Admin) ->
<<"report_threshold">> => 3
}),
Owner = api_test_runner:get_user_token(),
CalId = api_test_runner:create_calendar(Owner, #{title => <<"RevRepCal">>}),
CalId = api_test_runner:create_calendar(Owner, #{
title => <<"RevRepCal">>, type => <<"commercial">>, confirmation => <<"manual">>}),
#{<<"id">> := EventId} = api_test_runner:client_post(
<<"/v1/calendars/", CalId/binary, "/events">>, Owner,
#{title => <<"Event for review report">>,
+5 -1
View File
@@ -31,7 +31,11 @@ test() ->
ParticipantToken = api_test_runner:register_and_login(ParticipantEmail, <<"pass">>),
% Создаём календарь и событие
CalId = api_test_runner:create_calendar(OwnerToken, #{title => <<"BookingTest">>}),
CalId = api_test_runner:create_calendar(OwnerToken, #{
title => <<"BookingTest">>,
type => <<"commercial">>,
confirmation => <<"manual">>
}),
#{<<"id">> := EventId} = api_test_runner:client_post(
<<"/v1/calendars/", CalId/binary, "/events">>, OwnerToken,
#{title => <<"Event to book">>,
+5 -1
View File
@@ -27,7 +27,11 @@ test() ->
ParticipantToken = api_test_runner:register_and_login(ParticipantEmail, <<"pass">>),
% Создаём календарь и событие
CalId = api_test_runner:create_calendar(OwnerToken, #{title => <<"MyBookTest">>}),
CalId = api_test_runner:create_calendar(OwnerToken, #{
title => <<"MyBookTest">>,
type => <<"commercial">>,
confirmation => <<"manual">>
}),
#{<<"id">> := EventId} = api_test_runner:client_post(
<<"/v1/calendars/", CalId/binary, "/events">>, OwnerToken,
#{title => <<"Event for my booking">>,
+2 -1
View File
@@ -27,7 +27,8 @@ test() ->
ParticipantToken = api_test_runner:register_and_login(ParticipantEmail, <<"pass">>),
% Создаём календарь и событие
CalId = api_test_runner:create_calendar(OwnerToken, #{title => <<"MyRevTest">>}),
CalId = api_test_runner:create_calendar(OwnerToken, #{
title => <<"MyRevTest">>, type => <<"commercial">>, confirmation => <<"manual">>}),
#{<<"id">> := EventId} = api_test_runner:client_post(
<<"/v1/calendars/", CalId/binary, "/events">>, OwnerToken,
#{title => <<"Event for my review">>,
+2 -1
View File
@@ -34,7 +34,8 @@ test() ->
StrangerToken = api_test_runner:register_and_login(StrangerEmail, <<"pass">>),
% Создаём календарь, событие, бронирование и отзыв
CalId = api_test_runner:create_calendar(OwnerToken, #{title => <<"RevById">>}),
CalId = api_test_runner:create_calendar(OwnerToken, #{
title => <<"RevById">>, type => <<"commercial">>, confirmation => <<"manual">>}),
#{<<"id">> := EventId} = api_test_runner:client_post(
<<"/v1/calendars/", CalId/binary, "/events">>, OwnerToken,
#{title => <<"Event for review">>,
+2 -1
View File
@@ -22,7 +22,8 @@ test() ->
OtherEmail = api_test_runner:unique_email(<<"rvother">>),
OtherToken = api_test_runner:register_and_login(OtherEmail, <<"pass">>),
CalId = api_test_runner:create_calendar(OwnerToken, #{title => <<"VoteCal">>}),
CalId = api_test_runner:create_calendar(OwnerToken, #{
title => <<"VoteCal">>, type => <<"commercial">>, confirmation => <<"manual">>}),
#{<<"id">> := EventId} = api_test_runner:client_post(
<<"/v1/calendars/", CalId/binary, "/events">>, OwnerToken,
#{title => <<"Event for votes">>,
+5 -1
View File
@@ -31,7 +31,11 @@ test() ->
StrangerToken = api_test_runner:register_and_login(StrangerEmail, <<"pass">>),
% Создаём календарь и событие
CalId = api_test_runner:create_calendar(OwnerToken, #{title => <<"ReviewTest">>}),
CalId = api_test_runner:create_calendar(OwnerToken, #{
title => <<"ReviewTest">>,
type => <<"commercial">>,
confirmation => <<"manual">>
}),
#{<<"id">> := EventId} = api_test_runner:client_post(
<<"/v1/calendars/", CalId/binary, "/events">>, OwnerToken,
#{title => <<"Event for review">>,
+13 -14
View File
@@ -2,7 +2,7 @@
-include_lib("eunit/include/eunit.hrl").
-include("records.hrl").
-define(TABLES, [user, calendar, event, booking, admin]).
-define(TABLES, [user, calendar, event, booking, admin, subscription]).
setup() ->
eh_test_support:start_mnesia(),
@@ -19,7 +19,7 @@ booking_integration_test_() ->
fun setup/0,
fun cleanup/1,
[
{"Booking create stays pending", fun test_pending_booking_flow/0},
{"Booking auto confirms", fun test_auto_booking_flow/0},
{"Full booking flow with manual confirmation", fun test_manual_booking_flow/0},
{"Capacity management test", fun test_capacity_management/0},
{"Multiple bookings test", fun test_multiple_bookings/0}
@@ -39,24 +39,25 @@ create_user() ->
mnesia:dirty_write(User),
UserId.
create_commercial(OwnerId, Confirmation) ->
{ok, _} = core_subscription:create(OwnerId, monthly, true),
{ok, Calendar} = core_calendar:create(OwnerId, <<"Cal">>, <<"">>, Confirmation, commercial),
Calendar.
add_days(DateTime, Days) ->
Sec = calendar:datetime_to_gregorian_seconds(DateTime) + Days * 86400,
calendar:gregorian_seconds_to_datetime(Sec).
test_pending_booking_flow() ->
test_auto_booking_flow() ->
OwnerId = create_user(),
ParticipantId = create_user(),
{ok, Calendar} = core_calendar:create(OwnerId, <<"Auto">>, <<"">>, auto),
Calendar = create_commercial(OwnerId, auto),
StartTime = eh_test_support:future_start(),
{ok, Event} = core_event:create(Calendar#calendar.id, <<"Event">>, StartTime, 60),
{ok, Booking} = logic_booking:create_booking(ParticipantId, Event#event.id),
?assertEqual(pending, Booking#booking.status),
{ok, Stored} = core_booking:get_by_id(Booking#booking.id),
?assertEqual(pending, Stored#booking.status),
?assertEqual(confirmed, Booking#booking.status),
{ok, EventBookings} = logic_booking:list_event_bookings(Event#event.id),
?assertEqual(1, length(EventBookings)),
@@ -67,8 +68,7 @@ test_pending_booking_flow() ->
test_manual_booking_flow() ->
OwnerId = create_user(),
ParticipantId = create_user(),
{ok, Calendar} = core_calendar:create(OwnerId, <<"Manual">>, <<"">>, manual),
Calendar = create_commercial(OwnerId, manual),
StartTime = eh_test_support:future_start(),
{ok, Event} = core_event:create(Calendar#calendar.id, <<"Event">>, StartTime, 60),
@@ -88,7 +88,7 @@ test_capacity_management() ->
Participant2Id = create_user(),
Participant3Id = create_user(),
{ok, Calendar} = core_calendar:create(OwnerId, <<"Test">>, <<"">>, manual),
Calendar = create_commercial(OwnerId, manual),
StartTime = eh_test_support:future_start(),
{ok, Event} = core_event:create(Calendar#calendar.id, <<"Event">>, StartTime, 60),
@@ -108,8 +108,7 @@ test_capacity_management() ->
test_multiple_bookings() ->
OwnerId = create_user(),
ParticipantId = create_user(),
{ok, Calendar} = core_calendar:create(OwnerId, <<"Test">>, <<"">>, manual),
Calendar = create_commercial(OwnerId, manual),
Base = eh_test_support:future_start(),
StartTime1 = Base,
+2 -1
View File
@@ -129,7 +129,8 @@ table_opts(calendar_share) ->
table_opts(calendar_follow) ->
[{ram_copies, [node()]}, {attributes, record_info(fields, calendar_follow)}];
table_opts(calendar_specialist) ->
[{ram_copies, [node()]}, {attributes, record_info(fields, calendar_specialist)}];
[{ram_copies, [node()]}, {attributes, record_info(fields, calendar_specialist)},
{index, [calendar_id, user_id]}];
table_opts(event) ->
[{ram_copies, [node()]}, {attributes, record_info(fields, event)}];
table_opts(recurrence_exception) ->
+48 -8
View File
@@ -2,7 +2,7 @@
-include_lib("eunit/include/eunit.hrl").
-include("records.hrl").
-define(TABLES, [user, calendar, event, booking, admin]).
-define(TABLES, [user, calendar, event, booking, admin, subscription, calendar_specialist]).
setup() ->
eh_test_support:start_mnesia(),
@@ -19,14 +19,17 @@ logic_booking_test_() ->
fun setup/0,
fun cleanup/1,
[
{"Create booking returns pending", fun test_create_booking_pending/0},
{"Create booking auto confirms", fun test_create_booking_auto/0},
{"Create booking manual pending", fun test_create_booking_pending/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 do not fill capacity", fun test_pending_does_not_fill/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},
@@ -59,8 +62,17 @@ create_test_admin() ->
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) ->
{ok, Calendar} = core_calendar:create(OwnerId, <<"Test Calendar">>, <<"">>, 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) ->
@@ -75,18 +87,34 @@ create_test_event_with_capacity(CalendarId, Capacity) ->
Updated#event.id.
%% Тесты
test_create_booking_pending() ->
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_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),
@@ -111,7 +139,7 @@ test_booking_event_full() ->
{ok, _} = logic_booking:confirm_booking(OwnerId, B1#booking.id, confirm),
{error, full} = logic_booking:create_booking(Participant2Id, EventId).
test_pending_does_not_fill() ->
test_pending_fills_capacity() ->
OwnerId = create_test_user(user),
Participant1Id = create_test_user(user),
Participant2Id = create_test_user(user),
@@ -119,8 +147,7 @@ test_pending_does_not_fill() ->
EventId = create_test_event_with_capacity(CalendarId, 1),
{ok, _} = logic_booking:create_booking(Participant1Id, EventId),
{ok, B2} = logic_booking:create_booking(Participant2Id, EventId),
?assertEqual(pending, B2#booking.status).
{error, full} = logic_booking:create_booking(Participant2Id, EventId).
test_confirm_booking() ->
OwnerId = create_test_user(user),
@@ -151,6 +178,19 @@ test_confirm_stranger_denied() ->
{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),
+5
View File
@@ -12,9 +12,14 @@ setup() ->
{attributes, record_info(fields, calendar)},
{ram_copies, [node()]}
]),
mnesia:create_table(subscription, [
{attributes, record_info(fields, subscription)},
{ram_copies, [node()]}
]),
ok.
cleanup(_) ->
mnesia:delete_table(subscription),
mnesia:delete_table(calendar),
mnesia:delete_table(user),
mnesia:stop(),
Regular → Executable
+7 -1
View File
@@ -2,7 +2,7 @@
-include_lib("eunit/include/eunit.hrl").
-include("records.hrl").
-define(TABLES, [user, calendar, event]).
-define(TABLES, [user, calendar, event, subscription]).
setup() ->
eh_test_support:start_mnesia(),
@@ -50,6 +50,12 @@ create_test_user(Role) ->
UserId.
create_test_calendar(OwnerId, Type, Tags) ->
case Type of
commercial ->
_ = core_subscription:create(OwnerId, monthly, true);
_ ->
ok
end,
{ok, Calendar} = core_calendar:create(OwnerId, <<"Test Calendar">>, <<"Description">>, manual),
core_calendar:update(Calendar#calendar.id, [{type, Type}, {tags, Tags}]),
{ok, Updated} = core_calendar:get_by_id(Calendar#calendar.id),
+2 -1
View File
@@ -11,7 +11,8 @@
"20260717180000_stats_counters",
"20260717190000_admin_stats_indexes",
"20260719210000_review_vote",
"20260720210000_calendar_follow"
"20260720210000_calendar_follow",
"20260722150000_calendar_specialist_id"
]).
setup() ->
Regular → Executable
+4 -1
View File
@@ -45,7 +45,10 @@ test_event_rating_top() ->
ok = mnesia:dirty_write(make_event(<<"e_low">>, 2.0, Now)),
ok = mnesia:dirty_write(make_event(<<"e_high">>, 5.0, Now)),
ok = mnesia:dirty_write(make_event(<<"e_mid">>, 3.5, Now)),
wait_top_event(<<"e_high">>, 30),
wait_until(fun() ->
[E#event.id || E <- core_event:get_top_events_by_rating(2)]
=:= [<<"e_high">>, <<"e_mid">>]
end, 40),
Top = core_event:get_top_events_by_rating(2),
Ids = [E#event.id || E <- Top],
?assertEqual([<<"e_high">>, <<"e_mid">>], Ids).