fix: owner event bookings list and paid plan durations from plan_to_months.
CI / test (push) Successful in 6m55s
CI / deploy-ift (push) Successful in 3m33s
CI / e2e-ift (push) Failing after 1m4s
CI / deploy-stage (push) Has been skipped
CI / e2e-stage (push) Has been skipped

Fixes EventHub/EventHubBack#51
Fixes EventHub/EventHubBack#52

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-21 20:18:49 +03:00
parent 7368adfb37
commit 5ac23219ca
6 changed files with 133 additions and 26 deletions
+6 -19
View File
@@ -8,35 +8,27 @@
-export([count_subscriptions_by_plan/0, count_subscriptions_by_status/0,
count_trial_subscriptions/0, get_ending_paid_subscriptions/1]).
-define(TRIAL_DAYS, 30).
%%%-------------------------------------------------------------------
%%% @doc Создание подписки.
%%% `TrialUsed` `true`, если подписка платная; `false` для пробного периода.
%%% `TrialUsed` флаг (использован ли trial); на длительность не влияет.
%%% Длительность всегда считается из `Plan` через `plan_to_months/1`.
%%% Все поля записи инициализированы, `undefined` не возникает.
%%% @end
%%%-------------------------------------------------------------------
-spec create(UserId :: binary(), Plan :: monthly | quarterly | biannual | annual,
-spec create(UserId :: binary(), Plan :: monthly | quarterly | biannual | annual | trial,
TrialUsed :: boolean()) -> {ok, #subscription{}} | {error, term()}.
create(UserId, Plan, TrialUsed) ->
Id = infra_utils:generate_id(16),
Now = calendar:universal_time(),
{StartDate, EndDate} = case TrialUsed of
true ->
DurationMonths = plan_to_months(Plan),
End = add_months(Now, DurationMonths),
{Now, End};
false ->
End = add_days(Now, ?TRIAL_DAYS),
{Now, End}
end,
DurationMonths = plan_to_months(Plan),
EndDate = add_months(Now, DurationMonths),
Subscription = #subscription{
id = Id,
user_id = UserId,
plan = Plan,
status = active,
trial_used = TrialUsed,
started_at = StartDate,
started_at = Now,
expires_at = EndDate,
created_at = Now,
updated_at = Now
@@ -167,11 +159,6 @@ add_months(DateTime, Months) ->
NewDays = Days + (Months * 30),
calendar:gregorian_seconds_to_datetime(NewDays * 86400).
-spec add_days(calendar:datetime(), pos_integer()) -> calendar:datetime().
add_days(DateTime, Days) ->
Seconds = calendar:datetime_to_gregorian_seconds(DateTime),
calendar:gregorian_seconds_to_datetime(Seconds + (Days * 86400)).
%%%===================================================================
%%% Новые обёртки для админки
%%%===================================================================
+27 -2
View File
@@ -3,7 +3,8 @@
-export([create_booking/2, confirm_booking/2, confirm_booking/3,
cancel_booking/2, cancel_booking/3, get_booking/2,
list_bookings/2, list_user_bookings/1, delete_booking/2,
list_bookings_admin/0, get_booking_admin/1, list_event_bookings/1]).
list_bookings_admin/0, get_booking_admin/1,
list_event_bookings/1, list_event_bookings/2]).
%%%-------------------------------------------------------------------
%%% @doc Создание бронирования со статусом `pending`.
@@ -151,13 +152,37 @@ get_booking_admin(BookingId) ->
core_booking:get_by_id(BookingId).
%%%-------------------------------------------------------------------
%%% @doc Список бронирований события (административный).
%%% @doc Список бронирований события (административный / внутренний).
%%% @end
%%%-------------------------------------------------------------------
-spec list_event_bookings(EventId :: binary()) -> {ok, [#booking{}]}.
list_event_bookings(EventId) ->
core_booking:list_by_event(EventId).
%%%-------------------------------------------------------------------
%%% @doc Список бронирований события для владельца календаря (или admin).
%%% Возвращает полный список заявок без фильтрации по участнику.
%%% @end
%%%-------------------------------------------------------------------
-spec list_event_bookings(UserId :: binary(), EventId :: binary()) ->
{ok, [#booking{}]} | {error, not_found | access_denied}.
list_event_bookings(UserId, EventId) ->
case core_event:get_by_id(EventId) of
{ok, Event} ->
case core_calendar:get_by_id(Event#event.calendar_id) of
{ok, Calendar} ->
case admin_utils:is_admin(UserId)
orelse Calendar#calendar.owner_id =:= UserId of
true -> core_booking:list_by_event(EventId);
false -> {error, access_denied}
end;
{error, not_found} ->
{error, not_found}
end;
{error, not_found} ->
{error, not_found}
end.
%%%-------------------------------------------------------------------
%%% @doc Список всех бронирований (административный).
%%% @end
+3 -4
View File
@@ -6,8 +6,6 @@
-export([check_user_subscription/1, can_create_commercial_calendar/1]).
-export([handle_expired_subscriptions/0]).
-define(TRIAL_DAYS, 30).
%% ============ Управление подписками ============
%% Начать пробный период (вызывается при первой попытке создать commercial календарь)
@@ -22,6 +20,7 @@ start_trial(UserId) ->
true ->
{error, trial_already_used};
false ->
% trial_used=true — флаг; длительность из plan=trial (~1 месяц)
case core_subscription:create(UserId, trial, true) of
{ok, Subscription} ->
{ok, Subscription};
@@ -34,7 +33,7 @@ start_trial(UserId) ->
activate_subscription(UserId, Plan, PaymentInfo) ->
case process_payment(PaymentInfo, plan_price(Plan)) of
ok ->
% Проверяем, была ли у пользователя хоть одна подписка
% Флаг trial_used: была ли уже любая подписка (не влияет на длительность)
{ok, AllSubs} = core_subscription:list_by_user(UserId),
TrialUsed = length(AllSubs) > 0,
@@ -45,7 +44,7 @@ activate_subscription(UserId, Plan, PaymentInfo) ->
_ -> ok
end,
% Создаём новую подписку
% Длительность всегда из Plan (plan_to_months), TrialUsed — только флаг
case core_subscription:create(UserId, Plan, TrialUsed) of
{ok, Subscription} ->
{ok, Subscription};
+33
View File
@@ -27,6 +27,10 @@ core_subscription_test_() ->
[
{"Create trial subscription", fun test_create_trial/0},
{"Create paid subscription", fun test_create_paid/0},
{"Create monthly duration", fun test_create_monthly_duration/0},
{"Create quarterly duration", fun test_create_quarterly_duration/0},
{"Create biannual duration", fun test_create_biannual_duration/0},
{"Create annual duration", fun test_create_annual_duration/0},
{"Get active by user", fun test_get_active_by_user/0},
{"List by user", fun test_list_by_user/0},
{"Update status", fun test_update_status/0},
@@ -51,6 +55,35 @@ test_create_paid() ->
?assertEqual(true, Sub#subscription.trial_used),
?assertEqual(active, Sub#subscription.status).
%% Длительность не зависит от trial_used — только от Plan (месяц ≈ 30 дней).
%% add_months считает по календарным дням от полуночи даты старта.
assert_plan_duration(Sub, Months) ->
{{Y1, M1, D1}, _} = Sub#subscription.started_at,
{{Y2, M2, D2}, Time2} = Sub#subscription.expires_at,
StartDays = calendar:date_to_gregorian_days({Y1, M1, D1}),
EndDays = calendar:date_to_gregorian_days({Y2, M2, D2}),
?assertEqual(Months * 30, EndDays - StartDays),
?assertEqual({0, 0, 0}, Time2).
test_create_monthly_duration() ->
%% Даже при trial_used=false длительность = 1 месяц, не «trial 30 дней» отдельно
{ok, Sub} = core_subscription:create(<<"u-m">>, monthly, false),
?assertEqual(false, Sub#subscription.trial_used),
assert_plan_duration(Sub, 1).
test_create_quarterly_duration() ->
{ok, Sub} = core_subscription:create(<<"u-q">>, quarterly, false),
?assertEqual(false, Sub#subscription.trial_used),
assert_plan_duration(Sub, 3).
test_create_biannual_duration() ->
{ok, Sub} = core_subscription:create(<<"u-b">>, biannual, true),
assert_plan_duration(Sub, 6).
test_create_annual_duration() ->
{ok, Sub} = core_subscription:create(<<"u-a">>, annual, false),
assert_plan_duration(Sub, 12).
test_get_active_by_user() ->
UserId = <<"user123">>,
{ok, Sub1} = core_subscription:create(UserId, trial, false),
+28 -1
View File
@@ -2,7 +2,7 @@
-include_lib("eunit/include/eunit.hrl").
-include("records.hrl").
-define(TABLES, [user, calendar, event, booking]).
-define(TABLES, [user, calendar, event, booking, admin]).
setup() ->
eh_test_support:start_mnesia(),
@@ -29,6 +29,8 @@ logic_booking_test_() ->
{"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}
]}.
@@ -163,6 +165,31 @@ test_list_event_bookings() ->
{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),
+36
View File
@@ -11,6 +11,10 @@ logic_subscription_test_() ->
{"Start trial duplicate", fun test_start_trial_duplicate/0},
{"Activate subscription (no trial)", fun test_activate_subscription_no_trial/0},
{"Activate subscription (after trial)", fun test_activate_subscription_after_trial/0},
{"Activate quarterly duration", fun test_activate_quarterly_duration/0},
{"Activate monthly duration", fun test_activate_monthly_duration/0},
{"Activate biannual duration", fun test_activate_biannual_duration/0},
{"Activate annual duration", fun test_activate_annual_duration/0},
{"Check subscription - free", fun test_check_free/0},
{"Check subscription - trial", fun test_check_trial/0},
{"Check subscription - paid", fun test_check_paid/0},
@@ -76,6 +80,38 @@ test_activate_subscription_after_trial() ->
?assertEqual(monthly, Sub#subscription.plan),
?assertEqual(true, Sub#subscription.trial_used).
%% Длительность при активации (в т.ч. первой платной с trial_used=false) из Plan.
assert_plan_duration(Sub, Months) ->
{{Y1, M1, D1}, _} = Sub#subscription.started_at,
{{Y2, M2, D2}, Time2} = Sub#subscription.expires_at,
StartDays = calendar:date_to_gregorian_days({Y1, M1, D1}),
EndDays = calendar:date_to_gregorian_days({Y2, M2, D2}),
?assertEqual(Months * 30, EndDays - StartDays),
?assertEqual({0, 0, 0}, Time2).
test_activate_monthly_duration() ->
UserId = create_test_user(),
{ok, Sub} = logic_subscription:activate_subscription(UserId, monthly, #{card => "4242"}),
?assertEqual(false, Sub#subscription.trial_used),
assert_plan_duration(Sub, 1).
test_activate_quarterly_duration() ->
UserId = create_test_user(),
{ok, Sub} = logic_subscription:activate_subscription(UserId, quarterly, #{card => "4242"}),
?assertEqual(quarterly, Sub#subscription.plan),
?assertEqual(false, Sub#subscription.trial_used),
assert_plan_duration(Sub, 3).
test_activate_biannual_duration() ->
UserId = create_test_user(),
{ok, Sub} = logic_subscription:activate_subscription(UserId, biannual, #{card => "4242"}),
assert_plan_duration(Sub, 6).
test_activate_annual_duration() ->
UserId = create_test_user(),
{ok, Sub} = logic_subscription:activate_subscription(UserId, annual, #{card => "4242"}),
assert_plan_duration(Sub, 12).
test_check_free() ->
UserId = create_test_user(),
{ok, free, free} = logic_subscription:check_user_subscription(UserId).