diff --git a/apps/limiter/src/lim_config_codec.erl b/apps/limiter/src/lim_config_codec.erl index 5a145f1..7a99c2a 100644 --- a/apps/limiter/src/lim_config_codec.erl +++ b/apps/limiter/src/lim_config_codec.erl @@ -6,6 +6,8 @@ -export([unmarshal/2]). -export([marshal_config/1]). -export([unmarshal_body_type/1]). +-export([unmarshal_op_behaviour/1]). +-export([maybe_apply/2]). %% Types @@ -17,6 +19,7 @@ -type decoded_value() :: decoded_value(any()). -type decoded_value(T) :: T. +-spec maybe_apply(any(), function()) -> any(). maybe_apply(undefined, _) -> undefined; maybe_apply(Value, Fun) -> @@ -59,9 +62,21 @@ marshal_config(Config) -> time_range_type = marshal_time_range_type(lim_config_machine:time_range_type(Config)), context_type = marshal_context_type(lim_config_machine:context_type(Config)), type = maybe_apply(lim_config_machine:type(Config), fun marshal_type/1), - scope = maybe_apply(lim_config_machine:scope(Config), fun marshal_scope/1) + scope = maybe_apply(lim_config_machine:scope(Config), fun marshal_scope/1), + op_behaviour = maybe_apply(lim_config_machine:op_behaviour(Config), fun marshal_op_behaviour/1) }. +marshal_op_behaviour(OpBehaviour) -> + PaymentRefund = maps:get(invoice_payment_refund, OpBehaviour, undefined), + #limiter_config_OperationLimitBehaviour{ + invoice_payment_refund = maybe_apply(PaymentRefund, fun marshal_behaviour/1) + }. + +marshal_behaviour(subtraction) -> + {subtraction, #limiter_config_Subtraction{}}; +marshal_behaviour(addition) -> + {addition, #limiter_config_Addition{}}. + marshal_body_type(amount) -> {amount, #limiter_config_LimitBodyTypeAmount{}}; marshal_body_type({cash, Currency}) -> @@ -139,7 +154,8 @@ unmarshal_config(#limiter_config_LimitConfig{ time_range_type = TimeRangeType, context_type = ContextType, type = Type, - scope = Scope + scope = Scope, + op_behaviour = OpBehaviour }) -> genlib_map:compact(#{ id => ID, @@ -152,9 +168,24 @@ unmarshal_config(#limiter_config_LimitConfig{ context_type => unmarshal_context_type(ContextType), type => maybe_apply(Type, fun unmarshal_type/1), scope => maybe_apply(Scope, fun unmarshal_scope/1), - description => Description + description => Description, + op_behaviour => maybe_apply(OpBehaviour, fun unmarshal_op_behaviour/1) }). +-spec unmarshal_op_behaviour(encoded_value()) -> decoded_value(). +unmarshal_op_behaviour(OpBehaviour) -> + #limiter_config_OperationLimitBehaviour{ + invoice_payment_refund = Refund + } = OpBehaviour, + genlib_map:compact(#{ + invoice_payment_refund => maybe_apply(Refund, fun unmarshal_behaviour/1) + }). + +unmarshal_behaviour({subtraction, #limiter_config_Subtraction{}}) -> + subtraction; +unmarshal_behaviour({addition, #limiter_config_Addition{}}) -> + addition. + -spec unmarshal_body_type(encoded_value()) -> decoded_value(). unmarshal_body_type({amount, #limiter_config_LimitBodyTypeAmount{}}) -> amount; diff --git a/apps/limiter/src/lim_config_machine.erl b/apps/limiter/src/lim_config_machine.erl index 53784e7..7ef93af 100644 --- a/apps/limiter/src/lim_config_machine.erl +++ b/apps/limiter/src/lim_config_machine.erl @@ -16,6 +16,7 @@ -export([type/1]). -export([scope/1]). -export([context_type/1]). +-export([op_behaviour/1]). %% API @@ -62,7 +63,8 @@ context_type := context_type(), type => limit_type(), scope => limit_scope(), - description => description() + description => description(), + op_behaviour => op_behaviour() }. -type create_params() :: #{ @@ -74,9 +76,13 @@ context_type := context_type(), type => limit_type(), scope => limit_scope(), - description => description() + description => description(), + op_behaviour => op_behaviour() }. +-type op_behaviour() :: #{operation_type() := addition | subtraction}. +-type operation_type() :: invoice_payment_refund. + -type lim_id() :: lim_limiter_thrift:'LimitID'(). -type lim_change() :: lim_limiter_thrift:'LimitChange'(). -type limit() :: lim_limiter_thrift:'Limit'(). @@ -209,6 +215,12 @@ scope(_) -> context_type(#{context_type := Value}) -> Value. +-spec op_behaviour(config()) -> lim_maybe:maybe(op_behaviour()). +op_behaviour(#{op_behaviour := Value}) -> + Value; +op_behaviour(_) -> + undefined. + %% -spec start(lim_id(), create_params(), lim_context()) -> {ok, config()}. diff --git a/apps/limiter/src/lim_configurator.erl b/apps/limiter/src/lim_configurator.erl index 02b3d42..b6dd416 100644 --- a/apps/limiter/src/lim_configurator.erl +++ b/apps/limiter/src/lim_configurator.erl @@ -30,7 +30,8 @@ handle_function_( name = Name, description = Description, started_at = StartedAt, - body_type = BodyType + body_type = BodyType, + op_behaviour = OpBehaviour }}, LimitContext, _Opts @@ -39,11 +40,15 @@ handle_function_( {ok, Config} -> {ok, LimitConfig} = lim_config_machine:start( ID, - Config#{ + genlib_map:compact(Config#{ description => Description, started_at => StartedAt, - body_type => lim_config_codec:unmarshal_body_type(BodyType) - }, + 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 + ) + }), LimitContext ), {ok, lim_config_codec:marshal_config(LimitConfig)}; @@ -67,7 +72,7 @@ mk_limit_config(<<"ShopDayTurnover">>) -> processor_type => <<"TurnoverProcessor">>, type => turnover, scope => {scope, shop}, - shard_size => 12, + shard_size => 7, context_type => payment_processing, time_range_type => {calendar, day} }}; diff --git a/apps/limiter/src/lim_turnover_processor.erl b/apps/limiter/src/lim_turnover_processor.erl index 851e953..2ef8933 100644 --- a/apps/limiter/src/lim_turnover_processor.erl +++ b/apps/limiter/src/lim_turnover_processor.erl @@ -84,7 +84,8 @@ hold(LimitChange = #limiter_LimitChange{id = LimitID}, Config = #{body_type := B {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), - lim_accounting:hold(construct_plan_id(LimitChange), {1, Postings}, LimitContext) + Postings1 = apply_op_behaviour(Postings, LimitContext, Config), + lim_accounting:hold(construct_plan_id(LimitChange), {1, Postings1}, LimitContext) end). -spec commit(lim_change(), config(), lim_context()) -> ok | {error, commit_error()}. @@ -130,16 +131,27 @@ partial_commit(PartialBody, LimitChange = #limiter_LimitChange{id = LimitID}, Co TimeRange = lim_config_machine:calculate_time_range(Timestamp, Config), {ok, #{account_id_from := AccountIDFrom, account_id_to := AccountIDTo}} = lim_range_machine:get_range(TimeRange, LimitRangeState), - - PartialPostings = lim_p_transfer:construct_postings(AccountIDFrom, AccountIDTo, PartialBody), - FullPostings = lim_p_transfer:construct_postings(AccountIDFrom, AccountIDTo, FullBody), - NewBatchList = [{2, lim_p_transfer:reverse_postings(FullPostings)} | [{3, PartialPostings}]], - + PartialPostings0 = lim_p_transfer:construct_postings(AccountIDFrom, AccountIDTo, PartialBody), + FullPostings0 = lim_p_transfer:construct_postings(AccountIDFrom, AccountIDTo, FullBody), + PartialPostings1 = apply_op_behaviour(PartialPostings0, LimitContext, Config), + FullPostings1 = apply_op_behaviour(FullPostings0, LimitContext, Config), + NewBatchList = [{2, lim_p_transfer:reverse_postings(FullPostings1)} | [{3, PartialPostings1}]], PlanID = construct_plan_id(LimitChange), unwrap(lim_accounting:plan(PlanID, NewBatchList, LimitContext)), - unwrap(lim_accounting:commit(PlanID, [{1, FullPostings} | NewBatchList], LimitContext)) + unwrap(lim_accounting:commit(PlanID, [{1, FullPostings1} | NewBatchList], LimitContext)) end). +apply_op_behaviour(Posting, LimitContext, #{op_behaviour := ComputationConfig}) -> + {ok, Operation} = lim_context:get_operation(payment_processing, LimitContext), + case maps:get(Operation, ComputationConfig, undefined) of + subtraction -> + lim_p_transfer:reverse_postings(Posting); + Type when Type =:= undefined orelse Type =:= additional -> + Posting + end; +apply_op_behaviour(Body, _LimitContext, _Config) -> + Body. + assert_partial_body( {cash, #{amount := Partial, currency := Currency}}, {cash, #{amount := Full, currency := Currency}} diff --git a/apps/limiter/test/lim_ct_helper.hrl b/apps/limiter/test/lim_ct_helper.hrl new file mode 100644 index 0000000..f421210 --- /dev/null +++ b/apps/limiter/test/lim_ct_helper.hrl @@ -0,0 +1,47 @@ +-ifndef(__limiter_ct_helper__). +-define(__limiter_ct_helper__, 42). + +-include_lib("limiter_proto/include/lim_configurator_thrift.hrl"). + +-define(cash(Amount), #limiter_base_Cash{ + amount = Amount, + currency = #limiter_base_CurrencyRef{symbolic_code = <<"RUB">>} +}). + +-define(ctx_invoice_payment(Cost, CaptureCost), ?ctx_invoice_payment(undefined, undefined, Cost, CaptureCost)). + +-define(ctx_invoice_payment(OwnerID, ShopID, Cost, CaptureCost), #limiter_context_LimitContext{ + payment_processing = #limiter_context_ContextPaymentProcessing{ + op = {invoice_payment, #limiter_context_PaymentProcessingOperationInvoicePayment{}}, + invoice = #limiter_context_Invoice{ + owner_id = OwnerID, + shop_id = ShopID, + effective_payment = #limiter_context_InvoicePayment{ + created_at = <<"2000-01-01T00:00:00Z">>, + cost = Cost, + capture_cost = CaptureCost + } + } + } +}). + +-define(ctx_invoice_payment_refund(OwnerID, ShopID, Cost, CaptureCost, RefundCost), #limiter_context_LimitContext{ + payment_processing = #limiter_context_ContextPaymentProcessing{ + op = {invoice_payment_refund, #limiter_context_PaymentProcessingOperationInvoicePaymentRefund{}}, + invoice = #limiter_context_Invoice{ + owner_id = OwnerID, + shop_id = ShopID, + effective_payment = #limiter_context_InvoicePayment{ + created_at = <<"2000-01-01T00:00:00Z">>, + cost = Cost, + capture_cost = CaptureCost, + effective_refund = #limiter_context_InvoicePaymentRefund{ + cost = RefundCost, + created_at = <<"2000-01-01T00:00:00Z">> + } + } + } + } +}). + +-endif. diff --git a/apps/limiter/test/lim_turnover_SUITE.erl b/apps/limiter/test/lim_turnover_SUITE.erl index f3ce642..f34af0c 100644 --- a/apps/limiter/test/lim_turnover_SUITE.erl +++ b/apps/limiter/test/lim_turnover_SUITE.erl @@ -2,6 +2,7 @@ -include_lib("stdlib/include/assert.hrl"). -include_lib("common_test/include/ct.hrl"). +-include_lib("lim_ct_helper.hrl"). -include_lib("limiter_proto/include/lim_configurator_thrift.hrl"). -include_lib("xrates_proto/include/xrates_rate_thrift.hrl"). @@ -22,6 +23,7 @@ -export([hold_ok/1]). -export([commit_ok/1]). -export([rollback_ok/1]). +-export([refund_ok/1]). -export([get_config_ok/1]). -type test_case_name() :: atom(). @@ -48,7 +50,8 @@ groups() -> hold_ok, commit_ok, rollback_ok, - get_config_ok + get_config_ok, + refund_ok ]} ]. @@ -266,32 +269,45 @@ commit_ok(C) -> rollback_ok(C) -> ID = <<"ID">>, #{client := Client} = prepare_environment(ID, <<"GlobalMonthTurnover">>, C), - Context = #limiter_context_LimitContext{ - payment_processing = #limiter_context_ContextPaymentProcessing{ - op = {invoice_payment, #limiter_context_PaymentProcessingOperationInvoicePayment{}}, - invoice = #limiter_context_Invoice{ - effective_payment = #limiter_context_InvoicePayment{ - created_at = <<"2000-01-01T00:00:00Z">>, - cost = #limiter_base_Cash{ - amount = 10, - currency = #limiter_base_CurrencyRef{symbolic_code = <<"RUB">>} - }, - capture_cost = #limiter_base_Cash{ - amount = 0, - currency = #limiter_base_CurrencyRef{symbolic_code = <<"RUB">>} - } - } - } - } - }, + Context0 = ?ctx_invoice_payment(?cash(10), ?cash(10)), + Context1 = ?ctx_invoice_payment(?cash(10), ?cash(0)), + Timestamp = lim_time:to_rfc3339(lim_time:now()), LimitChangeID = <>, Change = #limiter_LimitChange{ id = ID, change_id = LimitChangeID }, - {ok, {vector, _}} = hold_and_commit(Change, Context, Client), - {ok, #limiter_Limit{}} = lim_client:get(ID, Context, Client). + {ok, {vector, _}} = lim_client:hold(Change, Context0, Client), + {ok, {vector, _}} = lim_client:commit(Change, Context1, Client). + +-spec refund_ok(config()) -> _. +refund_ok(C) -> + ID = lim_time:to_rfc3339(lim_time:now()), + OwnerID = <<"WWWcool Ltd">>, + ShopID = <<"shop">>, + #{client := Client} = _LimitConfig = prepare_environment(ID, <<"ShopDayTurnover">>, C), + Context0 = ?ctx_invoice_payment(OwnerID, ShopID, ?cash(15), ?cash(15)), + RefundContext1 = ?ctx_invoice_payment_refund(OwnerID, ShopID, ?cash(10), ?cash(10), ?cash(10)), + Timestamp = lim_time:to_rfc3339(lim_time:now()), + LimitChangeID = <>, + + Change = #limiter_LimitChange{ + id = ID, + change_id = LimitChangeID + }, + {ok, {vector, _}} = hold_and_commit(Change, Context0, Client), + + Timestamp2 = lim_time:to_rfc3339(lim_time:now()), + LimitChangeID2 = <>, + Change2 = #limiter_LimitChange{ + id = ID, + change_id = LimitChangeID2 + }, + + {ok, {vector, _}} = hold_and_commit(Change2, RefundContext1, Client), + {ok, #limiter_Limit{} = Limit2} = lim_client:get(ID, RefundContext1, Client), + ?assertEqual(Limit2#limiter_Limit.amount, 5). -spec get_config_ok(config()) -> _. get_config_ok(C) -> @@ -315,7 +331,10 @@ prepare_environment(ID, LimitName, _C) -> name = LimitName, description = <<"description">>, started_at = <<"2000-01-01T00:00:00Z">>, - body_type = {cash, #limiter_config_LimitBodyTypeCash{currency = <<"RUB">>}} + body_type = {cash, #limiter_config_LimitBodyTypeCash{currency = <<"RUB">>}}, + op_behaviour = #limiter_config_OperationLimitBehaviour{ + invoice_payment_refund = {subtraction, #limiter_config_Subtraction{}} + } }, {ok, LimitConfig} = lim_client:create_config(Params, Client), #{config => LimitConfig, client => Client}. diff --git a/rebar.lock b/rebar.lock index 4023090..1606da5 100644 --- a/rebar.lock +++ b/rebar.lock @@ -35,7 +35,7 @@ {<<"jsx">>,{pkg,<<"jsx">>,<<"3.0.0">>},1}, {<<"limiter_proto">>, {git,"git@github.com:rbkmoney/limiter-proto.git", - {ref,"d4b40ead589dd4dbd9d442397239c635d2a8382e"}}, + {ref,"9c0653ff7281ff443f515d2fddf9673fe837a5be"}}, 0}, {<<"machinery">>, {git,"https://github.com/rbkmoney/machinery.git",