Skip to content

Commit

Permalink
✨ Include running balance when displaying transaction list
Browse files Browse the repository at this point in the history
Use a SQL window function in a subquery to compute the balance, then select and sort from the subquery to get proper pagination and ordering.
  • Loading branch information
randycoulman committed Jul 21, 2024
1 parent 39b2636 commit 06be5ab
Show file tree
Hide file tree
Showing 5 changed files with 36 additions and 10 deletions.
9 changes: 7 additions & 2 deletions lib/freedom_account/transactions.ex
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ defmodule FreedomAccount.Transactions do
],
exports: [Transaction]

import Ecto.Query, only: [from: 1, subquery: 1]

alias Ecto.Changeset
alias FreedomAccount.Accounts.Account
alias FreedomAccount.Error
Expand Down Expand Up @@ -52,12 +54,15 @@ defmodule FreedomAccount.Transactions do
def list_fund_transactions(%Fund{} = fund, opts \\ []) do
limit = Keyword.get(opts, :per_page, 50)

%Page{entries: transactions, metadata: metadata} =
fund_transactions =
fund
|> LineItem.by_fund()
|> LineItem.join_transaction()
|> FundTransaction.with_running_balances()

%Page{entries: transactions, metadata: metadata} =
from(s in subquery(fund_transactions))
|> FundTransaction.newest_first()
|> FundTransaction.select()
|> Repo.paginate(
after: opts[:next_cursor],
before: opts[:prev_cursor],
Expand Down
22 changes: 15 additions & 7 deletions lib/freedom_account/transactions/fund_transaction.ex
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ defmodule FreedomAccount.Transactions.FundTransaction do
field :id, non_neg_integer()
field :inserted_at, NaiveDateTime.t()
field :memo, String.t()
field :running_balance, Money.t()
end

@spec compare(t(), t()) :: :eq | :gt | :lt
Expand All @@ -27,23 +28,30 @@ defmodule FreedomAccount.Transactions.FundTransaction do
end

@spec cursor_fields :: list()
def cursor_fields, do: [{{:transaction, :date}, :desc}, {{:line_item, :inserted_at}, :desc}, {{:line_item, :id}, :desc}]
def cursor_fields, do: [{:date, :desc}, {:inserted_at, :desc}, {:id, :desc}]

@spec newest_first(Queryable.t()) :: Queryable.t()
def newest_first(query) do
from [line_item: l, transaction: t] in query,
order_by: [desc: t.date, desc: l.inserted_at, desc: l.id]
from t in query,
order_by: [desc: t.date, desc: t.inserted_at, desc: t.id]
end

@spec select(Queryable.t()) :: Queryable.t()
def select(query) do
@spec with_running_balances(Queryable.t()) :: Queryable.t()
def with_running_balances(query) do
from [line_item: l, transaction: t] in query,
select: %__MODULE__{
amount: l.amount,
date: t.date,
id: l.id,
inserted_at: l.inserted_at,
memo: t.memo
}
memo: t.memo,
running_balance: over(sum(l.amount), :fund)
},
windows: [
fund: [
order_by: [t.date, l.inserted_at, l.id],
partition_by: l.fund_id
]
]
end
end
3 changes: 3 additions & 0 deletions lib/freedom_account_web/components/fund_transaction/index.ex
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ defmodule FreedomAccountWeb.FundTransaction.Index do
<:col :let={txn} label="In">
<span :if={Money.positive?(txn.amount)} data-role="deposit"><%= txn.amount %></span>
</:col>
<:col :let={txn} label="Balance">
<%= txn.running_balance %>
</:col>
<:empty_state>
<div id="no-transactions">
This fund has no transactions yet.
Expand Down
10 changes: 9 additions & 1 deletion test/freedom_account/transactions_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -151,10 +151,18 @@ defmodule FreedomAccount.TransactionsTest do
date: transaction.date,
id: line_item.id,
inserted_at: line_item.inserted_at,
memo: transaction.memo
memo: transaction.memo,
running_balance: Money.zero(:usd)
}
end)
|> Enum.sort_by(& &1, {:desc, FundTransaction})
|> Enum.reverse()
|> Enum.reduce({[], Money.zero(:usd)}, fn txn, {result, balance} ->
next_balance = Money.add!(balance, txn.amount)
txn_with_balance = %{txn | running_balance: next_balance}
{[txn_with_balance | result], next_balance}
end)
|> elem(0)
end
end

Expand Down
2 changes: 2 additions & 0 deletions test/freedom_account_web/components/fund_transaction_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ defmodule FreedomAccountWeb.FundTransactionTest do
[deposit_line_item] = deposit.line_items
withdrawal = Factory.withdrawal(account, fund)
[withdrawal_line_item] = withdrawal.line_items
balance = Money.add!(deposit_line_item.amount, withdrawal_line_item.amount)

conn
|> visit(~p"/funds/#{fund}")
Expand All @@ -28,6 +29,7 @@ defmodule FreedomAccountWeb.FundTransactionTest do
|> assert_has(table_cell(), text: "#{withdrawal.date}")
|> assert_has(table_cell(), text: withdrawal.memo)
|> assert_has(role("withdrawal"), text: "#{MoneyUtils.negate(withdrawal_line_item.amount)}")
|> assert_has(table_cell(), text: "#{balance}")
end

test "paginates transactions", %{conn: conn, fund: fund} do
Expand Down

0 comments on commit 06be5ab

Please sign in to comment.