mirror of
https://github.com/valitydev/hellgate.git
synced 2024-11-06 02:45:20 +00:00
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:
parent
42af922405
commit
7305ff03e6
@ -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(),
|
||||
|
@ -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().
|
||||
|
@ -12,7 +12,7 @@
|
||||
-spec is_triggered(
|
||||
cascade_behaviour() | undefined,
|
||||
operation_failure(),
|
||||
hg_routing:payment_route(),
|
||||
hg_route:payment_route(),
|
||||
[hg_session:t()]
|
||||
) ->
|
||||
boolean().
|
||||
|
@ -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().
|
||||
|
||||
%%
|
||||
|
||||
|
@ -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 = [
|
||||
|
@ -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.
|
||||
|
@ -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'().
|
||||
|
@ -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.
|
||||
|
||||
|
191
apps/hellgate/src/hg_route.erl
Normal file
191
apps/hellgate/src/hg_route.erl
Normal 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.
|
@ -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 = [
|
||||
|
215
apps/hellgate/src/hg_routing_ctx.erl
Normal file
215
apps/hellgate/src/hg_routing_ctx.erl
Normal 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.
|
@ -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
|
||||
).
|
||||
|
@ -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)}}.
|
||||
|
@ -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",
|
||||
|
Loading…
Reference in New Issue
Block a user