From eaeb81c95ae2184e5fc417c2a12c2299eb72168b Mon Sep 17 00:00:00 2001 From: George Belyakov <8051393+georgemadskillz@users.noreply.github.com> Date: Thu, 1 Apr 2021 17:16:33 +0300 Subject: [PATCH] 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 --- apps/ff_cth/src/ct_domain.erl | 17 ++ apps/ff_cth/src/ct_payment_system.erl | 61 ++++- apps/ff_transfer/src/ff_withdrawal.erl | 9 +- .../ff_transfer/src/ff_withdrawal_routing.erl | 13 +- apps/fistful/src/ff_p2p_provider.erl | 77 ++---- apps/fistful/src/ff_p2p_terminal.erl | 105 +++++++++ apps/fistful/test/ff_routing_rule_SUITE.erl | 139 ++++++++--- apps/p2p/src/p2p_session.erl | 26 +- apps/p2p/src/p2p_transfer.erl | 108 +-------- apps/p2p/src/p2p_transfer_routing.erl | 222 ++++++++++++++++++ 10 files changed, 569 insertions(+), 208 deletions(-) create mode 100644 apps/fistful/src/ff_p2p_terminal.erl create mode 100644 apps/p2p/src/p2p_transfer_routing.erl diff --git a/apps/ff_cth/src/ct_domain.erl b/apps/ff_cth/src/ct_domain.erl index 9037b50..d599af7 100644 --- a/apps/ff_cth/src/ct_domain.erl +++ b/apps/ff_cth/src/ct_domain.erl @@ -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), diff --git a/apps/ff_cth/src/ct_payment_system.erl b/apps/ff_cth/src/ct_payment_system.erl index 6c2514c..bbcdea5 100644 --- a/apps/ff_cth/src/ct_payment_system.erl +++ b/apps/ff_cth/src/ct_payment_system.erl @@ -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">>)), diff --git a/apps/ff_transfer/src/ff_withdrawal.erl b/apps/ff_transfer/src/ff_withdrawal.erl index 912a6bd..a207d19 100644 --- a/apps/ff_transfer/src/ff_withdrawal.erl +++ b/apps/ff_transfer/src/ff_withdrawal.erl @@ -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), diff --git a/apps/ff_transfer/src/ff_withdrawal_routing.erl b/apps/ff_transfer/src/ff_withdrawal_routing.erl index 2f5f966..9da270c 100644 --- a/apps/ff_transfer/src/ff_withdrawal_routing.erl +++ b/apps/ff_transfer/src/ff_withdrawal_routing.erl @@ -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) -> diff --git a/apps/fistful/src/ff_p2p_provider.erl b/apps/fistful/src/ff_p2p_provider.erl index fb83797..1615894 100644 --- a/apps/fistful/src/ff_p2p_provider.erl +++ b/apps/fistful/src/ff_p2p_provider.erl @@ -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, diff --git a/apps/fistful/src/ff_p2p_terminal.erl b/apps/fistful/src/ff_p2p_terminal.erl new file mode 100644 index 0000000..d32c937 --- /dev/null +++ b/apps/fistful/src/ff_p2p_terminal.erl @@ -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 + }). diff --git a/apps/fistful/test/ff_routing_rule_SUITE.erl b/apps/fistful/test/ff_routing_rule_SUITE.erl index 4ae039a..6f1399d 100644 --- a/apps/fistful/test/ff_routing_rule_SUITE.erl +++ b/apps/fistful/test/ff_routing_rule_SUITE.erl @@ -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 ). diff --git a/apps/p2p/src/p2p_session.erl b/apps/p2p/src/p2p_session.erl index 2b6b46e..21da9c9 100644 --- a/apps/p2p/src/p2p_session.erl +++ b/apps/p2p/src/p2p_session.erl @@ -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) -> diff --git a/apps/p2p/src/p2p_transfer.erl b/apps/p2p/src/p2p_transfer.erl index c2ec09f..54f16a3 100644 --- a/apps/p2p/src/p2p_transfer.erl +++ b/apps/p2p/src/p2p_transfer.erl @@ -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) }, diff --git a/apps/p2p/src/p2p_transfer_routing.erl b/apps/p2p/src/p2p_transfer_routing.erl new file mode 100644 index 0000000..eec29e3 --- /dev/null +++ b/apps/p2p/src/p2p_transfer_routing.erl @@ -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)) + ).