diff --git a/apps/capi/src/capi_handler_invoice_templates.erl b/apps/capi/src/capi_handler_invoice_templates.erl index 68f221c..b1ccc8d 100644 --- a/apps/capi/src/capi_handler_invoice_templates.erl +++ b/apps/capi/src/capi_handler_invoice_templates.erl @@ -3,11 +3,14 @@ -include_lib("damsel/include/dmsl_base_thrift.hrl"). -include_lib("damsel/include/dmsl_domain_thrift.hrl"). -include_lib("damsel/include/dmsl_payproc_thrift.hrl"). +-include_lib("damsel/include/dmsl_api_ext_thrift.hrl"). -behaviour(capi_handler). - -export([prepare/3]). +-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]). -spec prepare( @@ -212,6 +215,66 @@ prepare('GetInvoicePaymentMethodsByTemplateID' = OperationID, Req, Context) -> prepare(_OperationID, _Req, _Context) -> {error, noimpl}. +-spec handle_function(woody:func(), woody:args(), woody_context:ctx(), _) -> + {ok, term()} | no_return(). +handle_function(Function, Args, WoodyContext, Opts) -> + scoper:scope( + invoice_templating, + fun() -> + try handle_function_(Function, Args, WoodyContext, Opts) of + {ok, _} = Result -> + Result; + {exception, #payproc_InvoiceTemplateNotFound{} = Exception} -> + woody_error:raise(business, Exception); + {exception, #payproc_InvoiceTemplateRemoved{} = Exception} -> + woody_error:raise(business, Exception); + {exception, #payproc_PartyNotFound{} = Exception} -> + woody_error:raise(business, Exception); + {exception, #payproc_ShopNotFound{} = Exception} -> + woody_error:raise(business, Exception); + {exception, #payproc_InvalidPartyStatus{} = Exception} -> + woody_error:raise(business, Exception); + {exception, #payproc_InvalidShopStatus{} = Exception} -> + woody_error:raise(business, Exception); + {exception, #base_InvalidRequest{errors = Errors}} -> + woody_error:raise(business, #base_InvalidRequest{errors = Errors}) + catch + throw:(#payproc_InvoiceTemplateNotFound{} = Exception) -> + woody_error:raise(business, Exception); + throw:(#payproc_InvoiceTemplateRemoved{} = Exception) -> + woody_error:raise(business, Exception); + throw:invoice_cart_empty -> + woody_error:raise(business, #base_InvalidRequest{errors = [<<"Wrong size. Path to item: cart">>]}); + throw:zero_invoice_lifetime -> + woody_error:raise(business, #base_InvalidRequest{errors = [<<"Lifetime cannot be zero">>]}); + throw:{external_id_conflict, _ID, _UsedExternalID, _Schema} -> + woody_error:raise(business, #base_InvalidRequest{ + errors = [<<"This 'externalID' has been used by another request">>] + }) + end + end + ). + +handle_function_('Create', {InvoiceTemplateParams}, WoodyContext, _Opts) -> + %% NOTE Use same operation ID as the original in swagger/JSON API + InvoiceTemplateID = generate_thrift_invoice_template_id( + 'CreateInvoiceTemplate', InvoiceTemplateParams, WoodyContext + ), + CallArgs = {encode_thrift_invoice_tpl_create_params(InvoiceTemplateID, InvoiceTemplateParams)}, + case capi_woody_client:call_service(invoice_templating, 'Create', CallArgs, WoodyContext) of + {ok, InvoiceTpl} -> + {ok, make_thrift_invoice_tpl_and_token(InvoiceTpl, WoodyContext)}; + Passthrough -> + Passthrough + end; +handle_function_('Get', {InvoiceTemplateID}, WoodyContext, _Opts) -> + capi_woody_client:call_service(invoice_templating, 'Get', {InvoiceTemplateID}, WoodyContext); +handle_function_('Update', {InvoiceTemplateID, InvoiceTemplateParams}, WoodyContext, _Opts) -> + Params = encode_thrift_invoice_tpl_update_params(InvoiceTemplateParams), + capi_woody_client:call_service(invoice_templating, 'Update', {InvoiceTemplateID, Params}, WoodyContext); +handle_function_('Delete', {InvoiceTemplateID}, WoodyContext, _Opts) -> + capi_woody_client:call_service(invoice_templating, 'Delete', {InvoiceTemplateID}, WoodyContext). + mask_invoice_template_notfound(Resolution) -> % ED-206 % When bouncer says "forbidden" we can't really tell the difference between "forbidden because @@ -246,7 +309,121 @@ generate_invoice_template_id(OperationID, TemplateParams, PartyID, #{woody_conte Identity = capi_bender:make_identity(capi_feature_schemas:invoice_template(), TemplateParams), capi_bender:gen_snowflake(IdempKey, Identity, WoodyContext). -encode_invoice_tpl_create_params(InvoiceTemplateID, PartyID, Params) -> +generate_thrift_invoice_template_id( + OperationID, + #api_ext_InvoiceTemplateCreateParams{external_id = ExternalID, party_id = #domain_PartyConfigRef{id = PartyID}} = + TemplateParams, + WoodyContext +) -> + IdempKey = {OperationID, PartyID, ExternalID}, + Identity = capi_bender:make_identity( + capi_feature_schemas:invoice_template(), + decode_to_feature_container(TemplateParams) + ), + capi_bender:gen_snowflake(IdempKey, Identity, WoodyContext). + +decode_to_feature_container(#api_ext_InvoiceTemplateCreateParams{ + shop_id = #domain_ShopConfigRef{id = ShopID}, + invoice_lifetime = #domain_LifetimeInterval{days = DD, months = MM, years = YY}, + details = Details +}) -> + #{ + <<"shopID">> => ShopID, + <<"lifetime">> => #{<<"days">> => DD, <<"months">> => MM, <<"years">> => YY}, + <<"details">> => encode_details_to_feature_container(Details) + }. + +encode_details_to_feature_container( + {product, #domain_InvoiceTemplateProduct{ + product = Product, + price = Price, + metadata = Metadata + }} +) -> + genlib_map:compact(#{ + <<"templateType">> => <<"InvoiceTemplateSingleLine">>, + <<"product">> => Product, + <<"price">> => encode_price_to_feature_container(Price), + <<"taxMode">> => encode_tax_metadata_to_feature_container(Metadata) + }); +encode_details_to_feature_container({cart, #domain_InvoiceCart{lines = Lines}}) -> + {Cart, Currency} = encode_cart_lines_to_feature_container(Lines), + #{ + <<"templateType">> => <<"InvoiceTemplateMultiLine">>, + <<"currency">> => Currency, + <<"cart">> => Cart + }. + +encode_cart_lines_to_feature_container([]) -> + throw(invoice_cart_empty); +encode_cart_lines_to_feature_container(Lines) -> + {Cart, Currency} = lists:foldl( + fun( + #domain_InvoiceLine{ + product = Product, + quantity = Quantity, + price = #domain_Cash{amount = Amount, currency = #domain_CurrencyRef{symbolic_code = Curr}}, + metadata = Metadata + }, + {Items, _} + ) -> + { + [ + genlib_map:compact(#{ + <<"product">> => Product, + <<"quantity">> => Quantity, + <<"price">> => Amount, + <<"taxMode">> => encode_tax_metadata_to_feature_container(Metadata) + }) + | Items + ], + Curr + } + end, + {[], undefined}, + Lines + ), + {lists:reverse(Cart), Currency}. + +encode_price_to_feature_container({unlim, #domain_InvoiceTemplateCostUnlimited{}}) -> + #{<<"costType">> => <<"InvoiceTemplateLineCostUnlim">>}; +encode_price_to_feature_container( + {fixed, #domain_Cash{amount = Amount, currency = #domain_CurrencyRef{symbolic_code = Currency}}} +) -> + #{ + <<"costType">> => <<"InvoiceTemplateLineCostFixed">>, + <<"amount">> => Amount, + <<"currency">> => Currency + }; +encode_price_to_feature_container( + {range, #domain_CashRange{ + lower = {_, #domain_Cash{currency = #domain_CurrencyRef{symbolic_code = Currency}}} = LowerBound, + upper = UpperBound + }} +) -> + #{ + <<"costType">> => <<"InvoiceTemplateLineCostRange">>, + <<"currency">> => Currency, + <<"range">> => #{ + <<"lowerBound">> => encode_bound_to_feature_container(LowerBound, 1), + <<"upperBound">> => encode_bound_to_feature_container(UpperBound, -1) + } + }. + +encode_bound_to_feature_container({inclusive, #domain_Cash{amount = Bound}}, _Delta) -> + Bound; +encode_bound_to_feature_container({exclusive, #domain_Cash{amount = Bound}}, Delta) -> + Bound + Delta. + +encode_tax_metadata_to_feature_container(#{<<"TaxMode">> := {str, TM}}) -> + #{ + <<"type">> => <<"InvoiceLineTaxVAT">>, + <<"rate">> => TM + }; +encode_tax_metadata_to_feature_container(#{}) -> + undefined. + +encode_invoice_tpl_create_params(InvoiceTemplateID, PartyID, Params) when is_map(Params) -> Details = encode_invoice_tpl_details(genlib_map:get(<<"details">>, Params)), Product = get_product_from_tpl_details(Details), #payproc_InvoiceTemplateCreateParams{ @@ -275,12 +452,69 @@ encode_invoice_tpl_update_params(Params) -> mutations = capi_mutation:encode_amount_randomization_params(genlib_map:get(<<"randomizeAmount">>, Params)) }. +encode_thrift_invoice_tpl_update_params(#api_ext_InvoiceTemplateUpdateParams{ + invoice_lifetime = InvoiceLifetime, + name = Name, + description = Description, + details = Details, + context = Context +}) -> + ok = assert_cart_is_not_empty(Details), + Product = get_product_from_tpl_details(Details), + #payproc_InvoiceTemplateUpdateParams{ + invoice_lifetime = InvoiceLifetime, + product = Product, + name = Name, + description = Description, + details = Details, + context = Context + }. + +assert_cart_is_not_empty({cart, #domain_InvoiceCart{lines = []}}) -> + throw(invoice_cart_empty); +assert_cart_is_not_empty(_) -> + ok. + make_invoice_tpl_and_token(InvoiceTpl, ProcessingContext) -> #{ <<"invoiceTemplate">> => decode_invoice_tpl(InvoiceTpl), <<"invoiceTemplateAccessToken">> => capi_handler_utils:issue_access_token(InvoiceTpl, ProcessingContext) }. +encode_thrift_invoice_tpl_create_params(InvoiceTemplateID, #api_ext_InvoiceTemplateCreateParams{ + party_id = PartyID, + shop_id = ShopID, + invoice_lifetime = InvoiceLifetime, + name = Name, + description = Description, + details = Details, + context = Context +}) -> + Product = get_product_from_tpl_details(Details), + #payproc_InvoiceTemplateCreateParams{ + template_id = InvoiceTemplateID, + party_id = PartyID, + shop_id = ShopID, + invoice_lifetime = InvoiceLifetime, + product = Product, + name = Name, + description = Description, + details = Details, + context = Context + }. + +make_thrift_invoice_tpl_and_token(InvoiceTpl, WoodyContext) -> + TokenSpec = #{ + party => InvoiceTpl#domain_InvoiceTemplate.party_ref#domain_PartyConfigRef.id, + scope => {invoice_template, InvoiceTpl#domain_InvoiceTemplate.id}, + shop => InvoiceTpl#domain_InvoiceTemplate.shop_ref#domain_ShopConfigRef.id + }, + TokenPayload = capi_auth:issue_access_token(TokenSpec, WoodyContext), + #api_ext_InvoiceTemplateAndToken{ + invoice_template = InvoiceTpl, + invoice_template_access_token = #api_ext_AccessToken{payload = TokenPayload} + }. + encode_invoice_tpl_details(#{<<"templateType">> := <<"InvoiceTemplateSingleLine">>} = Details) -> {product, encode_invoice_tpl_product(Details)}; encode_invoice_tpl_details(#{<<"templateType">> := <<"InvoiceTemplateMultiLine">>} = Details) -> diff --git a/apps/capi/src/capi_sup.erl b/apps/capi/src/capi_sup.erl index 72d0575..9714c10 100644 --- a/apps/capi/src/capi_sup.erl +++ b/apps/capi/src/capi_sup.erl @@ -32,12 +32,35 @@ init([]) -> AdditionalRoutes = [{'_', [erl_health_handle:get_route(HealthCheck), get_prometheus_route()]}], SwaggerHandlerOpts = genlib_app:env(?APP, swagger_handler_opts, #{}), SwaggerSpec = capi_swagger_server:child_spec(AdditionalRoutes, LogicHandler, SwaggerHandlerOpts), + WoodyChildSPec = get_woody_child_spec(), {ok, { {one_for_all, 0, 1}, - [LechiffreSpec, SwaggerSpec, PartyClientSpec] + [LechiffreSpec, SwaggerSpec, PartyClientSpec, WoodyChildSPec] }}. +get_woody_child_spec() -> + {ok, IP} = inet:parse_address(genlib_app:env(capi_woody_server, ip, "::")), + EventHandlerOpts = genlib_app:env(capi_woody_server, scoper_event_handler_options, #{}), + woody_server:child_spec( + ?MODULE, + #{ + ip => IP, + port => genlib_app:env(capi_woody_server, port, 8022), + transport_opts => genlib_app:env(capi_woody_server, transport_opts, #{}), + protocol_opts => genlib_app:env(capi_woody_server, protocol_opts, #{}), + event_handler => {scoper_woody_event_handler, EventHandlerOpts}, + handlers => [ + %% TODO Proper path + {"/v2/extensions/invoice_templating", { + {dmsl_api_ext_thrift, 'InvoiceTemplating'}, {capi_handler_invoice_templates, #{}} + }} + ], + additional_routes => [], + shutdown_timeout => genlib_app:env(?MODULE, shutdown_timeout, 0) + } + ). + -spec get_logic_handler_info(capi_handler:handler_opts()) -> {Handler :: swag_server:logic_handler(_), [Spec :: supervisor:child_spec()] | []}. get_logic_handler_info(HandlerOpts) -> diff --git a/apps/capi/test/capi_idempotency_tests_SUITE.erl b/apps/capi/test/capi_idempotency_tests_SUITE.erl index d09890d..f37442f 100644 --- a/apps/capi/test/capi_idempotency_tests_SUITE.erl +++ b/apps/capi/test/capi_idempotency_tests_SUITE.erl @@ -7,6 +7,7 @@ -include_lib("damsel/include/dmsl_payproc_thrift.hrl"). -include_lib("damsel/include/dmsl_base_thrift.hrl"). -include_lib("damsel/include/dmsl_domain_thrift.hrl"). +-include_lib("damsel/include/dmsl_api_ext_thrift.hrl"). -export([all/0]). -export([groups/0]). @@ -30,6 +31,7 @@ -export([create_invoice_idemp_cart_fail_test/1]). -export([create_invoice_idemp_bank_account_fail_test/1]). -export([create_invoice_template_ok_test/1]). +-export([create_invoice_template_with_woody_ok_test/1]). -export([create_invoice_template_fail_test/1]). -export([create_invoice_with_template_ok_test/1]). -export([create_invoice_with_template_fail_test/1]). @@ -78,6 +80,7 @@ groups() -> ]}, {invoice_template_creation, [], [ create_invoice_template_ok_test, + create_invoice_template_with_woody_ok_test, create_invoice_template_fail_test ]}, {invoice_with_template_creation, [], [ @@ -463,6 +466,110 @@ create_invoice_template_ok_test(Config) -> UnusedParams1 ). +-spec create_invoice_template_with_woody_ok_test(config()) -> _. +create_invoice_template_with_woody_ok_test(Config) -> + BenderKey = <<"create_invoice_template_with_woody_ok_test_bender_key">>, + ExternalID = genlib:unique(), + CreateParams = #api_ext_InvoiceTemplateCreateParams{ + external_id = ExternalID, + party_id = #domain_PartyConfigRef{id = <<"2">>}, + shop_id = #domain_ShopConfigRef{id = <<"1">>}, + invoice_lifetime = #domain_LifetimeInterval{days = ?INTEGER, months = ?INTEGER, years = ?INTEGER}, + description = <<"Sample text">>, + details = + {cart, #domain_InvoiceCart{ + lines = [ + #domain_InvoiceLine{ + product = ?STRING, + quantity = ?INTEGER, + price = ?CASH, + metadata = #{?STRING => {obj, #{}}} + }, + #domain_InvoiceLine{ + product = ?STRING, + quantity = ?INTEGER, + price = ?CASH, + metadata = #{<<"TaxMode">> => {str, <<"18%">>}} + } + ] + }}, + context = ?CONTENT + }, + SwagReq = #{ + <<"externalID">> => ExternalID, + <<"shopID">> => <<"1">>, + <<"lifetime">> => #{ + <<"days">> => ?INTEGER, + <<"months">> => ?INTEGER, + <<"years">> => ?INTEGER + }, + <<"partyID">> => <<"2">>, + <<"details">> => ?INVOICE_TMPL_DETAILS_PARAMS, + <<"description">> => <<"Sample text">> + }, + Result = capi_ct_helper_bender:with_storage( + fun(StorageID) -> + _ = capi_ct_helper:mock_services( + [ + {invoice_templating, fun( + 'Create', + {#payproc_InvoiceTemplateCreateParams{template_id = TemplateID}} + ) -> + {ok, ?INVOICE_TPL(TemplateID)} + end}, + {bender, fun('GenerateID', {_Key, _, CtxMsgPack}) -> + capi_ct_helper_bender:get_internal_id(StorageID, BenderKey, CtxMsgPack) + end} + ], + Config + ), + %% Two thrift calls + [ + with_feature_storage(fun() -> + woody_client:call({{dmsl_api_ext_thrift, 'InvoiceTemplating'}, 'Create', {Params}}, #{ + url => "http://localhost:8022/v2/extensions/invoice_templating", + event_handler => scoper_woody_event_handler + }) + end) + || Params <- [ + CreateParams, + CreateParams#api_ext_InvoiceTemplateCreateParams{description = <<"whatever">>} + ] + ] ++ + %% And one swag request + [ + with_feature_storage(fun() -> + capi_client_invoice_templates:create(?config(context, Config), SwagReq) + end) + ] + end + ), + %% NOTE Since thrift parameters are decoded into the feature set only with + %% fields present in the schema, no other fields should be tracked as unused + %% by `capi_ct_features_reader_event_handler` and its storage. + %% NOTE See `capi_handler_invoice_templates:decode_to_feature_container/1`. + ?assertMatch( + [ + { + {ok, #api_ext_InvoiceTemplateAndToken{invoice_template = #domain_InvoiceTemplate{id = ID} = Template}}, + [] = UnusedParams + }, + { + {ok, #api_ext_InvoiceTemplateAndToken{invoice_template = Template}}, + UnusedParams + }, + { + {ok, #{<<"invoiceTemplate">> := #{<<"id">> := ID}}}, + [ + [<<"description">>], + [<<"externalID">>], + [<<"partyID">>] + ] = _OtherUnusedParams + } + ], + Result + ). + -spec create_invoice_template_fail_test(config()) -> _. create_invoice_template_fail_test(Config) -> BenderKey = <<"create_invoice_template_fail_test_bender_key">>, diff --git a/apps/capi/test/capi_invoice_template_woody_SUITE.erl b/apps/capi/test/capi_invoice_template_woody_SUITE.erl new file mode 100644 index 0000000..7b08cc6 --- /dev/null +++ b/apps/capi/test/capi_invoice_template_woody_SUITE.erl @@ -0,0 +1,233 @@ +-module(capi_invoice_template_woody_SUITE). + +-include_lib("common_test/include/ct.hrl"). +-include_lib("stdlib/include/assert.hrl"). + +-include_lib("capi_dummy_data.hrl"). +-include_lib("damsel/include/dmsl_payproc_thrift.hrl"). +%% -include_lib("damsel/include/dmsl_base_thrift.hrl"). +-include_lib("damsel/include/dmsl_domain_thrift.hrl"). +-include_lib("damsel/include/dmsl_api_ext_thrift.hrl"). + +-export([all/0]). +-export([groups/0]). +-export([init_per_suite/1]). +-export([end_per_suite/1]). +-export([init_per_group/2]). +-export([end_per_group/2]). +-export([init_per_testcase/2]). +-export([end_per_testcase/2]). + +-export([init/1]). + +-export([create_invoice_template_ok_test/1]). +-export([update_invoice_template_ok_test/1]). +-export([get_invoice_template_ok_test/1]). +-export([delete_invoice_template_ok_test/1]). + +-type test_case_name() :: atom(). +-type config() :: [{atom(), any()}]. +-type group_name() :: atom(). + +-behaviour(supervisor). + +-spec init([]) -> {ok, {supervisor:sup_flags(), [supervisor:child_spec()]}}. +init([]) -> + {ok, {#{strategy => one_for_all, intensity => 1, period => 1}, []}}. + +-spec all() -> [{group, test_case_name()}]. +all() -> + [ + {group, default} + ]. + +-spec groups() -> [{group_name(), list(), [test_case_name()]}]. +groups() -> + [ + %% NOTE Sequential execution due to mocks. + {default, [sequence], [ + create_invoice_template_ok_test, + update_invoice_template_ok_test, + get_invoice_template_ok_test, + delete_invoice_template_ok_test + ]} + ]. + +%% +%% starting/stopping +%% +-spec init_per_suite(config()) -> config(). +init_per_suite(Config0) -> + capi_ct_helper:init_suite(?MODULE, Config0). + +-spec end_per_suite(config()) -> _. +end_per_suite(C) -> + _ = capi_ct_helper:stop_mocked_service_sup(?config(suite_test_sup, C)), + _ = [application:stop(App) || App <- proplists:get_value(apps, C)], + ok. + +-spec init_per_group(group_name(), config()) -> config(). +init_per_group(_GroupName, Config) -> + SupPid = capi_ct_helper:start_mocked_service_sup(?MODULE), + Apps1 = capi_ct_helper_token_keeper:mock_user_session_token(SupPid), + [ + {group_apps, Apps1}, + {group_test_sup, SupPid} + | Config + ]. + +-spec end_per_group(group_name(), config()) -> _. +end_per_group(_Group, C) -> + _ = capi_utils:'maybe'(?config(group_test_sup, C), fun capi_ct_helper:stop_mocked_service_sup/1), + ok. + +-spec init_per_testcase(test_case_name(), config()) -> config(). +init_per_testcase(_Name, C) -> + [{test_sup, capi_ct_helper:start_mocked_service_sup(?MODULE)} | C]. + +-spec end_per_testcase(test_case_name(), config()) -> _. +end_per_testcase(_Name, C) -> + capi_ct_helper:stop_mocked_service_sup(?config(test_sup, C)), + ok. + +%% TESTS + +-spec create_invoice_template_ok_test(config()) -> _. +create_invoice_template_ok_test(Config) -> + Params = #api_ext_InvoiceTemplateCreateParams{ + external_id = ?STRING, + party_id = #domain_PartyConfigRef{id = <<"2">>}, + shop_id = #domain_ShopConfigRef{id = <<"1">>}, + invoice_lifetime = #domain_LifetimeInterval{days = ?INTEGER, months = ?INTEGER, years = ?INTEGER}, + description = <<"Sample text">>, + details = + {cart, #domain_InvoiceCart{ + lines = [ + #domain_InvoiceLine{ + product = ?STRING, + quantity = ?INTEGER, + price = ?CASH, + metadata = #{?STRING => {obj, #{}}} + }, + #domain_InvoiceLine{ + product = ?STRING, + quantity = ?INTEGER, + price = ?CASH, + metadata = #{<<"TaxMode">> => {str, <<"18%">>}} + } + ] + }}, + context = ?CONTENT + }, + InvoiceTemplateID = genlib:unique(), + _ = capi_ct_helper:mock_services( + [ + {invoice_templating, fun( + 'Create', + {#payproc_InvoiceTemplateCreateParams{template_id = TemplateID}} + ) -> + {ok, ?INVOICE_TPL(TemplateID)} + end}, + {bender, fun('GenerateID', {_Key, _, _Ctx}) -> + {ok, capi_ct_helper_bender:get_result(InvoiceTemplateID)} + end} + ], + Config + ), + ?assertMatch( + {ok, #api_ext_InvoiceTemplateAndToken{ + invoice_template = #domain_InvoiceTemplate{id = InvoiceTemplateID}, + invoice_template_access_token = #api_ext_AccessToken{payload = ?API_TOKEN} + }}, + woody_client:call({{dmsl_api_ext_thrift, 'InvoiceTemplating'}, 'Create', {Params}}, #{ + url => "http://localhost:8022/v2/extensions/invoice_templating", + event_handler => scoper_woody_event_handler + }) + ). + +-spec update_invoice_template_ok_test(config()) -> _. +update_invoice_template_ok_test(Config) -> + Params = #api_ext_InvoiceTemplateUpdateParams{ + invoice_lifetime = #domain_LifetimeInterval{days = ?INTEGER, months = ?INTEGER, years = ?INTEGER}, + description = <<"Sample text">>, + details = + {cart, #domain_InvoiceCart{ + lines = [ + #domain_InvoiceLine{ + product = ?STRING, + quantity = ?INTEGER, + price = ?CASH, + metadata = #{?STRING => {obj, #{}}} + }, + #domain_InvoiceLine{ + product = ?STRING, + quantity = ?INTEGER, + price = ?CASH, + metadata = #{<<"TaxMode">> => {str, <<"18%">>}} + } + ] + }}, + context = ?CONTENT + }, + InvoiceTemplateID = genlib:unique(), + _ = capi_ct_helper:mock_services( + [ + {invoice_templating, fun( + 'Update', + {TemplateID, #payproc_InvoiceTemplateUpdateParams{ + invoice_lifetime = _, + description = _, + details = _, + context = ?CONTENT + }} + ) -> + {ok, ?INVOICE_TPL(TemplateID)} + end} + ], + Config + ), + ?assertMatch( + {ok, #domain_InvoiceTemplate{id = InvoiceTemplateID}}, + woody_client:call({{dmsl_api_ext_thrift, 'InvoiceTemplating'}, 'Update', {InvoiceTemplateID, Params}}, #{ + url => "http://localhost:8022/v2/extensions/invoice_templating", + event_handler => scoper_woody_event_handler + }) + ). + +-spec get_invoice_template_ok_test(config()) -> _. +get_invoice_template_ok_test(Config) -> + InvoiceTemplateID = genlib:unique(), + _ = capi_ct_helper:mock_services( + [ + {invoice_templating, fun('Get', {TemplateID}) -> + {ok, ?INVOICE_TPL(TemplateID)} + end} + ], + Config + ), + ?assertMatch( + {ok, #domain_InvoiceTemplate{id = InvoiceTemplateID}}, + woody_client:call({{dmsl_api_ext_thrift, 'InvoiceTemplating'}, 'Get', {InvoiceTemplateID}}, #{ + url => "http://localhost:8022/v2/extensions/invoice_templating", + event_handler => scoper_woody_event_handler + }) + ). + +-spec delete_invoice_template_ok_test(config()) -> _. +delete_invoice_template_ok_test(Config) -> + InvoiceTemplateID = genlib:unique(), + _ = capi_ct_helper:mock_services( + [ + {invoice_templating, fun('Delete', {TemplateID}) when TemplateID =:= InvoiceTemplateID -> + {ok, ok} + end} + ], + Config + ), + ?assertMatch( + {ok, ok}, + woody_client:call({{dmsl_api_ext_thrift, 'InvoiceTemplating'}, 'Delete', {InvoiceTemplateID}}, #{ + url => "http://localhost:8022/v2/extensions/invoice_templating", + event_handler => scoper_woody_event_handler + }) + ). diff --git a/apps/capi/test/capi_invoice_template_woody_SUITE_data/keys/local/jwk.priv.json b/apps/capi/test/capi_invoice_template_woody_SUITE_data/keys/local/jwk.priv.json new file mode 100644 index 0000000..e7d6557 --- /dev/null +++ b/apps/capi/test/capi_invoice_template_woody_SUITE_data/keys/local/jwk.priv.json @@ -0,0 +1,10 @@ +{ + "use": "enc", + "kty": "EC", + "kid": "kxdD0orVPGoAxWrqAMTeQ0U5MRoK47uZxWiSJdgo0t0", + "crv": "P-256", + "alg": "ECDH-ES", + "x": "nHi7TCgBwfrPuNTf49bGvJMczk6WZOI-mCKAghbrOlM", + "y": "_8kiXGOIWkfz57m8K5dmTfbYzCJVYHZZZisCfbYicr0", + "d": "i45qDiARZ5qbS_uzeT-CiKnPUe64qHitKaVdAvcN6TI" +} \ No newline at end of file diff --git a/apps/capi/test/capi_invoice_template_woody_SUITE_data/keys/local/jwk.publ.json b/apps/capi/test/capi_invoice_template_woody_SUITE_data/keys/local/jwk.publ.json new file mode 100644 index 0000000..00b7002 --- /dev/null +++ b/apps/capi/test/capi_invoice_template_woody_SUITE_data/keys/local/jwk.publ.json @@ -0,0 +1,9 @@ +{ + "use": "enc", + "kty": "EC", + "kid": "kxdD0orVPGoAxWrqAMTeQ0U5MRoK47uZxWiSJdgo0t0", + "crv": "P-256", + "alg": "ECDH-ES", + "x": "nHi7TCgBwfrPuNTf49bGvJMczk6WZOI-mCKAghbrOlM", + "y": "_8kiXGOIWkfz57m8K5dmTfbYzCJVYHZZZisCfbYicr0" +} \ No newline at end of file diff --git a/apps/capi/test/capi_invoice_template_woody_SUITE_data/keys/local/private.pem b/apps/capi/test/capi_invoice_template_woody_SUITE_data/keys/local/private.pem new file mode 100644 index 0000000..4e6d12c --- /dev/null +++ b/apps/capi/test/capi_invoice_template_woody_SUITE_data/keys/local/private.pem @@ -0,0 +1,9 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIBOwIBAAJBAK9fx7qOJT7Aoseu7KKgaLagBh3wvDzg7F/ZMtGbPFikJnnvRWvF +B5oEGbMPblvtF0/fjqfu+eqjP3Z1tUSn7TkCAwEAAQJABUY5KIgr4JZEjwLYxQ9T +9uIbLP1Xe/E7yqoqmBk2GGhSrPY0OeRkYnUVLcP96UPQhF63iuG8VF6uZ7oAPsq+ +gQIhANZy3jSCzPjXYHRU1kRqQzpt2S+OqoEiqQ6YG1HrC/VxAiEA0Vq6JlQK2tOX +37SS00dK0Qog4Qi8dN73GliFQNP18EkCIQC4epSA48zkfJMzQBAbRraSuxDNApPX +BzQbo+pMrEDbYQIgY4AncQgIkLB4Qk5kah48JNYXglzQlQtTjiX8Ty9ueGECIQCM +GD3UbQKiA0gf5plBA24I4wFVKxxa4wXbW/7SfP6XmQ== +-----END RSA PRIVATE KEY----- diff --git a/config/sys.config b/config/sys.config index 32052d3..c39d1a1 100644 --- a/config/sys.config +++ b/config/sys.config @@ -40,7 +40,9 @@ %% {oops_bodies, #{ %% 500 => "oops_bodies/500_body" %% }}, - {graceful_shutdown_timeout, 5000}, % non_neg_integer() | infinity + + % non_neg_integer() | infinity + {graceful_shutdown_timeout, 5000}, {bouncer_ruleset_id, <<"service/authz/api">>}, {access_conf, #{ jwt => #{ @@ -63,13 +65,14 @@ } }}, {health_check, #{ - disk => {erl_health, disk , ["/", 99]}, - memory => {erl_health, cg_memory, [99]}, - service => {erl_health, service , [<<"capi">>]} + disk => {erl_health, disk, ["/", 99]}, + memory => {erl_health, cg_memory, [99]}, + service => {erl_health, service, [<<"capi">>]} }}, - {max_request_deadline, 60000}, % milliseconds + % milliseconds + {max_request_deadline, 60000}, {default_processing_deadline, <<"30m">>}, - {lechiffre_opts, #{ + {lechiffre_opts, #{ encryption_key_path => <<"path/to/key1.secret">>, decryption_key_paths => [<<"path/to/key1.secret">>] }}, @@ -83,37 +86,45 @@ }} ]}, + {capi_woody_server, [ + {ip, "::"}, + {port, 8022} + ]}, + {capi_woody_client, [ {services, #{ - party_management => <<"http://hellgate:8022/v1/processing/partymgmt">>, - invoicing => <<"http://hellgate:8022/v1/processing/invoicing">>, - invoice_templating => <<"http://hellgate:8022/v1/processing/invoice_templating">>, - webhook_manager => <<"http://hooker:8022/hook">> + party_management => <<"http://hellgate:8022/v1/processing/partymgmt">>, + invoicing => <<"http://hellgate:8022/v1/processing/invoicing">>, + invoice_templating => <<"http://hellgate:8022/v1/processing/invoice_templating">>, + webhook_manager => <<"http://hooker:8022/hook">> }}, {service_deadlines, #{ - party_management => 5000 % milliseconds + % milliseconds + party_management => 5000 }}, {service_retries, #{ - party_management => #{ - % function => retry strategy - % '_' work as "any" - % default value is 'finish' - % for more info look genlib_retry :: strategy() - % https://github.com/valitydev/genlib/blob/master/src/genlib_retry.erl#L19 - 'Get' => {linear, 3, 1000}, - '_' => finish + party_management => #{ + % function => retry strategy + % '_' work as "any" + % default value is 'finish' + % for more info look genlib_retry :: strategy() + % https://github.com/valitydev/genlib/blob/master/src/genlib_retry.erl#L19 + 'Get' => {linear, 3, 1000}, + '_' => finish } }} ]}, {dmt_client, [ - {cache_update_interval, 30000}, % milliseconds + % milliseconds + {cache_update_interval, 30000}, {max_cache_size, #{ elements => 20, - memory => 52428800 % 50Mb + % 50Mb + memory => 52428800 }}, {service_urls, #{ - 'Repository' => <<"http://dominant:8022/v1/domain/repository">>, + 'Repository' => <<"http://dominant:8022/v1/domain/repository">>, 'RepositoryClient' => <<"http://dominant:8022/v1/domain/repository_client">> }} ]}, @@ -123,16 +134,18 @@ 'party_management' => "http://hellgate:8022/v1/processing/partymgmt" }}, {woody, #{ - cache_mode => safe, % disabled | safe | aggressive + % disabled | safe | aggressive + cache_mode => safe, options => #{ woody_client => #{ - event_handler => {scoper_woody_event_handler, #{ - event_handler_opts => #{ - formatter_opts => #{ - max_length => 1000 + event_handler => + {scoper_woody_event_handler, #{ + event_handler_opts => #{ + formatter_opts => #{ + max_length => 1000 + } } - } - }} + }} } }, %retries => #{ @@ -203,7 +216,7 @@ ]}, {snowflake, [ - % {machine_id, 42} + % {machine_id, 42} ]}, {prometheus, [ diff --git a/elvis.config b/elvis.config index 7e974ae..5ac8919 100644 --- a/elvis.config +++ b/elvis.config @@ -40,6 +40,7 @@ capi_feature_schemas ] }}, + {elvis_style, private_data_types, disable}, {elvis_style, no_import, disable}, {elvis_style, no_throw, disable}, {elvis_style, export_used_types, disable} diff --git a/rebar.config b/rebar.config index 82325bf..0267d7d 100644 --- a/rebar.config +++ b/rebar.config @@ -36,7 +36,7 @@ {cowboy_draining_server, {git, "https://github.com/valitydev/cowboy_draining_server.git", {branch, "master"}}}, {woody, {git, "https://github.com/valitydev/woody_erlang.git", {tag, "v1.1.0"}}}, {woody_user_identity, {git, "https://github.com/valitydev/woody_erlang_user_identity.git", {tag, "v1.1.0"}}}, - {damsel, {git, "https://github.com/valitydev/damsel.git", {tag, "v2.2.17"}}}, + {damsel, {git, "https://github.com/valitydev/damsel.git", {tag, "v2.2.21"}}}, {bender_proto, {git, "https://github.com/valitydev/bender-proto.git", {branch, "master"}}}, {bender_client, {git, "https://github.com/valitydev/bender-client-erlang.git", {tag, "v1.1.0"}}}, {dmt_client, {git, "https://github.com/valitydev/dmt_client.git", {tag, "v2.0.3"}}}, @@ -126,7 +126,13 @@ {erlfmt, [ {print_width, 120}, - {files, ["apps/capi*/{src,include,test}/*.{hrl,erl}", "rebar.config", "elvis.config"]} + {files, [ + "apps/capi*/{src,include,test}/*.{hrl,erl}", + "apps/capi*/rebar.config", + "rebar.config", + "elvis.config", + "config/sys.config" + ]} ]}. {covertool, [ diff --git a/rebar.lock b/rebar.lock index 61fc5a6..e42f5f8 100644 --- a/rebar.lock +++ b/rebar.lock @@ -41,7 +41,7 @@ {<<"ctx">>,{pkg,<<"ctx">>,<<"0.6.0">>},2}, {<<"damsel">>, {git,"https://github.com/valitydev/damsel.git", - {ref,"f831d3aa5fdfd0338b41af44d1eeffe810ca9708"}}, + {ref,"0ac1d9c85e61a3f7428e1a6bc56330116e5a8129"}}, 0}, {<<"dmt_client">>, {git,"https://github.com/valitydev/dmt_client.git",