diff --git a/apps/hellgate/src/hg_invoice_payment.erl b/apps/hellgate/src/hg_invoice_payment.erl index 26874612..4ca840d9 100644 --- a/apps/hellgate/src/hg_invoice_payment.erl +++ b/apps/hellgate/src/hg_invoice_payment.erl @@ -1892,67 +1892,72 @@ process_risk_score(Action, St) -> -spec process_routing(action(), st()) -> machine_result(). process_routing(Action, St) -> {PaymentInstitution, VS, Revision} = route_args(St), - Ctx0 = hg_routing_ctx:with_guard(build_routing_context(PaymentInstitution, VS, Revision, St)), + Routing0 = 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 + %% before the routing decision pipeline. + case hg_routing:get_error(Routing0) of undefined -> - Ctx1 = run_routing_decision_pipeline(Ctx0, VS, St), + Routing1 = run_routing_decision_pipeline(Routing0, VS, St), _ = [ log_rejected_routes(Group, RejectedRoutes, VS) - || {Group, RejectedRoutes} <- hg_routing_ctx:rejections(Ctx1) + || {Group, RejectedRoutes} <- hg_routing:rejections(Routing1) ], - Events = produce_routing_events(Ctx1, Revision, St), + Events = produce_routing_events(Routing1, 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), + ok = log_rejected_routes(all, hg_routing:rejected_routes(Routing0), VS), handle_choose_route_error(Error, [], St, Action) end. -run_routing_decision_pipeline(Ctx0, VS, St) -> +run_routing_decision_pipeline(Routing0, VS, St) -> %% NOTE Since this is routing step then current attempt is not yet %% accounted for in `St`. NewIter = get_iter(St) + 1, - hg_routing_ctx:pipeline( - Ctx0, - [ - fun(Ctx) -> filter_attempted_routes(Ctx, St) end, - fun(Ctx) -> filter_routes_with_limit_hold(Ctx, VS, NewIter, St) end, - fun(Ctx) -> filter_routes_by_limit_overflow(Ctx, VS, NewIter, St) end, - fun(Ctx) -> hg_routing:filter_by_blacklist(Ctx, build_blacklist_context(St)) end, - fun hg_routing:filter_by_critical_provider_status/1, - fun hg_routing:choose_route_with_ctx/1 - ] - ). + hg_routing:resolve(#{ + routing => Routing0, + attempted_routes => St#st.routes, + limit_hold_fun => fun(Routes) -> hold_limit_routes(Routes, VS, NewIter, St) end, + limit_overflow_fun => fun(Routes) -> get_limit_overflow_routes(Routes, VS, NewIter, St) end, + blacklist_context => build_blacklist_context(St) + }). -produce_routing_events(#{error := Error} = Ctx, Revision, St) when Error =/= undefined -> +produce_routing_events(Routing, Revision, St) -> + case hg_routing:get_error(Routing) of + Error when Error =/= undefined -> + produce_failed_routing_events(Routing, Error, Revision, St); + undefined -> + produce_success_routing_events(Routing, Revision) + end. + +produce_failed_routing_events(Routing, Error, Revision, St) -> %% 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)), %% NOTE Not all initial candidates have their according limits held. And so %% we must account only for those that can be rolled back. - RollbackableCandidates = hg_routing_ctx:accounted_candidates(Ctx), + RollbackableCandidates = hg_routing:accounted_candidates(Routing), Route = hg_route:to_payment_route(hd(RollbackableCandidates)), Candidates = ordsets:from_list([hg_route:to_payment_route(R) || R <- RollbackableCandidates]), - RouteScores = hg_routing_ctx:route_scores(Ctx), - RouteLimits = hg_routing_ctx:route_limits(Ctx), + RouteScores = hg_routing:route_scores(Routing), + RouteLimits = hg_routing:route_limits(Routing), Decision = build_route_decision_context(Route, Revision), %% 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, RouteScores, RouteLimits, Decision), ?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)), + [?route_changed(Route, Candidates, RouteScores, RouteLimits, Decision), ?payment_rollback_started(Failure)]. + +produce_success_routing_events(Routing, Revision) -> + ok = log_route_choice_meta(#{choice_meta => hg_routing:choice_meta(Routing)}, Revision), + Route = hg_route:to_payment_route(hg_routing:chosen_route(Routing)), Candidates = - ordsets:from_list([hg_route:to_payment_route(R) || R <- hg_routing_ctx:considered_candidates(Ctx)]), - RouteScores = hg_routing_ctx:route_scores(Ctx), - RouteLimits = hg_routing_ctx:route_limits(Ctx), + ordsets:from_list([hg_route:to_payment_route(R) || R <- hg_routing:considered_candidates(Routing)]), + RouteScores = hg_routing:route_scores(Routing), + RouteLimits = hg_routing:route_limits(Routing), Decision = build_route_decision_context(Route, Revision), [?route_changed(Route, Candidates, RouteScores, RouteLimits, Decision)]. @@ -1984,7 +1989,7 @@ build_routing_context(PaymentInstitution, VS, Revision, St) -> Payer = get_payment_payer(St), case get_predefined_route(Payer) of {ok, PaymentRoute} -> - hg_routing_ctx:new([hg_route:from_payment_route(PaymentRoute)]); + hg_routing:resolve(#{predefined_routes => [hg_route:from_payment_route(PaymentRoute)]}); undefined -> gather_routes(PaymentInstitution, VS, Revision, St) end. @@ -2011,17 +2016,58 @@ build_blacklist_context(St) -> inspector => Inspector }. +-ifdef(TEST). + +new_routing_result(Candidates) -> + #{ + initial_candidates => Candidates, + candidates => Candidates, + rejections => #{}, + latest_rejection => undefined, + error => undefined, + chosen_route => undefined, + choice_meta => undefined + }. + filter_attempted_routes(Ctx, #st{routes = AttemptedRoutes}) -> + mark_attempted_routes(AttemptedRoutes, Ctx). + +mark_attempted_routes([], Routing) -> + Routing; +mark_attempted_routes(AttemptedRoutes, Routing0) -> 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) + fun(Route, Routing) -> + InnerRoute = hg_route:from_payment_route(Route), + RejectedRoute = hg_route:to_rejected_route(InnerRoute, {'AlreadyAttempted', undefined}), + reject_route_in_routing(already_attempted, RejectedRoute, Routing) end, - Ctx, + Routing0, AttemptedRoutes ). +reject_route_in_routing(GroupReason, RejectedRoute, Routing0) -> + #{rejections := Rejections0, candidates := Candidates0} = Routing0, + RejectedList = maps:get(GroupReason, Rejections0, []) ++ [RejectedRoute], + Routing0#{ + rejections := Rejections0#{GroupReason => RejectedList}, + candidates := exclude_routing_route(RejectedRoute, Candidates0), + latest_rejection := GroupReason + }. + +exclude_routing_route(Route, Routes) -> + lists:foldr( + fun(R, Acc) -> + case hg_route:equal(Route, R) of + true -> Acc; + false -> [R | Acc] + end + end, + [], + Routes + ). + +-endif. + handle_choose_route_error(Error, Events, St, Action) -> Failure = construct_routing_failure(Error), process_failure(get_activity(St), Events, Action, Failure, St). @@ -2502,23 +2548,6 @@ get_provider_payment_terms(St, Revision) -> VS1 = collect_validation_varset(get_party_config_ref(Opts), get_shop_obj(Opts, Revision), Payment, VS0), hg_routing:get_payment_terms(Route, VS1, Revision). -filter_routes_with_limit_hold(Ctx0, VS, Iter, St) -> - {_Routes, RejectedRoutes} = hold_limit_routes(hg_routing_ctx:candidates(Ctx0), VS, Iter, St), - Ctx1 = reject_routes(limit_misconfiguration, RejectedRoutes, Ctx0), - hg_routing_ctx:stash_current_candidates(Ctx1). - -filter_routes_by_limit_overflow(Ctx0, VS, Iter, St) -> - {_Routes, RejectedRoutes, Limits} = get_limit_overflow_routes(hg_routing_ctx:candidates(Ctx0), VS, Iter, St), - Ctx1 = hg_routing_ctx:stash_route_limits(Limits, Ctx0), - reject_routes(limit_overflow, RejectedRoutes, Ctx1). - -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, Iter, St) -> Opts = get_opts(St), Revision = get_payment_revision(St), @@ -3486,7 +3515,7 @@ get_limit_values(St, Opts) -> get_limit_values_(St, Mode) -> {PaymentInstitution, VS, Revision} = route_args(St), - Ctx = build_routing_context(PaymentInstitution, VS, Revision, St), + Routing = build_routing_context(PaymentInstitution, VS, Revision, St), Session = get_activity_session(St), Payment = get_payment(St), Invoice = get_invoice(get_opts(St)), @@ -3508,7 +3537,7 @@ get_limit_values_(St, Mode) -> Acc#{PaymentRoute => TurnoverLimitValues} end, #{}, - hg_routing_ctx:considered_candidates(Ctx) + hg_routing:considered_candidates(Routing) ). try_accrue_waiting_timing(Opts, #st{payment = Payment, timings = Timings}) -> @@ -3943,7 +3972,7 @@ filter_attempted_routes_test_() -> ?_assertMatch( #{candidates := []}, filter_attempted_routes( - hg_routing_ctx:new([]), + new_routing_result([]), #st{ activity = idle, routes = [ @@ -3956,16 +3985,16 @@ filter_attempted_routes_test_() -> ) ), ?_assertMatch( - #{candidates := []}, filter_attempted_routes(hg_routing_ctx:new([]), #st{activity = idle, routes = []}) + #{candidates := []}, filter_attempted_routes(new_routing_result([]), #st{activity = idle, routes = []}) ), ?_assertMatch( #{candidates := [R1, R2, R3]}, - filter_attempted_routes(hg_routing_ctx:new([R1, R2, R3]), #st{activity = idle, routes = []}) + filter_attempted_routes(new_routing_result([R1, R2, R3]), #st{activity = idle, routes = []}) ), ?_assertMatch( #{candidates := [R1, R2]}, filter_attempted_routes( - hg_routing_ctx:new([R1, R2, R3]), + new_routing_result([R1, R2, R3]), #st{ activity = idle, routes = [ @@ -3980,7 +4009,7 @@ filter_attempted_routes_test_() -> ?_assertMatch( #{candidates := []}, filter_attempted_routes( - hg_routing_ctx:new([R1, R2, R3]), + new_routing_result([R1, R2, R3]), #st{ activity = idle, routes = [ diff --git a/apps/hellgate/test/hg_route_rules_tests_SUITE.erl b/apps/hellgate/test/hg_route_rules_tests_SUITE.erl index 0cdc3c75..ad9ddf88 100644 --- a/apps/hellgate/test/hg_route_rules_tests_SUITE.erl +++ b/apps/hellgate/test/hg_route_rules_tests_SUITE.erl @@ -675,7 +675,7 @@ ruleset_misconfig(_C) -> }, ?assertMatch( {misconfiguration, {routing_decisions, {delegates, []}}}, - hg_routing_ctx:error(hg_routing:gather_routes(payment, PaymentInstitution, VS, Revision, Ctx)) + hg_routing:get_error(hg_routing:gather_routes(payment, PaymentInstitution, VS, Revision, Ctx)) ). -spec routes_selected_for_low_risk_score(config()) -> test_return(). @@ -718,11 +718,11 @@ choice_context_formats_ok(_C) -> Routes = [Route1, Route2, Route3], Revision = ?routing_with_fail_rate_domain_revision, - Result = {_, Context} = hg_routing:choose_route(Routes), - ?assertMatch( - {Route2, #{reject_reason := availability, preferable_route := Route3}}, - Result - ), + {ChosenRoute, Context} = resolve_predefined_routes(Routes), + ?assert(hg_route:equal(Route2, ChosenRoute)), + ?assert(hg_route:equal(ChosenRoute, maps:get(chosen_route, Context))), + ?assertEqual(availability, maps:get(reject_reason, Context)), + ?assert(hg_route:equal(Route3, maps:get(preferable_route, Context))), ?assertMatch( #{ reject_reason := availability, @@ -788,8 +788,10 @@ do_gather_routes(Revision, ExpectedRouteTerminal, ExpectedRejectedRoutes) -> terminal_priority_for_shop(C) -> 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)). + {ChosenRoute1, _} = terminal_priority_for_shop(?shop_id_for_ruleset_w_priority_distribution_1, C), + ?assert(hg_route:equal(Route1, ChosenRoute1)), + {ChosenRoute2, _} = terminal_priority_for_shop(?shop_id_for_ruleset_w_priority_distribution_2, C), + ?assert(hg_route:equal(Route2, ChosenRoute2)). terminal_priority_for_shop(ShopID, _C) -> Currency = ?cur(<<"RUB">>), @@ -813,7 +815,7 @@ terminal_priority_for_shop(ShopID, _C) -> {ok, {Routes, _RejectedRoutes}} = unwrap_routing_context( hg_routing:gather_routes(payment, PaymentInstitution, VS, Revision, Ctx) ), - hg_routing:choose_route(Routes). + resolve_predefined_routes(Routes). -spec gather_pinned_route(config()) -> test_return(). gather_pinned_route(_C) -> @@ -859,18 +861,16 @@ choose_route_w_override(_C) -> Route2 = hg_route:new(?prv(2), ?trm(2)), Route3 = hg_route:new(?prv(3), ?trm(3)), Routes = [Route1, Route2, Route3], - { - Route2, - #{ - preferable_route := Route3, - reject_reason := availability - } - } = hg_routing:choose_route(Routes), + {ChosenRoute, ChoiceContext} = resolve_predefined_routes(Routes), + ?assert(hg_route:equal(Route2, ChosenRoute)), + ?assert(hg_route:equal(Route3, maps:get(preferable_route, ChoiceContext))), + ?assertEqual(availability, maps:get(reject_reason, ChoiceContext)), %% with overrides Route3WithOV = hg_route:new(?prv(3), ?trm(3), 0, 1000, #{}, #domain_RouteFaultDetectorOverrides{enabled = true}), RoutesWithOV = [Route1, Route2, Route3WithOV], - {Route3WithOV, _} = hg_routing:choose_route(RoutesWithOV). + {ChosenRouteWithOV, _} = resolve_predefined_routes(RoutesWithOV), + ?assert(hg_route:equal(Route3WithOV, ChosenRouteWithOV)). -spec recurrent_payment_skip_recurrent_terms(config()) -> test_return(). recurrent_payment_skip_recurrent_terms(_C) -> @@ -1016,4 +1016,8 @@ maybe_set_risk_coverage(true, V) -> {value, V}. unwrap_routing_context(RoutingCtx) -> - {ok, {hg_routing_ctx:considered_candidates(RoutingCtx), hg_routing_ctx:rejected_routes(RoutingCtx)}}. + {ok, {hg_routing:considered_candidates(RoutingCtx), hg_routing:rejected_routes(RoutingCtx)}}. + +resolve_predefined_routes(Routes) -> + Routing = hg_routing:resolve(#{predefined_routes => Routes}), + {hg_routing:chosen_route(Routing), hg_routing:choice_meta(Routing)}. diff --git a/apps/routing/src/hg_route.erl b/apps/routing/src/hg_route.erl index 69fb3d18..9be8b8d7 100644 --- a/apps/routing/src/hg_route.erl +++ b/apps/routing/src/hg_route.erl @@ -1,19 +1,35 @@ -module(hg_route). -include_lib("hellgate/include/domain.hrl"). +-include_lib("damsel/include/dmsl_domain_thrift.hrl"). -export([new/2]). -export([new/4]). -export([new/5]). -export([new/6]). --export([provider_ref/1]). + +-export([set_fd_overrides/2]). +-export([set_prohibit/2]). +-export([set_accepted/2]). +-export([set_weight/2]). +-export([set_blacklisted/2]). +-export([set_availability/3]). +-export([set_conversion/3]). +-export([set_priority/2]). + +-export([route_data/1]). -export([terminal_ref/1]). +-export([provider_ref/1]). +-export([payment_route/1]). -export([priority/1]). -export([weight/1]). --export([set_weight/2]). -export([pin/1]). +-export([pin_hash/1]). -export([fd_overrides/1]). +-export([fd_score/1]). +-export([blacklisted/1]). +-export([score/1]). -export([equal/2]). -export([from_payment_route/1]). @@ -22,23 +38,40 @@ %% --record(route, { - provider_ref :: dmsl_domain_thrift:'ProviderRef'(), - terminal_ref :: dmsl_domain_thrift:'TerminalRef'(), - priority :: integer(), - pin :: pin(), - weight :: integer(), - fd_overrides :: fd_overrides() -}). - --type t() :: #route{}. +-type t() :: #{ + provider_ref := provider_ref(), + terminal_ref := terminal_ref(), + route_data := route_data(), + pin_data => pin(), + fd_overrides => fd_overrides() +}. -type payment_route() :: dmsl_domain_thrift:'PaymentRoute'(). +-type score() :: dmsl_domain_thrift:'PaymentRouteScores'(). -type route_rejection_reason() :: {atom(), term()} | {atom(), term(), term()}. -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 fd_score() :: #{ + availability_condition => integer(), + conversion_condition => integer(), + availability => float(), + conversion => float() +}. + +-type route_prohibit() :: boolean() | {boolean(), term()}. +-type route_accepted() :: boolean() | {boolean(), term()}. + +-type route_data() :: #{ + accepted => route_accepted(), + prohibit => route_prohibit(), + fd_score => fd_score(), + priority => integer(), + weight => integer(), + blacklisted => integer() +}. + -type currency() :: dmsl_domain_thrift:'CurrencyRef'(). -type payment_tool() :: dmsl_domain_thrift:'PaymentTool'(). -type client_ip() :: dmsl_domain_thrift:'IPAddress'(). @@ -57,7 +90,9 @@ -export_type([provider_ref/0]). -export_type([terminal_ref/0]). -export_type([payment_route/0]). +-export_type([score/0]). -export_type([rejected_route/0]). +-export_type([route_data/0]). %% @@ -82,42 +117,132 @@ new(ProviderRef, TerminalRef, Weight, Priority, Pin) -> -spec new(provider_ref(), terminal_ref(), integer(), integer(), pin(), fd_overrides()) -> t(). new(ProviderRef, TerminalRef, Weight, Priority, Pin, FdOverrides) -> - #route{ - provider_ref = ProviderRef, - terminal_ref = TerminalRef, - weight = Weight, - priority = Priority, - pin = Pin, - fd_overrides = FdOverrides + #{ + provider_ref => ProviderRef, + terminal_ref => TerminalRef, + route_data => #{ + accepted => true, + prohibit => false, + fd_score => #{ + availability_condition => 1, + availability => 1.0, + conversion_condition => 1, + conversion => 1.0 + }, + weight => Weight, + priority => Priority, + blacklisted => 0 + }, + pin_data => Pin, + fd_overrides => FdOverrides }. +-spec set_fd_overrides(fd_overrides(), t()) -> t(). +set_fd_overrides(FdOverrides, Route) -> + Route#{fd_overrides => FdOverrides}. + +-spec set_prohibit(route_prohibit(), t()) -> t(). +set_prohibit(Prohibit, #{route_data := Data} = Route) -> + Route#{route_data => Data#{prohibit => Prohibit}}. + +-spec set_accepted(route_accepted(), t()) -> t(). +set_accepted(Accepted, #{route_data := Data} = Route) -> + Route#{route_data => Data#{accepted => Accepted}}. + +-spec set_weight(integer(), t()) -> t(). +set_weight(Weight, #{route_data := Data} = Route) -> + Route#{route_data => Data#{weight => Weight}}. + +-spec set_blacklisted(boolean(), t()) -> t(). +set_blacklisted(true, #{route_data := Data} = Route) -> + Route#{route_data => Data#{blacklisted => 1}}; +set_blacklisted(false, #{route_data := Data} = Route) -> + Route#{route_data => Data#{blacklisted => 0}}. + +-spec set_availability(integer(), float(), t()) -> t(). +set_availability(Condition, Value, #{route_data := Data = #{fd_score := Score}} = Route) -> + Route#{route_data => Data#{fd_score => Score#{availability_condition => Condition, availability => Value}}}. + +-spec set_conversion(integer(), float(), t()) -> t(). +set_conversion(Condition, Value, #{route_data := Data = #{fd_score := Score}} = Route) -> + Route#{route_data => Data#{fd_score => Score#{conversion_condition => Condition, conversion => Value}}}. + +-spec set_priority(integer(), t()) -> t(). +set_priority(Priority, #{route_data := Data} = Route) -> + Route#{route_data => Data#{priority => Priority}}. + -spec provider_ref(t()) -> provider_ref(). -provider_ref(#route{provider_ref = Ref}) -> +provider_ref(#{provider_ref := Ref}) -> Ref. +-spec route_data(t()) -> route_data(). +route_data(#{route_data := Data}) -> + Data. + -spec terminal_ref(t()) -> terminal_ref(). -terminal_ref(#route{terminal_ref = Ref}) -> +terminal_ref(#{terminal_ref := Ref}) -> Ref. +-spec payment_route(t()) -> payment_route(). +payment_route(Route) -> + to_payment_route(Route). + -spec priority(t()) -> integer(). -priority(#route{priority = Priority}) -> +priority(#{route_data := #{priority := Priority}}) -> Priority. -spec weight(t()) -> integer(). -weight(#route{weight = Weight}) -> +weight(#{route_data := #{weight := Weight}}) -> Weight. -spec pin(t()) -> pin() | undefined. -pin(#route{pin = Pin}) -> - Pin. +pin(#{pin_data := Pin}) -> + Pin; +pin(_) -> + #{}. + +-spec pin_hash(t()) -> integer(). +pin_hash(#{pin_data := Pin}) when map_size(Pin) > 0 -> + erlang:phash2(Pin); +pin_hash(_) -> + 0. -spec fd_overrides(t()) -> fd_overrides(). -fd_overrides(#route{fd_overrides = FdOverrides}) -> - FdOverrides. +fd_overrides(#{fd_overrides := FdOverrides}) -> + FdOverrides; +fd_overrides(_) -> + #domain_RouteFaultDetectorOverrides{}. + +-spec fd_score(t()) -> fd_score(). +fd_score(#{route_data := #{fd_score := Score}}) -> + Score; +fd_score(_) -> + undefined. --spec set_weight(integer(), t()) -> t(). -set_weight(Weight, Route) -> - Route#route{weight = Weight}. +-spec blacklisted(t()) -> integer(). +blacklisted(#{route_data := #{blacklisted := Blacklisted}}) -> + Blacklisted; +blacklisted(_) -> + 0. + +-spec score(t()) -> score(). +score(Route) -> + #{ + availability_condition := AvailabilityCondition, + conversion_condition := ConversionCondition, + availability := Availability, + conversion := Conversion + } = fd_score(Route), + #domain_PaymentRouteScores{ + availability_condition = AvailabilityCondition, + conversion_condition = ConversionCondition, + terminal_priority_rating = priority(Route), + route_pin = pin_hash(Route), + random_condition = weight(Route), + availability = Availability, + conversion = Conversion, + blacklist_condition = blacklisted(Route) + }. -spec equal(R, R) -> boolean() when R :: t() | rejected_route() | payment_route() | {provider_ref(), terminal_ref()}. @@ -132,7 +257,7 @@ from_payment_route(Route) -> new(ProviderRef, TerminalRef). -spec to_payment_route(t()) -> payment_route(). -to_payment_route(#route{} = Route) -> +to_payment_route(Route) -> ?route(provider_ref(Route), terminal_ref(Route)). -spec to_rejected_route(t(), route_rejection_reason()) -> rejected_route(). @@ -146,7 +271,7 @@ routes_equal_(A, A) when A =/= undefined -> routes_equal_(_A, _B) -> false. -route_ref(#route{provider_ref = Prv, terminal_ref = Trm}) -> +route_ref(#{provider_ref := Prv, terminal_ref := Trm}) -> {Prv, Trm}; route_ref(#domain_PaymentRoute{provider = Prv, terminal = Trm}) -> {Prv, Trm}; diff --git a/apps/routing/src/hg_route_balancer.erl b/apps/routing/src/hg_route_balancer.erl new file mode 100644 index 00000000..f2b4a9b3 --- /dev/null +++ b/apps/routing/src/hg_route_balancer.erl @@ -0,0 +1,61 @@ +-module(hg_route_balancer). + +-export([fill/1]). + +-type route() :: hg_route:t(). +-type terminal_priority_rating() :: integer(). +-type availability_condition() :: integer(). +-type route_groups_by_priority() :: #{{availability_condition(), terminal_priority_rating()} => [route()]}. + +-spec fill([route()]) -> [route()]. +fill(Routes) -> + RouteGroups = lists:foldl( + fun group_routes_by_priority/2, + #{}, + Routes + ), + balance_route_groups(RouteGroups). + +-spec group_routes_by_priority(route(), route_groups_by_priority()) -> route_groups_by_priority(). +group_routes_by_priority(Route, RouteGroups) -> + Priority = hg_route:priority(Route), + #{availability_condition := AvailabilityCondition} = hg_route:fd_score(Route), + GroupKey = {AvailabilityCondition, Priority}, + GroupRoutes = maps:get(GroupKey, RouteGroups, []), + RouteGroups#{GroupKey => [Route | GroupRoutes]}. + +-spec balance_route_groups(route_groups_by_priority()) -> [route()]. +balance_route_groups(RouteGroups) -> + maps:fold( + fun(_GroupKey, Routes, Acc) -> + balance_group_routes(Routes) ++ Acc + end, + [], + RouteGroups + ). + +balance_group_routes(Routes) -> + SummaryWeight = get_summary_weight(Routes), + Random = rand:uniform() * SummaryWeight, + lists:reverse(calc_random_condition(0.0, Random, Routes, [])). + +get_summary_weight(Routes) -> + lists:foldl( + fun(Route, Acc) -> + Acc + hg_route:weight(Route) + end, + 0, + Routes + ). + +calc_random_condition(_, _, [], Routes) -> + Routes; +calc_random_condition(StartFrom, Random, [Route | Rest], Routes) -> + Weight = hg_route:weight(Route), + InRange = (Random >= StartFrom) and (Random < StartFrom + Weight), + UpdatedRoute = + case InRange of + true -> hg_route:set_weight(1, Route); + false -> hg_route:set_weight(0, Route) + end, + calc_random_condition(StartFrom + Weight, Random, Rest, [UpdatedRoute | Routes]). diff --git a/apps/routing/src/hg_route_collector.erl b/apps/routing/src/hg_route_collector.erl new file mode 100644 index 00000000..4ac5e8af --- /dev/null +++ b/apps/routing/src/hg_route_collector.erl @@ -0,0 +1,366 @@ +-module(hg_route_collector). + +-include_lib("damsel/include/dmsl_domain_thrift.hrl"). +-include_lib("damsel/include/dmsl_payproc_thrift.hrl"). +-include_lib("hellgate/include/domain.hrl"). + +-export([fill_blacklist/2]). +-export([fill_fd_overrides/2]). +-export([fill_prohibition/4]). +-export([fill_accepted/4]). +-export([get_routes/4]). + +-type payment_institution() :: dmsl_domain_thrift:'PaymentInstitution'(). +-type route_predestination() :: payment | recurrent_payment. +-type varset() :: hg_varset:varset(). +-type revision() :: hg_domain:revision(). + +-type currency() :: dmsl_domain_thrift:'CurrencyRef'(). +-type payment_tool() :: dmsl_domain_thrift:'PaymentTool'(). +-type client_ip() :: dmsl_domain_thrift:'IPAddress'(). +-type email() :: binary(). +-type card_token() :: dmsl_domain_thrift:'Token'(). + +-type gather_route_context() :: #{ + currency := currency(), + payment_tool := payment_tool(), + client_ip := client_ip() | undefined, + email => email() | undefined, + card_token => card_token() | undefined +}. + +-type blacklist_context() :: hg_inspector:blacklist_context(). + +-export_type([payment_institution/0]). +-export_type([route_predestination/0]). +-export_type([varset/0]). +-export_type([revision/0]). +-export_type([blacklist_context/0]). +-export_type([gather_route_context/0]). + +-define(fd_overrides(Enabled), #domain_RouteFaultDetectorOverrides{enabled = Enabled}). +-define(rejected(Reason), {rejected, Reason}). + +-spec fill_blacklist(hg_inspector:blacklist_context(), [hg_route:t()]) -> [hg_route:t()]. +fill_blacklist(_BlCtx, []) -> + []; +fill_blacklist(BlCtx, Routes) -> + [ + hg_route:set_blacklisted(hg_inspector:check_blacklist(BlCtx#{route => Route}), Route) + || Route <- Routes + ]. + +-spec fill_fd_overrides(revision(), [hg_route:t()]) -> [hg_route:t()]. +fill_fd_overrides(Revision, Routes) -> + lists:foldr( + fun(Route, Acc) -> + TerminalRef = hg_route:terminal_ref(Route), + FdOverrides = get_provider_fd_overrides(Revision, TerminalRef), + [hg_route:set_fd_overrides(FdOverrides, Route) | Acc] + end, + [], + Routes + ). + +get_provider_fd_overrides(Revision, TerminalRef) -> + #domain_Terminal{provider_ref = ProviderRef, route_fd_overrides = TrmFdOverrides} = + hg_domain:get(Revision, {terminal, TerminalRef}), + #domain_Provider{route_fd_overrides = PrvFdOverrides} = + hg_domain:get(Revision, {provider, ProviderRef}), + merge_fd_overrides(PrvFdOverrides, TrmFdOverrides). + +merge_fd_overrides(_A, B = ?fd_overrides(Enabled)) when Enabled =/= undefined -> + B; +merge_fd_overrides(A = ?fd_overrides(Enabled), _B) when Enabled =/= undefined -> + A; +merge_fd_overrides(_A, _B) -> + ?fd_overrides(undefined). + +-spec fill_prohibition(revision(), varset(), payment_institution(), [hg_route:t()]) -> [hg_route:t()]. +fill_prohibition(Revision, VS, #domain_PaymentInstitution{payment_routing_rules = RoutingRules}, Routes) -> + #domain_RoutingRules{prohibitions = Prohibitions} = RoutingRules, + Table = get_table_prohibitions(Prohibitions, VS, Revision), + lists:foldr( + fun(Route, Acc) -> + TerminalRef = hg_route:terminal_ref(Route), + case maps:find(TerminalRef, Table) of + error -> + [Route | Acc]; + {ok, Description} -> + [hg_route:set_prohibit({true, Description}, Route) | Acc] + end + end, + [], + Routes + ). + +get_table_prohibitions(Prohibitions, VS, Revision) -> + RuleSetDeny = compute_rule_set(Prohibitions, VS, Revision), + lists:foldr( + fun(#domain_RoutingCandidate{terminal = TerminalRef, description = Description}, Acc) -> + Acc#{TerminalRef => Description} + end, + #{}, + get_decisions_candidates(RuleSetDeny) + ). + +-spec fill_accepted(route_predestination(), revision(), varset(), [hg_route:t()]) -> [hg_route:t()]. +fill_accepted(Predestination, Revision, VS, Routes) -> + lists:foldr( + fun(Route, Acc) -> + ProviderRef = hg_route:provider_ref(Route), + TerminalRef = hg_route:terminal_ref(Route), + try + true = acceptable_terminal(Predestination, ProviderRef, TerminalRef, VS, Revision), + [Route | Acc] + catch + {rejected, Reason} -> + [hg_route:set_accepted({false, {rejected, Reason}}, Route) | Acc]; + error:{misconfiguration, Reason} -> + [hg_route:set_accepted({false, {misconfiguration, Reason}}, Route) | Acc] + end + end, + [], + Routes + ). + +-spec get_routes(revision(), varset(), payment_institution(), gather_route_context()) -> [hg_route:t()]. +get_routes(_, _, #domain_PaymentInstitution{payment_routing_rules = undefined}, _) -> + []; +get_routes(Revision, VS, #domain_PaymentInstitution{payment_routing_rules = RoutingRules}, Ctx) -> + #domain_RoutingRules{policies = Policies} = RoutingRules, + Candidates = get_candidates(Policies, VS, Revision), + collect_routes(Candidates, Revision, Ctx). + +get_candidates(RoutingRule, VS, Revision) -> + get_decisions_candidates( + compute_rule_set(RoutingRule, VS, Revision) + ). + +get_decisions_candidates(#domain_RoutingRuleset{decisions = Decisions}) -> + case Decisions of + {delegates, _Delegates} -> + throw({misconfiguration, {routing_decisions, Decisions}}); + {candidates, Candidates} -> + ok = validate_decisions_candidates(Candidates), + Candidates + end. + +compute_rule_set(RuleSetRef, VS, Revision) -> + {Client, Context} = get_party_client(), + {ok, RuleSet} = party_client_thrift:compute_routing_ruleset( + RuleSetRef, + Revision, + hg_varset:prepare_varset(VS), + Client, + Context + ), + RuleSet. + +validate_decisions_candidates([]) -> + ok; +validate_decisions_candidates([#domain_RoutingCandidate{allowed = {constant, true}} | Rest]) -> + validate_decisions_candidates(Rest); +validate_decisions_candidates([Candidate | _]) -> + throw({misconfiguration, {routing_candidate, Candidate}}). + +collect_routes(Candidates, Revision, Ctx) -> + lists:foldr( + fun(Candidate, Routes) -> + #domain_RoutingCandidate{ + terminal = TerminalRef, + priority = Priority, + weight = Weight, + pin = Pin + } = Candidate, + #domain_Terminal{provider_ref = ProviderRef} = hg_domain:get(Revision, {terminal, TerminalRef}), + GatheredPinInfo = gather_pin_info(Pin, Ctx), + Route = hg_route:new(ProviderRef, TerminalRef, Weight, Priority, GatheredPinInfo), + [Route | Routes] + end, + [], + Candidates + ). + +gather_pin_info(undefined, _Ctx) -> + #{}; +gather_pin_info(#domain_RoutingPin{features = Features}, Ctx) -> + FeaturesList = ordsets:to_list(Features), + lists:foldl( + fun(Feature, Acc) -> + Acc#{Feature => maps:get(Feature, Ctx, undefined)} + end, + #{}, + FeaturesList + ). + +-spec acceptable_terminal( + route_predestination(), + hg_route:provider_ref(), + hg_route:terminal_ref(), + varset(), + revision() +) -> true | no_return(). +acceptable_terminal(Predestination, ProviderRef, TerminalRef, VS, Revision) -> + {Client, Context} = get_party_client(), + Result = party_client_thrift:compute_provider_terminal_terms( + ProviderRef, + TerminalRef, + Revision, + hg_varset:prepare_varset(VS), + Client, + Context + ), + case Result of + {ok, ProvisionTermSet} -> + check_terms_acceptability(Predestination, ProvisionTermSet, VS); + {error, #payproc_ProvisionTermSetUndefined{}} -> + throw(?rejected({'ProvisionTermSet', undefined})) + end. + +get_party_client() -> + HgContext = hg_context:load(), + Client = hg_context:get_party_client(HgContext), + Context = hg_context:get_party_client_context(HgContext), + {Client, Context}. + +check_terms_acceptability(payment, Terms, VS) -> + acceptable_payment_terms(Terms#domain_ProvisionTermSet.payments, VS); +check_terms_acceptability(recurrent_payment, Terms, VS) -> + _ = acceptable_payment_terms(Terms#domain_ProvisionTermSet.payments, VS), + case Terms#domain_ProvisionTermSet.extension of + #domain_ExtendedProvisionTerms{skip_recurrent = true} -> + true; + _ -> + acceptable_recurrent_paytool_terms(Terms#domain_ProvisionTermSet.recurrent_paytools, VS) + end. + +acceptable_payment_terms( + #domain_PaymentsProvisionTerms{ + allow = Allow, + global_allow = GlobalAllow, + currencies = CurrenciesSelector, + categories = CategoriesSelector, + payment_methods = PMsSelector, + cash_limit = CashLimitSelector, + holds = HoldsTerms, + refunds = RefundsTerms, + risk_coverage = RiskCoverageSelector + }, + VS +) -> + ParentName = 'PaymentsProvisionTerms', + _ = acceptable_allow(ParentName, global_allow, GlobalAllow), + _ = acceptable_allow(ParentName, allow, Allow), + _ = try_accept_term(ParentName, currency, getv(currency, VS), CurrenciesSelector), + _ = try_accept_term(ParentName, category, getv(category, VS), CategoriesSelector), + _ = try_accept_term(ParentName, payment_tool, getv(payment_tool, VS), PMsSelector), + _ = try_accept_term(ParentName, cost, getv(cost, VS), CashLimitSelector), + _ = acceptable_holds_terms(HoldsTerms, getv(flow, VS, undefined)), + _ = acceptable_refunds_terms(RefundsTerms, getv(refunds, VS, undefined)), + _ = acceptable_risk(ParentName, RiskCoverageSelector, VS), + true; +acceptable_payment_terms(undefined, _VS) -> + throw(?rejected({'PaymentsProvisionTerms', undefined})). + +acceptable_holds_terms(_Terms, undefined) -> + true; +acceptable_holds_terms(_Terms, instant) -> + true; +acceptable_holds_terms(Terms, {hold, Lifetime}) -> + case Terms of + #domain_PaymentHoldsProvisionTerms{lifetime = LifetimeSelector} -> + _ = try_accept_term('PaymentHoldsProvisionTerms', lifetime, Lifetime, LifetimeSelector), + true; + undefined -> + throw(?rejected({'PaymentHoldsProvisionTerms', undefined})) + end. + +acceptable_risk(_ParentName, undefined, _VS) -> + true; +acceptable_risk(ParentName, Selector, VS) -> + RiskCoverage = get_selector_value(risk_coverage, Selector), + RiskScore = getv(risk_score, VS), + hg_inspector:compare_risk_score(RiskCoverage, RiskScore) >= 0 orelse + throw(?rejected({ParentName, risk_coverage})). + +acceptable_refunds_terms(_Terms, undefined) -> + true; +acceptable_refunds_terms( + #domain_PaymentRefundsProvisionTerms{ + partial_refunds = PartialRefundsTerms + }, + RVS +) -> + _ = acceptable_partial_refunds_terms( + PartialRefundsTerms, + getv(partial, RVS, undefined) + ), + true; +acceptable_refunds_terms(undefined, _RVS) -> + throw(?rejected({'PaymentRefundsProvisionTerms', undefined})). + +acceptable_partial_refunds_terms(_Terms, undefined) -> + true; +acceptable_partial_refunds_terms( + #domain_PartialRefundsProvisionTerms{cash_limit = CashLimitSelector}, + #{cash_limit := MerchantLimit} +) -> + ProviderLimit = get_selector_value(cash_limit, CashLimitSelector), + hg_cash_range:is_subrange(MerchantLimit, ProviderLimit) == true orelse + throw(?rejected({'PartialRefundsProvisionTerms', cash_limit})); +acceptable_partial_refunds_terms(undefined, _RVS) -> + throw(?rejected({'PartialRefundsProvisionTerms', undefined})). + +acceptable_allow(_ParentName, _Type, undefined) -> + true; +acceptable_allow(_ParentName, _Type, {constant, true}) -> + true; +acceptable_allow(ParentName, Type, {constant, false}) -> + throw(?rejected({ParentName, Type})); +acceptable_allow(_ParentName, Type, Ambiguous) -> + erlang:error({misconfiguration, {'Could not reduce predicate to a value', {Type, Ambiguous}}}). + +acceptable_recurrent_paytool_terms( + #domain_RecurrentPaytoolsProvisionTerms{ + categories = CategoriesSelector, + payment_methods = PMsSelector + }, + VS +) -> + _ = try_accept_term('RecurrentPaytoolsProvisionTerms', category, getv(category, VS), CategoriesSelector), + _ = try_accept_term('RecurrentPaytoolsProvisionTerms', payment_tool, getv(payment_tool, VS), PMsSelector), + true; +acceptable_recurrent_paytool_terms(undefined, _VS) -> + throw(?rejected({'RecurrentPaytoolsProvisionTerms', undefined})). + +try_accept_term(ParentName, Name, _Value, undefined) -> + throw(?rejected({ParentName, Name})); +try_accept_term(ParentName, Name, Value, Selector) -> + Values = get_selector_value(Name, Selector), + test_term(Name, Value, Values) orelse throw(?rejected({ParentName, Name})). + +test_term(currency, V, Vs) -> + ordsets:is_element(V, Vs); +test_term(category, V, Vs) -> + ordsets:is_element(V, Vs); +test_term(payment_tool, PT, PMs) -> + hg_payment_tool:has_any_payment_method(PT, PMs); +test_term(cost, Cost, CashRange) -> + hg_cash_range:is_inside(Cost, CashRange) == within; +test_term(lifetime, ?hold_lifetime(Lifetime), ?hold_lifetime(Allowed)) -> + Lifetime =< Allowed. + +get_selector_value(Name, Selector) -> + case Selector of + {value, V} -> + V; + Ambiguous -> + erlang:error({misconfiguration, {'Could not reduce selector to a value', {Name, Ambiguous}}}) + end. + +getv(Name, VS) -> + maps:get(Name, VS). + +getv(Name, VS, Default) -> + maps:get(Name, VS, Default). diff --git a/apps/routing/src/hg_route_fd.erl b/apps/routing/src/hg_route_fd.erl new file mode 100644 index 00000000..6ae003e9 --- /dev/null +++ b/apps/routing/src/hg_route_fd.erl @@ -0,0 +1,74 @@ +-module(hg_route_fd). + +-include_lib("damsel/include/dmsl_domain_thrift.hrl"). +-include_lib("fault_detector_proto/include/fd_proto_fault_detector_thrift.hrl"). + +-export([fill/1]). + +-type route() :: hg_route:t(). + +-define(fd_overrides(Enabled), #domain_RouteFaultDetectorOverrides{enabled = Enabled}). + +-spec fill([route()]) -> [route()]. +fill([]) -> + []; +fill(Routes) -> + RouteMap = build_route_map(Routes), + ServiceIDs = [Key || Key <- maps:keys(RouteMap), is_binary(Key)], + FDStats = hg_fault_detector_client:get_statistics(ServiceIDs), + UpdatedMap = lists:foldl(fun fill_fd_score/2, RouteMap, FDStats), + [maps:get(Route, UpdatedMap) || Route <- Routes]. + +build_route_map(Routes) -> + lists:foldl( + fun(Route, Acc) -> + #domain_ProviderRef{id = ID} = hg_route:provider_ref(Route), + AvailabilityID = hg_fault_detector_client:build_service_id(adapter_availability, ID), + ConversionID = hg_fault_detector_client:build_service_id(provider_conversion, ID), + Acc#{ + AvailabilityID => {availability, Route}, + ConversionID => {conversion, Route}, + Route => Route + } + end, + #{}, + Routes + ). + +fill_fd_score(#fault_detector_ServiceStatistics{service_id = ID, failure_rate = FailRate}, RouteMap) -> + case maps:get(ID, RouteMap, undefined) of + undefined -> + RouteMap; + {availability, RouteRef} -> + Route = maps:get(RouteRef, RouteMap), + AvailabilityConfig = maps:get(availability, genlib_app:env(hellgate, fault_detector, #{}), #{}), + CriticalFailRate = maps:get(critical_fail_rate, AvailabilityConfig, 0.7), + {Condition, Value} = calc_rate(FailRate >= CriticalFailRate, FailRate), + UpdatedRoute = maybe_override( + hg_route:fd_overrides(Route), + hg_route:set_availability(Condition, Value, Route), + Route + ), + RouteMap#{RouteRef => UpdatedRoute}; + {conversion, RouteRef} -> + Route = maps:get(RouteRef, RouteMap), + ConversionConfig = maps:get(conversion, genlib_app:env(hellgate, fault_detector, #{}), #{}), + CriticalFailRate = maps:get(critical_fail_rate, ConversionConfig, 0.7), + {Condition, Value} = calc_rate(FailRate >= CriticalFailRate, FailRate), + UpdatedRoute = maybe_override( + hg_route:fd_overrides(Route), + hg_route:set_conversion(Condition, Value, Route), + Route + ), + RouteMap#{RouteRef => UpdatedRoute} + end. + +maybe_override(?fd_overrides(true), _UpdatedRoute, Route) -> + Route; +maybe_override(_, UpdatedRoute, _Route) -> + UpdatedRoute. + +calc_rate(true, FailRate) -> + {0, 1.0 - FailRate}; +calc_rate(false, FailRate) -> + {1, 1.0 - FailRate}. diff --git a/apps/routing/src/hg_routing.erl b/apps/routing/src/hg_routing.erl index 5b43168b..7d2163f1 100644 --- a/apps/routing/src/hg_routing.erl +++ b/apps/routing/src/hg_routing.erl @@ -2,14 +2,18 @@ -module(hg_routing). +-compile({no_auto_import, [error/1]}). + -include_lib("damsel/include/dmsl_domain_thrift.hrl"). -include_lib("damsel/include/dmsl_payproc_thrift.hrl"). -include_lib("fault_detector_proto/include/fd_proto_fault_detector_thrift.hrl"). -include_lib("hellgate/include/domain.hrl"). +-export([get_routes/1]). +-export([filter_routes/2]). -export([gather_routes/5]). +-export([resolve/1]). -export([rate_routes/1]). --export([choose_route/1]). -export([choose_rated_route/1]). -export([get_payment_terms/3]). @@ -23,6 +27,16 @@ -export([filter_by_critical_provider_status/1]). -export([filter_by_blacklist/2]). -export([choose_route_with_ctx/1]). +-export([get_error/1]). +-export([rejected_routes/1]). +-export([rejections/1]). +-export([candidates/1]). +-export([considered_candidates/1]). +-export([accounted_candidates/1]). +-export([chosen_route/1]). +-export([choice_meta/1]). +-export([route_scores/1]). +-export([route_limits/1]). %% @@ -30,6 +44,14 @@ -type payment_terms() :: dmsl_domain_thrift:'PaymentsProvisionTerms'(). -type payment_institution() :: dmsl_domain_thrift:'PaymentInstitution'(). -type route_predestination() :: payment | recurrent_payment. +-type get_route_params() :: #{ + predestination := route_predestination(), + revision := revision(), + varset := varset(), + payment_institution := payment_institution(), + pin_context := gather_route_context(), + blacklist_context => hg_route_collector:blacklist_context() +}. -define(rejected(Reason), {rejected, Reason}). @@ -37,8 +59,6 @@ -define(ZERO, 0). --type fd_service_stats() :: fd_proto_fault_detector_thrift:'ServiceStatistics'(). - -type terminal_priority_rating() :: integer(). -type provider_status() :: {availability_status(), conversion_status()}. @@ -87,77 +107,293 @@ -type limits() :: #{hg_route:payment_route() => [hg_limiter:turnover_limit_value()]}. -type scores() :: #{hg_route:payment_route() => hg_routing:route_scores()}. -type misconfiguration_error() :: {misconfiguration, {routing_decisions, _} | {routing_candidate, _}}. +-type rejection_group() :: atom(). +-type result() :: #{ + initial_candidates := [hg_route:t()], + candidates := [hg_route:t()], + rejections := #{rejection_group() => [hg_route:rejected_route()]}, + latest_rejection := rejection_group() | undefined, + error := term() | undefined, + chosen_route := hg_route:t() | undefined, + choice_meta := route_choice_context() | undefined, + stashed_candidates => [hg_route:t()], + fail_rates => [fail_rated_route()], + route_limits => limits(), + route_scores => scores() +}. -export_type([route_predestination/0]). +-export_type([get_route_params/0]). -export_type([route_choice_context/0]). -export_type([fail_rated_route/0]). -export_type([blacklisted_route/0]). -export_type([route_scores/0]). -export_type([limits/0]). -export_type([scores/0]). +-export_type([result/0]). %% --spec filter_by_critical_provider_status(T) -> T when T :: hg_routing_ctx:t(). -filter_by_critical_provider_status(Ctx0) -> - RoutesFailRates = rate_routes(hg_routing_ctx:candidates(Ctx0)), - RouteScores = score_routes_map(RoutesFailRates), - Ctx1 = hg_routing_ctx:stash_route_scores(RouteScores, Ctx0), - 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, _ProviderStatus}, C) -> - C +-spec get_routes(get_route_params()) -> [hg_route:t()]. +get_routes(Params) -> + Routes0 = get_base_routes(Params), + Routes1 = maybe_fill_blacklist(maps:get(blacklist_context, Params, undefined), Routes0), + Routes2 = hg_route_fd:fill(Routes1), + hg_route_balancer:fill(Routes2). + +-spec filter_routes([hg_route:t()], [fun(([hg_route:t()]) -> [hg_route:t()])]) -> [hg_route:t()]. +filter_routes(Routes0, WithFilterFuns) -> + Routes1 = filter_flagged_routes(Routes0, [{accepted, false}, {prohibit, true}, {blacklisted, 1}]), + lists:foldr(fun(Fun, Routes) -> Fun(Routes) end, Routes1, WithFilterFuns). + +-spec resolve(map()) -> result(). +resolve(Params) -> + Result0 = + case maps:find(routing, Params) of + {ok, Routing} -> + Routing; + error -> + build_initial_result(Params) end, - hg_routing_ctx:with_fail_rates(RoutesFailRates, Ctx1), - RoutesFailRates - ). + case get_error(Result0) of + undefined -> + AttemptedRoutes = maps:get(attempted_routes, Params, []), + LimitHoldFun = maps:get(limit_hold_fun, Params, undefined), + LimitOverflowFun = maps:get(limit_overflow_fun, Params, undefined), + BlacklistContext = maps:get(blacklist_context, Params, undefined), + Result1 = reject_attempted_routes(AttemptedRoutes, Result0), + Result2 = process_result(Result1, fun(R) -> apply_limit_hold(LimitHoldFun, R) end), + Result3 = process_result(Result2, fun(R) -> apply_limit_overflow(LimitOverflowFun, R) end), + Result4 = process_result(Result3, fun(R) -> apply_blacklist(BlacklistContext, R) end), + Result5 = process_result(Result4, fun filter_by_critical_provider_status/1), + process_result(Result5, fun choose_route_with_ctx/1); + _ -> + Result0 + end. --spec filter_by_blacklist(T, hg_inspector:blacklist_context()) -> T when T :: hg_routing_ctx:t(). -filter_by_blacklist(Ctx, BlCtx) -> - BlacklistedRoutes = check_routes(hg_routing_ctx:candidates(Ctx), BlCtx), - lists:foldr( +-spec filter_by_critical_provider_status(result()) -> result(). +filter_by_critical_provider_status(Result0) -> + RoutesFailRates0 = rate_routes(candidates(Result0)), + RouteScores0 = score_routes_map(RoutesFailRates0), + Result1 = with_fail_rates(RoutesFailRates0, stash_route_scores(RouteScores0, Result0)), + Result2 = lists:foldr( fun - ({R, true = Status}, C) -> - R1 = hg_route:to_rejected_route(R, {'InBlackList', Status}), - Ctx0 = hg_routing_ctx:reject(in_blacklist, R1, C), - Scores0 = score_route(R), - Scores1 = Scores0#domain_PaymentRouteScores{blacklist_condition = 1}, - hg_routing_ctx:add_route_scores({hg_route:to_payment_route(R), Scores1}, Ctx0); - ({_R, _ProviderStatus}, C) -> - C + ({R, {{dead, _} = AvailabilityStatus, _ConversionStatus}}, Acc) -> + RejectedRoute = hg_route:to_rejected_route(R, {'ProviderDead', AvailabilityStatus}), + reject(adapter_unavailable, RejectedRoute, Acc); + ({_R, _ProviderStatus}, Acc) -> + Acc end, - Ctx, - BlacklistedRoutes + Result1, + RoutesFailRates0 + ), + BalancedRoutes = hg_route_balancer:fill(candidates(Result2)), + RouteScores1 = routes_scores_map(BalancedRoutes), + set_candidates(BalancedRoutes, stash_route_scores(RouteScores1, Result2)). + +-spec filter_by_blacklist(result(), hg_inspector:blacklist_context()) -> result(). +filter_by_blacklist(Result, undefined) -> + Result; +filter_by_blacklist(Result, BlCtx) -> + Routes = hg_route_collector:fill_blacklist(BlCtx, candidates(Result)), + Result1 = set_candidates(Routes, Result), + lists:foldr( + fun(Route, Acc) -> + case hg_route:blacklisted(Route) of + 1 -> + RejectedRoute = hg_route:to_rejected_route(Route, {'InBlackList', true}), + stash_route_scores(routes_scores_map([Route]), reject(in_blacklist, RejectedRoute, Acc)); + _ -> + Acc + end + end, + Result1, + Routes ). --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 +-spec choose_route_with_ctx(result()) -> result(). +choose_route_with_ctx(Result) -> + Candidates = candidates(Result), + {ChosenRoute, ChoiceContext} = + case maps:get(fail_rates, Result, undefined) of undefined -> - choose_route(Candidates); + choose_prepared_route(Candidates); FailRates -> RatedCandidates = filter_rated_routes_with_candidates(FailRates, Candidates), choose_rated_route(RatedCandidates) end, - hg_routing_ctx:set_choosen(ChoosenRoute, ChoiceContext, Ctx). + set_chosen(ChosenRoute, ChoiceContext, Result). 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 + fun({Route, _ProviderStatus} = FailRatedRoute, Acc) -> + case lists:any(fun(Candidate) -> hg_route:equal(Candidate, Route) end, Candidates) of + true -> + [FailRatedRoute | Acc]; + false -> + Acc end end, [], FailRates ). +new(Candidates0) -> + #{ + initial_candidates => Candidates0, + candidates => Candidates0, + rejections => #{}, + latest_rejection => undefined, + error => undefined, + chosen_route => undefined, + choice_meta => undefined + }. + +build_initial_result(#{predefined_routes := Routes}) -> + new(Routes); +build_initial_result(#{ + predestination := Predestination, + payment_institution := PaymentInstitution, + varset := VS, + revision := Revision, + pin_context := PinCtx +}) -> + gather_routes(Predestination, PaymentInstitution, VS, Revision, PinCtx). + +with_fail_rates(FailRates, Result) -> + Result#{fail_rates => FailRates}. + +set_candidates(Candidates, Result) -> + Result#{candidates => Candidates}. + +set_chosen(Route, ChoiceMeta, Result) -> + Result#{chosen_route => Route, choice_meta => ChoiceMeta}. + +set_error(ErrorReason, Result) -> + Result#{error => ErrorReason}. + +-spec get_error(result()) -> term() | undefined. +get_error(#{error := ErrorReason}) -> + ErrorReason. + +-spec rejected_routes(result()) -> [hg_route:rejected_route()]. +rejected_routes(#{rejections := Rejections}) -> + {_, Rejected} = lists:unzip(maps:to_list(Rejections)), + lists:flatten(Rejected). + +-spec rejections(result()) -> [{atom(), [hg_route:rejected_route()]}]. +rejections(#{rejections := Rejections}) -> + maps:to_list(Rejections). + +-spec candidates(result()) -> [hg_route:t()]. +candidates(#{candidates := Candidates0}) -> + Candidates0. + +-spec considered_candidates(result()) -> [hg_route:t()]. +considered_candidates(Result) -> + maps:get(stashed_candidates, Result, candidates(Result)). + +-spec accounted_candidates(result()) -> [hg_route:t()]. +accounted_candidates(Result) -> + maps:get(stashed_candidates, Result, maps:get(initial_candidates, Result, [])). + +-spec chosen_route(result()) -> hg_route:t() | undefined. +chosen_route(#{chosen_route := Chosen}) -> + Chosen. + +-spec choice_meta(result()) -> route_choice_context() | undefined. +choice_meta(Result) -> + maps:get(choice_meta, Result, undefined). + +-spec route_scores(result()) -> scores() | undefined. +route_scores(Result) -> + maps:get(route_scores, Result, undefined). + +-spec route_limits(result()) -> limits() | undefined. +route_limits(Result) -> + maps:get(route_limits, Result, undefined). + +stash_current_candidates(#{candidates := []} = Result) -> + Result; +stash_current_candidates(Result) -> + Result#{stashed_candidates => candidates(Result)}. + +stash_route_limits(RouteLimits, Result) -> + Result#{route_limits => RouteLimits}. + +stash_route_scores(RouteScoresNew, #{route_scores := RouteScores0} = Result) -> + Result#{route_scores => maps:merge(RouteScores0, RouteScoresNew)}; +stash_route_scores(RouteScores0, Result) -> + Result#{route_scores => RouteScores0}. + +reject(GroupReason, RejectedRoute, #{rejections := Rejections0, candidates := Candidates0} = Result) -> + RejectedList = maps:get(GroupReason, Rejections0, []) ++ [RejectedRoute], + Result#{ + rejections := Rejections0#{GroupReason => RejectedList}, + candidates := exclude_route(RejectedRoute, Candidates0), + latest_rejection := GroupReason + }. + +exclude_route(Route, Routes) -> + lists:foldr( + fun(R, Acc) -> + case hg_route:equal(Route, R) of + true -> Acc; + false -> [R | Acc] + end + end, + [], + Routes + ). + +latest_rejected_routes(#{latest_rejection := ReasonGroup, rejections := Rejections}) -> + {ReasonGroup, maps:get(ReasonGroup, Rejections, [])}. + +with_guard(#{candidates := [], error := undefined} = Result) -> + Result#{error := {rejected_routes, latest_rejected_routes(Result)}}; +with_guard(Result) -> + Result. + +reject_attempted_routes([], Result) -> + Result; +reject_attempted_routes(AttemptedRoutes, Result) -> + with_guard( + lists:foldr( + fun(Route, Acc) -> + InnerRoute = hg_route:from_payment_route(Route), + RejectedRoute = hg_route:to_rejected_route(InnerRoute, {'AlreadyAttempted', undefined}), + reject(already_attempted, RejectedRoute, Acc) + end, + Result, + AttemptedRoutes + ) + ). + +apply_limit_hold(undefined, Result) -> + Result; +apply_limit_hold(Fun, Result0) -> + {_HeldRoutes, RejectedRoutes} = Fun(candidates(Result0)), + Result1 = lists:foldr(fun(Route, Acc) -> reject(limit_misconfiguration, Route, Acc) end, Result0, RejectedRoutes), + stash_current_candidates(with_guard(Result1)). + +apply_limit_overflow(undefined, Result) -> + Result; +apply_limit_overflow(Fun, Result0) -> + {_AllowedRoutes, RejectedRoutes, Limits} = Fun(candidates(Result0)), + Result1 = stash_route_limits(Limits, Result0), + with_guard(lists:foldr(fun(Route, Acc) -> reject(limit_overflow, Route, Acc) end, Result1, RejectedRoutes)). + +apply_blacklist(undefined, Result) -> + Result; +apply_blacklist(BlacklistContext, Result) -> + with_guard(filter_by_blacklist(Result, BlacklistContext)). + +process_result(Result, Fun) -> + case get_error(Result) of + undefined -> with_guard(Fun(Result)); + _ -> Result + end. + %% -spec prepare_log_message(misconfiguration_error()) -> {io:format(), [term()]}. @@ -169,159 +405,148 @@ prepare_log_message({misconfiguration, {routing_candidate, Candidate}}) -> %% -spec gather_routes(route_predestination(), payment_institution(), varset(), revision(), gather_route_context()) -> - hg_routing_ctx:t(). + result(). gather_routes(_, #domain_PaymentInstitution{payment_routing_rules = undefined}, _, _, _) -> - hg_routing_ctx:new([]); -gather_routes(Predestination, #domain_PaymentInstitution{payment_routing_rules = RoutingRules}, VS, Revision, Ctx) -> - #domain_RoutingRules{ - policies = Policies, - prohibitions = Prohibitions - } = RoutingRules, + new([]); +gather_routes(Predestination, PaymentInstitution, VS, Revision, PinCtx) -> try - Candidates = get_candidates(Policies, VS, Revision), - {Accepted, RejectedRoutes} = filter_routes( - collect_routes(Predestination, Candidates, VS, Revision, Ctx), - get_table_prohibitions(Prohibitions, VS, Revision) - ), - lists:foldr( - fun(R, C) -> hg_routing_ctx:reject(forbidden, R, C) end, - hg_routing_ctx:new(Accepted), - lists:reverse(RejectedRoutes) - ) + Routes = get_base_routes(#{ + predestination => Predestination, + payment_institution => PaymentInstitution, + varset => VS, + revision => Revision, + pin_context => PinCtx + }), + with_guard(build_result_from_base_routes(Routes)) catch throw:{misconfiguration, _Reason} = Error -> - hg_routing_ctx:set_error(Error, hg_routing_ctx:new([])) + set_error(Error, new([])) end. -get_table_prohibitions(Prohibitions, VS, Revision) -> - RuleSetDeny = compute_rule_set(Prohibitions, VS, Revision), - lists:foldr( - fun(#domain_RoutingCandidate{terminal = K, description = V}, AccIn) -> - AccIn#{K => V} +-spec get_base_routes(get_route_params()) -> [hg_route:t()]. +get_base_routes(#{ + predestination := Predestination, + payment_institution := PaymentInstitution, + varset := VS, + revision := Revision, + pin_context := PinCtx +}) -> + Routes0 = hg_route_collector:get_routes(Revision, VS, PaymentInstitution, PinCtx), + Routes1 = hg_route_collector:fill_accepted(Predestination, Revision, VS, Routes0), + Routes2 = hg_route_collector:fill_prohibition(Revision, VS, PaymentInstitution, Routes1), + hg_route_collector:fill_fd_overrides(Revision, Routes2). + +maybe_fill_blacklist(undefined, Routes) -> + Routes; +maybe_fill_blacklist(BlacklistContext, Routes) -> + hg_route_collector:fill_blacklist(BlacklistContext, Routes). + +build_result_from_base_routes(Routes) -> + lists:foldl( + fun(Route, ResultAcc) -> + case route_filter_rejection(Route) of + undefined -> + append_candidate(Route, ResultAcc); + RejectedRoute -> + reject(forbidden, RejectedRoute, ResultAcc) + end end, - #{}, - get_decisions_candidates(RuleSetDeny) + new([]), + Routes ). -get_candidates(RoutingRule, VS, Revision) -> - get_decisions_candidates( - compute_rule_set(RoutingRule, VS, Revision) - ). +append_candidate(Route, #{initial_candidates := Initial, candidates := Candidates} = Result) -> + Result#{ + initial_candidates := Initial ++ [Route], + candidates := Candidates ++ [Route] + }. -get_decisions_candidates(#domain_RoutingRuleset{decisions = Decisions}) -> - case Decisions of - {delegates, _Delegates} -> - throw({misconfiguration, {routing_decisions, Decisions}}); - {candidates, Candidates} -> - ok = validate_decisions_candidates(Candidates), - Candidates +route_filter_rejection(Route) -> + RouteData = hg_route:route_data(Route), + case maps:get(accepted, RouteData, true) of + false -> + hg_route:to_rejected_route(Route, {'RoutingRule', undefined}); + {false, {rejected, Reason}} -> + hg_route:to_rejected_route(Route, Reason); + {false, {misconfiguration, Reason}} -> + hg_route:to_rejected_route(Route, {'Misconfiguration', Reason}); + {false, Reason} -> + hg_route:to_rejected_route(Route, Reason); + _ -> + route_prohibit_rejection(Route, RouteData) end. -validate_decisions_candidates([]) -> - ok; -validate_decisions_candidates([#domain_RoutingCandidate{allowed = {constant, true}} | Rest]) -> - validate_decisions_candidates(Rest); -validate_decisions_candidates([Candidate | _]) -> - throw({misconfiguration, {routing_candidate, Candidate}}). +route_prohibit_rejection(Route, RouteData) -> + case maps:get(prohibit, RouteData, false) of + true -> + hg_route:to_rejected_route(Route, {'RoutingRule', undefined}); + {true, Description} -> + hg_route:to_rejected_route(Route, {'RoutingRule', Description}); + _ -> + undefined + end. -collect_routes(Predestination, Candidates, VS, Revision, Ctx) -> - lists:foldr( - fun(Candidate, {Accepted, Rejected}) -> - #domain_RoutingCandidate{ - terminal = TerminalRef, - priority = Priority, - weight = Weight, - pin = Pin - } = Candidate, - {ProviderRef, FdOverrides} = get_provider_fd_overrides(Revision, TerminalRef), - GatheredPinInfo = gather_pin_info(Pin, Ctx), - try - true = acceptable_terminal(Predestination, ProviderRef, TerminalRef, VS, Revision), - Route = hg_route:new(ProviderRef, TerminalRef, Weight, Priority, GatheredPinInfo, FdOverrides), - {[Route | Accepted], Rejected} - catch - {rejected, Reason} -> - {Accepted, [{ProviderRef, TerminalRef, Reason} | Rejected]}; - error:{misconfiguration, Reason} -> - {Accepted, [{ProviderRef, TerminalRef, {'Misconfiguration', Reason}} | Rejected]} - end +filter_flagged_routes(Routes, Keys) -> + lists:filter( + fun(Route) -> + RouteData = hg_route:route_data(Route), + not lists:any( + fun({Key, Value}) -> + route_data_matches(maps:get(Key, RouteData, undefined), Value) + end, + Keys + ) end, - {[], []}, - Candidates + Routes ). -get_provider_fd_overrides(Revision, TerminalRef) -> - % 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, route_fd_overrides = TrmFdOverrides} = - hg_domain:get(Revision, {terminal, TerminalRef}), - #domain_Provider{route_fd_overrides = PrvFdOverrides} = - hg_domain:get(Revision, {provider, ProviderRef}), - %% TODO Consider moving this logic to party-management before (or after) - %% internal route structure refactoring. - {ProviderRef, merge_fd_overrides(PrvFdOverrides, TrmFdOverrides)}. - -merge_fd_overrides(_A, B = ?fd_overrides(Enabled)) when Enabled =/= undefined -> - B; -merge_fd_overrides(A = ?fd_overrides(Enabled), _B) when Enabled =/= undefined -> - A; -merge_fd_overrides(_A, _B) -> - ?fd_overrides(undefined). - -gather_pin_info(undefined, _Ctx) -> - #{}; -gather_pin_info(#domain_RoutingPin{features = Features}, Ctx) -> - FeaturesList = ordsets:to_list(Features), - lists:foldl( - fun(Feature, Acc) -> - Acc#{Feature => maps:get(Feature, Ctx, undefined)} - end, - #{}, - FeaturesList - ). +route_data_matches(Value, Expected) when Value =:= Expected -> + true; +route_data_matches({Expected, _}, Expected) -> + true; +route_data_matches(_, _) -> + false. -filter_routes({Routes, Rejected}, Prohibitions) -> +routes_scores_map(Routes) -> lists:foldr( - fun(Route, {AccIn, RejectedIn}) -> - TRef = hg_route:terminal_ref(Route), - case maps:find(TRef, Prohibitions) of - error -> - {[Route | AccIn], RejectedIn}; - {ok, Description} -> - RejectedOut = [hg_route:to_rejected_route(Route, {'RoutingRule', Description}) | RejectedIn], - {AccIn, RejectedOut} - end + fun(Route, Acc) -> + Acc#{hg_route:to_payment_route(Route) => hg_route:score(Route)} end, - {[], Rejected}, + #{}, Routes ). -compute_rule_set(RuleSetRef, VS, Revision) -> - Ctx = hg_context:load(), - {ok, RuleSet} = party_client_thrift:compute_routing_ruleset( - RuleSetRef, - Revision, - hg_varset:prepare_varset(VS), - hg_context:get_party_client(Ctx), - hg_context:get_party_client_context(Ctx) - ), - RuleSet. +route_provider_status(Route) -> + #{ + availability_condition := AvailabilityCondition, + availability := Availability, + conversion_condition := ConversionCondition, + conversion := Conversion + } = hg_route:fd_score(Route), + { + map_availability_status(AvailabilityCondition, Availability), + map_conversion_status(ConversionCondition, Conversion) + }. --spec check_routes([hg_route:t()], hg_inspector:blacklist_context()) -> [blacklisted_route()]. -check_routes([], _BlCtx) -> - []; -check_routes(Routes, BlCtx) -> - [{R, hg_inspector:check_blacklist(BlCtx#{route => R})} || R <- Routes]. +map_availability_status(0, Availability) -> + {dead, 1.0 - Availability}; +map_availability_status(_, Availability) -> + {alive, 1.0 - Availability}. + +map_conversion_status(0, Conversion) -> + {lacking, 1.0 - Conversion}; +map_conversion_status(_, Conversion) -> + {normal, 1.0 - Conversion}. -spec rate_routes([hg_route:t()]) -> [fail_rated_route()]. rate_routes(Routes) -> score_routes_with_fault_detector(Routes). --spec choose_route([hg_route:t()]) -> {hg_route:t(), route_choice_context()}. -choose_route(Routes) -> - FailRatedRoutes = rate_routes(Routes), - choose_rated_route(FailRatedRoutes). +choose_prepared_route([Route]) -> + {Route, #{chosen_route => Route}}; +choose_prepared_route([First | Rest]) -> + {ChosenRoute, IdealRoute} = find_best_prepared_routes(Rest, {First, First}), + {ChosenRoute, get_prepared_route_choice_context(ChosenRoute, IdealRoute)}. -spec choose_rated_route([fail_rated_route()]) -> {hg_route:t(), route_choice_context()}. choose_rated_route(FailRatedRoutes) -> @@ -332,6 +557,76 @@ choose_rated_route(FailRatedRoutes) -> {_, Route} = ChosenScoredRoute, {Route, RouteChoiceContext}. +find_best_prepared_routes([], Routes) -> + Routes; +find_best_prepared_routes([RouteIn | Rest], {CurrentChosen, CurrentIdeal}) -> + NewIdeal = select_better_prepared_route_ideal(RouteIn, CurrentIdeal), + NewChosen = select_better_prepared_route(RouteIn, CurrentChosen), + find_best_prepared_routes(Rest, {NewChosen, NewIdeal}). + +select_better_prepared_route_ideal(Left, Right) -> + IdealLeft = set_ideal_route_score(Left), + IdealRight = set_ideal_route_score(Right), + case select_better_prepared_route(IdealLeft, IdealRight) of + IdealLeft -> Left; + IdealRight -> Right + end. + +set_ideal_route_score(Route0) -> + Route1 = hg_route:set_availability(1, 1.0, Route0), + hg_route:set_conversion(1, 1.0, Route1). + +select_better_prepared_route(Left, Right) -> + LeftPin = hg_route:pin_hash(Left), + RightPin = hg_route:pin_hash(Right), + case {LeftPin, RightPin} of + _ when LeftPin /= ?ZERO, RightPin /= ?ZERO, RightPin =:= LeftPin -> + select_better_prepared_pinned_route(Left, Right); + _ -> + select_better_prepared_regular_route(Left, Right) + end. + +select_better_prepared_pinned_route(Left, Right) -> + LeftScore = (hg_route:score(Left))#domain_PaymentRouteScores{ + random_condition = 0, + route_pin = erlang:phash2({ + hg_route:pin_hash(Left), + hg_route:provider_ref(Left), + hg_route:terminal_ref(Left) + }) + }, + RightScore = (hg_route:score(Right))#domain_PaymentRouteScores{ + random_condition = 0, + route_pin = erlang:phash2({ + hg_route:pin_hash(Right), + hg_route:provider_ref(Right), + hg_route:terminal_ref(Right) + }) + }, + case max(LeftScore, RightScore) of + LeftScore -> Left; + RightScore -> Right + end. + +select_better_prepared_regular_route(Left, Right) -> + LeftScore = (hg_route:score(Left))#domain_PaymentRouteScores{route_pin = 0}, + RightScore = (hg_route:score(Right))#domain_PaymentRouteScores{route_pin = 0}, + case max({LeftScore, Left}, {RightScore, Right}) of + {LeftScore, Left} -> Left; + {RightScore, Right} -> Right + end. + +get_prepared_route_choice_context(SameRoute, SameRoute) -> + #{ + chosen_route => SameRoute + }; +get_prepared_route_choice_context(ChosenRoute, IdealRoute) -> + #{ + chosen_route => ChosenRoute, + preferable_route => IdealRoute, + reject_reason => map_route_switch_reason(hg_route:score(ChosenRoute), hg_route:score(IdealRoute)) + }. + -spec find_best_routes([scored_route()]) -> {Chosen :: scored_route(), Ideal :: scored_route()}. find_best_routes([Route]) -> {Route, Route}; @@ -579,64 +874,8 @@ get_conversion_score({lacking, FailRate}) -> {0, 1.0 - FailRate}. score_routes_with_fault_detector([]) -> []; score_routes_with_fault_detector(Routes) -> - IDs = build_ids(Routes), - FDStats = hg_fault_detector_client:get_statistics(IDs), - [{R, get_provider_status(R, FDStats)} || R <- Routes]. - --spec get_provider_status(hg_route:t(), [fd_service_stats()]) -> provider_status(). -get_provider_status(Route, FDStats) -> - 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_adapter_availability_status(FdOverrides, AvailabilityServiceID, FDStats), - ConversionStatus = get_provider_conversion_status(FdOverrides, ConversionServiceID, FDStats), - {AvailabilityStatus, ConversionStatus}. - -get_adapter_availability_status(?fd_overrides(true), _FDID, _Stats) -> - %% ignore fd statistic if set override - {alive, 0.0}; -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 - {value, #fault_detector_ServiceStatistics{failure_rate = FailRate}} when FailRate >= CriticalFailRate -> - {dead, FailRate}; - {value, #fault_detector_ServiceStatistics{failure_rate = FailRate}} -> - {alive, FailRate}; - false -> - {alive, 0.0} - end. - -get_provider_conversion_status(?fd_overrides(true), _FDID, _Stats) -> - %% ignore fd statistic if set override - {normal, 0.0}; -get_provider_conversion_status(_, FDID, Stats) -> - ConversionConfig = maps:get(conversion, genlib_app:env(hellgate, fault_detector, #{}), #{}), - CriticalFailRate = maps:get(critical_fail_rate, ConversionConfig, 0.7), - case lists:keysearch(FDID, #fault_detector_ServiceStatistics.service_id, Stats) of - {value, #fault_detector_ServiceStatistics{failure_rate = FailRate}} when FailRate >= CriticalFailRate -> - {lacking, FailRate}; - {value, #fault_detector_ServiceStatistics{failure_rate = FailRate}} -> - {normal, FailRate}; - false -> - {normal, 0.0} - end. - -build_ids(Routes) -> - lists:foldl(fun build_fd_ids/2, [], Routes). - -build_fd_ids(Route, IDs) -> - ProviderRef = hg_route:provider_ref(Route), - AvailabilityID = build_fd_availability_service_id(ProviderRef), - ConversionID = build_fd_conversion_service_id(ProviderRef), - [AvailabilityID, ConversionID | IDs]. - -build_fd_availability_service_id(#domain_ProviderRef{id = ID}) -> - hg_fault_detector_client:build_service_id(adapter_availability, ID). - -build_fd_conversion_service_id(#domain_ProviderRef{id = ID}) -> - hg_fault_detector_client:build_service_id(provider_conversion, ID). + PreparedRoutes = hg_route_fd:fill(Routes), + [{Route, route_provider_status(Route)} || Route <- PreparedRoutes]. -spec get_payment_terms(hg_route:payment_route(), varset(), revision()) -> payment_terms() | undefined. get_payment_terms(?route(ProviderRef, TerminalRef), VS, Revision) -> @@ -666,193 +905,26 @@ get_provision_terms(?route(ProviderRef, TerminalRef), VS, Revision) -> ), TermsSet. --spec acceptable_terminal( - route_predestination(), - hg_route:provider_ref(), - hg_route:terminal_ref(), - varset(), - revision() -) -> true | no_return(). -acceptable_terminal(Predestination, ProviderRef, TerminalRef, VS, Revision) -> - {Client, Context} = get_party_client(), - Result = party_client_thrift:compute_provider_terminal_terms( - ProviderRef, - TerminalRef, - Revision, - hg_varset:prepare_varset(VS), - Client, - Context - ), - case Result of - {ok, ProvisionTermSet} -> - check_terms_acceptability(Predestination, ProvisionTermSet, VS); - {error, #payproc_ProvisionTermSetUndefined{}} -> - throw(?rejected({'ProvisionTermSet', undefined})) - end. - -%% - get_party_client() -> HgContext = hg_context:load(), Client = hg_context:get_party_client(HgContext), Context = hg_context:get_party_client_context(HgContext), {Client, Context}. -check_terms_acceptability(payment, Terms, VS) -> - acceptable_payment_terms(Terms#domain_ProvisionTermSet.payments, VS); -check_terms_acceptability(recurrent_payment, Terms, VS) -> - _ = acceptable_payment_terms(Terms#domain_ProvisionTermSet.payments, VS), - case Terms#domain_ProvisionTermSet.extension of - #domain_ExtendedProvisionTerms{skip_recurrent = true} -> - true; - _ -> - acceptable_recurrent_paytool_terms(Terms#domain_ProvisionTermSet.recurrent_paytools, VS) - end. - -acceptable_payment_terms( - #domain_PaymentsProvisionTerms{ - allow = Allow, - global_allow = GlobalAllow, - currencies = CurrenciesSelector, - categories = CategoriesSelector, - payment_methods = PMsSelector, - cash_limit = CashLimitSelector, - holds = HoldsTerms, - refunds = RefundsTerms, - risk_coverage = RiskCoverageSelector - }, - VS -) -> - % TODO varsets getting mixed up - % it seems better to pass down here hierarchy of contexts w/ appropriate module accessors - ParentName = 'PaymentsProvisionTerms', - _ = acceptable_allow(ParentName, global_allow, GlobalAllow), - _ = acceptable_allow(ParentName, allow, Allow), - _ = try_accept_term(ParentName, currency, getv(currency, VS), CurrenciesSelector), - _ = try_accept_term(ParentName, category, getv(category, VS), CategoriesSelector), - _ = try_accept_term(ParentName, payment_tool, getv(payment_tool, VS), PMsSelector), - _ = try_accept_term(ParentName, cost, getv(cost, VS), CashLimitSelector), - _ = acceptable_holds_terms(HoldsTerms, getv(flow, VS, undefined)), - _ = acceptable_refunds_terms(RefundsTerms, getv(refunds, VS, undefined)), - _ = acceptable_risk(ParentName, RiskCoverageSelector, VS), - %% TODO Check chargeback terms when there will be any - %% _ = acceptable_chargeback_terms(...) - true; -acceptable_payment_terms(undefined, _VS) -> - throw(?rejected({'PaymentsProvisionTerms', undefined})). - -acceptable_holds_terms(_Terms, undefined) -> - true; -acceptable_holds_terms(_Terms, instant) -> - true; -acceptable_holds_terms(Terms, {hold, Lifetime}) -> - case Terms of - #domain_PaymentHoldsProvisionTerms{lifetime = LifetimeSelector} -> - _ = try_accept_term('PaymentHoldsProvisionTerms', lifetime, Lifetime, LifetimeSelector), - true; - undefined -> - throw(?rejected({'PaymentHoldsProvisionTerms', undefined})) - end. - -acceptable_risk(_ParentName, undefined, _VS) -> - true; -acceptable_risk(ParentName, Selector, VS) -> - RiskCoverage = get_selector_value(risk_coverage, Selector), - RiskScore = getv(risk_score, VS), - hg_inspector:compare_risk_score(RiskCoverage, RiskScore) >= 0 orelse - throw(?rejected({ParentName, risk_coverage})). - -acceptable_refunds_terms(_Terms, undefined) -> - true; -acceptable_refunds_terms( - #domain_PaymentRefundsProvisionTerms{ - partial_refunds = PartialRefundsTerms - }, - RVS -) -> - _ = acceptable_partial_refunds_terms( - PartialRefundsTerms, - getv(partial, RVS, undefined) - ), - true; -acceptable_refunds_terms(undefined, _RVS) -> - throw(?rejected({'PaymentRefundsProvisionTerms', undefined})). - -acceptable_partial_refunds_terms(_Terms, undefined) -> - true; -acceptable_partial_refunds_terms( - #domain_PartialRefundsProvisionTerms{cash_limit = CashLimitSelector}, - #{cash_limit := MerchantLimit} -) -> - ProviderLimit = get_selector_value(cash_limit, CashLimitSelector), - hg_cash_range:is_subrange(MerchantLimit, ProviderLimit) == true orelse - throw(?rejected({'PartialRefundsProvisionTerms', cash_limit})); -acceptable_partial_refunds_terms(undefined, _RVS) -> - throw(?rejected({'PartialRefundsProvisionTerms', undefined})). - -acceptable_allow(_ParentName, _Type, undefined) -> - true; -acceptable_allow(_ParentName, _Type, {constant, true}) -> - true; -acceptable_allow(ParentName, Type, {constant, false}) -> - throw(?rejected({ParentName, Type})); -acceptable_allow(_ParentName, Type, Ambiguous) -> - erlang:error({misconfiguration, {'Could not reduce predicate to a value', {Type, Ambiguous}}}). - -%% - -acceptable_recurrent_paytool_terms( - #domain_RecurrentPaytoolsProvisionTerms{ - categories = CategoriesSelector, - payment_methods = PMsSelector - }, - VS -) -> - _ = try_accept_term('RecurrentPaytoolsProvisionTerms', category, getv(category, VS), CategoriesSelector), - _ = try_accept_term('RecurrentPaytoolsProvisionTerms', payment_tool, getv(payment_tool, VS), PMsSelector), - true; -acceptable_recurrent_paytool_terms(undefined, _VS) -> - throw(?rejected({'RecurrentPaytoolsProvisionTerms', undefined})). - -try_accept_term(ParentName, Name, _Value, undefined) -> - throw(?rejected({ParentName, Name})); -try_accept_term(ParentName, Name, Value, Selector) -> - Values = get_selector_value(Name, Selector), - test_term(Name, Value, Values) orelse throw(?rejected({ParentName, Name})). - -test_term(currency, V, Vs) -> - ordsets:is_element(V, Vs); -test_term(category, V, Vs) -> - ordsets:is_element(V, Vs); -test_term(payment_tool, PT, PMs) -> - hg_payment_tool:has_any_payment_method(PT, PMs); -test_term(cost, Cost, CashRange) -> - hg_cash_range:is_inside(Cost, CashRange) == within; -test_term(lifetime, ?hold_lifetime(Lifetime), ?hold_lifetime(Allowed)) -> - Lifetime =< Allowed. - -%% - -get_selector_value(Name, Selector) -> - case Selector of - {value, V} -> - V; - Ambiguous -> - erlang:error({misconfiguration, {'Could not reduce selector to a value', {Name, Ambiguous}}}) - end. - -getv(Name, VS) -> - maps:get(Name, VS). - -getv(Name, VS, Default) -> - maps:get(Name, VS, Default). - %% -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). +-spec merge_fd_overrides(term(), term()) -> term(). +merge_fd_overrides(_A, B = ?fd_overrides(Enabled)) when Enabled =/= undefined -> + B; +merge_fd_overrides(A = ?fd_overrides(Enabled), _B) when Enabled =/= undefined -> + A; +merge_fd_overrides(_A, _B) -> + ?fd_overrides(undefined). + -spec test() -> _. -type testcase() :: {_, fun(() -> _)}. @@ -904,7 +976,7 @@ pin_random_test() -> {ST, _} -> ShuffledRoute; _ -> - error({ShuffledRoute, Acc}) + erlang:error({ShuffledRoute, Acc}) end end, undefined, @@ -941,13 +1013,13 @@ diff_pin_test() -> {S, 0} when S > 400 -> true; SomethingElse -> - error({{i1, i2}, SomethingElse}) + erlang:error({{i1, i2}, SomethingElse}) end, case I3 of _ when I3 > 300 -> true; _ -> - error({i3, I3}) + erlang:error({i3, I3}) end. -spec pin_weight_test() -> _. @@ -972,7 +1044,7 @@ pin_weight_test() -> _ when ShuffledRoute1 == ?trm(1), ShuffledRoute2 == ?trm(2) -> true; _ -> - error({ShuffledRoute1, ShuffledRoute2}) + erlang:error({ShuffledRoute1, ShuffledRoute2}) end end, true, diff --git a/apps/routing/src/hg_routing_ctx.erl b/apps/routing/src/hg_routing_ctx.erl deleted file mode 100644 index ee483360..00000000 --- a/apps/routing/src/hg_routing_ctx.erl +++ /dev/null @@ -1,281 +0,0 @@ --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([stash_current_candidates/1]). --export([considered_candidates/1]). --export([accounted_candidates/1]). --export([choosen_route/1]). --export([process/2]). --export([with_guard/1]). --export([pipeline/2]). --export([route_limits/1]). --export([stash_route_limits/2]). --export([route_scores/1]). --export([stash_route_scores/2]). --export([add_route_scores/2]). - --type rejection_group() :: atom(). --type error() :: {atom(), _Description}. --type route_limits() :: hg_routing:limits(). --type route_scores() :: hg_routing:scores(). --type one_route_scores() :: {hg_route:payment_route(), hg_routing:route_scores()}. - --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, - stashed_candidates => [hg_route:t()], - fail_rates => [hg_routing:fail_rated_route()], - route_limits => route_limits(), - route_scores => route_scores() -}. - --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, #{rejections := Rejections, candidates := Candidates} = Ctx) -> - 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). - -%% @doc List of currently considering candidates. -%% Route will be choosen among these. --spec candidates(t()) -> [hg_route:t()]. -candidates(#{candidates := Candidates}) -> - Candidates. - -%% @doc Lists candidates provided at very start of routing context formation. --spec initial_candidates(t()) -> [hg_route:t()]. -initial_candidates(#{initial_candidates := InitialCandidates}) -> - InitialCandidates. - -%% @doc Lists candidates (same as 'candidates/1') with only difference that list -%% includes previously considered candidates that were stashed to be -%% accounted for later. -%% -%% For __example__, it may consist of routes that were successfully staged -%% by limits accountant and thus stashed to be optionally rolled back -%% later. --spec considered_candidates(t()) -> [hg_route:t()]. -considered_candidates(Ctx) -> - maps:get(stashed_candidates, Ctx, candidates(Ctx)). - -%% @doc Same as 'considered_candidates/1' except for it fallbacks to initial -%% candidates if no were stashed. -%% -%% Its use-case is simillar to 'considered_candidates/1' as well. --spec accounted_candidates(t()) -> [hg_route:t()]. -accounted_candidates(Ctx) -> - maps:get(stashed_candidates, Ctx, initial_candidates(Ctx)). - --spec stash_current_candidates(t()) -> t(). -stash_current_candidates(#{candidates := []} = Ctx) -> - Ctx; -stash_current_candidates(Ctx) -> - Ctx#{stashed_candidates => candidates(Ctx)}. - --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). - -%% - --spec route_limits(t()) -> route_limits() | undefined. -route_limits(Ctx) -> - maps:get(route_limits, Ctx, undefined). - --spec stash_route_limits(route_limits(), t()) -> t(). -stash_route_limits(RouteLimits, Ctx) -> - Ctx#{route_limits => RouteLimits}. - --spec route_scores(t()) -> route_scores() | undefined. -route_scores(Ctx) -> - maps:get(route_scores, Ctx, undefined). - --spec stash_route_scores(route_scores(), t()) -> t(). -stash_route_scores(RouteScoresNew, #{route_scores := RouteScores} = Ctx) -> - Ctx#{route_scores => maps:merge(RouteScores, RouteScoresNew)}; -stash_route_scores(RouteScores, Ctx) -> - Ctx#{route_scores => RouteScores}. - --spec add_route_scores(one_route_scores(), t()) -> t(). -add_route_scores({PR, Scores}, Ctx) -> - Ctx#{route_scores => #{PR => Scores}}. - -%% - -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.