diff --git a/backend/.gitignore b/backend/.gitignore index 55e7abb..63c03ca 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -22,3 +22,5 @@ erl_crash.dump # Ignore package tarball (built via "mix hex.build"). week_budget-*.tar +.elixir_ls + diff --git a/backend/lib/api/server.ex b/backend/lib/api/server.ex index d3eb796..0e04682 100644 --- a/backend/lib/api/server.ex +++ b/backend/lib/api/server.ex @@ -11,6 +11,7 @@ defmodule WeekBudget.API.Server do @spec handle_request(Raxx.Request.t(), %{}) :: Raxx.Response.t() def handle_request(req, state) + # GET budget information def handle_request(%{method: :GET, path: ["budgets", uuid]}, _state) when uuid != "" do with {:ok, _} <- Ecto.UUID.dump(uuid), %WeekBudget.DB.Budget{} = wb <- WeekBudget.DB.Budget.get_by_secret(uuid) do @@ -23,38 +24,71 @@ defmodule WeekBudget.API.Server do end end + # POST new event into budget def handle_request(%{method: :POST, path: ["budgets", uuid, "events"], body: body}, _state) when uuid != "" and body != "" do - with {:uuid, {:ok, _}} <- {:uuid, Ecto.UUID.dump(uuid)}, - {:wb, %WeekBudget.DB.Budget{} = wb} <- {:wb, WeekBudget.DB.Budget.get_by_secret(uuid)}, - {:json, {:ok, %{"amount" => amnt}}} when is_number(amnt) <- {:json, Jason.decode(body)}, - {:currency, {:ok, curr}} <- {:currency, Money.validate_currency(wb.amount.currency)}, - {:money, %Money{} = money} <- {:money, WeekBudget.Core.MoneyUtils.create(curr, -amnt)} do - {:ok, event} = WeekBudget.DB.Event.create(wb, money) - event_json = serialize_event(event) - json_resp(:created, event_json) - else - {:uuid, _} -> json_resp(:bad_request, %{error: "Malformed UUID."}) - {:wb, _} -> json_resp(:not_found, %{error: "Budget not found."}) - {:json, _} -> json_resp(:bad_request, %{error: "Cannot parse JSON."}) - {:currency, _} -> json_resp(:bad_request, %{error: "Invalid currency."}) - {:money, _} -> json_resp(:internal_server_error, %{error: "Cannot create money."}) - end + {status, data} = + with {:uuid, {:ok, _}} <- {:uuid, Ecto.UUID.dump(uuid)}, + {:wb, %WeekBudget.DB.Budget{} = wb} <- {:wb, WeekBudget.DB.Budget.get_by_secret(uuid)}, + {:json, {:ok, %{"amount" => amnt}}} when is_number(amnt) <- + {:json, Jason.decode(body)}, + {:money, %Money{} = money} <- + {:money, WeekBudget.Core.MoneyUtils.create(wb.amount.currency, -amnt)} do + {:ok, event} = WeekBudget.DB.Event.create(wb, money) + event_json = serialize_event(event) + {:created, event_json} + else + {:uuid, _} -> {:bad_request, %{error: "Malformed UUID."}} + {:wb, _} -> {:not_found, %{error: "Budget not found."}} + {:json, _} -> {:bad_request, %{error: "Cannot parse JSON."}} + {:currency, _} -> {:bad_request, %{error: "Invalid currency."}} + {:money, _} -> {:internal_server_error, %{error: "Cannot create money."}} + end + + json_resp(status, data) end + # PATCH budget with a new amount, deleting all events + def handle_request(%{method: :PATCH, path: ["budgets", uuid], body: body}, _state) + when uuid != "" and body != "" do + {status, data} = + with {:uuid, {:ok, _}} <- {:uuid, Ecto.UUID.dump(uuid)}, + {:wb, %WeekBudget.DB.Budget{} = wb} <- {:wb, WeekBudget.DB.Budget.get_by_secret(uuid)}, + {:json, {:ok, %{"amount" => amnt}}} when is_number(amnt) <- + {:json, Jason.decode(body)}, + {:money, %Money{} = money} <- + {:money, WeekBudget.Core.MoneyUtils.create(wb.amount.currency, amnt)} do + {:ok, %{amount_update: budget}} = WeekBudget.DB.Budget.reset(wb, money) + budget_json = serialize_budget(%WeekBudget.DB.Budget{budget | events: []}) + {:ok, budget_json} + else + {:uuid, _} -> {:bad_request, %{error: "Malformed UUID."}} + {:wb, _} -> {:not_found, %{error: "Budget not found."}} + {:json, _} -> {:bad_request, %{error: "Cannot parse JSON."}} + {:currency, _} -> {:bad_request, %{error: "Invalid currency."}} + {:money, _} -> {:internal_server_error, %{error: "Cannot create money."}} + end + + json_resp(status, data) + end + + # POST to create a new budget def handle_request(%{method: :POST, path: ["budgets"], body: body}, _state) when body != "" do - with {:json, {:ok, %{"amount" => amnt, "currency" => curr_str}}} - when is_number(amnt) and is_binary(curr_str) <- {:json, Jason.decode(body)}, - {:currency, {:ok, curr}} <- {:currency, Money.validate_currency(curr_str)}, - {:money, %Money{} = money} <- {:money, WeekBudget.Core.MoneyUtils.create(curr, amnt)} do - {:ok, budget} = WeekBudget.DB.Budget.create(money) - budget_json = serialize_budget(%WeekBudget.DB.Budget{budget | events: []}) - json_resp(:created, budget_json) - else - {:json, _} -> json_resp(:bad_request, %{error: "Cannot parse JSON."}) - {:currency, _} -> json_resp(:bad_request, %{error: "Invalid currency."}) - {:money, _} -> json_resp(:internal_server_error, %{error: "Cannot create money."}) - end + {status, data} = + with {:json, {:ok, %{"amount" => amnt, "currency" => curr_str}}} + when is_number(amnt) and is_binary(curr_str) <- {:json, Jason.decode(body)}, + {:currency, {:ok, curr}} <- {:currency, Money.validate_currency(curr_str)}, + {:money, %Money{} = money} <- {:money, WeekBudget.Core.MoneyUtils.create(curr, amnt)} do + {:ok, budget} = WeekBudget.DB.Budget.create(money) + budget_json = serialize_budget(%WeekBudget.DB.Budget{budget | events: []}) + {:created, budget_json} + else + {:json, _} -> {:bad_request, %{error: "Cannot parse JSON."}} + {:currency, _} -> {:bad_request, %{error: "Invalid currency."}} + {:money, _} -> {:internal_server_error, %{error: "Cannot create money."}} + end + + json_resp(status, data) end def handle_request(req, _) do diff --git a/backend/lib/core/money_utils.ex b/backend/lib/core/money_utils.ex index 9526e09..2d46c16 100644 --- a/backend/lib/core/money_utils.ex +++ b/backend/lib/core/money_utils.ex @@ -8,8 +8,7 @@ defmodule WeekBudget.Core.MoneyUtils do frontends. It is normalised to a Money instance at first opportunity so it is believed that the impact and potential for errors is minimal. """ - @spec create(atom | string, integer | float) :: - {:ok, Money.t()} | {:error, {module, String.t()}} + @spec create(atom | String.t(), integer | float) :: Money.t() | {:error, {module, String.t()}} def create(currency, amount) def create(currency, amount) when is_integer(amount) do diff --git a/backend/lib/db/budget.ex b/backend/lib/db/budget.ex index 70f9d99..276b968 100644 --- a/backend/lib/db/budget.ex +++ b/backend/lib/db/budget.ex @@ -40,4 +40,20 @@ defmodule WeekBudget.DB.Budget do } |> WeekBudget.DB.Repo.insert() end + + @doc """ + Reset budget to given amount an delete all events. + """ + @spec reset(__MODULE__.t(), Money.t()) :: + {:ok, %{amount_update: __MODULE__.t(), delete_events: any()}} | {:error, any()} + def reset(budget, amount) do + update_cset = + budget + |> Ecto.Changeset.cast(%{amount: amount}, [:amount]) + + Ecto.Multi.new() + |> Ecto.Multi.update(:amount_update, update_cset) + |> Ecto.Multi.delete_all(:delete_events, Ecto.assoc(budget, :events)) + |> WeekBudget.DB.Repo.transaction() + end end