accounts.erl 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. -module(accounts).
  2. -include_lib("kvs/include/accounts.hrl").
  3. -include_lib("kvs/include/membership_packages.hrl").
  4. -include_lib("kvs/include/log.hrl").
  5. -export([debet/2, credit/2, balance/2, create_account/1, create_account/2]).
  6. -export([transaction/4, transaction/5]).
  7. -export([check_quota/1, check_quota/2]).
  8. -export([user_paid/1]).
  9. -spec transaction(string(), currency(), integer(), transaction_info()) -> {ok, transaction_id()} | {error, term()}.
  10. transaction(Account, Currency, 0, TransactionInfo) ->
  11. ?WARNING("zero transaction: Account=~p, Currency=~p,TransactionInfo=~p", [Account, Currency, TransactionInfo]),
  12. ok;
  13. transaction(Account, Currency, Amount, TransactionInfo) when Amount /= 0->
  14. {Remitter, Acceptor} = if Amount > 0 -> {system, Account};
  15. true -> {Account, system} end,
  16. transaction(Remitter, Acceptor, Currency, abs(Amount), TransactionInfo).
  17. %% @doc Add new transaction, will change state of accouts. All transactions
  18. %% are between user account and system account. So moving of the funds
  19. %% (money, kaush, game_points, etc.) should be performed with transactions.
  20. -spec transaction(Remitter::string(), Acceptor::string(), Currency::currency(), Amount::integer(),
  21. TransactionInfo::transaction_info()) -> {ok, transaction_id()} | {error, any()}.
  22. transaction(Remitter, Acceptor, Currency, Amount, TransactionInfo) ->
  23. TX = #transaction{id = generate_id(), acceptor = Acceptor, remitter = Remitter,
  24. amount = Amount, commit_time = now(), currency = Currency, info = TransactionInfo},
  25. commit_transaction(TX).
  26. %% @doc How many funds got from the start of the time.
  27. -spec debet(account_id(), currency()) -> {ok, integer()} | {error, term()}.
  28. debet(Account, Currency) ->
  29. case get_account(Account, Currency) of
  30. #account{debet = Debet} -> {ok, Debet};
  31. Error -> Error
  32. end.
  33. %% @doc How many funds spent from the start of the time.
  34. -spec credit(account_id(), currency()) -> {ok, integer()} | {error, term()}.
  35. credit(AccountId, Currency) ->
  36. case get_account(AccountId, Currency) of
  37. #account{credit = Credit} -> {ok, Credit};
  38. Error -> Error
  39. end.
  40. %% @doc balance of the given account by given currency
  41. -spec balance(account_id(), currency()) -> {ok, integer()} | {error, term()}.
  42. balance(AccountId, Currency) ->
  43. case get_account(AccountId, Currency) of
  44. #account{debet = Debet, credit = Credit} -> {ok, Debet - Credit};
  45. Error -> Error
  46. end.
  47. %% @doc Create account with for all supported currencies.
  48. -spec create_account(Username::string()|system) -> ok | {error, term()}.
  49. create_account(AccountId) ->
  50. Currencies = get_currencies(),
  51. try [{ok, Currency} = {create_account(AccountId, Currency), Currency} || Currency <- Currencies]
  52. catch _:_ -> {error, unable_create_account} end.
  53. %% @doc Create account for specified currency.
  54. create_account(AccountId, Currency) ->
  55. Account = #account{id = {AccountId, Currency},
  56. credit = 0, debet = 0, last_change = 0},
  57. case kvs:put(Account) of
  58. ok -> ok;
  59. Error -> ?ERROR("create_account: put to db error: ~p", [Error]),
  60. {error, unable_to_store_account}
  61. end.
  62. %% @doc Check quota balance against hard and soft limits
  63. -spec check_quota(Username::string()) -> ok | {error, soft_limit} | {error, hard_limit}.
  64. check_quota(User) ->
  65. check_quota(User, 0).
  66. %% @doc Check quota balance against hard and soft limit. Second argument is
  67. %% Amount planning to be cherged from user.
  68. -spec check_quota(User::string(), Amount::integer()) -> ok | {error, soft_limit} | {error, hard_limit}.
  69. check_quota(User, Amount) ->
  70. SoftLimit = kvs:get_config("accounts/quota_limit/soft", -20),
  71. {ok, Balance} = balance(User, quota),
  72. BalanceAfterChange = Balance - Amount,
  73. if
  74. BalanceAfterChange > SoftLimit ->
  75. ok;
  76. true ->
  77. HardLimit = kvs:get(config, "accounts/quota_limit/hard", -100),
  78. if
  79. BalanceAfterChange =< HardLimit ->
  80. {error, hard_limit};
  81. true ->
  82. {error, soft_limit}
  83. end
  84. end.
  85. commit_transaction(#transaction{remitter = R, acceptor = A, currency = Currency, amount = Amount} = TX) ->
  86. case change_accounts(R, A, Currency, Amount) of
  87. ok -> nsx_msg:notify_transaction(R,TX),
  88. nsx_msg:notify_transaction(A,TX);
  89. Error -> skip
  90. % case TX#transaction.info of
  91. % #tx_game_event{} ->
  92. % nsx_msg:notify_transaction(R,TX),
  93. % nsx_msg:notify_transaction(A,TX);
  94. % _ ->
  95. % ?ERROR("commit transaction error: change accounts ~p", [Error]),
  96. % Error
  97. % end
  98. end.
  99. change_accounts(Remitter, Acceptor, Currency, Amount) ->
  100. case {get_account(Remitter, Currency), get_account(Acceptor, Currency)} of
  101. {RA = #account{}, AA = #account{}} ->
  102. ?INFO("transacrion: RemitterAccount ~p, AcceptorAccount: ~p", [RA, AA]),
  103. %% check balance for remitter according to currency and amount
  104. case check_remitter_balance(RA, Amount) of
  105. %% all ok write changes
  106. ok -> %% increase credit of remmitter, last change is less then zero
  107. RA1 = RA#account{credit = RA#account.credit + Amount,
  108. last_change = -Amount },
  109. %% increase debet of acceptor, last change is positive
  110. AA1 = AA#account{debet = AA#account.debet + Amount,
  111. last_change = Amount},
  112. kvs:put([AA1, RA1]);
  113. {error, Reason} ->
  114. {error, {remitter_balance, Reason}}
  115. end;
  116. {{error, Reason}, #account{}} ->
  117. {error, {remitter_account_unavailable, Reason}};
  118. {#account{}, {error, Reason}} ->
  119. {error, {acceptor_account_unavailable, Reason}};
  120. {RE, AE} ->
  121. {error, both_accounts_unavailable}
  122. end.
  123. check_remitter_balance(#account{id = {system, _}}, _) -> ok;
  124. check_remitter_balance(_Account, _Amount) -> ok.
  125. get_account(Account, Currency) ->
  126. case kvs:get(account, {Account, Currency}) of
  127. {ok, #account{} = AR} -> AR;
  128. _ -> {error, account_not_found}
  129. end.
  130. get_currencies() -> [internal,
  131. currency,
  132. money,
  133. quota,
  134. points].
  135. generate_id() ->
  136. {MegSec, Sec, MicroSec} = now(),
  137. H = erlang:phash2(make_ref()),
  138. lists:concat([MegSec*1000000000000, Sec*1000000, MicroSec, "-", H]).
  139. user_paid(UId) ->
  140. {_, UP} = kvs:get(user_purchase, UId),
  141. case UP of
  142. notfound -> false;
  143. #user_purchase{top = undefined} -> false;
  144. _ -> true
  145. end.