mirror of
https://github.com/valitydev/limiter.git
synced 2024-11-06 00:55:22 +00:00
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:
parent
f577d1292a
commit
3b10d1cbf3
@ -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]).
|
||||||
|
@ -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.
|
||||||
|
@ -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) ->
|
||||||
|
@ -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.
|
|
||||||
|
@ -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} ->
|
||||||
|
@ -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
|
||||||
|
@ -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 ->
|
||||||
|
@ -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{}},
|
||||||
|
@ -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) ->
|
||||||
|
@ -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()]).
|
||||||
|
@ -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}
|
||||||
]
|
]
|
||||||
|
Loading…
Reference in New Issue
Block a user