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). -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([get_body/3]).
-export([create_body_from_cash/2]). -export([create_body_from_cash/2]).
@ -17,7 +14,7 @@
-type currency() :: lim_base_thrift:'CurrencySymbolicCode'(). -type currency() :: lim_base_thrift:'CurrencySymbolicCode'().
-type config() :: lim_config_machine:config(). -type config() :: lim_config_machine:config().
-type body_type() :: full | partial. -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([t/0]).
-export_type([cash/0]). -export_type([cash/0]).

View File

@ -1,7 +1,6 @@
-module(lim_config_machine). -module(lim_config_machine).
-include_lib("limiter_proto/include/lim_limiter_thrift.hrl"). -include_lib("limiter_proto/include/lim_limiter_thrift.hrl").
-include_lib("limiter_proto/include/lim_base_thrift.hrl").
%% Accessors %% Accessors
@ -9,6 +8,7 @@
-export([id/1]). -export([id/1]).
-export([description/1]). -export([description/1]).
-export([body_type/1]). -export([body_type/1]).
-export([currency/1]).
-export([started_at/1]). -export([started_at/1]).
-export([shard_size/1]). -export([shard_size/1]).
-export([time_range_type/1]). -export([time_range_type/1]).
@ -183,6 +183,12 @@ description(_) ->
body_type(#{body_type := BodyType}) -> body_type(#{body_type := BodyType}) ->
BodyType. BodyType.
-spec currency(config()) -> currency() | undefined.
currency(#{body_type := {cash, Currency}}) ->
Currency;
currency(#{body_type := amount}) ->
undefined.
-spec started_at(config()) -> timestamp(). -spec started_at(config()) -> timestamp().
started_at(#{started_at := Value}) -> started_at(#{started_at := Value}) ->
Value. Value.

View File

@ -86,8 +86,6 @@ handle_function_('Rollback', {LimitChange = ?LIMIT_CHANGE(LimitID), Clock, Conte
end. end.
-spec handle_get_error(_) -> no_return(). -spec handle_get_error(_) -> no_return().
handle_get_error({_, {limit, notfound}}) ->
woody_error:raise(business, #limiter_LimitNotFound{});
handle_get_error({_, {range, notfound}}) -> handle_get_error({_, {range, notfound}}) ->
woody_error:raise(business, #limiter_LimitNotFound{}); woody_error:raise(business, #limiter_LimitNotFound{});
handle_get_error(Error) -> handle_get_error(Error) ->
@ -102,16 +100,12 @@ handle_hold_error(Error) ->
-spec handle_commit_error(_) -> no_return(). -spec handle_commit_error(_) -> no_return().
handle_commit_error({_, {forbidden_operation_amount, Error}}) -> handle_commit_error({_, {forbidden_operation_amount, Error}}) ->
handle_forbidden_operation_amount_error(Error); handle_forbidden_operation_amount_error(Error);
handle_commit_error({_, {plan, notfound}}) ->
woody_error:raise(business, #limiter_LimitChangeNotFound{});
handle_commit_error({_, {invalid_request, Errors}}) -> handle_commit_error({_, {invalid_request, Errors}}) ->
woody_error:raise(business, #limiter_base_InvalidRequest{errors = Errors}); woody_error:raise(business, #limiter_base_InvalidRequest{errors = Errors});
handle_commit_error(Error) -> handle_commit_error(Error) ->
handle_default_error(Error). handle_default_error(Error).
-spec handle_rollback_error(_) -> no_return(). -spec handle_rollback_error(_) -> no_return().
handle_rollback_error({_, {plan, notfound}}) ->
woody_error:raise(business, #limiter_LimitChangeNotFound{});
handle_rollback_error({_, {invalid_request, Errors}}) -> handle_rollback_error({_, {invalid_request, Errors}}) ->
woody_error:raise(business, #limiter_base_InvalidRequest{errors = Errors}); woody_error:raise(business, #limiter_base_InvalidRequest{errors = Errors});
handle_rollback_error(Error) -> handle_rollback_error(Error) ->

View File

@ -1,94 +1,34 @@
-module(lim_p_transfer). -module(lim_p_transfer).
-include_lib("damsel/include/dmsl_accounter_thrift.hrl"). -include_lib("damsel/include/dmsl_accounter_thrift.hrl").
-include_lib("damsel/include/dmsl_base_thrift.hrl").
-export([construct_postings/3]). -export([construct_posting/2]).
-export([reverse_postings/1]). -export([reverse_posting/1]).
-export([assert_partial_posting_amount/2]).
-type amount() :: integer().
-type currency() :: binary().
-type account_id() :: lim_accounting:account_id().
-type posting() :: lim_accounting:posting(). -type posting() :: lim_accounting:posting().
-type body() :: lim_body:t(). -type body() :: lim_body:t().
-type forbidden_operation_amount_error() :: #{ -spec construct_posting(lim_range_machine:time_range_ext(), body()) -> posting().
type := positive | negative, construct_posting(#{account_id_from := From, account_id_to := To}, {cash, #{amount := Amount, currency := Currency}}) ->
partial := amount(), #accounter_Posting{
full := amount(), from_id = From,
currency := currency() 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 reverse_posting(posting()) -> posting().
reverse_posting(Posting = #accounter_Posting{from_id = AccountFrom, to_id = AccountTo}) ->
-spec construct_postings(account_id(), account_id(), body()) -> [posting()]. Posting#accounter_Posting{
construct_postings(AccountFrom, AccountTo, {cash, #{amount := Amount, currency := Currency}}) -> from_id = AccountTo,
[ to_id = AccountFrom
#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.

View File

@ -11,11 +11,9 @@
%% API %% API
-export([get/2]). -export([get/2]).
-export([ensure_exist/2]). -export([ensure_exists/3]).
-export([get_range/2]). -export([get_range/2]).
-export([get_range_balance/3]). -export([get_range_balance/3]).
-export([ensure_range_exist/3]).
-export([ensure_range_exist_in_state/3]).
%% Machinery callbacks %% Machinery callbacks
@ -38,13 +36,13 @@
-type woody_context() :: woody_context:ctx(). -type woody_context() :: woody_context:ctx().
-type lim_context() :: lim_context:t(). -type lim_context() :: lim_context:t().
-type timestamp() :: lim_config_machine:timestamp(). -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_type() :: lim_config_machine:time_range_type().
-type time_range() :: lim_config_machine:time_range(). -type time_range() :: lim_config_machine:time_range().
-type currency() :: lim_config_machine:currency(). -type currency() :: lim_config_machine:currency().
-type limit_range_state() :: #{ -type limit_range_state() :: #{
id := lim_id(), id := id(),
type := time_range_type(), type := time_range_type(),
created_at := timestamp(), created_at := timestamp(),
currency => currency(), currency => currency(),
@ -59,7 +57,7 @@
| {time_range_created, time_range_ext()}. | {time_range_created, time_range_ext()}.
-type limit_range() :: #{ -type limit_range() :: #{
id := lim_id(), id := id(),
type := time_range_type(), type := time_range_type(),
created_at := timestamp(), created_at := timestamp(),
currency => currency() currency => currency()
@ -73,7 +71,7 @@
}. }.
-type create_params() :: #{ -type create_params() :: #{
id := lim_id(), id := id(),
type := time_range_type(), type := time_range_type(),
created_at := timestamp(), created_at := timestamp(),
currency => currency() currency => currency()
@ -82,16 +80,18 @@
-type range_call() :: -type range_call() ::
{add_range, time_range()}. {add_range, time_range()}.
-export_type([time_range_ext/0]).
-export_type([timestamped_event/1]). -export_type([timestamped_event/1]).
-export_type([event/0]). -export_type([event/0]).
-define(NS, 'lim/range_v1'). -define(NS, 'lim/range_v1').
-import(lim_pipeline, [do/1, unwrap/1, unwrap/2]). -import(lim_pipeline, [do/1, unwrap/1]).
%% Accessors %% Accessors
-spec id(limit_range_state()) -> lim_id(). -spec id(limit_range_state()) -> id().
id(State) -> id(State) ->
maps:get(id, State). maps:get(id, State).
@ -117,54 +117,25 @@ currency(_State) ->
%%% API %%% 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) -> get(ID, LimitContext) ->
{ok, WoodyCtx} = lim_context:woody_context(LimitContext), {ok, WoodyCtx} = lim_context:woody_context(LimitContext),
get_state(ID, WoodyCtx). get_state(ID, WoodyCtx).
-spec ensure_exist(create_params(), lim_context()) -> {ok, limit_range_state()}. -spec ensure_exists(create_params(), time_range(), lim_context()) -> {ok, time_range_ext()}.
ensure_exist(Params = #{id := ID}, LimitContext) -> ensure_exists(Params = #{id := ID, currency := Currency}, TimeRange, LimitContext) ->
{ok, WoodyCtx} = lim_context:woody_context(LimitContext), {ok, WoodyCtx} = lim_context:woody_context(LimitContext),
case get_state(ID, WoodyCtx) of case get_state(ID, WoodyCtx) of
{ok, State} -> {ok, State} ->
{ok, State}; ensure_range_exist_in_state(TimeRange, State, WoodyCtx);
{error, notfound} -> {error, notfound} ->
_ = start(ID, Params, WoodyCtx), _ = start(ID, Params, [new_time_range_ext(TimeRange, Currency, WoodyCtx)], WoodyCtx),
case get_state(ID, WoodyCtx) of {ok, State} = get_state(ID, WoodyCtx),
{ok, State} -> get_range(TimeRange, State)
{ok, State};
{error, notfound} ->
erlang:error({cant_get_after_start, ID})
end
end. end.
-spec get_range(time_range(), limit_range_state()) -> {ok, time_range_ext()} | {error, notfound}. -spec ensure_range_exist_in_state(time_range(), limit_range_state(), woody_context()) -> {ok, time_range_ext()}.
get_range(TimeRange, State) -> ensure_range_exist_in_state(TimeRange, State, WoodyCtx) ->
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),
case find_time_range(TimeRange, ranges(State)) of case find_time_range(TimeRange, ranges(State)) of
{error, notfound} -> {error, notfound} ->
call(id(State), {add_range, TimeRange}, WoodyCtx); call(id(State), {add_range, TimeRange}, WoodyCtx);
@ -172,6 +143,21 @@ ensure_range_exist_in_state(TimeRange, State, LimitContext) ->
{ok, Range} {ok, Range}
end. 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 %%% Machinery callbacks
-spec init(args([event()]), machine(), handler_args(), handler_opts()) -> result(). -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()) -> -spec process_call(args(range_call()), machine(), handler_args(), handler_opts()) ->
{response(time_range_ext()), result()} | no_return(). {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), State = collapse(Machine),
case find_time_range(TimeRange0, ranges(State)) of case find_time_range(TimeRange, ranges(State)) of
{error, notfound} -> {error, notfound} ->
Currency = currency(State), TimeRangeExt = new_time_range_ext(TimeRange, currency(State), WoodyCtx),
{ok, LimitContext} = lim_context:create(WoodyCtx), {TimeRangeExt, #{events => emit_events([{time_range_created, TimeRangeExt}])}};
{ok, AccountIDFrom} = lim_accounting:create_account(Currency, LimitContext), {ok, TimeRangeExt} ->
{ok, AccountIDTo} = lim_accounting:create_account(Currency, LimitContext), {ok, TimeRangeExt}
TimeRange1 = TimeRange0#{
account_id_from => AccountIDFrom,
account_id_to => AccountIDTo
},
{TimeRange1, #{events => emit_events([{time_range_created, TimeRange1}])}};
{ok, Range} ->
{Range, #{}}
end. end.
-spec process_timeout(machine(), handler_args(), handler_opts()) -> no_return(). -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, [_Head | Rest]) ->
find_time_range(TimeRange, 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}. -spec start(id(), create_params(), [time_range_ext()], woody_context()) -> ok | {error, exists}.
start(ID, Params, WoodyCtx) -> start(ID, Params, TimeRanges, WoodyCtx) ->
machinery:start(?NS, ID, [{created, Params}], get_backend(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) -> call(ID, Msg, WoodyCtx) ->
machinery:call(?NS, ID, Msg, get_backend(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) -> get_state(ID, WoodyCtx) ->
case machinery:get(?NS, ID, get_backend(WoodyCtx)) of case machinery:get(?NS, ID, get_backend(WoodyCtx)) of
{ok, Machine} -> {ok, Machine} ->

View File

@ -1,8 +1,6 @@
-module(lim_rates). -module(lim_rates).
-include_lib("xrates_proto/include/xrates_rate_thrift.hrl"). -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]). -export([get_converted_amount/3]).
@ -11,9 +9,9 @@
-type limit_context() :: lim_context:t(). -type limit_context() :: lim_context:t().
-type config() :: lim_config_machine:config(). -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(APP, limiter).
-define(DEFAULT_FACTOR, 1.1). -define(DEFAULT_FACTOR, 1.1).
@ -21,7 +19,7 @@
-spec get_converted_amount({amount(), currency()}, config(), limit_context()) -> -spec get_converted_amount({amount(), currency()}, config(), limit_context()) ->
{ok, amount()} {ok, amount()}
| {error, convertation_error()}. | {error, conversion_error()}.
get_converted_amount(Cash = {_Amount, Currency}, Config, LimitContext) -> get_converted_amount(Cash = {_Amount, Currency}, Config, LimitContext) ->
Factor = get_exchange_factor(Currency), Factor = get_exchange_factor(Currency),
case case

View File

@ -1,8 +1,6 @@
-module(lim_turnover_processor). -module(lim_turnover_processor).
-include_lib("limiter_proto/include/lim_base_thrift.hrl").
-include_lib("limiter_proto/include/lim_limiter_thrift.hrl"). -include_lib("limiter_proto/include/lim_limiter_thrift.hrl").
-include_lib("damsel/include/dmsl_accounter_thrift.hrl").
-behaviour(lim_config_machine). -behaviour(lim_config_machine).
@ -24,7 +22,7 @@
full := amount() full := amount()
}. }.
-type get_limit_error() :: {limit | range, notfound}. -type get_limit_error() :: {range, notfound}.
-type hold_error() :: -type hold_error() ::
lim_body:get_body_error() lim_body:get_body_error()
@ -32,11 +30,12 @@
-type commit_error() :: -type commit_error() ::
{forbidden_operation_amount, forbidden_operation_amount_error()} {forbidden_operation_amount, forbidden_operation_amount_error()}
| {plan, notfound} | lim_body:get_body_error()
| {full | partial, lim_body:get_body_error()}
| lim_accounting:invalid_request_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([get_limit_error/0]).
-export_type([hold_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()}. -spec get_limit(lim_id(), config(), lim_context()) -> {ok, limit()} | {error, get_limit_error()}.
get_limit(LimitID, Config, LimitContext) -> get_limit(LimitID, Config, LimitContext) ->
do(fun() -> do(fun() ->
{ok, Timestamp} = lim_context:get_from_context(payment_processing, created_at, LimitContext), {LimitRangeID, TimeRange} = compute_limit_time_range_location(LimitID, Config, 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),
#{max_available_amount := Amount} = #{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{ #limiter_Limit{
id = LimitRangeID, id = LimitRangeID,
amount = Amount, amount = Amount,
@ -63,53 +59,66 @@ get_limit(LimitID, Config, LimitContext) ->
end). end).
-spec hold(lim_change(), config(), lim_context()) -> ok | {error, hold_error()}. -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() -> do(fun() ->
{ok, Timestamp} = lim_context:get_from_context(payment_processing, created_at, LimitContext), TimeRangeAccount = ensure_limit_time_range(LimitID, Config, LimitContext),
{ok, Body} = lim_body:get_body(full, Config, LimitContext), Body = unwrap(lim_body:get_body(full, Config, LimitContext)),
LimitRangeID = construct_range_id(LimitID, Timestamp, Config, LimitContext), Posting = construct_posting(TimeRangeAccount, Body, Config, LimitContext),
Currency = unwrap(lim_accounting:hold(construct_plan_id(LimitChange), {1, [Posting]}, LimitContext))
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)
end). end).
-spec commit(lim_change(), config(), lim_context()) -> ok | {error, commit_error()}. -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() -> do(fun() ->
case lim_body:get_body(partial, Config, LimitContext) of TimeRangeAccount = ensure_limit_time_range(LimitID, Config, LimitContext),
{ok, Body} -> PlanID = construct_plan_id(LimitChange),
unwrap(partial_commit(Body, LimitChange, Config, LimitContext)); Operations = construct_commit_plan(TimeRangeAccount, Config, LimitContext),
{error, notfound} -> ok = lists:foreach(
PlanID = construct_plan_id(LimitChange), fun
[Batch] = unwrap(plan, lim_accounting:get_plan(PlanID, LimitContext)), ({hold, Batch}) ->
unwrap(lim_accounting:commit(PlanID, [Batch], LimitContext)) % NOTE
end % 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). end).
-spec rollback(lim_change(), config(), lim_context()) -> ok | {error, rollback_error()}. -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() -> do(fun() ->
PlanID = construct_plan_id(LimitChange), TimeRangeAccount = ensure_limit_time_range(LimitID, Config, LimitContext),
BatchList = unwrap(plan, lim_accounting:get_plan(PlanID, LimitContext)), Body = unwrap(lim_body:get_body(full, Config, LimitContext)),
unwrap(lim_accounting:rollback(PlanID, BatchList, LimitContext)) Posting = construct_posting(TimeRangeAccount, Body, Config, LimitContext),
unwrap(lim_accounting:rollback(construct_plan_id(LimitChange), [{1, [Posting]}], LimitContext))
end). 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}) -> construct_plan_id(#limiter_LimitChange{change_id = ChangeID}) ->
% DISCUSS
ChangeID. ChangeID.
construct_range_id(LimitID, Timestamp, Config, LimitContext) -> 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), ShardID = lim_config_machine:calculate_shard_id(Timestamp, Config),
<<LimitID/binary, Prefix/binary, "/", ShardID/binary>>. <<LimitID/binary, Prefix/binary, "/", ShardID/binary>>.
partial_commit(PartialBody, LimitChange = #limiter_LimitChange{id = LimitID}, Config, LimitContext) -> construct_commit_plan(TimeRangeAccount, Config, LimitContext) ->
do(fun() -> Body = unwrap(lim_body:get_body(full, Config, LimitContext)),
{ok, Timestamp} = lim_context:get_from_context(payment_processing, created_at, LimitContext), MaybePartialBody = lim_body:get_body(partial, Config, LimitContext),
{ok, FullBody} = lim_body:get_body(full, Config, LimitContext), construct_commit_postings(TimeRangeAccount, Body, MaybePartialBody, Config, LimitContext).
ok = unwrap(assert_partial_body(PartialBody, FullBody)),
LimitRangeID = construct_range_id(LimitID, Timestamp, Config, LimitContext), construct_commit_postings(TimeRangeAccount, Full, MaybePartialBody, Config, LimitContext) ->
{ok, LimitRangeState} = lim_range_machine:get( OriginalHoldPosting = construct_posting(TimeRangeAccount, Full, Config, LimitContext),
LimitRangeID, case determine_commit_intent(MaybePartialBody, Full) of
LimitContext commit ->
), [{commit, [{1, [OriginalHoldPosting]}]}];
TimeRange = lim_config_machine:calculate_time_range(Timestamp, Config), rollback ->
{ok, #{account_id_from := AccountIDFrom, account_id_to := AccountIDTo}} = [{rollback, [{1, [OriginalHoldPosting]}]}];
lim_range_machine:get_range(TimeRange, LimitRangeState), {commit, Partial} ->
PartialPostings0 = lim_p_transfer:construct_postings(AccountIDFrom, AccountIDTo, PartialBody), % Partial body is less than full body
FullPostings0 = lim_p_transfer:construct_postings(AccountIDFrom, AccountIDTo, FullBody), ok = unwrap(assert_partial_body(Partial, Full)),
PartialPostings1 = apply_op_behaviour(PartialPostings0, LimitContext, Config), ReverseHoldPosting = lim_p_transfer:reverse_posting(OriginalHoldPosting),
FullPostings1 = apply_op_behaviour(FullPostings0, LimitContext, Config), PartialHoldPosting = construct_posting(TimeRangeAccount, Partial, Config, LimitContext),
NewBatchList = [{2, lim_p_transfer:reverse_postings(FullPostings1)} | [{3, PartialPostings1}]], PartialBatch = [ReverseHoldPosting, PartialHoldPosting],
PlanID = construct_plan_id(LimitChange), [
unwrap(lim_accounting:plan(PlanID, NewBatchList, LimitContext)), {hold, {2, PartialBatch}},
unwrap(lim_accounting:commit(PlanID, [{1, FullPostings1} | NewBatchList], LimitContext)) {commit, [
end). {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), {ok, Operation} = lim_context:get_operation(payment_processing, LimitContext),
case maps:get(Operation, ComputationConfig, undefined) of case maps:get(Operation, ComputationConfig, undefined) of
subtraction -> subtraction ->
lim_p_transfer:reverse_postings(Posting); lim_p_transfer:reverse_posting(Posting);
Type when Type =:= undefined orelse Type =:= additional -> Type when Type =:= undefined orelse Type =:= additional ->
Posting Posting
end; end;
apply_op_behaviour(Body, _LimitContext, _Config) -> apply_op_behaviour(Body, _Config, _LimitContext) ->
Body. Body.
assert_partial_body( assert_partial_body(
@ -164,7 +194,7 @@ assert_partial_body(
erlang:error({invalid_partial_cash, {Partial, PartialCurrency}, {Full, FullCurrency}}). erlang:error({invalid_partial_cash, {Partial, PartialCurrency}, {Full, FullCurrency}}).
compare_amount(Partial, Full, Currency) when Full > 0 -> compare_amount(Partial, Full, Currency) when Full > 0 ->
case Partial =< Full of case Partial < Full of
true -> true ->
ok; ok;
false -> false ->
@ -178,7 +208,7 @@ compare_amount(Partial, Full, Currency) when Full > 0 ->
})}} })}}
end; end;
compare_amount(Partial, Full, Currency) when Full < 0 -> compare_amount(Partial, Full, Currency) when Full < 0 ->
case Partial >= Full of case Partial > Full of
true -> true ->
ok; ok;
false -> false ->

View File

@ -8,11 +8,13 @@
currency = #limiter_base_CurrencyRef{symbolic_code = <<"RUB">>} 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(Cost, CaptureCost), ?ctx_invoice_payment(undefined, undefined, Cost, CaptureCost)).
-define(ctx_invoice_payment(OwnerID, ShopID, Cost, CaptureCost), #limiter_context_LimitContext{ -define(ctx_invoice_payment(OwnerID, ShopID, Cost, CaptureCost), #limiter_context_LimitContext{
payment_processing = #limiter_context_ContextPaymentProcessing{ payment_processing = #limiter_context_ContextPaymentProcessing{
op = {invoice_payment, #limiter_context_PaymentProcessingOperationInvoicePayment{}}, op = ?op_invoice_payment(),
invoice = #limiter_context_Invoice{ invoice = #limiter_context_Invoice{
owner_id = OwnerID, owner_id = OwnerID,
shop_id = ShopID, 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{ -define(ctx_invoice_payment_refund(OwnerID, ShopID, Cost, CaptureCost, RefundCost), #limiter_context_LimitContext{
payment_processing = #limiter_context_ContextPaymentProcessing{ payment_processing = #limiter_context_ContextPaymentProcessing{
op = {invoice_payment_refund, #limiter_context_PaymentProcessingOperationInvoicePaymentRefund{}}, op = {invoice_payment_refund, #limiter_context_PaymentProcessingOperationInvoicePaymentRefund{}},

View File

@ -19,9 +19,7 @@ stop_mocked_service_sup(SupPid) ->
exit(SupPid, shutdown). exit(SupPid, shutdown).
-define(HOST_IP, "::"). -define(HOST_IP, "::").
-define(HOST_PORT, 8080).
-define(HOST_NAME, "localhost"). -define(HOST_NAME, "localhost").
-define(HOST_URL, ?HOST_NAME ++ ":" ++ integer_to_list(?HOST_PORT)).
-spec mock_services(_, _) -> _. -spec mock_services(_, _) -> _.
mock_services(Services, SupOrConfig) -> mock_services(Services, SupOrConfig) ->

View File

@ -25,6 +25,14 @@
-export([rollback_ok/1]). -export([rollback_ok/1]).
-export([refund_ok/1]). -export([refund_ok/1]).
-export([get_config_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 group_name() :: atom().
-type test_case_name() :: atom(). -type test_case_name() :: atom().
@ -36,7 +44,8 @@
-spec all() -> [{group, group_name()}]. -spec all() -> [{group, group_name()}].
all() -> all() ->
[ [
{group, default} {group, default},
{group, idempotency}
]. ].
-spec groups() -> [{atom(), list(), [test_case_name()]}]. -spec groups() -> [{atom(), list(), [test_case_name()]}].
@ -52,7 +61,16 @@ groups() ->
commit_ok, commit_ok,
rollback_ok, rollback_ok,
get_config_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()) -> _. -spec end_per_suite(config()) -> _.
end_per_suite(Config) -> end_per_suite(Config) ->
_ = [application:stop(App) || App <- proplists:get_value(apps, Config)], genlib_app:test_application_stop(?config(apps, Config)).
Config.
-spec init_per_testcase(test_case_name(), config()) -> config(). -spec init_per_testcase(test_case_name(), config()) -> config().
init_per_testcase(_Name, C) -> init_per_testcase(Name, C) ->
[{test_sup, lim_mock:start_mocked_service_sup()} | 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. -spec end_per_testcase(test_case_name(), config()) -> ok.
end_per_testcase(_Name, C) -> 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()) -> _. -spec commit_with_default_exchange(config()) -> _.
commit_with_default_exchange(C) -> commit_with_default_exchange(C) ->
Rational = #base_Rational{p = 1000000, q = 100}, Rational = #base_Rational{p = 1000000, q = 100},
_ = mock_exchange(Rational, C), _ = mock_exchange(Rational, C),
ID = lim_time:to_rfc3339(lim_time:now()), _ = prepare_environment(<<"GlobalMonthTurnover">>, C),
#{client := Client} = prepare_environment(ID, <<"GlobalMonthTurnover">>, C), ID = ?config(id, C),
Context = #limiter_context_LimitContext{ Context = #limiter_context_LimitContext{
payment_processing = #limiter_context_ContextPaymentProcessing{ payment_processing = #limiter_context_ContextPaymentProcessing{
op = {invoice, #limiter_context_PaymentProcessingOperationInvoice{}}, op = {invoice, #limiter_context_PaymentProcessingOperationInvoice{}},
@ -117,21 +143,15 @@ commit_with_default_exchange(C) ->
} }
} }
}, },
Timestamp = lim_time:to_rfc3339(lim_time:now()), {ok, {vector, _}} = hold_and_commit(?LIMIT_CHANGE(ID), Context, ?config(client, C)),
LimitChangeID = <<Timestamp/binary, "Commit">>, {ok, #limiter_Limit{amount = 10000}} = lim_client:get(ID, Context, ?config(client, C)).
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).
-spec partial_commit_with_exchange(config()) -> _. -spec partial_commit_with_exchange(config()) -> _.
partial_commit_with_exchange(C) -> partial_commit_with_exchange(C) ->
Rational = #base_Rational{p = 800000, q = 100}, Rational = #base_Rational{p = 800000, q = 100},
_ = mock_exchange(Rational, C), _ = mock_exchange(Rational, C),
ID = lim_time:to_rfc3339(lim_time:now()), _ = prepare_environment(<<"GlobalMonthTurnover">>, C),
#{client := Client} = prepare_environment(ID, <<"GlobalMonthTurnover">>, C), ID = ?config(id, C),
Context = #limiter_context_LimitContext{ Context = #limiter_context_LimitContext{
payment_processing = #limiter_context_ContextPaymentProcessing{ payment_processing = #limiter_context_ContextPaymentProcessing{
op = {invoice_payment, #limiter_context_PaymentProcessingOperationInvoicePayment{}}, op = {invoice_payment, #limiter_context_PaymentProcessingOperationInvoicePayment{}},
@ -150,21 +170,15 @@ partial_commit_with_exchange(C) ->
} }
} }
}, },
Timestamp = lim_time:to_rfc3339(lim_time:now()), {ok, {vector, _}} = hold_and_commit(?LIMIT_CHANGE(ID), Context, ?config(client, C)),
LimitChangeID = <<Timestamp/binary, "PartialCommit">>, {ok, #limiter_Limit{amount = 8400}} = lim_client:get(ID, Context, ?config(client, C)).
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).
-spec commit_with_exchange(config()) -> _. -spec commit_with_exchange(config()) -> _.
commit_with_exchange(C) -> commit_with_exchange(C) ->
Rational = #base_Rational{p = 1000000, q = 100}, Rational = #base_Rational{p = 1000000, q = 100},
_ = mock_exchange(Rational, C), _ = mock_exchange(Rational, C),
ID = lim_time:to_rfc3339(lim_time:now()), _ = prepare_environment(<<"GlobalMonthTurnover">>, C),
#{client := Client} = prepare_environment(ID, <<"GlobalMonthTurnover">>, C), ID = ?config(id, C),
Context = #limiter_context_LimitContext{ Context = #limiter_context_LimitContext{
payment_processing = #limiter_context_ContextPaymentProcessing{ payment_processing = #limiter_context_ContextPaymentProcessing{
op = {invoice, #limiter_context_PaymentProcessingOperationInvoice{}}, op = {invoice, #limiter_context_PaymentProcessingOperationInvoice{}},
@ -177,14 +191,8 @@ commit_with_exchange(C) ->
} }
} }
}, },
Timestamp = lim_time:to_rfc3339(lim_time:now()), {ok, {vector, _}} = hold_and_commit(?LIMIT_CHANGE(ID), Context, ?config(client, C)),
LimitChangeID = <<Timestamp/binary, "Commit">>, {ok, #limiter_Limit{amount = 10500}} = lim_client:get(ID, Context, ?config(client, C)).
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).
-spec get_rate(config()) -> _. -spec get_rate(config()) -> _.
get_rate(C) -> get_rate(C) ->
@ -206,20 +214,19 @@ get_rate(C) ->
-spec get_limit_notfound(config()) -> _. -spec get_limit_notfound(config()) -> _.
get_limit_notfound(C) -> get_limit_notfound(C) ->
ID = lim_time:to_rfc3339(lim_time:now()), _ = prepare_environment(<<"GlobalMonthTurnover">>, C),
#{client := Client} = prepare_environment(ID, <<"GlobalMonthTurnover">>, C),
Context = #limiter_context_LimitContext{ Context = #limiter_context_LimitContext{
payment_processing = #limiter_context_ContextPaymentProcessing{ payment_processing = #limiter_context_ContextPaymentProcessing{
op = {invoice, #limiter_context_PaymentProcessingOperationInvoice{}}, op = {invoice, #limiter_context_PaymentProcessingOperationInvoice{}},
invoice = #limiter_context_Invoice{created_at = <<"2000-01-01T00:00:00Z">>} 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()) -> _. -spec hold_ok(config()) -> _.
hold_ok(C) -> hold_ok(C) ->
ID = <<"ID">>, _ = prepare_environment(<<"GlobalMonthTurnover">>, C),
#{client := Client} = prepare_environment(ID, <<"GlobalMonthTurnover">>, C), ID = ?config(id, C),
Context = #limiter_context_LimitContext{ Context = #limiter_context_LimitContext{
payment_processing = #limiter_context_ContextPaymentProcessing{ payment_processing = #limiter_context_ContextPaymentProcessing{
op = {invoice, #limiter_context_PaymentProcessingOperationInvoice{}}, op = {invoice, #limiter_context_PaymentProcessingOperationInvoice{}},
@ -232,19 +239,13 @@ hold_ok(C) ->
} }
} }
}, },
Timestamp = lim_time:to_rfc3339(lim_time:now()), {ok, {vector, #limiter_VectorClock{}}} = lim_client:hold(?LIMIT_CHANGE(ID), Context, ?config(client, C)),
LimitChangeID = <<Timestamp/binary, "Hold">>, {ok, #limiter_Limit{}} = lim_client:get(ID, Context, ?config(client, C)).
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).
-spec commit_ok(config()) -> _. -spec commit_ok(config()) -> _.
commit_ok(C) -> commit_ok(C) ->
ID = <<"ID">>, _ = prepare_environment(<<"GlobalMonthTurnover">>, C),
#{client := Client} = prepare_environment(ID, <<"GlobalMonthTurnover">>, C), ID = ?config(id, C),
Context = #limiter_context_LimitContext{ Context = #limiter_context_LimitContext{
payment_processing = #limiter_context_ContextPaymentProcessing{ payment_processing = #limiter_context_ContextPaymentProcessing{
op = {invoice, #limiter_context_PaymentProcessingOperationInvoice{}}, op = {invoice, #limiter_context_PaymentProcessingOperationInvoice{}},
@ -257,67 +258,162 @@ commit_ok(C) ->
} }
} }
}, },
Timestamp = lim_time:to_rfc3339(lim_time:now()), {ok, {vector, _}} = hold_and_commit(?LIMIT_CHANGE(ID), Context, ?config(client, C)),
LimitChangeID = <<Timestamp/binary, "Commit">>, {ok, #limiter_Limit{}} = lim_client:get(ID, Context, ?config(client, C)).
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).
-spec rollback_ok(config()) -> _. -spec rollback_ok(config()) -> _.
rollback_ok(C) -> rollback_ok(C) ->
ID = <<"ID">>, _ = prepare_environment(<<"GlobalMonthTurnover">>, C),
#{client := Client} = prepare_environment(ID, <<"GlobalMonthTurnover">>, C), ID = ?config(id, C),
Context0 = ?ctx_invoice_payment(?cash(10), ?cash(10)), Context0 = ?ctx_invoice_payment(?cash(10), ?cash(10)),
Context1 = ?ctx_invoice_payment(?cash(10), ?cash(0)), Context1 = ?ctx_invoice_payment(?cash(10), ?cash(0)),
Change = ?LIMIT_CHANGE(ID),
Timestamp = lim_time:to_rfc3339(lim_time:now()), {ok, {vector, _}} = lim_client:hold(Change, Context0, ?config(client, C)),
LimitChangeID = <<Timestamp/binary, "Rollback">>, {ok, {vector, _}} = lim_client:commit(Change, Context1, ?config(client, C)).
Change = #limiter_LimitChange{
id = ID,
change_id = LimitChangeID
},
{ok, {vector, _}} = lim_client:hold(Change, Context0, Client),
{ok, {vector, _}} = lim_client:commit(Change, Context1, Client).
-spec refund_ok(config()) -> _. -spec refund_ok(config()) -> _.
refund_ok(C) -> refund_ok(C) ->
ID = lim_time:to_rfc3339(lim_time:now()), ID = ?config(id, C),
Client = ?config(client, C),
OwnerID = <<"WWWcool Ltd">>, OwnerID = <<"WWWcool Ltd">>,
ShopID = <<"shop">>, ShopID = <<"shop">>,
#{client := Client} = _LimitConfig = prepare_environment(ID, <<"ShopDayTurnover">>, C), _ = prepare_environment(<<"ShopDayTurnover">>, C),
Context0 = ?ctx_invoice_payment(OwnerID, ShopID, ?cash(15), ?cash(15)), Context0 = ?ctx_invoice_payment(OwnerID, ShopID, ?cash(15), ?cash(15)),
RefundContext1 = ?ctx_invoice_payment_refund(OwnerID, ShopID, ?cash(10), ?cash(10), ?cash(10)), RefundContext1 = ?ctx_invoice_payment_refund(OwnerID, ShopID, ?cash(10), ?cash(10), ?cash(10)),
Timestamp = lim_time:to_rfc3339(lim_time:now()), {ok, {vector, _}} = hold_and_commit(?LIMIT_CHANGE(ID, <<"Payment">>), Context0, Client),
LimitChangeID = <<Timestamp/binary, "Payment">>, {ok, {vector, _}} = hold_and_commit(?LIMIT_CHANGE(ID, <<"Refund">>), RefundContext1, Client),
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, #limiter_Limit{} = Limit2} = lim_client:get(ID, RefundContext1, Client), {ok, #limiter_Limit{} = Limit2} = lim_client:get(ID, RefundContext1, Client),
?assertEqual(Limit2#limiter_Limit.amount, 5). ?assertEqual(Limit2#limiter_Limit.amount, 5).
-spec get_config_ok(config()) -> _. -spec get_config_ok(config()) -> _.
get_config_ok(C) -> get_config_ok(C) ->
ID = <<"ID">>, _ = prepare_environment(<<"GlobalMonthTurnover">>, C),
#{client := Client} = prepare_environment(ID, <<"GlobalMonthTurnover">>, C), {ok, #limiter_config_LimitConfig{}} = lim_client:get_config(?config(id, C), ?config(client, C)).
{ok, #limiter_config_LimitConfig{}} = lim_client:get_config(ID, Client).
-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) -> hold_and_commit(Change, Context, Client) ->
{ok, {vector, _}} = lim_client:hold(Change, Context, Client), {ok, {vector, _}} = lim_client:hold(Change, Context, Client),
{ok, {vector, _}} = lim_client:commit(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) -> mock_exchange(Rational, C) ->
lim_mock:mock_services([{xrates, fun('GetConvertedAmount', _) -> {ok, Rational} end}], C). lim_mock:mock_services([{xrates, fun('GetConvertedAmount', _) -> {ok, Rational} end}], C).
prepare_environment(ID, LimitName, _C) -> prepare_environment(LimitName, C) ->
Client = lim_client:new(), ID = ?config(id, C),
Params = #limiter_cfg_LimitCreateParams{ Params = #limiter_cfg_LimitCreateParams{
id = ID, id = ID,
name = LimitName, name = LimitName,
@ -337,5 +433,7 @@ prepare_environment(ID, LimitName, _C) ->
invoice_payment_refund = {subtraction, #limiter_config_Subtraction{}} invoice_payment_refund = {subtraction, #limiter_config_Subtraction{}}
} }
}, },
{ok, LimitConfig} = lim_client:legacy_create_config(Params, Client), {ok, _LimitConfig} = lim_client:legacy_create_config(Params, ?config(client, C)).
#{config => LimitConfig, client => Client}.
gen_unique_id(Prefix) ->
genlib:format("~s/~B", [Prefix, lim_time:now()]).

View File

@ -8,6 +8,7 @@
ruleset => erl_files, ruleset => erl_files,
rules => [ rules => [
{elvis_text_style, line_length, #{limit => 120}}, {elvis_text_style, line_length, #{limit => 120}},
{elvis_style, god_modules, #{limit => 30}},
{elvis_style, nesting_level, #{level => 3}}, {elvis_style, nesting_level, #{level => 3}},
{elvis_style, no_if_expression, disable} {elvis_style, no_if_expression, disable}
] ]