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
This commit is contained in:
Andrew Mayorov 2022-04-19 18:03:09 +03:00 committed by GitHub
parent f577d1292a
commit 3b10d1cbf3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 400 additions and 338 deletions

View File

@ -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]).

View File

@ -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.

View File

@ -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) ->

View File

@ -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
}.

View File

@ -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} ->

View File

@ -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

View File

@ -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),
<<LimitID/binary, Prefix/binary, "/", ShardID/binary>>.
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 ->

View File

@ -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{}},

View File

@ -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) ->

View File

@ -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 = <<Timestamp/binary, "Commit">>,
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 = <<Timestamp/binary, "PartialCommit">>,
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 = <<Timestamp/binary, "Commit">>,
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 = <<Timestamp/binary, "Hold">>,
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 = <<Timestamp/binary, "Commit">>,
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 = <<Timestamp/binary, "Rollback">>,
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 = <<Timestamp/binary, "Payment">>,
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 = <<Timestamp2/binary, "Refund">>,
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()]).

View File

@ -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}
]