Skip to content

Commit 58ddb86

Browse files
authored
TD-763: Add UI Cascade as default (#96)
* TD-763: Add UI Cascade as default * Fixes * Add tests * Fix format * Bump OTP version * Be less strict about OTP version * Refactor cascade decision making * New get_route_cascade_behaviour function
1 parent fe5ed51 commit 58ddb86

File tree

7 files changed

+301
-90
lines changed

7 files changed

+301
-90
lines changed

.env

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@
22
# You SHOULD specify point releases here so that build time and run time Erlang/OTPs
33
# are the same. See: https://github.com/erlware/relx/pull/902
44
SERVICE_NAME=hellgate
5-
OTP_VERSION=24.2.0
5+
OTP_VERSION=24.3.4
66
REBAR_VERSION=3.18
77
THRIFT_VERSION=0.14.2.3

apps/hellgate/src/hg_cascade.erl

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
-module(hg_cascade).
2+
3+
-include_lib("damsel/include/dmsl_domain_thrift.hrl").
4+
-include_lib("damsel/include/dmsl_user_interaction_thrift.hrl").
5+
6+
-type trigger_status() :: triggered | not_triggered | negative_trigger.
7+
-type cascade_behaviour() :: dmsl_domain_thrift:'CascadeBehaviour'().
8+
-type operation_failure() :: dmsl_domain_thrift:'OperationFailure'().
9+
10+
-export([is_triggered/4]).
11+
12+
-spec is_triggered(
13+
cascade_behaviour() | undefined,
14+
operation_failure(),
15+
hg_routing:payment_route(),
16+
[hg_session:t()]
17+
) ->
18+
boolean().
19+
is_triggered(undefined, _OperationFailure, Route, Sessions) ->
20+
handle_trigger_check(is_user_interaction_triggered_(Route, Sessions));
21+
is_triggered(
22+
#domain_CascadeBehaviour{
23+
mapped_errors = MappedErrors,
24+
no_user_interaction = NoUI
25+
},
26+
OperationFailure,
27+
Route,
28+
Sessions
29+
) ->
30+
TriggerStatuses = [
31+
is_mapped_errors_triggered(MappedErrors, OperationFailure),
32+
is_user_interaction_triggered(NoUI, Route, Sessions)
33+
],
34+
handle_trigger_check(lists:foldl(fun trigger_reduction/2, not_triggered, TriggerStatuses)).
35+
36+
handle_trigger_check(triggered) ->
37+
true;
38+
handle_trigger_check(not_triggered) ->
39+
false;
40+
handle_trigger_check(negative_trigger) ->
41+
false.
42+
43+
is_user_interaction_triggered(undefined, _, _) ->
44+
not_trigger;
45+
is_user_interaction_triggered(
46+
#domain_CascadeWhenNoUI{}, Route, Sessions
47+
) ->
48+
is_user_interaction_triggered_(Route, Sessions).
49+
50+
is_user_interaction_triggered_(Route, Sessions) ->
51+
lists:foldl(
52+
fun(Session, Status) ->
53+
case Session of
54+
#{route := Route, interaction := Interaction} when Interaction =/= undefined ->
55+
negative_trigger;
56+
_ ->
57+
Status
58+
end
59+
end,
60+
triggered,
61+
Sessions
62+
).
63+
64+
is_mapped_errors_triggered(undefined, _) ->
65+
not_triggered;
66+
is_mapped_errors_triggered(#domain_CascadeOnMappedErrors{error_signatures = Signatures}, {failure, Failure}) ->
67+
case failure_matches_any_transient(Failure, ordsets:to_list(Signatures)) of
68+
true ->
69+
triggered;
70+
false ->
71+
negative_trigger
72+
end;
73+
is_mapped_errors_triggered(#domain_CascadeOnMappedErrors{}, {operation_timeout, _}) ->
74+
negative_trigger.
75+
76+
failure_matches_any_transient(Failure, TransientErrorsList) ->
77+
lists:any(
78+
fun(ExpectNotation) ->
79+
payproc_errors:match_notation(Failure, fun
80+
(Notation) when binary_part(Notation, {0, byte_size(ExpectNotation)}) =:= ExpectNotation -> true;
81+
(_) -> false
82+
end)
83+
end,
84+
TransientErrorsList
85+
).
86+
87+
-spec trigger_reduction(trigger_status(), trigger_status()) -> trigger_status().
88+
trigger_reduction(_, negative_trigger) ->
89+
negative_trigger;
90+
trigger_reduction(negative_trigger, _) ->
91+
negative_trigger;
92+
trigger_reduction(triggered, _) ->
93+
triggered;
94+
trigger_reduction(not_triggered, triggered) ->
95+
triggered;
96+
trigger_reduction(not_triggered, not_triggered) ->
97+
not_triggered.
98+
99+
-ifdef(TEST).
100+
-include_lib("eunit/include/eunit.hrl").
101+
102+
-spec test() -> _.
103+
104+
-spec failure_matches_any_transient_test_() -> [_].
105+
failure_matches_any_transient_test_() ->
106+
TransientErrors = [
107+
%% 'preauthorization_failed' with all sub failure codes
108+
<<"preauthorization_failed">>,
109+
%% only 'rejected_by_inspector:*' sub failure codes
110+
<<"rejected_by_inspector:">>,
111+
%% 'authorization_failed:whatsgoingon' with sub failure codes
112+
<<"authorization_failed:whatsgoingon">>
113+
],
114+
[
115+
%% Does match
116+
?_assert(
117+
failure_matches_any_transient(
118+
#domain_Failure{code = <<"preauthorization_failed">>},
119+
TransientErrors
120+
)
121+
),
122+
?_assert(
123+
failure_matches_any_transient(
124+
#domain_Failure{code = <<"preauthorization_failed">>, sub = #domain_SubFailure{code = <<"unknown">>}},
125+
TransientErrors
126+
)
127+
),
128+
?_assert(
129+
failure_matches_any_transient(
130+
#domain_Failure{code = <<"rejected_by_inspector">>, sub = #domain_SubFailure{code = <<"whatever">>}},
131+
TransientErrors
132+
)
133+
),
134+
?_assert(
135+
failure_matches_any_transient(
136+
#domain_Failure{code = <<"authorization_failed">>, sub = #domain_SubFailure{code = <<"whatsgoingon">>}},
137+
TransientErrors
138+
)
139+
),
140+
%% Does NOT match
141+
?_assertNot(
142+
failure_matches_any_transient(
143+
#domain_Failure{code = <<"no_route_found">>},
144+
TransientErrors
145+
)
146+
),
147+
?_assertNot(
148+
failure_matches_any_transient(
149+
#domain_Failure{code = <<"no_route_found">>, sub = #domain_SubFailure{code = <<"unknown">>}},
150+
TransientErrors
151+
)
152+
),
153+
?_assertNot(
154+
failure_matches_any_transient(
155+
#domain_Failure{code = <<"rejected_by_inspector">>},
156+
TransientErrors
157+
)
158+
),
159+
?_assertNot(
160+
failure_matches_any_transient(
161+
#domain_Failure{code = <<"authorization_failed">>, sub = #domain_SubFailure{code = <<"unknown">>}},
162+
TransientErrors
163+
)
164+
)
165+
].
166+
167+
-endif.

apps/hellgate/src/hg_invoice_payment.erl

Lines changed: 18 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -2231,10 +2231,13 @@ process_result({payment, processing_failure}, Action, St = #st{failure = Failure
22312231
NewAction = hg_machine_action:set_timeout(0, Action),
22322232
%% We need to rollback only current route.
22332233
%% Previously used routes are supposed to have their limits already rolled back.
2234-
Routes = [get_route(St)],
2234+
Route = get_route(St),
2235+
Routes = [Route],
22352236
_ = rollback_payment_limits(Routes, get_iter(St), St),
22362237
_ = rollback_payment_cashflow(St),
2237-
case is_route_cascade_available(?failed(Failure), St) of
2238+
Revision = get_payment_revision(St),
2239+
Behaviour = get_route_cascade_behaviour(Route, Revision),
2240+
case is_route_cascade_available(Behaviour, Route, ?failed(Failure), St) of
22382241
true -> process_routing(NewAction, St);
22392242
false -> {done, {[?payment_status_changed(?failed(Failure))], NewAction}}
22402243
end;
@@ -3660,32 +3663,24 @@ get_party_client() ->
36603663
Context = hg_context:get_party_client_context(HgContext),
36613664
{Client, Context}.
36623665

3663-
is_route_cascade_available(?failed(OperationFailure), #st{routes = AttemptedRoutes} = St) ->
3664-
is_failure_cascade_trigger(OperationFailure) andalso
3666+
is_route_cascade_available(
3667+
Behaviour,
3668+
Route,
3669+
?failed(OperationFailure),
3670+
#st{routes = AttemptedRoutes, sessions = Sessions} = St
3671+
) ->
3672+
%% We don't care what type of UserInteraction was initiated, as long as there was none
3673+
SessionsList = lists:flatten(maps:values(Sessions)),
3674+
hg_cascade:is_triggered(Behaviour, OperationFailure, Route, SessionsList) andalso
36653675
%% For cascade viability we require at least one more route candidate
36663676
%% provided by recent routing.
36673677
length(get_candidate_routes(St)) > 1 andalso
36683678
length(AttemptedRoutes) < get_routing_attempt_limit(St).
36693679

3670-
is_failure_cascade_trigger({failure, Failure}) ->
3671-
failure_matches_any_transient(Failure, get_transient_errors_list());
3672-
is_failure_cascade_trigger(_OtherFailure) ->
3673-
false.
3674-
3675-
get_transient_errors_list() ->
3676-
PaymentConfig = genlib_app:env(hellgate, payment, #{}),
3677-
maps:get(default_transient_errors, PaymentConfig, [<<"preauthorization_failed">>]).
3678-
3679-
failure_matches_any_transient(Failure, TransientErrorsList) ->
3680-
lists:any(
3681-
fun(ExpectNotation) ->
3682-
payproc_errors:match_notation(Failure, fun
3683-
(Notation) when binary_part(Notation, {0, byte_size(ExpectNotation)}) =:= ExpectNotation -> true;
3684-
(_) -> false
3685-
end)
3686-
end,
3687-
TransientErrorsList
3688-
).
3680+
get_route_cascade_behaviour(Route, Revision) ->
3681+
ProviderRef = get_route_provider(Route),
3682+
#domain_Provider{cascade_behaviour = Behaviour} = hg_domain:get(Revision, {provider, ProviderRef}),
3683+
Behaviour.
36893684

36903685
-ifdef(TEST).
36913686
-include_lib("eunit/include/eunit.hrl").
@@ -3779,67 +3774,4 @@ filter_out_attempted_routes_test_() ->
37793774
)
37803775
].
37813776

3782-
-spec failure_matches_any_transient_test_() -> [_].
3783-
failure_matches_any_transient_test_() ->
3784-
TransientErrors = [
3785-
%% 'preauthorization_failed' with all sub failure codes
3786-
<<"preauthorization_failed">>,
3787-
%% only 'rejected_by_inspector:*' sub failure codes
3788-
<<"rejected_by_inspector:">>,
3789-
%% 'authorization_failed:whatsgoingon' with sub failure codes
3790-
<<"authorization_failed:whatsgoingon">>
3791-
],
3792-
[
3793-
%% Does match
3794-
?_assert(
3795-
failure_matches_any_transient(
3796-
#domain_Failure{code = <<"preauthorization_failed">>},
3797-
TransientErrors
3798-
)
3799-
),
3800-
?_assert(
3801-
failure_matches_any_transient(
3802-
#domain_Failure{code = <<"preauthorization_failed">>, sub = #domain_SubFailure{code = <<"unknown">>}},
3803-
TransientErrors
3804-
)
3805-
),
3806-
?_assert(
3807-
failure_matches_any_transient(
3808-
#domain_Failure{code = <<"rejected_by_inspector">>, sub = #domain_SubFailure{code = <<"whatever">>}},
3809-
TransientErrors
3810-
)
3811-
),
3812-
?_assert(
3813-
failure_matches_any_transient(
3814-
#domain_Failure{code = <<"authorization_failed">>, sub = #domain_SubFailure{code = <<"whatsgoingon">>}},
3815-
TransientErrors
3816-
)
3817-
),
3818-
%% Does NOT match
3819-
?_assertNot(
3820-
failure_matches_any_transient(
3821-
#domain_Failure{code = <<"no_route_found">>},
3822-
TransientErrors
3823-
)
3824-
),
3825-
?_assertNot(
3826-
failure_matches_any_transient(
3827-
#domain_Failure{code = <<"no_route_found">>, sub = #domain_SubFailure{code = <<"unknown">>}},
3828-
TransientErrors
3829-
)
3830-
),
3831-
?_assertNot(
3832-
failure_matches_any_transient(
3833-
#domain_Failure{code = <<"rejected_by_inspector">>},
3834-
TransientErrors
3835-
)
3836-
),
3837-
?_assertNot(
3838-
failure_matches_any_transient(
3839-
#domain_Failure{code = <<"authorization_failed">>, sub = #domain_SubFailure{code = <<"unknown">>}},
3840-
TransientErrors
3841-
)
3842-
)
3843-
].
3844-
38453777
-endif.

apps/hellgate/src/hg_session.erl

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,14 @@
5555
-export([proxy_state/1]).
5656
-export([timings/1]).
5757
-export([repair_scenario/1]).
58+
-export([user_interaction/1]).
5859

5960
%% API
6061

6162
-export([set_repair_scenario/2]).
6263
-export([set_payment_info/2]).
6364
-export([set_trx_info/2]).
65+
-export([collect_user_interactions/1]).
6466

6567
-export([create/0]).
6668
-export([deduce_activity/1]).
@@ -154,8 +156,23 @@ timings(T) ->
154156
repair_scenario(T) ->
155157
maps:get(repair_scenario, T, undefined).
156158

159+
-spec user_interaction(t()) -> hg_maybe:maybe(interaction()).
160+
user_interaction(T) ->
161+
maps:get(interaction, T, undefined).
162+
157163
%% API
158164

165+
-spec collect_user_interactions([t()]) -> [interaction()].
166+
collect_user_interactions(Ts) ->
167+
genlib_list:compact(
168+
lists:map(
169+
fun(T) ->
170+
user_interaction(T)
171+
end,
172+
Ts
173+
)
174+
).
175+
159176
-spec set_repair_scenario(repair_scenario(), t()) -> t().
160177
set_repair_scenario(Scenario, Session) ->
161178
Session#{repair_scenario => Scenario}.

0 commit comments

Comments
 (0)