TD-428: More tests and routing light refactor (#49)

* added cascade with limits test

* added machine test tool and limit complex test

* refactored routing

* fixed

* fixed log

* added requested changes

* fixed

* refactored ct machine

* Revert "Auxiliary commit to revert individual files from a2e126e6aeb8c71cde2d2b2560270ab5d25bf49f"

This reverts commit 58c9951ac2830dda4d17f2e920e1d66c72c4fd33.

* refactored test tool

* refactored test

* fixed format

* cleaned up

* fixed

* added minor

* added one more test

* changed to gen server

* fixed

* removed

* fixed types

* added patch

* fixed
This commit is contained in:
Артем 2022-12-02 14:47:28 +04:00 committed by GitHub
parent 1e74eb0c73
commit 75592f2a54
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 567 additions and 105 deletions

View File

@ -11,6 +11,8 @@
-define(LIMIT_TURNOVER_NUM_PAYTOOL_ID2, <<"ID2">>).
-define(LIMIT_TURNOVER_AMOUNT_PAYTOOL_ID1, <<"ID3">>).
-define(LIMIT_TURNOVER_AMOUNT_PAYTOOL_ID2, <<"ID4">>).
-define(LIMIT_TURNOVER_AMOUNT_PAYTOOL_ID3, <<"ID5">>).
-define(LIMIT_TURNOVER_AMOUNT_PAYTOOL_ID4, <<"ID6">>).
-define(glob(), #domain_GlobalsRef{}).
-define(cur(ID), #domain_CurrencyRef{symbolic_code = ID}).

View File

@ -3,6 +3,7 @@
-include_lib("common_test/include/ct.hrl").
-export([cfg/2]).
-export([cfg/3]).
-export([start_apps/1]).
-export([start_app/1]).
@ -99,6 +100,23 @@ start_app(dmt_client = AppName) ->
]),
#{}
};
start_app(party_client = AppName) ->
{
start_app_with(AppName, [
{services, #{
party_management => "http://party-management:8022/v1/processing/partymgmt"
}},
{woody, #{
cache_mode => safe,
options => #{
woody_client => #{
event_handler => {scoper_woody_event_handler, #{}}
}
}
}}
]),
#{}
};
start_app(ff_server = AppName) ->
{
start_app_with(AppName, [

View File

@ -71,6 +71,7 @@ start_processing_apps(Options) ->
scoper,
woody,
dmt_client,
party_client,
{fistful, [
{services, services(Options)},
{providers, identity_provider_config(Options)}
@ -460,6 +461,22 @@ domain_config(Options) ->
condition(cost_in, {903000, <<"RUB">>}),
?ruleset(?PAYINST1_ROUTING_POLICIES + 19)
),
delegate(
condition(cost_in, {904000, <<"RUB">>}),
?ruleset(?PAYINST1_ROUTING_POLICIES + 20)
),
delegate(
condition(cost_in, {3000000, <<"RUB">>}),
?ruleset(?PAYINST1_ROUTING_POLICIES + 21)
),
delegate(
condition(cost_in, {905000, <<"RUB">>}),
?ruleset(?PAYINST1_ROUTING_POLICIES + 22)
),
delegate(
condition(cost_in, {3001000, <<"RUB">>}),
?ruleset(?PAYINST1_ROUTING_POLICIES + 23)
),
delegate(
condition(cost_in, {910000, <<"RUB">>}),
?ruleset(?PAYINST1_ROUTING_POLICIES + 30)
@ -603,6 +620,36 @@ domain_config(Options) ->
]}
),
routing_ruleset(
?ruleset(?PAYINST1_ROUTING_POLICIES + 20),
{candidates, [
candidate({constant, true}, ?trm(2300), 4000),
candidate({constant, true}, ?trm(2400), 1000)
]}
),
routing_ruleset(
?ruleset(?PAYINST1_ROUTING_POLICIES + 21),
{candidates, [
candidate({constant, true}, ?trm(2400))
]}
),
routing_ruleset(
?ruleset(?PAYINST1_ROUTING_POLICIES + 22),
{candidates, [
candidate({constant, true}, ?trm(2500), 4000),
candidate({constant, true}, ?trm(2600), 1000)
]}
),
routing_ruleset(
?ruleset(?PAYINST1_ROUTING_POLICIES + 23),
{candidates, [
candidate({constant, true}, ?trm(2700))
]}
),
routing_ruleset(
?ruleset(?PAYINST1_ROUTING_POLICIES + 30),
{candidates, [
@ -955,6 +1002,81 @@ domain_config(Options) ->
}
),
ct_domain:withdrawal_terminal(
?trm(2300),
?prv(4),
#domain_ProvisionTermSet{
wallet = #domain_WalletProvisionTerms{
withdrawals = #domain_WithdrawalProvisionTerms{
turnover_limit =
{value, [
?trnvrlimit(?LIMIT_TURNOVER_AMOUNT_PAYTOOL_ID2, 2000000)
]}
}
}
}
),
ct_domain:withdrawal_terminal(
?trm(2400),
?prv(5),
#domain_ProvisionTermSet{
wallet = #domain_WalletProvisionTerms{
withdrawals = #domain_WithdrawalProvisionTerms{
turnover_limit =
{value, [
?trnvrlimit(?LIMIT_TURNOVER_AMOUNT_PAYTOOL_ID2, 3000000)
]}
}
}
}
),
ct_domain:withdrawal_terminal(
?trm(2500),
?prv(4),
#domain_ProvisionTermSet{
wallet = #domain_WalletProvisionTerms{
withdrawals = #domain_WithdrawalProvisionTerms{
turnover_limit =
{value, [
?trnvrlimit(?LIMIT_TURNOVER_AMOUNT_PAYTOOL_ID3, 2000000)
]}
}
}
}
),
ct_domain:withdrawal_terminal(
?trm(2600),
?prv(5),
#domain_ProvisionTermSet{
wallet = #domain_WalletProvisionTerms{
withdrawals = #domain_WithdrawalProvisionTerms{
turnover_limit =
{value, [
?trnvrlimit(?LIMIT_TURNOVER_AMOUNT_PAYTOOL_ID4, 3000000)
]}
}
}
}
),
ct_domain:withdrawal_terminal(
?trm(2700),
?prv(5),
#domain_ProvisionTermSet{
wallet = #domain_WalletProvisionTerms{
withdrawals = #domain_WithdrawalProvisionTerms{
turnover_limit =
{value, [
?trnvrlimit(?LIMIT_TURNOVER_AMOUNT_PAYTOOL_ID3, 4000000)
]}
}
}
}
),
ct_domain:withdrawal_terminal(
?trm(3000),
?prv(1),

View File

@ -18,7 +18,6 @@
created_at => ff_time:timestamp_ms(),
party_revision => party_revision(),
domain_revision => domain_revision(),
iteration => non_neg_integer(),
route => route(),
attempts => attempts(),
resource => destination_resource(),
@ -191,6 +190,7 @@
-export_type([adjustment_params/0]).
-export_type([start_adjustment_error/0]).
-export_type([limit_check_details/0]).
-export_type([activity/0]).
%% Transfer logic callbacks
@ -217,6 +217,7 @@
-export([destination_resource/1]).
-export([metadata/1]).
-export([params/1]).
-export([activity/1]).
%% API
@ -393,6 +394,10 @@ metadata(T) ->
params(#{params := V}) ->
V.
-spec activity(withdrawal_state()) -> activity().
activity(Withdrawal) ->
deduce_activity(Withdrawal).
%% API
-spec gen(gen_args()) -> withdrawal().
@ -744,7 +749,7 @@ do_process_transfer(routing, Withdrawal) ->
do_process_transfer(p_transfer_start, Withdrawal) ->
process_p_transfer_creation(Withdrawal);
do_process_transfer(p_transfer_prepare, Withdrawal) ->
ok = do_rollback_routing(route(Withdrawal), Withdrawal),
ok = do_rollback_routing([route(Withdrawal)], Withdrawal),
Tr = ff_withdrawal_route_attempt_utils:get_current_p_transfer(attempts(Withdrawal)),
{ok, Events} = ff_postings_transfer:prepare(Tr),
{continue, [{p_transfer, Ev} || Ev <- Events]};
@ -790,7 +795,7 @@ process_routing(Withdrawal) ->
-spec process_rollback_routing(withdrawal_state()) -> process_result().
process_rollback_routing(Withdrawal) ->
_ = do_rollback_routing(undefined, Withdrawal),
ok = do_rollback_routing([], Withdrawal),
{undefined, []}.
-spec do_process_routing(withdrawal_state()) -> {ok, [route()]} | {error, Reason} when
@ -799,7 +804,8 @@ process_rollback_routing(Withdrawal) ->
do_process_routing(Withdrawal) ->
do(fun() ->
{Varset, Context} = make_routing_varset_and_context(Withdrawal),
GatherResult = ff_withdrawal_routing:gather_routes(Varset, Context),
ExcludeRoutes = ff_withdrawal_route_attempt_utils:get_terminals(attempts(Withdrawal)),
GatherResult = ff_withdrawal_routing:gather_routes(Varset, Context, ExcludeRoutes),
FilterResult = ff_withdrawal_routing:filter_limit_overflow_routes(GatherResult, Varset, Context),
ff_withdrawal_routing:log_reject_context(FilterResult),
Routes = unwrap(ff_withdrawal_routing:routes(FilterResult)),
@ -813,46 +819,21 @@ do_process_routing(Withdrawal) ->
end
end).
% filter_attempts(#{routes := Routes} = Result, Withdrawal) ->
% NextRoutesResult = ff_withdrawal_route_attempt_utils:next_routes(
% [
% ff_withdrawal_routing:make_route(ProviderID, TerminalID)
% || #{
% provider_ref := #domain_ProviderRef{id = ProviderID},
% terminal_ref := #domain_TerminalRef{id = TerminalID}
% } <- Routes
% ],
% attempts(Withdrawal),
% get_attempt_limit(Withdrawal)
% ),
% case NextRoutesResult of
% {ok, Left} ->
% {ok, Result#{
% routes => [
% Route
% || Route = #{
% provider_ref := #domain_ProviderRef{id = ProviderID},
% terminal_ref := #domain_TerminalRef{id = TerminalID}
% } <- Routes,
% lists:member(ff_withdrawal_routing:make_route(ProviderID, TerminalID), Left)
% ]
% }};
% {error, Reason} ->
% {error, Reason}
% end.
do_rollback_routing(ExcludeRoute, Withdrawal) ->
do(fun() ->
do_rollback_routing(ExcludeRoutes0, Withdrawal) ->
{Varset, Context} = make_routing_varset_and_context(Withdrawal),
Routes = unwrap(ff_withdrawal_routing:routes(ff_withdrawal_routing:gather_routes(Varset, Context))),
RollbackRoutes = maybe_exclude_route(ExcludeRoute, Routes),
rollback_routes_limits(RollbackRoutes, Varset, Context)
end).
maybe_exclude_route(#{terminal_id := TerminalID}, Routes) ->
lists:filter(fun(#{terminal_id := TID}) -> TerminalID =/= TID end, Routes);
maybe_exclude_route(undefined, Routes) ->
Routes.
ExcludeUsedRoutes0 = ff_withdrawal_route_attempt_utils:get_terminals(attempts(Withdrawal)),
ExcludeUsedRoutes1 =
case ff_withdrawal_route_attempt_utils:get_current_p_transfer_status(attempts(Withdrawal)) of
cancelled ->
ExcludeUsedRoutes0;
_ ->
CurrentRoute = ff_withdrawal_route_attempt_utils:get_current_terminal(attempts(Withdrawal)),
lists:filter(fun(R) -> CurrentRoute =/= R end, ExcludeUsedRoutes0)
end,
ExcludeRoutes1 =
ExcludeUsedRoutes1 ++ lists:map(fun(R) -> ff_withdrawal_routing:get_terminal(R) end, ExcludeRoutes0),
Routes = ff_withdrawal_routing:get_routes(ff_withdrawal_routing:gather_routes(Varset, Context, ExcludeRoutes1)),
rollback_routes_limits(Routes, Varset, Context).
rollback_routes_limits(Routes, Withdrawal) ->
{Varset, Context} = make_routing_varset_and_context(Withdrawal),
@ -885,7 +866,7 @@ make_routing_varset_and_context(Withdrawal) ->
domain_revision => DomainRevision,
identity => Identity,
withdrawal => Withdrawal,
iteration => maps:get(iteration, Withdrawal)
iteration => ff_withdrawal_route_attempt_utils:get_index(attempts(Withdrawal))
},
{build_party_varset(VarsetParams), Context}.
@ -1906,15 +1887,8 @@ apply_event_({limit_check, Details}, T) ->
add_limit_check(Details, T);
apply_event_({p_transfer, Ev}, T) ->
Tr = ff_postings_transfer:apply_event(Ev, p_transfer(T)),
Iteration =
case maps:get(status, Tr, undefined) of
committed -> maps:get(iteration, T) + 1;
cancelled -> maps:get(iteration, T) + 1;
_ -> maps:get(iteration, T)
end,
Attempts = attempts(T),
R = ff_withdrawal_route_attempt_utils:update_current_p_transfer(Tr, Attempts),
update_attempts(R, T#{iteration => Iteration});
R = ff_withdrawal_route_attempt_utils:update_current_p_transfer(Tr, attempts(T)),
update_attempts(R, T);
apply_event_({session_started, SessionID}, T) ->
Session = #{id => SessionID},
Attempts = attempts(T),
@ -1941,11 +1915,10 @@ apply_event_({adjustment, _Ev} = Event, T) ->
make_state(#{route := Route} = T) ->
Attempts = ff_withdrawal_route_attempt_utils:new(),
T#{
iteration => 1,
attempts => ff_withdrawal_route_attempt_utils:new_route(Route, Attempts)
};
make_state(T) when not is_map_key(route, T) ->
T#{iteration => 1}.
T.
get_attempt_limit(Withdrawal) ->
#{

View File

@ -20,8 +20,10 @@
-export([new_route/2]).
-export([next_route/3]).
-export([next_routes/3]).
-export([get_index/1]).
-export([get_current_session/1]).
-export([get_current_p_transfer/1]).
-export([get_current_p_transfer_status/1]).
-export([get_current_limit_checks/1]).
-export([update_current_session/2]).
-export([update_current_p_transfer/2]).
@ -29,21 +31,27 @@
-export([get_sessions/1]).
-export([get_attempt/1]).
-export([get_terminals/1]).
-export([get_current_terminal/1]).
-opaque attempts() :: #{
attempts := #{route_key() => attempt()},
inversed_routes := [route_key()],
attempt := non_neg_integer(),
current => route_key()
current => route_key(),
index := index()
}.
-type index() :: non_neg_integer().
-define(DEFAULT_INDEX, 1).
-export_type([attempts/0]).
%% Iternal types
-type p_transfer() :: ff_postings_transfer:transfer().
-type p_transfer_status() :: ff_postings_transfer:status().
-type limit_check_details() :: ff_withdrawal:limit_check_details().
-type account() :: ff_account:account().
-type route() :: ff_withdrawal_routing:route().
-type route_key() :: {ff_payouts_provider:id(), ff_payouts_terminal:id()} | unknown.
-type session() :: ff_withdrawal:session().
@ -62,7 +70,8 @@ new() ->
#{
attempts => #{},
inversed_routes => [],
attempt => 0
attempt => 0,
index => ?DEFAULT_INDEX
}.
-spec new_route(route(), attempts()) -> attempts().
@ -99,6 +108,12 @@ next_routes(Routes, #{attempts := Existing}, _AttemptLimit) ->
Routes
)}.
-spec get_index(attempts() | undefined) -> index().
get_index(undefined) ->
?DEFAULT_INDEX;
get_index(#{index := Index}) ->
Index.
-spec get_current_session(attempts()) -> undefined | session().
get_current_session(Attempts) ->
Attempt = current(Attempts),
@ -109,6 +124,11 @@ get_current_p_transfer(Attempts) ->
Attempt = current(Attempts),
maps:get(p_transfer, Attempt, undefined).
-spec get_current_p_transfer_status(attempts()) -> undefined | p_transfer_status().
get_current_p_transfer_status(Attempts) ->
Attempt = current(Attempts),
maps:get(status, maps:get(p_transfer, Attempt, #{}), undefined).
-spec get_current_limit_checks(attempts()) -> undefined | [limit_check_details()].
get_current_limit_checks(Attempts) ->
Attempt = current(Attempts),
@ -122,13 +142,19 @@ update_current_session(Session, Attempts) ->
},
update_current(Updated, Attempts).
-spec update_current_p_transfer(account(), attempts()) -> attempts().
update_current_p_transfer(Account, Attempts) ->
-spec update_current_p_transfer(p_transfer(), attempts()) -> attempts().
update_current_p_transfer(PTransfer, Attempts = #{index := Index}) ->
Attempt = current(Attempts),
Updated = Attempt#{
p_transfer => Account
p_transfer => PTransfer
},
update_current(Updated, Attempts).
NewIndex =
case maps:get(status, PTransfer, undefined) of
committed -> Index + 1;
cancelled -> Index + 1;
_ -> Index
end,
update_current(Updated, Attempts#{index => NewIndex}).
-spec update_current_limit_checks([limit_check_details()], attempts()) -> attempts().
update_current_limit_checks(LimitChecks, Routes) ->
@ -160,6 +186,18 @@ get_sessions(#{attempts := Attempts, inversed_routes := InvRoutes}) ->
get_attempt(#{attempt := Attempt}) ->
Attempt.
-spec get_terminals(attempts()) -> [ff_payouts_terminal:id()].
get_terminals(#{attempts := Attempts}) ->
lists:map(fun({_, TerminalID}) -> TerminalID end, maps:keys(Attempts));
get_terminals(_) ->
[].
-spec get_current_terminal(attempts()) -> undefined | ff_payouts_terminal:id().
get_current_terminal(#{current := {_, TerminalID}}) ->
TerminalID;
get_current_terminal(_) ->
undefined.
%% Internal
-spec route_key(route()) -> route_key().
@ -192,8 +230,4 @@ update_current(Attempt, #{current := Route, attempts := Attempts} = R) ->
attempts => Attempts#{
Route => Attempt
}
};
update_current(Attempt, R) when not is_map_key(current, R) ->
% There are some legacy operations without a route in storage
% It looks like we should save other data without route.
update_current(Attempt, add_route(unknown, R)).
}.

View File

@ -5,6 +5,7 @@
-export([prepare_routes/2]).
-export([prepare_routes/3]).
-export([gather_routes/2]).
-export([gather_routes/3]).
-export([filter_limit_overflow_routes/3]).
-export([rollback_routes_limits/3]).
-export([commit_routes_limits/3]).
@ -12,6 +13,7 @@
-export([get_provider/1]).
-export([get_terminal/1]).
-export([routes/1]).
-export([get_routes/1]).
-export([log_reject_context/1]).
-import(ff_pipeline, [do/1, unwrap/1]).
@ -26,8 +28,8 @@
-type routing_context() :: #{
domain_revision := domain_revision(),
identity := identity(),
withdrawal => withdrawal(),
iteration => pos_integer()
iteration := pos_integer(),
withdrawal => withdrawal()
}.
-type routing_state() :: #{
@ -66,7 +68,7 @@
-spec prepare_routes(party_varset(), identity(), domain_revision()) ->
{ok, [route()]} | {error, route_not_found}.
prepare_routes(PartyVarset, Identity, DomainRevision) ->
prepare_routes(PartyVarset, #{identity => Identity, domain_revision => DomainRevision}).
prepare_routes(PartyVarset, #{identity => Identity, domain_revision => DomainRevision, iteration => 1}).
-spec prepare_routes(party_varset(), routing_context()) ->
{ok, [route()]} | {error, route_not_found}.
@ -77,7 +79,12 @@ prepare_routes(PartyVarset, Context) ->
-spec gather_routes(party_varset(), routing_context()) ->
routing_state().
gather_routes(PartyVarset, Context = #{identity := Identity, domain_revision := DomainRevision}) ->
gather_routes(PartyVarset, Context) ->
gather_routes(PartyVarset, Context, []).
-spec gather_routes(party_varset(), routing_context(), [terminal_id()]) ->
routing_state().
gather_routes(PartyVarset, Context = #{identity := Identity, domain_revision := DomainRevision}, ExcludeRoutes) ->
{ok, PaymentInstitutionID} = ff_party:get_identity_payment_institution_id(Identity),
{ok, PaymentInstitution} = ff_payment_institution:get(PaymentInstitutionID, PartyVarset, DomainRevision),
{Routes, RejectContext} = ff_routing_rule:gather_routes(
@ -86,7 +93,8 @@ gather_routes(PartyVarset, Context = #{identity := Identity, domain_revision :=
PartyVarset,
DomainRevision
),
filter_valid_routes(#{routes => Routes, reject_context => RejectContext}, PartyVarset, Context).
State = exclude_routes(#{routes => Routes, reject_context => RejectContext}, ExcludeRoutes),
filter_valid_routes(State, PartyVarset, Context).
-spec filter_limit_overflow_routes(routing_state(), party_varset(), routing_context()) ->
routing_state().
@ -130,9 +138,9 @@ make_route(ProviderID, TerminalID) ->
get_provider(#{provider_id := ProviderID}) ->
ProviderID.
-spec get_terminal(route()) -> ff_maybe:maybe(terminal_id()).
get_terminal(Route) ->
maps:get(terminal_id, Route, undefined).
-spec get_terminal(route()) -> terminal_id().
get_terminal(#{terminal_id := TerminalID}) ->
TerminalID.
-spec routes(routing_state()) ->
{ok, [route()]} | {error, route_not_found}.
@ -141,6 +149,17 @@ routes(#{routes := Routes = [_ | _]}) ->
routes(_) ->
{error, route_not_found}.
-spec get_routes(routing_state()) ->
[route()].
get_routes(#{routes := Routes}) ->
[
make_route(P, T)
|| #{
provider_ref := #domain_ProviderRef{id = P},
terminal_ref := #domain_TerminalRef{id = T}
} <- Routes
].
-spec sort_routes([routing_rule_route()]) -> [route()].
sort_routes(RoutingRuleRoutes) ->
ProviderTerminalMap = lists:foldl(
@ -233,6 +252,26 @@ get_route_terms_and_process(
{error, Error}
end.
exclude_routes(#{routes := Routes, reject_context := RejectContext}, ExcludeRoutes) ->
lists:foldl(
fun(Route, State = #{routes := ValidRoutes0, reject_context := RejectContext0}) ->
ProviderRef = maps:get(provider_ref, Route),
TerminalRef = maps:get(terminal_ref, Route),
case not lists:member(ff_routing_rule:terminal_id(Route), ExcludeRoutes) of
true ->
ValidRoutes1 = [Route | ValidRoutes0],
State#{routes => ValidRoutes1};
false ->
RejectedRoutes0 = maps:get(rejected_routes, RejectContext0),
RejectedRoutes1 = [{ProviderRef, TerminalRef, member_of_exlude_list} | RejectedRoutes0],
RejectContext1 = maps:put(rejected_routes, RejectedRoutes1, RejectContext0),
State#{reject_context => RejectContext1}
end
end,
#{routes => [], reject_context => RejectContext},
Routes
).
-spec do_rollback_limits(withdrawal_provision_terms(), party_varset(), route(), routing_context()) ->
ok.
do_rollback_limits(CombinedTerms, _PartyVarset, Route, #{withdrawal := Withdrawal, iteration := Iter}) ->

View File

@ -0,0 +1,57 @@
-module(ff_ct_barrier).
-export([start_link/0]).
-export([stop/1]).
-export([enter/2]).
-export([release/1]).
%% Gen Server
-behaviour(gen_server).
-export([init/1]).
-export([handle_call/3]).
-export([handle_cast/2]).
-type caller() :: {pid(), reference()}.
-type st() :: #{
blocked := [caller()]
}.
%%
-spec enter(pid(), timeout()) -> ok.
enter(ServerRef, Timeout) ->
gen_server:call(ServerRef, enter, Timeout).
-spec release(pid()) -> ok.
release(ServerRef) ->
gen_server:call(ServerRef, release).
-spec start_link() -> {ok, pid()}.
start_link() ->
gen_server:start_link(?MODULE, [], []).
-spec stop(pid()) -> ok.
stop(ServerRef) ->
proc_lib:stop(ServerRef, normal, 5000).
-spec init(_) -> {ok, st()}.
init(_Args) ->
{ok, #{blocked => []}}.
-spec handle_call(enter | release, caller(), st()) ->
{noreply, st()}
| {reply, ok, st()}.
handle_call(enter, From = {ClientPid, _}, St = #{blocked := Blocked}) ->
false = lists:any(fun({Pid, _}) -> Pid == ClientPid end, Blocked),
{noreply, St#{blocked => [From | Blocked]}};
handle_call(release, _From, St = #{blocked := Blocked}) ->
ok = lists:foreach(fun(Caller) -> gen_server:reply(Caller, ok) end, Blocked),
{reply, ok, St#{blocked => []}};
handle_call(Call, _From, _St) ->
error({badcall, Call}).
-spec handle_cast(_Cast, st()) -> no_return().
handle_cast(Cast, _St) ->
error({badcast, Cast}).

View File

@ -0,0 +1,53 @@
%%%
%%% Test machine
%%%
-module(ff_ct_machine).
-dialyzer({nowarn_function, dispatch_signal/4}).
-export([load_per_suite/0]).
-export([unload_per_suite/0]).
-export([set_hook/2]).
-export([clear_hook/1]).
-spec load_per_suite() -> ok.
load_per_suite() ->
meck:new(machinery, [no_link, passthrough]),
meck:expect(machinery, dispatch_signal, fun dispatch_signal/4),
meck:expect(machinery, dispatch_call, fun dispatch_call/4).
-spec unload_per_suite() -> ok.
unload_per_suite() ->
meck:unload(machinery).
-type hook() :: fun((machinery:machine(_, _), module(), _Args) -> _).
-spec set_hook(timeout, hook()) -> ok.
set_hook(On = timeout, Fun) when is_function(Fun, 3) ->
persistent_term:put({?MODULE, hook, On}, Fun).
-spec clear_hook(timeout) -> ok.
clear_hook(On = timeout) ->
_ = persistent_term:erase({?MODULE, hook, On}),
ok.
dispatch_signal({init, Args}, Machine, {Handler, HandlerArgs}, Opts) ->
Handler:init(Args, Machine, HandlerArgs, Opts);
dispatch_signal(timeout, Machine, {Handler, HandlerArgs}, Opts) when Handler =/= fistful ->
_ =
case persistent_term:get({?MODULE, hook, timeout}, undefined) of
Fun when is_function(Fun) ->
Fun(Machine, Handler, HandlerArgs);
undefined ->
ok
end,
Handler:process_timeout(Machine, HandlerArgs, Opts);
dispatch_signal(timeout, Machine, {Handler, HandlerArgs}, Opts) ->
Handler:process_timeout(Machine, HandlerArgs, Opts);
dispatch_signal({notification, Args}, Machine, {Handler, HandlerArgs}, Opts) ->
Handler:process_notification(Args, Machine, HandlerArgs, Opts).
dispatch_call(Args, Machine, {Handler, HandlerArgs}, Opts) ->
Handler:process_call(Args, Machine, HandlerArgs, Opts).

View File

@ -33,6 +33,14 @@ init_per_suite(Config) ->
{ok, #config_LimitConfig{}} = ff_ct_limiter_client:create_config(
limiter_create_amount_params(?LIMIT_TURNOVER_AMOUNT_PAYTOOL_ID2),
ct_helper:get_woody_ctx(Config)
),
{ok, #config_LimitConfig{}} = ff_ct_limiter_client:create_config(
limiter_create_amount_params(?LIMIT_TURNOVER_AMOUNT_PAYTOOL_ID3),
ct_helper:get_woody_ctx(Config)
),
{ok, #config_LimitConfig{}} = ff_ct_limiter_client:create_config(
limiter_create_amount_params(?LIMIT_TURNOVER_AMOUNT_PAYTOOL_ID4),
ct_helper:get_woody_ctx(Config)
).
-spec get_limit_amount(id(), withdrawal(), config()) -> integer().

View File

@ -1,7 +1,6 @@
-module(ff_withdrawal_limits_SUITE).
-include_lib("stdlib/include/assert.hrl").
-include_lib("fistful_proto/include/fistful_fistful_base_thrift.hrl").
-include_lib("ff_cth/include/ct_domain.hrl").
-include_lib("damsel/include/dmsl_wthd_domain_thrift.hrl").
@ -21,6 +20,9 @@
-export([limit_overflow/1]).
-export([choose_provider_without_limit_overflow/1]).
-export([provider_limits_exhaust_orderly/1]).
-export([provider_retry/1]).
-export([limit_exhaust_on_provider_retry/1]).
-export([first_limit_exhaust_on_provider_retry/1]).
%% Internal types
@ -44,12 +46,16 @@ groups() ->
limit_success,
limit_overflow,
choose_provider_without_limit_overflow,
provider_limits_exhaust_orderly
provider_limits_exhaust_orderly,
provider_retry,
limit_exhaust_on_provider_retry,
first_limit_exhaust_on_provider_retry
]}
].
-spec init_per_suite(config()) -> config().
init_per_suite(C0) ->
ff_ct_machine:load_per_suite(),
C1 = ct_helper:makeup_cfg(
[
ct_helper:test_case_name(init),
@ -62,6 +68,7 @@ init_per_suite(C0) ->
-spec end_per_suite(config()) -> _.
end_per_suite(C) ->
ff_ct_machine:unload_per_suite(),
ok = ct_payment_system:shutdown(C).
%%
@ -77,20 +84,43 @@ end_per_group(_, _) ->
%%
-spec init_per_testcase(test_case_name(), config()) -> config().
init_per_testcase(Name, C) ->
init_per_testcase(Name, C0) ->
C1 = ct_helper:makeup_cfg(
[
ct_helper:test_case_name(Name),
ct_helper:woody_ctx()
],
C
C0
),
ok = ct_helper:set_context(C1),
C1.
PartyID = create_party(C1),
C2 = ct_helper:cfg('$party', PartyID, C1),
case Name of
Name when
Name =:= provider_retry orelse
Name =:= limit_exhaust_on_provider_retry orelse
Name =:= first_limit_exhaust_on_provider_retry
->
_ = set_retryable_errors(PartyID, [<<"authorization_error">>]);
_ ->
ok
end,
C2.
-spec end_per_testcase(test_case_name(), config()) -> _.
end_per_testcase(_Name, _C) ->
ok = ct_helper:unset_context().
end_per_testcase(Name, C) ->
case Name of
Name when
Name =:= provider_retry orelse
Name =:= limit_exhaust_on_provider_retry orelse
Name =:= first_limit_exhaust_on_provider_retry
->
PartyID = ct_helper:cfg('$party', C),
_ = set_retryable_errors(PartyID, []);
_ ->
ok
end,
ct_helper:unset_context().
%% Tests
@ -234,16 +264,105 @@ provider_limits_exhaust_orderly(C) ->
Result = await_final_withdrawal_status(WithdrawalID),
?assertMatch({failed, #{code := <<"no_route_found">>}}, Result).
%% Utils
-spec provider_retry(config()) -> test_return().
provider_retry(C) ->
Currency = <<"RUB">>,
Cash = {904000, Currency},
#{
wallet_id := WalletID,
destination_id := DestinationID
} = prepare_standard_environment(Cash, C),
WithdrawalID = generate_id(),
WithdrawalParams = #{
id => WithdrawalID,
destination_id => DestinationID,
wallet_id => WalletID,
body => Cash,
external_id => WithdrawalID
},
ok = ff_withdrawal_machine:create(WithdrawalParams, ff_entity_context:new()),
?assertEqual(succeeded, await_final_withdrawal_status(WithdrawalID)),
Withdrawal = get_withdrawal(WithdrawalID),
?assertEqual(WalletID, ff_withdrawal:wallet_id(Withdrawal)),
?assertEqual(DestinationID, ff_withdrawal:destination_id(Withdrawal)),
?assertEqual(Cash, ff_withdrawal:body(Withdrawal)),
?assertEqual(WithdrawalID, ff_withdrawal:external_id(Withdrawal)).
get_limit_amount(Cash, WalletID, DestinationID, LimitID, C) ->
-spec limit_exhaust_on_provider_retry(config()) -> test_return().
limit_exhaust_on_provider_retry(C) ->
?assertEqual(
{failed, #{code => <<"authorization_error">>, sub => #{code => <<"insufficient_funds">>}}},
await_provider_retry(904000, 3000000, 4000000, C)
).
-spec first_limit_exhaust_on_provider_retry(config()) -> test_return().
first_limit_exhaust_on_provider_retry(C) ->
?assertEqual(succeeded, await_provider_retry(905000, 3001000, 4000000, C)).
await_provider_retry(FirstAmount, SecondAmount, TotalAmount, C) ->
Currency = <<"RUB">>,
#{
wallet_id := WalletID,
destination_id := DestinationID
} = prepare_standard_environment({TotalAmount, Currency}, C),
WithdrawalID1 = generate_id(),
WithdrawalParams1 = #{
id => WithdrawalID1,
destination_id => DestinationID,
wallet_id => WalletID,
body => {FirstAmount, Currency},
external_id => WithdrawalID1
},
WithdrawalID2 = generate_id(),
WithdrawalParams2 = #{
id => WithdrawalID2,
destination_id => DestinationID,
wallet_id => WalletID,
body => {SecondAmount, Currency},
external_id => WithdrawalID2
},
Activity = {fail, session},
{ok, Barrier} = ff_ct_barrier:start_link(),
ok = ff_ct_machine:set_hook(
timeout,
fun
(Machine, ff_withdrawal_machine, _Args) ->
Withdrawal = ff_machine:model(ff_machine:collapse(ff_withdrawal, Machine)),
case {ff_withdrawal:id(Withdrawal), ff_withdrawal:activity(Withdrawal)} of
{WithdrawalID1, Activity} ->
ff_ct_barrier:enter(Barrier, _Timeout = 10000);
_ ->
ok
end;
(_Machine, _Handler, _Args) ->
false
end
),
ok = ff_withdrawal_machine:create(WithdrawalParams1, ff_entity_context:new()),
_ = await_withdrawal_activity(Activity, WithdrawalID1),
ok = ff_withdrawal_machine:create(WithdrawalParams2, ff_entity_context:new()),
?assertEqual(succeeded, await_final_withdrawal_status(WithdrawalID2)),
ok = ff_ct_barrier:release(Barrier),
Status = await_final_withdrawal_status(WithdrawalID1),
ok = ff_ct_machine:clear_hook(timeout),
ok = ff_ct_barrier:stop(Barrier),
Status.
set_retryable_errors(PartyID, ErrorList) ->
application:set_env(ff_transfer, withdrawal, #{
party_transient_errors => #{
PartyID => ErrorList
}
}).
get_limit_withdrawal(Cash, WalletID, DestinationID) ->
{ok, WalletMachine} = ff_wallet_machine:get(WalletID),
Wallet = ff_wallet_machine:wallet(WalletMachine),
WalletAccount = ff_wallet:account(Wallet),
{ok, SenderSt} = ff_identity_machine:get(ff_account:identity(WalletAccount)),
SenderIdentity = ff_identity_machine:identity(SenderSt),
Withdrawal = #wthd_domain_Withdrawal{
#wthd_domain_Withdrawal{
created_at = ff_codec:marshal(timestamp_ms, ff_time:now()),
body = ff_dmsl_codec:marshal(cash, Cash),
destination = ff_adapter_withdrawal_codec:marshal(resource, get_destination_resource(DestinationID)),
@ -251,7 +370,10 @@ get_limit_amount(Cash, WalletID, DestinationID, LimitID, C) ->
id => ff_identity:id(SenderIdentity),
owner_id => ff_identity:party(SenderIdentity)
})
},
}.
get_limit_amount(Cash, WalletID, DestinationID, LimitID, C) ->
Withdrawal = get_limit_withdrawal(Cash, WalletID, DestinationID),
ff_limiter_helper:get_limit_amount(LimitID, Withdrawal, C).
get_destination_resource(DestinationID) ->
@ -261,15 +383,15 @@ get_destination_resource(DestinationID) ->
Resource.
prepare_standard_environment({_Amount, Currency} = WithdrawalCash, C) ->
Party = create_party(C),
IdentityID = create_person_identity(Party, C),
PartyID = ct_helper:cfg('$party', C),
IdentityID = create_person_identity(PartyID, C),
WalletID = create_wallet(IdentityID, <<"My wallet">>, Currency, C),
ok = await_wallet_balance({0, Currency}, WalletID),
DestinationID = create_destination(IdentityID, Currency, C),
ok = set_wallet_balance(WithdrawalCash, WalletID),
#{
identity_id => IdentityID,
party_id => Party,
party_id => PartyID,
wallet_id => WalletID,
destination_id => DestinationID
}.
@ -299,6 +421,16 @@ await_final_withdrawal_status(WithdrawalID) ->
),
get_withdrawal_status(WithdrawalID).
await_withdrawal_activity(Activity, WithdrawalID) ->
ct_helper:await(
Activity,
fun() ->
{ok, Machine} = ff_withdrawal_machine:get(WithdrawalID),
ff_withdrawal:activity(ff_withdrawal_machine:withdrawal(Machine))
end,
genlib_retry:linear(20, 1000)
).
create_party(_C) ->
ID = genlib:bsuuid(),
_ = ff_party:create(ID),

View File

@ -57,7 +57,6 @@
%% Internal types
-type id() :: ff_accounting:id().
-type account() :: ff_account:account().
%%
@ -177,7 +176,7 @@ cancel(#{status := Status}) ->
%%
-spec apply_event(event(), ff_maybe:maybe(account())) -> account().
-spec apply_event(event(), ff_maybe:maybe(transfer())) -> transfer().
apply_event({created, Transfer}, undefined) ->
Transfer;
apply_event({status_changed, S}, Transfer) ->

View File

@ -6,11 +6,18 @@
-export([gather_routes/4]).
-export([log_reject_context/1]).
%% Accessors
-export([provider_id/1]).
-export([terminal_id/1]).
-type payment_institution() :: ff_payment_institution:payment_institution().
-type routing_ruleset_ref() :: dmsl_domain_thrift:'RoutingRulesetRef'().
-type provider_ref() :: dmsl_domain_thrift:'ProviderRef'().
-type provider() :: dmsl_domain_thrift:'Provider'().
-type terminal_ref() :: dmsl_domain_thrift:'TerminalRef'().
-type provider_id() :: dmsl_domain_thrift:'ObjectID'().
-type terminal_id() :: dmsl_domain_thrift:'ObjectID'().
-type priority() :: integer().
-type weight() :: integer().
-type varset() :: ff_varset:varset().
@ -33,6 +40,7 @@
-type reject_context() :: #{
varset := varset(),
accepted_routes := [route()],
rejected_routes := [rejected_route()]
}.
@ -42,12 +50,23 @@
-import(ff_pipeline, [do/1, unwrap/1]).
%% Accessors
-spec provider_id(route()) -> provider_id().
provider_id(#{provider_ref := #domain_ProviderRef{id = ProviderID}}) ->
ProviderID.
-spec terminal_id(route()) -> terminal_id().
terminal_id(#{terminal_ref := #domain_TerminalRef{id = TerminalID}}) ->
TerminalID.
%%
-spec new_reject_context(varset()) -> reject_context().
new_reject_context(VS) ->
#{
varset => VS,
accepted_routes => [],
rejected_routes => []
}.
@ -56,7 +75,10 @@ gather_routes(PaymentInstitution, RoutingRuleTag, VS, Revision) ->
RejectContext = new_reject_context(VS),
case do_gather_routes(PaymentInstitution, RoutingRuleTag, VS, Revision) of
{ok, {AcceptedRoutes, RejectedRoutes}} ->
{AcceptedRoutes, RejectContext#{rejected_routes => RejectedRoutes}};
{AcceptedRoutes, RejectContext#{
accepted_routes => AcceptedRoutes,
rejected_routes => RejectedRoutes
}};
{error, misconfiguration} ->
logger:warning("Routing rule misconfiguration. Varset:~n~p", [VS]),
{[], RejectContext}
@ -155,18 +177,14 @@ make_route(Candidate, Revision) ->
-spec log_reject_context(reject_context()) -> ok.
log_reject_context(RejectContext) ->
Level = warning,
RejectReason = unknown,
_ = logger:log(
Level,
"No route found, reason = ~p, varset: ~p",
[RejectReason, maps:get(varset, RejectContext)],
logger:get_process_metadata()
),
_ = logger:log(
Level,
"No route found, reason = ~p, rejected routes: ~p",
[RejectReason, maps:get(rejected_routes, RejectContext)],
info,
"Routing reject context: rejected routes: ~p, accepted routes: ~p, varset: ~p",
[
maps:get(rejected_routes, RejectContext),
maps:get(accepted_routes, RejectContext),
maps:get(varset, RejectContext)
],
logger:get_process_metadata()
),
ok.

View File

@ -39,12 +39,19 @@
{elvis_text_style, line_length, #{limit => 120}},
{elvis_style, nesting_level, #{level => 3}},
{elvis_style, no_if_expression, disable},
{elvis_style, invalid_dynamic_call, #{ignore => [ff_ct_provider_handler]}},
{elvis_style, invalid_dynamic_call, #{
ignore => [
ff_ct_provider_handler,
ff_ct_barrier,
ff_ct_machine
]
}},
% We want to use `ct:pal/2` and friends in test code.
{elvis_style, no_debug_call, disable},
% Assert macros can trigger use of ignored binding, yet we want them for better
% readability.
{elvis_style, used_ignored_variable, disable},
{elvis_style, state_record_and_type, disable},
% Tests are usually more comprehensible when a bit more verbose.
{elvis_style, dont_repeat_yourself, #{min_complexity => 50}},
{elvis_style, god_modules, disable}