Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Build artifacts
/_build/
/_checkouts/
*.o
*.beam
*.plt
Expand Down
11 changes: 11 additions & 0 deletions apps/capi/src/capi.erl
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
-spec start(normal, any()) -> {ok, pid()} | {error, any()}.
start(_StartType, _StartArgs) ->
ok = setup_metrics(),
ok = validate_checkout_url_generation_config(),
capi_sup:start_link().

-spec stop(any()) -> ok.
Expand All @@ -24,3 +25,13 @@ stop(_State) ->
setup_metrics() ->
ok = woody_ranch_prometheus_collector:setup(),
ok = woody_hackney_prometheus_collector:setup().

validate_checkout_url_generation_config() ->
case genlib_app:env(capi, checkout_url_generation) of
undefined ->
erlang:throw({missing, [capi, checkout_url_generation]});
#{default_base_url := V} when is_binary(V) ->
ok;
_ ->
erlang:throw({missing, [capi, checkout_url_generation, default_base_url]})
end.
16 changes: 11 additions & 5 deletions apps/capi/src/capi_handler_decoder_invoicing.erl
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
-export([decode_recurrent_parent/1]).
-export([decode_make_recurrent/1]).

-export([make_invoice_and_token/2]).
-export([make_invoice_and_token/3]).

-type processing_context() :: capi_handler:processing_context().
-type decode_data() :: capi_handler_decoder_utils:decode_data().
Expand Down Expand Up @@ -718,12 +718,18 @@ decode_mobile_phone(#domain_MobilePhone{cc = Cc, ctn = Ctn}) ->
gen_phone_number(#{<<"cc">> := Cc, <<"ctn">> := Ctn}) ->
<<"+", Cc/binary, Ctn/binary>>.

-spec make_invoice_and_token(capi_handler_encoder:encode_data(), processing_context()) ->
capi_handler_decoder_utils:decode_data().
make_invoice_and_token(Invoice, ProcessingContext) ->
-spec make_invoice_and_token(
capi_handler_encoder:encode_data(),
capi_handler_utils:url_params(),
processing_context()
) -> capi_handler_decoder_utils:decode_data().
make_invoice_and_token(Invoice, UrlParams, ProcessingContext) ->
#{<<"payload">> := AccessToken} =
InvoiceAccessToken = capi_handler_utils:issue_access_token(Invoice, ProcessingContext),
#{
<<"invoice">> => decode_invoice(Invoice),
<<"invoiceAccessToken">> => capi_handler_utils:issue_access_token(Invoice, ProcessingContext)
<<"invoiceAccessToken">> => InvoiceAccessToken,
<<"invoiceUrl">> => capi_handler_utils:create_checkout_url(Invoice, AccessToken, UrlParams, ProcessingContext)
}.

%%
Expand Down
53 changes: 36 additions & 17 deletions apps/capi/src/capi_handler_invoice_templates.erl
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,13 @@
-behaviour(woody_server_thrift_handler).
-export([handle_function/4]).

-import(capi_handler_utils, [general_error/2, logic_error/2, conflict_error/1, map_service_result/1]).
-import(capi_handler_utils, [
general_error/2,
logic_error/2,
conflict_error/1,
invalid_url_params_error/1,
map_service_result/1
]).

-spec prepare(
OperationID :: capi_handler:operation_id(),
Expand Down Expand Up @@ -156,24 +162,31 @@ prepare('CreateInvoiceWithTemplate' = OperationID, Req, Context) ->
Process = fun() ->
capi_handler:respond_if_undefined(InvoiceTpl, general_error(404, <<"Invoice template not found">>)),
InvoiceParams = maps:get('InvoiceParamsWithTemplate', Req),
UrlParams = maps:get(<<"urlParams">>, InvoiceParams, #{}),
PartyID = InvoiceTpl#domain_InvoiceTemplate.party_ref#domain_PartyConfigRef.id,
try create_invoice(PartyID, InvoiceTplID, InvoiceParams, Context, OperationID) of
{ok, #'payproc_Invoice'{invoice = Invoice}} ->
{ok, {201, #{}, capi_handler_decoder_invoicing:make_invoice_and_token(Invoice, Context)}};
{exception, #base_InvalidRequest{errors = Errors}} ->
FormattedErrors = capi_handler_utils:format_request_errors(Errors),
{ok, logic_error('invalidRequest', FormattedErrors)};
{exception, #payproc_InvalidPartyStatus{}} ->
{ok, logic_error('invalidPartyStatus', <<"Invalid party status">>)};
{exception, #payproc_InvalidShopStatus{}} ->
{ok, logic_error('invalidShopStatus', <<"Invalid shop status">>)};
{exception, #payproc_InvoiceTemplateNotFound{}} ->
{ok, general_error(404, <<"Invoice Template not found">>)};
{exception, #payproc_InvoiceTemplateRemoved{}} ->
{ok, general_error(404, <<"Invoice Template not found">>)};
{exception, #payproc_InvoiceTermsViolated{}} ->
{ok, logic_error('invoiceTermsViolated', <<"Invoice parameters violate terms">>)}
try
ok = validate_checkout_url_params(UrlParams),
case create_invoice(PartyID, InvoiceTplID, InvoiceParams, Context, OperationID) of
{ok, #'payproc_Invoice'{invoice = Invoice}} ->
{ok,
{201, #{}, capi_handler_decoder_invoicing:make_invoice_and_token(Invoice, UrlParams, Context)}};
{exception, #base_InvalidRequest{errors = Errors}} ->
FormattedErrors = capi_handler_utils:format_request_errors(Errors),
{ok, logic_error('invalidRequest', FormattedErrors)};
{exception, #payproc_InvalidPartyStatus{}} ->
{ok, logic_error('invalidPartyStatus', <<"Invalid party status">>)};
{exception, #payproc_InvalidShopStatus{}} ->
{ok, logic_error('invalidShopStatus', <<"Invalid shop status">>)};
{exception, #payproc_InvoiceTemplateNotFound{}} ->
{ok, general_error(404, <<"Invoice Template not found">>)};
{exception, #payproc_InvoiceTemplateRemoved{}} ->
{ok, general_error(404, <<"Invoice Template not found">>)};
{exception, #payproc_InvoiceTermsViolated{}} ->
{ok, logic_error('invoiceTermsViolated', <<"Invoice parameters violate terms">>)}
end
catch
throw:{invalid_url_params, Reason} ->
{ok, invalid_url_params_error(Reason)};
throw:{bad_invoice_params, currency_no_amount} ->
{ok, logic_error('invalidRequest', <<"Amount is required for the currency">>)};
throw:{bad_invoice_params, amount_no_currency} ->
Expand Down Expand Up @@ -286,6 +299,12 @@ mask_invoice_template_notfound(Resolution) ->

%%

validate_checkout_url_params(UrlParams) ->
case capi_handler_utils:validate_checkout_url_params(UrlParams) of
ok -> ok;
{error, Reason} -> erlang:throw({invalid_url_params, Reason})
end.

create_invoice(PartyID, InvoiceTplID, InvoiceParams, Context, BenderPrefix) ->
#{woody_context := WoodyCtx} = Context,
% CAPI#344: Since the prefixes are different, it's possible to create 2 copies of the same Invoice with the same
Expand Down
46 changes: 44 additions & 2 deletions apps/capi/src/capi_handler_invoices.erl
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,13 @@

-export([prepare/3]).

-import(capi_handler_utils, [general_error/2, logic_error/2, conflict_error/1, map_service_result/1]).
-import(capi_handler_utils, [
general_error/2,
logic_error/2,
conflict_error/1,
invalid_url_params_error/1,
map_service_result/1
]).

-spec prepare(
OperationID :: capi_handler:operation_id(),
Expand All @@ -28,11 +34,14 @@ prepare('CreateInvoice' = OperationID, Req, Context) ->
end,
Process = fun() ->
try
UrlParams = maps:get(<<"urlParams">>, InvoiceParams, #{}),
ok = validate_checkout_url_params(UrlParams),
Allocation = maps:get(<<"allocation">>, InvoiceParams, undefined),
ok = validate_allocation(Allocation),
case create_invoice(PartyID, InvoiceParams, Context, OperationID) of
{ok, #'payproc_Invoice'{invoice = Invoice}} ->
{ok, {201, #{}, capi_handler_decoder_invoicing:make_invoice_and_token(Invoice, Context)}};
{ok,
{201, #{}, capi_handler_decoder_invoicing:make_invoice_and_token(Invoice, UrlParams, Context)}};
{exception, #'payproc_PartyNotFound'{}} ->
{ok, logic_error('invalidPartyID', <<"Party not found">>)};
{exception, #base_InvalidRequest{errors = Errors}} ->
Expand All @@ -55,6 +64,8 @@ prepare('CreateInvoice' = OperationID, Req, Context) ->
{ok, logic_error('invalidAllocation', Message)}
end
catch
throw:{invalid_url_params, Reason} ->
{ok, invalid_url_params_error(Reason)};
throw:invoice_cart_empty ->
{ok, logic_error('invalidInvoiceCart', <<"Wrong size. Path to item: cart">>)};
throw:invalid_invoice_cost ->
Expand Down Expand Up @@ -86,6 +97,31 @@ prepare('CreateInvoiceAccessToken' = OperationID, Req, Context) ->
{ok, {201, #{}, Response}}
end,
{ok, #{authorize => Authorize, process => Process}};
prepare('CreateInvoiceUrl' = OperationID, Req, Context) ->
InvoiceID = maps:get('invoiceID', Req),
ResultInvoice = map_service_result(capi_handler_utils:get_invoice_by_id(InvoiceID, Context)),
Authorize = fun() ->
Prototypes = [
{operation, #{id => OperationID, invoice => InvoiceID}},
{payproc, #{invoice => ResultInvoice}}
],
Resolution = capi_auth:authorize_operation(Prototypes, Context),
{ok, Resolution}
end,
Process = fun() ->
UrlParams = maps:get('InvoiceUrlParams', Req),
case capi_handler_utils:validate_checkout_url_params(UrlParams) of
ok ->
capi_handler:respond_if_undefined(ResultInvoice, general_error(404, <<"Invoice not found">>)),
Invoice = ResultInvoice#payproc_Invoice.invoice,
#{<<"payload">> := AccessToken} = capi_handler_utils:issue_access_token(Invoice, Context),
Response = capi_handler_utils:create_checkout_url(Invoice, AccessToken, UrlParams, Context),
{ok, {201, #{}, Response}};
{error, Reason} ->
{ok, invalid_url_params_error(Reason)}
end
end,
{ok, #{authorize => Authorize, process => Process}};
prepare('GetInvoiceByID' = OperationID, Req, Context) ->
InvoiceID = maps:get('invoiceID', Req),
ResultInvoice = map_service_result(capi_handler_utils:get_invoice_by_id(InvoiceID, Context)),
Expand Down Expand Up @@ -256,6 +292,12 @@ prepare(_OperationID, _Req, _Context) ->

%%

validate_checkout_url_params(UrlParams) ->
case capi_handler_utils:validate_checkout_url_params(UrlParams) of
ok -> ok;
{error, Reason} -> erlang:throw({invalid_url_params, Reason})
end.

validate_allocation(Allocation) ->
case capi_allocation:validate(Allocation) of
ok -> ok;
Expand Down
81 changes: 81 additions & 0 deletions apps/capi/src/capi_handler_utils.erl
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
-export([logic_error/2]).
-export([server_error/1]).
-export([format_request_errors/1]).
-export([invalid_url_params_error/1]).

-export([assert_party_accessible/2]).
-export([run_if_party_accessible/3]).
Expand All @@ -24,6 +25,8 @@
-export([get_party_id/1]).

-export([issue_access_token/2]).
-export([validate_checkout_url_params/1]).
-export([create_checkout_url/4]).
-export([merge_and_compact/2]).
-export([get_time/2]).
-export([collect_events/4]).
Expand All @@ -36,6 +39,8 @@

-export([emplace_token_provider_data/3]).

-export_type([url_params/0]).

-type processing_context() :: capi_handler:processing_context().
-type response() :: capi_handler:response().
-type entity() ::
Expand All @@ -44,6 +49,8 @@
| dmsl_customer_thrift:'Customer'().
-type token_source() :: capi_auth:token_spec() | entity().

-type url_params() :: #{binary() => binary()}.

-spec conflict_error(binary() | {binary(), binary()}) -> response().
conflict_error({ID, ExternalID}) ->
Data = #{
Expand Down Expand Up @@ -86,6 +93,19 @@ server_error(Code) when Code >= 500 andalso Code < 600 ->
format_request_errors([]) -> <<>>;
format_request_errors(Errors) -> genlib_string:join(<<"\n">>, Errors).

-spec invalid_url_params_error(term()) -> response().
invalid_url_params_error({bad_keys, [_ | _] = BadKeys, Whitelist}) when is_list(BadKeys) andalso is_list(Whitelist) ->
Message = [
<<"Bad keys: ">>,
genlib_string:join(<<", ">>, BadKeys),
<<$\n>>,
<<"Allowed keys: ">>,
genlib_string:join(<<", ">>, Whitelist)
],
logic_error('invalidUrlParams', genlib_string:join(<<>>, Message));
invalid_url_params_error(_Reason) ->
logic_error('invalidUrlParams', <<"Bad URL params">>).

%%%

-spec service_call({atom(), atom(), tuple()}, processing_context()) -> woody:result().
Expand Down Expand Up @@ -120,6 +140,67 @@ get_party_id(Context) ->

%% Utils

-spec validate_checkout_url_params(url_params()) ->
ok
| {error,
Reason ::
{invalid_input | invalid_encoding, term()}
| {bad_keys, [binary()], [binary()]}}.
validate_checkout_url_params(Params0) ->
UrlGenOpts = genlib_app:env(capi, checkout_url_generation),
Whitelist = maps:get(params_whitelist, UrlGenOpts, []),
case maps:keys(maps:without(Whitelist, Params0)) of
[] ->
Params1 = maps:with(Whitelist, Params0),
case uri_string:compose_query(maps:to_list(Params1), [{encoding, utf8}]) of
{error, Error, Term} ->
{error, {Error, Term}};
_ ->
ok
end;
BadKeys ->
{error, {bad_keys, BadKeys, Whitelist}}
end.

-spec create_checkout_url(
dmsl_domain_thrift:'Invoice'(),
token_keeper_client:token(),
url_params(),
processing_context()
) -> map() | no_return().
create_checkout_url(Invoice, AccessToken, Params0, ProcessingContext) ->
UrlGenOpts = genlib_app:env(capi, checkout_url_generation),
Params1 = maps:with(maps:get(params_whitelist, UrlGenOpts, []), Params0),
%% TODO Warn if params filtered out
Params2 = maps:merge(Params1, #{
<<"invoiceID">> => Invoice#domain_Invoice.id,
<<"invoiceAccessToken">> => AccessToken
}),
BaseUrl = get_base_url(Invoice, UrlGenOpts, ProcessingContext),
%% TODO Sanitize params?
case uri_string:compose_query(maps:to_list(Params2), [{encoding, utf8}]) of
{error, Error, Term} ->
erlang:throw({Error, Term});
EncodedParams ->
#{<<"url">> => <<BaseUrl/binary, $?, EncodedParams/binary>>}
end.

get_base_url(
#domain_Invoice{party_ref = #domain_PartyConfigRef{id = PartyID}, shop_ref = #domain_ShopConfigRef{id = ShopID}},
#{default_base_url := Default},
ProcessingContext
) ->
case capi_party:get_shop(PartyID, ShopID, ProcessingContext) of
{ok, #domain_ShopConfig{
checkout_location = #domain_ShopCheckoutLocation{
locations = [#domain_CheckoutLocation{base_url = V} | _]
}
}} ->
V;
_ ->
Default
end.

-spec issue_access_token(token_source(), processing_context()) -> map().
issue_access_token(#domain_Invoice{} = Invoice, ProcessingContext) ->
TokenSpec = #{
Expand Down
Loading
Loading