HG-392: check limits for wallet and withdrawal (#25)

* fix gitmodules

* remove wtf

* HG-392: check limits for wallet and withdrawal
This commit is contained in:
Evgeny Levenets 2018-10-30 17:30:42 +03:00 committed by GitHub
parent 1b8c6a13b8
commit f1e3355dca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 159 additions and 33 deletions

2
.gitmodules vendored
View File

@ -1,6 +1,6 @@
[submodule "build-utils"]
path = build-utils
url = git+ssh://github.com/rbkmoney/build_utils
url = git@github.com:rbkmoney/build_utils.git
[submodule "schemes/swag"]
path = schemes/swag
url = git@github.com:rbkmoney/swag-wallets.git

View File

@ -109,23 +109,24 @@ route(T) -> ff_transfer:route(T).
}.
create(ID, #{wallet_id := WalletID, destination_id := DestinationID, body := Body}, Ctx) ->
{_Amount, CurrencyID} = Body,
do(fun() ->
Wallet = ff_wallet_machine:wallet(unwrap(wallet, ff_wallet_machine:get(WalletID))),
WalletAccount = ff_wallet:account(Wallet),
Destination = ff_destination:get(
unwrap(destination, ff_destination:get_machine(DestinationID))
),
ok = unwrap(destination, valid(authorized, ff_destination:status(Destination))),
Terms = unwrap(contract, get_contract_terms(Wallet, Body, ff_time:now())),
valid = unwrap(terms, ff_party:validate_withdrawal_creation(Terms, CurrencyID)),
valid = unwrap(terms, ff_party:validate_withdrawal_creation(Terms, Body, WalletAccount)),
CashFlowPlan = unwrap(cash_flow_plan, ff_party:get_withdrawal_cash_flow_plan(Terms)),
Params = #{
handler => ?MODULE,
body => Body,
params => #{
wallet_id => WalletID,
destination_id => DestinationID,
wallet_account => ff_wallet:account(Wallet),
wallet_account => WalletAccount,
destination_account => ff_destination:account(Destination),
wallet_cash_flow_plan => CashFlowPlan
}
@ -251,20 +252,23 @@ create_p_transfer(Withdrawal) ->
{error, _Reason}.
create_session(Withdrawal) ->
ID = construct_session_id(id(Withdrawal)),
Body = body(Withdrawal),
#{
wallet_id := WalletID,
wallet_account := WalletAccount,
destination_account := DestinationAccount
} = params(Withdrawal),
#{provider_id := ProviderID} = route(Withdrawal),
{ok, SenderSt} = ff_identity_machine:get(ff_account:identity(WalletAccount)),
{ok, ReceiverSt} = ff_identity_machine:get(ff_account:identity(DestinationAccount)),
TransferData = #{
id => ID,
cash => body(Withdrawal),
sender => ff_identity_machine:identity(SenderSt),
receiver => ff_identity_machine:identity(ReceiverSt)
},
do(fun () ->
valid = validate_wallet_limits(WalletID, Body, WalletAccount),
#{provider_id := ProviderID} = route(Withdrawal),
SenderSt = unwrap(ff_identity_machine:get(ff_account:identity(WalletAccount))),
ReceiverSt = unwrap(ff_identity_machine:get(ff_account:identity(DestinationAccount))),
TransferData = #{
id => ID,
cash => body(Withdrawal),
sender => ff_identity_machine:identity(SenderSt),
receiver => ff_identity_machine:identity(ReceiverSt)
},
SessionParams = #{
destination => destination_id(Withdrawal),
provider_id => ProviderID
@ -347,3 +351,8 @@ finalize_cash_flow(CashFlowPlan, WalletAccount, DestinationAccount, SystemAccoun
{provider, settlement} => ProviderAccount
}),
ff_cash_flow:finalize(CashFlowPlan, Accounts, Constants).
validate_wallet_limits(WalletID, Body, Account) ->
Wallet = ff_wallet_machine:wallet(unwrap(wallet, ff_wallet_machine:get(WalletID))),
Terms = unwrap(contract, get_contract_terms(Wallet, Body, ff_time:now())),
unwrap(wallet_limit, ff_party:validate_wallet_limits(Account, Terms)).

View File

@ -1,6 +1,8 @@
-module(ff_transfer_SUITE).
-include_lib("fistful_proto/include/ff_proto_fistful_thrift.hrl").
-include_lib("dmsl/include/dmsl_domain_thrift.hrl").
-export([all/0]).
-export([groups/0]).
@ -220,6 +222,45 @@ deposit_withdrawal_ok(C) ->
genlib_retry:linear(15, 1000)
),
ok = await_wallet_balance({10000 - 4240, <<"RUB">>}, WalID),
ok = await_destination_balance({4240 - 424, <<"RUB">>}, DestID),
% Fail withdrawal because of limits
WdrID2 = generate_id(),
ok = ff_withdrawal:create(
WdrID2,
#{wallet_id => WalID, destination_id => DestID, body => {10000 - 4240 + 1, <<"RUB">>}},
ff_ctx:new()
),
{ok, WdrM2} = ff_withdrawal:get_machine(WdrID2),
pending = ff_withdrawal:status(ff_withdrawal:get(WdrM2)),
FailedDueToLimit = {failed, {wallet_limit, {terms_violation,
{cash_range, {
#domain_Cash{
amount = -1,
currency = #domain_CurrencyRef{symbolic_code = <<"RUB">>}
},
#domain_CashRange{
lower = {inclusive, #domain_Cash{
amount = 0,
currency = #domain_CurrencyRef{symbolic_code = <<"RUB">>}
}},
upper = {exclusive, #domain_Cash{
amount = 10000001,
currency = #domain_CurrencyRef{symbolic_code = <<"RUB">>}
}}
}
}}
}}},
FailedDueToLimit = ct_helper:await(
FailedDueToLimit,
fun () ->
{ok, TmpWdrM} = ff_withdrawal:get_machine(WdrID2),
ff_withdrawal:status(ff_withdrawal:get(TmpWdrM))
end,
genlib_retry:linear(15, 1000)
),
ok = await_wallet_balance({10000 - 4240, <<"RUB">>}, WalID),
ok = await_destination_balance({4240 - 424, <<"RUB">>}, DestID).
create_party(_C) ->

View File

@ -41,7 +41,9 @@
-export([create_contract/2]).
-export([change_contractor_level/3]).
-export([validate_account_creation/2]).
-export([validate_withdrawal_creation/2]).
-export([validate_withdrawal_creation/3]).
-export([validate_wallet_limits/2]).
-export([get_contract_terms/4]).
-export([get_withdrawal_cash_flow_plan/1]).
@ -53,9 +55,13 @@
-type withdrawal_terms() :: dmsl_domain_thrift:'WithdrawalServiceTerms'().
-type currency_id() :: ff_currency:id().
-type currency_ref() :: dmsl_domain_thrift:'CurrencyRef'().
-type domain_cash() :: dmsl_domain_thrift:'Cash'().
-type cash_range() :: dmsl_domain_thrift:'CashRange'().
-type timestamp() :: ff_time:timestamp_ms().
-type currency_validation_error() :: {terms_violation, {not_allowed_currency, _Details}}.
-type withdrawal_currency_error() :: {invalid_withdrawal_currency, currency_id(), {wallet_currency, currency_id()}}.
-type cash_range_validation_error() :: {terms_violation, {cash_range, {domain_cash(), cash_range()}}}.
%% Pipeline
@ -157,22 +163,25 @@ validate_account_creation(Terms, CurrencyID) ->
#domain_TermSet{wallets = WalletTerms} = Terms,
do(fun () ->
valid = unwrap(validate_wallet_creation_terms_is_reduced(WalletTerms)),
valid = unwrap(validate_wallet_currency(CurrencyID, WalletTerms))
valid = unwrap(validate_wallet_terms_currency(CurrencyID, WalletTerms))
end).
-spec validate_withdrawal_creation(terms(), currency_id()) -> Result when
-spec validate_withdrawal_creation(terms(), cash(), ff_account:account()) -> Result when
Result :: {ok, valid} | {error, Error},
Error ::
{invalid_terms, _Details} |
currency_validation_error().
currency_validation_error() |
withdrawal_currency_error().
validate_withdrawal_creation(Terms, CurrencyID) ->
validate_withdrawal_creation(Terms, {_, CurrencyID} = Cash, Account) ->
#domain_TermSet{wallets = WalletTerms} = Terms,
do(fun () ->
valid = unwrap(validate_withdrawal_terms_is_reduced(WalletTerms)),
valid = unwrap(validate_wallet_currency(CurrencyID, WalletTerms)),
valid = unwrap(validate_wallet_terms_currency(CurrencyID, WalletTerms)),
#domain_WalletServiceTerms{withdrawals = WithdrawalTerms} = WalletTerms,
valid = unwrap(validate_withdrawal_currency(CurrencyID, WithdrawalTerms))
valid = unwrap(validate_withdrawal_wallet_currency(CurrencyID, Account)),
valid = unwrap(validate_withdrawal_terms_currency(CurrencyID, WithdrawalTerms)),
valid = unwrap(validate_withdrawal_cash_limit(Cash, WithdrawalTerms))
end).
-spec get_withdrawal_cash_flow_plan(terms()) ->
@ -369,7 +378,9 @@ validate_wallet_creation_terms_is_reduced(Terms) ->
#domain_WalletServiceTerms{
currencies = CurrenciesSelector
} = Terms,
do_validate_terms_is_reduced([{currencies, CurrenciesSelector}]).
do_validate_terms_is_reduced([
{wallet_currencies, CurrenciesSelector}
]).
-spec validate_withdrawal_terms_is_reduced(wallet_terms()) ->
{ok, valid} | {error, {invalid_terms, _Details}}.
@ -387,15 +398,12 @@ validate_withdrawal_terms_is_reduced(Terms) ->
cash_limit = CashLimitSelector,
cash_flow = CashFlowSelector
} = WithdrawalTerms,
Selectors = [
do_validate_terms_is_reduced([
{wallet_currencies, WalletCurrenciesSelector},
{withdrawal_currencies, WithdrawalCurrenciesSelector},
{withdrawal_cash_limit, CashLimitSelector},
{withdrawal_cash_flow, CashFlowSelector}
],
do(fun () ->
valid = unwrap(do_validate_terms_is_reduced(Selectors))
end).
]).
do_validate_terms_is_reduced([]) ->
{ok, valid};
@ -414,22 +422,68 @@ selector_is_reduced({value, _Value}) ->
selector_is_reduced({decisions, _Decisions}) ->
not_reduced.
-spec validate_wallet_currency(currency_id(), wallet_terms()) ->
-spec validate_wallet_terms_currency(currency_id(), wallet_terms()) ->
{ok, valid} | {error, currency_validation_error()}.
validate_wallet_currency(CurrencyID, Terms) ->
validate_wallet_terms_currency(CurrencyID, Terms) ->
#domain_WalletServiceTerms{
currencies = {value, Currencies}
} = Terms,
validate_currency(CurrencyID, Currencies).
-spec validate_withdrawal_currency(currency_id(), withdrawal_terms()) ->
-spec validate_wallet_limits(ff_account:account(), terms()) ->
{ok, valid} | {error, cash_range_validation_error()}.
validate_wallet_limits(Account, #domain_TermSet{wallets = WalletTerms}) ->
%% TODO add turnover validation here
do(fun () ->
valid = unwrap(validate_wallet_limits_terms_is_reduced(WalletTerms)),
#domain_WalletServiceTerms{
wallet_limit = {value, CashRange}
} = WalletTerms,
{Amounts, CurrencyID} = unwrap(ff_transaction:balance(
ff_account:accounter_account_id(Account)
)),
ExpMinCash = encode_cash({ff_indef:expmin(Amounts), CurrencyID}),
ExpMaxCash = encode_cash({ff_indef:expmax(Amounts), CurrencyID}),
valid = unwrap(validate_cash_range(ExpMinCash, CashRange)),
valid = unwrap(validate_cash_range(ExpMaxCash, CashRange))
end).
-spec validate_wallet_limits_terms_is_reduced(wallet_terms()) ->
{ok, valid} | {error, {invalid_terms, _Details}}.
validate_wallet_limits_terms_is_reduced(Terms) ->
#domain_WalletServiceTerms{
wallet_limit = WalletLimitSelector
} = Terms,
do_validate_terms_is_reduced([
{wallet_limit, WalletLimitSelector}
]).
-spec validate_withdrawal_wallet_currency(currency_id(), ff_account:account()) ->
{ok, valid} | {error, withdrawal_currency_error()}.
validate_withdrawal_wallet_currency(CurrencyID, Account) ->
case ff_account:currency(Account) of
CurrencyID ->
{ok, valid};
OtherCurrencyID ->
{error, {invalid_withdrawal_currency, CurrencyID, {wallet_currency, OtherCurrencyID}}}
end.
-spec validate_withdrawal_terms_currency(currency_id(), withdrawal_terms()) ->
{ok, valid} | {error, currency_validation_error()}.
validate_withdrawal_currency(CurrencyID, Terms) ->
validate_withdrawal_terms_currency(CurrencyID, Terms) ->
#domain_WithdrawalServiceTerms{
currencies = {value, Currencies}
} = Terms,
validate_currency(CurrencyID, Currencies).
-spec validate_withdrawal_cash_limit(cash(), withdrawal_terms()) ->
{ok, valid} | {error, cash_range_validation_error()}.
validate_withdrawal_cash_limit(Cash, Terms) ->
#domain_WithdrawalServiceTerms{
cash_limit = {value, CashRange}
} = Terms,
validate_cash_range(encode_cash(Cash), CashRange).
-spec validate_currency(currency_id(), ordsets:ordset(currency_ref())) ->
{ok, valid} | {error, currency_validation_error()}.
validate_currency(CurrencyID, Currencies) ->
@ -441,6 +495,29 @@ validate_currency(CurrencyID, Currencies) ->
{error, {terms_violation, {not_allowed_currency, {CurrencyID, Currencies}}}}
end.
-spec validate_cash_range(domain_cash(), cash_range()) ->
{ok, valid} | {error, cash_range_validation_error()}.
validate_cash_range(Cash, CashRange) ->
case is_inside(Cash, CashRange) of
true ->
{ok, valid};
_ ->
{error, {terms_violation, {cash_range, {Cash, CashRange}}}}
end.
is_inside(Cash, #domain_CashRange{lower = Lower, upper = Upper}) ->
compare_cash(fun erlang:'>'/2, Cash, Lower) andalso
compare_cash(fun erlang:'<'/2, Cash, Upper).
compare_cash(_Fun, V, {inclusive, V}) ->
true;
compare_cash(
Fun,
#domain_Cash{amount = A, currency = C},
{_, #domain_Cash{amount = Am, currency = C}}
) ->
Fun(A, Am).
%% Domain cash flow unmarshalling
-spec decode_domain_postings(ff_cash_flow:domain_plan_postings()) ->
@ -496,7 +573,7 @@ decode_rounding_method(RoundingMethod) ->
decode_rational(#'Rational'{p = P, q = Q}) ->
genlib_rational:new(P, Q).
-spec decode_domain_cash(dmsl_domain_thrift:'Cash'()) ->
-spec decode_domain_cash(domain_cash()) ->
ff_cash_flow:cash().
decode_domain_cash(
#domain_Cash{
@ -527,7 +604,7 @@ encode_currency(CurrencyID) ->
#domain_CurrencyRef{symbolic_code = CurrencyID}.
-spec encode_cash(cash() | undefined) ->
dmsl_domain_thrift:'Cash'() | undefined.
domain_cash() | undefined.
encode_cash(undefined) ->
undefined;
encode_cash({Amount, CurrencyID}) ->

View File

@ -90,7 +90,6 @@ create(ID, IdentityID, Name, CurrencyID) ->
do(fun () ->
Identity = ff_identity_machine:identity(unwrap(identity, ff_identity_machine:get(IdentityID))),
Contract = ff_identity:contract(Identity),
Contract = ff_identity:contract(Identity),
Currency = unwrap(currency, ff_currency:get(CurrencyID)),
Wallet = #{
name => Name,