Skip to content

Commit

Permalink
feat: add set revenues page (#286)
Browse files Browse the repository at this point in the history
  • Loading branch information
Gladear committed Oct 8, 2024
1 parent f406517 commit 382da2a
Show file tree
Hide file tree
Showing 24 changed files with 1,052 additions and 210 deletions.
4 changes: 2 additions & 2 deletions apps/app/lib/app/balance/balance_config.ex
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,9 @@ defmodule App.Balance.BalanceConfig do
timestamps()
end

def changeset(struct, attrs) do
def revenues_changeset(struct, attrs) do
struct
|> cast(attrs, [:owner_id, :annual_income])
|> cast(attrs, [:annual_income])
|> validate_annual_income()
end

Expand Down
70 changes: 64 additions & 6 deletions apps/app/lib/app/balance/balance_configs.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,23 @@ defmodule App.Balance.BalanceConfigs do
"""
import Ecto.Query

alias App.Accounts.User
alias App.Balance.BalanceConfig
alias App.Books.BookMember
alias App.Repo
alias App.Transfers.Peer

@doc """
Get the balance configuration of a member.
Returns `nil` if the member has no balance configuration.
"""
@spec get_balance_config_of_member(BookMember.t()) :: BalanceConfig.t() | nil
def get_balance_config_of_member(%BookMember{balance_config_id: nil} = _member), do: nil

def get_balance_config_of_member(%BookMember{} = member) do
Repo.get!(BalanceConfig, member.balance_config_id)
end

@doc """
Check if a member has revenues set in their balance configuration.
Expand All @@ -27,12 +41,36 @@ defmodule App.Balance.BalanceConfigs do
|> Repo.one!()
end

@doc """
Try to delete a balance configuration. If the balance configuration is linked to
an entity, this will fail silently.
"""
@spec try_to_delete_balance_config(BalanceConfig.t()) :: :ok
def try_to_delete_balance_config(%BalanceConfig{} = balance_config) do
## Update revenues

@spec create_balance_config(BookMember.t(), User.t(), map()) ::
{:ok, BalanceConfig.t()} | {:error, Ecto.Changeset.t()}
def create_balance_config(%BookMember{} = member, %User{} = owner, attrs) do
former_balance_config = get_balance_config_of_member(member)

changeset =
%BalanceConfig{owner_id: owner.id}
|> BalanceConfig.revenues_changeset(attrs)

result =
Ecto.Multi.new()
|> Ecto.Multi.insert(:balance_config, changeset)
|> Ecto.Multi.update(:member, fn %{balance_config: balance_config} ->
BookMember.change_balance_config(member, balance_config)
end)
|> Ecto.Multi.run(:delete_old_config, fn _repo, _changes ->
if former_balance_config, do: try_to_delete_balance_config(former_balance_config)
{:ok, nil}
end)
|> Repo.transaction()

case result do
{:ok, %{balance_config: balance_config}} -> {:ok, balance_config}
{:error, :balance_config, changeset, _changes} -> {:error, changeset}
end
end

defp try_to_delete_balance_config(%BalanceConfig{} = balance_config) do
balance_config
|> Ecto.Changeset.cast(%{}, [])
|> Ecto.Changeset.foreign_key_constraint(:book_members,
Expand All @@ -42,7 +80,27 @@ defmodule App.Balance.BalanceConfigs do
name: "transfers_peers_balance_config_id_fkey"
)
|> Repo.delete(mode: :savepoint)
end

@doc """
Link a balance configuration to a list of peers.
"""
@spec link_balance_config_to_peers(BalanceConfig.t(), [Peer.t()]) :: :ok
def link_balance_config_to_peers(%BalanceConfig{} = balance_config, peers) do
peer_ids = Enum.map(peers, & &1.id)

{_, nil} =
from([peer: peer] in Peer.base_query(), where: peer.id in ^peer_ids)
|> Repo.update_all(set: [balance_config_id: balance_config.id])

:ok
end

@doc """
Return an `%Ecto.Changeset{}` for tracking changes to a balance config annual income.
"""
@spec change_balance_config_revenues(BalanceConfig.t(), map()) :: Ecto.Changeset.t()
def change_balance_config_revenues(balance_config, attrs \\ %{}) do
BalanceConfig.revenues_changeset(balance_config, attrs)
end
end
6 changes: 6 additions & 0 deletions apps/app/lib/app/books/book_member.ex
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ defmodule App.Books.BookMember do

## Changesets

@spec nickname_changeset(t(), map()) :: Ecto.Changeset.t()
def nickname_changeset(struct, attrs) do
struct
|> cast(attrs, [:nickname])
Expand All @@ -69,6 +70,11 @@ defmodule App.Books.BookMember do
|> validate_length(:nickname, min: 1, max: 255)
end

@spec change_balance_config(t(), BalanceConfig.t()) :: Ecto.Changeset.t()
def change_balance_config(struct, %BalanceConfig{} = balance_config) do
change(struct, balance_config_id: balance_config.id)
end

## Queries

@doc """
Expand Down
11 changes: 3 additions & 8 deletions apps/app/lib/app/books/members.ex
Original file line number Diff line number Diff line change
Expand Up @@ -126,14 +126,9 @@ defmodule App.Books.Members do
"""
@spec link_book_member_to_user(BookMember.t(), User.t()) :: :ok
def link_book_member_to_user(book_member, user) do
{:ok, _results} =
Ecto.Multi.new()
|> Ecto.Multi.update_all(
:book_member,
from(BookMember, where: [id: ^book_member.id]),
set: [user_id: user.id]
)
|> Repo.transaction()
{1, nil} =
from(BookMember, where: [id: ^book_member.id])
|> Repo.update_all(set: [user_id: user.id])

:ok
end
Expand Down
4 changes: 4 additions & 0 deletions apps/app/lib/app/transfers/money_transfer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ defmodule App.Transfers.MoneyTransfer do
foreign_key: :transfer_id,
on_replace: :delete_if_exists

# The current peer is used in some operations, like when updating the revenues,
# in the "transfers" step, to know the link between the current member and the transfer.
field :current_peer, :map, virtual: true

# Sum of all the peer `:total_weight`. Depends on the transfer balance means
field :total_peer_weight, :decimal, virtual: true

Expand Down
20 changes: 7 additions & 13 deletions apps/app/test/app/balance/balance_config_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -10,28 +10,22 @@ defmodule App.Balance.BalanceConfigTest do
%{user: user_fixture()}
end

describe "changeset/2" do
describe "revenues_changeset/2" do
test "allows valid `:annual_income`", %{user: user} do
changeset =
BalanceConfig.changeset(
%BalanceConfig{},
balance_config_attributes(
owner_id: user.id,
annual_income: 0
)
BalanceConfig.revenues_changeset(
%BalanceConfig{owner_id: user.id},
balance_config_attributes(annual_income: 0)
)

assert changeset.valid?
end

test "does not allow negative `:annual_income`", %{user: user} do
changeset =
BalanceConfig.changeset(
%BalanceConfig{},
balance_config_attributes(
owner_id: user.id,
annual_income: -1
)
BalanceConfig.revenues_changeset(
%BalanceConfig{owner_id: user.id},
balance_config_attributes(annual_income: -1)
)

refute changeset.valid?
Expand Down
106 changes: 78 additions & 28 deletions apps/app/test/app/balance/balance_configs_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,23 @@ defmodule App.Balance.BalanceConfigsTest do
import App.BooksFixtures
import App.TransfersFixtures

alias App.Balance.BalanceConfig
alias App.Balance.BalanceConfigs

describe "get_balance_config_of_member/1" do
test "returns the balance config of the member" do
%{id: id} = balance_config_fixture()
member = book_member_fixture(book_fixture(), balance_config_id: id)

assert %{id: ^id} = BalanceConfigs.get_balance_config_of_member(member)
end

test "returns nil if the member has no balance configuration" do
member = book_member_fixture(book_fixture())

assert BalanceConfigs.get_balance_config_of_member(member) == nil
end
end

describe "member_has_revenues?/1" do
setup do
%{book: book_fixture()}
Expand All @@ -35,58 +49,94 @@ defmodule App.Balance.BalanceConfigsTest do
end
end

describe "try_to_delete_balance_config/1" do
setup do
%{balance_config: balance_config_fixture()}
describe "create_balance_config/3" do
test "creates a balance configuration" do
member = book_member_fixture(book_fixture())
owner = user_fixture()

assert {:ok, balance_config} =
BalanceConfigs.create_balance_config(member, owner, %{annual_income: 5432})

assert balance_config.owner_id == owner.id
assert balance_config.annual_income == 5432

# updates the member
member = Repo.reload!(member)
assert member.balance_config_id == balance_config.id
end

test "returns an error if the attributes are invalid" do
member = book_member_fixture(book_fixture())
owner = user_fixture()

assert {:error, changeset} =
BalanceConfigs.create_balance_config(member, owner, %{annual_income: -1})

assert errors_on(changeset) == %{annual_income: ["must be greater than or equal to 0"]}
end

test "deletes the balance configuration if it's not linked to any entity", %{
balance_config: balance_config
} do
:ok = BalanceConfigs.try_to_delete_balance_config(balance_config)
test "deletes the former balance configuration if it's not linked to any entity" do
balance_config = balance_config_fixture()
member = book_member_fixture(book_fixture(), balance_config_id: balance_config.id)

{:ok, _} = BalanceConfigs.create_balance_config(member, user_fixture(), %{annual_income: 0})

refute Repo.reload(balance_config)
end

test "does not delete the balance configuration if it's linked to a member", %{
balance_config: balance_config
} do
test "does not delete the balance configuration if it's linked to a member" do
book = book_fixture()

balance_config = balance_config_fixture()
member = book_member_fixture(book, balance_config_id: balance_config.id)

# There is no reason for this case to happen, but better be safe than sorry
_member = book_member_fixture(book, balance_config_id: balance_config.id)

:ok = BalanceConfigs.try_to_delete_balance_config(balance_config)
{:ok, _} = BalanceConfigs.create_balance_config(member, user_fixture(), %{annual_income: 0})

assert Repo.reload(balance_config)
end

test "does not delete the balance configuration if it's linked to a peer", %{
balance_config: balance_config
} do
test "does not delete the balance configuration if it's linked to a peer" do
balance_config = balance_config_fixture()

book = book_fixture()
member = book_member_fixture(book)
member = book_member_fixture(book, balance_config_id: balance_config.id)
transfer = money_transfer_fixture(book, tenant_id: member.id)
_peer = peer_fixture(transfer, member_id: member.id, balance_config_id: balance_config.id)

:ok = BalanceConfigs.try_to_delete_balance_config(balance_config)
{:ok, _} = BalanceConfigs.create_balance_config(member, user_fixture(), %{annual_income: 0})

assert Repo.reload(balance_config)
end
end

test "does not end the transaction on error", %{balance_config: balance_config} do
user = user_fixture()
describe "change_balance_config_revenues/2" do
setup do
%{balance_config: balance_config_fixture()}
end

{:ok, balance_config} =
Repo.transaction(fn ->
book = book_fixture()
_member = book_member_fixture(book, balance_config_id: balance_config.id)
test "returns a changeset", %{balance_config: balance_config} do
assert %Ecto.Changeset{} =
changeset =
BalanceConfigs.change_balance_config_revenues(balance_config, %{
annual_income: 2345
})

:ok = BalanceConfigs.try_to_delete_balance_config(balance_config)
assert changeset.valid?
assert changeset.changes == %{annual_income: 2345}
end

# Try to insert another entity in the database
Repo.insert!(%BalanceConfig{owner_id: user.id})
end)
test "cannot change the owner", %{balance_config: balance_config} do
assert %Ecto.Changeset{} =
changeset =
BalanceConfigs.change_balance_config_revenues(balance_config, %{
owner_id: user_fixture().id
})

assert balance_config.owner_id == user.id
assert changeset.valid?
assert changeset.changes == %{}
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ defmodule App.Balance.BalanceConfigsFixtures do
Ecto.Multi.new()
|> Ecto.Multi.insert(
:balance_config,
BalanceConfig.changeset(
BalanceConfig.revenues_changeset(
%BalanceConfig{},
balance_config_attributes(attrs)
)
Expand Down
12 changes: 0 additions & 12 deletions apps/app_web/assets/js/app_events.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,3 @@
// Navigate back in the browser history
document.addEventListener("app:navigate-back", function (event) {
event.preventDefault();
window.history.back();
});

// Copy the content of the target element to the clipboard
//
// The field to be copied can be specified by the `field` attribute.
Expand All @@ -13,9 +7,3 @@ document.addEventListener("app:copy-to-clipboard", async function (event) {
const copied = dispatcher[field];
await navigator.clipboard.writeText(copied);
});

// Open the target dialog
document.addEventListener("app:open-dialog", (event) => event.target.showModal());

// Close the target dialog
document.addEventListener("app:close-dialog", (event) => event.target.close());
14 changes: 1 addition & 13 deletions apps/app_web/lib/app_web/components/core_components.ex
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ defmodule AppWeb.CoreComponents do
@link_attrs ~w(navigate patch href replace method csrf_token download hreflang referrerpolicy rel target type)

# Attributes of the `<input>` HTML element
@input_attrs ~w(name value checked step)
@input_attrs ~w(name value checked step disabled)

# prepend a class in `[:rest, :class]`
defp prepend_class(assigns, class) do
Expand Down Expand Up @@ -898,18 +898,6 @@ defmodule AppWeb.CoreComponents do
"""
end

## JS Commands

# TODO(v2,end) drop `show_dialog/2` and `hide_dialog/2` helper functions

def show_dialog(js \\ %JS{}, selector) do
JS.dispatch(js, "app:open-dialog", to: selector)
end

def hide_dialog(js \\ %JS{}, selector) do
JS.dispatch(js, "app:close-dialog", to: selector)
end

@doc """
Translates an error message using gettext.
"""
Expand Down
Loading

0 comments on commit 382da2a

Please sign in to comment.