%%%------------------------------------------------------------------- %%% @doc Административный обработчик подписок. %%% GET – список с пагинацией и фильтрацией. %%% @end %%%------------------------------------------------------------------- -module(admin_handler_subscriptions). -behaviour(cowboy_handler). -export([init/2]). -export([trails/0]). -include("records.hrl"). -spec init(cowboy_req:req(), any()) -> {ok, cowboy_req:req(), any()}. init(Req, _Opts) -> case cowboy_req:method(Req) of <<"GET">> -> list_subscriptions(Req); _ -> handler_utils:send_error(Req, 405, <<"Method not allowed">>) end. -spec trails() -> [map()]. trails() -> [ #{ path => <<"/v1/admin/subscriptions">>, method => <<"GET">>, description => <<"List all subscriptions (admin)">>, tags => [<<"Subscriptions">>], parameters => [ #{name => <<"plan">>, in => <<"query">>, schema => #{type => string, enum => [<<"monthly">>, <<"quarterly">>, <<"biannual">>, <<"annual">>]}, description => <<"Filter by plan">>}, #{name => <<"status">>, in => <<"query">>, schema => #{type => string, enum => [<<"active">>, <<"expired">>, <<"cancelled">>]}, description => <<"Filter by status">>}, #{name => <<"limit">>, in => <<"query">>, schema => #{type => integer}, description => <<"Page size">>}, #{name => <<"offset">>, in => <<"query">>, schema => #{type => integer}, description => <<"Offset">>} ], responses => #{ 200 => #{ description => <<"Array of subscriptions">>, content => #{<<"application/json">> => #{schema => #{ type => array, items => subscription_schema() }}} } } } ]. subscription_schema() -> #{ type => object, properties => #{ id => #{type => string}, user_id => #{type => string}, plan => #{type => string, enum => [<<"monthly">>, <<"quarterly">>, <<"biannual">>, <<"annual">>]}, status => #{type => string, enum => [<<"active">>, <<"expired">>, <<"cancelled">>]}, trial_used => #{type => boolean}, started_at => #{type => string, format => <<"date-time">>}, expires_at => #{type => string, format => <<"date-time">>}, created_at => #{type => string, format => <<"date-time">>}, updated_at => #{type => string, format => <<"date-time">>} } }. %%% Internal functions list_subscriptions(Req) -> case handler_utils:auth_admin(Req) of {ok, _AdminId, Req1} -> Filters = parse_subscription_filters(Req1), Pagination = handler_utils:parse_pagination_params(Req1), {ok, AllSubscriptions} = core_subscription:list_all(), Filtered = apply_filters(AllSubscriptions, Filters), Sorted = sort_subscriptions(Filtered, Pagination), Total = length(Sorted), Page = lists:sublist(Sorted, maps:get(offset, Pagination) + 1, maps:get(limit, Pagination)), Json = [handler_utils:subscription_to_json(S) || S <- Page], ExtraHeaders = handler_utils:pagination_headers(Pagination, Total), handler_utils:send_json(Req1, 200, Json, ExtraHeaders); {error, Code, Msg, Req1} -> handler_utils:send_error(Req1, Code, Msg) end. parse_subscription_filters(Req) -> Qs = cowboy_req:parse_qs(Req), #{ plan => proplists:get_value(<<"plan">>, Qs), status => proplists:get_value(<<"status">>, Qs) }. apply_filters(Subs, Filters) -> PlanBin = maps:get(plan, Filters, undefined), StatusBin = maps:get(status, Filters, undefined), F1 = case PlanBin of undefined -> Subs; _ -> Plan = try binary_to_existing_atom(PlanBin, utf8) catch error:badarg -> PlanBin end, [S || S <- Subs, S#subscription.plan =:= Plan] end, case StatusBin of undefined -> F1; _ -> Status = try binary_to_existing_atom(StatusBin, utf8) catch error:badarg -> StatusBin end, [S || S <- F1, S#subscription.status =:= Status] end. sort_subscriptions(Subs, #{sort := Sort, order := Order}) -> Field = binary_to_existing_atom(Sort, utf8), lists:sort( fun(A, B) -> ValA = sub_field(A, Field), ValB = sub_field(B, Field), if Order == <<"asc">> -> ValA =< ValB; true -> ValA >= ValB end end, Subs). sub_field(#subscription{created_at = V}, created_at) -> V; sub_field(#subscription{expires_at = V}, expires_at) -> V; sub_field(_, _) -> undefined.