defmodule SoftBank.Account do
@moduledoc """
An Account represents accounts in the system which are of _asset_,
_liability_, or _equity_ types, in accordance with the "accounting equation".
Each account must be set to one of the following types:
| TYPE | NORMAL BALANCE | DESCRIPTION |
| :-------- | :-------------:| :--------------------------------------|
| asset | Debit | Resources owned by the Business Entity |
| liability | Credit | Debts owed to outsiders |
| equity | Credit | Owners rights to the Assets |
Each account can also be marked as a _Contra Account_. A contra account will have it's
normal balance swapped. For example, to remove equity, a "Drawing" account may be created
as a contra equity account as follows:
`account = %SoftBank.Account{name: "Drawing", type: "asset", contra: true}`
At all times the balance of all accounts should conform to the "accounting equation"
*Assets = Liabilities + Owner's Equity*
Each account type acts as it's own ledger.
For more details see:
[Wikipedia - Accounting Equation](http://en.wikipedia.org/wiki/Accounting_equation)
[Wikipedia - Debits, Credits, and Contra Accounts](http://en.wikipedia.org/wiki/Debits_and_credits)
"""
import Kernel, except: [abs: 1]
use SoftBank.Schema
import Ecto.Changeset
import Ecto.Query
alias SoftBank.Owner
alias SoftBank.Repo
alias SoftBank.Amount
alias SoftBank.Account
alias SoftBank.Config
@typedoc "An Account type."
@type t :: %__MODULE__{
name: String.t(),
account_number: String.t(),
type: String.t(),
contra: Boolean.t(),
default_currency: String.t(),
amounts: [SoftBank.Amount]
}
schema "softbank_accounts" do
field(:name, :string)
field(:account_number, :string)
field(:type, :string)
field(:contra, :boolean)
field(:default_currency, :string)
belongs_to(:owner, Owner)
field(:balance, Money.Ecto.Composite.Type, virtual: true)
has_many(:amounts, Amount, on_delete: :delete_all)
has_many(:entry, through: [:amounts, :entry], on_delete: :delete_all)
timestamps()
end
@params ~w(account_number type contra name id default_currency)a
@credit_types ["asset"]
@required_fields ~w(account_number)a
@doc """
Builds a changeset based on the `struct` and `params`.
"""
def changeset(struct, params \\ %{}) do
struct
|> cast(params, @params)
|> validate_required(@required_fields)
end
@doc false
def to_changeset(struct, params \\ %{}) do
struct
|> cast(params, @params)
end
@doc """
Create new account with default ledgers
"""
def new(owner) do
default_currency = Config.get(:default_currency, :USD)
new(owner, default_currency)
end
def new(owner, currency) do
name = owner.name
currency =
case currency do
:default -> Config.get(:default_currency, :USD)
_ -> currency
end
known? = Cldr.Currency.known_currency_code?(currency)
case known? do
true ->
currency = to_string(currency)
asset_struct = %{name: name <> " Assets", type: "asset", default_currency: currency}
account_number = bank_account_number()
config = SoftBank.Config.new()
query =
%Account{}
|> Account.to_changeset(asset_struct)
|> put_change(:account_number, account_number)
|> put_assoc(:owner, owner)
|> validate_required(@required_fields)
{_, debit_account} = config |> Repo.insert(query)
liablilty_struct = %{
name: name <> " Liabilities",
type: "liability",
default_currency: currency
}
account_number = bank_account_number()
query =
%Account{}
|> Account.to_changeset(liablilty_struct)
|> put_change(:account_number, account_number)
|> put_assoc(:owner, owner)
|> validate_required(@required_fields)
{_, credit_account} = config |> Repo.insert(query)
equity_struct = %{name: name <> " Equity", type: "equity", default_currency: currency}
account_number = bank_account_number()
query =
%Account{}
|> Account.to_changeset(equity_struct)
|> put_change(:account_number, account_number)
|> put_assoc(:owner, owner)
|> validate_required(@required_fields)
{_, equity_account} = config |> Repo.insert(query)
%{
debit_account: debit_account,
credit_account: credit_account,
equity_account: equity_account
}
false ->
{:error, "unknown currency"}
end
end
@doc false
@spec amount_sum(Ecto.Repo.t(), SoftBank.Account.t(), String.t()) :: Decimal.t()
def amount_sum(repo, account, type) do
config = SoftBank.Config.new()
query =
Amount
|> Amount.for_account(account)
|> Amount.select_type(type)
records = config |> repo.all(query)
default_currency = account.default_currency
default_currency = String.to_atom(default_currency)
latest_rates = Money.ExchangeRates.latest_rates()
rates =
case(latest_rates) do
{:error, _rates} -> []
{:ok, rates} -> rates
end
default_records =
Enum.map(records, fn x ->
Money.to_currency!(x, default_currency, rates)
end)
new_amt = Money.new(default_currency, 0)
reply =
Enum.reduce(default_records, new_amt, fn r, acc ->
{_, new_amt} = Money.add(r, acc)
new_amt
end)
reply
end
@doc false
@spec amount_sum(Ecto.Repo.t(), SoftBank.Account.t(), String.t(), map) :: Decimal.t()
def amount_sum(repo, account, type, dates) do
config = SoftBank.Config.new()
query =
Amount
|> Amount.for_account(account)
|> Amount.dated(dates)
|> Amount.select_type(type)
records = config |> repo.all(query)
default_currency = account.default_currency
default_currency = String.to_atom(default_currency)
latest_rates = Money.ExchangeRates.latest_rates()
rates =
case(latest_rates) do
{:error, _rates} -> []
{:ok, rates} -> rates
end
default_records =
Enum.map(records, fn x ->
Money.to_currency!(x, default_currency, rates)
end)
new_amt = Money.new(default_currency, 0)
reply =
Enum.reduce(default_records, new_amt, fn r, acc ->
{_, new_amt} = Money.add(r, acc)
new_amt
end)
reply
end
@doc """
Computes the account balance for a given `SoftBank.Account` in a given
Ecto.Repo when provided with a map of dates in the format
`%{from_date: from_date, to_date: to_date}`.
Returns Decimal type.
"""
@spec balance(Ecto.Repo.t(), [SoftBank.Account.t()], Ecto.Date.t()) :: Decimal.t()
def account_balance(repo \\ Repo, account_or_account_list, dates \\ nil) do
balance(repo, account_or_account_list, dates)
end
@doc """
Computes the account balance for a list of `SoftBank.Account` in a given
Ecto.Repo inclusive of all entries. This function is intended to be used with a
list of `SoftBank.Account`s of the same type.
Returns Decimal type.
"""
# Balance for individual account with dates
def balance(
repo,
account = %Account{
account_number: _account_number,
type: type,
contra: contra,
default_currency: _default_currency
},
dates
)
when is_nil(dates) do
credits = Account.amount_sum(repo, account, "credit")
debits = Account.amount_sum(repo, account, "debit")
credits =
case is_nil(credits) do
true ->
data = Money.new(:USD, 0)
[data]
false ->
credits
end
debits =
case is_nil(debits) do
true ->
data = Money.new(:USD, 0)
[data]
false ->
debits
end
if type in @credit_types && !contra do
Money.sub(debits, credits)
else
Money.sub(credits, debits)
end
end
@doc false
def balance(
repo,
account = %Account{
account_number: _account_number,
type: type,
contra: contra,
default_currency: _default_currency
},
dates
) do
credits = Account.amount_sum(repo, account, "credit", dates)
debits = Account.amount_sum(repo, account, "debit", dates)
credits =
case is_nil(credits) do
true ->
data = Money.new(:USD, 0)
[data]
false ->
credits
end
debits =
case is_nil(debits) do
true ->
data = Money.new(:USD, 0)
[data]
false ->
debits
end
if type in @credit_types && !contra do
Money.sub(debits, credits)
else
Money.sub(credits, debits)
end
end
@doc false
def balance(repo, accounts, dates) when is_list(accounts) do
new_amt = Money.new(:USD, 0)
balance =
Enum.reduce(accounts, new_amt, fn account, acc ->
{_, money} = Account.balance(repo, account, dates)
{_, new_amt} = Money.add(money, acc)
new_amt
end)
balance
end
def bank_account_number(number \\ 12) do
Nanoid.generate(number, "0123456789")
end
@doc """
Fetch the Account from the Repo.
"""
def fetch(account, repo \\ Repo)
def fetch(%{account_number: account_number}, repo) do
config = SoftBank.Config.new()
query =
Account
|> where([a], a.account_number == ^account_number)
|> select([a], %Account{
account_number: a.account_number,
type: a.type,
contra: a.contra,
id: a.id,
default_currency: a.default_currency
})
config |> repo.one(query)
end
@doc """
Computes a test balance for all accounts in the provided Ecto.Repo.
Returns Money type.
"""
def test_balance(repo \\ Repo) do
config = SoftBank.Config.new()
accounts = repo.all(config, Account)
default_currency = Config.get(:default_currency, :USD)
case Enum.count(accounts) > 0 do
true ->
accounts_by_type = Enum.group_by(accounts, fn i -> String.to_atom(i.type) end)
accounts_by_type =
Enum.map(accounts_by_type, fn {account_type, accounts} ->
{account_type, Account.account_balance(repo, accounts)}
end)
accounts_by_type[:asset]
|> Money.sub!(accounts_by_type[:liability])
|> Money.sub!(accounts_by_type[:equity])
false ->
Money.new(default_currency, 0)
end
end
end