Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(transfers): add created_by filter to transfers page #339

Merged
merged 4 commits into from
Jan 12, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 33 additions & 19 deletions apps/app/lib/app/transfers.ex
Original file line number Diff line number Diff line change
Expand Up @@ -77,16 +77,18 @@ defmodule App.Transfers do
## Filters

@filters_default %{
sort_by: :most_recent,
tenanted_by: nil
tenanted_by: nil,
created_by: nil,
sort_by: :most_recent
}
@filters_types %{
# Values are `nil`, `member_id` or `{:not, member_id}`
tenanted_by: :any,
created_by: {:array, :integer},
sort_by:
Ecto.ParameterizedType.init(Ecto.Enum,
values: [:most_recent, :oldest, :last_created, :first_created]
),
# Values are `nil`, `member_id` or `{:not, member_id}`
tenanted_by: :any
)
}

defp filter_money_transfers_query(query, raw_filters) do
Expand All @@ -96,10 +98,28 @@ defmodule App.Transfers do
|> Ecto.Changeset.apply_changes()

query
|> sort_money_transfers_by(filters[:sort_by])
|> filter_money_transfers_by_tenancy(filters[:tenanted_by])
|> filter_money_transfers_by_creator(filters[:created_by])
|> sort_money_transfers_by(filters[:sort_by])
end

defp filter_money_transfers_by_tenancy(query, {:not, member_id}) when is_integer(member_id) do
from [money_transfer: money_transfer] in query, where: money_transfer.tenant_id != ^member_id
end

defp filter_money_transfers_by_tenancy(query, member_id) when is_integer(member_id) do
from [money_transfer: money_transfer] in query, where: money_transfer.tenant_id == ^member_id
end

defp filter_money_transfers_by_tenancy(query, nil), do: query

defp filter_money_transfers_by_creator(query, creator_ids) when is_list(creator_ids) do
from [money_transfer: money_transfer] in query,
where: money_transfer.creator_id in ^creator_ids
end

defp filter_money_transfers_by_creator(query, nil), do: query

defp sort_money_transfers_by(query, :most_recent) do
from [money_transfer: money_transfer] in query, order_by: [desc: money_transfer.date]
end
Expand All @@ -116,16 +136,6 @@ defmodule App.Transfers do
from [money_transfer: money_transfer] in query, order_by: [asc: money_transfer.inserted_at]
end

defp filter_money_transfers_by_tenancy(query, {:not, member_id}) when is_integer(member_id) do
from [money_transfer: money_transfer] in query, where: money_transfer.tenant_id != ^member_id
end

defp filter_money_transfers_by_tenancy(query, member_id) when is_integer(member_id) do
from [money_transfer: money_transfer] in query, where: money_transfer.tenant_id == ^member_id
end

defp filter_money_transfers_by_tenancy(query, nil), do: query

## Pagination

defp paginate_query(query, offset, limit) do
Expand Down Expand Up @@ -197,12 +207,16 @@ defmodule App.Transfers do
@doc """
Creates a money_transfer.
"""
@spec create_money_transfer(Book.t(), MoneyTransfer.type(), map()) ::
@spec create_money_transfer(Book.t(), BookMember.t(), MoneyTransfer.type(), map()) ::
{:ok, MoneyTransfer.t()} | {:error, Ecto.Changeset.t()}
def create_money_transfer(%Book{} = book, type, attrs)
def create_money_transfer(%Book{} = book, %BookMember{} = creator, type, attrs)
when is_map(attrs) and type in [:payment, :income] do
changeset =
%MoneyTransfer{book_id: book.id, type: type}
%MoneyTransfer{
book_id: book.id,
creator_id: creator.id,
type: type
}
|> MoneyTransfer.changeset(attrs)

result =
Expand Down
7 changes: 5 additions & 2 deletions apps/app/lib/app/transfers/money_transfer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@ defmodule App.Transfers.MoneyTransfer do
amount: Money.t(),
type: type(),
date: Date.t(),
book: Book.t(),
book_id: Book.id(),
creator_id: BookMember.id(),
tenant: BookMember.t(),
tenant_id: BookMember.id(),
balance_params: TransferParams.t(),
peers: [Peer.t()],
total_peer_weight: Decimal.t(),
Expand All @@ -41,7 +43,8 @@ defmodule App.Transfers.MoneyTransfer do
field :type, Ecto.Enum, values: @transfer_types
field :date, :date, read_after_writes: true

belongs_to :book, Book
field :book_id, :integer
field :creator_id, :integer
belongs_to :tenant, BookMember

# balance
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
defmodule App.Repo.Migrations.BackfillMoneyTransfersCreatorId do
use Ecto.Migration

import Ecto.Query

@disable_ddl_transaction true
@disable_migration_lock true
@batch_size 50
@throttle_ms 500

def up do
throttle_change_in_batches(&page_query/0, &do_change/1)
end

def down, do: :ok

defp do_change(batch_of_ids) do
from(t in "money_transfers",
where: t.id in ^batch_of_ids,
update: [set: [creator_id: t.tenant_id]]
)
|> repo().update_all([], log: :info, timeout: :infinity)
end

defp page_query do
from t in "money_transfers",
where: is_nil(t.creator_id),
order_by: [asc: t.id],
limit: @batch_size,
select: t.id
end

defp throttle_change_in_batches(query_fun, change_fun) do
case repo().all(query_fun.(), log: :info, timeout: :infinity) do
[] ->
:ok

ids ->
change_fun.(ids)
Process.sleep(@throttle_ms)
throttle_change_in_batches(query_fun, change_fun)
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
defmodule App.Repo.Migrations.AddMoneyTransfersCreatorId do
use Ecto.Migration

def change do
alter table("money_transfers") do
add :creator_id, references("book_members", on_delete: :nilify_all)
end
end
end
50 changes: 46 additions & 4 deletions apps/app/test/app/transfers_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ defmodule App.TransfersTest do

alias App.Balance.TransferParams
alias App.Books.Book
alias App.Books.BookMember
alias App.Transfers
alias App.Transfers.MoneyTransfer
alias App.Transfers.Peer
Expand Down Expand Up @@ -98,6 +99,23 @@ defmodule App.TransfersTest do
|> Enum.map(& &1.id) == [transfer2.id]
end

test "filters by creator", %{book: book} do
member1 = book_member_fixture(book)
member2 = book_member_fixture(book)
member3 = book_member_fixture(book)

transfer1 = money_transfer_fixture(book, tenant_id: member1.id, creator_id: member1.id)
transfer2 = money_transfer_fixture(book, tenant_id: member1.id, creator_id: member2.id)

assert Transfers.list_transfers_of_book(book, filters: %{created_by: [member1.id]})
|> Enum.map(& &1.id) == [transfer1.id]

assert Transfers.list_transfers_of_book(book, filters: %{created_by: [member2.id]})
|> Enum.map(& &1.id) == [transfer2.id]

assert Transfers.list_transfers_of_book(book, filters: %{created_by: [member3.id]}) == []
end

test "paginates results", %{book: book, member: member} do
transfer1 = money_transfer_fixture(book, tenant_id: member.id, date: ~D[2020-06-29])
transfer2 = money_transfer_fixture(book, tenant_id: member.id, date: ~D[2020-06-30])
Expand Down Expand Up @@ -203,13 +221,14 @@ defmodule App.TransfersTest do
end
end

describe "create_money_transfer/1" do
describe "create_money_transfer/4" do
setup :book_with_member_context

test "creates a money transfer", %{book: book, member: member} do
assert {:ok, transfer} =
Transfers.create_money_transfer(
book,
member,
:payment,
money_transfer_attributes(
tenant_id: member.id,
Expand All @@ -224,6 +243,7 @@ defmodule App.TransfersTest do
assert transfer.type == :payment
assert transfer.date == ~D[2022-06-23]
assert transfer.balance_params == struct!(TransferParams, transfer_params_attributes())
assert transfer.creator_id == member.id

transfer = Repo.preload(transfer, :peers)
assert Enum.empty?(transfer.peers)
Expand All @@ -233,6 +253,7 @@ defmodule App.TransfersTest do
assert {:ok, transfer} =
Transfers.create_money_transfer(
book,
member,
:payment,
money_transfer_attributes(
tenant_id: member.id,
Expand All @@ -254,6 +275,7 @@ defmodule App.TransfersTest do
assert {:ok, transfer} =
Transfers.create_money_transfer(
book,
member,
:payment,
money_transfer_attributes(
tenant_id: member.id,
Expand All @@ -278,6 +300,7 @@ defmodule App.TransfersTest do
assert_raise Ecto.ConstraintError, ~r/transfers_peers_transfer_id_member_id_index/, fn ->
Transfers.create_money_transfer(
book,
member,
:payment,
money_transfer_attributes(
tenant_id: member.id,
Expand All @@ -291,6 +314,18 @@ defmodule App.TransfersTest do
assert_raise Ecto.ConstraintError, ~r/money_transfers_book_id_fkey/, fn ->
Transfers.create_money_transfer(
%Book{id: 0},
member,
:payment,
money_transfer_attributes(tenant_id: member.id)
)
end
end

test "fails with invalid creator_id", %{book: book, member: member} do
assert_raise Ecto.ConstraintError, ~r/money_transfers_creator_id_fkey/, fn ->
Transfers.create_money_transfer(
book,
%BookMember{id: 0},
:payment,
money_transfer_attributes(tenant_id: member.id)
)
Expand All @@ -301,23 +336,30 @@ defmodule App.TransfersTest do
assert_raise FunctionClauseError, fn ->
Transfers.create_money_transfer(
book,
member,
:reimbursement,
money_transfer_attributes(tenant_id: member.id)
)
end
end

test "fails with missing tenant_id", %{book: book} do
test "fails with missing tenant_id", %{book: book, member: member} do
assert {:error, changeset} =
Transfers.create_money_transfer(book, :payment, money_transfer_attributes())
Transfers.create_money_transfer(
book,
member,
:payment,
money_transfer_attributes()
)

assert errors_on(changeset) == %{tenant_id: ["can't be blank"]}
end

test "fails with invalid tenant_id", %{book: book} do
test "fails with invalid tenant_id", %{book: book, member: member} do
assert {:error, changeset} =
Transfers.create_money_transfer(
book,
member,
:payment,
money_transfer_attributes(tenant_id: 0)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -425,9 +425,9 @@ defmodule AppWeb.BookTransferFormLive do
end

defp submit_form(socket, :new, money_transfer_params) do
%{book: book, type: type} = socket.assigns
%{book: book, current_member: current_member, type: type} = socket.assigns

case Transfers.create_money_transfer(book, type, money_transfer_params) do
case Transfers.create_money_transfer(book, current_member, type, money_transfer_params) do
{:ok, _money_transfer} ->
{:noreply, push_navigate(socket, to: ~p"/books/#{book}/transfers")}

Expand Down
19 changes: 19 additions & 0 deletions apps/app_web/lib/app_web/live/books/book_transfers_live.ex
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ defmodule AppWeb.BookTransfersLive do
others: gettext("Others")
]
),
multi_select(
name: "created_by",
label: gettext("Created by"),
options: @book_members_options
),
sort_by(
options: [
most_recent: gettext("Most recent"),
Expand Down Expand Up @@ -122,6 +127,7 @@ defmodule AppWeb.BookTransfersLive do
page: 1,
per_page: 25
)
|> assign_book_members_options()
|> assign_filters(%{"sort_by" => "most_recent"})
|> paginate_transfers(1)

Expand Down Expand Up @@ -209,6 +215,19 @@ defmodule AppWeb.BookTransfersLive do
{:noreply, socket}
end

defp assign_book_members_options(socket) do
book = socket.assigns.book

book_members_options =
from(book_member in BookMember.book_query(book),
order_by: [asc: book_member.nickname],
select: {book_member.id, book_member.nickname}
)
|> Repo.all()

assign(socket, :book_members_options, book_members_options)
end

defp assign_filters(socket, filters) do
current_member = socket.assigns.current_member

Expand Down
4 changes: 4 additions & 0 deletions apps/app_web/priv/gettext/default.pot
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,10 @@ msgstr ""
msgid "Create reimbursement"
msgstr ""

#, elixir-autogen, elixir-format
msgid "Created by"
msgstr ""

#, elixir-autogen, elixir-format
msgid "Current password"
msgstr ""
Expand Down
4 changes: 4 additions & 0 deletions apps/app_web/priv/gettext/en/LC_MESSAGES/default.po
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,10 @@ msgstr "Create manually"
msgid "Create reimbursement"
msgstr "Create reimbursement"

#, elixir-autogen, elixir-format
msgid "Created by"
msgstr "Created by"

#, elixir-autogen, elixir-format
msgid "Current password"
msgstr "Current password"
Expand Down
4 changes: 4 additions & 0 deletions apps/app_web/priv/gettext/fr/LC_MESSAGES/default.po
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,10 @@ msgstr "Créer manuellement"
msgid "Create reimbursement"
msgstr "Créer le remboursement"

#, elixir-autogen, elixir-format
msgid "Created by"
msgstr "Créé par"

#, elixir-autogen, elixir-format
msgid "Current password"
msgstr "Mot de passe actuel"
Expand Down