ED-62: p2p transfer add terminals (#377)

* move terms validation from ff_p2p_provider to p2p_transfer

* add terminals to p2p_transfer

* fix wrong route return

* return withdrawal changes

* fix type after merge

* add p2p_terminals to tests

* add get/2 to p2p_terminal (like p2p_provider's get)

* fix wrong ruleset tag

* wrong terminal in p2p routing tests

* wrong terminal in decisions in p2p routing test rulesets

* Revert 2x "Merge branch 'master' into ED-62/ft/p2p_transfer_add_terminals"

* Revert "Revert 2x "Merge branch 'master' into ED-62/ft/p2p_transfer_add_terminals""

This reverts commit cd593cb602b3ab06bcbe77e2b7b0974982e7a92c.

* return back 6,7,8 withdrawal terminals (being unused for my point of view)

* fix wrong func contract

* dialyzer
This commit is contained in:
George Belyakov 2021-04-01 17:16:33 +03:00 committed by GitHub
parent 2c49e05d56
commit eaeb81c95a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 569 additions and 208 deletions

View File

@ -24,6 +24,7 @@
-export([withdrawal_provider/4]).
-export([withdrawal_terminal/1]).
-export([p2p_provider/4]).
-export([p2p_terminal/1]).
%%
@ -106,6 +107,22 @@ p2p_provider(Ref, ProxyRef, IdentityID, C) ->
}
}}.
-spec p2p_terminal(?dtp('TerminalRef')) -> object().
p2p_terminal(Ref) ->
{terminal, #domain_TerminalObject{
ref = Ref,
data = #domain_Terminal{
name = <<"P2PTerminal">>,
description = <<"P2P terminal">>,
terms = #domain_ProvisionTermSet{
wallet = #domain_WalletProvisionTerms{
p2p = #domain_P2PProvisionTerms{}
}
},
provider_ref = ?prv(101)
}
}}.
-spec withdrawal_provider(?dtp('ProviderRef'), ?dtp('ProxyRef'), binary(), ct_helper:config()) -> object().
withdrawal_provider(?prv(16) = Ref, ProxyRef, IdentityID, C) ->
AccountID = account(<<"RUB">>, C),

View File

@ -374,40 +374,71 @@ dummy_provider_identity_id(Options) ->
domain_config(Options, C) ->
P2PAdapterAdr = maps:get(p2p_adapter_adr, genlib_app:env(fistful, test, #{})),
Decision1 =
WithdrawalDecision1 =
{delegates, [
delegate(condition(party, <<"12345">>), ?ruleset(2)),
delegate(condition(party, <<"67890">>), ?ruleset(4))
]},
Decision2 =
WithdrawalDecision2 =
{delegates, [
delegate(condition(cost_in, {0, 1000, <<"RUB">>}), ?ruleset(3))
]},
Decision3 =
WithdrawalDecision3 =
{candidates, [
candidate({constant, true}, ?trm(1)),
candidate({constant, true}, ?trm(2))
]},
Decision4 =
WithdrawalDecision4 =
{candidates, [
candidate({constant, true}, ?trm(3)),
candidate({constant, true}, ?trm(4)),
candidate({constant, true}, ?trm(5))
]},
Decision5 =
WithdrawalDecision5 =
{candidates, [
candidate({constant, true}, ?trm(4))
]},
P2PDecision1 =
{delegates, [
delegate(condition(party, <<"12345">>), ?ruleset(102)),
delegate(condition(party, <<"67890">>), ?ruleset(104))
]},
P2PDecision2 =
{delegates, [
delegate(condition(cost_in, {0, 1000, <<"RUB">>}), ?ruleset(103))
]},
P2PDecision3 =
{candidates, [
candidate({constant, true}, ?trm(101)),
candidate({constant, true}, ?trm(102))
]},
P2PDecision4 =
{candidates, [
candidate({constant, true}, ?trm(103)),
candidate({constant, true}, ?trm(104)),
candidate({constant, true}, ?trm(105))
]},
P2PDecision5 =
{candidates, [
candidate({constant, true}, ?trm(104))
]},
Default = [
ct_domain:globals(?eas(1), [?payinst(1)]),
ct_domain:external_account_set(?eas(1), <<"Default">>, ?cur(<<"RUB">>), C),
routing_ruleset(?ruleset(1), <<"Rule#1">>, Decision1),
routing_ruleset(?ruleset(2), <<"Rule#2">>, Decision2),
routing_ruleset(?ruleset(3), <<"Rule#3">>, Decision3),
routing_ruleset(?ruleset(4), <<"Rule#4">>, Decision4),
routing_ruleset(?ruleset(5), <<"Rule#5">>, Decision5),
routing_ruleset(?ruleset(1), <<"WithdrawalRuleset#1">>, WithdrawalDecision1),
routing_ruleset(?ruleset(2), <<"WithdrawalRuleset#2">>, WithdrawalDecision2),
routing_ruleset(?ruleset(3), <<"WithdrawalRuleset#3">>, WithdrawalDecision3),
routing_ruleset(?ruleset(4), <<"WithdrawalRuleset#4">>, WithdrawalDecision4),
routing_ruleset(?ruleset(5), <<"WithdrawalRuleset#5">>, WithdrawalDecision5),
routing_ruleset(?ruleset(101), <<"P2PRuleset#1">>, P2PDecision1),
routing_ruleset(?ruleset(102), <<"P2PRuleset#2">>, P2PDecision2),
routing_ruleset(?ruleset(103), <<"P2PRuleset#3">>, P2PDecision3),
routing_ruleset(?ruleset(104), <<"P2PRuleset#4">>, P2PDecision4),
routing_ruleset(?ruleset(105), <<"P2PRuleset#5">>, P2PDecision5),
{payment_institution, #domain_PaymentInstitutionObject{
ref = ?payinst(1),
@ -420,6 +451,10 @@ domain_config(Options, C) ->
policies = ?ruleset(1),
prohibitions = ?ruleset(5)
},
p2p_transfer_routing_rules = #domain_RoutingRules{
policies = ?ruleset(101),
prohibitions = ?ruleset(105)
},
inspector = {value, ?insp(1)},
residences = ['rus'],
realm = live,
@ -659,6 +694,12 @@ domain_config(Options, C) ->
% Provider 17 satellite
ct_domain:withdrawal_terminal(?trm(8)),
ct_domain:p2p_terminal(?trm(101)),
ct_domain:p2p_terminal(?trm(102)),
ct_domain:p2p_terminal(?trm(103)),
ct_domain:p2p_terminal(?trm(104)),
ct_domain:p2p_terminal(?trm(105)),
ct_domain:currency(?cur(<<"RUB">>)),
ct_domain:currency(?cur(<<"USD">>)),
ct_domain:currency(?cur(<<"EUR">>)),

View File

@ -768,7 +768,8 @@ do_process_routing(Withdrawal) ->
}),
do(fun() ->
Routes = unwrap(prepare_routes(build_party_varset(VarsetParams), Identity, DomainRevision)),
Varset = build_party_varset(VarsetParams),
Routes = unwrap(ff_withdrawal_routing:prepare_routes(Varset, Identity, DomainRevision)),
case quote(Withdrawal) of
undefined ->
Routes;
@ -779,10 +780,6 @@ do_process_routing(Withdrawal) ->
end
end).
-spec prepare_routes(party_varset(), identity(), domain_revision()) -> {ok, [route()]} | {error, route_not_found}.
prepare_routes(PartyVarset, Identity, DomainRevision) ->
ff_withdrawal_routing:prepare_routes(PartyVarset, Identity, DomainRevision).
-spec validate_quote_route(route(), quote_state()) -> {ok, valid} | {error, InconsistentQuote} when
InconsistentQuote :: {inconsistent_quote_route, {provider_id, provider_id()} | {terminal_id, terminal_id()}}.
validate_quote_route(Route, #{route := QuoteRoute}) ->
@ -1197,7 +1194,7 @@ get_quote_(Params) ->
} = Params,
Resource = maps:get(resource, Params, undefined),
[Route | _] = unwrap(route, prepare_routes(Varset, Identity, DomainRevision)),
[Route | _] = unwrap(route, ff_withdrawal_routing:prepare_routes(Varset, Identity, DomainRevision)),
{Adapter, AdapterOpts} = ff_withdrawal_session:get_adapter_with_opts(Route),
GetQuoteParams = #{
external_id => maps:get(external_id, Params, undefined),

View File

@ -37,7 +37,6 @@
-type withdrawal_provision_terms() :: dmsl_domain_thrift:'WithdrawalProvisionTerms'().
-type currency_selector() :: dmsl_domain_thrift:'CurrencySelector'().
-type cash_limit_selector() :: dmsl_domain_thrift:'CashLimitSelector'().
-type provision_terms() :: dmsl_domain_thrift:'WithdrawalProvisionTerms'().
%%
@ -86,7 +85,7 @@ get_provider(#{provider_id := ProviderID}) ->
get_terminal(Route) ->
maps:get(terminal_id, Route, undefined).
-spec provision_terms(route(), domain_revision()) -> ff_maybe:maybe(provision_terms()).
-spec provision_terms(route(), domain_revision()) -> ff_maybe:maybe(withdrawal_provision_terms()).
provision_terms(Route, DomainRevision) ->
{ok, Provider} = ff_payouts_provider:get(get_provider(Route), DomainRevision),
ProviderTerms = ff_payouts_provider:provision_terms(Provider),
@ -103,7 +102,7 @@ provision_terms(Route, DomainRevision) ->
-spec merge_withdrawal_terms(
ff_payouts_provider:provision_terms() | undefined,
ff_payouts_terminal:provision_terms() | undefined
) -> ff_maybe:maybe(provision_terms()).
) -> ff_maybe:maybe(withdrawal_provision_terms()).
merge_withdrawal_terms(
#domain_WithdrawalProvisionTerms{
currencies = PCurrencies,
@ -214,7 +213,7 @@ get_valid_terminals_with_priority([{TerminalID, Priority} | Rest], Provider, Par
end,
get_valid_terminals_with_priority(Rest, Provider, PartyVarset, DomainRevision, Acc).
-spec validate_terms(provider(), terminal(), hg_selector:varset()) ->
-spec validate_terms(provider(), terminal(), party_varset()) ->
{ok, valid}
| {error, Error :: term()}.
validate_terms(Provider, Terminal, PartyVarset) ->
@ -231,7 +230,7 @@ assert_terms_defined(undefined, undefined) ->
assert_terms_defined(_, _) ->
{ok, valid}.
-spec validate_combined_terms(withdrawal_provision_terms(), hg_selector:varset()) ->
-spec validate_combined_terms(withdrawal_provision_terms(), party_varset()) ->
{ok, valid}
| {error, Error :: term()}.
validate_combined_terms(CombinedTerms, PartyVarset) ->
@ -265,7 +264,7 @@ validate_selectors_defined(Terms) ->
{error, terms_undefined}
end.
-spec validate_currencies(currency_selector(), hg_selector:varset()) ->
-spec validate_currencies(currency_selector(), party_varset()) ->
{ok, valid}
| {error, Error :: term()}.
validate_currencies(CurrenciesSelector, #{currency := CurrencyRef} = VS) ->
@ -277,7 +276,7 @@ validate_currencies(CurrenciesSelector, #{currency := CurrencyRef} = VS) ->
{error, {terms_violation, {not_allowed_currency, {CurrencyRef, Currencies}}}}
end.
-spec validate_cash_limit(cash_limit_selector(), hg_selector:varset()) ->
-spec validate_cash_limit(cash_limit_selector(), party_varset()) ->
{ok, valid}
| {error, Error :: term()}.
validate_cash_limit(CashLimitSelector, #{cost := Cash} = VS) ->

View File

@ -17,30 +17,26 @@
-type adapter_opts() :: map().
-type provider_ref() :: dmsl_domain_thrift:'ProviderRef'().
-type currency_ref() :: dmsl_domain_thrift:'CurrencyRef'().
-type cash() :: dmsl_domain_thrift:'Cash'().
-type cash_range() :: dmsl_domain_thrift:'CashRange'().
-type validate_terms_error() ::
{terms_violation,
{not_allowed_currency, {currency_ref(), [currency_ref()]}}
| {cash_range, {cash(), cash_range()}}}.
-type term_set() :: dmsl_domain_thrift:'ProvisionTermSet'().
-type provision_terms() :: dmsl_domain_thrift:'P2PProvisionTerms'().
-export_type([id/0]).
-export_type([provider/0]).
-export_type([adapter/0]).
-export_type([adapter_opts/0]).
-export_type([validate_terms_error/0]).
-export_type([provision_terms/0]).
-export([id/1]).
-export([accounts/1]).
-export([adapter/1]).
-export([adapter_opts/1]).
-export([terms/1]).
-export([provision_terms/1]).
-export([ref/1]).
-export([get/1]).
-export([get/2]).
-export([compute_fees/2]).
-export([validate_terms/2]).
%% Pipeline
@ -65,6 +61,24 @@ adapter(#{adapter := Adapter}) ->
adapter_opts(#{adapter_opts := AdapterOpts}) ->
AdapterOpts.
-spec terms(provider()) -> term_set() | undefined.
terms(Provider) ->
maps:get(terms, Provider, undefined).
-spec provision_terms(provider()) -> provision_terms() | undefined.
provision_terms(Provider) ->
case terms(Provider) of
Terms when Terms =/= undefined ->
case Terms#domain_ProvisionTermSet.wallet of
WalletTerms when WalletTerms =/= undefined ->
WalletTerms#domain_WalletProvisionTerms.p2p;
_ ->
undefined
end;
_ ->
undefined
end.
%%
-spec ref(id()) -> provider_ref().
@ -80,9 +94,9 @@ get(ID) ->
-spec get(head | ff_domain_config:revision(), id()) ->
{ok, provider()}
| {error, notfound}.
get(Revision, ID) ->
get(DomainRevision, ID) ->
do(fun() ->
P2PProvider = unwrap(ff_domain_config:object(Revision, {provider, ref(ID)})),
P2PProvider = unwrap(ff_domain_config:object(DomainRevision, {provider, ref(ID)})),
decode(ID, P2PProvider)
end).
@ -96,47 +110,6 @@ compute_fees(#{terms := Terms}, VS) ->
postings => ff_cash_flow:decode_domain_postings(CashFlow)
}.
-spec validate_terms(provider(), hg_selector:varset()) ->
{ok, valid}
| {error, validate_terms_error()}.
validate_terms(#{terms := Terms}, VS) ->
#domain_ProvisionTermSet{wallet = WalletTerms} = Terms,
#domain_WalletProvisionTerms{p2p = P2PTerms} = WalletTerms,
#domain_P2PProvisionTerms{
currencies = CurrenciesSelector,
fees = FeeSelector,
cash_limit = CashLimitSelector
} = P2PTerms,
do(fun() ->
valid = unwrap(validate_currencies(CurrenciesSelector, VS)),
valid = unwrap(validate_fee_term_is_reduced(FeeSelector, VS)),
valid = unwrap(validate_cash_limit(CashLimitSelector, VS))
end).
%%
validate_currencies(CurrenciesSelector, #{currency := CurrencyRef} = VS) ->
{ok, Currencies} = hg_selector:reduce_to_value(CurrenciesSelector, VS),
case ordsets:is_element(CurrencyRef, Currencies) of
true ->
{ok, valid};
false ->
{error, {terms_violation, {not_allowed_currency, {CurrencyRef, Currencies}}}}
end.
validate_fee_term_is_reduced(FeeSelector, VS) ->
{ok, _Fees} = hg_selector:reduce_to_value(FeeSelector, VS),
{ok, valid}.
validate_cash_limit(CashLimitSelector, #{cost := Cash} = VS) ->
{ok, CashRange} = hg_selector:reduce_to_value(CashLimitSelector, VS),
case hg_cash_range:is_inside(Cash, CashRange) of
within ->
{ok, valid};
_NotInRange ->
{error, {terms_violation, {cash_range, {Cash, CashRange}}}}
end.
decode(ID, #domain_Provider{
proxy = Proxy,
identity = Identity,

View File

@ -0,0 +1,105 @@
-module(ff_p2p_terminal).
-include_lib("damsel/include/dmsl_domain_thrift.hrl").
-type terminal() :: #{
id := id(),
name := binary(),
description := binary(),
options => dmsl_domain_thrift:'ProxyOptions'(),
risk_coverage => atom(),
provider_ref => dmsl_domain_thrift:'ProviderRef'(),
terms => dmsl_domain_thrift:'ProvisionTermSet'()
}.
-type id() :: dmsl_domain_thrift:'ObjectID'().
-type terminal_priority() :: integer() | undefined.
-type terminal_ref() :: dmsl_domain_thrift:'TerminalRef'().
-type term_set() :: dmsl_domain_thrift:'ProvisionTermSet'().
-type provision_terms() :: dmsl_domain_thrift:'P2PProvisionTerms'().
-type domain_revision() :: ff_domain_config:revision().
-export_type([id/0]).
-export_type([terminal/0]).
-export_type([terminal_ref/0]).
-export_type([terminal_priority/0]).
-export_type([provision_terms/0]).
-export_type([domain_revision/0]).
-export([adapter_opts/1]).
-export([terms/1]).
-export([provision_terms/1]).
-export([ref/1]).
-export([get/1]).
-export([get/2]).
%% Pipeline
-import(ff_pipeline, [do/1, unwrap/1]).
%%
-spec adapter_opts(terminal()) -> map().
adapter_opts(Terminal) ->
maps:get(options, Terminal, #{}).
-spec terms(terminal()) -> term_set() | undefined.
terms(Terminal) ->
maps:get(terms, Terminal, undefined).
-spec provision_terms(terminal()) -> provision_terms() | undefined.
provision_terms(Terminal) ->
case terms(Terminal) of
Terms when Terms =/= undefined ->
case Terms#domain_ProvisionTermSet.wallet of
WalletTerms when WalletTerms =/= undefined ->
WalletTerms#domain_WalletProvisionTerms.p2p;
_ ->
undefined
end;
_ ->
undefined
end.
%%
-spec ref(id()) -> terminal_ref().
ref(ID) ->
#domain_TerminalRef{id = ID}.
-spec get(id()) ->
{ok, terminal()}
| {error, notfound}.
get(ID) ->
get(head, ID).
-spec get(head | domain_revision(), id()) ->
{ok, terminal()}
| {error, notfound}.
get(DomainRevision, ID) ->
do(fun() ->
P2PTerminal = unwrap(ff_domain_config:object(DomainRevision, {terminal, ref(ID)})),
decode(ID, P2PTerminal)
end).
%%
decode(ID, #domain_Terminal{
name = Name,
description = Description,
options = ProxyOptions,
risk_coverage = RiskCoverage,
provider_ref = ProviderRef,
terms = ProvisionTermSet
}) ->
genlib_map:compact(#{
id => ID,
name => Name,
description => Description,
options => ProxyOptions,
risk_coverage => RiskCoverage,
provider_ref => ProviderRef,
terms => ProvisionTermSet
}).

View File

@ -17,11 +17,16 @@
%% Tests
-export([routes_found_test/1]).
-export([no_routes_found_test/1]).
-export([rejected_by_prohibitions_table_test/1]).
-export([ruleset_misconfig_test/1]).
-export([rules_not_found_test/1]).
-export([withdrawal_routes_found_test/1]).
-export([withdrawal_no_routes_found_test/1]).
-export([withdrawal_rejected_by_prohibitions_table_test/1]).
-export([withdrawal_ruleset_misconfig_test/1]).
-export([withdrawal_rules_not_found_test/1]).
-export([p2p_routes_found_test/1]).
-export([p2p_no_routes_found_test/1]).
-export([p2p_rejected_by_prohibitions_table_test/1]).
-export([p2p_ruleset_misconfig_test/1]).
-export([p2p_rules_not_found_test/1]).
%% Internal types
@ -44,11 +49,16 @@ all() ->
groups() ->
[
{default, [
routes_found_test,
no_routes_found_test,
rejected_by_prohibitions_table_test,
ruleset_misconfig_test,
rules_not_found_test
withdrawal_routes_found_test,
withdrawal_no_routes_found_test,
withdrawal_rejected_by_prohibitions_table_test,
withdrawal_ruleset_misconfig_test,
withdrawal_rules_not_found_test,
p2p_routes_found_test,
p2p_no_routes_found_test,
p2p_rejected_by_prohibitions_table_test,
p2p_ruleset_misconfig_test,
p2p_rules_not_found_test
]}
].
@ -90,9 +100,10 @@ end_per_testcase(_Name, _C) ->
%% Tests
-spec routes_found_test(config()) -> test_return().
routes_found_test(_C) ->
-spec withdrawal_routes_found_test(config()) -> test_return().
withdrawal_routes_found_test(_C) ->
VS = make_varset(?cash(999, <<"RUB">>), <<"12345">>),
PaymentInstitutionID = 1,
?assertMatch(
{
[
@ -101,23 +112,25 @@ routes_found_test(_C) ->
],
#{rejected_routes := []}
},
gather_routes(VS, 1)
gather_routes(withdrawal_routing_rules, PaymentInstitutionID, VS)
).
-spec no_routes_found_test(config()) -> test_return().
no_routes_found_test(_C) ->
-spec withdrawal_no_routes_found_test(config()) -> test_return().
withdrawal_no_routes_found_test(_C) ->
VS = make_varset(?cash(1000, <<"RUB">>), <<"12345">>),
PaymentInstitutionID = 1,
?assertMatch(
{
[],
#{rejected_routes := []}
},
gather_routes(VS, 1)
gather_routes(withdrawal_routing_rules, PaymentInstitutionID, VS)
).
-spec rejected_by_prohibitions_table_test(config()) -> test_return().
rejected_by_prohibitions_table_test(_C) ->
-spec withdrawal_rejected_by_prohibitions_table_test(config()) -> test_return().
withdrawal_rejected_by_prohibitions_table_test(_C) ->
VS = make_varset(?cash(1000, <<"RUB">>), <<"67890">>),
PaymentInstitutionID = 1,
?assertMatch(
{
[
@ -130,29 +143,101 @@ rejected_by_prohibitions_table_test(_C) ->
]
}
},
gather_routes(VS, 1)
gather_routes(withdrawal_routing_rules, PaymentInstitutionID, VS)
).
-spec ruleset_misconfig_test(config()) -> test_return().
ruleset_misconfig_test(_C) ->
-spec withdrawal_ruleset_misconfig_test(config()) -> test_return().
withdrawal_ruleset_misconfig_test(_C) ->
VS = #{party_id => <<"12345">>},
PaymentInstitutionID = 1,
?assertMatch(
{
[],
#{rejected_routes := []}
},
gather_routes(VS, 1)
gather_routes(withdrawal_routing_rules, PaymentInstitutionID, VS)
).
-spec rules_not_found_test(config()) -> test_return().
rules_not_found_test(_C) ->
-spec withdrawal_rules_not_found_test(config()) -> test_return().
withdrawal_rules_not_found_test(_C) ->
VS = make_varset(?cash(1000, <<"RUB">>), <<"12345">>),
PaymentInstitutionID = 2,
?assertMatch(
{
[],
#{rejected_routes := []}
},
gather_routes(VS, 2)
gather_routes(withdrawal_routing_rules, PaymentInstitutionID, VS)
).
-spec p2p_routes_found_test(config()) -> test_return().
p2p_routes_found_test(_C) ->
VS = make_varset(?cash(999, <<"RUB">>), <<"12345">>),
PaymentInstitutionID = 1,
?assertMatch(
{
[
#{terminal_ref := ?trm(101)},
#{terminal_ref := ?trm(102)}
],
#{rejected_routes := []}
},
gather_routes(p2p_transfer_routing_rules, PaymentInstitutionID, VS)
).
-spec p2p_no_routes_found_test(config()) -> test_return().
p2p_no_routes_found_test(_C) ->
VS = make_varset(?cash(1000, <<"RUB">>), <<"12345">>),
PaymentInstitutionID = 1,
?assertMatch(
{
[],
#{rejected_routes := []}
},
gather_routes(p2p_transfer_routing_rules, PaymentInstitutionID, VS)
).
-spec p2p_rejected_by_prohibitions_table_test(config()) -> test_return().
p2p_rejected_by_prohibitions_table_test(_C) ->
VS = make_varset(?cash(1000, <<"RUB">>), <<"67890">>),
PaymentInstitutionID = 1,
?assertMatch(
{
[
#{terminal_ref := ?trm(103)},
#{terminal_ref := ?trm(105)}
],
#{
rejected_routes := [
{_, ?trm(104), {'RoutingRule', <<"Candidate description">>}}
]
}
},
gather_routes(p2p_transfer_routing_rules, PaymentInstitutionID, VS)
).
-spec p2p_ruleset_misconfig_test(config()) -> test_return().
p2p_ruleset_misconfig_test(_C) ->
VS = #{party_id => <<"12345">>},
PaymentInstitutionID = 1,
?assertMatch(
{
[],
#{rejected_routes := []}
},
gather_routes(p2p_transfer_routing_rules, PaymentInstitutionID, VS)
).
-spec p2p_rules_not_found_test(config()) -> test_return().
p2p_rules_not_found_test(_C) ->
VS = make_varset(?cash(1000, <<"RUB">>), <<"12345">>),
PaymentInstitutionID = 2,
?assertMatch(
{
[],
#{rejected_routes := []}
},
gather_routes(p2p_transfer_routing_rules, PaymentInstitutionID, VS)
).
%%
@ -168,12 +253,12 @@ make_varset(Cash, PartyID) ->
party_id => PartyID
}.
gather_routes(Varset, PaymentInstitutionID) ->
gather_routes(RoutingRulesTag, PaymentInstitutionID, Varset) ->
Revision = ff_domain_config:head(),
{ok, PaymentInstitution} = ff_payment_institution:get(PaymentInstitutionID, Revision),
ff_routing_rule:gather_routes(
PaymentInstitution,
withdrawal_routing_rules,
RoutingRulesTag,
Varset,
Revision
).

View File

@ -92,9 +92,7 @@
provider_fees => ff_fees_final:fees()
}.
-type route() :: #{
provider_id := ff_p2p_provider:id()
}.
-type route() :: p2p_transfer_routing:route().
-type body() :: ff_transaction:body().
@ -219,9 +217,25 @@ create(ID, TransferParams, #{
-spec get_adapter_with_opts(session_state()) -> adapter_with_opts().
get_adapter_with_opts(SessionState) ->
#{provider_id := ProviderID} = route(SessionState),
{ok, Provider} = ff_p2p_provider:get(head, ProviderID),
{ff_p2p_provider:adapter(Provider), ff_p2p_provider:adapter_opts(Provider)}.
Route = route(SessionState),
ProviderID = p2p_transfer_routing:get_provider(Route),
TerminalID = p2p_transfer_routing:get_terminal(Route),
get_adapter_with_opts(ProviderID, TerminalID).
-spec get_adapter_with_opts(ProviderID, TerminalID) -> adapter_with_opts() when
ProviderID :: ff_p2p_provider:id(),
TerminalID :: ff_p2p_terminal:id() | undefined.
get_adapter_with_opts(ProviderID, TerminalID) when is_integer(ProviderID) ->
{ok, Provider} = ff_p2p_provider:get(ProviderID),
ProviderOpts = ff_p2p_provider:adapter_opts(Provider),
TerminalOpts = get_adapter_terminal_opts(TerminalID),
{ff_p2p_provider:adapter(Provider), maps:merge(ProviderOpts, TerminalOpts)}.
get_adapter_terminal_opts(undefined) ->
#{};
get_adapter_terminal_opts(TerminalID) ->
{ok, Terminal} = ff_p2p_terminal:get(TerminalID),
ff_p2p_terminal:adapter_opts(Terminal).
-spec process_session(session_state()) -> result().
process_session(SessionState) ->

View File

@ -114,10 +114,7 @@
| {resource_owner(), {bin_data, ff_bin_data:bin_data_error()}}
| {resource_owner(), different_resource}.
-type route() :: #{
version := 1,
provider_id := provider_id()
}.
-type route() :: p2p_transfer_routing:route().
-type adjustment_params() :: #{
id := adjustment_id(),
@ -152,7 +149,6 @@
-export_type([quote/0]).
-export_type([quote_state/0]).
-export_type([event/0]).
-export_type([route/0]).
-export_type([create_error/0]).
-export_type([action/0]).
-export_type([adjustment_params/0]).
@ -224,7 +220,6 @@
-type adjustments_index() :: ff_adjustment_utils:index().
-type party_revision() :: ff_party:revision().
-type domain_revision() :: ff_domain_config:revision().
-type party_varset() :: hg_selector:varset().
-type risk_score() :: p2p_inspector:risk_score().
-type participant() :: p2p_participant:participant().
-type resource() :: ff_resource:resource().
@ -234,11 +229,6 @@
-type wrapped_adjustment_event() :: ff_adjustment_utils:wrapped_event().
-type provider_id() :: ff_p2p_provider:id().
-type routing_rule_route() :: ff_routing_rule:route().
-type reject_context() :: ff_routing_rule:reject_context().
-type legacy_event() :: any().
-type session() :: #{
@ -681,105 +671,26 @@ do_risk_scoring(P2PTransferState) ->
-spec process_routing(p2p_transfer_state()) -> process_result().
process_routing(P2PTransferState) ->
case do_process_routing(P2PTransferState) of
{ok, ProviderID} ->
{ok, Route} ->
{continue, [
{route_changed, #{
version => 1,
provider_id => ProviderID
}}
{route_changed, Route}
]};
{error, route_not_found} ->
process_transfer_fail(route_not_found, P2PTransferState)
end.
-spec do_process_routing(p2p_transfer_state()) -> {ok, provider_id()} | {error, route_not_found}.
-spec do_process_routing(p2p_transfer_state()) -> {ok, route()} | {error, route_not_found}.
do_process_routing(P2PTransferState) ->
DomainRevision = domain_revision(P2PTransferState),
{ok, Identity} = get_identity(owner(P2PTransferState)),
do(fun() ->
VarSet = create_varset(Identity, P2PTransferState),
unwrap(prepare_route(VarSet, Identity, DomainRevision))
Routes = unwrap(p2p_transfer_routing:prepare_routes(VarSet, Identity, DomainRevision)),
Route = hd(Routes),
Route
end).
-spec prepare_route(party_varset(), identity(), domain_revision()) -> {ok, provider_id()} | {error, route_not_found}.
prepare_route(PartyVarset, Identity, DomainRevision) ->
{ok, PaymentInstitutionID} = ff_party:get_identity_payment_institution_id(Identity),
{ok, PaymentInstitution} = ff_payment_institution:get(PaymentInstitutionID, DomainRevision),
{Routes, RejectContext0} = ff_routing_rule:gather_routes(
PaymentInstitution,
p2p_transfer_routing_rules,
PartyVarset,
DomainRevision
),
{ValidatedRoutes, RejectContext1} = filter_valid_routes(Routes, RejectContext0, PartyVarset),
case ValidatedRoutes of
[] ->
ff_routing_rule:log_reject_context(RejectContext1),
logger:log(info, "Fallback to legacy method of routes gathering"),
{ok, Providers} = ff_payment_institution:compute_p2p_transfer_providers(PaymentInstitution, PartyVarset),
choose_provider_legacy(Providers, PartyVarset);
[ProviderID | _] ->
{ok, ProviderID}
end.
-spec filter_valid_routes([routing_rule_route()], reject_context(), party_varset()) -> {[route()], reject_context()}.
filter_valid_routes(Routes, RejectContext, PartyVarset) ->
filter_valid_routes_(Routes, PartyVarset, {#{}, RejectContext}).
filter_valid_routes_([], _, {Acc, RejectContext}) when map_size(Acc) == 0 ->
{[], RejectContext};
filter_valid_routes_([], _, {Acc, RejectContext}) ->
{convert_to_route(Acc), RejectContext};
filter_valid_routes_([Route | Rest], PartyVarset, {Acc0, RejectContext0}) ->
Terminal = maps:get(terminal, Route),
TerminalRef = maps:get(terminal_ref, Route),
ProviderRef = Terminal#domain_Terminal.provider_ref,
ProviderID = ProviderRef#domain_ProviderRef.id,
Priority = maps:get(priority, Route, undefined),
{ok, Provider} = ff_p2p_provider:get(ProviderID),
{Acc, RejectConext} =
case ff_p2p_provider:validate_terms(Provider, PartyVarset) of
{ok, valid} ->
Terms = maps:get(Priority, Acc0, []),
Acc1 = maps:put(Priority, [ProviderID | Terms], Acc0),
{Acc1, RejectContext0};
{error, RejectReason} ->
RejectedRoutes0 = maps:get(rejected_routes, RejectContext0),
RejectedRoutes1 = [{ProviderRef, TerminalRef, RejectReason} | RejectedRoutes0],
RejectContext1 = maps:put(rejected_routes, RejectedRoutes1, RejectContext0),
{Acc0, RejectContext1}
end,
filter_valid_routes_(Rest, PartyVarset, {RejectConext, Acc}).
convert_to_route(ProviderTerminalMap) ->
lists:foldl(
fun({_Priority, Providers}, Acc) ->
lists:sort(Providers) ++ Acc
end,
[],
lists:keysort(1, maps:to_list(ProviderTerminalMap))
).
-spec choose_provider_legacy([provider_id()], party_varset()) -> {ok, provider_id()} | {error, route_not_found}.
choose_provider_legacy(Providers, VS) ->
case lists:filter(fun(P) -> validate_terms(P, VS) end, Providers) of
[ProviderID | _] ->
{ok, ProviderID};
[] ->
{error, route_not_found}
end.
-spec validate_terms(provider_id(), party_varset()) -> boolean().
validate_terms(ID, VS) ->
{ok, Provider} = ff_p2p_provider:get(ID),
case ff_p2p_provider:validate_terms(Provider, VS) of
{ok, valid} ->
true;
{error, _Error} ->
false
end.
-spec process_p_transfer_creation(p2p_transfer_state()) -> process_result().
process_p_transfer_creation(P2PTransferState) ->
FinalCashFlow = make_final_cash_flow(P2PTransferState),
@ -800,11 +711,8 @@ process_session_creation(P2PTransferState) ->
merchant_fees => MerchantFees,
provider_fees => ProviderFees
}),
#{provider_id := ProviderID} = route(P2PTransferState),
Params = #{
route => #{
provider_id => ProviderID
},
route => route(P2PTransferState),
domain_revision => domain_revision(P2PTransferState),
party_revision => party_revision(P2PTransferState)
},

View File

@ -0,0 +1,222 @@
-module(p2p_transfer_routing).
-include_lib("damsel/include/dmsl_domain_thrift.hrl").
-export([prepare_routes/3]).
-export([get_provider/1]).
-export([get_terminal/1]).
-import(ff_pipeline, [do/1, unwrap/1]).
-type route() :: #{
version := 1,
provider_id := provider_id(),
terminal_id => terminal_id()
}.
-export_type([route/0]).
-type identity() :: ff_identity:identity_state().
-type domain_revision() :: ff_domain_config:revision().
-type party_varset() :: hg_selector:varset().
-type provider_id() :: ff_p2p_provider:id().
-type provider() :: ff_p2p_provider:provider().
-type terminal_id() :: ff_p2p_terminal:id().
-type terminal() :: ff_p2p_terminal:terminal().
-type routing_rule_route() :: ff_routing_rule:route().
-type reject_context() :: ff_routing_rule:reject_context().
-type p2p_provision_terms() :: dmsl_domain_thrift:'P2PProvisionTerms'().
-spec prepare_routes(party_varset(), identity(), domain_revision()) -> {ok, [route()]} | {error, route_not_found}.
prepare_routes(PartyVarset, Identity, DomainRevision) ->
{ok, PaymentInstitutionID} = ff_party:get_identity_payment_institution_id(Identity),
{ok, PaymentInstitution} = ff_payment_institution:get(PaymentInstitutionID, DomainRevision),
{Routes, RejectContext0} = ff_routing_rule:gather_routes(
PaymentInstitution,
p2p_transfer_routing_rules,
PartyVarset,
DomainRevision
),
{ValidatedRoutes, RejectContext1} = filter_valid_routes(Routes, RejectContext0, PartyVarset),
case ValidatedRoutes of
[] ->
ff_routing_rule:log_reject_context(RejectContext1),
logger:log(info, "Fallback to legacy method of routes gathering"),
{ok, Providers} = ff_payment_institution:compute_p2p_transfer_providers(PaymentInstitution, PartyVarset),
FilteredRoutes = filter_routes_legacy(Providers, PartyVarset),
case FilteredRoutes of
[] ->
{error, route_not_found};
[_Route | _] ->
{ok, FilteredRoutes}
end;
[_Route | _] ->
{ok, ValidatedRoutes}
end.
-spec make_route(provider_id(), terminal_id() | undefined) -> route().
make_route(ProviderID, TerminalID) ->
genlib_map:compact(#{
version => 1,
provider_id => ProviderID,
terminal_id => TerminalID
}).
-spec get_provider(route()) -> provider_id().
get_provider(#{provider_id := ProviderID}) ->
ProviderID.
-spec get_terminal(route()) -> ff_maybe:maybe(terminal_id()).
get_terminal(Route) ->
maps:get(terminal_id, Route, undefined).
-spec filter_valid_routes([routing_rule_route()], reject_context(), party_varset()) -> {[route()], reject_context()}.
filter_valid_routes(Routes, RejectContext, PartyVarset) ->
filter_valid_routes_(Routes, PartyVarset, {#{}, RejectContext}).
filter_valid_routes_([], _, {Acc, RejectContext}) when map_size(Acc) == 0 ->
{[], RejectContext};
filter_valid_routes_([], _, {Acc, RejectContext}) ->
{convert_to_route(Acc), RejectContext};
filter_valid_routes_([Route | Rest], PartyVarset, {Acc0, RejectContext0}) ->
Terminal = maps:get(terminal, Route),
TerminalRef = maps:get(terminal_ref, Route),
TerminalID = TerminalRef#domain_TerminalRef.id,
ProviderRef = Terminal#domain_Terminal.provider_ref,
ProviderID = ProviderRef#domain_ProviderRef.id,
Priority = maps:get(priority, Route, undefined),
{ok, P2PTerminal} = ff_p2p_terminal:get(TerminalID),
{ok, P2PProvider} = ff_p2p_provider:get(ProviderID),
{Acc, RejectConext} =
case validate_terms(P2PProvider, P2PTerminal, PartyVarset) of
{ok, valid} ->
Terms = maps:get(Priority, Acc0, []),
Acc1 = maps:put(Priority, [{ProviderID, TerminalID} | Terms], Acc0),
{Acc1, RejectContext0};
{error, RejectReason} ->
RejectedRoutes0 = maps:get(rejected_routes, RejectContext0),
RejectedRoutes1 = [{ProviderRef, TerminalRef, RejectReason} | RejectedRoutes0],
RejectContext1 = maps:put(rejected_routes, RejectedRoutes1, RejectContext0),
{Acc0, RejectContext1}
end,
filter_valid_routes_(Rest, PartyVarset, {RejectConext, Acc}).
-spec filter_routes_legacy([provider_id()], party_varset()) -> [route()].
filter_routes_legacy(Providers, VS) ->
lists:foldr(
fun(ProviderID, Acc) ->
{ok, Provider} = ff_p2p_provider:get(ProviderID),
case validate_terms_legacy(Provider, VS) of
{ok, valid} ->
[make_route(ProviderID, undefined) | Acc];
{error, _Error} ->
Acc
end
end,
[],
Providers
).
-spec validate_terms_legacy(provider(), party_varset()) ->
{ok, valid}
| {error, Error :: term()}.
validate_terms_legacy(Provider, VS) ->
do(fun() ->
ProviderTerms = ff_p2p_provider:provision_terms(Provider),
unwrap(validate_combined_terms(ProviderTerms, VS))
end).
-spec validate_terms(provider(), terminal(), party_varset()) ->
{ok, valid}
| {error, Error :: term()}.
validate_terms(Provider, Terminal, VS) ->
do(fun() ->
ProviderTerms = ff_p2p_provider:provision_terms(Provider),
TerminalTerms = ff_p2p_terminal:provision_terms(Terminal),
_ = unwrap(assert_terms_defined(TerminalTerms, ProviderTerms)),
CombinedTerms = merge_p2p_terms(ProviderTerms, TerminalTerms),
unwrap(validate_combined_terms(CombinedTerms, VS))
end).
assert_terms_defined(undefined, undefined) ->
{error, terms_undefined};
assert_terms_defined(_, _) ->
{ok, valid}.
-spec validate_combined_terms(p2p_provision_terms(), party_varset()) ->
{ok, valid}
| {error, Error :: term()}.
validate_combined_terms(CombinedTerms, VS) ->
do(fun() ->
#domain_P2PProvisionTerms{
currencies = CurrenciesSelector,
fees = FeeSelector,
cash_limit = CashLimitSelector
} = CombinedTerms,
valid = unwrap(validate_currencies(CurrenciesSelector, VS)),
valid = unwrap(validate_fee_term_is_reduced(FeeSelector, VS)),
valid = unwrap(validate_cash_limit(CashLimitSelector, VS))
end).
validate_currencies(CurrenciesSelector, #{currency := CurrencyRef} = VS) ->
{ok, Currencies} = hg_selector:reduce_to_value(CurrenciesSelector, VS),
case ordsets:is_element(CurrencyRef, Currencies) of
true ->
{ok, valid};
false ->
{error, {terms_violation, {not_allowed_currency, {CurrencyRef, Currencies}}}}
end.
validate_fee_term_is_reduced(FeeSelector, VS) ->
{ok, _Fees} = hg_selector:reduce_to_value(FeeSelector, VS),
{ok, valid}.
validate_cash_limit(CashLimitSelector, #{cost := Cash} = VS) ->
{ok, CashRange} = hg_selector:reduce_to_value(CashLimitSelector, VS),
case hg_cash_range:is_inside(Cash, CashRange) of
within ->
{ok, valid};
_NotInRange ->
{error, {terms_violation, {cash_range, {Cash, CashRange}}}}
end.
-spec merge_p2p_terms(
ff_p2p_provider:provision_terms() | undefined,
ff_p2p_terminal:provision_terms() | undefined
) -> ff_maybe:maybe(p2p_provision_terms()).
merge_p2p_terms(
#domain_P2PProvisionTerms{
currencies = PCurrencies,
fees = PFees,
cash_limit = PCashLimit,
cash_flow = PCashflow
},
#domain_P2PProvisionTerms{
currencies = TCurrencies,
fees = TFees,
cash_limit = TCashLimit,
cash_flow = TCashflow
}
) ->
#domain_P2PProvisionTerms{
currencies = ff_maybe:get_defined(TCurrencies, PCurrencies),
fees = ff_maybe:get_defined(PFees, TFees),
cash_limit = ff_maybe:get_defined(TCashLimit, PCashLimit),
cash_flow = ff_maybe:get_defined(TCashflow, PCashflow)
};
merge_p2p_terms(ProviderTerms, TerminalTerms) ->
ff_maybe:get_defined(TerminalTerms, ProviderTerms).
convert_to_route(ProviderTerminalMap) ->
lists:foldl(
fun({_, Data}, Acc) ->
SortedRoutes = [make_route(P, T) || {P, T} <- lists:sort(Data)],
SortedRoutes ++ Acc
end,
[],
lists:keysort(1, maps:to_list(ProviderTerminalMap))
).