%%%------------------------------------------------------------------- %%% @doc Административный обработчик статистики по подпискам. %%% GET /v1/admin/subscriptions/stats – возвращает общее количество, %%% распределение по планам, статусам, пробные, заканчивающиеся платные. %%% @end %%%------------------------------------------------------------------- -module(admin_handler_subscription_stats). -behaviour(cowboy_handler). -export([init/2]). -export([trails/0]). -include("records.hrl"). %%% cowboy_handler callback -spec init(cowboy_req:req(), any()) -> {ok, cowboy_req:req(), any()}. init(Req, _Opts) -> case cowboy_req:method(Req) of <<"GET">> -> get_subscription_stats(Req); _ -> handler_utils:send_error(Req, 405, <<"Method not allowed">>) end. %%% Swagger metadata -spec trails() -> [map()]. trails() -> [ #{ path => <<"/v1/admin/subscriptions/stats">>, method => <<"GET">>, description => <<"Get detailed subscription statistics (total, by plan, by status, trial count, ending paid subscriptions)">>, tags => [<<"Statistics">>], responses => #{ 200 => #{ description => <<"Subscription statistics object">>, content => #{<<"application/json">> => #{schema => subscription_stats_schema()}} }, 403 => #{description => <<"Admin access required">>} } } ]. subscription_stats_schema() -> #{ type => object, properties => #{ <<"total_subscriptions">> => #{type => integer, description => <<"Total number of subscriptions">>}, <<"subscriptions_by_plan">> => #{ type => object, description => <<"Number of subscriptions grouped by plan">>, additionalProperties => #{type => integer} }, <<"subscriptions_by_status">> => #{ type => object, description => <<"Number of subscriptions grouped by status">>, additionalProperties => #{type => integer} }, <<"trial_subscriptions">> => #{type => integer, description => <<"Number of subscriptions in trial period (trial_used = false)">>}, <<"ending_paid_subscriptions">> => #{ type => array, description => <<"Paid subscriptions expiring within 30 days">>, items => #{ type => object, properties => #{ <<"id">> => #{type => string}, <<"user_id">> => #{type => string}, <<"plan">> => #{type => string}, <<"expires_at">> => #{type => string, format => <<"date-time">>} } } } } }. %%% Internal functions %% @doc GET /v1/admin/subscriptions/stats – статистика по подпискам. -spec get_subscription_stats(cowboy_req:req()) -> {ok, binary(), cowboy_req:req()}. get_subscription_stats(Req) -> case handler_utils:auth_admin(Req) of {ok, _AdminId, Req1} -> Total = core_subscription:count_subscriptions(), ByPlan = core_subscription:count_subscriptions_by_plan(), ByStatus = core_subscription:count_subscriptions_by_status(), Trial = core_subscription:count_trial_subscriptions(), EndingPaid = core_subscription:get_ending_paid_subscriptions(30), Stats = #{ <<"total_subscriptions">> => Total, <<"subscriptions_by_plan">> => maps:from_list([{atom_to_binary(Plan, utf8), Count} || {Plan, Count} <- ByPlan]), <<"subscriptions_by_status">> => maps:from_list([{atom_to_binary(Status, utf8), Count} || {Status, Count} <- ByStatus]), <<"trial_subscriptions">> => Trial, <<"ending_paid_subscriptions">> => lists:map(fun(Sub) -> #{ <<"id">> => Sub#subscription.id, <<"user_id">> => Sub#subscription.user_id, <<"plan">> => atom_to_binary(Sub#subscription.plan, utf8), <<"expires_at">> => handler_utils:datetime_to_iso8601(Sub#subscription.expires_at) } end, EndingPaid) }, handler_utils:send_json(Req1, 200, Stats); {error, Code, Msg, Req1} -> handler_utils:send_error(Req1, Code, Msg) end.