%%%------------------------------------------------------------------- %%% @doc Обработчик подписки текущего пользователя (клиентский API). %%% %%% GET – получить информацию о подписке. %%% POST – активировать подписку или начать пробный период. %%% @end %%%------------------------------------------------------------------- -module(handler_subscription). -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) -> handle(Req, Opts). %%% Swagger metadata -spec trails() -> [map()]. trails() -> [ #{ % GET path => <<"/v1/subscription">>, method => <<"GET">>, description => <<"Get current user subscription">>, tags => [<<"Subscription">>], responses => #{ 200 => #{ description => <<"Subscription details">>, content => #{<<"application/json">> => #{schema => subscription_schema()}} }, 401 => #{description => <<"Unauthorized">>} } }, #{ % POST path => <<"/v1/subscription">>, method => <<"POST">>, description => <<"Activate subscription or start trial">>, tags => [<<"Subscription">>], requestBody => #{ required => true, content => #{<<"application/json">> => #{schema => #{ type => object, required => [<<"action">>], properties => #{ action => #{type => string, enum => [<<"start_trial">>, <<"activate">>]}, plan => #{type => string, enum => [<<"monthly">>, <<"quarterly">>, <<"biannual">>, <<"annual">>]}, payment_info => #{type => object, description => <<"Payment information">>} } }}} }, responses => #{ 201 => #{description => <<"Subscription activated or trial started">>}, 400 => #{description => <<"Invalid action or JSON">>}, 402 => #{description => <<"Payment failed">>}, 409 => #{description => <<"Already has active subscription">>} } } ]. 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">>} } }. %%%=================================================================== %%% HTTP-методы %%%=================================================================== %% @private -spec handle(cowboy_req:req(), any()) -> {ok, cowboy_req:req(), any()}. handle(Req, _Opts) -> case cowboy_req:method(Req) of <<"GET">> -> get_subscription(Req); <<"POST">> -> create_subscription(Req); _ -> handler_utils:send_error(Req, 405, <<"Method not allowed">>) end. %% @doc GET /v1/subscription — получить подписку текущего пользователя. -spec get_subscription(cowboy_req:req()) -> {ok, binary(), cowboy_req:req()}. get_subscription(Req) -> case handler_utils:auth_user(Req) of {ok, UserId, Req1} -> case logic_subscription:get_user_subscription(UserId) of {ok, Subscription} -> handler_utils:send_json(Req1, 200, subscription_to_json(Subscription)); {error, _} -> handler_utils:send_error(Req1, 500, <<"Internal server error">>) end; {error, Code, Message, Req1} -> handler_utils:send_error(Req1, Code, Message) end. %% @doc POST /v1/subscription — активировать подписку. -spec create_subscription(cowboy_req:req()) -> {ok, binary(), cowboy_req:req()}. create_subscription(Req) -> case handler_utils:auth_user(Req) of {ok, UserId, Req1} -> {ok, Body, Req2} = cowboy_req:read_body(Req1), try jsx:decode(Body, [return_maps]) of Decoded when is_map(Decoded) -> case Decoded of #{<<"action">> := <<"start_trial">>} -> case logic_subscription:start_trial(UserId) of {ok, Subscription} -> Response = subscription_to_json(Subscription), handler_utils:send_json(Req2, 201, Response); {error, already_has_subscription} -> handler_utils:send_error(Req2, 409, <<"Already has active subscription">>); {error, _} -> handler_utils:send_error(Req2, 500, <<"Internal server error">>) end; #{<<"action">> := <<"activate">>, <<"plan">> := PlanBin} -> Plan = parse_plan(PlanBin), PaymentInfo = maps:get(<<"payment_info">>, Decoded, #{}), case logic_subscription:activate_subscription(UserId, Plan, PaymentInfo) of {ok, Subscription} -> Response = subscription_to_json(Subscription), handler_utils:send_json(Req2, 201, Response); {error, payment_failed} -> handler_utils:send_error(Req2, 402, <<"Payment failed">>); {error, _} -> handler_utils:send_error(Req2, 500, <<"Internal server error">>) end; _ -> handler_utils:send_error(Req2, 400, <<"Invalid action. Use 'start_trial' or 'activate'">>) end; _ -> handler_utils:send_error(Req2, 400, <<"Invalid JSON">>) catch _:_ -> handler_utils:send_error(Req2, 400, <<"Invalid JSON format">>) end; {error, Code, Message, Req1} -> handler_utils:send_error(Req1, Code, Message) end. %%%=================================================================== %%% Внутренние функции %%%=================================================================== %% @private Преобразует бинарное имя плана в атом. -spec parse_plan(binary()) -> monthly | quarterly | biannual | annual. parse_plan(<<"monthly">>) -> monthly; parse_plan(<<"quarterly">>) -> quarterly; parse_plan(<<"biannual">>) -> biannual; parse_plan(<<"annual">>) -> annual; parse_plan(_) -> monthly. %% @private Формирует JSON-представление подписки (поддерживает как запись, так и карту). -spec subscription_to_json(#subscription{} | map()) -> map(). subscription_to_json(Subscription) when is_tuple(Subscription) -> handler_utils:subscription_to_json(Subscription); subscription_to_json(Subscription) when is_map(Subscription) -> Subscription.