From 3b10d1cbf3e664d6d0670518d236b0cf526170f2 Mon Sep 17 00:00:00 2001 From: Andrew Mayorov Date: Tue, 19 Apr 2022 18:03:09 +0300 Subject: [PATCH] TD-275: Ensure limit changes handled idempotently (#3) While also involving minimal number of interactions with accounter in order to improve runtime behaviour. * Add annotated testcases for changed exception behavior * Test (kinda) that multirange limits work --- apps/limiter/src/lim_body.erl | 5 +- apps/limiter/src/lim_config_machine.erl | 8 +- apps/limiter/src/lim_handler.erl | 6 - apps/limiter/src/lim_p_transfer.erl | 110 ++------ apps/limiter/src/lim_range_machine.erl | 119 ++++---- apps/limiter/src/lim_rates.erl | 8 +- apps/limiter/src/lim_turnover_processor.erl | 176 +++++++----- apps/limiter/test/lim_ct_helper.hrl | 13 +- apps/limiter/test/lim_mock.erl | 2 - apps/limiter/test/lim_turnover_SUITE.erl | 290 +++++++++++++------- elvis.config | 1 + 11 files changed, 400 insertions(+), 338 deletions(-) diff --git a/apps/limiter/src/lim_body.erl b/apps/limiter/src/lim_body.erl index 903d448..d5cd134 100644 --- a/apps/limiter/src/lim_body.erl +++ b/apps/limiter/src/lim_body.erl @@ -1,8 +1,5 @@ -module(lim_body). --include_lib("limiter_proto/include/lim_base_thrift.hrl"). --include_lib("limiter_proto/include/lim_limiter_thrift.hrl"). - -export([get_body/3]). -export([create_body_from_cash/2]). @@ -17,7 +14,7 @@ -type currency() :: lim_base_thrift:'CurrencySymbolicCode'(). -type config() :: lim_config_machine:config(). -type body_type() :: full | partial. --type get_body_error() :: notfound | lim_rates:convertation_error(). +-type get_body_error() :: notfound | lim_rates:conversion_error(). -export_type([t/0]). -export_type([cash/0]). diff --git a/apps/limiter/src/lim_config_machine.erl b/apps/limiter/src/lim_config_machine.erl index 2fe204e..987240d 100644 --- a/apps/limiter/src/lim_config_machine.erl +++ b/apps/limiter/src/lim_config_machine.erl @@ -1,7 +1,6 @@ -module(lim_config_machine). -include_lib("limiter_proto/include/lim_limiter_thrift.hrl"). --include_lib("limiter_proto/include/lim_base_thrift.hrl"). %% Accessors @@ -9,6 +8,7 @@ -export([id/1]). -export([description/1]). -export([body_type/1]). +-export([currency/1]). -export([started_at/1]). -export([shard_size/1]). -export([time_range_type/1]). @@ -183,6 +183,12 @@ description(_) -> body_type(#{body_type := BodyType}) -> BodyType. +-spec currency(config()) -> currency() | undefined. +currency(#{body_type := {cash, Currency}}) -> + Currency; +currency(#{body_type := amount}) -> + undefined. + -spec started_at(config()) -> timestamp(). started_at(#{started_at := Value}) -> Value. diff --git a/apps/limiter/src/lim_handler.erl b/apps/limiter/src/lim_handler.erl index ce83b7e..0fceda9 100644 --- a/apps/limiter/src/lim_handler.erl +++ b/apps/limiter/src/lim_handler.erl @@ -86,8 +86,6 @@ handle_function_('Rollback', {LimitChange = ?LIMIT_CHANGE(LimitID), Clock, Conte end. -spec handle_get_error(_) -> no_return(). -handle_get_error({_, {limit, notfound}}) -> - woody_error:raise(business, #limiter_LimitNotFound{}); handle_get_error({_, {range, notfound}}) -> woody_error:raise(business, #limiter_LimitNotFound{}); handle_get_error(Error) -> @@ -102,16 +100,12 @@ handle_hold_error(Error) -> -spec handle_commit_error(_) -> no_return(). handle_commit_error({_, {forbidden_operation_amount, Error}}) -> handle_forbidden_operation_amount_error(Error); -handle_commit_error({_, {plan, notfound}}) -> - woody_error:raise(business, #limiter_LimitChangeNotFound{}); handle_commit_error({_, {invalid_request, Errors}}) -> woody_error:raise(business, #limiter_base_InvalidRequest{errors = Errors}); handle_commit_error(Error) -> handle_default_error(Error). -spec handle_rollback_error(_) -> no_return(). -handle_rollback_error({_, {plan, notfound}}) -> - woody_error:raise(business, #limiter_LimitChangeNotFound{}); handle_rollback_error({_, {invalid_request, Errors}}) -> woody_error:raise(business, #limiter_base_InvalidRequest{errors = Errors}); handle_rollback_error(Error) -> diff --git a/apps/limiter/src/lim_p_transfer.erl b/apps/limiter/src/lim_p_transfer.erl index 666465a..2e1637b 100644 --- a/apps/limiter/src/lim_p_transfer.erl +++ b/apps/limiter/src/lim_p_transfer.erl @@ -1,94 +1,34 @@ -module(lim_p_transfer). -include_lib("damsel/include/dmsl_accounter_thrift.hrl"). --include_lib("damsel/include/dmsl_base_thrift.hrl"). --export([construct_postings/3]). --export([reverse_postings/1]). --export([assert_partial_posting_amount/2]). +-export([construct_posting/2]). +-export([reverse_posting/1]). --type amount() :: integer(). --type currency() :: binary(). --type account_id() :: lim_accounting:account_id(). -type posting() :: lim_accounting:posting(). -type body() :: lim_body:t(). --type forbidden_operation_amount_error() :: #{ - type := positive | negative, - partial := amount(), - full := amount(), - currency := currency() -}. +-spec construct_posting(lim_range_machine:time_range_ext(), body()) -> posting(). +construct_posting(#{account_id_from := From, account_id_to := To}, {cash, #{amount := Amount, currency := Currency}}) -> + #accounter_Posting{ + from_id = From, + to_id = To, + amount = Amount, + currency_sym_code = Currency, + description = <<>> + }; +construct_posting(#{account_id_from := From, account_id_to := To}, {amount, Amount}) -> + #accounter_Posting{ + from_id = From, + to_id = To, + amount = Amount, + currency_sym_code = lim_accounting:get_default_currency(), + description = <<>> + }. --export_type([forbidden_operation_amount_error/0]). - --spec construct_postings(account_id(), account_id(), body()) -> [posting()]. -construct_postings(AccountFrom, AccountTo, {cash, #{amount := Amount, currency := Currency}}) -> - [ - #accounter_Posting{ - from_id = AccountFrom, - to_id = AccountTo, - amount = Amount, - currency_sym_code = Currency, - description = <<>> - } - ]; -construct_postings(AccountFrom, AccountTo, {amount, Amount}) -> - [ - #accounter_Posting{ - from_id = AccountFrom, - to_id = AccountTo, - amount = Amount, - currency_sym_code = lim_accounting:get_default_currency(), - description = <<>> - } - ]. - --spec reverse_postings([posting()]) -> [posting()]. -reverse_postings(Postings) -> - [ - Posting#accounter_Posting{ - from_id = AccountTo, - to_id = AccountFrom - } - || Posting = #accounter_Posting{from_id = AccountFrom, to_id = AccountTo} <- Postings - ]. - --spec assert_partial_posting_amount([posting()], [posting()]) -> ok | {error, forbidden_operation_amount_error()}. -assert_partial_posting_amount( - [#accounter_Posting{amount = Partial, currency_sym_code = Currency} | _], - [#accounter_Posting{amount = Full, currency_sym_code = Currency} | _] -) -> - compare_amount(Partial, Full, Currency); -assert_partial_posting_amount( - [#accounter_Posting{amount = Partial, currency_sym_code = PartialCurrency} | _], - [#accounter_Posting{amount = Full, currency_sym_code = FullCurrency} | _] -) -> - erlang:error({invalid_partial_cash, {Partial, PartialCurrency}, {Full, FullCurrency}}). - -compare_amount(Partial, Full, Currency) when Full > 0 -> - case Partial =< Full of - true -> - ok; - false -> - {error, - {forbidden_operation_amount, #{ - type => positive, - partial => Partial, - full => Full, - currency => Currency - }}} - end; -compare_amount(Partial, Full, Currency) when Full < 0 -> - case Partial >= Full of - true -> - ok; - false -> - {error, - {forbidden_operation_amount, #{ - type => negative, - partial => Partial, - full => Full, - currency => Currency - }}} - end. +-spec reverse_posting(posting()) -> posting(). +reverse_posting(Posting = #accounter_Posting{from_id = AccountFrom, to_id = AccountTo}) -> + Posting#accounter_Posting{ + from_id = AccountTo, + to_id = AccountFrom + }. diff --git a/apps/limiter/src/lim_range_machine.erl b/apps/limiter/src/lim_range_machine.erl index eac9093..0660f3c 100644 --- a/apps/limiter/src/lim_range_machine.erl +++ b/apps/limiter/src/lim_range_machine.erl @@ -11,11 +11,9 @@ %% API -export([get/2]). --export([ensure_exist/2]). +-export([ensure_exists/3]). -export([get_range/2]). -export([get_range_balance/3]). --export([ensure_range_exist/3]). --export([ensure_range_exist_in_state/3]). %% Machinery callbacks @@ -38,13 +36,13 @@ -type woody_context() :: woody_context:ctx(). -type lim_context() :: lim_context:t(). -type timestamp() :: lim_config_machine:timestamp(). --type lim_id() :: lim_config_machine:lim_id(). +-type id() :: binary(). -type time_range_type() :: lim_config_machine:time_range_type(). -type time_range() :: lim_config_machine:time_range(). -type currency() :: lim_config_machine:currency(). -type limit_range_state() :: #{ - id := lim_id(), + id := id(), type := time_range_type(), created_at := timestamp(), currency => currency(), @@ -59,7 +57,7 @@ | {time_range_created, time_range_ext()}. -type limit_range() :: #{ - id := lim_id(), + id := id(), type := time_range_type(), created_at := timestamp(), currency => currency() @@ -73,7 +71,7 @@ }. -type create_params() :: #{ - id := lim_id(), + id := id(), type := time_range_type(), created_at := timestamp(), currency => currency() @@ -82,16 +80,18 @@ -type range_call() :: {add_range, time_range()}. +-export_type([time_range_ext/0]). + -export_type([timestamped_event/1]). -export_type([event/0]). -define(NS, 'lim/range_v1'). --import(lim_pipeline, [do/1, unwrap/1, unwrap/2]). +-import(lim_pipeline, [do/1, unwrap/1]). %% Accessors --spec id(limit_range_state()) -> lim_id(). +-spec id(limit_range_state()) -> id(). id(State) -> maps:get(id, State). @@ -117,54 +117,25 @@ currency(_State) -> %%% API --spec get(lim_id(), lim_context()) -> {ok, limit_range_state()} | {error, notfound}. +-spec get(id(), lim_context()) -> {ok, limit_range_state()} | {error, notfound}. get(ID, LimitContext) -> {ok, WoodyCtx} = lim_context:woody_context(LimitContext), get_state(ID, WoodyCtx). --spec ensure_exist(create_params(), lim_context()) -> {ok, limit_range_state()}. -ensure_exist(Params = #{id := ID}, LimitContext) -> +-spec ensure_exists(create_params(), time_range(), lim_context()) -> {ok, time_range_ext()}. +ensure_exists(Params = #{id := ID, currency := Currency}, TimeRange, LimitContext) -> {ok, WoodyCtx} = lim_context:woody_context(LimitContext), case get_state(ID, WoodyCtx) of {ok, State} -> - {ok, State}; + ensure_range_exist_in_state(TimeRange, State, WoodyCtx); {error, notfound} -> - _ = start(ID, Params, WoodyCtx), - case get_state(ID, WoodyCtx) of - {ok, State} -> - {ok, State}; - {error, notfound} -> - erlang:error({cant_get_after_start, ID}) - end + _ = start(ID, Params, [new_time_range_ext(TimeRange, Currency, WoodyCtx)], WoodyCtx), + {ok, State} = get_state(ID, WoodyCtx), + get_range(TimeRange, State) end. --spec get_range(time_range(), limit_range_state()) -> {ok, time_range_ext()} | {error, notfound}. -get_range(TimeRange, State) -> - find_time_range(TimeRange, ranges(State)). - --spec get_range_balance(time_range(), limit_range_state(), lim_context()) -> - {ok, lim_accounting:balance()} - | {error, {range, notfound}}. -get_range_balance(TimeRange, State, LimitContext) -> - do(fun() -> - #{account_id_to := AccountID} = unwrap(range, find_time_range(TimeRange, ranges(State))), - {ok, Balance} = lim_accounting:get_balance(AccountID, LimitContext), - Balance - end). - --spec ensure_range_exist(lim_id(), time_range(), lim_context()) -> - {ok, time_range_ext()} - | {error, {limit_range, notfound}}. -ensure_range_exist(ID, TimeRange, LimitContext) -> - do(fun() -> - {ok, WoodyCtx} = lim_context:woody_context(LimitContext), - State = unwrap(limit_range, get_state(ID, WoodyCtx)), - unwrap(ensure_range_exist_in_state(TimeRange, State, LimitContext)) - end). - --spec ensure_range_exist_in_state(time_range(), limit_range_state(), lim_context()) -> {ok, time_range_ext()}. -ensure_range_exist_in_state(TimeRange, State, LimitContext) -> - {ok, WoodyCtx} = lim_context:woody_context(LimitContext), +-spec ensure_range_exist_in_state(time_range(), limit_range_state(), woody_context()) -> {ok, time_range_ext()}. +ensure_range_exist_in_state(TimeRange, State, WoodyCtx) -> case find_time_range(TimeRange, ranges(State)) of {error, notfound} -> call(id(State), {add_range, TimeRange}, WoodyCtx); @@ -172,6 +143,21 @@ ensure_range_exist_in_state(TimeRange, State, LimitContext) -> {ok, Range} end. +-spec get_range(time_range(), limit_range_state()) -> {ok, time_range_ext()} | {error, notfound}. +get_range(TimeRange, State) -> + find_time_range(TimeRange, ranges(State)). + +-spec get_range_balance(id(), time_range(), lim_context()) -> + {ok, lim_accounting:balance()} + | {error, notfound}. +get_range_balance(ID, TimeRange, LimitContext) -> + do(fun() -> + State = unwrap(get(ID, LimitContext)), + #{account_id_to := AccountID} = unwrap(get_range(TimeRange, State)), + {ok, Balance} = lim_accounting:get_balance(AccountID, LimitContext), + Balance + end). + %%% Machinery callbacks -spec init(args([event()]), machine(), handler_args(), handler_opts()) -> result(). @@ -182,21 +168,14 @@ init(Events, _Machine, _HandlerArgs, _HandlerOpts) -> -spec process_call(args(range_call()), machine(), handler_args(), handler_opts()) -> {response(time_range_ext()), result()} | no_return(). -process_call({add_range, TimeRange0}, Machine, _HandlerArgs, #{woody_ctx := WoodyCtx}) -> +process_call({add_range, TimeRange}, Machine, _HandlerArgs, #{woody_ctx := WoodyCtx}) -> State = collapse(Machine), - case find_time_range(TimeRange0, ranges(State)) of + case find_time_range(TimeRange, ranges(State)) of {error, notfound} -> - Currency = currency(State), - {ok, LimitContext} = lim_context:create(WoodyCtx), - {ok, AccountIDFrom} = lim_accounting:create_account(Currency, LimitContext), - {ok, AccountIDTo} = lim_accounting:create_account(Currency, LimitContext), - TimeRange1 = TimeRange0#{ - account_id_from => AccountIDFrom, - account_id_to => AccountIDTo - }, - {TimeRange1, #{events => emit_events([{time_range_created, TimeRange1}])}}; - {ok, Range} -> - {Range, #{}} + TimeRangeExt = new_time_range_ext(TimeRange, currency(State), WoodyCtx), + {TimeRangeExt, #{events => emit_events([{time_range_created, TimeRangeExt}])}}; + {ok, TimeRangeExt} -> + {ok, TimeRangeExt} end. -spec process_timeout(machine(), handler_args(), handler_opts()) -> no_return(). @@ -216,17 +195,27 @@ find_time_range(#{lower := Lower}, [Head = #{lower := Lower} | _Rest]) -> find_time_range(TimeRange, [_Head | Rest]) -> find_time_range(TimeRange, Rest). +new_time_range_ext(TimeRange, Currency, WoodyCtx) -> + {ok, LimitContext} = lim_context:create(WoodyCtx), + {ok, AccountIDFrom} = lim_accounting:create_account(Currency, LimitContext), + {ok, AccountIDTo} = lim_accounting:create_account(Currency, LimitContext), + TimeRange#{ + account_id_from => AccountIDFrom, + account_id_to => AccountIDTo + }. + %% --spec start(lim_id(), create_params(), woody_context()) -> ok | {error, exists}. -start(ID, Params, WoodyCtx) -> - machinery:start(?NS, ID, [{created, Params}], get_backend(WoodyCtx)). +-spec start(id(), create_params(), [time_range_ext()], woody_context()) -> ok | {error, exists}. +start(ID, Params, TimeRanges, WoodyCtx) -> + TimeRangeEvents = [{time_range_created, TR} || TR <- TimeRanges], + machinery:start(?NS, ID, [{created, Params} | TimeRangeEvents], get_backend(WoodyCtx)). --spec call(lim_id(), range_call(), woody_context()) -> {ok, response(_)} | {error, notfound}. +-spec call(id(), range_call(), woody_context()) -> {ok, response(_)} | {error, notfound}. call(ID, Msg, WoodyCtx) -> machinery:call(?NS, ID, Msg, get_backend(WoodyCtx)). --spec get_state(lim_id(), woody_context()) -> {ok, limit_range_state()} | {error, notfound}. +-spec get_state(id(), woody_context()) -> {ok, limit_range_state()} | {error, notfound}. get_state(ID, WoodyCtx) -> case machinery:get(?NS, ID, get_backend(WoodyCtx)) of {ok, Machine} -> diff --git a/apps/limiter/src/lim_rates.erl b/apps/limiter/src/lim_rates.erl index 2dc1add..143f5ce 100644 --- a/apps/limiter/src/lim_rates.erl +++ b/apps/limiter/src/lim_rates.erl @@ -1,8 +1,6 @@ -module(lim_rates). -include_lib("xrates_proto/include/xrates_rate_thrift.hrl"). --include_lib("limiter_proto/include/lim_base_thrift.hrl"). --include_lib("limiter_proto/include/lim_limiter_thrift.hrl"). -export([get_converted_amount/3]). @@ -11,9 +9,9 @@ -type limit_context() :: lim_context:t(). -type config() :: lim_config_machine:config(). --type convertation_error() :: quote_not_found | currency_not_found. +-type conversion_error() :: quote_not_found | currency_not_found. --export_type([convertation_error/0]). +-export_type([conversion_error/0]). -define(APP, limiter). -define(DEFAULT_FACTOR, 1.1). @@ -21,7 +19,7 @@ -spec get_converted_amount({amount(), currency()}, config(), limit_context()) -> {ok, amount()} - | {error, convertation_error()}. + | {error, conversion_error()}. get_converted_amount(Cash = {_Amount, Currency}, Config, LimitContext) -> Factor = get_exchange_factor(Currency), case diff --git a/apps/limiter/src/lim_turnover_processor.erl b/apps/limiter/src/lim_turnover_processor.erl index 5d86c5e..92451ab 100644 --- a/apps/limiter/src/lim_turnover_processor.erl +++ b/apps/limiter/src/lim_turnover_processor.erl @@ -1,8 +1,6 @@ -module(lim_turnover_processor). --include_lib("limiter_proto/include/lim_base_thrift.hrl"). -include_lib("limiter_proto/include/lim_limiter_thrift.hrl"). --include_lib("damsel/include/dmsl_accounter_thrift.hrl"). -behaviour(lim_config_machine). @@ -24,7 +22,7 @@ full := amount() }. --type get_limit_error() :: {limit | range, notfound}. +-type get_limit_error() :: {range, notfound}. -type hold_error() :: lim_body:get_body_error() @@ -32,11 +30,12 @@ -type commit_error() :: {forbidden_operation_amount, forbidden_operation_amount_error()} - | {plan, notfound} - | {full | partial, lim_body:get_body_error()} + | lim_body:get_body_error() | lim_accounting:invalid_request_error(). --type rollback_error() :: {plan, notfound} | lim_accounting:invalid_request_error(). +-type rollback_error() :: + lim_body:get_body_error() + | lim_accounting:invalid_request_error(). -export_type([get_limit_error/0]). -export_type([hold_error/0]). @@ -48,12 +47,9 @@ -spec get_limit(lim_id(), config(), lim_context()) -> {ok, limit()} | {error, get_limit_error()}. get_limit(LimitID, Config, LimitContext) -> do(fun() -> - {ok, Timestamp} = lim_context:get_from_context(payment_processing, created_at, LimitContext), - LimitRangeID = construct_range_id(LimitID, Timestamp, Config, LimitContext), - LimitRange = unwrap(limit, lim_range_machine:get(LimitRangeID, LimitContext)), - TimeRange = lim_config_machine:calculate_time_range(Timestamp, Config), + {LimitRangeID, TimeRange} = compute_limit_time_range_location(LimitID, Config, LimitContext), #{max_available_amount := Amount} = - unwrap(lim_range_machine:get_range_balance(TimeRange, LimitRange, LimitContext)), + unwrap(range, lim_range_machine:get_range_balance(LimitRangeID, TimeRange, LimitContext)), #limiter_Limit{ id = LimitRangeID, amount = Amount, @@ -63,53 +59,66 @@ get_limit(LimitID, Config, LimitContext) -> end). -spec hold(lim_change(), config(), lim_context()) -> ok | {error, hold_error()}. -hold(LimitChange = #limiter_LimitChange{id = LimitID}, Config = #{body_type := BodyType}, LimitContext) -> +hold(LimitChange = #limiter_LimitChange{id = LimitID}, Config, LimitContext) -> do(fun() -> - {ok, Timestamp} = lim_context:get_from_context(payment_processing, created_at, LimitContext), - {ok, Body} = lim_body:get_body(full, Config, LimitContext), - LimitRangeID = construct_range_id(LimitID, Timestamp, Config, LimitContext), - Currency = - case BodyType of - {cash, CashCurrency} -> CashCurrency; - amount -> undefined - end, - CreateParams = genlib_map:compact(#{ - id => LimitRangeID, - type => lim_config_machine:time_range_type(Config), - created_at => Timestamp, - currency => Currency - }), - {ok, LimitRangeState} = lim_range_machine:ensure_exist(CreateParams, LimitContext), - TimeRange = lim_config_machine:calculate_time_range(Timestamp, Config), - {ok, #{account_id_from := AccountIDFrom, account_id_to := AccountIDTo}} = - lim_range_machine:ensure_range_exist_in_state(TimeRange, LimitRangeState, LimitContext), - Postings = lim_p_transfer:construct_postings(AccountIDFrom, AccountIDTo, Body), - Postings1 = apply_op_behaviour(Postings, LimitContext, Config), - lim_accounting:hold(construct_plan_id(LimitChange), {1, Postings1}, LimitContext) + TimeRangeAccount = ensure_limit_time_range(LimitID, Config, LimitContext), + Body = unwrap(lim_body:get_body(full, Config, LimitContext)), + Posting = construct_posting(TimeRangeAccount, Body, Config, LimitContext), + unwrap(lim_accounting:hold(construct_plan_id(LimitChange), {1, [Posting]}, LimitContext)) end). -spec commit(lim_change(), config(), lim_context()) -> ok | {error, commit_error()}. -commit(LimitChange, Config, LimitContext) -> +commit(LimitChange = #limiter_LimitChange{id = LimitID}, Config, LimitContext) -> do(fun() -> - case lim_body:get_body(partial, Config, LimitContext) of - {ok, Body} -> - unwrap(partial_commit(Body, LimitChange, Config, LimitContext)); - {error, notfound} -> - PlanID = construct_plan_id(LimitChange), - [Batch] = unwrap(plan, lim_accounting:get_plan(PlanID, LimitContext)), - unwrap(lim_accounting:commit(PlanID, [Batch], LimitContext)) - end + TimeRangeAccount = ensure_limit_time_range(LimitID, Config, LimitContext), + PlanID = construct_plan_id(LimitChange), + Operations = construct_commit_plan(TimeRangeAccount, Config, LimitContext), + ok = lists:foreach( + fun + ({hold, Batch}) -> + % NOTE + % This operation **can** fail with `InvalidRequest` when the plan is already + % committed, yet we knowingly ignore any them. Instead we rely on the fact that + % accounter guarantees us that it commits **only** when submitted plan consists + % of exactly the same set of batches which were held before. + lim_accounting:hold(PlanID, Batch, LimitContext); + ({commit, Batches}) -> + unwrap(lim_accounting:commit(PlanID, Batches, LimitContext)); + ({rollback, Batches}) -> + unwrap(lim_accounting:rollback(PlanID, Batches, LimitContext)) + end, + Operations + ) end). -spec rollback(lim_change(), config(), lim_context()) -> ok | {error, rollback_error()}. -rollback(LimitChange, _Config, LimitContext) -> +rollback(LimitChange = #limiter_LimitChange{id = LimitID}, Config, LimitContext) -> do(fun() -> - PlanID = construct_plan_id(LimitChange), - BatchList = unwrap(plan, lim_accounting:get_plan(PlanID, LimitContext)), - unwrap(lim_accounting:rollback(PlanID, BatchList, LimitContext)) + TimeRangeAccount = ensure_limit_time_range(LimitID, Config, LimitContext), + Body = unwrap(lim_body:get_body(full, Config, LimitContext)), + Posting = construct_posting(TimeRangeAccount, Body, Config, LimitContext), + unwrap(lim_accounting:rollback(construct_plan_id(LimitChange), [{1, [Posting]}], LimitContext)) end). +compute_limit_time_range_location(LimitID, Config, LimitContext) -> + {ok, Timestamp} = lim_context:get_from_context(payment_processing, created_at, LimitContext), + LimitRangeID = construct_range_id(LimitID, Timestamp, Config, LimitContext), + TimeRange = lim_config_machine:calculate_time_range(Timestamp, Config), + {LimitRangeID, TimeRange}. + +ensure_limit_time_range(LimitID, Config, LimitContext) -> + {ok, Timestamp} = lim_context:get_from_context(payment_processing, created_at, LimitContext), + {LimitRangeID, TimeRange} = compute_limit_time_range_location(LimitID, Config, LimitContext), + CreateParams = genlib_map:compact(#{ + id => LimitRangeID, + type => lim_config_machine:time_range_type(Config), + created_at => Timestamp, + currency => lim_config_machine:currency(Config) + }), + unwrap(lim_range_machine:ensure_exists(CreateParams, TimeRange, LimitContext)). + construct_plan_id(#limiter_LimitChange{change_id = ChangeID}) -> + % DISCUSS ChangeID. construct_range_id(LimitID, Timestamp, Config, LimitContext) -> @@ -117,39 +126,60 @@ construct_range_id(LimitID, Timestamp, Config, LimitContext) -> ShardID = lim_config_machine:calculate_shard_id(Timestamp, Config), <>. -partial_commit(PartialBody, LimitChange = #limiter_LimitChange{id = LimitID}, Config, LimitContext) -> - do(fun() -> - {ok, Timestamp} = lim_context:get_from_context(payment_processing, created_at, LimitContext), - {ok, FullBody} = lim_body:get_body(full, Config, LimitContext), - ok = unwrap(assert_partial_body(PartialBody, FullBody)), +construct_commit_plan(TimeRangeAccount, Config, LimitContext) -> + Body = unwrap(lim_body:get_body(full, Config, LimitContext)), + MaybePartialBody = lim_body:get_body(partial, Config, LimitContext), + construct_commit_postings(TimeRangeAccount, Body, MaybePartialBody, Config, LimitContext). - LimitRangeID = construct_range_id(LimitID, Timestamp, Config, LimitContext), - {ok, LimitRangeState} = lim_range_machine:get( - LimitRangeID, - LimitContext - ), - TimeRange = lim_config_machine:calculate_time_range(Timestamp, Config), - {ok, #{account_id_from := AccountIDFrom, account_id_to := AccountIDTo}} = - lim_range_machine:get_range(TimeRange, LimitRangeState), - PartialPostings0 = lim_p_transfer:construct_postings(AccountIDFrom, AccountIDTo, PartialBody), - FullPostings0 = lim_p_transfer:construct_postings(AccountIDFrom, AccountIDTo, FullBody), - PartialPostings1 = apply_op_behaviour(PartialPostings0, LimitContext, Config), - FullPostings1 = apply_op_behaviour(FullPostings0, LimitContext, Config), - NewBatchList = [{2, lim_p_transfer:reverse_postings(FullPostings1)} | [{3, PartialPostings1}]], - PlanID = construct_plan_id(LimitChange), - unwrap(lim_accounting:plan(PlanID, NewBatchList, LimitContext)), - unwrap(lim_accounting:commit(PlanID, [{1, FullPostings1} | NewBatchList], LimitContext)) - end). +construct_commit_postings(TimeRangeAccount, Full, MaybePartialBody, Config, LimitContext) -> + OriginalHoldPosting = construct_posting(TimeRangeAccount, Full, Config, LimitContext), + case determine_commit_intent(MaybePartialBody, Full) of + commit -> + [{commit, [{1, [OriginalHoldPosting]}]}]; + rollback -> + [{rollback, [{1, [OriginalHoldPosting]}]}]; + {commit, Partial} -> + % Partial body is less than full body + ok = unwrap(assert_partial_body(Partial, Full)), + ReverseHoldPosting = lim_p_transfer:reverse_posting(OriginalHoldPosting), + PartialHoldPosting = construct_posting(TimeRangeAccount, Partial, Config, LimitContext), + PartialBatch = [ReverseHoldPosting, PartialHoldPosting], + [ + {hold, {2, PartialBatch}}, + {commit, [ + {1, [OriginalHoldPosting]}, + {2, PartialBatch} + ]} + ] + end. -apply_op_behaviour(Posting, LimitContext, #{op_behaviour := ComputationConfig}) -> +determine_commit_intent({error, notfound}, _FullBody) -> + % No partial body specified + commit; +determine_commit_intent({ok, FullBody}, FullBody) -> + % Partial body is equal to full body + commit; +determine_commit_intent({ok, {amount, 0}}, _FullBody) -> + % Partial body is 0, this is rollback + rollback; +determine_commit_intent({ok, {cash, #{amount := 0}}}, _FullBody) -> + % Partial body is 0, this is rollback + rollback; +determine_commit_intent({ok, Partial}, _FullBody) -> + {commit, Partial}. + +construct_posting(TimeRangeAccount, Body, Config, LimitContext) -> + apply_op_behaviour(lim_p_transfer:construct_posting(TimeRangeAccount, Body), Config, LimitContext). + +apply_op_behaviour(Posting, #{op_behaviour := ComputationConfig}, LimitContext) -> {ok, Operation} = lim_context:get_operation(payment_processing, LimitContext), case maps:get(Operation, ComputationConfig, undefined) of subtraction -> - lim_p_transfer:reverse_postings(Posting); + lim_p_transfer:reverse_posting(Posting); Type when Type =:= undefined orelse Type =:= additional -> Posting end; -apply_op_behaviour(Body, _LimitContext, _Config) -> +apply_op_behaviour(Body, _Config, _LimitContext) -> Body. assert_partial_body( @@ -164,7 +194,7 @@ assert_partial_body( erlang:error({invalid_partial_cash, {Partial, PartialCurrency}, {Full, FullCurrency}}). compare_amount(Partial, Full, Currency) when Full > 0 -> - case Partial =< Full of + case Partial < Full of true -> ok; false -> @@ -178,7 +208,7 @@ compare_amount(Partial, Full, Currency) when Full > 0 -> })}} end; compare_amount(Partial, Full, Currency) when Full < 0 -> - case Partial >= Full of + case Partial > Full of true -> ok; false -> diff --git a/apps/limiter/test/lim_ct_helper.hrl b/apps/limiter/test/lim_ct_helper.hrl index f421210..10c1f6b 100644 --- a/apps/limiter/test/lim_ct_helper.hrl +++ b/apps/limiter/test/lim_ct_helper.hrl @@ -8,11 +8,13 @@ currency = #limiter_base_CurrencyRef{symbolic_code = <<"RUB">>} }). +-define(op_invoice_payment(), {invoice_payment, #limiter_context_PaymentProcessingOperationInvoicePayment{}}). + -define(ctx_invoice_payment(Cost, CaptureCost), ?ctx_invoice_payment(undefined, undefined, Cost, CaptureCost)). -define(ctx_invoice_payment(OwnerID, ShopID, Cost, CaptureCost), #limiter_context_LimitContext{ payment_processing = #limiter_context_ContextPaymentProcessing{ - op = {invoice_payment, #limiter_context_PaymentProcessingOperationInvoicePayment{}}, + op = ?op_invoice_payment(), invoice = #limiter_context_Invoice{ owner_id = OwnerID, shop_id = ShopID, @@ -25,6 +27,15 @@ } }). +-define(ctx_invoice_payment(Payment), #limiter_context_LimitContext{ + payment_processing = #limiter_context_ContextPaymentProcessing{ + op = ?op_invoice_payment(), + invoice = #limiter_context_Invoice{ + effective_payment = Payment + } + } +}). + -define(ctx_invoice_payment_refund(OwnerID, ShopID, Cost, CaptureCost, RefundCost), #limiter_context_LimitContext{ payment_processing = #limiter_context_ContextPaymentProcessing{ op = {invoice_payment_refund, #limiter_context_PaymentProcessingOperationInvoicePaymentRefund{}}, diff --git a/apps/limiter/test/lim_mock.erl b/apps/limiter/test/lim_mock.erl index 1b749cb..6d84817 100644 --- a/apps/limiter/test/lim_mock.erl +++ b/apps/limiter/test/lim_mock.erl @@ -19,9 +19,7 @@ stop_mocked_service_sup(SupPid) -> exit(SupPid, shutdown). -define(HOST_IP, "::"). --define(HOST_PORT, 8080). -define(HOST_NAME, "localhost"). --define(HOST_URL, ?HOST_NAME ++ ":" ++ integer_to_list(?HOST_PORT)). -spec mock_services(_, _) -> _. mock_services(Services, SupOrConfig) -> diff --git a/apps/limiter/test/lim_turnover_SUITE.erl b/apps/limiter/test/lim_turnover_SUITE.erl index 7b9b7c1..e7557e3 100644 --- a/apps/limiter/test/lim_turnover_SUITE.erl +++ b/apps/limiter/test/lim_turnover_SUITE.erl @@ -25,6 +25,14 @@ -export([rollback_ok/1]). -export([refund_ok/1]). -export([get_config_ok/1]). +-export([commit_inexistent_hold_fails/1]). +-export([partial_commit_inexistent_hold_fails/1]). +-export([commit_multirange_limit_ok/1]). + +-export([commit_processes_idempotently/1]). +-export([full_commit_processes_idempotently/1]). +-export([partial_commit_processes_idempotently/1]). +-export([rollback_processes_idempotently/1]). -type group_name() :: atom(). -type test_case_name() :: atom(). @@ -36,7 +44,8 @@ -spec all() -> [{group, group_name()}]. all() -> [ - {group, default} + {group, default}, + {group, idempotency} ]. -spec groups() -> [{atom(), list(), [test_case_name()]}]. @@ -52,7 +61,16 @@ groups() -> commit_ok, rollback_ok, get_config_ok, - refund_ok + refund_ok, + commit_inexistent_hold_fails, + partial_commit_inexistent_hold_fails, + commit_multirange_limit_ok + ]}, + {idempotency, [parallel], [ + commit_processes_idempotently, + full_commit_processes_idempotently, + partial_commit_processes_idempotently, + rollback_processes_idempotently ]} ]. @@ -85,12 +103,16 @@ init_per_suite(Config) -> -spec end_per_suite(config()) -> _. end_per_suite(Config) -> - _ = [application:stop(App) || App <- proplists:get_value(apps, Config)], - Config. + genlib_app:test_application_stop(?config(apps, Config)). -spec init_per_testcase(test_case_name(), config()) -> config(). -init_per_testcase(_Name, C) -> - [{test_sup, lim_mock:start_mocked_service_sup()} | C]. +init_per_testcase(Name, C) -> + [ + {id, gen_unique_id(Name)}, + {client, lim_client:new()}, + {test_sup, lim_mock:start_mocked_service_sup()} + | C + ]. -spec end_per_testcase(test_case_name(), config()) -> ok. end_per_testcase(_Name, C) -> @@ -99,12 +121,16 @@ end_per_testcase(_Name, C) -> %% +-define(CHANGE_ID, 42). +-define(LIMIT_CHANGE(ID), ?LIMIT_CHANGE(ID, ?CHANGE_ID)). +-define(LIMIT_CHANGE(ID, ChangeID), #limiter_LimitChange{id = ID, change_id = gen_change_id(ID, ChangeID)}). + -spec commit_with_default_exchange(config()) -> _. commit_with_default_exchange(C) -> Rational = #base_Rational{p = 1000000, q = 100}, _ = mock_exchange(Rational, C), - ID = lim_time:to_rfc3339(lim_time:now()), - #{client := Client} = prepare_environment(ID, <<"GlobalMonthTurnover">>, C), + _ = prepare_environment(<<"GlobalMonthTurnover">>, C), + ID = ?config(id, C), Context = #limiter_context_LimitContext{ payment_processing = #limiter_context_ContextPaymentProcessing{ op = {invoice, #limiter_context_PaymentProcessingOperationInvoice{}}, @@ -117,21 +143,15 @@ commit_with_default_exchange(C) -> } } }, - Timestamp = lim_time:to_rfc3339(lim_time:now()), - LimitChangeID = <>, - Change = #limiter_LimitChange{ - id = ID, - change_id = LimitChangeID - }, - {ok, {vector, _}} = hold_and_commit(Change, Context, Client), - {ok, #limiter_Limit{amount = 10000}} = lim_client:get(ID, Context, Client). + {ok, {vector, _}} = hold_and_commit(?LIMIT_CHANGE(ID), Context, ?config(client, C)), + {ok, #limiter_Limit{amount = 10000}} = lim_client:get(ID, Context, ?config(client, C)). -spec partial_commit_with_exchange(config()) -> _. partial_commit_with_exchange(C) -> Rational = #base_Rational{p = 800000, q = 100}, _ = mock_exchange(Rational, C), - ID = lim_time:to_rfc3339(lim_time:now()), - #{client := Client} = prepare_environment(ID, <<"GlobalMonthTurnover">>, C), + _ = prepare_environment(<<"GlobalMonthTurnover">>, C), + ID = ?config(id, C), Context = #limiter_context_LimitContext{ payment_processing = #limiter_context_ContextPaymentProcessing{ op = {invoice_payment, #limiter_context_PaymentProcessingOperationInvoicePayment{}}, @@ -150,21 +170,15 @@ partial_commit_with_exchange(C) -> } } }, - Timestamp = lim_time:to_rfc3339(lim_time:now()), - LimitChangeID = <>, - Change = #limiter_LimitChange{ - id = ID, - change_id = LimitChangeID - }, - {ok, {vector, _}} = hold_and_commit(Change, Context, Client), - {ok, #limiter_Limit{amount = 8400}} = lim_client:get(ID, Context, Client). + {ok, {vector, _}} = hold_and_commit(?LIMIT_CHANGE(ID), Context, ?config(client, C)), + {ok, #limiter_Limit{amount = 8400}} = lim_client:get(ID, Context, ?config(client, C)). -spec commit_with_exchange(config()) -> _. commit_with_exchange(C) -> Rational = #base_Rational{p = 1000000, q = 100}, _ = mock_exchange(Rational, C), - ID = lim_time:to_rfc3339(lim_time:now()), - #{client := Client} = prepare_environment(ID, <<"GlobalMonthTurnover">>, C), + _ = prepare_environment(<<"GlobalMonthTurnover">>, C), + ID = ?config(id, C), Context = #limiter_context_LimitContext{ payment_processing = #limiter_context_ContextPaymentProcessing{ op = {invoice, #limiter_context_PaymentProcessingOperationInvoice{}}, @@ -177,14 +191,8 @@ commit_with_exchange(C) -> } } }, - Timestamp = lim_time:to_rfc3339(lim_time:now()), - LimitChangeID = <>, - Change = #limiter_LimitChange{ - id = ID, - change_id = LimitChangeID - }, - {ok, {vector, _}} = hold_and_commit(Change, Context, Client), - {ok, #limiter_Limit{amount = 10500}} = lim_client:get(ID, Context, Client). + {ok, {vector, _}} = hold_and_commit(?LIMIT_CHANGE(ID), Context, ?config(client, C)), + {ok, #limiter_Limit{amount = 10500}} = lim_client:get(ID, Context, ?config(client, C)). -spec get_rate(config()) -> _. get_rate(C) -> @@ -206,20 +214,19 @@ get_rate(C) -> -spec get_limit_notfound(config()) -> _. get_limit_notfound(C) -> - ID = lim_time:to_rfc3339(lim_time:now()), - #{client := Client} = prepare_environment(ID, <<"GlobalMonthTurnover">>, C), + _ = prepare_environment(<<"GlobalMonthTurnover">>, C), Context = #limiter_context_LimitContext{ payment_processing = #limiter_context_ContextPaymentProcessing{ op = {invoice, #limiter_context_PaymentProcessingOperationInvoice{}}, invoice = #limiter_context_Invoice{created_at = <<"2000-01-01T00:00:00Z">>} } }, - {exception, #limiter_LimitNotFound{}} = lim_client:get(ID, Context, Client). + {exception, #limiter_LimitNotFound{}} = lim_client:get(?config(id, C), Context, ?config(client, C)). -spec hold_ok(config()) -> _. hold_ok(C) -> - ID = <<"ID">>, - #{client := Client} = prepare_environment(ID, <<"GlobalMonthTurnover">>, C), + _ = prepare_environment(<<"GlobalMonthTurnover">>, C), + ID = ?config(id, C), Context = #limiter_context_LimitContext{ payment_processing = #limiter_context_ContextPaymentProcessing{ op = {invoice, #limiter_context_PaymentProcessingOperationInvoice{}}, @@ -232,19 +239,13 @@ hold_ok(C) -> } } }, - Timestamp = lim_time:to_rfc3339(lim_time:now()), - LimitChangeID = <>, - Change = #limiter_LimitChange{ - id = ID, - change_id = LimitChangeID - }, - {ok, {vector, #limiter_VectorClock{}}} = lim_client:hold(Change, Context, Client), - {ok, #limiter_Limit{}} = lim_client:get(ID, Context, Client). + {ok, {vector, #limiter_VectorClock{}}} = lim_client:hold(?LIMIT_CHANGE(ID), Context, ?config(client, C)), + {ok, #limiter_Limit{}} = lim_client:get(ID, Context, ?config(client, C)). -spec commit_ok(config()) -> _. commit_ok(C) -> - ID = <<"ID">>, - #{client := Client} = prepare_environment(ID, <<"GlobalMonthTurnover">>, C), + _ = prepare_environment(<<"GlobalMonthTurnover">>, C), + ID = ?config(id, C), Context = #limiter_context_LimitContext{ payment_processing = #limiter_context_ContextPaymentProcessing{ op = {invoice, #limiter_context_PaymentProcessingOperationInvoice{}}, @@ -257,67 +258,162 @@ commit_ok(C) -> } } }, - Timestamp = lim_time:to_rfc3339(lim_time:now()), - LimitChangeID = <>, - Change = #limiter_LimitChange{ - id = ID, - change_id = LimitChangeID - }, - {ok, {vector, _}} = hold_and_commit(Change, Context, Client), - {ok, #limiter_Limit{}} = lim_client:get(ID, Context, Client). + {ok, {vector, _}} = hold_and_commit(?LIMIT_CHANGE(ID), Context, ?config(client, C)), + {ok, #limiter_Limit{}} = lim_client:get(ID, Context, ?config(client, C)). -spec rollback_ok(config()) -> _. rollback_ok(C) -> - ID = <<"ID">>, - #{client := Client} = prepare_environment(ID, <<"GlobalMonthTurnover">>, C), + _ = prepare_environment(<<"GlobalMonthTurnover">>, C), + ID = ?config(id, C), Context0 = ?ctx_invoice_payment(?cash(10), ?cash(10)), Context1 = ?ctx_invoice_payment(?cash(10), ?cash(0)), - - Timestamp = lim_time:to_rfc3339(lim_time:now()), - LimitChangeID = <>, - Change = #limiter_LimitChange{ - id = ID, - change_id = LimitChangeID - }, - {ok, {vector, _}} = lim_client:hold(Change, Context0, Client), - {ok, {vector, _}} = lim_client:commit(Change, Context1, Client). + Change = ?LIMIT_CHANGE(ID), + {ok, {vector, _}} = lim_client:hold(Change, Context0, ?config(client, C)), + {ok, {vector, _}} = lim_client:commit(Change, Context1, ?config(client, C)). -spec refund_ok(config()) -> _. refund_ok(C) -> - ID = lim_time:to_rfc3339(lim_time:now()), + ID = ?config(id, C), + Client = ?config(client, C), OwnerID = <<"WWWcool Ltd">>, ShopID = <<"shop">>, - #{client := Client} = _LimitConfig = prepare_environment(ID, <<"ShopDayTurnover">>, C), + _ = prepare_environment(<<"ShopDayTurnover">>, C), Context0 = ?ctx_invoice_payment(OwnerID, ShopID, ?cash(15), ?cash(15)), RefundContext1 = ?ctx_invoice_payment_refund(OwnerID, ShopID, ?cash(10), ?cash(10), ?cash(10)), - Timestamp = lim_time:to_rfc3339(lim_time:now()), - LimitChangeID = <>, - - Change = #limiter_LimitChange{ - id = ID, - change_id = LimitChangeID - }, - {ok, {vector, _}} = hold_and_commit(Change, Context0, Client), - - Timestamp2 = lim_time:to_rfc3339(lim_time:now()), - LimitChangeID2 = <>, - Change2 = #limiter_LimitChange{ - id = ID, - change_id = LimitChangeID2 - }, - - {ok, {vector, _}} = hold_and_commit(Change2, RefundContext1, Client), + {ok, {vector, _}} = hold_and_commit(?LIMIT_CHANGE(ID, <<"Payment">>), Context0, Client), + {ok, {vector, _}} = hold_and_commit(?LIMIT_CHANGE(ID, <<"Refund">>), RefundContext1, Client), {ok, #limiter_Limit{} = Limit2} = lim_client:get(ID, RefundContext1, Client), ?assertEqual(Limit2#limiter_Limit.amount, 5). -spec get_config_ok(config()) -> _. get_config_ok(C) -> - ID = <<"ID">>, - #{client := Client} = prepare_environment(ID, <<"GlobalMonthTurnover">>, C), - {ok, #limiter_config_LimitConfig{}} = lim_client:get_config(ID, Client). + _ = prepare_environment(<<"GlobalMonthTurnover">>, C), + {ok, #limiter_config_LimitConfig{}} = lim_client:get_config(?config(id, C), ?config(client, C)). + +-spec commit_inexistent_hold_fails(config()) -> _. +commit_inexistent_hold_fails(C) -> + ID = ?config(id, C), + _ = prepare_environment(<<"GlobalMonthTurnover">>, C), + Context = ?ctx_invoice_payment(?cash(42), undefined), + % NOTE + % We do not expect `LimitChangeNotFound` here because we no longer reconcile with accounter + % before requesting him to hold / commit. + {exception, #limiter_base_InvalidRequest{}} = + lim_client:commit(?LIMIT_CHANGE(ID), Context, ?config(client, C)). + +-spec partial_commit_inexistent_hold_fails(config()) -> _. +partial_commit_inexistent_hold_fails(C) -> + ID = ?config(id, C), + _ = prepare_environment(<<"GlobalMonthTurnover">>, C), + Context = ?ctx_invoice_payment(?cash(42), ?cash(21)), + % NOTE + % We do not expect `LimitChangeNotFound` here because we no longer reconcile with accounter + % before requesting him to hold / commit. + {exception, #limiter_base_InvalidRequest{}} = + lim_client:commit(?LIMIT_CHANGE(ID), Context, ?config(client, C)). + +-spec commit_multirange_limit_ok(config()) -> _. +commit_multirange_limit_ok(C) -> + ID = ?config(id, C), + Client = ?config(client, C), + Params = #limiter_config_LimitConfigParams{ + id = ID, + body_type = {cash, #limiter_config_LimitBodyTypeCash{currency = <<"RUB">>}}, + started_at = <<"2000-01-01T00:00:00Z">>, + shard_size = 12, + time_range_type = {calendar, {month, #time_range_TimeRangeTypeCalendarMonth{}}}, + context_type = {payment_processing, #limiter_config_LimitContextTypePaymentProcessing{}}, + type = {turnover, #limiter_config_LimitTypeTurnover{}}, + scope = {scope_global, #limiter_config_LimitScopeGlobal{}}, + op_behaviour = #limiter_config_OperationLimitBehaviour{} + }, + {ok, _LimitConfig} = lim_client:create_config(Params, Client), + % NOTE + % Expecting those 3 changes will be accounted in the same limit range machine. + % We have no way to verify it here though. + PaymentJan = #limiter_context_InvoicePayment{ + created_at = <<"2020-01-01T00:00:00Z">>, + cost = ?cash(42) + }, + {ok, _} = hold_and_commit(?LIMIT_CHANGE(ID, 1), ?ctx_invoice_payment(PaymentJan), Client), + PaymentFeb = #limiter_context_InvoicePayment{ + created_at = <<"2020-02-01T00:00:00Z">>, + cost = ?cash(43) + }, + {ok, _} = hold_and_commit(?LIMIT_CHANGE(ID, 2), ?ctx_invoice_payment(PaymentFeb), Client), + PaymentApr = #limiter_context_InvoicePayment{ + created_at = <<"2020-04-01T00:00:00Z">>, + cost = ?cash(44) + }, + {ok, _} = hold_and_commit(?LIMIT_CHANGE(ID, 3), ?ctx_invoice_payment(PaymentApr), Client), + {ok, #limiter_Limit{amount = 42}} = lim_client:get(ID, ?ctx_invoice_payment(PaymentJan), Client), + {ok, #limiter_Limit{amount = 43}} = lim_client:get(ID, ?ctx_invoice_payment(PaymentFeb), Client), + {ok, #limiter_Limit{amount = 44}} = lim_client:get(ID, ?ctx_invoice_payment(PaymentApr), Client). %% +-spec commit_processes_idempotently(config()) -> _. +commit_processes_idempotently(C) -> + ID = ?config(id, C), + Client = ?config(client, C), + _ = prepare_environment(<<"GlobalMonthTurnover">>, C), + Context = ?ctx_invoice_payment(?cash(42), undefined), + Change = ?LIMIT_CHANGE(ID), + {ok, _} = lim_client:hold(Change, Context, Client), + {ok, _} = lim_client:hold(Change, Context, Client), + {ok, _} = lim_client:commit(Change, Context, Client), + {ok, Limit = #limiter_Limit{amount = 42}} = lim_client:get(ID, Context, Client), + {ok, _} = lim_client:commit(Change, Context, Client), + {ok, Limit} = lim_client:get(ID, Context, Client). + +-spec full_commit_processes_idempotently(config()) -> _. +full_commit_processes_idempotently(C) -> + ID = ?config(id, C), + Client = ?config(client, C), + _ = prepare_environment(<<"GlobalMonthTurnover">>, C), + Cost = ?cash(42), + Context = ?ctx_invoice_payment(Cost, Cost), + Change = ?LIMIT_CHANGE(ID), + {ok, _} = lim_client:hold(Change, Context, Client), + {ok, _} = lim_client:hold(Change, Context, Client), + {ok, _} = lim_client:commit(Change, Context, Client), + {ok, Limit = #limiter_Limit{amount = 42}} = lim_client:get(ID, Context, Client), + {ok, _} = lim_client:commit(Change, Context, Client), + {ok, Limit} = lim_client:get(ID, Context, Client). + +-spec partial_commit_processes_idempotently(config()) -> _. +partial_commit_processes_idempotently(C) -> + ID = ?config(id, C), + Client = ?config(client, C), + _ = prepare_environment(<<"GlobalMonthTurnover">>, C), + Context = ?ctx_invoice_payment(?cash(42), ?cash(40)), + Change = ?LIMIT_CHANGE(ID), + {ok, _} = lim_client:hold(Change, Context, Client), + {ok, _} = lim_client:hold(Change, Context, Client), + {ok, _} = lim_client:commit(Change, Context, Client), + {ok, Limit = #limiter_Limit{amount = 40}} = lim_client:get(ID, Context, Client), + {ok, _} = lim_client:commit(Change, Context, Client), + {ok, Limit = #limiter_Limit{amount = 40}} = lim_client:get(ID, Context, Client). + +-spec rollback_processes_idempotently(config()) -> _. +rollback_processes_idempotently(C) -> + ID = ?config(id, C), + Client = ?config(client, C), + _ = prepare_environment(<<"GlobalMonthTurnover">>, C), + Context = ?ctx_invoice_payment(?cash(42), ?cash(0)), + Change = ?LIMIT_CHANGE(ID), + {ok, _} = lim_client:hold(Change, Context, Client), + {ok, _} = lim_client:hold(Change, Context, Client), + {ok, _} = lim_client:commit(Change, Context, Client), + {ok, Limit = #limiter_Limit{amount = 0}} = lim_client:get(ID, Context, Client), + {ok, _} = lim_client:commit(Change, Context, Client), + {ok, Limit = #limiter_Limit{amount = 0}} = lim_client:get(ID, Context, Client). + +%% + +gen_change_id(LimitID, ChangeID) -> + genlib:format("~s/~p", [LimitID, ChangeID]). + hold_and_commit(Change, Context, Client) -> {ok, {vector, _}} = lim_client:hold(Change, Context, Client), {ok, {vector, _}} = lim_client:commit(Change, Context, Client). @@ -325,8 +421,8 @@ hold_and_commit(Change, Context, Client) -> mock_exchange(Rational, C) -> lim_mock:mock_services([{xrates, fun('GetConvertedAmount', _) -> {ok, Rational} end}], C). -prepare_environment(ID, LimitName, _C) -> - Client = lim_client:new(), +prepare_environment(LimitName, C) -> + ID = ?config(id, C), Params = #limiter_cfg_LimitCreateParams{ id = ID, name = LimitName, @@ -337,5 +433,7 @@ prepare_environment(ID, LimitName, _C) -> invoice_payment_refund = {subtraction, #limiter_config_Subtraction{}} } }, - {ok, LimitConfig} = lim_client:legacy_create_config(Params, Client), - #{config => LimitConfig, client => Client}. + {ok, _LimitConfig} = lim_client:legacy_create_config(Params, ?config(client, C)). + +gen_unique_id(Prefix) -> + genlib:format("~s/~B", [Prefix, lim_time:now()]). diff --git a/elvis.config b/elvis.config index 22ae88d..988dab4 100644 --- a/elvis.config +++ b/elvis.config @@ -8,6 +8,7 @@ ruleset => erl_files, rules => [ {elvis_text_style, line_length, #{limit => 120}}, + {elvis_style, god_modules, #{limit => 30}}, {elvis_style, nesting_level, #{level => 3}}, {elvis_style, no_if_expression, disable} ]