From ad3de6e8acd19161a5bae500874af282454618fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC?= Date: Tue, 24 Mar 2026 18:19:38 +0300 Subject: [PATCH 1/4] import new routing engine --- apps/hellgate/src/hg_inspector.erl | 28 + apps/hellgate/src/hg_invoice_payment.erl | 326 +++-- .../src/hg_invoice_payment_chargeback.erl | 4 +- .../src/hg_invoice_payment_refund.erl | 2 +- .../src/hg_invoice_registered_payment.erl | 2 +- apps/hellgate/src/hg_party.erl | 41 + apps/routing/src/hg_route.erl | 272 ++-- apps/routing/src/hg_route_balancer.erl | 134 ++ apps/routing/src/hg_route_collector.erl | 415 ++++++ apps/routing/src/hg_route_fd.erl | 83 ++ apps/routing/src/hg_routing.erl | 1227 ++++------------- apps/routing/src/hg_routing_ctx.erl | 281 ---- 12 files changed, 1403 insertions(+), 1412 deletions(-) create mode 100644 apps/routing/src/hg_route_balancer.erl create mode 100644 apps/routing/src/hg_route_collector.erl create mode 100644 apps/routing/src/hg_route_fd.erl delete mode 100644 apps/routing/src/hg_routing_ctx.erl diff --git a/apps/hellgate/src/hg_inspector.erl b/apps/hellgate/src/hg_inspector.erl index 25edf860..4854e3fc 100644 --- a/apps/hellgate/src/hg_inspector.erl +++ b/apps/hellgate/src/hg_inspector.erl @@ -1,5 +1,6 @@ -module(hg_inspector). +-export([fill_blacklist/2]). -export([check_blacklist/1]). -export([inspect/4]). @@ -26,6 +27,33 @@ inspector := inspector() }. +-spec fill_blacklist(hg_route:t(), blacklist_context()) -> hg_route:t(). +fill_blacklist(Route, #{revision := Revision, token := Token, inspector := #domain_Inspector{ + proxy = Proxy +}}) when Token =/= undefined -> + #domain_ProviderRef{id = ProviderID} = hg_route:provider_ref(Route), + #domain_TerminalRef{id = TerminalID} = hg_route:terminal_ref(Route), + Context = #proxy_inspector_BlackListContext{ + first_id = genlib:to_binary(ProviderID), + second_id = genlib:to_binary(TerminalID), + field_name = <<"CARD_TOKEN">>, + value = Token + }, + DeadLine = woody_deadline:from_timeout(genlib_app:env(hellgate, inspect_timeout, infinity)), + {ok, Check} = issue_call( + 'IsBlacklisted', + {Context}, + hg_proxy:get_call_options( + Proxy, + Revision + ), + false, + DeadLine + ), + hg_route:set_blacklisted(Check, Route); +fill_blacklist(Route, _Ctx) -> + Route. + -spec check_blacklist(blacklist_context()) -> boolean(). check_blacklist(#{ route := Route, diff --git a/apps/hellgate/src/hg_invoice_payment.erl b/apps/hellgate/src/hg_invoice_payment.erl index 26874612..7ba391d5 100644 --- a/apps/hellgate/src/hg_invoice_payment.erl +++ b/apps/hellgate/src/hg_invoice_payment.erl @@ -698,7 +698,7 @@ validate_limit(Cash, CashRange) -> throw_invalid_request(<<"Invalid amount, more than allowed maximum">>) end. -gather_routes(PaymentInstitution, VS, Revision, St) -> +get_routes_(PaymentInstitution, VS, Revision, St) -> Payment = get_payment(St), Predestination = choose_routing_predestination(Payment), #domain_Cash{currency = Currency} = get_payment_cost(Payment), @@ -707,12 +707,18 @@ gather_routes(PaymentInstitution, VS, Revision, St) -> CardToken = get_payer_card_token(Payer), PaymentTool = get_payer_payment_tool(Payer), ClientIP = get_payer_client_ip(Payer), - hg_routing:gather_routes(Predestination, PaymentInstitution, VS, Revision, #{ - currency => Currency, - payment_tool => PaymentTool, - client_ip => ClientIP, - email => Email, - card_token => CardToken + hg_routing:get_routes(#{ + predestination => Predestination, + revision => Revision, + varset => VS, + payment_institution => PaymentInstitution, + pin_context => #{ + currency => Currency, + payment_tool => PaymentTool, + client_ip => ClientIP, + email => Email, + card_token => CardToken + } }). -spec check_risk_score(risk_score()) -> ok | {error, risk_score_is_too_high}. @@ -729,17 +735,13 @@ choose_routing_predestination(#domain_InvoicePayment{payer = ?payment_resource_p % Other payers has predefined routes -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(notice, "Routing decision made", #{routing => Metadata}). maybe_log_misconfigurations({misconfiguration, _} = Error) -> {Format, Details} = hg_routing:prepare_log_message(Error), - ?LOG_MD(warning, Format, Details); -maybe_log_misconfigurations(_Error) -> - ok. + ?LOG_MD(warning, Format, Details). log_rejected_routes(_, [], _VS) -> ok; @@ -931,7 +933,7 @@ partial_capture(St0, Reason, Cost, Cart, Opts, MerchantTerms, Timestamp, Allocat VS = collect_validation_varset(St, Opts), ok = validate_merchant_hold_terms(MerchantTerms), Route = get_route(St), - ProviderTerms = hg_routing:get_payment_terms(Route, VS, Revision), + ProviderTerms = hg_party:get_route_payment_terms(Route, VS, Revision), ok = validate_provider_holds_terms(ProviderTerms), Context = #{ provision_terms => ProviderTerms, @@ -1892,53 +1894,52 @@ 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)), - %% 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(Ctx1) - ], - Events = produce_routing_events(Ctx1, Revision, St), - {next, {Events, hg_machine_action:set_timeout(0, Action)}}; - Error -> + case get_routes(PaymentInstitution, VS, Revision, St) of + #{error := 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) + handle_choose_route_error(Error, [], St, Action); + #{routes := Routes} -> + FilterResult = hg_routing:filter_routes(Routes, build_routing_filter_funs(VS, St)), + case FilterResult of + #{routes := []} -> + ok = log_rejected_route_groups(FilterResult, VS), + handle_filtered_routes_exhaustion(FilterResult, Revision, St, Action); + #{routes := FilteredRoutes} -> + {ChosenRoute, ChoiceMeta} = hg_routing:choose_route(FilteredRoutes), + Events = produce_routing_events( + build_route_selection_context(ChosenRoute, ChoiceMeta, FilterResult), + Revision, + St + ), + {next, {Events, hg_machine_action:set_timeout(0, Action)}} + end end. -run_routing_decision_pipeline(Ctx0, VS, St) -> +build_routing_filter_funs(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 - ] - ). + [ + fun(Result) -> filter_attempted_routes(Result, St) end, + fun(Result) -> filter_routes_with_limit_hold(Result, VS, NewIter, St) end, + fun(Result) -> filter_routes_by_limit_overflow(Result, VS, NewIter, St) end, + fun filter_routes_by_critical_provider_status/1 + ]. -produce_routing_events(#{error := Error} = Ctx, Revision, St) when Error =/= undefined -> +produce_routing_events(#{error := Error, considered_routes := RollbackableCandidates} = Ctx, 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)), %% 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), 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 = maps:get(route_scores, Ctx, undefined), + RouteLimits = maps:get(route_limits, Ctx, undefined), 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 @@ -1946,18 +1947,18 @@ produce_routing_events(#{error := Error} = Ctx, Revision, St) when Error =/= und %% 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) -> +produce_routing_events(#{chosen_route := ChosenRoute, considered_routes := ConsideredRoutes} = Ctx, Revision, _St) -> ok = log_route_choice_meta(Ctx, Revision), - Route = hg_route:to_payment_route(hg_routing_ctx:choosen_route(Ctx)), + Route = hg_route:to_payment_route(ChosenRoute), 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 <- ConsideredRoutes]), + RouteScores = maps:get(route_scores, Ctx, undefined), + RouteLimits = maps:get(route_limits, Ctx, undefined), Decision = build_route_decision_context(Route, Revision), [?route_changed(Route, Candidates, RouteScores, RouteLimits, Decision)]. build_route_decision_context(Route, Revision) -> - ProvisionTerms = hg_routing:get_provision_terms(Route, #{}, Revision), + ProvisionTerms = hg_party:get_route_provision_terms(Route, #{}, Revision), SkipRecurrent = case ProvisionTerms#domain_ProvisionTermSet.extension of #domain_ExtendedProvisionTerms{skip_recurrent = true} -> @@ -1980,51 +1981,98 @@ route_args(St) -> PaymentInstitution = hg_payment_institution:compute_payment_institution(PaymentInstitutionRef, VS1, Revision), {PaymentInstitution, VS3, Revision}. -build_routing_context(PaymentInstitution, VS, Revision, St) -> +get_routes(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_route:from_payment_route(PaymentRoute)]; undefined -> - gather_routes(PaymentInstitution, VS, Revision, St) + get_routes_(PaymentInstitution, VS, Revision, St) end. -build_blacklist_context(St) -> - Revision = get_payment_revision(St), - #domain_InvoicePayment{payer = Payer} = get_payment(St), - Token = - case get_payer_payment_tool(Payer) of - {bank_card, #domain_BankCard{token = CardToken}} -> - CardToken; - _ -> - undefined +filter_attempted_routes(#{routes := Routes} = Result, #st{routes = AttemptedRoutes}) -> + {AcceptedRoutes, RejectedRoutes} = lists:foldr( + fun(Route, {AcceptedAcc, RejectedAcc}) -> + case lists:any(fun(AttemptedRoute) -> hg_route:equal(Route, AttemptedRoute) end, AttemptedRoutes) of + true -> + {AcceptedAcc, [hg_route:to_rejected_route(Route, {'AlreadyAttempted', undefined}) | RejectedAcc]}; + false -> + {[Route | AcceptedAcc], RejectedAcc} + end end, - Opts = get_opts(St), - VS1 = get_varset(St, #{}), - PaymentInstitutionRef = get_payment_institution_ref(Opts, Revision), - PaymentInstitution = hg_payment_institution:compute_payment_institution(PaymentInstitutionRef, VS1, Revision), - InspectorRef = get_selector_value(inspector, PaymentInstitution#domain_PaymentInstitution.inspector), - Inspector = hg_domain:get(Revision, {inspector, InspectorRef}), + {[], []}, + Routes + ), + append_rejected_routes(already_attempted, AcceptedRoutes, RejectedRoutes, Result). + +handle_choose_route_error(Error, Events, St, Action) -> + Failure = construct_routing_failure(Error), + process_failure(get_activity(St), Events, Action, Failure, St). + +handle_filtered_routes_exhaustion(Result, Revision, St, Action) -> + Error = latest_rejected_error(Result), + case maps:get(considered_routes, Result, []) of + [] -> + handle_choose_route_error(Error, [], St, Action); + ConsideredRoutes -> + Events = produce_routing_events( + #{ + error => Error, + considered_routes => ConsideredRoutes, + route_scores => maps:get(route_scores, Result, undefined), + route_limits => maps:get(route_limits, Result, undefined) + }, + Revision, + St + ), + {next, {Events, hg_machine_action:set_timeout(0, Action)}} + end. + +log_rejected_route_groups(Result, VS) -> + maps:fold( + fun(Group, RejectedRoutes, ok) -> + log_rejected_routes(Group, RejectedRoutes, VS) + end, + ok, + maps:get(rejection_groups, Result, #{}) + ). + +build_route_selection_context(ChosenRoute, ChoiceMeta, Result) -> #{ - revision => Revision, - token => Token, - inspector => Inspector + chosen_route => ChosenRoute, + choice_meta => ChoiceMeta, + considered_routes => maps:get(considered_routes, Result, maps:get(routes, Result)), + route_scores => maps:get(route_scores, Result, build_route_scores(maps:get(routes, Result))), + route_limits => maps:get(route_limits, Result, undefined) }. -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) +latest_rejected_error(Result) -> + Group = maps:get(latest_rejected_group, Result, forbidden), + RejectedGroups = maps:get(rejection_groups, Result, #{}), + RejectedRoutes = maps:get(Group, RejectedGroups, maps:get(rejected_routes, Result, [])), + {rejected_routes, {Group, RejectedRoutes}}. + +build_route_scores(Routes) -> + lists:foldl( + fun(Route, Acc) -> + Acc#{hg_route:to_payment_route(Route) => hg_route:score(Route)} end, - Ctx, - AttemptedRoutes + #{}, + Routes ). -handle_choose_route_error(Error, Events, St, Action) -> - Failure = construct_routing_failure(Error), - process_failure(get_activity(St), Events, Action, Failure, St). +append_rejected_routes(_Group, Routes, [], Result) -> + Result#{routes => Routes}; +append_rejected_routes(Group, Routes, RejectedRoutes, Result0) -> + RejectionGroups0 = maps:get(rejection_groups, Result0, #{}), + CurrentRejected = maps:get(rejected_routes, Result0, []), + GroupRejected = maps:get(Group, RejectionGroups0, []), + Result0#{ + routes => Routes, + rejected_routes => CurrentRejected ++ RejectedRoutes, + latest_rejected_group => Group, + rejection_groups => RejectionGroups0#{Group => GroupRejected ++ RejectedRoutes} + }. %% NOTE See damsel payproc errors (proto/payment_processing_errors.thrift) for no route found @@ -2040,9 +2088,7 @@ construct_routing_failure({rejected_routes, {_SubCode, RejectedRoutes}}) -> construct_routing_failure({misconfiguration = Code, Details}) -> construct_routing_failure([unknown, {unknown_error, atom_to_binary(Code)}], genlib:format(Details)); construct_routing_failure(risk_score_is_too_high = Code) -> - construct_routing_failure([Code], undefined); -construct_routing_failure(Error) when is_atom(Error) -> - construct_routing_failure([{unknown_error, Error}], undefined). + construct_routing_failure([Code], undefined). construct_routing_failure(Codes, Reason) -> {failure, payproc_errors:construct('PaymentFailure', mk_static_error([no_route_found | Codes]), Reason)}. @@ -2272,7 +2318,7 @@ process_result({payment, processing_accounter}, Action, #st{new_cash = Cost} = S VS = collect_validation_varset(St1, Opts), MerchantTerms = get_merchant_payments_terms(Opts, Revision, Timestamp, VS), Route = get_route(St1), - ProviderTerms = hg_routing:get_payment_terms(Route, VS, Revision), + ProviderTerms = hg_party:get_route_payment_terms(Route, VS, Revision), Context = #{ provision_terms => ProviderTerms, merchant_terms => MerchantTerms, @@ -2500,24 +2546,46 @@ get_provider_payment_terms(St, Revision) -> Payment = get_payment(St), VS0 = reconstruct_payment_flow(Payment, #{}), 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 - ). + hg_party:get_route_payment_terms(Route, VS1, Revision). + +filter_routes_with_limit_hold(#{routes := Routes} = Result0, VS, Iter, St) -> + {AcceptedRoutes, RejectedRoutes} = hold_limit_routes(Routes, VS, Iter, St), + Result1 = Result0#{ + considered_routes => AcceptedRoutes + }, + append_rejected_routes(limit_misconfiguration, AcceptedRoutes, lists:reverse(RejectedRoutes), Result1). + +filter_routes_by_limit_overflow(#{routes := Routes} = Result0, VS, Iter, St) -> + {AcceptedRoutes0, RejectedRoutes0, Limits} = get_limit_overflow_routes(Routes, VS, Iter, St), + AcceptedRoutes = lists:reverse(AcceptedRoutes0), + RejectedRoutes = lists:reverse(RejectedRoutes0), + Result1 = Result0#{ + route_limits => Limits + }, + append_rejected_routes(limit_overflow, AcceptedRoutes, RejectedRoutes, Result1). + +filter_routes_by_critical_provider_status(#{routes := Routes} = Result0) -> + RouteScores = build_route_scores(Routes), + {AcceptedRoutes, RejectedRoutes} = lists:foldr( + fun(Route, {AcceptedAcc, RejectedAcc}) -> + case hg_route:fd_score(Route) of + #{availability_condition := 0, availability := Availability} -> + RejectedRoute = hg_route:to_rejected_route( + Route, + {'ProviderDead', {dead, 1.0 - Availability}} + ), + {AcceptedAcc, [RejectedRoute | RejectedAcc]}; + _ -> + {[Route | AcceptedAcc], RejectedAcc} + end + end, + {[], []}, + Routes + ), + Result1 = Result0#{ + route_scores => RouteScores + }, + append_rejected_routes(adapter_unavailable, AcceptedRoutes, RejectedRoutes, Result1). get_limit_overflow_routes(Routes, VS, Iter, St) -> Opts = get_opts(St), @@ -2528,7 +2596,7 @@ get_limit_overflow_routes(Routes, VS, Iter, St) -> lists:foldl( fun(Route, {RoutesNoOverflowIn, RejectedIn, LimitsIn}) -> PaymentRoute = hg_route:to_payment_route(Route), - ProviderTerms = hg_routing:get_payment_terms(PaymentRoute, VS, Revision), + ProviderTerms = hg_party:get_route_payment_terms(PaymentRoute, VS, Revision), TurnoverLimits = get_turnover_limits(ProviderTerms, strict), case hg_limiter:check_limits(TurnoverLimits, Invoice, Payment, Session, PaymentRoute, Iter) of {ok, Limits} -> @@ -2603,7 +2671,7 @@ hold_limit_routes(Routes0, VS, Iter, St) -> {Routes1, Rejected} = lists:foldl( fun(Route, {LimitHeldRoutes, RejectedRoutes} = Acc) -> PaymentRoute = hg_route:to_payment_route(Route), - ProviderTerms = hg_routing:get_payment_terms(PaymentRoute, VS, Revision), + ProviderTerms = hg_party:get_route_payment_terms(PaymentRoute, VS, Revision), TurnoverLimits = get_turnover_limits(ProviderTerms, strict), try ok = hg_limiter:hold_payment_limits(TurnoverLimits, Invoice, Payment, Session, PaymentRoute, Iter), @@ -2638,7 +2706,7 @@ rollback_payment_limits(Routes, Iter, St, Flags) -> VS = get_varset(St, #{}), lists:foreach( fun(Route) -> - ProviderTerms = hg_routing:get_payment_terms(Route, VS, Revision), + ProviderTerms = hg_party:get_route_payment_terms(Route, VS, Revision), TurnoverLimits = get_turnover_limits(ProviderTerms, strict), ok = hg_limiter:rollback_payment_limits(TurnoverLimits, Invoice, Payment, Session, Route, Iter, Flags) end, @@ -3486,7 +3554,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), + #{routes := Routes} = get_routes(PaymentInstitution, VS, Revision, St), Session = get_activity_session(St), Payment = get_payment(St), Invoice = get_invoice(get_opts(St)), @@ -3501,14 +3569,14 @@ get_limit_values_(St, Mode) -> lists:foldl( fun(Route, Acc) -> PaymentRoute = hg_route:to_payment_route(Route), - ProviderTerms = hg_routing:get_payment_terms(PaymentRoute, VS, Revision), + ProviderTerms = hg_party:get_route_payment_terms(PaymentRoute, VS, Revision), TurnoverLimits = get_turnover_limits(ProviderTerms, Mode), TurnoverLimitValues = hg_limiter:get_limit_values(TurnoverLimits, Invoice, Payment, Session, PaymentRoute, Iter), Acc#{PaymentRoute => TurnoverLimitValues} end, #{}, - hg_routing_ctx:considered_candidates(Ctx) + Routes ). try_accrue_waiting_timing(Opts, #st{payment = Payment, timings = Timings}) -> @@ -3918,6 +3986,7 @@ get_route_cascade_behaviour(Route, Revision) -> filter_attempted_routes_test_() -> [R1, R2, R3] = [ hg_route:new( + 1, #domain_ProviderRef{id = 171}, #domain_TerminalRef{id = 307}, 20, @@ -3925,6 +3994,7 @@ filter_attempted_routes_test_() -> #{client_ip => <<127, 0, 0, 1>>} ), hg_route:new( + 1, #domain_ProviderRef{id = 171}, #domain_TerminalRef{id = 344}, 80, @@ -3932,6 +4002,7 @@ filter_attempted_routes_test_() -> #{} ), hg_route:new( + 1, #domain_ProviderRef{id = 162}, #domain_TerminalRef{id = 227}, 1, @@ -3941,9 +4012,9 @@ filter_attempted_routes_test_() -> ], [ ?_assertMatch( - #{candidates := []}, + #{routes := []}, filter_attempted_routes( - hg_routing_ctx:new([]), + #{routes => [], rejected_routes => []}, #st{ activity = idle, routes = [ @@ -3956,16 +4027,26 @@ filter_attempted_routes_test_() -> ) ), ?_assertMatch( - #{candidates := []}, filter_attempted_routes(hg_routing_ctx:new([]), #st{activity = idle, routes = []}) + #{routes := []}, + filter_attempted_routes( + #{routes => [], rejected_routes => []}, + #st{activity = idle, routes = []} + ) ), ?_assertMatch( - #{candidates := [R1, R2, R3]}, - filter_attempted_routes(hg_routing_ctx:new([R1, R2, R3]), #st{activity = idle, routes = []}) + #{routes := [R1, R2, R3]}, + filter_attempted_routes( + #{routes => [R1, R2, R3], rejected_routes => []}, + #st{activity = idle, routes = []} + ) ), ?_assertMatch( - #{candidates := [R1, R2]}, + #{ + routes := [R1, R2], + rejected_routes := [{_, _, {'AlreadyAttempted', undefined}}] + }, filter_attempted_routes( - hg_routing_ctx:new([R1, R2, R3]), + #{routes => [R1, R2, R3], rejected_routes => []}, #st{ activity = idle, routes = [ @@ -3978,9 +4059,16 @@ filter_attempted_routes_test_() -> ) ), ?_assertMatch( - #{candidates := []}, + #{ + routes := [], + rejected_routes := [ + {_, _, {'AlreadyAttempted', undefined}}, + {_, _, {'AlreadyAttempted', undefined}}, + {_, _, {'AlreadyAttempted', undefined}} + ] + }, filter_attempted_routes( - hg_routing_ctx:new([R1, R2, R3]), + #{routes => [R1, R2, R3], rejected_routes => []}, #st{ activity = idle, routes = [ diff --git a/apps/hellgate/src/hg_invoice_payment_chargeback.erl b/apps/hellgate/src/hg_invoice_payment_chargeback.erl index 90967845..80adbf8e 100644 --- a/apps/hellgate/src/hg_invoice_payment_chargeback.erl +++ b/apps/hellgate/src/hg_invoice_payment_chargeback.erl @@ -291,7 +291,7 @@ do_create(Opts, CreateParams = ?chargeback_params(Levy, Body, _Reason)) -> ShopConfigRef = get_invoice_shop_config_ref(Invoice), ShopObj = {_, Shop} = hg_party:get_shop(ShopConfigRef, PartyConfigRef, Revision), VS = collect_validation_varset(PartyConfigRef, ShopObj, Payment, Body), - PaymentsTerms = hg_routing:get_payment_terms(Route, VS, Revision), + PaymentsTerms = hg_party:get_route_payment_terms(Route, VS, Revision), ProviderTerms = get_provider_chargeback_terms(PaymentsTerms, Payment), ServiceTerms = get_merchant_chargeback_terms(Party, Shop, VS, Revision, CreatedAt), _ = validate_currency(Body, Payment), @@ -427,7 +427,7 @@ build_chargeback_final_cash_flow(State, Opts) -> ShopObj = {_, Shop} = hg_party:get_shop(ShopConfigRef, PartyConfigRef, Revision), VS = collect_validation_varset(PartyConfigRef, ShopObj, Payment, Body), ServiceTerms = get_merchant_chargeback_terms(Party, Shop, VS, Revision, CreatedAt), - PaymentsTerms = hg_routing:get_payment_terms(Route, VS, Revision), + PaymentsTerms = hg_party:get_route_payment_terms(Route, VS, Revision), ProviderTerms = get_provider_chargeback_terms(PaymentsTerms, Payment), ServiceCashFlow = get_chargeback_service_cash_flow(ServiceTerms), ProviderCashFlow = get_chargeback_provider_cash_flow(ProviderTerms), diff --git a/apps/hellgate/src/hg_invoice_payment_refund.erl b/apps/hellgate/src/hg_invoice_payment_refund.erl index e0463015..6c18a766 100644 --- a/apps/hellgate/src/hg_invoice_payment_refund.erl +++ b/apps/hellgate/src/hg_invoice_payment_refund.erl @@ -381,7 +381,7 @@ get_provider_terms(Revision, Payment, Invoice, Refund) -> ShopObj = hg_party:get_shop(ShopConfigRef, PartyConfigRef, Revision), VS0 = construct_payment_flow(Payment), VS1 = collect_validation_varset(get_injected_party_config_ref(Refund), ShopObj, Payment, VS0), - hg_routing:get_payment_terms(Route, VS1, Revision). + hg_party:get_route_payment_terms(Route, VS1, Revision). construct_payment_flow(Payment) -> #domain_InvoicePayment{ diff --git a/apps/hellgate/src/hg_invoice_registered_payment.erl b/apps/hellgate/src/hg_invoice_registered_payment.erl index 59e4c3af..8414ccaf 100644 --- a/apps/hellgate/src/hg_invoice_registered_payment.erl +++ b/apps/hellgate/src/hg_invoice_registered_payment.erl @@ -214,7 +214,7 @@ get_turnover_limits(Payment, Route, St) -> PaymentTool = get_payer_payment_tool(Payer), RiskScore = hg_invoice_payment:get_risk_score(St), VS = collect_validation_varset(PartyConfigRef, ShopObj, Cost, PaymentTool, RiskScore), - ProviderTerms = hg_routing:get_payment_terms(Route, VS, Revision), + ProviderTerms = hg_party:get_route_payment_terms(Route, VS, Revision), hg_limiter:get_turnover_limits(ProviderTerms, strict). construct_payment( diff --git a/apps/hellgate/src/hg_party.erl b/apps/hellgate/src/hg_party.erl index 3fdefd36..bf4ce10d 100644 --- a/apps/hellgate/src/hg_party.erl +++ b/apps/hellgate/src/hg_party.erl @@ -2,9 +2,12 @@ -include_lib("damsel/include/dmsl_domain_thrift.hrl"). -include_lib("damsel/include/dmsl_domain_conf_v2_thrift.hrl"). +-include_lib("hellgate/include/domain.hrl"). %% Party support functions +-export([get_route_payment_terms/3]). +-export([get_route_provision_terms/3]). -export([get_party/1]). -export([get_party_revision/0]). -export([checkout/2]). @@ -21,9 +24,47 @@ -type party_config_ref() :: dmsl_domain_thrift:'PartyConfigRef'(). -type shop() :: dmsl_domain_thrift:'ShopConfig'(). -type shop_config_ref() :: dmsl_domain_thrift:'ShopConfigRef'(). +-type payment_terms() :: dmsl_domain_thrift:'PaymentsProvisionTerms'(). +-type provision_terms() :: dmsl_domain_thrift:'ProvisionTermSet'(). +-type varset() :: hg_varset:varset(). +-type revision() :: hg_domain:revision(). %% Interface +-spec get_route_payment_terms(hg_route:payment_route(), varset(), revision()) -> payment_terms() | undefined. +get_route_payment_terms(?route(ProviderRef, TerminalRef), VS, Revision) -> + PreparedVS = hg_varset:prepare_varset(VS), + {Client, Context} = get_party_client(), + {ok, TermsSet} = party_client_thrift:compute_provider_terminal_terms( + ProviderRef, + TerminalRef, + Revision, + PreparedVS, + Client, + Context + ), + TermsSet#domain_ProvisionTermSet.payments. + +-spec get_route_provision_terms(hg_route:payment_route(), varset(), revision()) -> provision_terms() | undefined. +get_route_provision_terms(?route(ProviderRef, TerminalRef), VS, Revision) -> + PreparedVS = hg_varset:prepare_varset(VS), + {Client, Context} = get_party_client(), + {ok, TermsSet} = party_client_thrift:compute_provider_terminal_terms( + ProviderRef, + TerminalRef, + Revision, + PreparedVS, + Client, + Context + ), + TermsSet. + +get_party_client() -> + HgContext = hg_context:load(), + Client = hg_context:get_party_client(HgContext), + Context = hg_context:get_party_client_context(HgContext), + {Client, Context}. + -spec get_party(party_config_ref()) -> {party_config_ref(), party()} | hg_domain:get_error(). get_party(PartyConfigRef) -> checkout(PartyConfigRef, get_party_revision()). diff --git a/apps/routing/src/hg_route.erl b/apps/routing/src/hg_route.erl index 69fb3d18..a1e27327 100644 --- a/apps/routing/src/hg_route.erl +++ b/apps/routing/src/hg_route.erl @@ -1,19 +1,32 @@ -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,22 +35,44 @@ %% --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 payment_route() :: dmsl_domain_thrift:'PaymentRoute'(). --type route_rejection_reason() :: {atom(), term()} | {atom(), term(), term()}. --type rejected_route() :: {provider_ref(), terminal_ref(), route_rejection_reason()}. +-type revision() :: hg_domain:revision(). -type provider_ref() :: dmsl_domain_thrift:'ProviderRef'(). -type terminal_ref() :: dmsl_domain_thrift:'TerminalRef'(). +-type payment_route() :: dmsl_domain_thrift:'PaymentRoute'(). + -type fd_overrides() :: dmsl_domain_thrift:'RouteFaultDetectorOverrides'(). +-type route_rejection_reason() :: {atom(), term()} | {atom(), term(), term()}. +-type rejected_route() :: {provider_ref(), terminal_ref(), route_rejection_reason()}. + +-type t() :: #{ + revision := revision(), + provider_ref := dmsl_domain_thrift:'ProviderRef'(), + terminal_ref := dmsl_domain_thrift:'TerminalRef'(), + route_data := route_data(), + pin_data => pin_data(), + fd_overrides => fd_overrides() +}. + +-type fd_score() :: #{ + availability_condition => integer(), + conversion_condition => integer(), + availability => float(), + conversion => float() +}. + +-type route_prohibit() :: boolean() | {boolean(), _DescOrAttrs}. +-type route_accepted() :: boolean() | {boolean(), _DescOrAttrs}. +-type blacklist_condition() :: 0 | 1. + +-type route_data() :: #{ + accepted => route_accepted(), + prohibit => route_prohibit(), + fd_score => fd_score(), + priority => integer(), + weight => integer(), + pin_score => integer(), + blacklisted => blacklist_condition() +}. -type currency() :: dmsl_domain_thrift:'CurrencyRef'(). -type payment_tool() :: dmsl_domain_thrift:'PaymentTool'(). @@ -45,7 +80,7 @@ -type email() :: binary(). -type card_token() :: dmsl_domain_thrift:'Token'(). --type pin() :: #{ +-type pin_data() :: #{ currency => currency(), payment_tool => payment_tool(), client_ip => client_ip() | undefined, @@ -53,110 +88,189 @@ card_token => card_token() | undefined }. +-type score() :: #domain_PaymentRouteScores{}. + -export_type([t/0]). +-export_type([route_data/0]). -export_type([provider_ref/0]). -export_type([terminal_ref/0]). -export_type([payment_route/0]). -export_type([rejected_route/0]). +-export_type([score/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()) -> t(). -new(ProviderRef, TerminalRef, Weight, Priority, Pin, FdOverrides) -> - #route{ - provider_ref = ProviderRef, - terminal_ref = TerminalRef, - weight = Weight, - priority = Priority, - pin = Pin, - fd_overrides = FdOverrides +-spec new(revision(), provider_ref(), terminal_ref(), integer(), integer(), pin_data() | undefined) -> t(). +new(Revision, ProviderRef, TerminalRef, Weight, Priority, Pin) -> + #{ + revision => Revision, + 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 }. +-spec set_fd_overrides(fd_overrides(), t()) -> + t(). +set_fd_overrides(V, R) -> + R#{fd_overrides => V}. + +-spec set_prohibit(route_prohibit(), t()) -> + t(). +set_prohibit(V, R = #{route_data := Data}) -> + R#{route_data => Data#{prohibit => V}}. + +-spec set_accepted(route_accepted(), t()) -> + t(). +set_accepted(V, R = #{route_data := Data}) -> + R#{route_data => Data#{accepted => V}}. + +-spec set_weight(integer(), t()) -> t(). +set_weight(Weight, R = #{route_data := Data}) -> + R#{route_data => Data#{weight => Weight}}. + +-spec set_blacklisted(boolean() | blacklist_condition(), t()) -> + t(). +set_blacklisted(true, R) -> + set_blacklisted(1, R); +set_blacklisted(false, R) -> + set_blacklisted(0, R); +set_blacklisted(V, R = #{route_data := Data}) -> + R#{route_data => Data#{blacklisted => V}}. + +-spec set_availability(integer(), float(), t()) -> + t(). +set_availability(C, V, R = #{route_data := Data = #{fd_score := Score}}) -> + R#{route_data => Data#{fd_score => Score#{availability_condition => C, availability => V}}}. + +-spec set_conversion(integer(), float(), t()) -> + t(). +set_conversion(C, V, R = #{route_data := Data = #{fd_score := Score}}) -> + R#{route_data => Data#{fd_score => Score#{conversion_condition => C, conversion => V}}}. + +-spec set_priority(integer(), t()) -> + t(). +set_priority(V, R = #{route_data := Data}) -> + R#{route_data => Data#{priority => V}}. + -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 := V}) -> + V. + -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(#{payment_route := V}) -> + V. + -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}) -> +-spec pin(t()) -> pin_data() | undefined. +pin(#{pin_data := Pin}) -> Pin. +-spec pin_hash(t()) -> pin_data() | undefined. +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}) -> +fd_overrides(#{fd_overrides := FdOverrides}) -> FdOverrides. --spec set_weight(integer(), t()) -> t(). -set_weight(Weight, Route) -> - Route#route{weight = Weight}. +-spec fd_score(t()) -> fd_score(). +fd_score(#{route_data := #{fd_score := V}}) -> + V; +fd_score(_) -> + undefined. + +-spec blacklisted(t()) -> + blacklist_condition(). +blacklisted(#{route_data := #{blacklisted := V}}) -> + V; +blacklisted(_) -> + 0. + +-spec score(t()) -> score(). +score(R) -> + #{ + availability_condition := AvailabilityCondition, + conversion_condition := ConversionCondition, + availability := Availability, + conversion := Conversion + } = fd_score(R), + #domain_PaymentRouteScores{ + availability_condition = AvailabilityCondition, + conversion_condition = ConversionCondition, + terminal_priority_rating = priority(R), + route_pin = pin_hash(R), + random_condition = weight(R), + availability = Availability, + conversion = Conversion, + blacklist_condition = blacklisted(R) + }. -spec equal(R, R) -> boolean() when - R :: t() | rejected_route() | payment_route() | {provider_ref(), terminal_ref()}. + R :: t() | payment_route() | rejected_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}) -> +route_ref(#{provider_ref := Prv, terminal_ref := Trm}) -> {Prv, Trm}; route_ref(#domain_PaymentRoute{provider = Prv, terminal = Trm}) -> {Prv, Trm}; -route_ref({Prv, Trm}) -> +route_ref({Prv, Trm, _Reason}) -> {Prv, Trm}; -route_ref({Prv, Trm, _RejectionReason}) -> +route_ref({Prv, Trm}) -> {Prv, Trm}; route_ref(_) -> undefined. +-spec from_payment_route(payment_route()) -> t(). +from_payment_route(Route) -> + ?route(ProviderRef, TerminalRef) = Route, + new(hg_domain:head(), ProviderRef, TerminalRef, 0, 1000, undefined). + +-spec to_payment_route(t()) -> payment_route(). +to_payment_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}. + %% -ifdef(TEST). @@ -180,12 +294,10 @@ routes_equality_test_() -> 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), + A = new(1, Prv1, Trm1, 1, 1, #{}), + B = new(1, Prv2, Trm2, 1, 1, #{}), lists:flatten([[{F1(A), F2(B)} || F1 <- Fs] || F2 <- Fs]). -endif. diff --git a/apps/routing/src/hg_route_balancer.erl b/apps/routing/src/hg_route_balancer.erl new file mode 100644 index 00000000..e4b243bb --- /dev/null +++ b/apps/routing/src/hg_route_balancer.erl @@ -0,0 +1,134 @@ +-module(hg_route_balancer). + +-behaviour(hg_route_collector). +-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) -> + FilteredRouteGroups = lists:foldl( + fun group_routes_by_priority/2, + #{}, + Routes + ), + balance_route_groups(FilteredRouteGroups). + +-spec group_routes_by_priority(route(), Acc :: route_groups_by_priority()) -> route_groups_by_priority(). +group_routes_by_priority(Route, SortedRoutes) -> + Priority = hg_route:priority(Route), + #{availability_condition := ACond} = hg_route:fd_score(Route), + Key = {ACond, Priority}, + Routes = maps:get(Key, SortedRoutes, []), + SortedRoutes#{Key => [Route | Routes]}. + +-spec balance_route_groups(route_groups_by_priority()) -> [route()]. +balance_route_groups(RouteGroups) -> + maps:fold( + fun(_Priority, Routes, Acc) -> + NewRoutes = set_routes_random_condition(Routes), + NewRoutes ++ Acc + end, + [], + RouteGroups + ). + +set_routes_random_condition(Routes) -> + Summary = get_summary_weight(Routes), + Random = rand:uniform() * Summary, + lists:reverse(calc_random_condition(0.0, Random, Routes, [])). + +get_summary_weight(Routes) -> + lists:foldl( + fun(Route, Acc) -> + Weight = hg_route:weight(Route), + Acc + Weight + 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), + case InRange of + true -> + NewRoute = hg_route:set_weight(1, Route), + calc_random_condition(StartFrom + Weight, Random, Rest, [NewRoute | Routes]); + false -> + NewRoute = hg_route:set_weight(0, Route), + calc_random_condition(StartFrom + Weight, Random, Rest, [NewRoute | Routes]) + end. + +%% + +-ifdef(TEST). + +-include_lib("eunit/include/eunit.hrl"). +-include_lib("damsel/include/dmsl_domain_thrift.hrl"). + +-spec test() -> _. +-type testcase() :: {_, fun(() -> _)}. + +-define(prv(ID), #domain_ProviderRef{id = ID}). +-define(trm(ID), #domain_TerminalRef{id = ID}). + +-spec balance_routes_test_() -> [testcase()]. +balance_routes_test_() -> + WithWeight = [ + hg_route:new(1, ?prv(1), ?trm(1), 1, ?DOMAIN_CANDIDATE_PRIORITY, #{}), + hg_route:new(1, ?prv(2), ?trm(1), 2, ?DOMAIN_CANDIDATE_PRIORITY, #{}), + hg_route:new(1, ?prv(3), ?trm(1), 0, ?DOMAIN_CANDIDATE_PRIORITY, #{}), + hg_route:new(1, ?prv(4), ?trm(1), 1, ?DOMAIN_CANDIDATE_PRIORITY, #{}), + hg_route:new(1, ?prv(5), ?trm(1), 0, ?DOMAIN_CANDIDATE_PRIORITY, #{}) + ], + + Result1 = [ + hg_route:new(1, ?prv(1), ?trm(1), 1, ?DOMAIN_CANDIDATE_PRIORITY, #{}), + hg_route:new(1, ?prv(2), ?trm(1), 0, ?DOMAIN_CANDIDATE_PRIORITY, #{}), + hg_route:new(1, ?prv(3), ?trm(1), 0, ?DOMAIN_CANDIDATE_PRIORITY, #{}), + hg_route:new(1, ?prv(4), ?trm(1), 0, ?DOMAIN_CANDIDATE_PRIORITY, #{}), + hg_route:new(1, ?prv(5), ?trm(1), 0, ?DOMAIN_CANDIDATE_PRIORITY, #{}) + ], + Result2 = [ + hg_route:new(1, ?prv(1), ?trm(1), 0, ?DOMAIN_CANDIDATE_PRIORITY, #{}), + hg_route:new(1, ?prv(2), ?trm(1), 1, ?DOMAIN_CANDIDATE_PRIORITY, #{}), + hg_route:new(1, ?prv(3), ?trm(1), 0, ?DOMAIN_CANDIDATE_PRIORITY, #{}), + hg_route:new(1, ?prv(4), ?trm(1), 0, ?DOMAIN_CANDIDATE_PRIORITY, #{}), + hg_route:new(1, ?prv(5), ?trm(1), 0, ?DOMAIN_CANDIDATE_PRIORITY, #{}) + ], + Result3 = [ + hg_route:new(1, ?prv(1), ?trm(1), 0, ?DOMAIN_CANDIDATE_PRIORITY, #{}), + hg_route:new(1, ?prv(2), ?trm(1), 0, ?DOMAIN_CANDIDATE_PRIORITY, #{}), + hg_route:new(1, ?prv(3), ?trm(1), 0, ?DOMAIN_CANDIDATE_PRIORITY, #{}), + hg_route:new(1, ?prv(4), ?trm(1), 0, ?DOMAIN_CANDIDATE_PRIORITY, #{}), + hg_route:new(1, ?prv(5), ?trm(1), 0, ?DOMAIN_CANDIDATE_PRIORITY, #{}) + ], + [ + ?_assertEqual(Result1, lists:reverse(calc_random_condition(0.0, 0.2, WithWeight, []))), + ?_assertEqual(Result2, lists:reverse(calc_random_condition(0.0, 1.5, WithWeight, []))), + ?_assertEqual(Result3, lists:reverse(calc_random_condition(0.0, 4.0, WithWeight, []))) + ]. + +-spec balance_routes_with_default_weight_test_() -> testcase(). +balance_routes_with_default_weight_test_() -> + Routes = [ + hg_route:new(1, ?prv(1), ?trm(1), 0, ?DOMAIN_CANDIDATE_PRIORITY, #{}), + hg_route:new(1, ?prv(2), ?trm(1), 0, ?DOMAIN_CANDIDATE_PRIORITY, #{}) + ], + Result = [ + hg_route:new(1, ?prv(1), ?trm(1), 0, ?DOMAIN_CANDIDATE_PRIORITY, #{}), + hg_route:new(1, ?prv(2), ?trm(1), 0, ?DOMAIN_CANDIDATE_PRIORITY, #{}) + ], + ?_assertEqual(Result, set_routes_random_condition(Routes)). + +-endif. diff --git a/apps/routing/src/hg_route_collector.erl b/apps/routing/src/hg_route_collector.erl new file mode 100644 index 00000000..46668acc --- /dev/null +++ b/apps/routing/src/hg_route_collector.erl @@ -0,0 +1,415 @@ +-module(hg_route_collector). + +-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([fill_blacklist/2]). +-export([fill_fd_overrides/2]). +-export([fill_prohibition/4]). +-export([fill_accepted/4]). +-export([get_routes/4]). + +-callback fill(_Params, [hg_route:t()]) -> [hg_route:t()]. +-callback fill([hg_route:t()]) -> [hg_route:t()]. +-optional_callbacks([fill/1, fill/2]). + +-type payment_institution() :: dmsl_domain_thrift:'PaymentInstitution'(). +-type route_predestination() :: payment | recurrent_paytool | 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 get_route_resut() :: #{ + routes := [hg_route:t()], + error => {misconfiguration, _Reason} +}. + +-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]). +-export_type([get_route_resut/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_inspector:fill_blacklist(R, BlCtx) || R <- Routes]. + +-spec fill_fd_overrides(revision(), [hg_route:t()]) -> + [hg_route:t()]. +fill_fd_overrides(Revision, Routes) -> + lists:foldr( + fun(Route, AccIn) -> + TRef = hg_route:terminal_ref(Route), + FdOverrides = get_provider_fd_overrides(Revision, TRef), + [hg_route:set_fd_overrides(FdOverrides, Route) | AccIn] + end, + [], + 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). + +-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, AccIn) -> + TRef = hg_route:terminal_ref(Route), + case maps:find(TRef, Table) of + error -> + [Route | AccIn]; + {ok, Description} -> + [hg_route:set_prohibit({true, Description}, Route) | AccIn] + end + end, + [], + Routes + ). + +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} + 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, AccIn) -> + PRef = hg_route:provider_ref(Route), + TRef = hg_route:terminal_ref(Route), + try + true = acceptable_terminal(Predestination, PRef, TRef, VS, Revision), + [Route | AccIn] + catch + {rejected, Reason} -> + [hg_route:set_accepted({false, {rejected, Reason}}, Route) | AccIn]; + error:{misconfiguration, Reason} -> + [hg_route:set_accepted({false, {misconfiguration, Reason}}, Route) | AccIn] + end + end, + [], + Routes + ). + +-spec get_routes(revision(), varset(), payment_institution(), gather_route_context()) -> + get_route_resut(). +get_routes(_, _, #domain_PaymentInstitution{payment_routing_rules = undefined}, _) -> + #{routes => [], error => {misconfiguration, {payment_routing_rules, empty}}}; +get_routes(Revision, VS, #domain_PaymentInstitution{payment_routing_rules = RoutingRules}, Ctx) -> + #domain_RoutingRules{ + policies = Policies + } = RoutingRules, + try + Candidates = get_candidates(Policies, VS, Revision), + #{routes => collect_routes(Candidates, Revision, Ctx)} + catch + throw:{misconfiguration, _Reason} = Error -> + #{routes => [], error => Error} + end. + +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(Revision, 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 + ). + +%% Accept + +-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_paytool, Terms, VS) -> + acceptable_recurrent_paytool_terms(Terms#domain_ProvisionTermSet.recurrent_paytools, VS); +check_terms_acceptability(recurrent_payment, Terms, VS) -> + % Use provider check combined from recurrent_paytool and payment check + _ = acceptable_payment_terms(Terms#domain_ProvisionTermSet.payments, VS), + acceptable_recurrent_paytool_terms(Terms#domain_ProvisionTermSet.recurrent_paytools, VS). + +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 test() -> _. + +-spec merge_fd_overrides_test_() -> _. +merge_fd_overrides_test_() -> + [ + ?_assertEqual(?fd_overrides(undefined), merge_fd_overrides(undefined, ?fd_overrides(undefined))), + ?_assertEqual(?fd_overrides(true), merge_fd_overrides(?fd_overrides(true), undefined)), + ?_assertEqual(?fd_overrides(true), merge_fd_overrides(?fd_overrides(true), ?fd_overrides(undefined))), + ?_assertEqual(?fd_overrides(false), merge_fd_overrides(?fd_overrides(true), ?fd_overrides(false))) + ]. + +-endif. \ No newline at end of file diff --git a/apps/routing/src/hg_route_fd.erl b/apps/routing/src/hg_route_fd.erl new file mode 100644 index 00000000..8e049734 --- /dev/null +++ b/apps/routing/src/hg_route_fd.erl @@ -0,0 +1,83 @@ +-module(hg_route_fd). + +-include_lib("damsel/include/dmsl_domain_thrift.hrl"). +-include_lib("fault_detector_proto/include/fd_proto_fault_detector_thrift.hrl"). + +%% + +-behaviour(hg_route_collector). +-export([fill/1]). + +%% + +-type route() :: hg_route:t(). + +-define(fd_overrides(Enabled), #domain_RouteFaultDetectorOverrides{enabled = Enabled}). + +%% + +-spec fill([route()]) -> [route()]. +fill([]) -> + []; +fill(Routes) -> + #{ + service_ids := ServiceIDs, + service_map := ServiceMap, + routes := RouteMap + } = lists:foldl(fun build_route_map/2, #{service_ids => [], service_map => #{}, routes => #{}}, Routes), + FDStats = hg_fault_detector_client:get_statistics(ServiceIDs), + maps:values(lists:foldl(fun(Stats, Map) -> fill_fd_score(Stats, ServiceMap, Map) end, RouteMap, FDStats)). + +%% + +build_route_map(Route, #{service_ids := ServiceIDs, service_map := ServiceMap, routes := RouteMap}) -> + #domain_ProviderRef{id = ProviderID} = hg_route:provider_ref(Route), + PaymentRoute = hg_route:to_payment_route(Route), + AvailabilityID = hg_fault_detector_client:build_service_id(adapter_availability, ProviderID), + ConversionID = hg_fault_detector_client:build_service_id(provider_conversion, ProviderID), + #{ + service_ids => [AvailabilityID, ConversionID | ServiceIDs], + service_map => ServiceMap#{ + AvailabilityID => {availability, PaymentRoute}, + ConversionID => {conversion, PaymentRoute} + }, + routes => RouteMap#{PaymentRoute => Route} + }. + +fill_fd_score(#fault_detector_ServiceStatistics{service_id = ID, failure_rate = FailRate}, ServiceMap, RouteMap) -> + case maps:get(ID, ServiceMap, undefined) of + undefined -> + RouteMap; + {availability, PaymentRoute} -> + Route = maps:get(PaymentRoute, 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), + NewRoute = maybe_override( + hg_route:fd_overrides(Route), + hg_route:set_availability(Condition, Value, Route), + Route + ), + RouteMap#{PaymentRoute => NewRoute}; + {conversion, PaymentRoute} -> + Route = maps:get(PaymentRoute, 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), + NewRoute = maybe_override( + hg_route:fd_overrides(Route), + hg_route:set_conversion(Condition, Value, Route), + Route + ), + RouteMap#{PaymentRoute => NewRoute} + end. + +maybe_override(?fd_overrides(true), _NewRoute, Route) -> + Route; +maybe_override(_, NewRoute, _Route) -> + NewRoute. + +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..a7e31f3c 100644 --- a/apps/routing/src/hg_routing.erl +++ b/apps/routing/src/hg_routing.erl @@ -1,63 +1,24 @@ -%%% Naïve routing oracle - -module(hg_routing). -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([gather_routes/5]). --export([rate_routes/1]). --export([choose_route/1]). --export([choose_rated_route/1]). - --export([get_payment_terms/3]). --export([get_provision_terms/3]). - --export([get_logger_metadata/2]). --export([prepare_log_message/1]). %% - --export([filter_by_critical_provider_status/1]). --export([filter_by_blacklist/2]). --export([choose_route_with_ctx/1]). +-export([prepare_log_message/1]). +-export([get_logger_metadata/2]). +-export([get_routes/1]). +-export([filter_routes/2]). +-export([choose_route/1]). %% --type provision_terms() :: dmsl_domain_thrift:'ProvisionTermSet'(). --type payment_terms() :: dmsl_domain_thrift:'PaymentsProvisionTerms'(). --type payment_institution() :: dmsl_domain_thrift:'PaymentInstitution'(). --type route_predestination() :: payment | recurrent_payment. - --define(rejected(Reason), {rejected, Reason}). - --define(fd_overrides(Enabled), #domain_RouteFaultDetectorOverrides{enabled = Enabled}). - --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()}. - --type availability_status() :: {availability_condition(), availability_fail_rate()}. --type conversion_status() :: {conversion_condition(), conversion_fail_rate()}. - --type availability_condition() :: alive | dead. --type availability_fail_rate() :: float(). - --type conversion_condition() :: normal | lacking. --type conversion_fail_rate() :: float(). - --type route_groups_by_priority() :: #{{availability_condition(), terminal_priority_rating()} => [fail_rated_route()]}. - --type fail_rated_route() :: {hg_route:t(), provider_status()}. --type blacklisted_route() :: {hg_route:t(), boolean()}. - --type scored_route() :: {route_scores(), hg_route:t()}. +-type get_route_params() :: #{ + predestination := hg_route_collector:route_predestination(), + revision := hg_route_collector:revision(), + varset := hg_route_collector:varset(), + payment_institution := hg_route_collector:payment_institution(), + pin_context := hg_route_collector:gather_route_context(), + blacklist_context => hg_route_collector:blacklist_context() +}. -type route_choice_context() :: #{ chosen_route => hg_route:t(), @@ -66,273 +27,147 @@ reject_reason => atom() }. --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 rejection_group() :: atom(). +-type filter_routes_fun() :: fun((filter_routes_result()) -> filter_routes_result()). + +-type filter_routes_result() :: #{ + routes := [hg_route:t()], + rejected_routes => [hg_route:rejected_route()], + latest_rejected_group => rejection_group() | undefined, + rejection_groups => #{rejection_group() => [hg_route:rejected_route()]}, + considered_routes => [hg_route:t()], + route_limits => limits(), + route_scores => scores() }. --type varset() :: hg_varset:varset(). --type revision() :: hg_domain:revision(). - -type route_scores() :: #domain_PaymentRouteScores{}. -type limits() :: #{hg_route:payment_route() => [hg_limiter:turnover_limit_value()]}. -type scores() :: #{hg_route:payment_route() => hg_routing:route_scores()}. +-type route_predestination() :: payment | recurrent_payment. + -type misconfiguration_error() :: {misconfiguration, {routing_decisions, _} | {routing_candidate, _}}. --export_type([route_predestination/0]). --export_type([route_choice_context/0]). --export_type([fail_rated_route/0]). --export_type([blacklisted_route/0]). +-export_type([get_route_params/0]). +-export_type([filter_routes_result/0]). -export_type([route_scores/0]). -export_type([limits/0]). -export_type([scores/0]). +-export_type([route_predestination/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 - end, - hg_routing_ctx:with_fail_rates(RoutesFailRates, Ctx1), - RoutesFailRates - ). - --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( - 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 - end, - Ctx, - BlacklistedRoutes - ). - --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). - -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 - ). - -%% +-define(ZERO, 0). -spec prepare_log_message(misconfiguration_error()) -> {io:format(), [term()]}. prepare_log_message({misconfiguration, {routing_decisions, Details}}) -> {"PaymentRoutingDecisions couldn't be reduced to candidates, ~p", [Details]}; prepare_log_message({misconfiguration, {routing_candidate, Candidate}}) -> - {"PaymentRoutingCandidate couldn't be reduced, ~p", [Candidate]}. - -%% - --spec gather_routes(route_predestination(), payment_institution(), varset(), revision(), gather_route_context()) -> - hg_routing_ctx:t(). -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, - 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) - ) - catch - throw:{misconfiguration, _Reason} = Error -> - hg_routing_ctx:set_error(Error, hg_routing_ctx:new([])) - end. + {"PaymentRoutingCandidate couldn't be reduced, ~p", [Candidate]}; +prepare_log_message({misconfiguration, {payment_routing_rules, empty} }) -> + {"PaymentRoutingRules are empty", []}. -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_logger_metadata(route_choice_context(), hg_route_collector:revision()) -> LoggerFormattedMetadata :: map(). +get_logger_metadata(RouteChoiceContext, Revision) -> + maps:fold( + fun(K, V, Acc) -> + Acc#{K => format_logger_metadata(K, V, Revision)} end, #{}, - get_decisions_candidates(RuleSetDeny) - ). - -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. - -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(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 - end, - {[], []}, - Candidates + RouteChoiceContext ). -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). +format_logger_metadata(reject_reason, Reason, _) -> + Reason; +format_logger_metadata(Meta, Route, Revision) when + Meta =:= chosen_route; + Meta =:= preferable_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 => hg_route:priority(Route), + weight => hg_route:weight(Route) + }). -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 get_routes(get_route_params()) -> hg_route_collector:get_route_resut(). +get_routes(Params = #{ + predestination := Predestination, + revision := Revision, + varset := VS, + payment_institution := PI, + pin_context := PinCtx +}) -> + Result = #{routes := Routes0} = hg_route_collector:get_routes(Revision, VS, PI, PinCtx), + Routes1 = hg_route_collector:fill_accepted(Predestination, Revision, VS, Routes0), + Routes2 = hg_route_collector:fill_prohibition(Revision, VS, PI, Routes1), + Routes3 = hg_route_collector:fill_fd_overrides(Revision, Routes2), + Routes4 = case maps:get(blacklist_context, Params, undefined) of + undefined -> + Routes3; + BlCtx -> + hg_route_collector:fill_blacklist(BlCtx, Routes3) + end, + Routes5 = hg_route_fd:fill(Routes4), + Result#{routes => hg_route_balancer:fill(Routes5)}. + +-spec filter_routes([hg_route:t()], [filter_routes_fun()]) -> filter_routes_result(). +filter_routes(Routes0, WithFilterFuns) -> + Result0 = init_filter_result(filter(Routes0, [{accepted, false}, {prohibit, true}, {blacklisted, 1}])), + lists:foldl(fun(Fun, Result) -> Fun(Result) end, Result0, WithFilterFuns). + +init_filter_result(#{rejected_routes := []} = Result) -> + Result#{ + latest_rejected_group => undefined, + rejection_groups => #{} + }; +init_filter_result(#{rejected_routes := RejectedRoutes} = Result) -> + Result#{ + latest_rejected_group => forbidden, + rejection_groups => #{ + forbidden => RejectedRoutes + } + }. -filter_routes({Routes, Rejected}, Prohibitions) -> +filter(Routes, Keys) -> 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} + fun(Route, #{routes := Accepted, rejected_routes := Rejected} = Acc) -> + case route_rejection_reason(Route, Keys) of + undefined -> + Acc#{routes => [Route | Accepted]}; + Reason -> + Acc#{rejected_routes => [{Route, Reason} | Rejected]} end end, - {[], Rejected}, + #{routes => [], rejected_routes => []}, 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. - --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]. +route_rejection_reason(Route, Keys) -> + Data = hg_route:route_data(Route), + get_rejection_reason(Keys, Data). --spec rate_routes([hg_route:t()]) -> [fail_rated_route()]. -rate_routes(Routes) -> - score_routes_with_fault_detector(Routes). +get_rejection_reason([{Key, Value} | Rest], Data) -> + case maps:get(Key, Data, undefined) of + {Value, Reason} -> + {Key, Reason}; + Value -> + {Key, Value}; + _ -> + get_rejection_reason(Rest, Data) + end; +get_rejection_reason([], _) -> + undefined. -spec choose_route([hg_route:t()]) -> {hg_route:t(), route_choice_context()}. choose_route(Routes) -> - FailRatedRoutes = rate_routes(Routes), - choose_rated_route(FailRatedRoutes). - --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), - {ChosenScoredRoute, IdealRoute} = find_best_routes(ScoredRoutes), + {ChosenScoredRoute, IdealRoute} = find_best_routes(Routes), RouteChoiceContext = get_route_choice_context(ChosenScoredRoute, IdealRoute), - {_, Route} = ChosenScoredRoute, - {Route, RouteChoiceContext}. + {ChosenScoredRoute, RouteChoiceContext}. +%% --spec find_best_routes([scored_route()]) -> {Chosen :: scored_route(), Ideal :: scored_route()}. +-spec find_best_routes([hg_route:t()]) -> {Chosen :: hg_route:t(), Ideal :: hg_route:t()}. find_best_routes([Route]) -> {Route, Route}; find_best_routes([First | Rest]) -> @@ -346,9 +181,21 @@ find_best_routes([First | Rest]) -> Rest ). -select_better_route({LeftScore, _} = Left, {RightScore, _} = Right) -> - LeftPin = LeftScore#domain_PaymentRouteScores.route_pin, - RightPin = RightScore#domain_PaymentRouteScores.route_pin, +select_better_route_ideal(Left, Right) -> + IdealLeft = set_ideal_score(Left), + IdealRight = set_ideal_score(Right), + case select_better_route(IdealLeft, IdealRight) of + IdealLeft -> Left; + IdealRight -> Right + end. + +set_ideal_score(Route0) -> + Route1 = hg_route:set_availability(1, 1.0, Route0), + hg_route:set_conversion(1, 1.0, Route1). + +select_better_route(Left, Right) -> + LeftPin = hg_route:pin_hash(Left), + RightPin = hg_route:pin_hash(Right), Res = case {LeftPin, RightPin} of _ when LeftPin /= ?ZERO, RightPin /= ?ZERO, RightPin == LeftPin -> @@ -358,102 +205,56 @@ select_better_route({LeftScore, _} = Left, {RightScore, _} = Right) -> end, Res. -select_better_pinned_route({LeftScore0, LeftRoute} = Left, {RightScore0, RightRoute} = Right) -> - LeftScore1 = LeftScore0#domain_PaymentRouteScores{ +select_better_pinned_route(Left, Right) -> + LeftScore = (hg_route:score(Left))#domain_PaymentRouteScores{ random_condition = 0, route_pin = erlang:phash2({ - LeftScore0#domain_PaymentRouteScores.route_pin, - hg_route:provider_ref(LeftRoute), - hg_route:terminal_ref(LeftRoute) + hg_route:pin_hash(Left), + hg_route:provider_ref(Left), + hg_route:terminal_ref(Left) }) }, - RightScore1 = RightScore0#domain_PaymentRouteScores{ + RightScore = (hg_route:score(Right))#domain_PaymentRouteScores{ random_condition = 0, route_pin = erlang:phash2({ - RightScore0#domain_PaymentRouteScores.route_pin, - hg_route:provider_ref(RightRoute), - hg_route:terminal_ref(RightRoute) + hg_route:pin_hash(Right), + hg_route:provider_ref(Right), + hg_route:terminal_ref(Right) }) }, - case max(LeftScore1, RightScore1) of - LeftScore1 -> + case max(LeftScore, RightScore) of + LeftScore -> Left; - RightScore1 -> + RightScore -> Right end. -select_better_regular_route({LeftScore0, LRoute}, {RightScore0, RRoute}) -> - LeftScore1 = LeftScore0#domain_PaymentRouteScores{ +select_better_regular_route(Left, Right) -> + LeftScore = (hg_route:score(Left))#domain_PaymentRouteScores{ route_pin = 0 }, - RightScore1 = RightScore0#domain_PaymentRouteScores{ + RightScore = (hg_route:score(Right))#domain_PaymentRouteScores{ route_pin = 0 }, - case max({LeftScore1, LRoute}, {RightScore1, RRoute}) of - {LeftScore1, LRoute} -> - {LeftScore0, LRoute}; - {RightScore1, RRoute} -> - {RightScore0, RRoute} - end. - -select_better_route_ideal(Left, Right) -> - IdealLeft = set_ideal_score(Left), - IdealRight = set_ideal_score(Right), - case select_better_route(IdealLeft, IdealRight) of - IdealLeft -> Left; - IdealRight -> Right + case max(LeftScore, RightScore) of + LeftScore -> + Left; + RightScore -> + Right end. -set_ideal_score({RouteScores, PT}) -> - { - RouteScores#domain_PaymentRouteScores{ - availability_condition = 1, - availability = 1.0, - conversion_condition = 1, - conversion = 1.0 - }, - PT - }. - -get_route_choice_context({_, SameRoute}, {_, SameRoute}) -> +get_route_choice_context(SameRoute, SameRoute) -> #{ chosen_route => SameRoute }; -get_route_choice_context({ChosenScores, ChosenRoute}, {IdealScores, IdealRoute}) -> +get_route_choice_context(ChosenRoute, IdealRoute) -> #{ chosen_route => ChosenRoute, preferable_route => IdealRoute, - reject_reason => map_route_switch_reason(ChosenScores, IdealScores) + reject_reason => map_route_switch_reason(hg_route:score(ChosenRoute), hg_route:score(IdealRoute)) }. --spec get_logger_metadata(route_choice_context(), revision()) -> LoggerFormattedMetadata :: map(). -get_logger_metadata(RouteChoiceContext, Revision) -> - maps:fold( - fun(K, V, Acc) -> - Acc#{K => format_logger_metadata(K, V, Revision)} - end, - #{}, - RouteChoiceContext - ). - -format_logger_metadata(reject_reason, Reason, _) -> - Reason; -format_logger_metadata(Meta, Route, Revision) when - Meta =:= chosen_route; - Meta =:= preferable_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 => hg_route:priority(Route), - weight => hg_route:weight(Route) - }). - map_route_switch_reason(SameScores, SameScores) -> unknown; map_route_switch_reason(RealScores, IdealScores) when @@ -471,387 +272,12 @@ find_idx_of_difference([{Same, Same} | Rest], I) -> find_idx_of_difference(_, I) -> I. --spec balance_routes([fail_rated_route()]) -> [fail_rated_route()]. -balance_routes(FailRatedRoutes) -> - FilteredRouteGroups = lists:foldl( - fun group_routes_by_priority/2, - #{}, - FailRatedRoutes - ), - balance_route_groups(FilteredRouteGroups). - --spec group_routes_by_priority(fail_rated_route(), Acc :: route_groups_by_priority()) -> route_groups_by_priority(). -group_routes_by_priority({Route, {ProviderCondition, _}} = FailRatedRoute, SortedRoutes) -> - TerminalPriority = hg_route:priority(Route), - Key = {ProviderCondition, TerminalPriority}, - Routes = maps:get(Key, SortedRoutes, []), - SortedRoutes#{Key => [FailRatedRoute | Routes]}. - --spec balance_route_groups(route_groups_by_priority()) -> [fail_rated_route()]. -balance_route_groups(RouteGroups) -> - maps:fold( - fun(_Priority, Routes, Acc) -> - NewRoutes = set_routes_random_condition(Routes), - NewRoutes ++ Acc - end, - [], - RouteGroups - ). - -set_routes_random_condition(Routes) -> - Summary = get_summary_weight(Routes), - Random = rand:uniform() * Summary, - lists:reverse(calc_random_condition(0.0, Random, Routes, [])). - -get_summary_weight(FailRatedRoutes) -> - lists:foldl( - fun({Route, _}, Acc) -> - Weight = hg_route:weight(Route), - Acc + Weight - end, - 0, - FailRatedRoutes - ). - -calc_random_condition(_, _, [], Routes) -> - Routes; -calc_random_condition(StartFrom, Random, [FailRatedRoute | Rest], Routes) -> - {Route, Status} = FailRatedRoute, - Weight = hg_route:weight(Route), - InRange = (Random >= StartFrom) and (Random < StartFrom + Weight), - case InRange of - true -> - NewRoute = hg_route:set_weight(1, Route), - calc_random_condition(StartFrom + Weight, Random, Rest, [{NewRoute, Status} | Routes]); - false -> - NewRoute = hg_route:set_weight(0, Route), - calc_random_condition(StartFrom + Weight, Random, Rest, [{NewRoute, Status} | Routes]) - end. - --spec score_routes_map([fail_rated_route()]) -> #{hg_route:payment_route() => route_scores()}. -score_routes_map(Routes) -> - lists:foldl( - fun({Route, _} = FailRatedRoute, Acc) -> - Acc#{hg_route:to_payment_route(Route) => score_route_ext(FailRatedRoute)} - end, - #{}, - Routes - ). - --spec score_routes([fail_rated_route()]) -> [scored_route()]. -score_routes(Routes) -> - [{score_route_ext(FailRatedRoute), Route} || {Route, _} = FailRatedRoute <- Routes]. - -score_route_ext({Route, ProviderStatus}) -> - {AvailabilityStatus, ConversionStatus} = ProviderStatus, - {AvailabilityCondition, Availability} = get_availability_score(AvailabilityStatus), - {ConversionCondition, Conversion} = get_conversion_score(ConversionStatus), - Scores = score_route(Route), - Scores#domain_PaymentRouteScores{ - availability_condition = AvailabilityCondition, - conversion_condition = ConversionCondition, - availability = Availability, - conversion = Conversion - }. - -score_route(Route) -> - PriorityRate = hg_route:priority(Route), - Pin = hg_route:pin(Route), - #domain_PaymentRouteScores{ - terminal_priority_rating = PriorityRate, - route_pin = get_pin_hash(Pin), - random_condition = hg_route:weight(Route), - blacklist_condition = 0 - }. - -get_pin_hash(Pin) when map_size(Pin) == 0 -> - ?ZERO; -get_pin_hash(Pin) -> - erlang:phash2(Pin). - -get_availability_score({alive, FailRate}) -> {1, 1.0 - FailRate}; -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([hg_route:t()]) -> [fail_rated_route()]. -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). - --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(), - {ok, TermsSet} = party_client_thrift:compute_provider_terminal_terms( - ProviderRef, - TerminalRef, - Revision, - PreparedVS, - Client, - Context - ), - TermsSet#domain_ProvisionTermSet.payments. - --spec get_provision_terms(hg_route:payment_route(), varset(), revision()) -> provision_terms() | undefined. -get_provision_terms(?route(ProviderRef, TerminalRef), VS, Revision) -> - PreparedVS = hg_varset:prepare_varset(VS), - {Client, Context} = get_party_client(), - {ok, TermsSet} = party_client_thrift:compute_provider_terminal_terms( - ProviderRef, - TerminalRef, - Revision, - PreparedVS, - Client, - Context - ), - 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"). +-include_lib("damsel/include/dmsl_domain_thrift.hrl"). -spec test() -> _. -type testcase() :: {_, fun(() -> _)}. @@ -861,50 +287,49 @@ getv(Name, VS, Default) -> -spec record_comparsion_test() -> _. record_comparsion_test() -> - Bigger = { - #domain_PaymentRouteScores{ - availability_condition = 1, - availability = 0.5, - conversion_condition = 1, - conversion = 0.5, - terminal_priority_rating = 1, - route_pin = 0, - random_condition = 1 - }, - {42, 42} - }, - Smaller = { - #domain_PaymentRouteScores{ - availability_condition = 0, - availability = 0.1, - conversion_condition = 1, - conversion = 0.5, - terminal_priority_rating = 1, - route_pin = 0, - random_condition = 1 - }, - {99, 99} - }, - ?assertEqual(Bigger, select_better_route(Bigger, Smaller)). + Bigger = new_route(42, 42, 0, {1, 0.5}, {1, 0.5}), + Middle = new_route(50, 50, 0, {1, 0.1}, {1, 0.5}), + Smaller = new_route(99, 99, 0, {0, 0.1}, {1, 0.5}), + ?assertEqual(Bigger, select_better_route(Bigger, Smaller)), + ?assertEqual(Middle, select_better_route(Middle, Smaller)), + ?assertEqual(Bigger, select_better_route(Bigger, Middle)), + ?assertMatch( + {?trm(42), _}, + balance_and_choose_route([ + Bigger, + Smaller + ]) + ), + ?assertMatch( + {?trm(50), _}, + balance_and_choose_route([ + Middle, + Smaller + ]) + ), + ?assertMatch( + {?trm(42), _}, + balance_and_choose_route([ + Middle, + Bigger + ]) + ). -spec pin_random_test() -> _. pin_random_test() -> - Pin = #{ - email => <<"example@mail.com">> - }, - Scores = {{alive, 0.0}, {normal, 0.0}}, - Route1 = {hg_route:new(?prv(1), ?trm(1), 50, 1, Pin), Scores}, - Route2 = {hg_route:new(?prv(2), ?trm(2), 50, 1, Pin), Scores}, + Pin = #{email => <<"example@mail.com">>}, + Route1 = new_route(1, 1, 50, {1, 0.0}, {1, 0.0}, Pin), + Route2 = new_route(2, 2, 50, {1, 0.0}, {1, 0.0}, Pin), lists:foldl( fun(_I, Acc) -> - {ST, _} = ShuffledRoute = shuffle_routes([Route1, Route2]), + {ST, _} = Route = balance_and_choose_route([Route1, Route2]), case Acc of undefined -> - ShuffledRoute; + Route; {ST, _} -> - ShuffledRoute; + Route; _ -> - error({ShuffledRoute, Acc}) + error({Route, Acc}) end end, undefined, @@ -913,16 +338,13 @@ pin_random_test() -> -spec diff_pin_test() -> _. diff_pin_test() -> - Pin = #{ - email => <<"example@mail.com">> - }, - Scores = {{alive, 0.0}, {normal, 0.0}}, - Route1 = {hg_route:new(?prv(1), ?trm(1), 50, 33, Pin), Scores}, - Route2 = {hg_route:new(?prv(1), ?trm(2), 50, 33, Pin), Scores}, - Route3 = {hg_route:new(?prv(1), ?trm(3), 50, 33, Pin#{client_ip => <<"IP">>}), Scores}, + Pin = #{email => <<"example@mail.com">>}, + Route1 = new_route(1, 1, 50, {1, 0.0}, {1, 0.0}, Pin), + Route2 = new_route(1, 2, 50, {1, 0.0}, {1, 0.0}, Pin), + Route3 = new_route(1, 3, 50, {1, 0.0}, {1, 0.0}, Pin#{client_ip => <<"IP">>}), {I1, I2, I3} = lists:foldl( fun(_I, {Iter1, Iter2, Iter3}) -> - {ST, _} = shuffle_routes([Route1, Route2, Route3]), + {ST, _} = balance_and_choose_route([Route1, Route2, Route3]), case ST of ?trm(1) -> {Iter1 + 1, Iter2, Iter3}; @@ -952,202 +374,151 @@ diff_pin_test() -> -spec pin_weight_test() -> _. pin_weight_test() -> - Pin0 = #{ - email => <<"example@mail.com">> - }, - Pin1 = #{ - email => <<"example1@mail.com">> - }, - Scores1 = {{alive, 0.0}, {normal, 0.0}}, - Scores2 = {{alive, 0.0}, {normal, 0.0}}, - Route1 = {hg_route:new(?prv(1), ?trm(1), 50, 1, Pin0, ?fd_overrides(true)), Scores1}, - Route2 = {hg_route:new(?prv(1), ?trm(2), 50, 1, Pin0, ?fd_overrides(true)), Scores2}, - Route3 = {hg_route:new(?prv(1), ?trm(1), 50, 1, Pin1, ?fd_overrides(true)), Scores1}, - Route4 = {hg_route:new(?prv(1), ?trm(2), 50, 1, Pin1, ?fd_overrides(true)), Scores2}, + Pin0 = #{email => <<"example@mail.com">>}, + Pin1 = #{email => <<"example1@mail.com">>}, + Route1 = new_route(1, 1, 50, {1, 0.0}, {1, 0.0}, Pin0), + Route2 = new_route(1, 2, 50, {1, 0.0}, {1, 0.0}, Pin0), + Route3 = new_route(1, 1, 50, {1, 0.0}, {1, 0.0}, Pin1), + Route4 = new_route(1, 2, 50, {1, 0.0}, {1, 0.0}, Pin1), true = lists:foldl( fun(_I, _A) -> - {ShuffledRoute1, _} = shuffle_routes([Route1, Route2]), - {ShuffledRoute2, _} = shuffle_routes([Route3, Route4]), + {BalancedRoute1, _} = balance_and_choose_route([Route1, Route2]), + {BalancedRoute2, _} = balance_and_choose_route([Route3, Route4]), case true of - _ when ShuffledRoute1 == ?trm(1), ShuffledRoute2 == ?trm(2) -> + _ when BalancedRoute1 == ?trm(1), BalancedRoute2 == ?trm(2) -> true; _ -> - error({ShuffledRoute1, ShuffledRoute2}) + error({BalancedRoute1, BalancedRoute2}) end end, true, lists:seq(0, 1000) ). -shuffle_routes(Routes) -> - BalancedRoutes = balance_routes(Routes), - ScoredRoutes = score_routes(BalancedRoutes), - {{_, ChosenScoredRoute}, _IdealRoute} = find_best_routes(ScoredRoutes), - {hg_route:terminal_ref(ChosenScoredRoute), ChosenScoredRoute}. - --spec balance_routes_test_() -> [testcase()]. -balance_routes_test_() -> - Status = {{alive, 0.0}, {normal, 0.0}}, - WithWeight = [ - {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 = [ - {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 = [ - {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 = [ - {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, []))), - ?_assertEqual(Result2, lists:reverse(calc_random_condition(0.0, 1.5, WithWeight, []))), - ?_assertEqual(Result3, lists:reverse(calc_random_condition(0.0, 4.0, WithWeight, []))) - ]. +new_route(PNum, TNum, Weight, {ACond, AValue}, {CCond, CValue}) -> + new_route(PNum, TNum, Weight, {ACond, AValue}, {CCond, CValue}, #{}). + +new_route(PNum, TNum, Weight, {ACond, AValue}, {CCond, CValue}, Pin) -> + Route0 = hg_route:new(1, ?prv(PNum), ?trm(TNum), Weight, ?DOMAIN_CANDIDATE_PRIORITY, Pin), + Route1 = hg_route:set_availability(ACond, AValue, Route0), + hg_route:set_conversion(CCond, CValue, Route1). + +balance_and_choose_route(Routes0) -> + Routes1 = hg_route_balancer:fill(Routes0), + {Route, Ctx} = choose_route(Routes1), + {hg_route:terminal_ref(Route), {Route, Ctx}}. + +-spec filter_routes_splits_accepted_and_rejected_test() -> _. +filter_routes_splits_accepted_and_rejected_test() -> + AcceptedRoute = new_route(1, 1, 0, {1, 1.0}, {1, 1.0}), + RejectedByTerms = hg_route:set_accepted( + {false, {rejected, {'ProvisionTermSet', undefined}}}, + new_route(1, 2, 0, {1, 1.0}, {1, 1.0}) + ), + RejectedByProhibition = hg_route:set_prohibit( + {true, <<"blocked">>}, + new_route(1, 3, 0, {1, 1.0}, {1, 1.0}) + ), + RejectedByBlacklist = hg_route:set_blacklisted( + 1, + new_route(1, 4, 0, {1, 1.0}, {1, 1.0}) + ), --spec balance_routes_with_default_weight_test_() -> testcase(). -balance_routes_with_default_weight_test_() -> - Status = {{alive, 0.0}, {normal, 0.0}}, - Routes = [ - {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 = [ - {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)). + ?assertEqual( + #{ + routes => [AcceptedRoute], + rejected_routes => [ + {RejectedByTerms, {rejected, {'ProvisionTermSet', undefined}}}, + {RejectedByProhibition, <<"blocked">>}, + {RejectedByBlacklist, blacklisted} + ] + }, + filter_routes( + [AcceptedRoute, RejectedByTerms, RejectedByProhibition, RejectedByBlacklist], + [] + ) + ). -spec preferable_route_scoring_test_() -> [testcase()]. preferable_route_scoring_test_() -> - StatusAlive = {{alive, 0.0}, {normal, 0.0}}, - StatusAliveLowerConversion = {{alive, 0.0}, {normal, 0.1}}, - StatusDead = {{dead, 0.4}, {lacking, 0.6}}, - StatusDegraded = {{alive, 0.1}, {normal, 0.1}}, - StatusBroken = {{alive, 0.1}, {lacking, 0.8}}, - 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), + RouteFallback0 = hg_route:new(1, ?prv(2), ?trm(99), 0, 0, #{}), + RouteFallback1 = hg_route:set_availability(1, 1.0, RouteFallback0), + RouteFallback2 = hg_route:set_conversion(1, 1.0, RouteFallback1), [ ?_assertMatch( - {RoutePreferred1, #{}}, - choose_rated_route([ - {RoutePreferred1, StatusAlive}, - {RouteFallback, StatusAlive} + {?trm(1), _}, + balance_and_choose_route([ + new_route(1, 1, 0, {1, 1.0}, {1, 1.0}), + RouteFallback2 ]) ), - ?_assertEqual( - {RoutePreferred3, #{ - chosen_route => RoutePreferred3 - }}, - choose_rated_route([ - {RoutePreferred1, StatusDead}, - {RoutePreferred2, StatusDead}, - {RoutePreferred3, StatusAlive} + ?_assertMatch( + {?trm(3), _}, + balance_and_choose_route([ + new_route(1, 1, 0, {0, 0.6}, {0, 0.4}), + new_route(1, 2, 0, {0, 0.6}, {0, 0.4}), + new_route(1, 3, 0, {1, 1.0}, {1, 1.0}) ]) ), ?_assertMatch( - {RouteFallback, #{ - preferable_route := RoutePreferred1, + {?trm(99), {_, #{ + preferable_route := #{terminal_ref := ?trm(1)}, reject_reason := availability_condition - }}, - choose_rated_route([ - {RoutePreferred1, StatusDead}, - {RouteFallback, StatusAlive} + }}}, + balance_and_choose_route([ + new_route(1, 1, 0, {0, 0.6}, {0, 0.4}), + RouteFallback2 ]) ), ?_assertMatch( - {RouteFallback, #{ - preferable_route := RoutePreferred1, + {?trm(99), {_, #{ + preferable_route := #{terminal_ref := ?trm(1)}, reject_reason := conversion_condition - }}, - choose_rated_route([ - {RoutePreferred1, StatusBroken}, - {RouteFallback, StatusAlive} + }}}, + balance_and_choose_route([ + new_route(1, 1, 0, {1, 0.9}, {0, 0.2}), + RouteFallback2 ]) ), ?_assertMatch( - {RoutePreferred1, #{ - preferable_route := RoutePreferred2, + {?trm(1), {_, #{ + preferable_route := #{terminal_ref := ?trm(2)}, reject_reason := conversion - }}, - choose_rated_route([ - {RoutePreferred1, StatusAlive}, - {RoutePreferred2, StatusAliveLowerConversion} + }}}, + balance_and_choose_route([ + new_route(1, 2, 0, {1, 1.0}, {1, 0.9}), + new_route(1, 1, 0, {1, 1.0}, {1, 1.0}) ]) ), - % TODO TD-344 - % We rely here on inverted order of preference which is just an accidental - % side effect. ?_assertMatch( - {RoutePreferred1, #{ - preferable_route := RoutePreferred2, + {?trm(1), {_, #{ + preferable_route := #{terminal_ref := ?trm(2)}, reject_reason := availability - }}, - choose_rated_route([ - {RoutePreferred1, StatusAlive}, - {RoutePreferred2, StatusDegraded}, - {RouteFallback, StatusAlive} + }}}, + balance_and_choose_route([ + new_route(1, 2, 0, {1, 0.9}, {1, 0.9}), + new_route(1, 1, 0, {1, 1.0}, {1, 1.0}), + RouteFallback2 ]) ) ]. --spec prefer_weight_over_availability_test() -> _. -prefer_weight_over_availability_test() -> - 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), +-spec prefer_priority_over_availability_test() -> _. +prefer_priority_over_availability_test() -> + Route1 = new_route(1, 1, 0, {1, 0.7}, {1, 0.7}), + Route2 = hg_route:set_priority(1005, new_route(2, 2, 0, {1, 0.5}, {1, 0.7})), + Route3 = new_route(3, 3, 0, {1, 0.7}, {1, 0.7}), Routes = [Route1, Route2, Route3], - ProviderStatuses = [ - {{alive, 0.3}, {normal, 0.3}}, - {{alive, 0.5}, {normal, 0.3}}, - {{alive, 0.3}, {normal, 0.3}} - ], - FailRatedRoutes = lists:zip(Routes, ProviderStatuses), - ?assertMatch({Route2, _}, choose_rated_route(FailRatedRoutes)). + ?assertMatch({?trm(2), _}, balance_and_choose_route(Routes)). --spec prefer_weight_over_conversion_test() -> _. -prefer_weight_over_conversion_test() -> - 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), +-spec prefer_priority_over_conversion_test() -> _. +prefer_priority_over_conversion_test() -> + Route1 = new_route(1, 1, 0, {1, 0.7}, {1, 0.7}), + Route2 = hg_route:set_priority(1005, new_route(2, 2, 0, {1, 0.7}, {1, 0.5})), + Route3 = new_route(3, 3, 0, {1, 0.7}, {1, 0.7}), Routes = [Route1, Route2, Route3], - ProviderStatuses = [ - {{alive, 0.3}, {normal, 0.5}}, - {{alive, 0.3}, {normal, 0.3}}, - {{alive, 0.3}, {normal, 0.3}} - ], - FailRatedRoutes = lists:zip(Routes, ProviderStatuses), - {Route2, _Meta} = choose_rated_route(FailRatedRoutes). - --spec merge_fd_overrides_test_() -> _. -merge_fd_overrides_test_() -> - [ - ?_assertEqual(?fd_overrides(undefined), merge_fd_overrides(undefined, ?fd_overrides(undefined))), - ?_assertEqual(?fd_overrides(true), merge_fd_overrides(?fd_overrides(true), undefined)), - ?_assertEqual(?fd_overrides(true), merge_fd_overrides(?fd_overrides(true), ?fd_overrides(undefined))), - ?_assertEqual(?fd_overrides(false), merge_fd_overrides(?fd_overrides(true), ?fd_overrides(false))) - ]. + ?assertMatch({?trm(2), _}, balance_and_choose_route(Routes)). -endif. 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. From d5d5e540107661a5fafdc340d6ad1c8cb6a307ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC?= Date: Tue, 24 Mar 2026 20:11:00 +0300 Subject: [PATCH 2/4] finish first path --- apps/hellgate/src/hg_inspector.erl | 10 +- apps/hellgate/src/hg_invoice_payment.erl | 60 +++++----- apps/routing/src/hg_route.erl | 46 +++++--- apps/routing/src/hg_route_balancer.erl | 41 +++---- apps/routing/src/hg_route_collector.erl | 21 ++-- apps/routing/src/hg_routing.erl | 140 +++++++++++++---------- 6 files changed, 172 insertions(+), 146 deletions(-) diff --git a/apps/hellgate/src/hg_inspector.erl b/apps/hellgate/src/hg_inspector.erl index 4854e3fc..476aada0 100644 --- a/apps/hellgate/src/hg_inspector.erl +++ b/apps/hellgate/src/hg_inspector.erl @@ -28,9 +28,13 @@ }. -spec fill_blacklist(hg_route:t(), blacklist_context()) -> hg_route:t(). -fill_blacklist(Route, #{revision := Revision, token := Token, inspector := #domain_Inspector{ - proxy = Proxy -}}) when Token =/= undefined -> +fill_blacklist(Route, #{ + revision := Revision, + token := Token, + inspector := #domain_Inspector{ + proxy = Proxy + } +}) when Token =/= undefined -> #domain_ProviderRef{id = ProviderID} = hg_route:provider_ref(Route), #domain_TerminalRef{id = TerminalID} = hg_route:terminal_ref(Route), Context = #proxy_inspector_BlackListContext{ diff --git a/apps/hellgate/src/hg_invoice_payment.erl b/apps/hellgate/src/hg_invoice_payment.erl index 7ba391d5..68c23da4 100644 --- a/apps/hellgate/src/hg_invoice_payment.erl +++ b/apps/hellgate/src/hg_invoice_payment.erl @@ -1898,8 +1898,18 @@ process_routing(Action, St) -> #{error := Error} -> ok = maybe_log_misconfigurations(Error), handle_choose_route_error(Error, [], St, Action); - #{routes := Routes} -> - FilterResult = hg_routing:filter_routes(Routes, build_routing_filter_funs(VS, St)), + #{routes := Routes} = GetResult -> + FilterResult0 = append_rejected_routes(forbidden, Routes, maps:get(rejected_routes, GetResult, []), #{}), + %% NOTE Since this is routing step then current attempt is not yet + %% accounted for in `St`. + NewIter = get_iter(St) + 1, + FilterFuns = [ + fun(Result) -> filter_attempted_routes(Result, St) end, + fun(Result) -> filter_routes_with_limit_hold(Result, VS, NewIter, St) end, + fun(Result) -> filter_routes_by_limit_overflow(Result, VS, NewIter, St) end, + fun filter_routes_by_critical_provider_status/1 + ], + FilterResult = hg_routing:filter_routes(FilterResult0, FilterFuns), case FilterResult of #{routes := []} -> ok = log_rejected_route_groups(FilterResult, VS), @@ -1915,19 +1925,8 @@ process_routing(Action, St) -> end end. -build_routing_filter_funs(VS, St) -> - %% NOTE Since this is routing step then current attempt is not yet - %% accounted for in `St`. - NewIter = get_iter(St) + 1, - [ - fun(Result) -> filter_attempted_routes(Result, St) end, - fun(Result) -> filter_routes_with_limit_hold(Result, VS, NewIter, St) end, - fun(Result) -> filter_routes_by_limit_overflow(Result, VS, NewIter, St) end, - fun filter_routes_by_critical_provider_status/1 - ]. - -produce_routing_events(#{error := Error, considered_routes := RollbackableCandidates} = Ctx, Revision, St) - when Error =/= undefined +produce_routing_events(#{error := Error, considered_routes := RollbackableCandidates} = Ctx, 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 @@ -1985,7 +1984,7 @@ get_routes(PaymentInstitution, VS, Revision, St) -> Payer = get_payment_payer(St), case get_predefined_route(Payer) of {ok, PaymentRoute} -> - [hg_route:from_payment_route(PaymentRoute)]; + #{routes => [hg_route:from_payment_route(PaymentRoute)]}; undefined -> get_routes_(PaymentInstitution, VS, Revision, St) end. @@ -1995,7 +1994,10 @@ filter_attempted_routes(#{routes := Routes} = Result, #st{routes = AttemptedRout fun(Route, {AcceptedAcc, RejectedAcc}) -> case lists:any(fun(AttemptedRoute) -> hg_route:equal(Route, AttemptedRoute) end, AttemptedRoutes) of true -> - {AcceptedAcc, [hg_route:to_rejected_route(Route, {'AlreadyAttempted', undefined}) | RejectedAcc]}; + {AcceptedAcc, [ + hg_route:set_rejection_reason({'AlreadyAttempted', undefined}, Route) + | RejectedAcc + ]}; false -> {[Route | AcceptedAcc], RejectedAcc} end @@ -2031,7 +2033,7 @@ handle_filtered_routes_exhaustion(Result, Revision, St, Action) -> log_rejected_route_groups(Result, VS) -> maps:fold( fun(Group, RejectedRoutes, ok) -> - log_rejected_routes(Group, RejectedRoutes, VS) + log_rejected_routes(Group, [hg_route:to_rejected_route(R) || R <- RejectedRoutes], VS) end, ok, maps:get(rejection_groups, Result, #{}) @@ -2570,9 +2572,9 @@ filter_routes_by_critical_provider_status(#{routes := Routes} = Result0) -> fun(Route, {AcceptedAcc, RejectedAcc}) -> case hg_route:fd_score(Route) of #{availability_condition := 0, availability := Availability} -> - RejectedRoute = hg_route:to_rejected_route( - Route, - {'ProviderDead', {dead, 1.0 - Availability}} + RejectedRoute = hg_route:set_rejection_reason( + {'ProviderDead', {dead, 1.0 - Availability}}, + Route ), {AcceptedAcc, [RejectedRoute | RejectedAcc]}; _ -> @@ -2602,7 +2604,7 @@ get_limit_overflow_routes(Routes, VS, Iter, St) -> {ok, Limits} -> {[Route | RoutesNoOverflowIn], RejectedIn, LimitsIn#{PaymentRoute => Limits}}; {error, {limit_overflow, IDs, Limits}} -> - RejectedRoute = hg_route:to_rejected_route(Route, {'LimitOverflow', IDs}), + RejectedRoute = hg_route:set_rejection_reason({'LimitOverflow', IDs}, Route), {RoutesNoOverflowIn, [RejectedRoute | RejectedIn], LimitsIn#{PaymentRoute => Limits}} end end, @@ -2694,7 +2696,7 @@ hold_limit_routes(Routes0, VS, Iter, St) -> do_reject_route(LimiterError, Route, TurnoverLimits, {LimitHeldRoutes, RejectedRoutes}) -> LimitsIDs = [T#domain_TurnoverLimit.ref#domain_LimitConfigRef.id || T <- TurnoverLimits], - RejectedRoute = hg_route:to_rejected_route(Route, {'LimitHoldError', LimitsIDs, LimiterError}), + RejectedRoute = hg_route:set_rejection_reason({'LimitHoldError', LimitsIDs, LimiterError}, Route), {LimitHeldRoutes, [RejectedRoute | RejectedRoutes]}. rollback_payment_limits(Routes, Iter, St, Flags) -> @@ -4043,7 +4045,8 @@ filter_attempted_routes_test_() -> ?_assertMatch( #{ routes := [R1, R2], - rejected_routes := [{_, _, {'AlreadyAttempted', undefined}}] + rejected_routes := [#{rejection_reason := {'AlreadyAttempted', undefined}}], + latest_rejected_group := already_attempted }, filter_attempted_routes( #{routes => [R1, R2, R3], rejected_routes => []}, @@ -4062,10 +4065,11 @@ filter_attempted_routes_test_() -> #{ routes := [], rejected_routes := [ - {_, _, {'AlreadyAttempted', undefined}}, - {_, _, {'AlreadyAttempted', undefined}}, - {_, _, {'AlreadyAttempted', undefined}} - ] + #{rejection_reason := {'AlreadyAttempted', undefined}}, + #{rejection_reason := {'AlreadyAttempted', undefined}}, + #{rejection_reason := {'AlreadyAttempted', undefined}} + ], + latest_rejected_group := already_attempted }, filter_attempted_routes( #{routes => [R1, R2, R3], rejected_routes => []}, diff --git a/apps/routing/src/hg_route.erl b/apps/routing/src/hg_route.erl index a1e27327..681f6508 100644 --- a/apps/routing/src/hg_route.erl +++ b/apps/routing/src/hg_route.erl @@ -17,7 +17,6 @@ -export([route_data/1]). -export([terminal_ref/1]). -export([provider_ref/1]). --export([payment_route/1]). -export([priority/1]). -export([weight/1]). -export([pin/1]). @@ -25,13 +24,15 @@ -export([fd_overrides/1]). -export([fd_score/1]). -export([blacklisted/1]). +-export([rejection_reason/1]). +-export([set_rejection_reason/2]). -export([score/1]). -export([equal/2]). -export([from_payment_route/1]). -export([to_payment_route/1]). --export([to_rejected_route/2]). +-export([to_rejected_route/1]). %% @@ -50,7 +51,8 @@ terminal_ref := dmsl_domain_thrift:'TerminalRef'(), route_data := route_data(), pin_data => pin_data(), - fd_overrides => fd_overrides() + fd_overrides => fd_overrides(), + rejection_reason => route_rejection_reason() | undefined }. -type fd_score() :: #{ @@ -129,16 +131,16 @@ set_fd_overrides(V, R) -> -spec set_prohibit(route_prohibit(), t()) -> t(). -set_prohibit(V, R = #{route_data := Data}) -> +set_prohibit(V, #{route_data := Data} = R) -> R#{route_data => Data#{prohibit => V}}. -spec set_accepted(route_accepted(), t()) -> t(). -set_accepted(V, R = #{route_data := Data}) -> +set_accepted(V, #{route_data := Data} = R) -> R#{route_data => Data#{accepted => V}}. -spec set_weight(integer(), t()) -> t(). -set_weight(Weight, R = #{route_data := Data}) -> +set_weight(Weight, #{route_data := Data} = R) -> R#{route_data => Data#{weight => Weight}}. -spec set_blacklisted(boolean() | blacklist_condition(), t()) -> @@ -147,22 +149,22 @@ set_blacklisted(true, R) -> set_blacklisted(1, R); set_blacklisted(false, R) -> set_blacklisted(0, R); -set_blacklisted(V, R = #{route_data := Data}) -> +set_blacklisted(V, #{route_data := Data} = R) -> R#{route_data => Data#{blacklisted => V}}. -spec set_availability(integer(), float(), t()) -> t(). -set_availability(C, V, R = #{route_data := Data = #{fd_score := Score}}) -> +set_availability(C, V, #{route_data := Data = #{fd_score := Score}} = R) -> R#{route_data => Data#{fd_score => Score#{availability_condition => C, availability => V}}}. -spec set_conversion(integer(), float(), t()) -> t(). -set_conversion(C, V, R = #{route_data := Data = #{fd_score := Score}}) -> +set_conversion(C, V, #{route_data := Data = #{fd_score := Score}} = R) -> R#{route_data => Data#{fd_score => Score#{conversion_condition => C, conversion => V}}}. -spec set_priority(integer(), t()) -> t(). -set_priority(V, R = #{route_data := Data}) -> +set_priority(V, #{route_data := Data} = R) -> R#{route_data => Data#{priority => V}}. -spec provider_ref(t()) -> provider_ref(). @@ -177,10 +179,6 @@ route_data(#{route_data := V}) -> terminal_ref(#{terminal_ref := Ref}) -> Ref. --spec payment_route(t()) -> payment_route(). -payment_route(#{payment_route := V}) -> - V. - -spec priority(t()) -> integer(). priority(#{route_data := #{priority := Priority}}) -> Priority. @@ -193,7 +191,7 @@ weight(#{route_data := #{weight := Weight}}) -> pin(#{pin_data := Pin}) -> Pin. --spec pin_hash(t()) -> pin_data() | undefined. +-spec pin_hash(t()) -> non_neg_integer(). pin_hash(#{pin_data := Pin}) when map_size(Pin) > 0 -> erlang:phash2(Pin); pin_hash(_) -> @@ -216,6 +214,18 @@ blacklisted(#{route_data := #{blacklisted := V}}) -> blacklisted(_) -> 0. +-spec rejection_reason(t()) -> + route_rejection_reason(). +rejection_reason(#{rejection_reason := V}) -> + V; +rejection_reason(_) -> + undefined. + +-spec set_rejection_reason(route_rejection_reason(), t()) -> + t(). +set_rejection_reason(Reason, R) -> + R#{rejection_reason => Reason}. + -spec score(t()) -> score(). score(R) -> #{ @@ -267,9 +277,9 @@ from_payment_route(Route) -> to_payment_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}. +-spec to_rejected_route(t()) -> rejected_route(). +to_rejected_route(Route) -> + {provider_ref(Route), terminal_ref(Route), rejection_reason(Route)}. %% diff --git a/apps/routing/src/hg_route_balancer.erl b/apps/routing/src/hg_route_balancer.erl index e4b243bb..c3aee3f9 100644 --- a/apps/routing/src/hg_route_balancer.erl +++ b/apps/routing/src/hg_route_balancer.erl @@ -82,37 +82,34 @@ calc_random_condition(StartFrom, Random, [Route | Rest], Routes) -> -define(prv(ID), #domain_ProviderRef{id = ID}). -define(trm(ID), #domain_TerminalRef{id = ID}). +balanced_test_route(ProviderId, Weight) -> + hg_route:new(1, ?prv(ProviderId), ?trm(1), Weight, ?DOMAIN_CANDIDATE_PRIORITY, #{}). + -spec balance_routes_test_() -> [testcase()]. balance_routes_test_() -> WithWeight = [ - hg_route:new(1, ?prv(1), ?trm(1), 1, ?DOMAIN_CANDIDATE_PRIORITY, #{}), - hg_route:new(1, ?prv(2), ?trm(1), 2, ?DOMAIN_CANDIDATE_PRIORITY, #{}), - hg_route:new(1, ?prv(3), ?trm(1), 0, ?DOMAIN_CANDIDATE_PRIORITY, #{}), - hg_route:new(1, ?prv(4), ?trm(1), 1, ?DOMAIN_CANDIDATE_PRIORITY, #{}), - hg_route:new(1, ?prv(5), ?trm(1), 0, ?DOMAIN_CANDIDATE_PRIORITY, #{}) + balanced_test_route(1, 1), + balanced_test_route(2, 2), + balanced_test_route(3, 0), + balanced_test_route(4, 1), + balanced_test_route(5, 0) ], Result1 = [ - hg_route:new(1, ?prv(1), ?trm(1), 1, ?DOMAIN_CANDIDATE_PRIORITY, #{}), - hg_route:new(1, ?prv(2), ?trm(1), 0, ?DOMAIN_CANDIDATE_PRIORITY, #{}), - hg_route:new(1, ?prv(3), ?trm(1), 0, ?DOMAIN_CANDIDATE_PRIORITY, #{}), - hg_route:new(1, ?prv(4), ?trm(1), 0, ?DOMAIN_CANDIDATE_PRIORITY, #{}), - hg_route:new(1, ?prv(5), ?trm(1), 0, ?DOMAIN_CANDIDATE_PRIORITY, #{}) + balanced_test_route(1, 1), + balanced_test_route(2, 0), + balanced_test_route(3, 0), + balanced_test_route(4, 0), + balanced_test_route(5, 0) ], Result2 = [ - hg_route:new(1, ?prv(1), ?trm(1), 0, ?DOMAIN_CANDIDATE_PRIORITY, #{}), - hg_route:new(1, ?prv(2), ?trm(1), 1, ?DOMAIN_CANDIDATE_PRIORITY, #{}), - hg_route:new(1, ?prv(3), ?trm(1), 0, ?DOMAIN_CANDIDATE_PRIORITY, #{}), - hg_route:new(1, ?prv(4), ?trm(1), 0, ?DOMAIN_CANDIDATE_PRIORITY, #{}), - hg_route:new(1, ?prv(5), ?trm(1), 0, ?DOMAIN_CANDIDATE_PRIORITY, #{}) - ], - Result3 = [ - hg_route:new(1, ?prv(1), ?trm(1), 0, ?DOMAIN_CANDIDATE_PRIORITY, #{}), - hg_route:new(1, ?prv(2), ?trm(1), 0, ?DOMAIN_CANDIDATE_PRIORITY, #{}), - hg_route:new(1, ?prv(3), ?trm(1), 0, ?DOMAIN_CANDIDATE_PRIORITY, #{}), - hg_route:new(1, ?prv(4), ?trm(1), 0, ?DOMAIN_CANDIDATE_PRIORITY, #{}), - hg_route:new(1, ?prv(5), ?trm(1), 0, ?DOMAIN_CANDIDATE_PRIORITY, #{}) + balanced_test_route(1, 0), + balanced_test_route(2, 1), + balanced_test_route(3, 0), + balanced_test_route(4, 0), + balanced_test_route(5, 0) ], + Result3 = [balanced_test_route(Prv, 0) || Prv <- lists:seq(1, 5)], [ ?_assertEqual(Result1, lists:reverse(calc_random_condition(0.0, 0.2, WithWeight, []))), ?_assertEqual(Result2, lists:reverse(calc_random_condition(0.0, 1.5, WithWeight, []))), diff --git a/apps/routing/src/hg_route_collector.erl b/apps/routing/src/hg_route_collector.erl index 46668acc..ca528ada 100644 --- a/apps/routing/src/hg_route_collector.erl +++ b/apps/routing/src/hg_route_collector.erl @@ -34,11 +34,13 @@ card_token => card_token() | undefined }. --type get_route_resut() :: #{ +-type get_routes_resut() :: #{ routes := [hg_route:t()], - error => {misconfiguration, _Reason} + error => get_routes_error() }. +-type get_routes_error() :: {misconfiguration, _Reason}. + -type blacklist_context() :: hg_inspector:blacklist_context(). -export_type([payment_institution/0]). @@ -47,7 +49,7 @@ -export_type([revision/0]). -export_type([blacklist_context/0]). -export_type([gather_route_context/0]). --export_type([get_route_resut/0]). +-export_type([get_routes_error/0]). -define(fd_overrides(Enabled), #domain_RouteFaultDetectorOverrides{enabled = Enabled}). -define(rejected(Reason), {rejected, Reason}). @@ -72,16 +74,11 @@ fill_fd_overrides(Revision, 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(PrvFdOverrides, TrmFdOverrides). merge_fd_overrides(_A, B = ?fd_overrides(Enabled)) when Enabled =/= undefined -> B; @@ -143,7 +140,7 @@ fill_accepted(Predestination, Revision, VS, Routes) -> ). -spec get_routes(revision(), varset(), payment_institution(), gather_route_context()) -> - get_route_resut(). + get_routes_resut(). get_routes(_, _, #domain_PaymentInstitution{payment_routing_rules = undefined}, _) -> #{routes => [], error => {misconfiguration, {payment_routing_rules, empty}}}; get_routes(Revision, VS, #domain_PaymentInstitution{payment_routing_rules = RoutingRules}, Ctx) -> @@ -173,7 +170,7 @@ get_decisions_candidates(#domain_RoutingRuleset{decisions = Decisions}) -> end. compute_rule_set(RuleSetRef, VS, Revision) -> - {Client, Context} = get_party_client(), + {Client, Context} = get_party_client(), {ok, RuleSet} = party_client_thrift:compute_routing_ruleset( RuleSetRef, Revision, @@ -412,4 +409,4 @@ merge_fd_overrides_test_() -> ?_assertEqual(?fd_overrides(false), merge_fd_overrides(?fd_overrides(true), ?fd_overrides(false))) ]. --endif. \ No newline at end of file +-endif. diff --git a/apps/routing/src/hg_routing.erl b/apps/routing/src/hg_routing.erl index a7e31f3c..695e6eaa 100644 --- a/apps/routing/src/hg_routing.erl +++ b/apps/routing/src/hg_routing.erl @@ -30,11 +30,17 @@ -type rejection_group() :: atom(). -type filter_routes_fun() :: fun((filter_routes_result()) -> filter_routes_result()). +-type get_routes_result() :: #{ + routes := [hg_route:t()], + rejected_routes => [hg_route:t()], + error => hg_route_collector:get_routes_error() +}. + -type filter_routes_result() :: #{ routes := [hg_route:t()], - rejected_routes => [hg_route:rejected_route()], + rejected_routes => [hg_route:t()], latest_rejected_group => rejection_group() | undefined, - rejection_groups => #{rejection_group() => [hg_route:rejected_route()]}, + rejection_groups => #{rejection_group() => [hg_route:t()]}, considered_routes => [hg_route:t()], route_limits => limits(), route_scores => scores() @@ -48,6 +54,7 @@ -type misconfiguration_error() :: {misconfiguration, {routing_decisions, _} | {routing_candidate, _}}. -export_type([get_route_params/0]). +-export_type([get_routes_result/0]). -export_type([filter_routes_result/0]). -export_type([route_scores/0]). -export_type([limits/0]). @@ -61,7 +68,7 @@ prepare_log_message({misconfiguration, {routing_decisions, Details}}) -> {"PaymentRoutingDecisions couldn't be reduced to candidates, ~p", [Details]}; prepare_log_message({misconfiguration, {routing_candidate, Candidate}}) -> {"PaymentRoutingCandidate couldn't be reduced, ~p", [Candidate]}; -prepare_log_message({misconfiguration, {payment_routing_rules, empty} }) -> +prepare_log_message({misconfiguration, {payment_routing_rules, empty}}) -> {"PaymentRoutingRules are empty", []}. -spec get_logger_metadata(route_choice_context(), hg_route_collector:revision()) -> LoggerFormattedMetadata :: map(). @@ -91,45 +98,39 @@ format_logger_metadata(Meta, Route, Revision) when weight => hg_route:weight(Route) }). --spec get_routes(get_route_params()) -> hg_route_collector:get_route_resut(). -get_routes(Params = #{ - predestination := Predestination, - revision := Revision, - varset := VS, - payment_institution := PI, - pin_context := PinCtx -}) -> +-spec get_routes(get_route_params()) -> get_routes_result(). +get_routes( + #{ + predestination := Predestination, + revision := Revision, + varset := VS, + payment_institution := PI, + pin_context := PinCtx + } = Params +) -> Result = #{routes := Routes0} = hg_route_collector:get_routes(Revision, VS, PI, PinCtx), Routes1 = hg_route_collector:fill_accepted(Predestination, Revision, VS, Routes0), Routes2 = hg_route_collector:fill_prohibition(Revision, VS, PI, Routes1), Routes3 = hg_route_collector:fill_fd_overrides(Revision, Routes2), - Routes4 = case maps:get(blacklist_context, Params, undefined) of - undefined -> - Routes3; - BlCtx -> - hg_route_collector:fill_blacklist(BlCtx, Routes3) - end, + Routes4 = + case maps:get(blacklist_context, Params, undefined) of + undefined -> + Routes3; + BlCtx -> + hg_route_collector:fill_blacklist(BlCtx, Routes3) + end, Routes5 = hg_route_fd:fill(Routes4), - Result#{routes => hg_route_balancer:fill(Routes5)}. + genlib_map:compact( + maps:merge( + #{error => maps:get(error, Result, undefined)}, + filter(hg_route_balancer:fill(Routes5), [{accepted, false}, {prohibit, true}, {blacklisted, 1}]) + ) + ). --spec filter_routes([hg_route:t()], [filter_routes_fun()]) -> filter_routes_result(). -filter_routes(Routes0, WithFilterFuns) -> - Result0 = init_filter_result(filter(Routes0, [{accepted, false}, {prohibit, true}, {blacklisted, 1}])), +-spec filter_routes(filter_routes_result(), [filter_routes_fun()]) -> filter_routes_result(). +filter_routes(Result0, WithFilterFuns) -> lists:foldl(fun(Fun, Result) -> Fun(Result) end, Result0, WithFilterFuns). -init_filter_result(#{rejected_routes := []} = Result) -> - Result#{ - latest_rejected_group => undefined, - rejection_groups => #{} - }; -init_filter_result(#{rejected_routes := RejectedRoutes} = Result) -> - Result#{ - latest_rejected_group => forbidden, - rejection_groups => #{ - forbidden => RejectedRoutes - } - }. - filter(Routes, Keys) -> lists:foldr( fun(Route, #{routes := Accepted, rejected_routes := Rejected} = Acc) -> @@ -137,7 +138,7 @@ filter(Routes, Keys) -> undefined -> Acc#{routes => [Route | Accepted]}; Reason -> - Acc#{rejected_routes => [{Route, Reason} | Rejected]} + Acc#{rejected_routes => [hg_route:set_rejection_reason(Reason, Route) | Rejected]} end end, #{routes => [], rejected_routes => []}, @@ -423,19 +424,20 @@ filter_routes_splits_accepted_and_rejected_test() -> 1, new_route(1, 4, 0, {1, 1.0}, {1, 1.0}) ), - - ?assertEqual( - #{ - routes => [AcceptedRoute], - rejected_routes => [ - {RejectedByTerms, {rejected, {'ProvisionTermSet', undefined}}}, - {RejectedByProhibition, <<"blocked">>}, - {RejectedByBlacklist, blacklisted} - ] - }, - filter_routes( + Rejected = [ + hg_route:set_rejection_reason({accepted, {rejected, {'ProvisionTermSet', undefined}}}, RejectedByTerms), + hg_route:set_rejection_reason({prohibit, <<"blocked">>}, RejectedByProhibition), + hg_route:set_rejection_reason({blacklisted, 1}, RejectedByBlacklist) + ], + Result = #{ + routes => [AcceptedRoute], + rejected_routes => Rejected + }, + ?assertMatch( + Result, + filter( [AcceptedRoute, RejectedByTerms, RejectedByProhibition, RejectedByBlacklist], - [] + [{accepted, false}, {prohibit, true}, {blacklisted, 1}] ) ). @@ -461,40 +463,52 @@ preferable_route_scoring_test_() -> ]) ), ?_assertMatch( - {?trm(99), {_, #{ - preferable_route := #{terminal_ref := ?trm(1)}, - reject_reason := availability_condition - }}}, + { + ?trm(99), + {_, #{ + preferable_route := #{terminal_ref := ?trm(1)}, + reject_reason := availability_condition + }} + }, balance_and_choose_route([ new_route(1, 1, 0, {0, 0.6}, {0, 0.4}), RouteFallback2 ]) ), ?_assertMatch( - {?trm(99), {_, #{ - preferable_route := #{terminal_ref := ?trm(1)}, - reject_reason := conversion_condition - }}}, + { + ?trm(99), + {_, #{ + preferable_route := #{terminal_ref := ?trm(1)}, + reject_reason := conversion_condition + }} + }, balance_and_choose_route([ new_route(1, 1, 0, {1, 0.9}, {0, 0.2}), RouteFallback2 ]) ), ?_assertMatch( - {?trm(1), {_, #{ - preferable_route := #{terminal_ref := ?trm(2)}, - reject_reason := conversion - }}}, + { + ?trm(1), + {_, #{ + preferable_route := #{terminal_ref := ?trm(2)}, + reject_reason := conversion + }} + }, balance_and_choose_route([ new_route(1, 2, 0, {1, 1.0}, {1, 0.9}), new_route(1, 1, 0, {1, 1.0}, {1, 1.0}) ]) ), ?_assertMatch( - {?trm(1), {_, #{ - preferable_route := #{terminal_ref := ?trm(2)}, - reject_reason := availability - }}}, + { + ?trm(1), + {_, #{ + preferable_route := #{terminal_ref := ?trm(2)}, + reject_reason := availability + }} + }, balance_and_choose_route([ new_route(1, 2, 0, {1, 0.9}, {1, 0.9}), new_route(1, 1, 0, {1, 1.0}, {1, 1.0}), From f4c4dc797172d30724e36dec27f8c45b906a5b06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC?= Date: Thu, 26 Mar 2026 18:27:48 +0300 Subject: [PATCH 3/4] fixed --- .../test/hg_route_rules_tests_SUITE.erl | 238 +++++++++++------- apps/routing/src/hg_route_collector.erl | 14 +- apps/routing/src/hg_routing.erl | 2 +- 3 files changed, 166 insertions(+), 88 deletions(-) diff --git a/apps/hellgate/test/hg_route_rules_tests_SUITE.erl b/apps/hellgate/test/hg_route_rules_tests_SUITE.erl index 0cdc3c75..fbc3d667 100644 --- a/apps/hellgate/test/hg_route_rules_tests_SUITE.erl +++ b/apps/hellgate/test/hg_route_rules_tests_SUITE.erl @@ -512,19 +512,19 @@ no_route_found_for_payment(_C) -> payment_tool => PaymentTool, client_ip => undefined }, - {ok, {[], RejectedRoutes1}} = unwrap_routing_context( - hg_routing:gather_routes(payment, PaymentInstitution, VS, Revision, Ctx0) + #{routes := [], rejected_routes := Rejected1} = get_routes( + payment, PaymentInstitution, VS, Revision, Ctx0 ), ?assert_set_equal( [ - {?prv(1), ?trm(1), {'PaymentsProvisionTerms', cost}}, - {?prv(2), ?trm(2), {'PaymentsProvisionTerms', category}}, - {?prv(3), ?trm(3), {'PaymentsProvisionTerms', payment_tool}}, - {?prv(4), ?trm(4), {'PaymentsProvisionTerms', allow}}, - {?prv(7), ?trm(7), {'PaymentsProvisionTerms', global_allow}} + {?prv(1), ?trm(1), {rejected, {'PaymentsProvisionTerms', cost}}}, + {?prv(2), ?trm(2), {rejected, {'PaymentsProvisionTerms', category}}}, + {?prv(3), ?trm(3), {rejected, {'PaymentsProvisionTerms', payment_tool}}}, + {?prv(4), ?trm(4), {rejected, {'PaymentsProvisionTerms', allow}}}, + {?prv(7), ?trm(7), {rejected, {'PaymentsProvisionTerms', global_allow}}} ], - RejectedRoutes1 + to_rejected_routes(Rejected1) ), Currency1 = ?cur(<<"EUR">>), @@ -535,18 +535,18 @@ no_route_found_for_payment(_C) -> Ctx1 = Ctx0#{ currency => Currency1 }, - {ok, {[], RejectedRoutes2}} = unwrap_routing_context( - hg_routing:gather_routes(payment, PaymentInstitution, VS1, Revision, Ctx1) + #{routes := [], rejected_routes := Rejected2} = get_routes( + payment, PaymentInstitution, VS1, Revision, Ctx1 ), ?assert_set_equal( [ - {?prv(1), ?trm(1), {'PaymentsProvisionTerms', currency}}, - {?prv(2), ?trm(2), {'PaymentsProvisionTerms', category}}, - {?prv(3), ?trm(3), {'PaymentsProvisionTerms', payment_tool}}, - {?prv(4), ?trm(4), {'PaymentsProvisionTerms', allow}}, - {?prv(7), ?trm(7), {'PaymentsProvisionTerms', global_allow}} + {?prv(1), ?trm(1), {rejected, {'PaymentsProvisionTerms', currency}}}, + {?prv(2), ?trm(2), {rejected, {'PaymentsProvisionTerms', category}}}, + {?prv(3), ?trm(3), {rejected, {'PaymentsProvisionTerms', payment_tool}}}, + {?prv(4), ?trm(4), {rejected, {'PaymentsProvisionTerms', allow}}}, + {?prv(7), ?trm(7), {rejected, {'PaymentsProvisionTerms', global_allow}}} ], - RejectedRoutes2 + to_rejected_routes(Rejected2) ). -spec gather_route_success(config()) -> test_return(). @@ -570,18 +570,18 @@ gather_route_success(_C) -> payment_tool => PaymentTool, client_ip => undefined }, - {ok, {[Route], RejectedRoutes}} = unwrap_routing_context( - hg_routing:gather_routes(payment, PaymentInstitution, VS, Revision, Ctx) + #{routes := [Route], rejected_routes := RejectedRoutes} = get_routes( + payment, PaymentInstitution, VS, Revision, Ctx ), ?assertMatch(?trm(1), hg_route:terminal_ref(Route)), - ?assertMatch( + ?assert_set_equal( [ - {?prv(2), ?trm(2), {'PaymentsProvisionTerms', category}}, - {?prv(3), ?trm(3), {'PaymentsProvisionTerms', payment_tool}}, - {?prv(4), ?trm(4), {'PaymentsProvisionTerms', allow}}, - {?prv(7), ?trm(7), {'PaymentsProvisionTerms', global_allow}} + {?prv(2), ?trm(2), {rejected, {'PaymentsProvisionTerms', category}}}, + {?prv(3), ?trm(3), {rejected, {'PaymentsProvisionTerms', payment_tool}}}, + {?prv(4), ?trm(4), {rejected, {'PaymentsProvisionTerms', allow}}}, + {?prv(7), ?trm(7), {rejected, {'PaymentsProvisionTerms', global_allow}}} ], - RejectedRoutes + to_rejected_routes(RejectedRoutes) ). -spec rejected_by_table_prohibitions(config()) -> test_return(). @@ -612,18 +612,18 @@ rejected_by_table_prohibitions(_C) -> payment_tool => PaymentTool, client_ip => undefined }, - {ok, {[], RejectedRoutes}} = unwrap_routing_context( - hg_routing:gather_routes(payment, PaymentInstitution, VS, Revision, Ctx) + #{routes := [], rejected_routes := RejectedRoutes} = get_routes( + payment, PaymentInstitution, VS, Revision, Ctx ), ?assert_set_equal( [ {?prv(3), ?trm(3), {'RoutingRule', undefined}}, - {?prv(1), ?trm(1), {'PaymentsProvisionTerms', payment_tool}}, - {?prv(2), ?trm(2), {'PaymentsProvisionTerms', category}}, - {?prv(4), ?trm(4), {'PaymentsProvisionTerms', allow}}, - {?prv(7), ?trm(7), {'PaymentsProvisionTerms', global_allow}} + {?prv(1), ?trm(1), {rejected, {'PaymentsProvisionTerms', payment_tool}}}, + {?prv(2), ?trm(2), {rejected, {'PaymentsProvisionTerms', category}}}, + {?prv(4), ?trm(4), {rejected, {'PaymentsProvisionTerms', allow}}}, + {?prv(7), ?trm(7), {rejected, {'PaymentsProvisionTerms', global_allow}}} ], - RejectedRoutes + to_rejected_routes(RejectedRoutes) ), ok. @@ -654,8 +654,8 @@ empty_candidate_ok(_C) -> client_ip => undefined }, ?assertMatch( - {ok, {[], []}}, - unwrap_routing_context(hg_routing:gather_routes(payment, PaymentInstitution, VS, Revision, Ctx)) + #{routes := [], rejected_routes := []}, + get_routes(payment, PaymentInstitution, VS, Revision, Ctx) ). -spec ruleset_misconfig(config()) -> test_return(). @@ -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)) + route_error(get_routes(payment, PaymentInstitution, VS, Revision, Ctx)) ). -spec routes_selected_for_low_risk_score(config()) -> test_return(). @@ -705,27 +705,29 @@ routes_selected_with_risk_score(_C, RiskScore, ProviderRefs) -> payment_tool => PaymentTool, client_ip => undefined }, - {ok, {Routes, _}} = unwrap_routing_context( - hg_routing:gather_routes(payment, PaymentInstitution, VS, Revision, Ctx) + #{routes := Routes, rejected_routes := _} = get_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_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, - Result = {_, Context} = hg_routing:choose_route(Routes), + Route1 = set_route_fd_score(new_route(Revision, ?prv(1), ?trm(1)), {0, 0.1}, {1, 1.0}), + Route2 = set_route_fd_score(new_route(Revision, ?prv(2), ?trm(2)), {1, 0.9}, {1, 1.0}), + Route3 = set_route_fd_score( + new_route(Revision, ?prv(3), ?trm(3), 0, ?DOMAIN_CANDIDATE_PRIORITY), + {0, 0.8}, + {1, 1.0} + ), + Result = {_, Context} = hg_routing:choose_route([Route1, Route2, Route3]), ?assertMatch( - {Route2, #{reject_reason := availability, preferable_route := Route3}}, + {Route2, #{reject_reason := availability_condition, preferable_route := Route3}}, Result ), ?assertMatch( #{ - reject_reason := availability, + reject_reason := availability_condition, chosen_route := #{ provider := #{id := 2, name := <<_/binary>>}, terminal := #{id := 2, name := <<_/binary>>}, @@ -748,8 +750,8 @@ empty_terms_allow_test(_C) -> -spec not_reduced_terms_allow_test(config()) -> test_return(). not_reduced_terms_allow_test(_C) -> - Error = {'Misconfiguration', {'Could not reduce predicate to a value', {allow, {all_of, [{constant, false}]}}}}, - do_gather_routes(?not_reduced_allow_revision, undefined, [{?prv(6), ?trm(6), Error}]). + Error = {'Could not reduce predicate to a value', {allow, {all_of, [{constant, false}]}}}, + do_gather_routes(?not_reduced_allow_revision, undefined, [{?prv(6), ?trm(6), {misconfiguration, Error}}]). do_gather_routes(Revision, ExpectedRouteTerminal, ExpectedRejectedRoutes) -> Currency = ?cur(<<"RUB">>), @@ -770,8 +772,8 @@ do_gather_routes(Revision, ExpectedRouteTerminal, ExpectedRejectedRoutes) -> payment_tool => PaymentTool, client_ip => undefined }, - {ok, {Routes, RejectedRoutes}} = unwrap_routing_context( - hg_routing:gather_routes(payment, PaymentInstitution, VS, Revision, Ctx) + #{routes := Routes, rejected_routes := RejectedRoutes} = get_routes( + payment, PaymentInstitution, VS, Revision, Ctx ), case ExpectedRouteTerminal of undefined -> @@ -780,16 +782,16 @@ do_gather_routes(Revision, ExpectedRouteTerminal, ExpectedRejectedRoutes) -> [Route] = Routes, ?assertMatch(Terminal, hg_route:terminal_ref(Route)) end, - ?assertMatch(ExpectedRejectedRoutes, RejectedRoutes). + ?assertMatch(ExpectedRejectedRoutes, to_rejected_routes(RejectedRoutes)). %%% Terminal priority tests -spec terminal_priority_for_shop(config()) -> test_return(). 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)). + {Route1, _} = terminal_priority_for_shop(?shop_id_for_ruleset_w_priority_distribution_1, C), + {Route2, _} = terminal_priority_for_shop(?shop_id_for_ruleset_w_priority_distribution_2, C), + ?assertMatch(?trm(11), hg_route:terminal_ref(Route1)), + ?assertMatch(?trm(12), hg_route:terminal_ref(Route2)). terminal_priority_for_shop(ShopID, _C) -> Currency = ?cur(<<"RUB">>), @@ -810,8 +812,8 @@ terminal_priority_for_shop(ShopID, _C) -> payment_tool => PaymentTool, client_ip => undefined }, - {ok, {Routes, _RejectedRoutes}} = unwrap_routing_context( - hg_routing:gather_routes(payment, PaymentInstitution, VS, Revision, Ctx) + #{routes := Routes, rejected_routes := _RejectedRoutes} = get_routes( + payment, PaymentInstitution, VS, Revision, Ctx ), hg_routing:choose_route(Routes). @@ -836,8 +838,8 @@ gather_pinned_route(_C) -> card_token => undefined, email => undefined }, - {ok, {Routes, _RejectedRoutes}} = unwrap_routing_context( - hg_routing:gather_routes(payment, PaymentInstitution, VS, Revision, Ctx) + #{routes := Routes, rejected_routes := _RejectedRoutes} = get_routes( + payment, PaymentInstitution, VS, Revision, Ctx ), Pin = #{ currency => Currency, @@ -845,32 +847,69 @@ gather_pinned_route(_C) -> }, ?assert_set_equal( [ - hg_route:new(?prv(1), ?trm(1), 0, 0, Ctx, ?fd_overrides(undefined)), - hg_route:new(?prv(2), ?trm(2), 0, 0, Pin, ?fd_overrides(true)), - hg_route:new(?prv(3), ?trm(3), 0, 0, Pin, ?fd_overrides(false)) + {?trm(1), Ctx, ?fd_overrides(undefined), #{ + availability_condition => 0, + availability => fd_value(0.9), + conversion_condition => 0, + conversion => fd_value(0.9) + }}, + {?trm(2), Pin, ?fd_overrides(true), #{ + availability_condition => 1, + availability => 1.0, + conversion_condition => 1, + conversion => 1.0 + }}, + {?trm(3), Pin, ?fd_overrides(false), #{ + availability_condition => 1, + availability => 0.8, + conversion_condition => 1, + conversion => 0.8 + }} ], - Routes + [ + {hg_route:terminal_ref(Route), hg_route:pin(Route), hg_route:fd_overrides(Route), hg_route:fd_score(Route)} + || Route <- Routes + ] ). -spec choose_route_w_override(config()) -> test_return(). choose_route_w_override(_C) -> - %% without overrides - 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, + Revision = ?routing_with_fail_rate_domain_revision, + Routes0 = [ + new_route(Revision, ?prv(1), ?trm(1)), + new_route(Revision, ?prv(2), ?trm(2)), + new_route(Revision, ?prv(3), ?trm(3)) + ], + [Route1, Route2, Route3] = hg_route_fd:fill(hg_route_collector:fill_fd_overrides(Revision, Routes0)), + ?assertEqual( #{ - preferable_route := Route3, - reject_reason := availability - } - } = hg_routing:choose_route(Routes), - - %% 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). + availability_condition => 0, + availability => fd_value(0.9), + conversion_condition => 0, + conversion => fd_value(0.9) + }, + hg_route:fd_score(Route1) + ), + ?assertEqual( + #{ + availability_condition => 1, + availability => 1.0, + conversion_condition => 1, + conversion => 1.0 + }, + hg_route:fd_score(Route2) + ), + ?assertEqual( + #{ + availability_condition => 1, + availability => 0.8, + conversion_condition => 1, + conversion => 0.8 + }, + hg_route:fd_score(Route3) + ), + {ChosenRoute, _} = hg_routing:choose_route([Route1, Route2, Route3]), + ?assertMatch(?trm(2), hg_route:terminal_ref(ChosenRoute)). -spec recurrent_payment_skip_recurrent_terms(config()) -> test_return(). recurrent_payment_skip_recurrent_terms(_C) -> @@ -894,8 +933,8 @@ recurrent_payment_skip_recurrent_terms(_C) -> payment_tool => PaymentTool, client_ip => undefined }, - {ok, {Routes, _RejectedRoutes}} = unwrap_routing_context( - hg_routing:gather_routes(recurrent_payment, PaymentInstitution, VS, Revision, Ctx) + #{routes := Routes, rejected_routes := _RejectedRoutes} = get_routes( + recurrent_payment, PaymentInstitution, VS, Revision, Ctx ), ?assertEqual(1, length(Routes)), [Route] = Routes, @@ -923,11 +962,14 @@ recurrent_payment_rejected_without_terms(_C) -> payment_tool => PaymentTool, client_ip => undefined }, - {ok, {Routes, RejectedRoutes}} = unwrap_routing_context( - hg_routing:gather_routes(recurrent_payment, PaymentInstitution, VS, Revision, Ctx) + #{routes := Routes, rejected_routes := RejectedRoutes} = get_routes( + recurrent_payment, PaymentInstitution, VS, Revision, Ctx ), ?assertEqual([], Routes), - ?assertMatch([{?prv(9), ?trm(9), {'RecurrentPaytoolsProvisionTerms', undefined}}], RejectedRoutes). + ?assertEqual( + [{?prv(9), ?trm(9), {rejected, {'RecurrentPaytoolsProvisionTerms', undefined}}}], + to_rejected_routes(RejectedRoutes) + ). %%% Domain config fixtures @@ -1015,5 +1057,33 @@ maybe_set_risk_coverage(false, _) -> 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)}}. +to_rejected_routes(RejectedRoutes) -> + [hg_route:to_rejected_route(R) || R <- RejectedRoutes]. + +get_routes(Predestination, PaymentInstitution, VS, Revision, Ctx) -> + hg_routing:get_routes(#{ + predestination => Predestination, + revision => Revision, + varset => VS, + payment_institution => PaymentInstitution, + pin_context => Ctx + }). + +route_error(RoutingCtx) -> + maps:get(error, RoutingCtx, undefined). + +fd_value(FailureRate) -> + 1.0 - FailureRate. + +new_route(Revision, ProviderRef, TerminalRef) -> + new_route(Revision, ProviderRef, TerminalRef, 0, ?DOMAIN_CANDIDATE_PRIORITY). + +new_route(Revision, ProviderRef, TerminalRef, Weight, Priority) -> + new_route(Revision, ProviderRef, TerminalRef, Weight, Priority, #{}). + +new_route(Revision, ProviderRef, TerminalRef, Weight, Priority, Pin) -> + hg_route:new(Revision, ProviderRef, TerminalRef, Weight, Priority, Pin). + +set_route_fd_score(Route0, {AvailabilityCondition, Availability}, {ConversionCondition, Conversion}) -> + Route1 = hg_route:set_availability(AvailabilityCondition, Availability, Route0), + hg_route:set_conversion(ConversionCondition, Conversion, Route1). diff --git a/apps/routing/src/hg_route_collector.erl b/apps/routing/src/hg_route_collector.erl index ca528ada..63e6d767 100644 --- a/apps/routing/src/hg_route_collector.erl +++ b/apps/routing/src/hg_route_collector.erl @@ -89,6 +89,8 @@ merge_fd_overrides(_A, _B) -> -spec fill_prohibition(revision(), varset(), payment_institution(), [hg_route:t()]) -> [hg_route:t()]. +fill_prohibition(_Revision, _VS, #domain_PaymentInstitution{payment_routing_rules = undefined}, Routes) -> + Routes; fill_prohibition(Revision, VS, #domain_PaymentInstitution{payment_routing_rules = RoutingRules}, Routes) -> #domain_RoutingRules{ prohibitions = Prohibitions @@ -101,7 +103,7 @@ fill_prohibition(Revision, VS, #domain_PaymentInstitution{payment_routing_rules error -> [Route | AccIn]; {ok, Description} -> - [hg_route:set_prohibit({true, Description}, Route) | AccIn] + [hg_route:set_prohibit({true, {'RoutingRule', Description}}, Route) | AccIn] end end, [], @@ -254,9 +256,15 @@ check_terms_acceptability(payment, Terms, VS) -> check_terms_acceptability(recurrent_paytool, Terms, VS) -> acceptable_recurrent_paytool_terms(Terms#domain_ProvisionTermSet.recurrent_paytools, VS); check_terms_acceptability(recurrent_payment, Terms, VS) -> - % Use provider check combined from recurrent_paytool and payment check + % Recurrent payment may be accepted either by recurrent terms + % or via the skip_recurrent extension flag. _ = acceptable_payment_terms(Terms#domain_ProvisionTermSet.payments, VS), - acceptable_recurrent_paytool_terms(Terms#domain_ProvisionTermSet.recurrent_paytools, 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{ diff --git a/apps/routing/src/hg_routing.erl b/apps/routing/src/hg_routing.erl index 695e6eaa..0f471cb7 100644 --- a/apps/routing/src/hg_routing.erl +++ b/apps/routing/src/hg_routing.erl @@ -137,7 +137,7 @@ filter(Routes, Keys) -> case route_rejection_reason(Route, Keys) of undefined -> Acc#{routes => [Route | Accepted]}; - Reason -> + {_Key, Reason} -> Acc#{rejected_routes => [hg_route:set_rejection_reason(Reason, Route) | Rejected]} end end, From 402f7caa8c11b7982c0bbf4edb78d2fbd09d7195 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC?= Date: Thu, 26 Mar 2026 20:08:46 +0300 Subject: [PATCH 4/4] fixed --- .../test/hg_route_rules_tests_SUITE.erl | 46 ++++++++++--------- apps/routing/src/hg_routing.erl | 10 ++-- 2 files changed, 31 insertions(+), 25 deletions(-) diff --git a/apps/hellgate/test/hg_route_rules_tests_SUITE.erl b/apps/hellgate/test/hg_route_rules_tests_SUITE.erl index fbc3d667..ff58b289 100644 --- a/apps/hellgate/test/hg_route_rules_tests_SUITE.erl +++ b/apps/hellgate/test/hg_route_rules_tests_SUITE.erl @@ -518,11 +518,11 @@ no_route_found_for_payment(_C) -> ?assert_set_equal( [ - {?prv(1), ?trm(1), {rejected, {'PaymentsProvisionTerms', cost}}}, - {?prv(2), ?trm(2), {rejected, {'PaymentsProvisionTerms', category}}}, - {?prv(3), ?trm(3), {rejected, {'PaymentsProvisionTerms', payment_tool}}}, - {?prv(4), ?trm(4), {rejected, {'PaymentsProvisionTerms', allow}}}, - {?prv(7), ?trm(7), {rejected, {'PaymentsProvisionTerms', global_allow}}} + {?prv(1), ?trm(1), {accepted, {false, {rejected, {'PaymentsProvisionTerms', cost}}}}}, + {?prv(2), ?trm(2), {accepted, {false, {rejected, {'PaymentsProvisionTerms', category}}}}}, + {?prv(3), ?trm(3), {accepted, {false, {rejected, {'PaymentsProvisionTerms', payment_tool}}}}}, + {?prv(4), ?trm(4), {accepted, {false, {rejected, {'PaymentsProvisionTerms', allow}}}}}, + {?prv(7), ?trm(7), {accepted, {false, {rejected, {'PaymentsProvisionTerms', global_allow}}}}} ], to_rejected_routes(Rejected1) ), @@ -540,11 +540,11 @@ no_route_found_for_payment(_C) -> ), ?assert_set_equal( [ - {?prv(1), ?trm(1), {rejected, {'PaymentsProvisionTerms', currency}}}, - {?prv(2), ?trm(2), {rejected, {'PaymentsProvisionTerms', category}}}, - {?prv(3), ?trm(3), {rejected, {'PaymentsProvisionTerms', payment_tool}}}, - {?prv(4), ?trm(4), {rejected, {'PaymentsProvisionTerms', allow}}}, - {?prv(7), ?trm(7), {rejected, {'PaymentsProvisionTerms', global_allow}}} + {?prv(1), ?trm(1), {accepted, {false, {rejected, {'PaymentsProvisionTerms', currency}}}}}, + {?prv(2), ?trm(2), {accepted, {false, {rejected, {'PaymentsProvisionTerms', category}}}}}, + {?prv(3), ?trm(3), {accepted, {false, {rejected, {'PaymentsProvisionTerms', payment_tool}}}}}, + {?prv(4), ?trm(4), {accepted, {false, {rejected, {'PaymentsProvisionTerms', allow}}}}}, + {?prv(7), ?trm(7), {accepted, {false, {rejected, {'PaymentsProvisionTerms', global_allow}}}}} ], to_rejected_routes(Rejected2) ). @@ -576,10 +576,10 @@ gather_route_success(_C) -> ?assertMatch(?trm(1), hg_route:terminal_ref(Route)), ?assert_set_equal( [ - {?prv(2), ?trm(2), {rejected, {'PaymentsProvisionTerms', category}}}, - {?prv(3), ?trm(3), {rejected, {'PaymentsProvisionTerms', payment_tool}}}, - {?prv(4), ?trm(4), {rejected, {'PaymentsProvisionTerms', allow}}}, - {?prv(7), ?trm(7), {rejected, {'PaymentsProvisionTerms', global_allow}}} + {?prv(2), ?trm(2), {accepted, {false, {rejected, {'PaymentsProvisionTerms', category}}}}}, + {?prv(3), ?trm(3), {accepted, {false, {rejected, {'PaymentsProvisionTerms', payment_tool}}}}}, + {?prv(4), ?trm(4), {accepted, {false, {rejected, {'PaymentsProvisionTerms', allow}}}}}, + {?prv(7), ?trm(7), {accepted, {false, {rejected, {'PaymentsProvisionTerms', global_allow}}}}} ], to_rejected_routes(RejectedRoutes) ). @@ -617,11 +617,11 @@ rejected_by_table_prohibitions(_C) -> ), ?assert_set_equal( [ - {?prv(3), ?trm(3), {'RoutingRule', undefined}}, - {?prv(1), ?trm(1), {rejected, {'PaymentsProvisionTerms', payment_tool}}}, - {?prv(2), ?trm(2), {rejected, {'PaymentsProvisionTerms', category}}}, - {?prv(4), ?trm(4), {rejected, {'PaymentsProvisionTerms', allow}}}, - {?prv(7), ?trm(7), {rejected, {'PaymentsProvisionTerms', global_allow}}} + {?prv(3), ?trm(3), {prohibit, {true, {'RoutingRule', undefined}}}}, + {?prv(1), ?trm(1), {accepted, {false, {rejected, {'PaymentsProvisionTerms', payment_tool}}}}}, + {?prv(2), ?trm(2), {accepted, {false, {rejected, {'PaymentsProvisionTerms', category}}}}}, + {?prv(4), ?trm(4), {accepted, {false, {rejected, {'PaymentsProvisionTerms', allow}}}}}, + {?prv(7), ?trm(7), {accepted, {false, {rejected, {'PaymentsProvisionTerms', global_allow}}}}} ], to_rejected_routes(RejectedRoutes) ), @@ -751,7 +751,9 @@ empty_terms_allow_test(_C) -> -spec not_reduced_terms_allow_test(config()) -> test_return(). not_reduced_terms_allow_test(_C) -> Error = {'Could not reduce predicate to a value', {allow, {all_of, [{constant, false}]}}}, - do_gather_routes(?not_reduced_allow_revision, undefined, [{?prv(6), ?trm(6), {misconfiguration, Error}}]). + do_gather_routes(?not_reduced_allow_revision, undefined, [ + {?prv(6), ?trm(6), {accepted, {false, {misconfiguration, Error}}}} + ]). do_gather_routes(Revision, ExpectedRouteTerminal, ExpectedRejectedRoutes) -> Currency = ?cur(<<"RUB">>), @@ -967,7 +969,9 @@ recurrent_payment_rejected_without_terms(_C) -> ), ?assertEqual([], Routes), ?assertEqual( - [{?prv(9), ?trm(9), {rejected, {'RecurrentPaytoolsProvisionTerms', undefined}}}], + [ + {?prv(9), ?trm(9), {accepted, {false, {rejected, {'RecurrentPaytoolsProvisionTerms', undefined}}}}} + ], to_rejected_routes(RejectedRoutes) ). diff --git a/apps/routing/src/hg_routing.erl b/apps/routing/src/hg_routing.erl index 0f471cb7..940ae39f 100644 --- a/apps/routing/src/hg_routing.erl +++ b/apps/routing/src/hg_routing.erl @@ -137,7 +137,7 @@ filter(Routes, Keys) -> case route_rejection_reason(Route, Keys) of undefined -> Acc#{routes => [Route | Accepted]}; - {_Key, Reason} -> + Reason -> Acc#{rejected_routes => [hg_route:set_rejection_reason(Reason, Route) | Rejected]} end end, @@ -152,7 +152,7 @@ route_rejection_reason(Route, Keys) -> get_rejection_reason([{Key, Value} | Rest], Data) -> case maps:get(Key, Data, undefined) of {Value, Reason} -> - {Key, Reason}; + {Key, {Value, Reason}}; Value -> {Key, Value}; _ -> @@ -425,8 +425,10 @@ filter_routes_splits_accepted_and_rejected_test() -> new_route(1, 4, 0, {1, 1.0}, {1, 1.0}) ), Rejected = [ - hg_route:set_rejection_reason({accepted, {rejected, {'ProvisionTermSet', undefined}}}, RejectedByTerms), - hg_route:set_rejection_reason({prohibit, <<"blocked">>}, RejectedByProhibition), + hg_route:set_rejection_reason( + {accepted, {false, {rejected, {'ProvisionTermSet', undefined}}}}, RejectedByTerms + ), + hg_route:set_rejection_reason({prohibit, {true, <<"blocked">>}}, RejectedByProhibition), hg_route:set_rejection_reason({blacklisted, 1}, RejectedByBlacklist) ], Result = #{