TD-304: Handle amount limit body type (#8)

* Bump to valitydev/limiter-proto@6723e86.
* Introduce _noncurrency_ concept instead of default currency which is kinda misleading.
* Rename `lim_p_transfer` → `lim_posting`.
* (Minor API break) Respond w/ 0 balance on uninitialized limit ranges.
This commit is contained in:
Andrew Mayorov 2022-06-10 12:56:13 +03:00 committed by GitHub
parent 0c6b2afd6b
commit d36b97f44a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 464 additions and 329 deletions

View File

@ -9,9 +9,10 @@
-export([rollback/3]).
-export([get_plan/2]).
-export([get_balance/2]).
-export([get_default_currency/0]).
-export([create_account/2]).
-export([noncurrency/0]).
-type currency() :: dmsl_domain_thrift:'CurrencySymbolicCode'().
-type amount() :: dmsl_domain_thrift:'Amount'().
-type plan_id() :: dmsl_accounter_thrift:'PlanID'().
@ -40,7 +41,7 @@
-export_type([batch_id/0]).
-export_type([invalid_request_error/0]).
-define(DEFAULT_CURRENCY, <<"RUB">>).
-define(NONCURRENCY, <<>>).
-spec plan(plan_id(), [batch()], lim_context()) -> ok | {error, invalid_request_error()}.
plan(_PlanID, [], _LimitContext) ->
@ -133,9 +134,9 @@ construct_balance(
currency => Currency
}.
-spec get_default_currency() -> currency().
get_default_currency() ->
?DEFAULT_CURRENCY.
-spec noncurrency() -> currency().
noncurrency() ->
?NONCURRENCY.
-spec create_account(currency(), lim_context()) -> {ok, account_id()}.
create_account(CurrencyCode, LimitContext) ->
@ -157,7 +158,7 @@ construct_prototype(CurrencyCode, Description) ->
%%
call_accounter(Function, Args, LimitContext) ->
{ok, WoodyContext} = lim_context:woody_context(LimitContext),
WoodyContext = lim_context:woody_context(LimitContext),
lim_client_woody:call(accounter, Function, Args, WoodyContext).
convert_exception(#'InvalidRequest'{errors = Errors}) ->

View File

@ -1,9 +1,6 @@
-module(lim_body).
-export([get_body/3]).
-export([create_body_from_cash/2]).
-type t() :: {amount, amount()} | {cash, cash()}.
-export([get/3]).
-type amount() :: integer().
-type cash() :: #{
@ -14,32 +11,25 @@
-type currency() :: lim_base_thrift:'CurrencySymbolicCode'().
-type config() :: lim_config_machine:config().
-type body_type() :: full | partial.
-type get_body_error() :: notfound | lim_rates:conversion_error().
-export_type([t/0]).
-export_type([amount/0]).
-export_type([currency/0]).
-export_type([cash/0]).
-export_type([get_body_error/0]).
-spec get_body(body_type(), config(), lim_context:t()) -> {ok, t()} | {error, get_body_error()}.
get_body(BodyType, Config = #{body_type := {cash, ConfigCurrency}}, LimitContext) ->
ContextType = lim_config_machine:context_type(Config),
{ok, Operation} = lim_context:get_operation(ContextType, LimitContext),
case get_body_for_operation(BodyType, Operation, Config, LimitContext) of
{ok, {cash, #{currency := ConfigCurrency}}} = Result ->
Result;
{ok, {cash, #{amount := Amount, currency := Currency}}} ->
case lim_rates:get_converted_amount({Amount, Currency}, Config, LimitContext) of
{ok, ConvertedAmount} ->
{ok, create_body_from_cash(ConvertedAmount, ConfigCurrency)};
Error ->
Error
end;
Error ->
Error
end.
-import(lim_pipeline, [do/1, unwrap/1]).
-spec get(body_type(), config(), lim_context:t()) ->
{ok, cash()} | {error, notfound}.
get(BodyType, Config, LimitContext) ->
do(fun() ->
ContextType = lim_config_machine:context_type(Config),
{ok, Operation} = lim_context:get_operation(ContextType, LimitContext),
Body = unwrap(get_body_for_operation(BodyType, Operation, Config, LimitContext)),
apply_op_behaviour(Body, Config, LimitContext)
end).
-spec get_body_for_operation(body_type(), lim_context:context_operation(), config(), lim_context:t()) ->
{ok, t()} | {error, notfound}.
{ok, cash()} | {error, notfound}.
get_body_for_operation(full, invoice = Operation, Config, LimitContext) ->
ContextType = lim_config_machine:context_type(Config),
lim_context:get_from_context(ContextType, cost, Operation, LimitContext);
@ -73,6 +63,18 @@ get_body_for_operation(partial, invoice_payment_refund = Operation, Config, Limi
get_body_for_operation(partial, invoice_payment_chargeback, _Config, _LimitContext) ->
{error, notfound}.
-spec create_body_from_cash(amount(), currency()) -> t().
create_body_from_cash(Amount, Currency) ->
{cash, #{amount => Amount, currency => Currency}}.
apply_op_behaviour(Body, #{op_behaviour := ComputationConfig}, LimitContext) ->
{ok, Operation} = lim_context:get_operation(payment_processing, LimitContext),
case maps:get(Operation, ComputationConfig, undefined) of
addition ->
Body;
subtraction ->
invert_body(Body);
undefined ->
Body
end;
apply_op_behaviour(Body, _Config, _LimitContext) ->
Body.
invert_body(Cash = #{amount := Amount}) ->
Cash#{amount := -Amount}.

View File

@ -5,7 +5,6 @@
-export([marshal/2]).
-export([unmarshal/2]).
-export([marshal_config/1]).
-export([unmarshal_body_type/1]).
-export([unmarshal_op_behaviour/1]).
-export([unmarshal_params/1]).
-export([maybe_apply/2]).
@ -20,12 +19,18 @@
-type decoded_value() :: decoded_value(any()).
-type decoded_value(T) :: T.
-spec maybe_apply(any(), function()) -> any().
-spec maybe_apply(T, fun((T) -> U)) -> U | undefined.
maybe_apply(undefined, _) ->
undefined;
maybe_apply(Value, Fun) ->
Fun(Value).
-spec maybe_apply(T, fun((T) -> U), Default) -> U | Default.
maybe_apply(undefined, _, Default) ->
Default;
maybe_apply(Value, Fun, _Default) ->
Fun(Value).
%% API
-spec marshal(type_name(), decoded_value()) -> encoded_value().
@ -56,7 +61,6 @@ marshal_config(Config) ->
id = lim_config_machine:id(Config),
processor_type = lim_config_machine:processor_type(Config),
description = lim_config_machine:description(Config),
body_type = marshal_body_type(lim_config_machine:body_type(Config)),
created_at = lim_config_machine:created_at(Config),
started_at = lim_config_machine:started_at(Config),
shard_size = lim_config_machine:shard_size(Config),
@ -78,11 +82,6 @@ marshal_behaviour(subtraction) ->
marshal_behaviour(addition) ->
{addition, #limiter_config_Addition{}}.
marshal_body_type(amount) ->
{amount, #limiter_config_LimitBodyTypeAmount{}};
marshal_body_type({cash, Currency}) ->
{cash, #limiter_config_LimitBodyTypeCash{currency = Currency}}.
marshal_time_range_type({calendar, CalendarType}) ->
{calendar, marshal_calendar_time_range_type(CalendarType)};
marshal_time_range_type({interval, Amount}) ->
@ -100,8 +99,15 @@ marshal_calendar_time_range_type(year) ->
marshal_context_type(payment_processing) ->
{payment_processing, #limiter_config_LimitContextTypePaymentProcessing{}}.
marshal_type(turnover) ->
{turnover, #limiter_config_LimitTypeTurnover{}}.
marshal_type({turnover, Metric}) ->
{turnover, #limiter_config_LimitTypeTurnover{
metric = marshal_turnover_metric(Metric)
}}.
marshal_turnover_metric(number) ->
{number, #limiter_config_LimitTurnoverNumber{}};
marshal_turnover_metric({amount, Currency}) ->
{amount, #limiter_config_LimitTurnoverAmount{currency = Currency}}.
marshal_scope(Types) ->
{multi, ordsets:from_list(lists:map(fun marshal_scope_type/1, ordsets:to_list(Types)))}.
@ -145,7 +151,6 @@ unmarshal_timestamp(Timestamp) when is_binary(Timestamp) ->
unmarshal_params(#limiter_config_LimitConfigParams{
id = ID,
description = Description,
body_type = BodyType,
started_at = StartedAt,
shard_size = ShardSize,
time_range_type = TimeRangeType,
@ -156,7 +161,6 @@ unmarshal_params(#limiter_config_LimitConfigParams{
}) ->
genlib_map:compact(#{
id => ID,
body_type => unmarshal_body_type(BodyType),
started_at => StartedAt,
shard_size => ShardSize,
time_range_type => unmarshal_time_range_type(TimeRangeType),
@ -174,31 +178,43 @@ unmarshal_config(#limiter_config_LimitConfig{
id = ID,
processor_type = ProcessorType,
description = Description,
body_type = BodyType,
created_at = CreatedAt,
started_at = StartedAt,
shard_size = ShardSize,
time_range_type = TimeRangeType,
context_type = ContextType,
type = Type,
type = TypeIn,
scope = Scope,
op_behaviour = OpBehaviour
op_behaviour = OpBehaviour,
body_type_deprecated = BodyTypeIn
}) ->
Type = maybe_apply(TypeIn, fun unmarshal_type/1),
BodyType = maybe_apply(BodyTypeIn, fun unmarshal_body_type_deprecated/1),
genlib_map:compact(#{
id => ID,
processor_type => ProcessorType,
created_at => lim_time:from_rfc3339(CreatedAt),
body_type => unmarshal_body_type(BodyType),
started_at => StartedAt,
shard_size => ShardSize,
time_range_type => unmarshal_time_range_type(TimeRangeType),
context_type => unmarshal_context_type(ContextType),
type => maybe_apply(Type, fun unmarshal_type/1),
type => derive_type(Type, BodyType),
scope => maybe_apply(Scope, fun unmarshal_scope/1),
description => Description,
op_behaviour => maybe_apply(OpBehaviour, fun unmarshal_op_behaviour/1)
}).
derive_type(Type, undefined) ->
% NOTE
% Current protocol disallows configuring (deprecated) body type, thus we trust limit type.
Type;
derive_type({turnover, _}, {cash, Currency}) ->
% NOTE
% Treating limits with configured (deprecated) body type as turnover limits with amount metric.
{turnover, {amount, Currency}};
derive_type(undefined, {cash, Currency}) ->
{turnover, {amount, Currency}}.
-spec unmarshal_op_behaviour(encoded_value()) -> decoded_value().
unmarshal_op_behaviour(OpBehaviour) ->
#limiter_config_OperationLimitBehaviour{
@ -213,10 +229,8 @@ unmarshal_behaviour({subtraction, #limiter_config_Subtraction{}}) ->
unmarshal_behaviour({addition, #limiter_config_Addition{}}) ->
addition.
-spec unmarshal_body_type(encoded_value()) -> decoded_value().
unmarshal_body_type({amount, #limiter_config_LimitBodyTypeAmount{}}) ->
amount;
unmarshal_body_type({cash, #limiter_config_LimitBodyTypeCash{currency = Currency}}) ->
-spec unmarshal_body_type_deprecated(encoded_value()) -> decoded_value().
unmarshal_body_type_deprecated({cash, #limiter_config_LimitBodyTypeCash{currency = Currency}}) ->
{cash, Currency}.
unmarshal_time_range_type({calendar, CalendarType}) ->
@ -236,8 +250,13 @@ unmarshal_calendar_time_range_type({year, _}) ->
unmarshal_context_type({payment_processing, #limiter_config_LimitContextTypePaymentProcessing{}}) ->
payment_processing.
unmarshal_type({turnover, #limiter_config_LimitTypeTurnover{}}) ->
turnover.
unmarshal_type({turnover, #limiter_config_LimitTypeTurnover{metric = Metric}}) ->
{turnover, maybe_apply(Metric, fun unmarshal_turnover_metric/1, number)}.
unmarshal_turnover_metric({number, _}) ->
number;
unmarshal_turnover_metric({amount, #limiter_config_LimitTurnoverAmount{currency = Currency}}) ->
{amount, Currency}.
unmarshal_scope({single, Type}) ->
ordsets:from_list([unmarshal_scope_type(Type)]);
@ -263,23 +282,65 @@ unmarshal_scope_type({payment_tool, _}) ->
-spec test() -> _.
-spec marshal_unmarshal_created_test() -> _.
marshal_unmarshal_created_test() ->
Created =
{created, #{
id => <<"id">>,
processor_type => <<"type">>,
created_at => lim_time:now(),
body_type => {cash, <<"RUB">>},
started_at => <<"2000-01-01T00:00:00Z">>,
shard_size => 7,
time_range_type => {calendar, day},
context_type => payment_processing,
type => turnover,
type => {turnover, number},
scope => ordsets:from_list([party, shop]),
description => <<"description">>
}},
Event = {ev, lim_time:machinery_now(), Created},
?assertEqual(Event, unmarshal(timestamped_change, marshal(timestamped_change, Event))).
-spec unmarshal_created_w_deprecated_body_type_test_() -> [_TestGen].
unmarshal_created_w_deprecated_body_type_test_() ->
Now = lim_time:now(),
Config = #limiter_config_LimitConfig{
id = <<"ID">>,
processor_type = <<"TurnoverProcessor">>,
created_at = lim_time:to_rfc3339(Now),
started_at = <<"2000-01-01T00:00:00Z">>,
shard_size = 42,
time_range_type = {calendar, {day, #time_range_TimeRangeTypeCalendarDay{}}},
context_type = {payment_processing, #limiter_config_LimitContextTypePaymentProcessing{}},
body_type_deprecated = {cash, #limiter_config_LimitBodyTypeCash{currency = <<"☭☭☭"/utf8>>}}
},
[
?_assertMatch(
{created, #{
id := <<"ID">>,
created_at := Now,
type := {turnover, {amount, <<"☭☭☭"/utf8>>}}
}},
unmarshal_change(
{created, #limiter_config_CreatedChange{
limit_config = Config#limiter_config_LimitConfig{
type = undefined
}
}}
)
),
?_assertMatch(
{created, #{
id := <<"ID">>,
created_at := Now,
type := {turnover, {amount, <<"☭☭☭"/utf8>>}}
}},
unmarshal_change(
{created, #limiter_config_CreatedChange{
limit_config = Config#limiter_config_LimitConfig{
type = {turnover, #limiter_config_LimitTypeTurnover{}}
}
}}
)
)
].
-endif.

View File

@ -7,8 +7,6 @@
-export([created_at/1]).
-export([id/1]).
-export([description/1]).
-export([body_type/1]).
-export([currency/1]).
-export([started_at/1]).
-export([shard_size/1]).
-export([time_range_type/1]).
@ -38,10 +36,9 @@
-type processor() :: lim_router:processor().
-type description() :: binary().
-type limit_type() :: turnover.
-type limit_type() :: {turnover, lim_turnover_metric:t()}.
-type limit_scope() :: ordsets:ordset(limit_scope_type()).
-type limit_scope_type() :: party | shop | wallet | identity.
-type body_type() :: {cash, currency()} | amount.
-type shard_size() :: pos_integer().
-type shard_id() :: binary().
-type prefix() :: binary().
@ -57,12 +54,11 @@
id := lim_id(),
processor_type := processor_type(),
created_at := lim_time:timestamp_ms(),
body_type := body_type(),
started_at := timestamp(),
shard_size := shard_size(),
time_range_type := time_range_type(),
context_type := context_type(),
type => limit_type(),
type := limit_type(),
scope => limit_scope(),
description => description(),
op_behaviour => op_behaviour()
@ -70,7 +66,6 @@
-type create_params() :: #{
processor_type := processor_type(),
body_type := body_type(),
started_at := timestamp(),
shard_size := shard_size(),
time_range_type := time_range_type(),
@ -88,16 +83,13 @@
-type lim_change() :: lim_limiter_thrift:'LimitChange'().
-type limit() :: lim_limiter_thrift:'Limit'().
-type timestamp() :: lim_base_thrift:'Timestamp'().
-type currency() :: lim_base_thrift:'CurrencySymbolicCode'().
-export_type([config/0]).
-export_type([body_type/0]).
-export_type([limit_type/0]).
-export_type([limit_scope/0]).
-export_type([time_range_type/0]).
-export_type([time_range/0]).
-export_type([create_params/0]).
-export_type([currency/0]).
-export_type([lim_id/0]).
-export_type([lim_change/0]).
-export_type([limit/0]).
@ -180,16 +172,6 @@ description(#{description := ID}) ->
description(_) ->
undefined.
-spec body_type(config()) -> body_type().
body_type(#{body_type := BodyType}) ->
BodyType.
-spec currency(config()) -> currency() | undefined.
currency(#{body_type := {cash, Currency}}) ->
Currency;
currency(#{body_type := amount}) ->
undefined.
-spec started_at(config()) -> timestamp().
started_at(#{started_at := Value}) ->
Value.
@ -206,11 +188,11 @@ time_range_type(#{time_range_type := Value}) ->
processor_type(#{processor_type := Value}) ->
Value.
-spec type(config()) -> lim_maybe:maybe(limit_type()).
-spec type(config()) -> limit_type().
type(#{type := Value}) ->
Value;
type(_) ->
undefined.
{turnover, number}.
-spec scope(config()) -> limit_scope().
scope(#{scope := Value}) ->
@ -232,7 +214,7 @@ op_behaviour(_) ->
-spec start(lim_id(), create_params(), lim_context()) -> {ok, config()}.
start(ID, Params, LimitContext) ->
{ok, WoodyCtx} = lim_context:woody_context(LimitContext),
WoodyCtx = lim_context:woody_context(LimitContext),
Config = genlib_map:compact(Params#{id => ID, created_at => lim_time:now()}),
case machinery:start(?NS, ID, [{created, Config}], get_backend(WoodyCtx)) of
ok ->
@ -245,7 +227,7 @@ start(ID, Params, LimitContext) ->
-spec get(lim_id(), lim_context()) -> {ok, config()} | {error, notfound}.
get(ID, LimitContext) ->
do(fun() ->
{ok, WoodyCtx} = lim_context:woody_context(LimitContext),
WoodyCtx = lim_context:woody_context(LimitContext),
Machine = unwrap(machinery:get(?NS, ID, get_backend(WoodyCtx))),
collapse(Machine)
end).

View File

@ -96,12 +96,11 @@ marshal_unmarshal_created_test() ->
id => ID,
processor_type => <<"type">>,
created_at => lim_time:now(),
body_type => {cash, <<"RUB">>},
started_at => <<"2000-01-01T00:00:00Z">>,
shard_size => 7,
time_range_type => {calendar, day},
context_type => payment_processing,
type => turnover,
type => {turnover, {amount, <<"RUB">>}},
scope => ordsets:from_list([party]),
description => <<"description">>
}},

View File

@ -8,21 +8,19 @@
-export([handle_function/4]).
%%
-type lim_context() :: lim_context:t().
-define(DEFAULT_CURRENCY, <<"RUB">>).
%%
-spec handle_function(woody:func(), woody:args(), woody_context:ctx(), woody:options()) -> {ok, woody:result()}.
handle_function(Fn, Args, WoodyCtx, Opts) ->
{ok, LimitContext} = lim_context:create(WoodyCtx),
LimitContext = lim_context:create(WoodyCtx),
scoper:scope(
configurator,
fun() -> handle_function_(Fn, Args, LimitContext, Opts) end
).
-spec handle_function_(woody:func(), woody:args(), lim_context(), woody:options()) -> {ok, woody:result()}.
-spec handle_function_(woody:func(), woody:args(), lim_context:t(), woody:options()) -> {ok, woody:result()}.
handle_function_(
'CreateLegacy',
{#limiter_cfg_LimitCreateParams{
@ -30,7 +28,6 @@ handle_function_(
name = Name,
description = Description,
started_at = StartedAt,
body_type = BodyType,
op_behaviour = OpBehaviour
}},
LimitContext,
@ -43,7 +40,6 @@ handle_function_(
genlib_map:compact(Config#{
description => Description,
started_at => StartedAt,
body_type => lim_config_codec:unmarshal_body_type(BodyType),
op_behaviour => lim_config_codec:maybe_apply(
OpBehaviour,
fun lim_config_codec:unmarshal_op_behaviour/1
@ -77,7 +73,7 @@ handle_function_('Get', {LimitID}, LimitContext, _Opts) ->
woody_error:raise(business, #limiter_cfg_LimitConfigNotFound{})
end.
map_type(turnover) ->
map_type({turnover, _}) ->
<<"TurnoverProcessor">>;
map_type(_) ->
woody_error:raise(
@ -88,7 +84,7 @@ map_type(_) ->
mk_limit_config(<<"ShopDayTurnover">>) ->
{ok, #{
processor_type => <<"TurnoverProcessor">>,
type => turnover,
type => {turnover, {amount, ?DEFAULT_CURRENCY}},
scope => ordsets:from_list([shop]),
shard_size => 7,
context_type => payment_processing,
@ -97,7 +93,7 @@ mk_limit_config(<<"ShopDayTurnover">>) ->
mk_limit_config(<<"PartyDayTurnover">>) ->
{ok, #{
processor_type => <<"TurnoverProcessor">>,
type => turnover,
type => {turnover, {amount, ?DEFAULT_CURRENCY}},
scope => ordsets:from_list([party]),
shard_size => 7,
context_type => payment_processing,
@ -106,7 +102,7 @@ mk_limit_config(<<"PartyDayTurnover">>) ->
mk_limit_config(<<"ShopMonthTurnover">>) ->
{ok, #{
processor_type => <<"TurnoverProcessor">>,
type => turnover,
type => {turnover, {amount, ?DEFAULT_CURRENCY}},
scope => ordsets:from_list([shop]),
shard_size => 12,
context_type => payment_processing,
@ -115,7 +111,7 @@ mk_limit_config(<<"ShopMonthTurnover">>) ->
mk_limit_config(<<"PartyMonthTurnover">>) ->
{ok, #{
processor_type => <<"TurnoverProcessor">>,
type => turnover,
type => {turnover, {amount, ?DEFAULT_CURRENCY}},
scope => ordsets:from_list([party]),
shard_size => 12,
context_type => payment_processing,
@ -124,7 +120,7 @@ mk_limit_config(<<"PartyMonthTurnover">>) ->
mk_limit_config(<<"GlobalMonthTurnover">>) ->
{ok, #{
processor_type => <<"TurnoverProcessor">>,
type => turnover,
type => {turnover, {amount, ?DEFAULT_CURRENCY}},
scope => ordsets:new(),
shard_size => 12,
context_type => payment_processing,

View File

@ -96,13 +96,13 @@
-export_type([context_type/0]).
-export_type([context_operation/0]).
-spec create(woody_context()) -> {ok, t()}.
-spec create(woody_context()) -> t().
create(WoodyContext) ->
{ok, #{woody_context => WoodyContext}}.
#{woody_context => WoodyContext}.
-spec woody_context(t()) -> {ok, woody_context()}.
-spec woody_context(t()) -> woody_context().
woody_context(Context) ->
{ok, maps:get(woody_context, Context)}.
maps:get(woody_context, Context).
-spec clock(t()) -> {ok, clock()} | {error, notfound}.
clock(#{clock := Clock}) ->
@ -294,7 +294,7 @@ unmarshal_payment_processing_invoice_payment_chargeback(#limiter_context_Invoice
}).
unmarshal_cash(#limiter_base_Cash{amount = Amount, currency = #limiter_base_CurrencyRef{symbolic_code = Currency}}) ->
lim_body:create_body_from_cash(Amount, Currency).
#{amount => Amount, currency => Currency}.
unmarshal_string(Value) ->
Value.

View File

@ -18,7 +18,7 @@
-spec handle_function(woody:func(), woody:args(), woody_context:ctx(), woody:options()) -> {ok, woody:result()}.
handle_function(Fn, Args, WoodyCtx, Opts) ->
{ok, LimitContext} = lim_context:create(WoodyCtx),
LimitContext = lim_context:create(WoodyCtx),
scoper:scope(
limiter,
fun() -> handle_function_(Fn, Args, LimitContext, Opts) end
@ -79,8 +79,6 @@ handle_function_('Rollback', {LimitChange = ?LIMIT_CHANGE(LimitID), Clock, Conte
end.
-spec handle_get_error(_) -> no_return().
handle_get_error({_, {range, notfound}}) ->
woody_error:raise(business, #limiter_LimitNotFound{});
handle_get_error(Error) ->
handle_default_error(Error).

View File

@ -1,34 +0,0 @@
-module(lim_p_transfer).
-include_lib("damsel/include/dmsl_accounter_thrift.hrl").
-export([construct_posting/2]).
-export([reverse_posting/1]).
-type posting() :: lim_accounting:posting().
-type body() :: lim_body:t().
-spec construct_posting(lim_range_machine:time_range_ext(), body()) -> posting().
construct_posting(#{account_id_from := From, account_id_to := To}, {cash, #{amount := Amount, currency := Currency}}) ->
#accounter_Posting{
from_id = From,
to_id = To,
amount = Amount,
currency_sym_code = Currency,
description = <<>>
};
construct_posting(#{account_id_from := From, account_id_to := To}, {amount, Amount}) ->
#accounter_Posting{
from_id = From,
to_id = To,
amount = Amount,
currency_sym_code = lim_accounting:get_default_currency(),
description = <<>>
}.
-spec reverse_posting(posting()) -> posting().
reverse_posting(Posting = #accounter_Posting{from_id = AccountFrom, to_id = AccountTo}) ->
Posting#accounter_Posting{
from_id = AccountTo,
to_id = AccountFrom
}.

View File

@ -0,0 +1,32 @@
-module(lim_posting).
-include_lib("damsel/include/dmsl_accounter_thrift.hrl").
-export([new/3]).
-export([reverse/1]).
-type posting() :: lim_accounting:posting().
-spec new(lim_range_machine:time_range_ext(), lim_body:amount(), lim_body:currency()) ->
posting().
new(#{account_id_from := From, account_id_to := To}, Amount, Currency) ->
reverse_negative_posting(#accounter_Posting{
from_id = From,
to_id = To,
amount = Amount,
currency_sym_code = Currency,
description = <<>>
}).
reverse_negative_posting(Posting = #accounter_Posting{amount = Amount}) when Amount < 0 ->
PostingReversed = reverse(Posting),
PostingReversed#accounter_Posting{amount = -Amount};
reverse_negative_posting(Posting) ->
Posting.
-spec reverse(posting()) -> posting().
reverse(Posting = #accounter_Posting{from_id = AccountFrom, to_id = AccountTo}) ->
Posting#accounter_Posting{
from_id = AccountTo,
to_id = AccountFrom
}.

View File

@ -39,7 +39,7 @@
-type id() :: binary().
-type time_range_type() :: lim_config_machine:time_range_type().
-type time_range() :: lim_config_machine:time_range().
-type currency() :: lim_config_machine:currency().
-type currency() :: lim_body:currency().
-type limit_range_state() :: #{
id := id(),
@ -109,27 +109,27 @@ ranges(#{ranges := Ranges}) ->
ranges(_State) ->
[].
-spec currency(limit_range_state()) -> currency().
-spec currency(limit_range_state() | create_params()) -> currency().
currency(#{currency := Currency}) ->
Currency;
currency(_State) ->
lim_accounting:get_default_currency().
lim_accounting:noncurrency().
%%% API
-spec get(id(), lim_context()) -> {ok, limit_range_state()} | {error, notfound}.
get(ID, LimitContext) ->
{ok, WoodyCtx} = lim_context:woody_context(LimitContext),
get_state(ID, WoodyCtx).
get_state(ID, lim_context:woody_context(LimitContext)).
-spec ensure_exists(create_params(), time_range(), lim_context()) -> {ok, time_range_ext()}.
ensure_exists(Params = #{id := ID, currency := Currency}, TimeRange, LimitContext) ->
{ok, WoodyCtx} = lim_context:woody_context(LimitContext),
ensure_exists(Params = #{id := ID}, TimeRange, LimitContext) ->
WoodyCtx = lim_context:woody_context(LimitContext),
case get_state(ID, WoodyCtx) of
{ok, State} ->
ensure_range_exist_in_state(TimeRange, State, WoodyCtx);
{error, notfound} ->
_ = start(ID, Params, [new_time_range_ext(TimeRange, Currency, WoodyCtx)], WoodyCtx),
TimeRangeExt = new_time_range_ext(TimeRange, currency(Params), WoodyCtx),
_ = start(ID, Params, [TimeRangeExt], WoodyCtx),
{ok, State} = get_state(ID, WoodyCtx),
get_range(TimeRange, State)
end.
@ -162,9 +162,7 @@ get_range_balance(ID, TimeRange, LimitContext) ->
-spec init(args([event()]), machine(), handler_args(), handler_opts()) -> result().
init(Events, _Machine, _HandlerArgs, _HandlerOpts) ->
#{
events => emit_events(Events)
}.
#{events => emit_events(Events)}.
-spec process_call(args(range_call()), machine(), handler_args(), handler_opts()) ->
{response(time_range_ext()), result()} | no_return().
@ -196,7 +194,7 @@ find_time_range(TimeRange, [_Head | Rest]) ->
find_time_range(TimeRange, Rest).
new_time_range_ext(TimeRange, Currency, WoodyCtx) ->
{ok, LimitContext} = lim_context:create(WoodyCtx),
LimitContext = lim_context:create(WoodyCtx),
{ok, AccountIDFrom} = lim_accounting:create_account(Currency, LimitContext),
{ok, AccountIDTo} = lim_accounting:create_account(Currency, LimitContext),
TimeRange#{

View File

@ -2,10 +2,8 @@
-include_lib("xrates_proto/include/xrates_rate_thrift.hrl").
-export([get_converted_amount/3]).
-export([convert/4]).
-type amount() :: dmsl_domain_thrift:'Amount'().
-type currency() :: dmsl_domain_thrift:'CurrencySymbolicCode'().
-type limit_context() :: lim_context:t().
-type config() :: lim_config_machine:config().
@ -17,37 +15,32 @@
-define(DEFAULT_FACTOR, 1.1).
-define(DEFAULT_FACTOR_NAME, <<"DEFAULT">>).
-spec get_converted_amount({amount(), currency()}, config(), limit_context()) ->
{ok, amount()}
-spec convert(lim_body:cash(), lim_body:currency(), config(), limit_context()) ->
{ok, lim_body:cash()}
| {error, conversion_error()}.
get_converted_amount(Cash = {_Amount, Currency}, Config, LimitContext) ->
Factor = get_exchange_factor(Currency),
case
call_rates(
'GetConvertedAmount',
{<<"CBR">>, construct_conversion_request(Cash, Config, LimitContext)},
LimitContext
)
of
convert(#{amount := Amount, currency := Currency}, DestinationCurrency, Config, LimitContext) ->
ContextType = lim_config_machine:context_type(Config),
{ok, Timestamp} = lim_context:get_from_context(ContextType, created_at, LimitContext),
Request = #rate_ConversionRequest{
source = Currency,
destination = DestinationCurrency,
amount = Amount,
datetime = Timestamp
},
case call_rates('GetConvertedAmount', {<<"CBR">>, Request}, LimitContext) of
{ok, #base_Rational{p = P, q = Q}} ->
Rational = genlib_rational:new(P, Q),
{ok, genlib_rational:round(genlib_rational:mul(Rational, Factor))};
Factor = get_exchange_factor(Currency),
{ok, #{
amount => genlib_rational:round(genlib_rational:mul(Rational, Factor)),
currency => DestinationCurrency
}};
{exception, #rate_QuoteNotFound{}} ->
{error, quote_not_found};
{exception, #rate_CurrencyNotFound{}} ->
{error, currency_not_found}
end.
construct_conversion_request({Amount, Currency}, Config = #{body_type := {cash, DestinationCurrency}}, LimitContext) ->
ContextType = lim_config_machine:context_type(Config),
{ok, Timestamp} = lim_context:get_from_context(ContextType, created_at, LimitContext),
#rate_ConversionRequest{
source = Currency,
destination = DestinationCurrency,
amount = Amount,
datetime = Timestamp
}.
get_exchange_factor(Currency) ->
Factors = genlib_app:env(?APP, exchange_factors, #{}),
case maps:get(Currency, Factors, undefined) of
@ -65,5 +58,4 @@ get_exchange_factor(Currency) ->
%%
call_rates(Function, Args, LimitContext) ->
{ok, WoodyContext} = lim_context:woody_context(LimitContext),
lim_client_woody:call(xrates, Function, Args, WoodyContext).
lim_client_woody:call(xrates, Function, Args, lim_context:woody_context(LimitContext)).

View File

@ -0,0 +1,61 @@
-module(lim_turnover_metric).
-export([compute/4]).
-type amount() :: lim_body:amount().
-type currency() :: lim_base_thrift:'CurrencySymbolicCode'().
-type stage() :: hold | commit.
-type t() :: number | {amount, currency()}.
-export_type([t/0]).
%%
-spec compute(t(), stage(), lim_config_machine:config(), lim_context:t()) ->
{ok, amount()} | {error, lim_rates:conversion_error()}.
compute(number, hold, Config, LimitContext) ->
#{amount := Amount} = get_body(Config, LimitContext),
{ok, sign(Amount)};
compute(number, commit, Config, LimitContext) ->
case get_commit_body(Config, LimitContext) of
#{amount := Amount} when Amount /= 0 ->
{ok, sign(Amount)};
#{amount := 0} ->
% Zero amount operation currently means "rollback" in the protocol.
{ok, 0}
end;
compute({amount, Currency}, hold, Config, LimitContext) ->
Body = get_body(Config, LimitContext),
denominate(Body, Currency, Config, LimitContext);
compute({amount, Currency}, commit, Config, LimitContext) ->
Body = get_commit_body(Config, LimitContext),
denominate(Body, Currency, Config, LimitContext).
get_body(Config, LimitContext) ->
{ok, Body} = lim_body:get(full, Config, LimitContext),
Body.
get_commit_body(Config, LimitContext) ->
case lim_body:get(partial, Config, LimitContext) of
{ok, Body} ->
Body;
{error, notfound} ->
get_body(Config, LimitContext)
end.
%%
denominate(#{amount := Amount, currency := Currency}, Currency, _Config, _LimitContext) ->
{ok, Amount};
denominate(Body = #{}, DestinationCurrency, Config, LimitContext) ->
case lim_rates:convert(Body, DestinationCurrency, Config, LimitContext) of
{ok, #{amount := AmountConverted}} ->
{ok, AmountConverted};
{error, _} = Error ->
Error
end.
sign(Amount) when Amount > 0 ->
+1;
sign(Amount) when Amount < 0 ->
-1.

View File

@ -25,16 +25,16 @@
-type get_limit_error() :: {range, notfound}.
-type hold_error() ::
lim_body:get_body_error()
lim_rates:conversion_error()
| lim_accounting:invalid_request_error().
-type commit_error() ::
{forbidden_operation_amount, forbidden_operation_amount_error()}
| lim_body:get_body_error()
| lim_rates:conversion_error()
| lim_accounting:invalid_request_error().
-type rollback_error() ::
lim_body:get_body_error()
lim_rates:conversion_error()
| lim_accounting:invalid_request_error().
-export_type([get_limit_error/0]).
@ -42,14 +42,13 @@
-export_type([commit_error/0]).
-export_type([rollback_error/0]).
-import(lim_pipeline, [do/1, unwrap/1, unwrap/2]).
-import(lim_pipeline, [do/1, unwrap/1]).
-spec get_limit(lim_id(), config(), lim_context()) -> {ok, limit()} | {error, get_limit_error()}.
get_limit(LimitID, Config, LimitContext) ->
do(fun() ->
{LimitRangeID, TimeRange} = compute_limit_time_range_location(LimitID, Config, LimitContext),
#{max_available_amount := Amount} =
unwrap(range, lim_range_machine:get_range_balance(LimitRangeID, TimeRange, LimitContext)),
Amount = find_range_balance_amount(LimitRangeID, TimeRange, LimitContext),
#limiter_Limit{
id = LimitRangeID,
amount = Amount,
@ -58,12 +57,20 @@ get_limit(LimitID, Config, LimitContext) ->
}
end).
find_range_balance_amount(LimitRangeID, TimeRange, LimitContext) ->
case lim_range_machine:get_range_balance(LimitRangeID, TimeRange, LimitContext) of
{ok, #{max_available_amount := Amount}} ->
Amount;
{error, notfound} ->
0
end.
-spec hold(lim_change(), config(), lim_context()) -> ok | {error, hold_error()}.
hold(LimitChange = #limiter_LimitChange{id = LimitID}, Config, LimitContext) ->
do(fun() ->
TimeRangeAccount = ensure_limit_time_range(LimitID, Config, LimitContext),
Body = unwrap(lim_body:get_body(full, Config, LimitContext)),
Posting = construct_posting(TimeRangeAccount, Body, Config, LimitContext),
Metric = unwrap(compute_metric(hold, Config, LimitContext)),
Posting = lim_posting:new(TimeRangeAccount, Metric, currency(Config)),
unwrap(lim_accounting:hold(construct_plan_id(LimitChange), {1, [Posting]}, LimitContext))
end).
@ -95,8 +102,8 @@ commit(LimitChange = #limiter_LimitChange{id = LimitID}, Config, LimitContext) -
rollback(LimitChange = #limiter_LimitChange{id = LimitID}, Config, LimitContext) ->
do(fun() ->
TimeRangeAccount = ensure_limit_time_range(LimitID, Config, LimitContext),
Body = unwrap(lim_body:get_body(full, Config, LimitContext)),
Posting = construct_posting(TimeRangeAccount, Body, Config, LimitContext),
Metric = unwrap(compute_metric(hold, Config, LimitContext)),
Posting = lim_posting:new(TimeRangeAccount, Metric, currency(Config)),
unwrap(lim_accounting:rollback(construct_plan_id(LimitChange), [{1, [Posting]}], LimitContext))
end).
@ -113,7 +120,7 @@ ensure_limit_time_range(LimitID, Config, LimitContext) ->
id => LimitRangeID,
type => lim_config_machine:time_range_type(Config),
created_at => Timestamp,
currency => lim_config_machine:currency(Config)
currency => currency(Config)
}),
unwrap(lim_range_machine:ensure_exists(CreateParams, TimeRange, LimitContext)).
@ -127,22 +134,24 @@ construct_range_id(LimitID, Timestamp, Config, LimitContext) ->
<<LimitID/binary, Prefix/binary, "/", ShardID/binary>>.
construct_commit_plan(TimeRangeAccount, Config, LimitContext) ->
Body = unwrap(lim_body:get_body(full, Config, LimitContext)),
MaybePartialBody = lim_body:get_body(partial, Config, LimitContext),
construct_commit_postings(TimeRangeAccount, Body, MaybePartialBody, Config, LimitContext).
MetricHold = unwrap(compute_metric(hold, Config, LimitContext)),
MetricCommit = unwrap(compute_metric(commit, Config, LimitContext)),
construct_commit_postings(TimeRangeAccount, MetricHold, MetricCommit, Config).
construct_commit_postings(TimeRangeAccount, Full, MaybePartialBody, Config, LimitContext) ->
OriginalHoldPosting = construct_posting(TimeRangeAccount, Full, Config, LimitContext),
case determine_commit_intent(MaybePartialBody, Full) of
commit ->
construct_commit_postings(TimeRangeAccount, MetricHold, MetricCommit, Config) ->
OriginalHoldPosting = lim_posting:new(TimeRangeAccount, MetricHold, currency(Config)),
case MetricCommit of
MetricHold ->
% Commit-time metric is equal to hold-time metric
[{commit, [{1, [OriginalHoldPosting]}]}];
rollback ->
0 ->
% Commit-time metric is 0, this is rollback
[{rollback, [{1, [OriginalHoldPosting]}]}];
{commit, Partial} ->
_MetricPartial ->
% Partial body is less than full body
ok = unwrap(assert_partial_body(Partial, Full)),
ReverseHoldPosting = lim_p_transfer:reverse_posting(OriginalHoldPosting),
PartialHoldPosting = construct_posting(TimeRangeAccount, Partial, Config, LimitContext),
ok = unwrap(validate_metric(MetricCommit, MetricHold)),
ReverseHoldPosting = lim_posting:reverse(OriginalHoldPosting),
PartialHoldPosting = lim_posting:new(TimeRangeAccount, MetricCommit, currency(Config)),
PartialBatch = [ReverseHoldPosting, PartialHoldPosting],
[
{hold, {2, PartialBatch}},
@ -153,71 +162,36 @@ construct_commit_postings(TimeRangeAccount, Full, MaybePartialBody, Config, Limi
]
end.
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}.
compute_metric(Stage, Config, LimitContext) ->
{turnover, Metric} = lim_config_machine:type(Config),
lim_turnover_metric:compute(Metric, Stage, Config, LimitContext).
construct_posting(TimeRangeAccount, Body, Config, LimitContext) ->
apply_op_behaviour(lim_p_transfer:construct_posting(TimeRangeAccount, Body), Config, LimitContext).
currency(#{type := {turnover, {amount, Currency}}}) ->
Currency;
currency(#{type := {turnover, number}}) ->
lim_accounting:noncurrency().
apply_op_behaviour(Posting, #{op_behaviour := ComputationConfig}, LimitContext) ->
{ok, Operation} = lim_context:get_operation(payment_processing, LimitContext),
case maps:get(Operation, ComputationConfig, undefined) of
subtraction ->
lim_p_transfer:reverse_posting(Posting);
Type when Type =:= undefined orelse Type =:= additional ->
Posting
end;
apply_op_behaviour(Body, _Config, _LimitContext) ->
Body.
assert_partial_body(
{cash, #{amount := Partial, currency := Currency}},
{cash, #{amount := Full, currency := Currency}}
) ->
compare_amount(Partial, Full, Currency);
assert_partial_body(
{cash, #{amount := Partial, currency := PartialCurrency}},
{cash, #{amount := Full, currency := FullCurrency}}
) ->
erlang:error({invalid_partial_cash, {Partial, PartialCurrency}, {Full, FullCurrency}}).
compare_amount(Partial, Full, Currency) when Full > 0 ->
case Partial < Full of
validate_metric(MetricCommit, MetricHold) when MetricHold > 0 ->
case MetricCommit < MetricHold of
true ->
ok;
false ->
{error,
{forbidden_operation_amount,
genlib_map:compact(#{
type => positive,
partial => Partial,
full => Full,
currency => Currency
})}}
{forbidden_operation_amount, #{
type => positive,
partial => MetricCommit,
full => MetricHold
}}}
end;
compare_amount(Partial, Full, Currency) when Full < 0 ->
case Partial > Full of
validate_metric(MetricCommit, MetricHold) when MetricHold < 0 ->
case MetricCommit > MetricHold of
true ->
ok;
false ->
{error,
{forbidden_operation_amount,
genlib_map:compact(#{
type => negative,
partial => Partial,
full => Full,
currency => Currency
})}}
{forbidden_operation_amount, #{
type => negative,
partial => MetricCommit,
full => MetricHold
}}}
end.

View File

@ -85,8 +85,7 @@ legacy_create_config(C) ->
id = ID,
name = <<"GlobalMonthTurnover">>,
description = Description,
started_at = <<"2000-01-01T00:00:00Z">>,
body_type = ?body_type_cash()
started_at = <<"2000-01-01T00:00:00Z">>
},
?assertMatch(
{ok, #limiter_config_LimitConfig{
@ -105,10 +104,9 @@ create_config(C) ->
id = ?config(limit_id, C),
description = Description,
started_at = <<"2000-01-01T00:00:00Z">>,
body_type = ?body_type_cash(<<"RUB">>),
shard_size = 4,
time_range_type = ?time_range_week(),
type = ?lim_type_turnover(),
type = ?lim_type_turnover(?turnover_metric_amount()),
scope = ?scope([
?scope_shop(),
?scope_party()
@ -130,7 +128,6 @@ create_config_single_scope(C) ->
Params = #limiter_config_LimitConfigParams{
id = ?config(limit_id, C),
started_at = <<"2000-01-01T00:00:00Z">>,
body_type = ?body_type_cash(),
time_range_type = ?time_range_week(),
shard_size = 1,
type = ?lim_type_turnover(),
@ -157,8 +154,7 @@ prepare_environment(ID, LimitName, _C) ->
id = ID,
name = LimitName,
description = <<"description">>,
started_at = <<"2000-01-01T00:00:00Z">>,
body_type = ?body_type_cash()
started_at = <<"2000-01-01T00:00:00Z">>
},
{ok, LimitConfig} = lim_client:legacy_create_config(Params, Client),
#{config => LimitConfig, client => Client}.

View File

@ -17,13 +17,15 @@
-define(scope_party(), {party, #limiter_config_LimitScopeEmptyDetails{}}).
-define(scope_shop(), {shop, #limiter_config_LimitScopeEmptyDetails{}}).
-define(body_type_cash(), ?body_type_cash(?currency)).
-define(body_type_cash(Currency),
{cash, #limiter_config_LimitBodyTypeCash{currency = Currency}}
-define(lim_type_turnover(), ?lim_type_turnover(?turnover_metric_number())).
-define(lim_type_turnover(Metric),
{turnover, #limiter_config_LimitTypeTurnover{metric = Metric}}
).
-define(lim_type_turnover(),
{turnover, #limiter_config_LimitTypeTurnover{}}
-define(turnover_metric_number(), {number, #limiter_config_LimitTurnoverNumber{}}).
-define(turnover_metric_amount(), ?turnover_metric_amount(?currency)).
-define(turnover_metric_amount(Currency),
{amount, #limiter_config_LimitTurnoverAmount{currency = Currency}}
).
-define(time_range_day(),
@ -48,8 +50,19 @@
{payment_processing, #limiter_config_LimitContextTypePaymentProcessing{}}
).
-define(op_invoice(), {invoice, #limiter_context_PaymentProcessingOperationInvoice{}}).
-define(op_invoice_payment(), {invoice_payment, #limiter_context_PaymentProcessingOperationInvoicePayment{}}).
-define(ctx_invoice(Cost), #limiter_context_LimitContext{
payment_processing = #limiter_context_ContextPaymentProcessing{
op = ?op_invoice(),
invoice = #limiter_context_Invoice{
created_at = <<"2000-01-01T00:00:00Z">>,
cost = Cost
}
}
}).
-define(ctx_invoice_payment(Cost, CaptureCost), ?ctx_invoice_payment(undefined, undefined, Cost, CaptureCost)).
-define(ctx_invoice_payment(OwnerID, ShopID, Cost, CaptureCost), #limiter_context_LimitContext{

View File

@ -19,6 +19,7 @@
-export([partial_commit_with_exchange/1]).
-export([commit_with_exchange/1]).
-export([get_rate/1]).
-export([get_limit_ok/1]).
-export([get_limit_notfound/1]).
-export([hold_ok/1]).
-export([commit_ok/1]).
@ -34,6 +35,11 @@
-export([partial_commit_processes_idempotently/1]).
-export([rollback_processes_idempotently/1]).
-export([commit_number_ok/1]).
-export([rollback_number_ok/1]).
-export([commit_refund_keep_number_unchanged/1]).
-export([partial_commit_number_counts_as_single_op/1]).
-type group_name() :: atom().
-type test_case_name() :: atom().
@ -45,6 +51,7 @@
all() ->
[
{group, default},
{group, cashless},
{group, idempotency}
].
@ -56,6 +63,7 @@ groups() ->
partial_commit_with_exchange,
commit_with_exchange,
get_rate,
get_limit_ok,
get_limit_notfound,
hold_ok,
commit_ok,
@ -66,6 +74,12 @@ groups() ->
partial_commit_inexistent_hold_fails,
commit_multirange_limit_ok
]},
{cashless, [parallel], [
commit_number_ok,
rollback_number_ok,
commit_refund_keep_number_unchanged,
partial_commit_number_counts_as_single_op
]},
{idempotency, [parallel], [
commit_processes_idempotently,
full_commit_processes_idempotently,
@ -129,8 +143,7 @@ end_per_testcase(_Name, C) ->
commit_with_default_exchange(C) ->
Rational = #base_Rational{p = 1000000, q = 100},
_ = mock_exchange(Rational, C),
_ = configure_limit(?time_range_month(), ?global(), C),
ID = ?config(id, C),
ID = configure_limit(?time_range_month(), ?global(), C),
Context = #limiter_context_LimitContext{
payment_processing = #limiter_context_ContextPaymentProcessing{
op = {invoice, #limiter_context_PaymentProcessingOperationInvoice{}},
@ -150,8 +163,7 @@ commit_with_default_exchange(C) ->
partial_commit_with_exchange(C) ->
Rational = #base_Rational{p = 800000, q = 100},
_ = mock_exchange(Rational, C),
_ = configure_limit(?time_range_month(), ?global(), C),
ID = ?config(id, C),
ID = configure_limit(?time_range_month(), ?global(), C),
Context = #limiter_context_LimitContext{
payment_processing = #limiter_context_ContextPaymentProcessing{
op = {invoice_payment, #limiter_context_PaymentProcessingOperationInvoicePayment{}},
@ -177,8 +189,7 @@ partial_commit_with_exchange(C) ->
commit_with_exchange(C) ->
Rational = #base_Rational{p = 1000000, q = 100},
_ = mock_exchange(Rational, C),
_ = configure_limit(?time_range_month(), ?global(), C),
ID = ?config(id, C),
ID = configure_limit(?time_range_month(), ?global(), C),
Context = #limiter_context_LimitContext{
payment_processing = #limiter_context_ContextPaymentProcessing{
op = {invoice, #limiter_context_PaymentProcessingOperationInvoice{}},
@ -212,40 +223,33 @@ get_rate(C) ->
WoodyContext
).
-spec get_limit_ok(config()) -> _.
get_limit_ok(C) ->
ID = configure_limit(?time_range_month(), ?global(), C),
Context = ?ctx_invoice(_Cost = undefined),
?assertMatch(
{ok, #limiter_Limit{amount = 0}},
lim_client:get(ID, Context, ?config(client, C))
).
-spec get_limit_notfound(config()) -> _.
get_limit_notfound(C) ->
_ = configure_limit(?time_range_month(), ?global(), C),
Context = #limiter_context_LimitContext{
payment_processing = #limiter_context_ContextPaymentProcessing{
op = {invoice, #limiter_context_PaymentProcessingOperationInvoice{}},
invoice = #limiter_context_Invoice{created_at = <<"2000-01-01T00:00:00Z">>}
}
},
{exception, #limiter_LimitNotFound{}} = lim_client:get(?config(id, C), Context, ?config(client, C)).
Context = ?ctx_invoice(_Cost = undefined),
?assertEqual(
{exception, #limiter_LimitNotFound{}},
lim_client:get(<<"NOSUCHLIMITID">>, Context, ?config(client, C))
).
-spec hold_ok(config()) -> _.
hold_ok(C) ->
_ = configure_limit(?time_range_month(), ?global(), C),
ID = ?config(id, C),
Context = #limiter_context_LimitContext{
payment_processing = #limiter_context_ContextPaymentProcessing{
op = {invoice, #limiter_context_PaymentProcessingOperationInvoice{}},
invoice = #limiter_context_Invoice{
created_at = <<"2000-01-01T00:00:00Z">>,
cost = #limiter_base_Cash{
amount = 10,
currency = #limiter_base_CurrencyRef{symbolic_code = <<"RUB">>}
}
}
}
},
ID = configure_limit(?time_range_month(), ?global(), C),
Context = ?ctx_invoice(?cash(10)),
{ok, {vector, #limiter_VectorClock{}}} = lim_client:hold(?LIMIT_CHANGE(ID), Context, ?config(client, C)),
{ok, #limiter_Limit{}} = lim_client:get(ID, Context, ?config(client, C)).
-spec commit_ok(config()) -> _.
commit_ok(C) ->
_ = configure_limit(?time_range_month(), ?global(), C),
ID = ?config(id, C),
ID = configure_limit(?time_range_month(), ?global(), C),
Context = #limiter_context_LimitContext{
payment_processing = #limiter_context_ContextPaymentProcessing{
op = {invoice, #limiter_context_PaymentProcessingOperationInvoice{}},
@ -263,8 +267,7 @@ commit_ok(C) ->
-spec rollback_ok(config()) -> _.
rollback_ok(C) ->
_ = configure_limit(?time_range_week(), ?global(), C),
ID = ?config(id, C),
ID = configure_limit(?time_range_week(), ?global(), C),
Context0 = ?ctx_invoice_payment(?cash(10), ?cash(10)),
Context1 = ?ctx_invoice_payment(?cash(10), ?cash(0)),
Change = ?LIMIT_CHANGE(ID),
@ -273,11 +276,10 @@ rollback_ok(C) ->
-spec refund_ok(config()) -> _.
refund_ok(C) ->
ID = ?config(id, C),
Client = ?config(client, C),
OwnerID = <<"WWWcool Ltd">>,
ShopID = <<"shop">>,
_ = configure_limit(?time_range_day(), ?scope([?scope_party(), ?scope_shop()]), C),
ID = configure_limit(?time_range_day(), ?scope([?scope_party(), ?scope_shop()]), C),
Context0 = ?ctx_invoice_payment(OwnerID, ShopID, ?cash(15), ?cash(15)),
RefundContext1 = ?ctx_invoice_payment_refund(OwnerID, ShopID, ?cash(10), ?cash(10), ?cash(10)),
{ok, {vector, _}} = hold_and_commit(?LIMIT_CHANGE(ID, <<"Payment">>), Context0, Client),
@ -287,13 +289,12 @@ refund_ok(C) ->
-spec get_config_ok(config()) -> _.
get_config_ok(C) ->
_ = configure_limit(?time_range_week(), ?global(), C),
{ok, #limiter_config_LimitConfig{}} = lim_client:get_config(?config(id, C), ?config(client, C)).
ID = configure_limit(?time_range_week(), ?global(), C),
{ok, #limiter_config_LimitConfig{}} = lim_client:get_config(ID, ?config(client, C)).
-spec commit_inexistent_hold_fails(config()) -> _.
commit_inexistent_hold_fails(C) ->
ID = ?config(id, C),
_ = configure_limit(?time_range_week(), ?global(), C),
ID = configure_limit(?time_range_week(), ?global(), C),
Context = ?ctx_invoice_payment(?cash(42), undefined),
% NOTE
% We do not expect `LimitChangeNotFound` here because we no longer reconcile with accounter
@ -303,8 +304,7 @@ commit_inexistent_hold_fails(C) ->
-spec partial_commit_inexistent_hold_fails(config()) -> _.
partial_commit_inexistent_hold_fails(C) ->
ID = ?config(id, C),
_ = configure_limit(?time_range_week(), ?global(), C),
ID = configure_limit(?time_range_week(), ?global(), C),
Context = ?ctx_invoice_payment(?cash(42), ?cash(21)),
% NOTE
% We do not expect `LimitChangeNotFound` here because we no longer reconcile with accounter
@ -318,12 +318,11 @@ commit_multirange_limit_ok(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{}},
time_range_type = ?time_range_month(),
context_type = ?ctx_type_payproc(),
type = ?lim_type_turnover(?turnover_metric_amount(<<"RUB">>)),
scope = ?scope([]),
op_behaviour = #limiter_config_OperationLimitBehaviour{}
},
@ -354,9 +353,8 @@ commit_multirange_limit_ok(C) ->
-spec commit_processes_idempotently(config()) -> _.
commit_processes_idempotently(C) ->
ID = ?config(id, C),
Client = ?config(client, C),
_ = configure_limit(?time_range_week(), ?global(), C),
ID = configure_limit(?time_range_week(), ?global(), C),
Context = ?ctx_invoice_payment(?cash(42), undefined),
Change = ?LIMIT_CHANGE(ID),
{ok, _} = lim_client:hold(Change, Context, Client),
@ -368,9 +366,8 @@ commit_processes_idempotently(C) ->
-spec full_commit_processes_idempotently(config()) -> _.
full_commit_processes_idempotently(C) ->
ID = ?config(id, C),
Client = ?config(client, C),
_ = configure_limit(?time_range_week(), ?global(), C),
ID = configure_limit(?time_range_week(), ?global(), C),
Cost = ?cash(42),
Context = ?ctx_invoice_payment(Cost, Cost),
Change = ?LIMIT_CHANGE(ID),
@ -383,9 +380,8 @@ full_commit_processes_idempotently(C) ->
-spec partial_commit_processes_idempotently(config()) -> _.
partial_commit_processes_idempotently(C) ->
ID = ?config(id, C),
Client = ?config(client, C),
_ = configure_limit(?time_range_week(), ?global(), C),
ID = configure_limit(?time_range_week(), ?global(), C),
Context = ?ctx_invoice_payment(?cash(42), ?cash(40)),
Change = ?LIMIT_CHANGE(ID),
{ok, _} = lim_client:hold(Change, Context, Client),
@ -397,9 +393,8 @@ partial_commit_processes_idempotently(C) ->
-spec rollback_processes_idempotently(config()) -> _.
rollback_processes_idempotently(C) ->
ID = ?config(id, C),
Client = ?config(client, C),
_ = configure_limit(?time_range_week(), ?global(), C),
ID = configure_limit(?time_range_week(), ?global(), C),
Context = ?ctx_invoice_payment(?cash(42), ?cash(0)),
Change = ?LIMIT_CHANGE(ID),
{ok, _} = lim_client:hold(Change, Context, Client),
@ -411,30 +406,98 @@ rollback_processes_idempotently(C) ->
%%
-spec commit_number_ok(config()) -> _.
commit_number_ok(C) ->
Client = ?config(client, C),
ID = configure_limit(?time_range_week(), ?global(), ?turnover_metric_number(), C),
Context = ?ctx_invoice_payment(?cash(10), ?cash(10)),
{ok, LimitState0} = lim_client:get(ID, Context, Client),
_ = hold_and_commit(?LIMIT_CHANGE(ID), Context, Client),
{ok, LimitState1} = lim_client:get(ID, Context, Client),
?assertEqual(
LimitState1#limiter_Limit.amount,
LimitState0#limiter_Limit.amount + 1
).
-spec rollback_number_ok(config()) -> _.
rollback_number_ok(C) ->
Client = ?config(client, C),
ID = configure_limit(?time_range_week(), ?global(), ?turnover_metric_number(), C),
Context = ?ctx_invoice_payment(?cash(10), ?cash(10)),
ContextRollback = ?ctx_invoice_payment(?cash(10), ?cash(0)),
{ok, LimitState0} = lim_client:get(ID, Context, Client),
_ = hold_and_commit(?LIMIT_CHANGE(ID), Context, ContextRollback, Client),
{ok, LimitState1} = lim_client:get(ID, Context, Client),
?assertEqual(
LimitState1#limiter_Limit.amount,
LimitState0#limiter_Limit.amount
).
-spec commit_refund_keep_number_unchanged(config()) -> _.
commit_refund_keep_number_unchanged(C) ->
Client = ?config(client, C),
ID = configure_limit(?time_range_week(), ?global(), ?turnover_metric_number(), C),
Cost = ?cash(10),
CaptureCost = ?cash(8),
RefundCost = ?cash(5),
PaymentContext = ?ctx_invoice_payment(<<"OWNER">>, <<"SHOP">>, Cost, CaptureCost),
RefundContext = ?ctx_invoice_payment_refund(<<"OWNER">>, <<"SHOP">>, Cost, CaptureCost, RefundCost),
{ok, LimitState0} = lim_client:get(ID, PaymentContext, Client),
_ = hold_and_commit(?LIMIT_CHANGE(ID, 1), PaymentContext, Client),
_ = hold_and_commit(?LIMIT_CHANGE(ID, 2), RefundContext, Client),
{ok, LimitState1} = lim_client:get(ID, PaymentContext, Client),
?assertEqual(
% Expected to be the same because refund decreases counter given limit config
LimitState1#limiter_Limit.amount,
LimitState0#limiter_Limit.amount
).
-spec partial_commit_number_counts_as_single_op(config()) -> _.
partial_commit_number_counts_as_single_op(C) ->
Client = ?config(client, C),
ID = configure_limit(?time_range_week(), ?global(), ?turnover_metric_number(), C),
Context = ?ctx_invoice_payment(?cash(10), ?cash(10)),
ContextPartial = ?ctx_invoice_payment(?cash(10), ?cash(5)),
{ok, LimitState0} = lim_client:get(ID, Context, Client),
_ = hold_and_commit(?LIMIT_CHANGE(ID), Context, ContextPartial, Client),
{ok, LimitState1} = lim_client:get(ID, Context, Client),
?assertEqual(
LimitState1#limiter_Limit.amount,
LimitState0#limiter_Limit.amount + 1
).
%%
gen_change_id(LimitID, ChangeID) ->
genlib:format("~s/~p", [LimitID, ChangeID]).
hold_and_commit(Change, Context, Client) ->
hold_and_commit(Change, Context, Context, Client).
hold_and_commit(Change, Context, ContextCommit, Client) ->
{ok, {vector, _}} = lim_client:hold(Change, Context, Client),
{ok, {vector, _}} = lim_client:commit(Change, Context, Client).
{ok, {vector, _}} = lim_client:commit(Change, ContextCommit, Client).
mock_exchange(Rational, C) ->
lim_mock:mock_services([{xrates, fun('GetConvertedAmount', _) -> {ok, Rational} end}], C).
configure_limit(TimeRange, Scope, C) ->
configure_limit(TimeRange, Scope, ?turnover_metric_amount(<<"RUB">>), C).
configure_limit(TimeRange, Scope, Metric, C) ->
ID = ?config(id, C),
Params = #limiter_config_LimitConfigParams{
id = ID,
started_at = <<"2000-01-01T00:00:00Z">>,
body_type = ?body_type_cash(<<"RUB">>),
time_range_type = TimeRange,
shard_size = 1,
type = ?lim_type_turnover(),
type = ?lim_type_turnover(Metric),
scope = Scope,
context_type = ?ctx_type_payproc(),
op_behaviour = ?op_behaviour(?op_subtraction())
},
{ok, _LimitConfig} = lim_client:create_config(Params, ?config(client, C)).
{ok, _LimitConfig} = lim_client:create_config(Params, ?config(client, C)),
ID.
gen_unique_id(Prefix) ->
genlib:format("~s/~B", [Prefix, lim_time:now()]).

View File

@ -25,7 +25,8 @@
% readability.
{elvis_style, used_ignored_variable, disable},
% Tests are usually more comprehensible when a bit more verbose.
{elvis_style, dont_repeat_yourself, #{min_complexity => 20}}
{elvis_style, dont_repeat_yourself, #{min_complexity => 20}},
{elvis_style, god_modules, #{limit => 50}}
]
},
#{

View File

@ -25,7 +25,7 @@
{<<"jsx">>,{pkg,<<"jsx">>,<<"3.1.0">>},1},
{<<"limiter_proto">>,
{git,"https://github.com/valitydev/limiter-proto.git",
{ref,"d390910cd246f2356f10c2db410ecf93e55eff4d"}},
{ref,"6723e862157a7f78194a64271899c2ef1581e177"}},
0},
{<<"machinery">>,
{git,"https://github.com/valitydev/machinery-erlang.git",