TD-820: Refactors routing to support critical score rejections (#105)

* TD-820: Refactors routing to support critical score rejections

* Moves routing context helpers to appropriate module

* Splits large functions, fixes misconfiguration error bug

* Fixes second fail rates query

* Fixes routes equality check

* Extracts route struct functions

* Extracts routing context into separate module

* Fixes rejected routes ordering consistency

* Fixes funx naming and adds routes equality tests

* Makes rejected routes reasons more explicit in code

* Adds routing ctx pipeline test

* Adds specified routing sub failures

* Updates tests; replaces static errors translation

* Fixes ubiquity of availability status naming

* Updates route reject error codes

* Bumps damsel
This commit is contained in:
Aleksey Kashapov 2023-12-07 10:30:47 +03:00 committed by GitHub
parent 42af922405
commit 7305ff03e6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 955 additions and 511 deletions

View File

@ -5,8 +5,8 @@
activity :: hg_invoice_payment:activity(),
payment :: undefined | hg_invoice_payment:payment(),
risk_score :: undefined | hg_inspector:risk_score(),
routes = [] :: [hg_routing:payment_route()],
candidate_routes :: undefined | [hg_routing:payment_route()],
routes = [] :: [hg_route:payment_route()],
candidate_routes :: undefined | [hg_route:payment_route()],
interim_payment_status :: undefined | hg_invoice_payment:payment_status(),
new_cash :: undefined | hg_cash:cash(),
new_cash_provided :: undefined | boolean(),

View File

@ -39,7 +39,7 @@
-type payment() :: dmsl_domain_thrift:'InvoicePayment'().
-type party() :: dmsl_domain_thrift:'Party'().
-type shop() :: dmsl_domain_thrift:'Shop'().
-type route() :: hg_routing:payment_route().
-type route() :: hg_route:payment_route().
-type payment_institution() :: dmsl_domain_thrift:'PaymentInstitution'().
-type provider() :: dmsl_domain_thrift:'Provider'().
-type varset() :: hg_varset:varset().

View File

@ -12,7 +12,7 @@
-spec is_triggered(
cascade_behaviour() | undefined,
operation_failure(),
hg_routing:payment_route(),
hg_route:payment_route(),
[hg_session:t()]
) ->
boolean().

View File

@ -29,7 +29,7 @@
-type shop_id() :: dmsl_domain_thrift:'ShopID'().
-type party_id() :: dmsl_domain_thrift:'PartyID'().
-type route() :: hg_routing:payment_route().
-type route() :: hg_route:payment_route().
%%

View File

@ -190,7 +190,7 @@
-type target() :: dmsl_domain_thrift:'TargetInvoicePaymentStatus'().
-type session_target_type() :: 'processed' | 'captured' | 'cancelled' | 'refunded'.
-type risk_score() :: hg_inspector:risk_score().
-type route() :: hg_routing:payment_route().
-type route() :: hg_route:payment_route().
-type final_cash_flow() :: hg_cashflow:final_cash_flow().
-type trx_info() :: dmsl_domain_thrift:'TransactionInfo'().
-type tag() :: dmsl_proxy_provider_thrift:'CallbackTag'().
@ -235,6 +235,8 @@
%%
-define(LOG_MD(Level, Format, Args), logger:log(Level, Format, Args, logger:get_process_metadata())).
-spec get_party_revision(st()) -> {hg_party:party_revision(), hg_datetime:timestamp()}.
get_party_revision(#st{activity = {payment, _}} = St) ->
#domain_InvoicePayment{party_revision = Revision, created_at = Timestamp} = get_payment(St),
@ -767,31 +769,12 @@ gather_routes(PaymentInstitution, VS, Revision, St) ->
Payer = get_payment_payer(St),
PaymentTool = get_payer_payment_tool(Payer),
ClientIP = get_payer_client_ip(Payer),
case
hg_routing:gather_routes(
Predestination,
PaymentInstitution,
VS,
Revision,
#{
hg_routing:gather_routes(Predestination, PaymentInstitution, VS, Revision, #{
currency => Currency,
payment_tool => PaymentTool,
party_id => PartyID,
client_ip => ClientIP
}
)
of
{ok, {[], RejectedRoutes}} ->
_ = log_rejected_routes(no_route_found, RejectedRoutes, VS),
throw({no_route_found, {rejected_routes, RejectedRoutes}});
{ok, {Routes, RejectedRoutes}} ->
erlang:length(RejectedRoutes) > 0 andalso
log_rejected_routes(rejected_route_found, RejectedRoutes, VS),
Routes;
{error, {misconfiguration, _Reason} = Error} ->
_ = log_misconfigurations(Error),
throw({no_route_found, Error})
end.
}).
-spec check_risk_score(risk_score()) -> ok | {error, risk_score_is_too_high}.
check_risk_score(fatal) ->
@ -807,58 +790,35 @@ choose_routing_predestination(#domain_InvoicePayment{payer = ?payment_resource_p
% Other payers has predefined routes
log_route_choice_meta(ChoiceMeta, Revision) ->
log_route_choice_meta(#{choice_meta := undefined}, _Revision) ->
ok;
log_route_choice_meta(#{choice_meta := ChoiceMeta}, Revision) ->
Metadata = hg_routing:get_logger_metadata(ChoiceMeta, Revision),
_ = logger:log(info, "Routing decision made", #{routing => Metadata}).
logger:log(info, "Routing decision made", #{routing => Metadata}).
log_misconfigurations({misconfiguration, _} = Error) ->
maybe_log_misconfigurations({misconfiguration, _} = Error) ->
{Format, Details} = hg_routing:prepare_log_message(Error),
_ = logger:warning(Format, Details),
?LOG_MD(warning, Format, Details);
maybe_log_misconfigurations(_Error) ->
ok.
log_rejected_routes(no_route_found, RejectedRoutes, Varset) ->
_ = logger:log(
warning,
"No route found for varset: ~p",
[Varset],
logger:get_process_metadata()
),
_ = logger:log(
warning,
"No route found, rejected routes: ~p",
[RejectedRoutes],
logger:get_process_metadata()
),
log_rejected_routes(_, [], _Varset) ->
ok;
log_rejected_routes(limit_hold_reject, RejectedRoutes, _Varset) ->
_ = logger:log(
warning,
"Limiter hold error caused route candidates to be rejected: ~p",
[RejectedRoutes],
logger:get_process_metadata()
),
ok;
log_rejected_routes(limit_overflow_reject, RejectedRoutes, _Varset) ->
_ = logger:log(
info,
"Limit overflow caused route candidates to be rejected: ~p",
[RejectedRoutes],
logger:get_process_metadata()
),
ok;
log_rejected_routes(rejected_route_found, RejectedRoutes, Varset) ->
_ = logger:log(
info,
"Rejected routes found for varset: ~p",
[Varset],
logger:get_process_metadata()
),
_ = logger:log(
info,
"Rejected routes found, rejected routes: ~p",
[RejectedRoutes],
logger:get_process_metadata()
),
log_rejected_routes(all, Routes, VS) ->
?LOG_MD(warning, "No route found for varset: ~p", [VS]),
?LOG_MD(warning, "No route found, rejected routes: ~p", [Routes]);
log_rejected_routes(limit_misconfiguration, Routes, _VS) ->
?LOG_MD(warning, "Limiter hold error caused route candidates to be rejected: ~p", [Routes]);
log_rejected_routes(limit_overflow, Routes, _VS) ->
?LOG_MD(info, "Limit overflow caused route candidates to be rejected: ~p", [Routes]);
log_rejected_routes(adapter_unavailable, Routes, _VS) ->
?LOG_MD(info, "Adapter unavailability caused route candidates to be rejected: ~p", [Routes]);
log_rejected_routes(provider_conversion_is_too_low, Routes, _VS) ->
?LOG_MD(info, "Lacking conversion of provider caused route candidates to be rejected: ~p", [Routes]);
log_rejected_routes(forbidden, Routes, VS) ->
?LOG_MD(info, "Rejected routes found for varset: ~p", [VS]),
?LOG_MD(info, "Rejected routes found, rejected routes: ~p", [Routes]);
log_rejected_routes(_, _Routes, _VS) ->
ok.
validate_refund_time(RefundCreatedAt, PaymentCreatedAt, TimeSpanSelector) ->
@ -1979,24 +1939,58 @@ process_risk_score(Action, St) ->
-spec process_routing(action(), st()) -> machine_result().
process_routing(Action, St) ->
{PaymentInstitution, VS, Revision} = route_args(St),
try
AllRoutes = get_candidates(PaymentInstitution, VS, Revision, St),
AvailableRoutes = filter_out_attempted_routes(AllRoutes, St),
%% Since this is routing step then current attempt is not yet accounted for in `St`.
Iter = get_iter(St) + 1,
Events = handle_gathered_route_result(
filter_limit_overflow_routes(AvailableRoutes, VS, Iter, St),
[hg_routing:to_payment_route(R) || R <- AllRoutes],
[hg_routing:to_payment_route(R) || R <- AvailableRoutes],
Revision,
St
),
{next, {Events, hg_machine_action:set_timeout(0, Action)}}
catch
throw:{no_route_found, Reason} ->
handle_choose_route_error(Reason, [], St, Action)
Ctx0 = hg_routing_ctx:with_guard(build_routing_context(PaymentInstitution, VS, Revision, St)),
%% NOTE We need to handle routing errors differently if route not found
%% before the pipeline.
case hg_routing_ctx:error(Ctx0) of
undefined ->
Ctx1 = run_routing_decision_pipeline(Ctx0, VS, St),
_ = [
log_rejected_routes(Group, RejectedRoutes, VS)
|| {Group, RejectedRoutes} <- hg_routing_ctx:rejections(Ctx0)
],
Events = produce_routing_events(Ctx1, Revision, St),
{next, {Events, hg_machine_action:set_timeout(0, Action)}};
Error ->
ok = maybe_log_misconfigurations(Error),
ok = log_rejected_routes(all, hg_routing_ctx:rejected_routes(Ctx0), VS),
handle_choose_route_error(Error, [], St, Action)
end.
run_routing_decision_pipeline(Ctx0, VS, St) ->
hg_routing_ctx:pipeline(
Ctx0,
[
fun(Ctx) -> filter_attempted_routes(Ctx, St) end,
%% Since this is routing step then current attempt is not yet
%% accounted for in `St`.
fun(Ctx) -> filter_routes_with_limit_hold(Ctx, VS, get_iter(St) + 1, St) end,
fun(Ctx) -> filter_routes_by_limit_overflow(Ctx, VS, St) end,
fun hg_routing:filter_by_critical_provider_status/1,
fun hg_routing:choose_route_with_ctx/1
]
).
produce_routing_events(Ctx = #{error := Error}, _Revision, St) when Error =/= undefined ->
%% TODO Pass failure subcode from error. Say, if last candidates were
%% rejected because of provider gone critical, then use subcode to highlight
%% the offender. Like 'provider_dead' or 'conversion_lacking'.
Failure = genlib:define(St#st.failure, construct_routing_failure(Error)),
InitialCandidates = [hg_route:to_payment_route(R) || R <- hg_routing_ctx:initial_candidates(Ctx)],
Route = hd(InitialCandidates),
Candidates = ordsets:from_list(InitialCandidates),
%% For protocol compatability we set choosen route in route_changed event.
%% It doesn't influence cash_flow building because this step will be
%% skipped. And all limit's 'hold' operations will be rolled back.
%% For same purpose in cascade routing we use route from unfiltered list of
%% originally resolved candidates.
[?route_changed(Route, Candidates), ?payment_rollback_started(Failure)];
produce_routing_events(Ctx, Revision, _St) ->
ok = log_route_choice_meta(Ctx, Revision),
Route = hg_route:to_payment_route(hg_routing_ctx:choosen_route(Ctx)),
Candidates = ordsets:from_list([hg_route:to_payment_route(R) || R <- hg_routing_ctx:candidates(Ctx)]),
[?route_changed(Route, Candidates)].
route_args(St) ->
Opts = get_opts(St),
Revision = get_payment_revision(St),
@ -2010,60 +2004,54 @@ route_args(St) ->
PaymentInstitution = hg_payment_institution:compute_payment_institution(PaymentInstitutionRef, VS1, Revision),
{PaymentInstitution, VS3, Revision}.
get_candidates(PaymentInstitution, VS, Revision, St) ->
build_routing_context(PaymentInstitution, VS, Revision, St) ->
Payer = get_payment_payer(St),
case get_predefined_route(Payer) of
{ok, PaymentRoute} ->
[hg_routing:from_payment_route(PaymentRoute)];
hg_routing_ctx:new([hg_route:from_payment_route(PaymentRoute)]);
undefined ->
gather_routes(PaymentInstitution, VS, Revision, St)
end.
filter_out_attempted_routes(Routes, #st{routes = AttemptedRoutes}) ->
lists:filter(
fun(Route) -> not lists:member(hg_routing:to_payment_route(Route), AttemptedRoutes) end,
Routes
filter_attempted_routes(Ctx, #st{routes = AttemptedRoutes}) ->
lists:foldr(
fun(R, C) ->
R1 = hg_route:from_payment_route(R),
R2 = hg_route:to_rejected_route(R1, {'AlreadyAttempted', undefined}),
hg_routing_ctx:reject(already_attempted, R2, C)
end,
Ctx,
AttemptedRoutes
).
handle_gathered_route_result({ok, RoutesNoOverflow}, _Routes, CandidateRoutes, Revision, _St) ->
{ChoosenRoute, ChoiceContext} = hg_routing:choose_route(RoutesNoOverflow),
_ = log_route_choice_meta(ChoiceContext, Revision),
[?route_changed(hg_routing:to_payment_route(ChoosenRoute), ordsets:from_list(CandidateRoutes))];
handle_gathered_route_result({error, _Reason}, Routes, CandidateRoutes, _Revision, #st{failure = Failure}) when
Failure =/= undefined
->
%% Pass original failure if it is set
handle_gathered_route_result_(Routes, CandidateRoutes, Failure);
handle_gathered_route_result({error, {not_found, RejectedRoutes}}, Routes, CandidateRoutes, _Revision, _St) ->
Failure = construct_routing_failure(forbidden, genlib:format({rejected_routes, RejectedRoutes})),
handle_gathered_route_result_(Routes, CandidateRoutes, Failure).
handle_gathered_route_result_(Routes, CandidateRoutes, Failure) ->
[Route | _] = Routes,
%% For protocol compatability we set choosen route in route_changed event.
%% It doesn't influence cash_flow building because this step will be skipped. And all limit's 'hold' operations
%% will be rolled back.
%% For same purpose in cascade routing we use route form unfiltered list of originally resolved candidates.
[?route_changed(Route, ordsets:from_list(CandidateRoutes)), ?payment_rollback_started(Failure)].
handle_choose_route_error({rejected_routes, _RejectedRoutes} = Reason, Events, St, Action) ->
do_handle_routing_error(unknown, genlib:format(Reason), Events, St, Action);
handle_choose_route_error({misconfiguration, _Details} = Reason, Events, St, Action) ->
do_handle_routing_error(unknown, genlib:format(Reason), Events, St, Action);
handle_choose_route_error(Reason, Events, St, Action) when is_atom(Reason) ->
do_handle_routing_error(Reason, undefined, Events, St, Action).
do_handle_routing_error(SubCode, Reason, Events, St, Action) ->
Failure = construct_routing_failure(SubCode, Reason),
handle_choose_route_error(Error, Events, St, Action) ->
Failure = construct_routing_failure(Error),
process_failure(get_activity(St), Events, Action, Failure, St).
construct_routing_failure(SubCode, Reason) ->
{failure,
payproc_errors:construct(
'PaymentFailure',
{no_route_found, {SubCode, #payproc_error_GeneralFailure{}}},
Reason
)}.
%% NOTE See damsel payproc errors (proto/payment_processing_errors.thrift) for no route found
construct_routing_failure({rejected_routes, {forbidden, RejectedRoutes}}) ->
construct_routing_failure([forbidden], genlib:format(RejectedRoutes));
construct_routing_failure({rejected_routes, {SubCode, RejectedRoutes}}) when
SubCode =:= limit_misconfiguration orelse
SubCode =:= limit_overflow orelse
SubCode =:= adapter_unavailable orelse
SubCode =:= provider_conversion_is_too_low
->
construct_routing_failure([rejected, SubCode], genlib:format(RejectedRoutes));
construct_routing_failure({misconfiguration = Code, Details}) ->
construct_routing_failure([unknown, {unknown_error, atom_to_binary(Code)}], genlib:format(Details));
construct_routing_failure(Code = risk_score_is_too_high) ->
construct_routing_failure([Code], undefined);
construct_routing_failure(Error) when is_atom(Error) ->
construct_routing_failure([{unknown_error, Error}], undefined).
construct_routing_failure(Codes, Reason) ->
{failure, payproc_errors:construct('PaymentFailure', mk_static_error([no_route_found | Codes]), Reason)}.
mk_static_error([_ | _] = Codes) -> mk_static_error_(#payproc_error_GeneralFailure{}, lists:reverse(Codes)).
mk_static_error_(T, []) -> T;
mk_static_error_(Sub, [Code | Codes]) -> mk_static_error_({Code, Sub}, Codes).
-spec process_cash_flow_building(action(), st()) -> machine_result().
process_cash_flow_building(Action, St) ->
@ -2280,7 +2268,7 @@ process_result({payment, processing_accounter}, Action, St0 = #st{new_cash = Cos
FinalCashflow = calculate_cashflow(Context, Opts),
%% Hold limits (only for chosen route) for new cashflow
{_PaymentInstitution, RouteVS, _Revision} = route_args(St1),
Routes = [hg_routing:from_payment_route(Route)],
Routes = [hg_route:from_payment_route(Route)],
_ = hold_limit_routes(Routes, RouteVS, get_iter(St1), St1),
%% Hold cashflow
St2 = St1#st{new_cash_flow = FinalCashflow},
@ -2487,43 +2475,45 @@ get_provider_terms(St, Revision) ->
VS1 = collect_validation_varset(get_party(Opts), get_shop(Opts), Payment, VS0),
hg_routing:get_payment_terms(Route, VS1, Revision).
filter_limit_overflow_routes(Routes, VS, Iter, St) ->
{UsableRoutes, HoldRejectedRoutes} = hold_limit_routes(Routes, VS, Iter, St),
case get_limit_overflow_routes(UsableRoutes, VS, St) of
{[], RejectedRoutesOut} ->
{error, {not_found, RejectedRoutesOut ++ HoldRejectedRoutes}};
{RoutesNoOverflow, _} ->
{ok, RoutesNoOverflow}
end.
filter_routes_with_limit_hold(Ctx, VS, Iter, St) ->
{_Routes, RejectedRoutes} = hold_limit_routes(hg_routing_ctx:candidates(Ctx), VS, Iter, St),
reject_routes(limit_misconfiguration, RejectedRoutes, Ctx).
filter_routes_by_limit_overflow(Ctx, VS, St) ->
{_Routes, RejectedRoutes} = get_limit_overflow_routes(hg_routing_ctx:candidates(Ctx), VS, St),
reject_routes(limit_overflow, RejectedRoutes, Ctx).
reject_routes(GroupReason, RejectedRoutes, Ctx) ->
lists:foldr(
fun(R, C) -> hg_routing_ctx:reject(GroupReason, R, C) end,
Ctx,
RejectedRoutes
).
get_limit_overflow_routes(Routes, VS, St) ->
Opts = get_opts(St),
Revision = get_payment_revision(St),
Payment = get_payment(St),
Invoice = get_invoice(Opts),
{_Routes, RejectedRoutes} =
Result = lists:foldl(
lists:foldl(
fun(Route, {RoutesNoOverflowIn, RejectedIn}) ->
PaymentRoute = hg_routing:to_payment_route(Route),
PaymentRoute = hg_route:to_payment_route(Route),
ProviderTerms = hg_routing:get_payment_terms(PaymentRoute, VS, Revision),
TurnoverLimits = get_turnover_limits(ProviderTerms),
case hg_limiter:check_limits(TurnoverLimits, Invoice, Payment, PaymentRoute) of
{ok, _} ->
{[Route | RoutesNoOverflowIn], RejectedIn};
{error, {limit_overflow, IDs}} ->
RejectedRoute = hg_routing:to_rejected_route(Route, {'LimitOverflow', IDs}),
RejectedRoute = hg_route:to_rejected_route(Route, {'LimitOverflow', IDs}),
{RoutesNoOverflowIn, [RejectedRoute | RejectedIn]}
end
end,
{[], []},
Routes
),
erlang:length(RejectedRoutes) > 0 andalso
log_rejected_routes(limit_overflow_reject, RejectedRoutes, VS),
Result.
).
-spec hold_limit_routes([hg_routing:route()], hg_varset:varset(), pos_integer(), st()) ->
{[hg_routing:route()], [hg_routing:rejected_route()]}.
-spec hold_limit_routes([hg_route:t()], hg_varset:varset(), pos_integer(), st()) ->
{[hg_route:t()], [hg_route:rejected_route()]}.
hold_limit_routes(Routes0, VS, Iter, St) ->
Opts = get_opts(St),
Revision = get_payment_revision(St),
@ -2531,7 +2521,7 @@ hold_limit_routes(Routes0, VS, Iter, St) ->
Invoice = get_invoice(Opts),
{Routes1, Rejected} = lists:foldl(
fun(Route, {LimitHeldRoutes, RejectedRoutes} = Acc) ->
PaymentRoute = hg_routing:to_payment_route(Route),
PaymentRoute = hg_route:to_payment_route(Route),
ProviderTerms = hg_routing:get_payment_terms(PaymentRoute, VS, Revision),
TurnoverLimits = get_turnover_limits(ProviderTerms),
try
@ -2549,13 +2539,11 @@ hold_limit_routes(Routes0, VS, Iter, St) ->
{[], []},
Routes0
),
erlang:length(Rejected) > 0 andalso
log_rejected_routes(limit_hold_reject, Rejected, VS),
{lists:reverse(Routes1), Rejected}.
do_reject_route(LimiterError, Route, TurnoverLimits, {LimitHeldRoutes, RejectedRoutes}) ->
Reason = {'LimitHoldError', [T#domain_TurnoverLimit.id || T <- TurnoverLimits], LimiterError},
RejectedRoute = hg_routing:to_rejected_route(Route, Reason),
LimitsIDs = [T#domain_TurnoverLimit.id || T <- TurnoverLimits],
RejectedRoute = hg_route:to_rejected_route(Route, {'LimitHoldError', LimitsIDs, LimiterError}),
{LimitHeldRoutes, [RejectedRoute | RejectedRoutes]}.
rollback_payment_limits(Routes, Iter, St) ->
@ -3285,12 +3273,9 @@ log_cascade_attempt_context(
#domain_PaymentsServiceTerms{attempt_limit = AttemptLimit},
#st{routes = AttemptedRoutes}
) ->
_ = logger:log(
info,
"Cascade context: merchant payment terms' attempt limit '~p', attempted routes: ~p",
[AttemptLimit, AttemptedRoutes],
logger:get_process_metadata()
).
?LOG_MD(info, "Cascade context: merchant payment terms' attempt limit '~p', attempted routes: ~p", [
AttemptLimit, AttemptedRoutes
]).
get_routing_attempt_limit_value(undefined) ->
1;
@ -3365,19 +3350,19 @@ accrue_status_timing(Name, Opts, #st{timings = Timings}) ->
-spec get_limit_values(st()) -> route_limit_context().
get_limit_values(St) ->
{PaymentInstitution, VS, Revision} = route_args(St),
Routes = get_candidates(PaymentInstitution, VS, Revision, St),
Ctx = build_routing_context(PaymentInstitution, VS, Revision, St),
Payment = get_payment(St),
Invoice = get_invoice(get_opts(St)),
lists:foldl(
fun(Route, Acc) ->
PaymentRoute = hg_routing:to_payment_route(Route),
PaymentRoute = hg_route:to_payment_route(Route),
ProviderTerms = hg_routing:get_payment_terms(PaymentRoute, VS, Revision),
TurnoverLimits = get_turnover_limits(ProviderTerms),
TurnoverLimitValues = hg_limiter:get_limit_values(TurnoverLimits, Invoice, Payment, PaymentRoute),
Acc#{PaymentRoute => TurnoverLimitValues}
end,
#{},
Routes
hg_routing_ctx:candidates(Ctx)
).
-spec get_limit_values(st(), opts()) -> route_limit_context().
@ -3797,24 +3782,24 @@ get_route_cascade_behaviour(Route, Revision) ->
-spec test() -> _.
-spec filter_out_attempted_routes_test_() -> [_].
filter_out_attempted_routes_test_() ->
-spec filter_attempted_routes_test_() -> [_].
filter_attempted_routes_test_() ->
[R1, R2, R3] = [
hg_routing:new(
hg_route:new(
#domain_ProviderRef{id = 171},
#domain_TerminalRef{id = 307},
20,
1000,
#{client_ip => <<127, 0, 0, 1>>}
),
hg_routing:new(
hg_route:new(
#domain_ProviderRef{id = 171},
#domain_TerminalRef{id = 344},
80,
1000,
#{}
),
hg_routing:new(
hg_route:new(
#domain_ProviderRef{id = 162},
#domain_TerminalRef{id = 227},
1,
@ -3823,10 +3808,10 @@ filter_out_attempted_routes_test_() ->
)
],
[
?_assert(
[] ==
filter_out_attempted_routes(
[],
?_assertMatch(
#{candidates := []},
filter_attempted_routes(
hg_routing_ctx:new([]),
#st{
activity = idle,
routes = [
@ -3838,16 +3823,17 @@ filter_out_attempted_routes_test_() ->
}
)
),
?_assert(
[] == filter_out_attempted_routes([], #st{activity = idle, routes = []})
?_assertMatch(
#{candidates := []}, filter_attempted_routes(hg_routing_ctx:new([]), #st{activity = idle, routes = []})
),
?_assert(
[R1, R2, R3] == filter_out_attempted_routes([R1, R2, R3], #st{activity = idle, routes = []})
?_assertMatch(
#{candidates := [R1, R2, R3]},
filter_attempted_routes(hg_routing_ctx:new([R1, R2, R3]), #st{activity = idle, routes = []})
),
?_assert(
[R1, R2] ==
filter_out_attempted_routes(
[R1, R2, R3],
?_assertMatch(
#{candidates := [R1, R2]},
filter_attempted_routes(
hg_routing_ctx:new([R1, R2, R3]),
#st{
activity = idle,
routes = [
@ -3859,10 +3845,10 @@ filter_out_attempted_routes_test_() ->
}
)
),
?_assert(
[] ==
filter_out_attempted_routes(
[R1, R2, R3],
?_assertMatch(
#{candidates := []},
filter_attempted_routes(
hg_routing_ctx:new([R1, R2, R3]),
#st{
activity = idle,
routes = [

View File

@ -11,7 +11,7 @@
-type turnover_limit() :: dmsl_domain_thrift:'TurnoverLimit'().
-type invoice() :: dmsl_domain_thrift:'Invoice'().
-type payment() :: dmsl_domain_thrift:'InvoicePayment'().
-type route() :: hg_routing:payment_route().
-type route() :: hg_route:payment_route().
-type refund() :: hg_invoice_payment:domain_refund().
-type cash() :: dmsl_domain_thrift:'Cash'().
-type handling_flag() :: ignore_business_error.

View File

@ -20,7 +20,7 @@
%%
-type trx_info() :: hg_invoice_payment:trx_info().
-type route() :: hg_routing:payment_route().
-type route() :: hg_route:payment_route().
-type change() :: dmsl_payproc_thrift:'SessionChangePayload'().
-type proxy_state() :: dmsl_base_thrift:'Opaque'().

View File

@ -51,7 +51,7 @@
-type rec_payment_tool_change() :: dmsl_payproc_thrift:'RecurrentPaymentToolChange'().
-type rec_payment_tool_params() :: dmsl_payproc_thrift:'RecurrentPaymentToolParams'().
-type route() :: hg_routing:payment_route().
-type route() :: hg_route:payment_route().
-type risk_score() :: hg_inspector:risk_score().
-type shop() :: dmsl_domain_thrift:'Shop'().
-type party() :: dmsl_domain_thrift:'Party'().
@ -251,7 +251,7 @@ init(EncodedParams, #{id := RecPaymentToolID}) ->
client_ip => get_client_info_ip(Params#payproc_RecurrentPaymentToolParams.payment_resource)
}),
{ChosenRoute, ChoiceContext} = hg_routing:choose_route(NonFailRatedRoutes),
ChosenPaymentRoute = hg_routing:to_payment_route(ChosenRoute),
ChosenPaymentRoute = hg_route:to_payment_route(ChosenRoute),
LoggerMetadata = hg_routing:get_logger_metadata(ChoiceContext, Revision),
_ = logger:log(info, "Routing decision made", #{routing => LoggerMetadata}),
RecPaymentTool2 = set_minimal_payment_cost(RecPaymentTool, ChosenPaymentRoute, VS, Revision),
@ -274,20 +274,13 @@ init(EncodedParams, #{id := RecPaymentToolID}) ->
gather_routes(PaymentInstitution, VS, Revision, Ctx) ->
Predestination = recurrent_paytool,
case
hg_routing:gather_routes(
Predestination,
PaymentInstitution,
VS,
Revision,
Ctx
)
of
{ok, {[], RejectedRoutes}} ->
throw({no_route_found, {unknown, RejectedRoutes}});
{ok, {Routes, _RejectContext}} ->
RoutingCtx = hg_routing:gather_routes(Predestination, PaymentInstitution, VS, Revision, Ctx),
case {hg_routing_ctx:candidates(RoutingCtx), hg_routing_ctx:error(RoutingCtx)} of
{[], undefined} ->
throw({no_route_found, {unknown, hg_routing_ctx:rejected_routes(RoutingCtx)}});
{Routes, undefined} ->
Routes;
{error, {misconfiguration, _Reason}} ->
{_Routes, {misconfiguration, _Reason}} ->
throw({no_route_found, misconfiguration})
end.

View File

@ -0,0 +1,191 @@
-module(hg_route).
-export([new/2]).
-export([new/4]).
-export([new/5]).
-export([new/6]).
-export([provider_ref/1]).
-export([terminal_ref/1]).
-export([priority/1]).
-export([weight/1]).
-export([set_weight/2]).
-export([pin/1]).
-export([fd_overrides/1]).
-export([equal/2]).
-export([from_payment_route/1]).
-export([to_payment_route/1]).
-export([to_rejected_route/2]).
%%
-include("domain.hrl").
-record(route, {
provider_ref :: dmsl_domain_thrift:'ProviderRef'(),
terminal_ref :: dmsl_domain_thrift:'TerminalRef'(),
weight :: integer(),
priority :: integer(),
pin :: pin(),
fd_overrides :: fd_overrides()
}).
-type t() :: #route{}.
-type payment_route() :: dmsl_domain_thrift:'PaymentRoute'().
-type route_rejection_reason() :: {atom(), _DescOrAttrs} | {atom(), _DescOrAttrs1, _DescOrAttrs2}.
-type rejected_route() :: {provider_ref(), terminal_ref(), route_rejection_reason()}.
-type provider_ref() :: dmsl_domain_thrift:'ProviderRef'().
-type terminal_ref() :: dmsl_domain_thrift:'TerminalRef'().
-type fd_overrides() :: dmsl_domain_thrift:'RouteFaultDetectorOverrides'().
-type currency() :: dmsl_domain_thrift:'CurrencyRef'().
-type payment_tool() :: dmsl_domain_thrift:'PaymentTool'().
-type party_id() :: dmsl_domain_thrift:'PartyID'().
-type client_ip() :: dmsl_domain_thrift:'IPAddress'().
-type pin() :: #{
currency => currency(),
payment_tool => payment_tool(),
party_id => party_id(),
client_ip => client_ip() | undefined
}.
-export_type([t/0]).
-export_type([provider_ref/0]).
-export_type([terminal_ref/0]).
-export_type([payment_route/0]).
-export_type([rejected_route/0]).
%%
-spec new(provider_ref(), terminal_ref()) -> t().
new(ProviderRef, TerminalRef) ->
new(
ProviderRef,
TerminalRef,
?DOMAIN_CANDIDATE_WEIGHT,
?DOMAIN_CANDIDATE_PRIORITY
).
-spec new(provider_ref(), terminal_ref(), integer() | undefined, integer()) -> t().
new(ProviderRef, TerminalRef, Weight, Priority) ->
new(ProviderRef, TerminalRef, Weight, Priority, #{}).
-spec new(provider_ref(), terminal_ref(), integer() | undefined, integer(), pin()) -> t().
new(ProviderRef, TerminalRef, undefined, Priority, Pin) ->
new(ProviderRef, TerminalRef, ?DOMAIN_CANDIDATE_WEIGHT, Priority, Pin);
new(ProviderRef, TerminalRef, Weight, Priority, Pin) ->
new(ProviderRef, TerminalRef, Weight, Priority, Pin, #domain_RouteFaultDetectorOverrides{}).
-spec new(provider_ref(), terminal_ref(), integer(), integer(), pin(), fd_overrides() | undefined) -> t().
new(ProviderRef, TerminalRef, Weight, Priority, Pin, undefined) ->
new(ProviderRef, TerminalRef, Weight, Priority, Pin, #domain_RouteFaultDetectorOverrides{});
new(ProviderRef, TerminalRef, Weight, Priority, Pin, FdOverrides) ->
#route{
provider_ref = ProviderRef,
terminal_ref = TerminalRef,
weight = Weight,
priority = Priority,
pin = Pin,
fd_overrides = FdOverrides
}.
-spec provider_ref(t()) -> provider_ref().
provider_ref(#route{provider_ref = Ref}) ->
Ref.
-spec terminal_ref(t()) -> terminal_ref().
terminal_ref(#route{terminal_ref = Ref}) ->
Ref.
-spec priority(t()) -> integer().
priority(#route{priority = Priority}) ->
Priority.
-spec weight(t()) -> integer().
weight(#route{weight = Weight}) ->
Weight.
-spec pin(t()) -> pin() | undefined.
pin(#route{pin = Pin}) ->
Pin.
-spec fd_overrides(t()) -> fd_overrides().
fd_overrides(#route{fd_overrides = FdOverrides}) ->
FdOverrides.
-spec set_weight(integer(), t()) -> t().
set_weight(Weight, Route) ->
Route#route{weight = Weight}.
-spec equal(R, R) -> boolean() when
R :: t() | rejected_route() | payment_route() | {provider_ref(), terminal_ref()}.
equal(A, B) ->
routes_equal_(route_ref(A), route_ref(B)).
%%
-spec from_payment_route(payment_route()) -> t().
from_payment_route(Route) ->
?route(ProviderRef, TerminalRef) = Route,
new(ProviderRef, TerminalRef).
-spec to_payment_route(t()) -> payment_route().
to_payment_route(#route{} = Route) ->
?route(provider_ref(Route), terminal_ref(Route)).
-spec to_rejected_route(t(), route_rejection_reason()) -> rejected_route().
to_rejected_route(Route, Reason) ->
{provider_ref(Route), terminal_ref(Route), Reason}.
%%
routes_equal_(A, A) when A =/= undefined ->
true;
routes_equal_(_A, _B) ->
false.
route_ref(#route{provider_ref = Prv, terminal_ref = Trm}) ->
{Prv, Trm};
route_ref(#domain_PaymentRoute{provider = Prv, terminal = Trm}) ->
{Prv, Trm};
route_ref({Prv, Trm}) ->
{Prv, Trm};
route_ref({Prv, Trm, _RejectionReason}) ->
{Prv, Trm};
route_ref(_) ->
undefined.
%%
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
-define(prv(ID), #domain_ProviderRef{id = ID}).
-define(trm(ID), #domain_TerminalRef{id = ID}).
-spec test() -> _.
-spec routes_equality_test_() -> [_].
routes_equality_test_() ->
lists:flatten([
[?_assert(equal(A, B)) || {A, B} <- route_pairs({?prv(1), ?trm(1)}, {?prv(1), ?trm(1)})],
[?_assertNot(equal(A, B)) || {A, B} <- route_pairs({?prv(1), ?trm(1)}, {?prv(1), ?trm(2)})],
[?_assertNot(equal(A, B)) || {A, B} <- route_pairs({?prv(1), ?trm(1)}, {?prv(2), ?trm(1)})],
[?_assertNot(equal(A, B)) || {A, B} <- route_pairs({?prv(1), ?trm(1)}, {?prv(2), ?trm(2)})]
]).
route_pairs({Prv1, Trm1}, {Prv2, Trm2}) ->
Fs = [
fun(X) -> X end,
fun to_payment_route/1,
fun(X) -> to_rejected_route(X, {test, <<"whatever">>}) end,
fun(X) -> {provider_ref(X), terminal_ref(X)} end
],
A = new(Prv1, Trm1),
B = new(Prv2, Trm2),
lists:flatten([[{F1(A), F2(B)} || F1 <- Fs] || F2 <- Fs]).
-endif.

View File

@ -7,55 +7,30 @@
-include_lib("fault_detector_proto/include/fd_proto_fault_detector_thrift.hrl").
-export([gather_routes/5]).
-export([rate_routes/1]).
-export([choose_route/1]).
-export([choose_rated_route/1]).
-export([get_payment_terms/3]).
-export([get_logger_metadata/2]).
-export([from_payment_route/1]).
-export([new/2]).
-export([new/4]).
-export([new/5]).
-export([new/6]).
-export([to_payment_route/1]).
-export([to_rejected_route/2]).
-export([provider_ref/1]).
-export([terminal_ref/1]).
-export([prepare_log_message/1]).
%%
-export([filter_by_critical_provider_status/1]).
-export([choose_route_with_ctx/1]).
%%
-include("domain.hrl").
-record(route, {
provider_ref :: dmsl_domain_thrift:'ProviderRef'(),
terminal_ref :: dmsl_domain_thrift:'TerminalRef'(),
weight :: integer(),
priority :: integer(),
pin :: pin(),
fd_overrides :: fd_overrides()
}).
-type pin() :: #{
currency => currency(),
payment_tool => payment_tool(),
party_id => party_id(),
client_ip => client_ip() | undefined
}.
-type route() :: #route{}.
-type payment_terms() :: dmsl_domain_thrift:'PaymentsProvisionTerms'().
-type payment_institution() :: dmsl_domain_thrift:'PaymentInstitution'().
-type payment_route() :: dmsl_domain_thrift:'PaymentRoute'().
-type route_predestination() :: payment | recurrent_paytool | recurrent_payment.
-define(rejected(Reason), {rejected, Reason}).
-type rejected_route() :: {provider_ref(), terminal_ref(), Reason :: term()}.
-type provider_ref() :: dmsl_domain_thrift:'ProviderRef'().
-type terminal_ref() :: dmsl_domain_thrift:'TerminalRef'().
-type fd_service_stats() :: fd_proto_fault_detector_thrift:'ServiceStatistics'().
-type terminal_priority_rating() :: integer().
@ -75,13 +50,13 @@
-type route_groups_by_priority() :: #{{availability_condition(), terminal_priority_rating()} => [fail_rated_route()]}.
-type fail_rated_route() :: {route(), provider_status()}.
-type fail_rated_route() :: {hg_route:t(), provider_status()}.
-type scored_route() :: {route_scores(), route()}.
-type scored_route() :: {route_scores(), hg_route:t()}.
-type route_choice_context() :: #{
chosen_route => route(),
preferable_route => route(),
chosen_route => hg_route:t(),
preferable_route => hg_route:t(),
% Contains one of the field names defined in #route_scores{}
reject_reason => atom()
}.
@ -100,7 +75,6 @@
-type varset() :: hg_varset:varset().
-type revision() :: hg_domain:revision().
-type fd_overrides() :: dmsl_domain_thrift:'RouteFaultDetectorOverrides'().
-record(route_scores, {
availability_condition :: condition_score(),
@ -115,84 +89,56 @@
-type route_scores() :: #route_scores{}.
-type misconfiguration_error() :: {misconfiguration, {routing_decisions, _} | {routing_candidate, _}}.
-export_type([route/0]).
-export_type([payment_route/0]).
-export_type([rejected_route/0]).
-export_type([route_predestination/0]).
-export_type([route_choice_context/0]).
-export_type([fail_rated_route/0]).
%% Route accessors
%%
-spec new(provider_ref(), terminal_ref()) -> route().
new(ProviderRef, TerminalRef) ->
new(
ProviderRef,
TerminalRef,
?DOMAIN_CANDIDATE_WEIGHT,
?DOMAIN_CANDIDATE_PRIORITY
-spec filter_by_critical_provider_status(T) -> T when T :: hg_routing_ctx:t().
filter_by_critical_provider_status(Ctx) ->
RoutesFailRates = rate_routes(hg_routing_ctx:candidates(Ctx)),
lists:foldr(
fun
({R, {{dead, _} = AvailabilityStatus, _ConversionStatus}}, C) ->
R1 = hg_route:to_rejected_route(R, {'ProviderDead', AvailabilityStatus}),
hg_routing_ctx:reject(adapter_unavailable, R1, C);
({R, {_AvailabitlyStatus, ConversionStatus = {lacking, _}}}, C) ->
R1 = hg_route:to_rejected_route(R, {'ConversionLacking', ConversionStatus}),
hg_routing_ctx:reject(provider_conversion_is_too_low, R1, C);
({_R, _ProviderStatus}, C) ->
C
end,
hg_routing_ctx:with_fail_rates(RoutesFailRates, Ctx),
RoutesFailRates
).
-spec new(provider_ref(), terminal_ref(), integer() | undefined, integer()) -> route().
new(ProviderRef, TerminalRef, Weight, Priority) ->
new(ProviderRef, TerminalRef, Weight, Priority, #{}).
-spec choose_route_with_ctx(T) -> T when T :: hg_routing_ctx:t().
choose_route_with_ctx(Ctx) ->
Candidates = hg_routing_ctx:candidates(Ctx),
{ChoosenRoute, ChoiceContext} =
case hg_routing_ctx:fail_rates(Ctx) of
undefined ->
choose_route(Candidates);
FailRates ->
RatedCandidates = filter_rated_routes_with_candidates(FailRates, Candidates),
choose_rated_route(RatedCandidates)
end,
hg_routing_ctx:set_choosen(ChoosenRoute, ChoiceContext, Ctx).
-spec new(provider_ref(), terminal_ref(), integer() | undefined, integer(), pin()) -> route().
new(ProviderRef, TerminalRef, undefined, Priority, Pin) ->
new(ProviderRef, TerminalRef, ?DOMAIN_CANDIDATE_WEIGHT, Priority, Pin);
new(ProviderRef, TerminalRef, Weight, Priority, Pin) ->
new(ProviderRef, TerminalRef, Weight, Priority, Pin, #domain_RouteFaultDetectorOverrides{}).
filter_rated_routes_with_candidates(FailRates, Candidates) ->
lists:foldr(
fun({R, _PS} = FR, Res) ->
case lists:any(fun(CR) -> hg_route:equal(CR, R) end, Candidates) of
true -> [FR | Res];
_Else -> Res
end
end,
[],
FailRates
).
-spec new(provider_ref(), terminal_ref(), integer(), integer(), pin(), fd_overrides() | undefined) -> route().
new(ProviderRef, TerminalRef, Weight, Priority, Pin, undefined) ->
new(ProviderRef, TerminalRef, Weight, Priority, Pin, #domain_RouteFaultDetectorOverrides{});
new(ProviderRef, TerminalRef, Weight, Priority, Pin, FdOverrides) ->
#route{
provider_ref = ProviderRef,
terminal_ref = TerminalRef,
weight = Weight,
priority = Priority,
pin = Pin,
fd_overrides = FdOverrides
}.
-spec provider_ref(route()) -> provider_ref().
provider_ref(#route{provider_ref = Ref}) ->
Ref.
-spec terminal_ref(route()) -> terminal_ref().
terminal_ref(#route{terminal_ref = Ref}) ->
Ref.
-spec priority(route()) -> integer().
priority(#route{priority = Priority}) ->
Priority.
-spec weight(route()) -> integer().
weight(#route{weight = Weight}) ->
Weight.
-spec pin(route()) -> pin() | undefined.
pin(#route{pin = Pin}) ->
Pin.
fd_overrides(#route{fd_overrides = FdOverrides}) ->
FdOverrides.
-spec from_payment_route(payment_route()) -> route().
from_payment_route(Route) ->
?route(ProviderRef, TerminalRef) = Route,
new(ProviderRef, TerminalRef).
-spec to_payment_route(route()) -> payment_route().
to_payment_route(#route{} = Route) ->
?route(provider_ref(Route), terminal_ref(Route)).
-spec to_rejected_route(route(), term()) -> rejected_route().
to_rejected_route(Route, Reason) ->
{provider_ref(Route), terminal_ref(Route), Reason}.
-spec set_weight(integer(), route()) -> route().
set_weight(Weight, Route) ->
Route#route{weight = Weight}.
%%
-spec prepare_log_message(misconfiguration_error()) -> {io:format(), [term()]}.
prepare_log_message({misconfiguration, {routing_decisions, Details}}) ->
@ -202,17 +148,10 @@ prepare_log_message({misconfiguration, {routing_candidate, Candidate}}) ->
%%
-spec gather_routes(
route_predestination(),
payment_institution(),
varset(),
revision(),
gather_route_context()
) ->
{ok, {[route()], [rejected_route()]}}
| {error, misconfiguration_error()}.
-spec gather_routes(route_predestination(), payment_institution(), varset(), revision(), gather_route_context()) ->
hg_routing_ctx:t().
gather_routes(_, #domain_PaymentInstitution{payment_routing_rules = undefined}, _, _, _) ->
{ok, {[], []}};
hg_routing_ctx:new([]);
gather_routes(Predestination, #domain_PaymentInstitution{payment_routing_rules = RoutingRules}, VS, Revision, Ctx) ->
#domain_RoutingRules{
policies = Policies,
@ -224,10 +163,14 @@ gather_routes(Predestination, #domain_PaymentInstitution{payment_routing_rules =
collect_routes(Predestination, Candidates, VS, Revision, Ctx),
get_table_prohibitions(Prohibitions, VS, Revision)
),
{ok, {Accepted, RejectedRoutes}}
lists:foldr(
fun(R, C) -> hg_routing_ctx:reject(forbidden, R, C) end,
hg_routing_ctx:new(Accepted),
lists:reverse(RejectedRoutes)
)
catch
throw:{misconfiguration, _Reason} = Error ->
{error, Error}
hg_routing_ctx:set_error(Error, hg_routing_ctx:new([]))
end.
get_table_prohibitions(Prohibitions, VS, Revision) ->
@ -270,7 +213,8 @@ collect_routes(Predestination, Candidates, VS, Revision, Ctx) ->
weight = Weight,
pin = Pin
} = Candidate,
% Looks like overhead, we got Terminal only for provider_ref. Maybe we can remove provider_ref from route().
% Looks like overhead, we got Terminal only for provider_ref. Maybe
% we can remove provider_ref from hg_route:t().
% https://github.com/rbkmoney/hellgate/pull/583#discussion_r682745123
#domain_Terminal{
provider_ref = ProviderRef,
@ -279,7 +223,7 @@ collect_routes(Predestination, Candidates, VS, Revision, Ctx) ->
GatheredPinInfo = gather_pin_info(Pin, Ctx),
try
true = acceptable_terminal(Predestination, ProviderRef, TerminalRef, VS, Revision),
Route = new(ProviderRef, TerminalRef, Weight, Priority, GatheredPinInfo, FdOverrides),
Route = hg_route:new(ProviderRef, TerminalRef, Weight, Priority, GatheredPinInfo, FdOverrides),
{[Route | Accepted], Rejected}
catch
{rejected, Reason} ->
@ -307,13 +251,12 @@ gather_pin_info(#domain_RoutingPin{features = Features}, Ctx) ->
filter_routes({Routes, Rejected}, Prohibitions) ->
lists:foldr(
fun(Route, {AccIn, RejectedIn}) ->
TRef = terminal_ref(Route),
TRef = hg_route:terminal_ref(Route),
case maps:find(TRef, Prohibitions) of
error ->
{[Route | AccIn], RejectedIn};
{ok, Description} ->
PRef = provider_ref(Route),
RejectedOut = [{PRef, TRef, {'RoutingRule', Description}} | RejectedIn],
RejectedOut = [hg_route:to_rejected_route(Route, {'RoutingRule', Description}) | RejectedIn],
{AccIn, RejectedOut}
end
end,
@ -332,16 +275,16 @@ compute_rule_set(RuleSetRef, VS, Revision) ->
),
RuleSet.
-spec gather_fail_rates([route()]) -> [fail_rated_route()].
gather_fail_rates(Routes) ->
-spec rate_routes([hg_route:t()]) -> [fail_rated_route()].
rate_routes(Routes) ->
score_routes_with_fault_detector(Routes).
-spec choose_route([route()]) -> {route(), route_choice_context()}.
-spec choose_route([hg_route:t()]) -> {hg_route:t(), route_choice_context()}.
choose_route(Routes) ->
FailRatedRoutes = gather_fail_rates(Routes),
FailRatedRoutes = rate_routes(Routes),
choose_rated_route(FailRatedRoutes).
-spec choose_rated_route([fail_rated_route()]) -> {route(), route_choice_context()}.
-spec choose_rated_route([fail_rated_route()]) -> {hg_route:t(), route_choice_context()}.
choose_rated_route(FailRatedRoutes) ->
BalancedRoutes = balance_routes(FailRatedRoutes),
ScoredRoutes = score_routes(BalancedRoutes),
@ -413,15 +356,15 @@ format_logger_metadata(Meta, Route, Revision) when
Meta =:= chosen_route;
Meta =:= preferable_route
->
ProviderRef = #domain_ProviderRef{id = ProviderID} = provider_ref(Route),
TerminalRef = #domain_TerminalRef{id = TerminalID} = terminal_ref(Route),
ProviderRef = #domain_ProviderRef{id = ProviderID} = hg_route:provider_ref(Route),
TerminalRef = #domain_TerminalRef{id = TerminalID} = hg_route:terminal_ref(Route),
#domain_Provider{name = ProviderName} = hg_domain:get(Revision, {provider, ProviderRef}),
#domain_Terminal{name = TerminalName} = hg_domain:get(Revision, {terminal, TerminalRef}),
genlib_map:compact(#{
provider => #{id => ProviderID, name => ProviderName},
terminal => #{id => TerminalID, name => TerminalName},
priority => priority(Route),
weight => weight(Route)
priority => hg_route:priority(Route),
weight => hg_route:weight(Route)
}).
map_route_switch_reason(SameScores, SameScores) ->
@ -452,7 +395,7 @@ balance_routes(FailRatedRoutes) ->
-spec group_routes_by_priority(fail_rated_route(), Acc :: route_groups_by_priority()) -> route_groups_by_priority().
group_routes_by_priority(FailRatedRoute = {Route, {ProviderCondition, _}}, SortedRoutes) ->
TerminalPriority = priority(Route),
TerminalPriority = hg_route:priority(Route),
Key = {ProviderCondition, TerminalPriority},
Routes = maps:get(Key, SortedRoutes, []),
SortedRoutes#{Key => [FailRatedRoute | Routes]}.
@ -476,7 +419,7 @@ set_routes_random_condition(Routes) ->
get_summary_weight(FailRatedRoutes) ->
lists:foldl(
fun({Route, _}, Acc) ->
Weight = weight(Route),
Weight = hg_route:weight(Route),
Acc + Weight
end,
0,
@ -487,14 +430,14 @@ calc_random_condition(_, _, [], Routes) ->
Routes;
calc_random_condition(StartFrom, Random, [FailRatedRoute | Rest], Routes) ->
{Route, Status} = FailRatedRoute,
Weight = weight(Route),
Weight = hg_route:weight(Route),
InRange = (Random >= StartFrom) and (Random < StartFrom + Weight),
case InRange of
true ->
NewRoute = set_weight(1, Route),
NewRoute = hg_route:set_weight(1, Route),
calc_random_condition(StartFrom + Weight, Random, Rest, [{NewRoute, Status} | Routes]);
false ->
NewRoute = set_weight(0, Route),
NewRoute = hg_route:set_weight(0, Route),
calc_random_condition(StartFrom + Weight, Random, Rest, [{NewRoute, Status} | Routes])
end.
@ -503,9 +446,9 @@ score_routes(Routes) ->
[{score_route(FailRatedRoute), Route} || {Route, _} = FailRatedRoute <- Routes].
score_route({Route, ProviderStatus}) ->
PriorityRate = priority(Route),
RandomCondition = weight(Route),
Pin = pin(Route),
PriorityRate = hg_route:priority(Route),
RandomCondition = hg_route:weight(Route),
Pin = hg_route:pin(Route),
PinHash = erlang:phash2(Pin),
{AvailabilityStatus, ConversionStatus} = ProviderStatus,
{AvailabilityCondition, Availability} = get_availability_score(AvailabilityStatus),
@ -526,7 +469,7 @@ get_availability_score({dead, FailRate}) -> {0, 1.0 - FailRate}.
get_conversion_score({normal, FailRate}) -> {1, 1.0 - FailRate};
get_conversion_score({lacking, FailRate}) -> {0, 1.0 - FailRate}.
-spec score_routes_with_fault_detector([route()]) -> [fail_rated_route()].
-spec score_routes_with_fault_detector([hg_route:t()]) -> [fail_rated_route()].
score_routes_with_fault_detector([]) ->
[];
score_routes_with_fault_detector(Routes) ->
@ -534,20 +477,19 @@ score_routes_with_fault_detector(Routes) ->
FDStats = hg_fault_detector_client:get_statistics(IDs),
[{R, get_provider_status(R, FDStats)} || R <- Routes].
-spec get_provider_status(route(), [fd_service_stats()]) -> provider_status().
-spec get_provider_status(hg_route:t(), [fd_service_stats()]) -> provider_status().
get_provider_status(Route, FDStats) ->
ProviderRef = provider_ref(Route),
FdOverrides = fd_overrides(Route),
ProviderRef = hg_route:provider_ref(Route),
FdOverrides = hg_route:fd_overrides(Route),
AvailabilityServiceID = build_fd_availability_service_id(ProviderRef),
ConversionServiceID = build_fd_conversion_service_id(ProviderRef),
AvailabilityStatus = get_provider_availability_status(FdOverrides, AvailabilityServiceID, FDStats),
AvailabilityStatus = get_adapter_availability_status(FdOverrides, AvailabilityServiceID, FDStats),
ConversionStatus = get_provider_conversion_status(FdOverrides, ConversionServiceID, FDStats),
{AvailabilityStatus, ConversionStatus}.
get_provider_availability_status(#domain_RouteFaultDetectorOverrides{enabled = true}, _FDID, _Stats) ->
get_adapter_availability_status(#domain_RouteFaultDetectorOverrides{enabled = true}, _FDID, _Stats) ->
%% ignore fd statistic if set override
{alive, 0.0};
get_provider_availability_status(_, FDID, Stats) ->
get_adapter_availability_status(_, FDID, Stats) ->
AvailabilityConfig = maps:get(availability, genlib_app:env(hellgate, fault_detector, #{}), #{}),
CriticalFailRate = maps:get(critical_fail_rate, AvailabilityConfig, 0.7),
case lists:keysearch(FDID, #fault_detector_ServiceStatistics.service_id, Stats) of
@ -578,7 +520,7 @@ build_ids(Routes) ->
lists:foldl(fun build_fd_ids/2, [], Routes).
build_fd_ids(Route, IDs) ->
ProviderRef = provider_ref(Route),
ProviderRef = hg_route:provider_ref(Route),
AvailabilityID = build_fd_availability_service_id(ProviderRef),
ConversionID = build_fd_conversion_service_id(ProviderRef),
[AvailabilityID, ConversionID | IDs].
@ -589,7 +531,7 @@ build_fd_availability_service_id(#domain_ProviderRef{id = ID}) ->
build_fd_conversion_service_id(#domain_ProviderRef{id = ID}) ->
hg_fault_detector_client:build_service_id(provider_conversion, ID).
-spec get_payment_terms(payment_route(), varset(), revision()) -> payment_terms() | undefined.
-spec get_payment_terms(hg_route:payment_route(), varset(), revision()) -> payment_terms() | undefined.
get_payment_terms(?route(ProviderRef, TerminalRef), VS, Revision) ->
PreparedVS = hg_varset:prepare_varset(VS),
{Client, Context} = get_party_client(),
@ -605,8 +547,8 @@ get_payment_terms(?route(ProviderRef, TerminalRef), VS, Revision) ->
-spec acceptable_terminal(
route_predestination(),
provider_ref(),
terminal_ref(),
hg_route:provider_ref(),
hg_route:terminal_ref(),
varset(),
revision()
) -> true | no_return().
@ -732,7 +674,7 @@ acceptable_allow(_ParentName, _Type, {constant, true}) ->
acceptable_allow(ParentName, Type, {constant, false}) ->
throw(?rejected({ParentName, Type}));
acceptable_allow(_ParentName, Type, Ambiguous) ->
error({misconfiguration, {'Could not reduce predicate to a value', {Type, Ambiguous}}}).
erlang:error({misconfiguration, {'Could not reduce predicate to a value', {Type, Ambiguous}}}).
%%
@ -773,7 +715,7 @@ get_selector_value(Name, Selector) ->
{value, V} ->
V;
Ambiguous ->
error({misconfiguration, {'Could not reduce selector to a value', {Name, Ambiguous}}})
erlang:error({misconfiguration, {'Could not reduce selector to a value', {Name, Ambiguous}}})
end.
getv(Name, VS) ->
@ -826,33 +768,33 @@ record_comparsion_test() ->
balance_routes_test_() ->
Status = {{alive, 0.0}, {normal, 0.0}},
WithWeight = [
{new(?prv(1), ?trm(1), 1, ?DOMAIN_CANDIDATE_PRIORITY), Status},
{new(?prv(2), ?trm(1), 2, ?DOMAIN_CANDIDATE_PRIORITY), Status},
{new(?prv(3), ?trm(1), 0, ?DOMAIN_CANDIDATE_PRIORITY), Status},
{new(?prv(4), ?trm(1), 1, ?DOMAIN_CANDIDATE_PRIORITY), Status},
{new(?prv(5), ?trm(1), 0, ?DOMAIN_CANDIDATE_PRIORITY), Status}
{hg_route:new(?prv(1), ?trm(1), 1, ?DOMAIN_CANDIDATE_PRIORITY), Status},
{hg_route:new(?prv(2), ?trm(1), 2, ?DOMAIN_CANDIDATE_PRIORITY), Status},
{hg_route:new(?prv(3), ?trm(1), 0, ?DOMAIN_CANDIDATE_PRIORITY), Status},
{hg_route:new(?prv(4), ?trm(1), 1, ?DOMAIN_CANDIDATE_PRIORITY), Status},
{hg_route:new(?prv(5), ?trm(1), 0, ?DOMAIN_CANDIDATE_PRIORITY), Status}
],
Result1 = [
{new(?prv(1), ?trm(1), 1, ?DOMAIN_CANDIDATE_PRIORITY), Status},
{new(?prv(2), ?trm(1), 0, ?DOMAIN_CANDIDATE_PRIORITY), Status},
{new(?prv(3), ?trm(1), 0, ?DOMAIN_CANDIDATE_PRIORITY), Status},
{new(?prv(4), ?trm(1), 0, ?DOMAIN_CANDIDATE_PRIORITY), Status},
{new(?prv(5), ?trm(1), 0, ?DOMAIN_CANDIDATE_PRIORITY), Status}
{hg_route:new(?prv(1), ?trm(1), 1, ?DOMAIN_CANDIDATE_PRIORITY), Status},
{hg_route:new(?prv(2), ?trm(1), 0, ?DOMAIN_CANDIDATE_PRIORITY), Status},
{hg_route:new(?prv(3), ?trm(1), 0, ?DOMAIN_CANDIDATE_PRIORITY), Status},
{hg_route:new(?prv(4), ?trm(1), 0, ?DOMAIN_CANDIDATE_PRIORITY), Status},
{hg_route:new(?prv(5), ?trm(1), 0, ?DOMAIN_CANDIDATE_PRIORITY), Status}
],
Result2 = [
{new(?prv(1), ?trm(1), 0, ?DOMAIN_CANDIDATE_PRIORITY), Status},
{new(?prv(2), ?trm(1), 1, ?DOMAIN_CANDIDATE_PRIORITY), Status},
{new(?prv(3), ?trm(1), 0, ?DOMAIN_CANDIDATE_PRIORITY), Status},
{new(?prv(4), ?trm(1), 0, ?DOMAIN_CANDIDATE_PRIORITY), Status},
{new(?prv(5), ?trm(1), 0, ?DOMAIN_CANDIDATE_PRIORITY), Status}
{hg_route:new(?prv(1), ?trm(1), 0, ?DOMAIN_CANDIDATE_PRIORITY), Status},
{hg_route:new(?prv(2), ?trm(1), 1, ?DOMAIN_CANDIDATE_PRIORITY), Status},
{hg_route:new(?prv(3), ?trm(1), 0, ?DOMAIN_CANDIDATE_PRIORITY), Status},
{hg_route:new(?prv(4), ?trm(1), 0, ?DOMAIN_CANDIDATE_PRIORITY), Status},
{hg_route:new(?prv(5), ?trm(1), 0, ?DOMAIN_CANDIDATE_PRIORITY), Status}
],
Result3 = [
{new(?prv(1), ?trm(1), 0, ?DOMAIN_CANDIDATE_PRIORITY), Status},
{new(?prv(2), ?trm(1), 0, ?DOMAIN_CANDIDATE_PRIORITY), Status},
{new(?prv(3), ?trm(1), 0, ?DOMAIN_CANDIDATE_PRIORITY), Status},
{new(?prv(4), ?trm(1), 0, ?DOMAIN_CANDIDATE_PRIORITY), Status},
{new(?prv(5), ?trm(1), 0, ?DOMAIN_CANDIDATE_PRIORITY), Status}
{hg_route:new(?prv(1), ?trm(1), 0, ?DOMAIN_CANDIDATE_PRIORITY), Status},
{hg_route:new(?prv(2), ?trm(1), 0, ?DOMAIN_CANDIDATE_PRIORITY), Status},
{hg_route:new(?prv(3), ?trm(1), 0, ?DOMAIN_CANDIDATE_PRIORITY), Status},
{hg_route:new(?prv(4), ?trm(1), 0, ?DOMAIN_CANDIDATE_PRIORITY), Status},
{hg_route:new(?prv(5), ?trm(1), 0, ?DOMAIN_CANDIDATE_PRIORITY), Status}
],
[
?_assertEqual(Result1, lists:reverse(calc_random_condition(0.0, 0.2, WithWeight, []))),
@ -864,12 +806,12 @@ balance_routes_test_() ->
balance_routes_with_default_weight_test_() ->
Status = {{alive, 0.0}, {normal, 0.0}},
Routes = [
{new(?prv(1), ?trm(1), 0, ?DOMAIN_CANDIDATE_PRIORITY), Status},
{new(?prv(2), ?trm(1), 0, ?DOMAIN_CANDIDATE_PRIORITY), Status}
{hg_route:new(?prv(1), ?trm(1), 0, ?DOMAIN_CANDIDATE_PRIORITY), Status},
{hg_route:new(?prv(2), ?trm(1), 0, ?DOMAIN_CANDIDATE_PRIORITY), Status}
],
Result = [
{new(?prv(1), ?trm(1), 0, ?DOMAIN_CANDIDATE_PRIORITY), Status},
{new(?prv(2), ?trm(1), 0, ?DOMAIN_CANDIDATE_PRIORITY), Status}
{hg_route:new(?prv(1), ?trm(1), 0, ?DOMAIN_CANDIDATE_PRIORITY), Status},
{hg_route:new(?prv(2), ?trm(1), 0, ?DOMAIN_CANDIDATE_PRIORITY), Status}
],
?_assertEqual(Result, set_routes_random_condition(Routes)).
@ -880,10 +822,10 @@ preferable_route_scoring_test_() ->
StatusDead = {{dead, 0.4}, {lacking, 0.6}},
StatusDegraded = {{alive, 0.1}, {normal, 0.1}},
StatusBroken = {{alive, 0.1}, {lacking, 0.8}},
RoutePreferred1 = new(?prv(1), ?trm(1), 0, 1),
RoutePreferred2 = new(?prv(1), ?trm(2), 0, 1),
RoutePreferred3 = new(?prv(1), ?trm(3), 0, 1),
RouteFallback = new(?prv(2), ?trm(2), 0, 0),
RoutePreferred1 = hg_route:new(?prv(1), ?trm(1), 0, 1),
RoutePreferred2 = hg_route:new(?prv(1), ?trm(2), 0, 1),
RoutePreferred3 = hg_route:new(?prv(1), ?trm(3), 0, 1),
RouteFallback = hg_route:new(?prv(2), ?trm(2), 0, 0),
[
?_assertMatch(
{RoutePreferred1, #{}},
@ -950,9 +892,9 @@ preferable_route_scoring_test_() ->
-spec prefer_weight_over_availability_test() -> _.
prefer_weight_over_availability_test() ->
Route1 = new(?prv(1), ?trm(1), 0, 1000),
Route2 = new(?prv(2), ?trm(2), 0, 1005),
Route3 = new(?prv(3), ?trm(3), 0, 1000),
Route1 = hg_route:new(?prv(1), ?trm(1), 0, 1000),
Route2 = hg_route:new(?prv(2), ?trm(2), 0, 1005),
Route3 = hg_route:new(?prv(3), ?trm(3), 0, 1000),
Routes = [Route1, Route2, Route3],
ProviderStatuses = [
@ -965,9 +907,9 @@ prefer_weight_over_availability_test() ->
-spec prefer_weight_over_conversion_test() -> _.
prefer_weight_over_conversion_test() ->
Route1 = new(?prv(1), ?trm(1), 0, 1000),
Route2 = new(?prv(2), ?trm(2), 0, 1005),
Route3 = new(?prv(3), ?trm(3), 0, 1000),
Route1 = hg_route:new(?prv(1), ?trm(1), 0, 1000),
Route2 = hg_route:new(?prv(2), ?trm(2), 0, 1005),
Route3 = hg_route:new(?prv(3), ?trm(3), 0, 1000),
Routes = [Route1, Route2, Route3],
ProviderStatuses = [

View File

@ -0,0 +1,215 @@
-module(hg_routing_ctx).
-export([new/1]).
-export([with_fail_rates/2]).
-export([fail_rates/1]).
-export([set_choosen/3]).
-export([set_error/2]).
-export([error/1]).
-export([reject/3]).
-export([rejected_routes/1]).
-export([rejections/1]).
-export([candidates/1]).
-export([initial_candidates/1]).
-export([choosen_route/1]).
-export([process/2]).
-export([with_guard/1]).
-export([pipeline/2]).
-type rejection_group() :: atom().
-type error() :: {atom(), _Description}.
-type t() :: #{
initial_candidates := [hg_route:t()],
candidates := [hg_route:t()],
rejections := #{rejection_group() => [hg_route:rejected_route()]},
latest_rejection := rejection_group() | undefined,
error := error() | undefined,
choosen_route := hg_route:t() | undefined,
choice_meta := hg_routing:route_choice_context() | undefined,
fail_rates => [hg_routing:fail_rated_route()]
}.
-export_type([t/0]).
%%
-spec new([hg_route:t()]) -> t().
new(Candidates) ->
#{
initial_candidates => Candidates,
candidates => Candidates,
rejections => #{},
latest_rejection => undefined,
error => undefined,
choosen_route => undefined,
choice_meta => undefined
}.
-spec with_fail_rates([hg_routing:fail_rated_route()], t()) -> t().
with_fail_rates(FailRates, Ctx) ->
maps:put(fail_rates, FailRates, Ctx).
-spec fail_rates(t()) -> [hg_routing:fail_rated_route()] | undefined.
fail_rates(Ctx) ->
maps:get(fail_rates, Ctx, undefined).
-spec set_choosen(hg_route:t(), hg_routing:route_choice_context(), t()) -> t().
set_choosen(Route, ChoiceMeta, Ctx) ->
Ctx#{choosen_route => Route, choice_meta => ChoiceMeta}.
-spec set_error(term(), t()) -> t().
set_error(ErrorReason, Ctx) ->
Ctx#{error => ErrorReason}.
-spec error(t()) -> term() | undefined.
error(#{error := Error}) ->
Error.
-spec reject(atom(), hg_route:rejected_route(), t()) -> t().
reject(GroupReason, RejectedRoute, Ctx = #{rejections := Rejections, candidates := Candidates}) ->
RejectedList = maps:get(GroupReason, Rejections, []) ++ [RejectedRoute],
Ctx#{
rejections := Rejections#{GroupReason => RejectedList},
candidates := exclude_route(RejectedRoute, Candidates),
latest_rejection := GroupReason
}.
-spec process(T, fun((T) -> T)) -> T when T :: t().
process(Ctx0, Fun) ->
case Ctx0 of
#{error := undefined} ->
with_guard(Fun(Ctx0));
ErroneousCtx ->
ErroneousCtx
end.
-spec with_guard(t()) -> t().
with_guard(Ctx0) ->
case Ctx0 of
NoRouteCtx = #{candidates := [], error := undefined} ->
NoRouteCtx#{error := {rejected_routes, latest_rejected_routes(NoRouteCtx)}};
Ctx1 ->
Ctx1
end.
-spec pipeline(T, [fun((T) -> T)]) -> T when T :: t().
pipeline(Ctx, Funs) ->
lists:foldl(fun(F, C) -> process(C, F) end, Ctx, Funs).
-spec rejected_routes(t()) -> [hg_route:rejected_route()].
rejected_routes(#{rejections := Rejections}) ->
{_, RejectedRoutes} = lists:unzip(maps:to_list(Rejections)),
lists:flatten(RejectedRoutes).
-spec candidates(t()) -> [hg_route:t()].
candidates(#{candidates := Candidates}) ->
Candidates.
-spec initial_candidates(t()) -> [hg_route:t()].
initial_candidates(#{initial_candidates := InitialCandidates}) ->
InitialCandidates.
-spec choosen_route(t()) -> hg_route:t() | undefined.
choosen_route(#{choosen_route := ChoosenRoute}) ->
ChoosenRoute.
-spec rejections(t()) -> [{atom(), [hg_route:rejected_route()]}].
rejections(#{rejections := Rejections}) ->
maps:to_list(Rejections).
%%
latest_rejected_routes(#{latest_rejection := ReasonGroup, rejections := Rejections}) ->
{ReasonGroup, maps:get(ReasonGroup, Rejections, [])}.
exclude_route(Route, Routes) ->
lists:foldr(
fun(R, RR) ->
case hg_route:equal(Route, R) of
true -> RR;
_Else -> [R | RR]
end
end,
[],
Routes
).
%%
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
-include_lib("damsel/include/dmsl_domain_thrift.hrl").
-define(prv(ID), #domain_ProviderRef{id = ID}).
-define(trm(ID), #domain_TerminalRef{id = ID}).
-spec test() -> _.
-spec route_exclusion_test_() -> [_].
route_exclusion_test_() ->
RouteA = hg_route:new(?prv(1), ?trm(1)),
RouteB = hg_route:new(?prv(1), ?trm(2)),
RouteC = hg_route:new(?prv(2), ?trm(1)),
[
?_assertEqual([], exclude_route(RouteA, [])),
?_assertEqual([RouteA, RouteB], exclude_route(RouteC, [RouteA, RouteB])),
?_assertEqual([RouteA, RouteB], exclude_route(RouteC, [RouteA, RouteB, RouteC])),
?_assertEqual([RouteA, RouteC], exclude_route(RouteB, [RouteA, RouteB, RouteC]))
].
-spec pipeline_test_() -> [_].
pipeline_test_() ->
RouteA = hg_route:new(?prv(1), ?trm(1)),
RouteB = hg_route:new(?prv(1), ?trm(2)),
RouteC = hg_route:new(?prv(2), ?trm(1)),
RejectedRouteA = hg_route:to_rejected_route(RouteA, {?MODULE, <<"whatever">>}),
[
?_assertMatch(
#{
initial_candidates := [RouteA],
candidates := [],
error := {rejected_routes, {test, [RejectedRouteA]}},
choosen_route := undefined
},
pipeline(new([RouteA]), [fun do_reject_route_a/1])
),
?_assertMatch(
#{
initial_candidates := [RouteA, RouteB, RouteC],
candidates := [RouteA, RouteB, RouteC],
error := undefined,
choosen_route := undefined
},
pipeline(new([RouteA, RouteB, RouteC]), [])
),
?_assertMatch(
#{
initial_candidates := [RouteA, RouteB, RouteC],
candidates := [RouteB, RouteC],
error := undefined,
choosen_route := undefined
},
pipeline(new([RouteA, RouteB, RouteC]), [fun do_reject_route_a/1])
),
?_assertMatch(
#{
initial_candidates := [RouteA, RouteB, RouteC],
candidates := [RouteB, RouteC],
error := undefined,
choosen_route := RouteB
},
pipeline(new([RouteA, RouteB, RouteC]), [fun do_reject_route_a/1, fun do_choose_route_b/1])
)
].
do_reject_route_a(Ctx) ->
RouteA = hg_route:new(?prv(1), ?trm(1)),
reject(test, hg_route:to_rejected_route(RouteA, {?MODULE, <<"whatever">>}), Ctx).
do_choose_route_b(Ctx) ->
RouteB = hg_route:new(?prv(1), ?trm(2)),
set_choosen(RouteB, #{}, Ctx).
-endif.

View File

@ -8,6 +8,7 @@
-include("hg_ct_invoice.hrl").
-include_lib("damsel/include/dmsl_repair_thrift.hrl").
-include_lib("hellgate/include/allocation.hrl").
-include_lib("fault_detector_proto/include/fd_proto_fault_detector_thrift.hrl").
-include_lib("stdlib/include/assert.hrl").
@ -200,6 +201,10 @@
-export([payment_cascade_fail_provider_error/1]).
-export([payment_cascade_fail_ui/1]).
-export([route_not_found_provider_unavailable/1]).
-export([payment_success_ruleset_provider_available/1]).
-export([route_not_found_provider_lacking_conversion/1]).
%%
-behaviour(supervisor).
@ -324,7 +329,11 @@ groups() ->
payment_capture_retries_exceeded,
payment_partial_capture_success,
payment_error_in_cancel_session_does_not_cause_payment_failure,
payment_error_in_capture_session_does_not_cause_payment_failure
payment_error_in_capture_session_does_not_cause_payment_failure,
payment_success_ruleset_provider_available,
route_not_found_provider_unavailable,
route_not_found_provider_lacking_conversion
]},
{adjustments, [], [
@ -525,6 +534,7 @@ init_per_suite(C) ->
{ok, SupPid} = supervisor:start_link(?MODULE, []),
_ = unlink(SupPid),
ok = start_kv_store(SupPid),
_ = mock_fault_detector(SupPid),
NewC = [
{party_id, PartyID},
{party_client, PartyClient},
@ -1190,28 +1200,30 @@ payment_limit_overflow(C) ->
Failure = create_payment_limit_overflow(PartyID, ShopID, 1000, Client, PmtSys),
ok = hg_limiter_helper:assert_payment_limit_amount(PaymentAmount, Payment, Invoice),
ok = payproc_errors:match(
'PaymentFailure',
Failure,
fun({no_route_found, {forbidden, _}}) -> ok end
).
ok = payproc_errors:match('PaymentFailure', Failure, fun({no_route_found, {rejected, {limit_overflow, _}}}) ->
ok
end).
-spec limit_hold_currency_error(config()) -> test_return().
limit_hold_currency_error(C) ->
payment_route_not_found(C).
Failure = payment_route_not_found(C),
?assertRouteNotFound(Failure, {rejected, {limit_misconfiguration, _}}, <<"[{">>).
-spec limit_hold_operation_not_supported(config()) -> test_return().
limit_hold_operation_not_supported(C) ->
payment_route_not_found(C).
Failure = payment_route_not_found(C),
?assertRouteNotFound(Failure, {rejected, {limit_misconfiguration, _}}, <<"[{">>).
-spec limit_hold_payment_tool_not_supported(config()) -> test_return().
limit_hold_payment_tool_not_supported(C) ->
{PaymentTool, Session} = hg_dummy_provider:make_payment_tool(crypto_currency, ?crypta(<<"bitcoin-ref">>)),
payment_route_not_found(PaymentTool, Session, C).
Failure = payment_route_not_found(PaymentTool, Session, C),
?assertRouteNotFound(Failure, {rejected, {limit_misconfiguration, _}}, <<"[{">>).
-spec limit_hold_two_routes_failure(config()) -> test_return().
limit_hold_two_routes_failure(C) ->
payment_route_not_found(C).
Failure = payment_route_not_found(C),
?assertRouteNotFound(Failure, {rejected, {limit_overflow, _}}, <<"[{">>).
payment_route_not_found(C) ->
PmtSys = ?pmt_sys(<<"visa-ref">>),
@ -1235,7 +1247,8 @@ payment_route_not_found(PaymentTool, Session, C) ->
_ = start_payment_ev(InvoiceID, Client),
?payment_ev(PaymentID, ?payment_rollback_started({failure, Failure})) =
next_change(InvoiceID, Client),
?assertRouteNotFound(Failure, _, <<"{rejected_routes,[{">>).
%% NOTE Failure reason is expected to contain non-empty list of rejected routes
Failure.
-spec switch_provider_after_limit_overflow(config()) -> test_return().
switch_provider_after_limit_overflow(C) ->
@ -1306,11 +1319,9 @@ refund_limit_success(C) ->
?payment(PaymentID) = Payment,
Failure = create_payment_limit_overflow(PartyID, ShopID, 50000, Client, PmtSys),
ok = payproc_errors:match(
'PaymentFailure',
Failure,
fun({no_route_found, {forbidden, _}}) -> ok end
),
ok = payproc_errors:match('PaymentFailure', Failure, fun({no_route_found, {rejected, {limit_overflow, _}}}) ->
ok
end),
% create a refund finally
RefundParams = make_refund_params(),
RefundID = execute_payment_refund(InvoiceID, PaymentID, RefundParams, Client),
@ -1435,7 +1446,8 @@ payment_w_misconfigured_routing_failed(C) ->
?payment_ev(PaymentID, ?risk_score_changed(_)),
?payment_ev(PaymentID, ?payment_status_changed(?failed({failure, Failure})))
] = next_changes(InvoiceID, 3, Client),
?assertRouteNotFound(Failure, {unknown, _}, <<"{misconfiguration,{">>).
Reason = genlib:format({routing_decisions, {delegates, []}}),
?assertRouteNotFound(Failure, {unknown, {{unknown_error, <<"misconfiguration">>}, _}}, Reason).
payment_w_misconfigured_routing_failed_fixture(_Revision, _C) ->
[
@ -1757,6 +1769,69 @@ repair_failed_cancel(InvoiceID, PaymentID, Reason, Client) ->
] = next_changes(InvoiceID, 2, Client),
PaymentID.
-spec payment_success_ruleset_provider_available(config()) -> test_return().
payment_success_ruleset_provider_available(C) ->
with_fault_detector(
mk_fd_stat(?prv(1), {0.5, 0.5}),
fun() ->
PartyID = cfg(party_id_big_merch, C),
RootUrl = cfg(root_url, C),
PartyClient = cfg(party_client, C),
Client = hg_client_invoicing:start_link(hg_ct_helper:create_client(RootUrl)),
ShopID = hg_ct_helper:create_shop(PartyID, ?cat(1), <<"RUB">>, ?tmpl(1), ?pinst(1), PartyClient),
InvoiceParams = make_invoice_params(PartyID, ShopID, <<"rubberduck">>, make_due_date(10), make_cash(42000)),
InvoiceID = create_invoice(InvoiceParams, Client),
?invoice_created(?invoice_w_status(?invoice_unpaid())) = next_change(InvoiceID, Client),
PaymentID = process_payment(InvoiceID, make_payment_params(?pmt_sys(<<"visa-ref">>)), Client),
PaymentID = await_payment_capture(InvoiceID, PaymentID, Client),
?invoice_state(
?invoice_w_status(?invoice_paid()),
[?payment_state(Payment)]
) = hg_client_invoicing:get(InvoiceID, Client),
?payment_w_status(PaymentID, ?captured()) = Payment
end
).
-spec route_not_found_provider_unavailable(config()) -> test_return().
route_not_found_provider_unavailable(C) ->
with_fault_detector(
mk_fd_stat(?prv(1), {0.5, 0.9}),
fun() ->
{_InvoiceID, _PaymentID, Failure} = failed_payment_wo_cascade(C),
?assertRouteNotFound(Failure, {rejected, {adapter_unavailable, _}}, <<"[{">>)
end
).
-spec route_not_found_provider_lacking_conversion(config()) -> test_return().
route_not_found_provider_lacking_conversion(C) ->
with_fault_detector(
mk_fd_stat(?prv(1), {0.9, 0.5}),
fun() ->
{_InvoiceID, _PaymentID, Failure} = failed_payment_wo_cascade(C),
?assertRouteNotFound(Failure, {rejected, {provider_conversion_is_too_low, _}}, <<"[{">>)
end
).
failed_payment_wo_cascade(C) ->
PartyID = cfg(party_id_big_merch, C),
RootUrl = cfg(root_url, C),
PartyClient = cfg(party_client, C),
Client = hg_client_invoicing:start_link(hg_ct_helper:create_client(RootUrl)),
ShopID = hg_ct_helper:create_shop(PartyID, ?cat(1), <<"RUB">>, ?tmpl(1), ?pinst(1), PartyClient),
InvoiceParams = make_invoice_params(PartyID, ShopID, <<"rubberduck">>, make_due_date(10), make_cash(42000)),
InvoiceID = create_invoice(InvoiceParams, Client),
?invoice_created(?invoice_w_status(?invoice_unpaid())) = next_change(InvoiceID, Client),
PaymentParams = make_payment_params(?pmt_sys(<<"visa-ref">>)),
?payment_state(?payment(PaymentID)) =
hg_client_invoicing:start_payment(InvoiceID, PaymentParams, Client),
_ = start_payment_ev(InvoiceID, Client),
?payment_ev(PaymentID, ?payment_rollback_started({failure, Failure})) =
next_change(InvoiceID, Client),
{InvoiceID, PaymentID, Failure}.
-spec payment_w_terminal_w_payment_service_success(config()) -> _ | no_return().
payment_w_terminal_w_payment_service_success(C) ->
Client = cfg(client, C),
@ -9870,3 +9945,44 @@ construct_term_set_for_partial_capture_provider_permit(Revision, _C) ->
set_processing_deadline(Timeout, PaymentParams) ->
Deadline = woody_deadline:to_binary(woody_deadline:from_timeout(Timeout)),
PaymentParams#payproc_InvoicePaymentParams{processing_deadline = Deadline}.
mk_fd_stat(?prv(ID), {ConversionFailureRate, AvailabilityFailureRate}) ->
[
mk_fd_stat(<<"provider_conversion">>, ?prv(ID), ConversionFailureRate),
mk_fd_stat(<<"adapter_availability">>, ?prv(ID), AvailabilityFailureRate)
].
mk_fd_stat(Type, ?prv(ID), FailureRate) ->
#fault_detector_ServiceStatistics{
service_id = <<"hellgate_service.", Type/binary, ".", (integer_to_binary(ID))/binary>>,
%% NOTE Testsuite config's critical failure threshold is .7
failure_rate = FailureRate,
%% Those are bullshit values, because we don't actually care for raw numbers
operations_count = 10,
error_operations_count = 9,
overtime_operations_count = 0,
success_operations_count = 1
}.
with_fault_detector(Statistics, Fun) ->
FDConfig = genlib_app:env(hellgate, fault_detector),
_ = application:set_env(hellgate, fault_detector, FDConfig#{enabled => true}),
_ = hg_kv_store:put(fd_statistics, Statistics),
Result = Fun(),
application:set_env(hellgate, fault_detector, FDConfig#{enabled => false}),
Result.
mock_fault_detector(SupPid) ->
hg_mock_helper:mock_services(
[
{fault_detector, fun
('InitService', _) ->
{ok, {}};
('RegisterOperation', _) ->
{ok, {}};
('GetStatistics', _) ->
{ok, hg_kv_store:get(fd_statistics)}
end}
],
SupPid
).

View File

@ -456,7 +456,9 @@ no_route_found_for_payment(_C) ->
party_id => ?dummy_party_id,
client_ip => undefined
},
{ok, {[], RejectedRoutes1}} = hg_routing:gather_routes(payment, PaymentInstitution, VS, Revision, Ctx0),
{ok, {[], RejectedRoutes1}} = unwrap_routing_context(
hg_routing:gather_routes(payment, PaymentInstitution, VS, Revision, Ctx0)
),
?assert_set_equal(
[
@ -477,7 +479,9 @@ no_route_found_for_payment(_C) ->
Ctx1 = Ctx0#{
currency => Currency1
},
{ok, {[], RejectedRoutes2}} = hg_routing:gather_routes(payment, PaymentInstitution, VS1, Revision, Ctx1),
{ok, {[], RejectedRoutes2}} = unwrap_routing_context(
hg_routing:gather_routes(payment, PaymentInstitution, VS1, Revision, Ctx1)
),
?assert_set_equal(
[
{?prv(1), ?trm(1), {'PaymentsProvisionTerms', currency}},
@ -511,14 +515,10 @@ gather_route_success(_C) ->
party_id => ?dummy_party_id,
client_ip => undefined
},
{ok, {[Route], RejectedRoutes}} = hg_routing:gather_routes(
payment,
PaymentInstitution,
VS,
Revision,
Ctx
{ok, {[Route], RejectedRoutes}} = unwrap_routing_context(
hg_routing:gather_routes(payment, PaymentInstitution, VS, Revision, Ctx)
),
?assertMatch(?trm(1), hg_routing:terminal_ref(Route)),
?assertMatch(?trm(1), hg_route:terminal_ref(Route)),
?assertMatch(
[
{?prv(2), ?trm(2), {'PaymentsProvisionTerms', category}},
@ -558,7 +558,9 @@ rejected_by_table_prohibitions(_C) ->
party_id => ?dummy_party_id,
client_ip => undefined
},
{ok, {[], RejectedRoutes}} = hg_routing:gather_routes(payment, PaymentInstitution, VS, Revision, Ctx),
{ok, {[], RejectedRoutes}} = unwrap_routing_context(
hg_routing:gather_routes(payment, PaymentInstitution, VS, Revision, Ctx)
),
?assert_set_equal(
[
{?prv(3), ?trm(3), {'RoutingRule', undefined}},
@ -600,7 +602,7 @@ empty_candidate_ok(_C) ->
},
?assertMatch(
{ok, {[], []}},
hg_routing:gather_routes(payment, PaymentInstitution, VS, Revision, Ctx)
unwrap_routing_context(hg_routing:gather_routes(payment, PaymentInstitution, VS, Revision, Ctx))
).
-spec ruleset_misconfig(config()) -> test_return().
@ -620,14 +622,8 @@ ruleset_misconfig(_C) ->
client_ip => undefined
},
?assertMatch(
{error, {misconfiguration, {routing_decisions, {delegates, []}}}},
hg_routing:gather_routes(
payment,
PaymentInstitution,
VS,
Revision,
Ctx
)
{misconfiguration, {routing_decisions, {delegates, []}}},
hg_routing_ctx:error(hg_routing:gather_routes(payment, PaymentInstitution, VS, Revision, Ctx))
).
-spec routes_selected_for_low_risk_score(config()) -> test_return().
@ -658,14 +654,16 @@ routes_selected_with_risk_score(_C, RiskScore, ProviderRefs) ->
party_id => ?dummy_party_id,
client_ip => undefined
},
{ok, {Routes, _}} = hg_routing:gather_routes(payment, PaymentInstitution, VS, Revision, Ctx),
?assert_set_equal(ProviderRefs, lists:map(fun hg_routing:provider_ref/1, Routes)).
{ok, {Routes, _}} = unwrap_routing_context(
hg_routing:gather_routes(payment, PaymentInstitution, VS, Revision, Ctx)
),
?assert_set_equal(ProviderRefs, lists:map(fun hg_route:provider_ref/1, Routes)).
-spec choice_context_formats_ok(config()) -> test_return().
choice_context_formats_ok(_C) ->
Route1 = hg_routing:new(?prv(1), ?trm(1)),
Route2 = hg_routing:new(?prv(2), ?trm(2)),
Route3 = hg_routing:new(?prv(3), ?trm(3)),
Route1 = hg_route:new(?prv(1), ?trm(1)),
Route2 = hg_route:new(?prv(2), ?trm(2)),
Route3 = hg_route:new(?prv(3), ?trm(3)),
Routes = [Route1, Route2, Route3],
Revision = ?routing_with_fail_rate_domain_revision,
@ -722,19 +720,15 @@ do_gather_routes(Revision, ExpectedRouteTerminal, ExpectedRejectedRoutes) ->
party_id => ?dummy_party_id,
client_ip => undefined
},
{ok, {Routes, RejectedRoutes}} = hg_routing:gather_routes(
payment,
PaymentInstitution,
VS,
Revision,
Ctx
{ok, {Routes, RejectedRoutes}} = unwrap_routing_context(
hg_routing:gather_routes(payment, PaymentInstitution, VS, Revision, Ctx)
),
case ExpectedRouteTerminal of
undefined ->
ok;
Terminal ->
[Route] = Routes,
?assertMatch(Terminal, hg_routing:terminal_ref(Route))
?assertMatch(Terminal, hg_route:terminal_ref(Route))
end,
?assertMatch(ExpectedRejectedRoutes, RejectedRoutes).
@ -742,8 +736,8 @@ do_gather_routes(Revision, ExpectedRouteTerminal, ExpectedRejectedRoutes) ->
-spec terminal_priority_for_shop(config()) -> test_return().
terminal_priority_for_shop(C) ->
Route1 = hg_routing:new(?prv(11), ?trm(11), 0, 10),
Route2 = hg_routing:new(?prv(12), ?trm(12), 0, 10),
Route1 = hg_route:new(?prv(11), ?trm(11), 0, 10),
Route2 = hg_route:new(?prv(12), ?trm(12), 0, 10),
?assertMatch({Route1, _}, terminal_priority_for_shop(?shop_id_for_ruleset_w_priority_distribution_1, C)),
?assertMatch({Route2, _}, terminal_priority_for_shop(?shop_id_for_ruleset_w_priority_distribution_2, C)).
@ -767,7 +761,9 @@ terminal_priority_for_shop(ShopID, _C) ->
party_id => ?dummy_party_id,
client_ip => undefined
},
{ok, {Routes, _RejectedRoutes}} = hg_routing:gather_routes(payment, PaymentInstitution, VS, Revision, Ctx),
{ok, {Routes, _RejectedRoutes}} = unwrap_routing_context(
hg_routing:gather_routes(payment, PaymentInstitution, VS, Revision, Ctx)
),
hg_routing:choose_route(Routes).
-spec gather_pinned_route(config()) -> test_return().
@ -790,16 +786,18 @@ gather_pinned_route(_C) ->
party_id => ?dummy_party_id,
client_ip => undefined
},
{ok, {Routes, _RejectedRoutes}} = hg_routing:gather_routes(payment, PaymentInstitution, VS, Revision, Ctx),
{ok, {Routes, _RejectedRoutes}} = unwrap_routing_context(
hg_routing:gather_routes(payment, PaymentInstitution, VS, Revision, Ctx)
),
Pin = #{
currency => Currency,
payment_tool => PaymentTool
},
?assert_set_equal(
[
hg_routing:new(?prv(1), ?trm(1), 0, 0, Ctx),
hg_routing:new(?prv(2), ?trm(2), 0, 0, Pin),
hg_routing:new(?prv(3), ?trm(3), 0, 0, Pin)
hg_route:new(?prv(1), ?trm(1), 0, 0, Ctx),
hg_route:new(?prv(2), ?trm(2), 0, 0, Pin),
hg_route:new(?prv(3), ?trm(3), 0, 0, Pin)
],
Routes
).
@ -823,9 +821,9 @@ choose_pinned_route(_C) ->
currency => Currency,
payment_tool => PaymentTool
},
Route1 = hg_routing:new(?prv(1), ?trm(1), 0, 0, Pin1),
Route2 = hg_routing:new(?prv(1), ?trm(1), 0, 0, Pin2),
Route3 = hg_routing:new(?prv(1), ?trm(1), 0, 0, Pin3),
Route1 = hg_route:new(?prv(1), ?trm(1), 0, 0, Pin1),
Route2 = hg_route:new(?prv(1), ?trm(1), 0, 0, Pin2),
Route3 = hg_route:new(?prv(1), ?trm(1), 0, 0, Pin3),
Routes = [
Route1,
Route2,
@ -837,9 +835,9 @@ choose_pinned_route(_C) ->
-spec choose_route_w_override(config()) -> test_return().
choose_route_w_override(_C) ->
%% without overrides
Route1 = hg_routing:new(?prv(1), ?trm(1)),
Route2 = hg_routing:new(?prv(2), ?trm(2)),
Route3 = hg_routing:new(?prv(3), ?trm(3)),
Route1 = hg_route:new(?prv(1), ?trm(1)),
Route2 = hg_route:new(?prv(2), ?trm(2)),
Route3 = hg_route:new(?prv(3), ?trm(3)),
Routes = [Route1, Route2, Route3],
{
Route2,
@ -850,7 +848,7 @@ choose_route_w_override(_C) ->
} = hg_routing:choose_route(Routes),
%% with overrides
Route3WithOV = hg_routing:new(?prv(3), ?trm(3), 0, 1000, #{}, #domain_RouteFaultDetectorOverrides{enabled = true}),
Route3WithOV = hg_route:new(?prv(3), ?trm(3), 0, 1000, #{}, #domain_RouteFaultDetectorOverrides{enabled = true}),
RoutesWithOV = [Route1, Route2, Route3WithOV],
{Route3WithOV, _} = hg_routing:choose_route(RoutesWithOV).
@ -928,3 +926,6 @@ maybe_set_risk_coverage(false, _) ->
undefined;
maybe_set_risk_coverage(true, V) ->
{value, V}.
unwrap_routing_context(RoutingCtx) ->
{ok, {hg_routing_ctx:candidates(RoutingCtx), hg_routing_ctx:rejected_routes(RoutingCtx)}}.

View File

@ -20,7 +20,7 @@
{<<"ctx">>,{pkg,<<"ctx">>,<<"0.6.0">>},2},
{<<"damsel">>,
{git,"https://github.com/valitydev/damsel.git",
{ref,"5b69a5bb59529a22f2bf0bbd78b7539773db3562"}},
{ref,"23211ff8c0c45699fa6d78afa55f304e95dbee87"}},
0},
{<<"dmt_client">>,
{git,"https://github.com/valitydev/dmt-client.git",