mirror of
https://github.com/valitydev/fistful-server.git
synced 2024-11-06 10:45:21 +00:00
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:
parent
1b8c6a13b8
commit
f1e3355dca
2
.gitmodules
vendored
2
.gitmodules
vendored
@ -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
|
||||
|
@ -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),
|
||||
do(fun () ->
|
||||
valid = validate_wallet_limits(WalletID, Body, WalletAccount),
|
||||
#{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)),
|
||||
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)
|
||||
},
|
||||
do(fun () ->
|
||||
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)).
|
||||
|
@ -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) ->
|
||||
|
@ -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}) ->
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user