diff --git a/rebar.config b/rebar.config index 2232f4e..7778d1c 100644 --- a/rebar.config +++ b/rebar.config @@ -77,8 +77,8 @@ {test, [ {deps, [ %% Libraries generated with swagger-codegen-erlang from valitydev/swag-wallets - {swag_server_wallet, {git, "https://github.com/valitydev/swag-wallets", {ref, "96add0f"}}}, - {swag_client_wallet, {git, "https://github.com/valitydev/swag-wallets", {ref, "5fd60dd"}}}, + {swag_server_wallet, {git, "https://github.com/valitydev/swag-wallets", {ref, "6ded7f2"}}}, + {swag_client_wallet, {git, "https://github.com/valitydev/swag-wallets", {ref, "1686026"}}}, {meck, "0.9.2"} ]}, {cover_enabled, true}, diff --git a/src/wapi_wallet_handler.erl b/src/wapi_wallet_handler.erl index f15037e..1ad8844 100644 --- a/src/wapi_wallet_handler.erl +++ b/src/wapi_wallet_handler.erl @@ -108,6 +108,25 @@ prepare('GetWalletAccount' = OperationID, #{'walletID' := WalletID}, Context, _O end end, {ok, #{authorize => Authorize, process => Process}}; +prepare('GetWalletCashLimits' = OperationID, #{'partyID' := PartyID, 'walletID' := WalletID}, Context, _Opts) -> + AuthContext = build_auth_context([{wallet, WalletID}], [], Context), + Authorize = fun() -> + Prototypes = [ + {operation, build_prototype_for(operation, #{id => OperationID}, AuthContext)}, + {wallet, build_prototype_for(wallet, [], AuthContext)} + ], + Resolution = wapi_auth:authorize_operation(Prototypes, Context), + {ok, Resolution} + end, + Process = fun() -> + case wapi_wallet_limits:get_wallet_limits(PartyID, WalletID, Context) of + {ok, Limits} -> + wapi_handler_utils:reply_ok(200, Limits); + {error, {wallet, notfound}} -> + wapi_handler_utils:reply_ok(404) + end + end, + {ok, #{authorize => Authorize, process => Process}}; %% Destinations prepare('ListDestinations' = OperationID, Req0, Context, _Opts) -> AuthContext = build_auth_context( diff --git a/src/wapi_wallet_limits.erl b/src/wapi_wallet_limits.erl new file mode 100644 index 0000000..038379a --- /dev/null +++ b/src/wapi_wallet_limits.erl @@ -0,0 +1,511 @@ +-module(wapi_wallet_limits). +% +% IMPORTANT: calculation is approximate and does NOT cover some cases: +% - selectors {decisions, _} for terminals are not handled +% - exclusive bounds are treated as inclusive (strictness lost) +% - terminals with withdrawal cash_limit=decisions are ignored (no provider fallback) +% - candidate terminals with allowed=false are ignored +% + +-include_lib("damsel/include/dmsl_domain_thrift.hrl"). +-include_lib("damsel/include/dmsl_payproc_thrift.hrl"). + +-export([get_wallet_limits/3]). + +-type handler_context() :: wapi_handler_utils:handler_context(). + +-spec get_wallet_limits(binary(), binary(), handler_context()) -> {ok, [map()]} | {error, {wallet, notfound}}. +get_wallet_limits(PartyID, WalletID, Context) -> + case get_wallet_config(PartyID, WalletID) of + {error, notfound} -> + {error, {wallet, notfound}}; + {ok, WalletConfig} -> + Currency = WalletConfig#domain_WalletConfig.account#domain_WalletAccount.currency, + Revision = wapi_domain_backend:head(), + WalletTerms = get_wallet_terms(WalletConfig#domain_WalletConfig.terms, Revision), + Methods = extract_withdrawal_methods(WalletTerms), + WithdrawalLimit = extract_withdrawal_limit(WalletTerms, Currency), + TerminalRefs = get_withdrawal_terminal_refs( + WalletConfig#domain_WalletConfig.payment_institution, + PartyID, + WalletID, + Revision, + Context + ), + TermLimit = aggregate_terminal_limits(TerminalRefs, Currency, Revision), + EffectiveLimit = intersect_optional(WithdrawalLimit, TermLimit), + Limits = lists:flatmap( + fun(Method) -> + encode_limits(Currency, Method, EffectiveLimit) + end, + Methods + ), + {ok, Limits} + end. + +get_wallet_config(PartyID, WalletID) -> + ObjectRef = {wallet_config, #domain_WalletConfigRef{id = WalletID}}, + case wapi_domain_backend:get_object(ObjectRef) of + {ok, #domain_WalletConfig{party_ref = #domain_PartyConfigRef{id = PartyID}} = WalletConfig} -> + {ok, WalletConfig}; + _ -> + {error, notfound} + end. + +get_wallet_terms(TermsRef, Revision) -> + case wapi_domain_backend:get_object(Revision, {term_set_hierarchy, TermsRef}) of + {ok, #domain_TermSetHierarchy{term_set = TermSet}} -> + TermSet; + _ -> + undefined + end. + +extract_withdrawal_methods(#domain_TermSet{ + wallets = #domain_WalletServiceTerms{ + withdrawals = #domain_WithdrawalServiceTerms{methods = {value, MethodRefs}} + } +}) -> + Methods = [normalize_withdrawal_method(Type) || #domain_PaymentMethodRef{id = {Type, _ID}} <- MethodRefs], + lists:usort(Methods); +extract_withdrawal_methods(_) -> + []. + +extract_withdrawal_limit(undefined, _Currency) -> + undefined; +extract_withdrawal_limit(#domain_TermSet{wallets = undefined}, _Currency) -> + undefined; +extract_withdrawal_limit(#domain_TermSet{wallets = Wallets}, Currency) -> + case Wallets#domain_WalletServiceTerms.withdrawals of + undefined -> + undefined; + #domain_WithdrawalServiceTerms{cash_limit = CashLimitSelector} -> + range_from_selector(CashLimitSelector, Currency) + end. + +get_withdrawal_terminal_refs(PiRef, PartyID, WalletID, Revision, Context) -> + case wapi_domain_backend:get_object(Revision, {payment_institution, PiRef}) of + {ok, #domain_PaymentInstitution{withdrawal_routing_rules = RulesetRef}} -> + case compute_routing_ruleset(RulesetRef, PartyID, WalletID, Revision, Context) of + {ok, #domain_RoutingRuleset{decisions = {candidates, Candidates}}} -> + AllowedCandidates = [ + C#domain_RoutingCandidate.terminal + || C <- Candidates, + predicate_allowed(C#domain_RoutingCandidate.allowed) + ], + lists:usort(AllowedCandidates); + _ -> + [] + end; + _ -> + [] + end. + +compute_routing_ruleset(undefined, _PartyID, _WalletID, _Revision, _Context) -> + undefined; +compute_routing_ruleset( + #domain_RoutingRules{policies = RulesetRef}, + PartyID, + WalletID, + Revision, + Context +) -> + Varset = #payproc_Varset{ + party_ref = #domain_PartyConfigRef{id = PartyID}, + wallet_id = WalletID + }, + case + wapi_handler_utils:service_call( + {party_management, 'ComputeRoutingRuleset', {RulesetRef, Revision, Varset}}, + Context + ) + of + {ok, #domain_RoutingRuleset{} = Ruleset} -> + {ok, Ruleset}; + _ -> + undefined + end. + +aggregate_terminal_limits([], _Currency, _Revision) -> + undefined; +aggregate_terminal_limits([TerminalRef | TerminalRefs], Currency, Revision) -> + Limit0 = get_terminal_limit(TerminalRef, Currency, Revision), + log_terminal_terms(TerminalRef, Limit0), + lists:foldl( + fun(TerminalRef1, LimitAcc) -> + Limit = get_terminal_limit(TerminalRef1, Currency, Revision), + log_terminal_terms(TerminalRef1, Limit), + union_optional(LimitAcc, Limit) + end, + Limit0, + TerminalRefs + ). + +log_terminal_terms(TerminalRef, Limit) -> + logger:debug( + "Wallet cash limits for terminal ~p: limit=~p", + [TerminalRef, Limit] + ). + +get_terminal_limit(TerminalRef, Currency, Revision) -> + case wapi_domain_backend:get_object(Revision, {terminal, TerminalRef}) of + {ok, #domain_Terminal{provider_ref = ProviderRef, terms = TerminalTerms}} -> + ProviderTerms = get_provider_terms(ProviderRef, Revision), + compute_terminal_limit(TerminalTerms, ProviderTerms, Currency); + _ -> + undefined + end. + +compute_terminal_limit(TerminalTerms, ProviderTerms, Currency) -> + TerminalWithdrawalTerms = extract_withdrawal_terms(TerminalTerms), + case terminal_and_provider_allowed(TerminalWithdrawalTerms, ProviderTerms) of + true -> + case extract_provider_limit(TerminalTerms, Currency) of + undefined -> + extract_provider_limit(ProviderTerms, Currency); + TerminalLimit -> + TerminalLimit + end; + false -> + undefined + end. + +extract_withdrawal_terms(#domain_ProvisionTermSet{ + wallet = #domain_WalletProvisionTerms{withdrawals = WithdrawalTerms} +}) -> + WithdrawalTerms; +extract_withdrawal_terms(_) -> + undefined. + +predicate_allowed({constant, false}) -> + false; +predicate_allowed({all_of, List}) when is_list(List) -> + lists:all(fun predicate_allowed/1, List); +predicate_allowed(_) -> + true. + +terminal_and_provider_allowed(undefined, ProviderTerms) -> + provider_withdrawal_allowed(ProviderTerms); +terminal_and_provider_allowed(#domain_WithdrawalProvisionTerms{} = TerminalTerms, ProviderTerms) -> + TerminalAllowed = predicate_allowed(TerminalTerms#domain_WithdrawalProvisionTerms.allow), + TerminalGlobalAllowed = predicate_allowed(TerminalTerms#domain_WithdrawalProvisionTerms.global_allow), + TerminalAllowed andalso TerminalGlobalAllowed andalso provider_withdrawal_allowed(ProviderTerms). + +provider_withdrawal_allowed(undefined) -> + true; +provider_withdrawal_allowed(#domain_ProvisionTermSet{ + wallet = #domain_WalletProvisionTerms{ + withdrawals = #domain_WithdrawalProvisionTerms{} = ProviderWithdrawalTerms + } +}) -> + predicate_allowed(ProviderWithdrawalTerms#domain_WithdrawalProvisionTerms.allow) andalso + predicate_allowed(ProviderWithdrawalTerms#domain_WithdrawalProvisionTerms.global_allow); +provider_withdrawal_allowed(_) -> + true. + +get_provider_terms(ProviderRef, Revision) -> + case wapi_domain_backend:get_object(Revision, {provider, ProviderRef}) of + {ok, #domain_Provider{terms = Terms}} -> + Terms; + _ -> + undefined + end. + +extract_provider_limit(undefined, _Currency) -> + undefined; +extract_provider_limit(#domain_ProvisionTermSet{wallet = undefined}, _Currency) -> + undefined; +extract_provider_limit(#domain_ProvisionTermSet{wallet = WalletTerms}, Currency) -> + case WalletTerms#domain_WalletProvisionTerms.withdrawals of + undefined -> + undefined; + #domain_WithdrawalProvisionTerms{cash_limit = CashLimitSelector} -> + range_from_selector(CashLimitSelector, Currency) + end. + +range_from_selector({value, #domain_CashRange{} = Range}, Currency) -> + normalize_range(Range, Currency); +range_from_selector(_, _Currency) -> + undefined. + +normalize_range(#domain_CashRange{lower = Lower, upper = Upper}, #domain_CurrencyRef{symbolic_code = CurrencyCode}) -> + {LowerAmount, LowerCode} = extract_bound(Lower), + {UpperAmount, UpperCode} = extract_bound(Upper), + case {LowerCode, UpperCode} of + {CurrencyCode, CurrencyCode} -> + #{ + currency => CurrencyCode, + lower => LowerAmount, + upper => UpperAmount + }; + _ -> + undefined + end. + +extract_bound({inclusive, #domain_Cash{amount = Amount, currency = #domain_CurrencyRef{symbolic_code = Code}}}) -> + {Amount, Code}; +extract_bound({exclusive, #domain_Cash{amount = Amount, currency = #domain_CurrencyRef{symbolic_code = Code}}}) -> + {Amount, Code}. + +intersect_optional(undefined, Range) -> + Range; +intersect_optional(Range, undefined) -> + Range; +intersect_optional(#{currency := Currency} = R1, #{currency := Currency} = R2) -> + intersect_ranges(R1, R2). + +intersect_ranges(#{lower := Lower1, upper := Upper1} = R1, #{lower := Lower2, upper := Upper2}) -> + Lower = max(Lower1, Lower2), + Upper = min(Upper1, Upper2), + case valid_range(Lower, Upper) of + true -> + R1#{lower => Lower, upper => Upper}; + false -> + undefined + end. + +union_optional(undefined, Range) -> + Range; +union_optional(Range, undefined) -> + Range; +union_optional(#{currency := Currency} = R1, #{currency := Currency} = R2) -> + union_ranges(R1, R2). + +union_ranges(#{lower := Lower1, upper := Upper1} = R1, #{lower := Lower2, upper := Upper2}) -> + Lower = min(Lower1, Lower2), + Upper = max(Upper1, Upper2), + R1#{lower => Lower, upper => Upper}. + +valid_range(LowerAmount, UpperAmount) when LowerAmount < UpperAmount -> + true; +valid_range(_, _) -> + false. + +encode_limits(_Currency, _Method, undefined) -> + []; +encode_limits(_Currency, Method, #{currency := CurrencyCode} = Range) -> + Encoded = encode_range(CurrencyCode, Range), + [Encoded#{<<"withdrawalMethod">> => encode_withdrawal_method(Method)}]. + +encode_range(CurrencyCode, #{lower := Lower, upper := Upper}) -> + #{ + <<"currency">> => CurrencyCode, + <<"lowerBound">> => encode_bound(Lower), + <<"upperBound">> => encode_bound(Upper) + }. + +encode_bound(Amount) -> + #{ + <<"amount">> => Amount, + <<"inclusive">> => true + }. + +normalize_withdrawal_method(bank_card) -> + bank_card; +normalize_withdrawal_method(digital_wallet) -> + digital_wallet; +normalize_withdrawal_method(_) -> + generic. + +encode_withdrawal_method(bank_card) -> + #{ + <<"method">> => <<"WithdrawalMethodBankCard">>, + <<"paymentSystems">> => [] + }; +encode_withdrawal_method(digital_wallet) -> + #{ + <<"method">> => <<"WithdrawalMethodDigitalWallet">>, + <<"providers">> => [] + }; +encode_withdrawal_method(generic) -> + #{<<"method">> => <<"WithdrawalMethodGeneric">>}. + +-ifdef(TEST). +-include_lib("eunit/include/eunit.hrl"). + +-spec test() -> _. + +-spec predicate_allowed_undefined_test() -> _. +predicate_allowed_undefined_test() -> + ?assertEqual(true, predicate_allowed(undefined)). + +-spec predicate_allowed_constant_true_test() -> _. +predicate_allowed_constant_true_test() -> + ?assertEqual(true, predicate_allowed({constant, true})). + +-spec predicate_allowed_constant_false_test() -> _. +predicate_allowed_constant_false_test() -> + ?assertEqual(false, predicate_allowed({constant, false})). + +-spec predicate_allowed_other_predicates_test() -> _. +predicate_allowed_other_predicates_test() -> + ?assertEqual(true, predicate_allowed({all_of, []})), + ?assertEqual(true, predicate_allowed({all_of, [{constant, true}, {constant, true}]})), + ?assertEqual(false, predicate_allowed({all_of, [{constant, true}, {constant, false}]})), + ?assertEqual(false, predicate_allowed({all_of, [{all_of, [{constant, true}, {constant, false}]}]})), + ?assertEqual(true, predicate_allowed({any_of, []})), + ?assertEqual(true, predicate_allowed({condition, []})). + +-spec terminal_and_provider_allowed_all_true_test() -> _. +terminal_and_provider_allowed_all_true_test() -> + TerminalTerms = #domain_WithdrawalProvisionTerms{ + allow = {constant, true}, + global_allow = {constant, true} + }, + ProviderTerms = #domain_ProvisionTermSet{ + wallet = #domain_WalletProvisionTerms{ + withdrawals = #domain_WithdrawalProvisionTerms{ + allow = {constant, true}, + global_allow = {constant, true} + } + } + }, + ?assertEqual(true, terminal_and_provider_allowed(TerminalTerms, ProviderTerms)). + +-spec terminal_and_provider_allowed_terminal_allow_false_test() -> _. +terminal_and_provider_allowed_terminal_allow_false_test() -> + TerminalTerms = #domain_WithdrawalProvisionTerms{ + allow = {constant, false}, + global_allow = {constant, true} + }, + ProviderTerms = #domain_ProvisionTermSet{ + wallet = #domain_WalletProvisionTerms{ + withdrawals = #domain_WithdrawalProvisionTerms{ + allow = {constant, true}, + global_allow = {constant, true} + } + } + }, + ?assertEqual(false, terminal_and_provider_allowed(TerminalTerms, ProviderTerms)). + +-spec terminal_and_provider_allowed_provider_global_allow_false_test() -> _. +terminal_and_provider_allowed_provider_global_allow_false_test() -> + TerminalTerms = #domain_WithdrawalProvisionTerms{ + allow = {constant, true}, + global_allow = {constant, true} + }, + ProviderTerms = #domain_ProvisionTermSet{ + wallet = #domain_WalletProvisionTerms{ + withdrawals = #domain_WithdrawalProvisionTerms{ + allow = {constant, true}, + global_allow = {constant, false} + } + } + }, + ?assertEqual(false, terminal_and_provider_allowed(TerminalTerms, ProviderTerms)). + +-spec terminal_and_provider_allowed_provider_allow_false_test() -> _. +terminal_and_provider_allowed_provider_allow_false_test() -> + TerminalTerms = #domain_WithdrawalProvisionTerms{ + allow = {constant, true}, + global_allow = {constant, true} + }, + ProviderTerms = #domain_ProvisionTermSet{ + wallet = #domain_WalletProvisionTerms{ + withdrawals = #domain_WithdrawalProvisionTerms{ + allow = {constant, false}, + global_allow = {constant, true} + } + } + }, + ?assertEqual(false, terminal_and_provider_allowed(TerminalTerms, ProviderTerms)). + +-spec terminal_and_provider_allowed_provider_undefined_test() -> _. +terminal_and_provider_allowed_provider_undefined_test() -> + TerminalTerms = #domain_WithdrawalProvisionTerms{ + allow = {constant, true}, + global_allow = {constant, true} + }, + ?assertEqual(true, terminal_and_provider_allowed(TerminalTerms, undefined)). + +-spec intersect_ranges_non_overlapping_test() -> _. +intersect_ranges_non_overlapping_test() -> + R1 = #{currency => <<"RUB">>, lower => 200, upper => 400}, + R2 = #{currency => <<"RUB">>, lower => 500, upper => 800}, + ?assertEqual(undefined, intersect_ranges(R1, R2)). + +-spec compute_terminal_limit_provider_disallowed_returns_undefined_test() -> _. +compute_terminal_limit_provider_disallowed_returns_undefined_test() -> + Rub = #domain_CurrencyRef{symbolic_code = <<"RUB">>}, + TerminalLimitRange = #domain_CashRange{ + lower = {inclusive, #domain_Cash{amount = 300, currency = Rub}}, + upper = {inclusive, #domain_Cash{amount = 900, currency = Rub}} + }, + TerminalTerms = #domain_ProvisionTermSet{ + wallet = #domain_WalletProvisionTerms{ + withdrawals = #domain_WithdrawalProvisionTerms{ + allow = {constant, true}, + global_allow = {constant, true}, + cash_limit = {value, TerminalLimitRange} + } + } + }, + ProviderTerms = #domain_ProvisionTermSet{ + wallet = #domain_WalletProvisionTerms{ + withdrawals = #domain_WithdrawalProvisionTerms{ + allow = {constant, true}, + global_allow = {constant, false} + } + } + }, + ?assertEqual(undefined, compute_terminal_limit(TerminalTerms, ProviderTerms, Rub)). + +-spec compute_terminal_limit_allowed_returns_terminal_limit_test() -> _. +compute_terminal_limit_allowed_returns_terminal_limit_test() -> + Rub = #domain_CurrencyRef{symbolic_code = <<"RUB">>}, + TerminalLimitRange = #domain_CashRange{ + lower = {inclusive, #domain_Cash{amount = 300, currency = Rub}}, + upper = {inclusive, #domain_Cash{amount = 900, currency = Rub}} + }, + TerminalTerms = #domain_ProvisionTermSet{ + wallet = #domain_WalletProvisionTerms{ + withdrawals = #domain_WithdrawalProvisionTerms{ + allow = {constant, true}, + global_allow = {constant, true}, + cash_limit = {value, TerminalLimitRange} + } + } + }, + ProviderTerms = #domain_ProvisionTermSet{ + wallet = #domain_WalletProvisionTerms{ + withdrawals = #domain_WithdrawalProvisionTerms{ + allow = {constant, true}, + global_allow = {constant, true} + } + } + }, + ?assertEqual( + #{currency => <<"RUB">>, lower => 300, upper => 900}, + compute_terminal_limit(TerminalTerms, ProviderTerms, Rub) + ). + +-spec compute_terminal_limit_allowed_fallback_to_provider_limit_test() -> _. +compute_terminal_limit_allowed_fallback_to_provider_limit_test() -> + Rub = #domain_CurrencyRef{symbolic_code = <<"RUB">>}, + ProviderLimitRange = #domain_CashRange{ + lower = {inclusive, #domain_Cash{amount = 100, currency = Rub}}, + upper = {inclusive, #domain_Cash{amount = 500, currency = Rub}} + }, + TerminalTerms = #domain_ProvisionTermSet{ + wallet = #domain_WalletProvisionTerms{ + withdrawals = #domain_WithdrawalProvisionTerms{ + allow = {constant, true}, + global_allow = {constant, true}, + cash_limit = undefined + } + } + }, + ProviderTerms = #domain_ProvisionTermSet{ + wallet = #domain_WalletProvisionTerms{ + withdrawals = #domain_WithdrawalProvisionTerms{ + allow = {constant, true}, + global_allow = {constant, true}, + cash_limit = {value, ProviderLimitRange} + } + } + }, + ?assertEqual( + #{currency => <<"RUB">>, lower => 100, upper => 500}, + compute_terminal_limit(TerminalTerms, ProviderTerms, Rub) + ). + +-endif. diff --git a/test/wapi_ct_helper.erl b/test/wapi_ct_helper.erl index 2da2e97..a54781e 100644 --- a/test/wapi_ct_helper.erl +++ b/test/wapi_ct_helper.erl @@ -133,28 +133,9 @@ start_app(woody = AppName) -> {acceptors_pool_size, 4} ]); start_app({dmt_client = AppName, SupPid}) -> - WalletConfigObject = #domain_WalletConfigObject{ - ref = #domain_WalletConfigRef{id = ?STRING}, - data = #domain_WalletConfig{ - name = ?STRING, - block = - {unblocked, #domain_Unblocked{ - reason = <<"">>, - since = wapi_time:rfc3339() - }}, - suspension = - {active, #domain_Active{ - since = wapi_time:rfc3339() - }}, - payment_institution = #domain_PaymentInstitutionRef{id = 1}, - terms = #domain_TermSetHierarchyRef{id = 1}, - account = #domain_WalletAccount{ - currency = #domain_CurrencyRef{symbolic_code = <<"RUB">>}, - settlement = ?INTEGER - }, - party_ref = #domain_PartyConfigRef{id = ?STRING} - } - }, + CurrencyRef = #domain_CurrencyRef{symbolic_code = <<"RUB">>}, + WalletConfigObject = mk_wallet_config(?STRING, 1), + WalletConfigLimitsOk = mk_wallet_config(?WALLET_ID_OK, 1), PartyConfigObject = #domain_PartyConfigObject{ ref = #domain_PartyConfigRef{id = ?STRING}, data = #domain_PartyConfig{ @@ -173,41 +154,71 @@ start_app({dmt_client = AppName, SupPid}) -> } } }, + %% Term set hierarchy (shared) + %% Wallet limit 100-1000 (wide) - terminal limits will constrain + WithdrawalLimitRange = #domain_CashRange{ + lower = {inclusive, #domain_Cash{amount = 100, currency = CurrencyRef}}, + upper = {inclusive, #domain_Cash{amount = 1000, currency = CurrencyRef}} + }, + PaymentMethods = [ + #domain_PaymentMethodRef{id = {bank_card, #domain_BankCardPaymentMethod{}}}, + #domain_PaymentMethodRef{id = {digital_wallet, #domain_PaymentServiceRef{id = <<"DW">>}}} + ], + TermSetHierarchyObject = #domain_TermSetHierarchyObject{ + ref = #domain_TermSetHierarchyRef{id = 1}, + data = #domain_TermSetHierarchy{ + term_set = #domain_TermSet{ + wallets = #domain_WalletServiceTerms{ + withdrawals = #domain_WithdrawalServiceTerms{ + methods = {value, PaymentMethods}, + cash_limit = {value, WithdrawalLimitRange} + } + } + } + } + }, + %% Term limits 200-400 and 300-500, union = 200-500 (terminal limits constrain) + Term10Limit = #domain_CashRange{ + lower = {inclusive, #domain_Cash{amount = 200, currency = CurrencyRef}}, + upper = {inclusive, #domain_Cash{amount = 400, currency = CurrencyRef}} + }, + Term20Limit = #domain_CashRange{ + lower = {inclusive, #domain_Cash{amount = 300, currency = CurrencyRef}}, + upper = {inclusive, #domain_Cash{amount = 500, currency = CurrencyRef}} + }, + Allowed = {constant, true}, + Terminal10 = mk_terminal_object(10, 11, Term10Limit, Allowed, Allowed), + Terminal20 = mk_terminal_object(20, 21, Term20Limit, Allowed, Allowed), + Provider11 = mk_provider_object(11, Allowed, Allowed), + Provider21 = mk_provider_object(21, Allowed, Allowed), + + PiObject = mk_pi_object(1, 100, 101), + DomainConfigClient = fun + ('CheckoutObject', {{version, V}, {wallet_config, #domain_WalletConfigRef{id = ?STRING}}}) -> + {ok, mk_versioned_object(wallet_config, WalletConfigObject, V)}; + ('CheckoutObject', {{version, V}, {wallet_config, #domain_WalletConfigRef{id = ?WALLET_ID_OK}}}) -> + {ok, mk_versioned_object(wallet_config, WalletConfigLimitsOk, V)}; + ('CheckoutObject', {{version, V}, {party_config, #domain_PartyConfigRef{id = ?STRING}}}) -> + {ok, mk_versioned_object(party_config, PartyConfigObject, V)}; + ('CheckoutObject', {{version, V}, {term_set_hierarchy, #domain_TermSetHierarchyRef{id = 1}}}) -> + {ok, mk_versioned_object(term_set_hierarchy, TermSetHierarchyObject, V)}; + ('CheckoutObject', {{version, V}, {payment_institution, #domain_PaymentInstitutionRef{id = 1}}}) -> + {ok, mk_versioned_object(payment_institution, PiObject, V)}; + ('CheckoutObject', {{version, V}, {terminal, #domain_TerminalRef{id = 10}}}) -> + {ok, mk_versioned_object(terminal, Terminal10, V)}; + ('CheckoutObject', {{version, V}, {terminal, #domain_TerminalRef{id = 20}}}) -> + {ok, mk_versioned_object(terminal, Terminal20, V)}; + ('CheckoutObject', {{version, V}, {provider, #domain_ProviderRef{id = 11}}}) -> + {ok, mk_versioned_object(provider, Provider11, V)}; + ('CheckoutObject', {{version, V}, {provider, #domain_ProviderRef{id = 21}}}) -> + {ok, mk_versioned_object(provider, Provider21, V)}; + ('CheckoutObject', _) -> + woody_error:raise(business, #domain_conf_v2_ObjectNotFound{}) + end, Urls = mock_services_( [ - {domain_config_client, fun - ('CheckoutObject', {{version, ?INTEGER}, {wallet_config, #domain_WalletConfigRef{id = ?STRING}}}) -> - {ok, #domain_conf_v2_VersionedObject{ - info = #domain_conf_v2_VersionedObjectInfo{ - version = ?INTEGER, - changed_at = genlib_rfc3339:format(genlib_time:unow(), second), - changed_by = #domain_conf_v2_Author{ - id = ?STRING, - name = ?STRING, - email = ?STRING - } - }, - object = {wallet_config, WalletConfigObject} - }}; - ('CheckoutObject', {{version, ?INTEGER}, {party_config, #domain_PartyConfigRef{id = ?STRING}}}) -> - {ok, #domain_conf_v2_VersionedObject{ - info = #domain_conf_v2_VersionedObjectInfo{ - version = ?INTEGER, - changed_at = genlib_rfc3339:format(genlib_time:unow(), second), - changed_by = #domain_conf_v2_Author{ - id = ?STRING, - name = ?STRING, - email = ?STRING - } - }, - object = {party_config, PartyConfigObject} - }}; - ('CheckoutObject', _) -> - woody_error:raise(business, #domain_conf_v2_ObjectNotFound{}) - end}, - {domain_config, fun('GetLatestVersion', _) -> - {ok, ?INTEGER} - end} + {domain_config_client, DomainConfigClient}, + {domain_config, fun('GetLatestVersion', _) -> {ok, ?INTEGER} end} ], SupPid ), @@ -399,3 +410,102 @@ create_auth_ctx(PartyID) -> #{ swagger_context => #{auth_context => {?STRING, PartyID, #{}}} }. + +mk_versioned_object(Type, Object, Version) -> + #domain_conf_v2_VersionedObject{ + info = #domain_conf_v2_VersionedObjectInfo{ + version = Version, + changed_at = genlib_rfc3339:format(genlib_time:unow(), second), + changed_by = #domain_conf_v2_Author{ + id = ?STRING, + name = ?STRING, + email = ?STRING + } + }, + object = {Type, Object} + }. + +%% Terminal helper: TerminalRefId, ProviderRefId, CashLimitRange, Allow, GlobalAllow +mk_terminal_object(TermId, ProvId, LimitRange, Allow, GlobalAllow) -> + #domain_TerminalObject{ + ref = #domain_TerminalRef{id = TermId}, + data = #domain_Terminal{ + name = <<"term">>, + description = <<"test">>, + provider_ref = #domain_ProviderRef{id = ProvId}, + terms = #domain_ProvisionTermSet{ + wallet = #domain_WalletProvisionTerms{ + withdrawals = #domain_WithdrawalProvisionTerms{ + cash_limit = {value, LimitRange}, + allow = Allow, + global_allow = GlobalAllow + } + } + } + } + }. + +%% Payment institution helper: PiRefId, PoliciesRulesetId, ProhibitionsRulesetId +mk_pi_object(PiId, PoliciesId, ProhibitionsId) -> + #domain_PaymentInstitutionObject{ + ref = #domain_PaymentInstitutionRef{id = PiId}, + data = #domain_PaymentInstitution{ + name = <<"test">>, + system_account_set = {value, #domain_SystemAccountSetRef{id = 1}}, + inspector = {value, #domain_InspectorRef{id = 1}}, + realm = test, + residences = [rus], + withdrawal_routing_rules = #domain_RoutingRules{ + policies = #domain_RoutingRulesetRef{id = PoliciesId}, + prohibitions = #domain_RoutingRulesetRef{id = ProhibitionsId} + } + } + }. + +%% Wallet config helper: WalletConfigRefId, PaymentInstitutionId +mk_wallet_config(WalletRefId, PiId) -> + #domain_WalletConfigObject{ + ref = #domain_WalletConfigRef{id = WalletRefId}, + data = #domain_WalletConfig{ + name = ?STRING, + block = + {unblocked, #domain_Unblocked{ + reason = <<"">>, + since = wapi_time:rfc3339() + }}, + suspension = + {active, #domain_Active{ + since = wapi_time:rfc3339() + }}, + payment_institution = #domain_PaymentInstitutionRef{id = PiId}, + terms = #domain_TermSetHierarchyRef{id = 1}, + account = #domain_WalletAccount{ + currency = #domain_CurrencyRef{symbolic_code = <<"RUB">>}, + settlement = ?INTEGER + }, + party_ref = #domain_PartyConfigRef{id = ?STRING} + } + }. + +%% Provider helper: ProviderRefId, Allow, GlobalAllow +mk_provider_object(ProvId, Allow, GlobalAllow) -> + #domain_ProviderObject{ + ref = #domain_ProviderRef{id = ProvId}, + data = #domain_Provider{ + name = <<"provider">>, + description = <<"test">>, + proxy = #domain_Proxy{ + ref = #domain_ProxyRef{id = 1}, + additional = #{} + }, + realm = test, + terms = #domain_ProvisionTermSet{ + wallet = #domain_WalletProvisionTerms{ + withdrawals = #domain_WithdrawalProvisionTerms{ + allow = Allow, + global_allow = GlobalAllow + } + } + } + } + }. diff --git a/test/wapi_wallet_dummy_data.hrl b/test/wapi_wallet_dummy_data.hrl index 817d0c7..dad30fd 100644 --- a/test/wapi_wallet_dummy_data.hrl +++ b/test/wapi_wallet_dummy_data.hrl @@ -1,5 +1,8 @@ -define(STRING, <<"TEST">>). -define(STRING2, <<"TEST2">>). +-define(VALID_EMAIL, <<"test@test.ru">>). + +-define(WALLET_ID_OK, <<"wallet_limits_ok">>). -define(RUB, <<"RUB">>). -define(USD, <<"USD">>). -define(BANKID_RU, <<"PUTIN">>). @@ -103,7 +106,7 @@ domain_revision = 123, contact_info = #fistful_base_ContactInfo{ phone_number = ?STRING, - email = ?STRING + email = ?VALID_EMAIL } }). diff --git a/test/wapi_wallet_tests_SUITE.erl b/test/wapi_wallet_tests_SUITE.erl index e36d82e..699f8b8 100644 --- a/test/wapi_wallet_tests_SUITE.erl +++ b/test/wapi_wallet_tests_SUITE.erl @@ -25,7 +25,8 @@ get_fail_wallet_notfound/1, get_account_ok/1, get_account_fail_wallet_notfound/1, - get_account_fail_account_notfound/1 + get_account_fail_account_notfound/1, + get_cash_limits_ok/1 ]). -define(EMPTY_RESP(Code), {error, {Code, #{}}}). @@ -54,7 +55,8 @@ groups() -> get_fail_wallet_notfound, get_account_ok, get_account_fail_wallet_notfound, - get_account_fail_account_notfound + get_account_fail_account_notfound, + get_cash_limits_ok ]} ]. @@ -113,22 +115,32 @@ get_fail_wallet_notfound(C) -> get_account_ok(C) -> PartyID = ?config(party, C), _ = wapi_ct_helper_bouncer:mock_assert_wallet_op_ctx(<<"GetWalletAccount">>, ?STRING, PartyID, C), - ok = mock_account_with_balance(?INTEGER, C), + ok = mock_party_management(?INTEGER, C), {ok, _} = get_account_call_api(?STRING, C). -spec get_account_fail_wallet_notfound(config()) -> _. get_account_fail_wallet_notfound(C) -> _ = wapi_ct_helper_bouncer:mock_arbiter(wapi_ct_helper_bouncer:judge_always_forbidden(), C), - ok = mock_account_with_balance(?INTEGER, C), + ok = mock_party_management(?INTEGER, C), ?assertEqual(?EMPTY_RESP(401), get_account_call_api(<<"non existant wallet id">>, C)). -spec get_account_fail_account_notfound(config()) -> _. get_account_fail_account_notfound(C) -> PartyID = ?config(party, C), _ = wapi_ct_helper_bouncer:mock_assert_wallet_op_ctx(<<"GetWalletAccount">>, ?STRING, PartyID, C), - ok = mock_account_with_balance(424242, C), + ok = mock_party_management(424242, C), ?assertEqual({error, {404, #{}}}, get_account_call_api(?STRING, C)). +-spec get_cash_limits_ok(config()) -> _. +get_cash_limits_ok(C) -> + PartyID = ?config(party, C), + WalletID = ?WALLET_ID_OK, + _ = wapi_ct_helper_bouncer:mock_assert_wallet_op_ctx(<<"GetWalletCashLimits">>, WalletID, PartyID, C), + ok = mock_party_management(?INTEGER, C), + {ok, Limits} = get_cash_limits_call_api(WalletID, PartyID, C), + %% Term union 200-500 (term1: 200-400, term2: 300-500), wallet 100-1000; terminal limits constrain + ?assertEqual(expected_wallet_limits(), Limits). + %% -spec call_api(function(), map(), wapi_client_lib:context()) -> {ok, term()} | {error, term()}. @@ -159,10 +171,61 @@ get_account_call_api(WalletID, C) -> wapi_ct_helper:cfg(context, C) ). -mock_account_with_balance(ExistingAccountID, C) -> +get_cash_limits_call_api(WalletID, PartyID, C) -> + call_api( + fun swag_client_wallet_wallets_api:get_wallet_cash_limits/3, + #{ + binding => #{ + <<"walletID">> => WalletID + }, + qs_val => #{ + <<"partyID">> => PartyID + } + }, + wapi_ct_helper:cfg(context, C) + ). + +expected_wallet_limits() -> + %% 200-500 from terminal union (term1: 200-400, term2: 300-500) + expected_wallet_limits(200, 500). + +expected_wallet_limits(LowerBound, UpperBound) -> + [ + #{ + <<"currency">> => <<"RUB">>, + <<"lowerBound">> => #{<<"amount">> => LowerBound, <<"inclusive">> => true}, + <<"upperBound">> => #{<<"amount">> => UpperBound, <<"inclusive">> => true}, + <<"withdrawalMethod">> => #{ + <<"method">> => <<"WithdrawalMethodBankCard">>, + <<"paymentSystems">> => [] + } + }, + #{ + <<"currency">> => <<"RUB">>, + <<"lowerBound">> => #{<<"amount">> => LowerBound, <<"inclusive">> => true}, + <<"upperBound">> => #{<<"amount">> => UpperBound, <<"inclusive">> => true}, + <<"withdrawalMethod">> => #{ + <<"method">> => <<"WithdrawalMethodDigitalWallet">>, + <<"providers">> => [] + } + } + ]. + +-spec mock_party_management(integer() | undefined, config()) -> ok. +mock_party_management(ExistingAccountID, C) -> _ = wapi_ct_helper:mock_services( [ {party_management, fun + ('ComputeRoutingRuleset', {#domain_RoutingRulesetRef{id = 100}, _V, _Varset}) -> + Allowed = {constant, true}, + {ok, #domain_RoutingRuleset{ + name = <<"both">>, + decisions = + {candidates, [ + #domain_RoutingCandidate{allowed = Allowed, terminal = #domain_TerminalRef{id = 10}}, + #domain_RoutingCandidate{allowed = Allowed, terminal = #domain_TerminalRef{id = 20}} + ]} + }}; ('GetAccountState', {_, AccountID, ?INTEGER}) when AccountID =:= ExistingAccountID -> {ok, #payproc_AccountState{ account_id = AccountID, diff --git a/test/wapi_withdrawal_tests_SUITE.erl b/test/wapi_withdrawal_tests_SUITE.erl index c02d6f1..7c43da6 100644 --- a/test/wapi_withdrawal_tests_SUITE.erl +++ b/test/wapi_withdrawal_tests_SUITE.erl @@ -152,7 +152,7 @@ create_ok(C) -> {ok, #{ <<"contactInfo">> := #{ <<"phoneNumber">> := ?STRING, - <<"email">> := ?STRING + <<"email">> := ?VALID_EMAIL } }} = create_withdrawal_call_api(C). @@ -647,7 +647,7 @@ create_withdrawal_call_api(C) -> <<"currency">> => ?RUB }, <<"contactInfo">> => #{ - <<"email">> => ?STRING, + <<"email">> => ?VALID_EMAIL, <<"phoneNumber">> => ?STRING } })