commit e6cbccaf6f99809706550a16be4ec89334afcb5e Author: Mikko Ahlroth Date: Mon May 27 06:02:42 2019 +0200 Initial commit diff --git a/backend/.formatter.exs b/backend/.formatter.exs new file mode 100644 index 0000000..d2cda26 --- /dev/null +++ b/backend/.formatter.exs @@ -0,0 +1,4 @@ +# Used by "mix format" +[ + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] +] diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000..55e7abb --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,24 @@ +# The directory Mix will write compiled artifacts to. +/_build/ + +# If you run "mix test --cover", coverage assets end up here. +/cover/ + +# The directory Mix downloads your dependencies sources to. +/deps/ + +# Where third-party dependencies like ExDoc output generated docs. +/doc/ + +# Ignore .fetch files in case you like to edit your project deps locally. +/.fetch + +# If the VM crashes, it generates a dump, let's ignore it too. +erl_crash.dump + +# Also ignore archive artifacts (built via "mix archive.build"). +*.ez + +# Ignore package tarball (built via "mix hex.build"). +week_budget-*.tar + diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..efb4add --- /dev/null +++ b/backend/README.md @@ -0,0 +1,21 @@ +# WeekBudget + +**TODO: Add description** + +## Installation + +If [available in Hex](https://hex.pm/docs/publish), the package can be installed +by adding `week_budget` to your list of dependencies in `mix.exs`: + +```elixir +def deps do + [ + {:week_budget, "~> 0.1.0"} + ] +end +``` + +Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) +and published on [HexDocs](https://hexdocs.pm). Once published, the docs can +be found at [https://hexdocs.pm/week_budget](https://hexdocs.pm/week_budget). + diff --git a/backend/config/config.exs b/backend/config/config.exs new file mode 100644 index 0000000..b324b03 --- /dev/null +++ b/backend/config/config.exs @@ -0,0 +1,38 @@ +# This file is responsible for configuring your application +# and its dependencies with the aid of the Mix.Config module. +use Mix.Config + +# This configuration is loaded before any dependency and is restricted +# to this project. If another project depends on this project, this +# file won't be loaded nor affect the parent project. For this reason, +# if you want to provide default values for your application for +# third-party users, it should be done in your "mix.exs" file. + +# You can configure your application as: +# +# config :week_budget, key: :value +# +# and access this configuration in your application as: +# +# Application.get_env(:week_budget, :key) +# +# You can also configure a third-party app: +# +# config :logger, level: :info +# + +# It is also possible to import configuration files, relative to this +# directory. For example, you can emulate configuration per environment +# by uncommenting the line below and defining dev.exs, test.exs and such. +# Configuration from the imported file will override the ones defined +# here (which is why it is important to import them last). +# +# import_config "#{Mix.env()}.exs" + +config :week_budget, + ecto_repos: [WeekBudget.DB.Repo], + database_url: "ecto://weekbudget:week@127.0.0.1/weekbudget" + +config :ex_money, + exchange_rates_retrieve_every: :never, + default_cldr_backend: WeekBudget.Core.Cldr diff --git a/backend/lib/api/server.ex b/backend/lib/api/server.ex new file mode 100644 index 0000000..d3eb796 --- /dev/null +++ b/backend/lib/api/server.ex @@ -0,0 +1,87 @@ +defmodule WeekBudget.API.Server do + @moduledoc """ + Simple server handling/delegating all API calls of the system. + """ + + require Logger + use Ace.HTTP.Service, port: 2019, cleartext: true + use Raxx.SimpleServer + + @impl Raxx.SimpleServer + @spec handle_request(Raxx.Request.t(), %{}) :: Raxx.Response.t() + def handle_request(req, state) + + 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 + wb + |> serialize_budget() + |> json_resp() + else + :error -> json_resp(:bad_request, %{error: "Malformed UUID."}) + nil -> json_resp(:not_found, %{error: "Budget not found."}) + end + end + + 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 + end + + 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 + end + + def handle_request(req, _) do + Logger.debug("Failed request:") + Logger.debug(inspect(req)) + json_resp(:not_found, %{error: "Unknown API path or invalid data."}) + end + + defp serialize_budget(%WeekBudget.DB.Budget{secret: secret, amount: amount, events: events}) do + # True amount is initial amount plus all event amounts + true_amount = Enum.reduce(events, amount, &Money.add!(&2, &1.amount)) + + %{ + uuid: secret, + init: Decimal.to_float(amount.amount), + amount: Decimal.to_float(true_amount.amount), + events: Enum.map(events, &serialize_event/1) + } + end + + defp serialize_event(%WeekBudget.DB.Event{at: at, amount: amount}) do + %{at: DateTime.to_iso8601(at), amount: Decimal.to_float(amount.amount)} + end + + defp json_resp(status \\ :ok, content) do + response(status) + |> set_header("content-type", "application/json") + |> set_body(Jason.encode!(content)) + end +end diff --git a/backend/lib/core/application.ex b/backend/lib/core/application.ex new file mode 100644 index 0000000..c817f1a --- /dev/null +++ b/backend/lib/core/application.ex @@ -0,0 +1,30 @@ +defmodule WeekBudget.Core.Application do + # See https://hexdocs.pm/elixir/Application.html + # for more information on OTP Applications + @moduledoc false + + use Application + + def start(_type, _args) do + case Code.ensure_loaded(ExSync) do + {:module, ExSync = mod} -> + mod.start() + + {:error, _} -> + :ok + end + + port = Application.get_env(:week_budget, :port, 2019) + + # List all child processes to be supervised + children = [ + {WeekBudget.DB.Repo, []}, + {WeekBudget.API.Server, [[], [port: port]]} + ] + + # See https://hexdocs.pm/elixir/Supervisor.html + # for other strategies and supported options + opts = [strategy: :one_for_one, name: WeekBudget.Core.Supervisor] + Supervisor.start_link(children, opts) + end +end diff --git a/backend/lib/core/cldr.ex b/backend/lib/core/cldr.ex new file mode 100644 index 0000000..78a0153 --- /dev/null +++ b/backend/lib/core/cldr.ex @@ -0,0 +1,3 @@ +defmodule WeekBudget.Core.Cldr do + use Cldr, locales: ["en"], default_locale: "en" +end diff --git a/backend/lib/core/money_utils.ex b/backend/lib/core/money_utils.ex new file mode 100644 index 0000000..9526e09 --- /dev/null +++ b/backend/lib/core/money_utils.ex @@ -0,0 +1,22 @@ +defmodule WeekBudget.Core.MoneyUtils do + @moduledoc """ + Utilities to do with money operations. + """ + + @doc """ + Create money from given integer or float and currency. Float is used to easier accept money from the + 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()}} + def create(currency, amount) + + def create(currency, amount) when is_integer(amount) do + Money.new(currency, amount) + end + + def create(currency, amount) when is_float(amount) do + Money.from_float(currency, amount) + end +end diff --git a/backend/lib/db/budget.ex b/backend/lib/db/budget.ex new file mode 100644 index 0000000..70f9d99 --- /dev/null +++ b/backend/lib/db/budget.ex @@ -0,0 +1,43 @@ +defmodule WeekBudget.DB.Budget do + @moduledoc """ + A single budget that has a secret token that can be used to join it and an initial sum. + + The final sum can be calculated by subtracting all the events from the initial sum. + """ + use Ecto.Schema + import Ecto.Query, only: [from: 2] + + @primary_key {:secret, :binary_id, autogenerate: true} + + schema "budgets" do + field(:amount, Money.Ecto.Composite.Type) + + timestamps() + + has_many(:events, WeekBudget.DB.Event, foreign_key: :budget_id) + end + + @doc """ + Get budget by its secret, if any exists. + """ + @spec get_by_secret(String.t()) :: __MODULE__.t() | nil + def get_by_secret(secret) do + from(w in __MODULE__, + where: w.secret == ^secret, + select: [:secret, :amount], + preload: [:events] + ) + |> WeekBudget.DB.Repo.one() + end + + @doc """ + Create budget with given initial amount. + """ + @spec create(Money.t()) :: {:ok, __MODULE__.t()} | {:error, Ecto.Changeset.t()} + def create(amount) do + %__MODULE__{ + amount: amount + } + |> WeekBudget.DB.Repo.insert() + end +end diff --git a/backend/lib/db/event.ex b/backend/lib/db/event.ex new file mode 100644 index 0000000..80b51b4 --- /dev/null +++ b/backend/lib/db/event.ex @@ -0,0 +1,32 @@ +defmodule WeekBudget.DB.Event do + @moduledoc """ + A money spending event that occurred on the budget. If amount is positive, money was spent. If amount + is negative, money was received. + """ + use Ecto.Schema + + schema "events" do + field(:amount, Money.Ecto.Composite.Type) + field(:at, :utc_datetime) + + belongs_to(:budget, WeekBudget.DB.Budget, + references: :secret, + foreign_key: :budget_id, + type: :binary_id + ) + end + + @doc """ + Create a new event with the given amount. Note that normally the amount should be negative. + """ + @spec create(WeekBudget.DB.Budget.t(), Money.t()) :: + {:ok, __MODULE__.t()} | {:error, Ecto.Changeset.t()} + def create(%WeekBudget.DB.Budget{} = budget, %Money{} = amount) do + %__MODULE__{ + amount: amount, + budget: budget, + at: DateTime.utc_now() |> DateTime.truncate(:second) + } + |> WeekBudget.DB.Repo.insert() + end +end diff --git a/backend/lib/db/repo.ex b/backend/lib/db/repo.ex new file mode 100644 index 0000000..b2378a5 --- /dev/null +++ b/backend/lib/db/repo.ex @@ -0,0 +1,9 @@ +defmodule WeekBudget.DB.Repo do + use Ecto.Repo, + otp_app: :week_budget, + adapter: Ecto.Adapters.Postgres + + def init(_type, config) do + {:ok, Keyword.put(config, :url, Application.get_env(:week_budget, :database_url))} + end +end diff --git a/backend/mix.exs b/backend/mix.exs new file mode 100644 index 0000000..77980de --- /dev/null +++ b/backend/mix.exs @@ -0,0 +1,37 @@ +defmodule WeekBudget.MixProject do + use Mix.Project + + def project do + [ + app: :week_budget, + version: "0.1.0", + elixir: "~> 1.8", + start_permanent: Mix.env() == :prod, + deps: deps() + ] + end + + # Run "mix help compile.app" to learn about applications. + def application do + [ + extra_applications: [:logger], + mod: {WeekBudget.Core.Application, []} + ] + end + + # Run "mix help deps" to learn about dependencies. + defp deps do + [ + {:raxx, "~> 1.0.1"}, + {:raxx_logger, "~> 0.2.2"}, + {:ace, "~> 0.18.8"}, + {:ecto, "~> 3.1.4"}, + {:ecto_sql, "~> 3.1.3"}, + {:jason, "~> 1.1.2"}, + {:observer_cli, "~> 1.5.0"}, + {:postgrex, ">= 0.0.0"}, + {:ex_money, "~> 3.4.2"}, + {:exsync, "~> 0.2.3", only: :dev} + ] + end +end diff --git a/backend/mix.lock b/backend/mix.lock new file mode 100644 index 0000000..97c0576 --- /dev/null +++ b/backend/mix.lock @@ -0,0 +1,26 @@ +%{ + "ace": {:hex, :ace, "0.18.8", "9853110247f769e7d1600be7cd2abd638b53391c8c7b1ab11baad15184cb95d4", [:mix], [{:hpack, "~> 0.2.3", [hex: :hpack_erl, repo: "hexpm", optional: false]}, {:raxx, "~> 0.17.0 or ~> 0.18.0 or ~> 1.0", [hex: :raxx, repo: "hexpm", optional: false]}], "hexpm"}, + "cldr_utils": {:hex, :cldr_utils, "2.2.0", "4f6ac090fc1871037ac41265c98bf0f785b2a4ede144dd2a5282986a5311dd14", [:mix], [{:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm"}, + "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm"}, + "db_connection": {:hex, :db_connection, "2.0.6", "bde2f85d047969c5b5800cb8f4b3ed6316c8cb11487afedac4aa5f93fd39abfa", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm"}, + "decimal": {:hex, :decimal, "1.7.0", "30d6b52c88541f9a66637359ddf85016df9eb266170d53105f02e4a67e00c5aa", [:mix], [], "hexpm"}, + "ecto": {:hex, :ecto, "3.1.4", "69d852da7a9f04ede725855a35ede48d158ca11a404fe94f8b2fb3b2162cd3c9", [:mix], [{:decimal, "~> 1.6", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm"}, + "ecto_sql": {:hex, :ecto_sql, "3.1.3", "2c536139190492d9de33c5fefac7323c5eaaa82e1b9bf93482a14649042f7cd9", [:mix], [{:db_connection, "~> 2.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.1.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:mariaex, "~> 0.9.1", [hex: :mariaex, repo: "hexpm", optional: true]}, {:myxql, "~> 0.2.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.14.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"}, + "ex_cldr": {:hex, :ex_cldr, "2.7.0", "be0f1c88f1baeb8364395df7d3266a8d7f075ebaeb00d20db743b1d72fa47e55", [:mix], [{:cldr_utils, "~> 2.1", [hex: :cldr_utils, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:gettext, "~> 0.13", [hex: :gettext, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 0.5", [hex: :nimble_parsec, repo: "hexpm", optional: false]}, {:plug, "~> 1.4", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm"}, + "ex_cldr_currencies": {:hex, :ex_cldr_currencies, "2.3.0", "bffae489416b8b05d4683403263f5d62aae17de70c24ff915a533541fea514de", [:mix], [{:ex_cldr, "~> 2.6", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm"}, + "ex_cldr_numbers": {:hex, :ex_cldr_numbers, "2.6.0", "431c744058a3980f561878f4886b78871d4067c6d9039ce78335ed9a41c831f8", [:mix], [{:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:ex_cldr, "~> 2.6", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:ex_cldr_currencies, "~> 2.3", [hex: :ex_cldr_currencies, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm"}, + "ex_money": {:hex, :ex_money, "3.4.2", "7da06947ed92fab7ad265f7fe1f6d36b30404f2b33c7b46de54670921dfe3420", [:mix], [{:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}, {:ex_cldr, "~> 2.6", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:ex_cldr_numbers, "~> 2.6", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm"}, + "ex_sync": {:hex, :ex_sync, "0.0.4", "468839964dead69df738a3698e4318d18fa3322e4f684adf98d5441704d1bc8c", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}], "hexpm"}, + "exsync": {:hex, :exsync, "0.2.3", "a1ac11b4bd3808706003dbe587902101fcc1387d9fc55e8b10972f13a563dd15", [:mix], [{:file_system, "~> 0.2", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm"}, + "file_system": {:hex, :file_system, "0.2.7", "e6f7f155970975789f26e77b8b8d8ab084c59844d8ecfaf58cbda31c494d14aa", [:mix], [], "hexpm"}, + "hpack": {:hex, :hpack_erl, "0.2.3", "17670f83ff984ae6cd74b1c456edde906d27ff013740ee4d9efaa4f1bf999633", [:rebar3], [], "hexpm"}, + "jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"}, + "nimble_parsec": {:hex, :nimble_parsec, "0.5.0", "90e2eca3d0266e5c53f8fbe0079694740b9c91b6747f2b7e3c5d21966bba8300", [:mix], [], "hexpm"}, + "observer_cli": {:hex, :observer_cli, "1.5.0", "9944882b71f55b2503663d9cb54d3f1c7bbdf7cc6dd01cc40ea8ef51207601ec", [:mix, :rebar3], [{:recon, "2.5.0", [hex: :recon, repo: "hexpm", optional: false]}], "hexpm"}, + "poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm"}, + "postgrex": {:hex, :postgrex, "0.14.3", "5754dee2fdf6e9e508cbf49ab138df964278700b764177e8f3871e658b345a1e", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm"}, + "raxx": {:hex, :raxx, "1.0.1", "8c51ec5227c85f999360fc844fc1d4e2e5a2adf2b0ce068eb56243ee6b2f65e3", [:mix], [], "hexpm"}, + "raxx_logger": {:hex, :raxx_logger, "0.2.2", "51bbce9371298e7329de028fe13cf050bf59547bc8f1926910d479ba4b31c4b4", [:mix], [{:raxx, "~> 0.17.5 or ~> 0.18.0 or ~> 1.0", [hex: :raxx, repo: "hexpm", optional: false]}], "hexpm"}, + "recon": {:hex, :recon, "2.5.0", "2f7fcbec2c35034bade2f9717f77059dc54eb4e929a3049ca7ba6775c0bd66cd", [:mix, :rebar3], [], "hexpm"}, + "telemetry": {:hex, :telemetry, "0.4.0", "8339bee3fa8b91cb84d14c2935f8ecf399ccd87301ad6da6b71c09553834b2ab", [:rebar3], [], "hexpm"}, +} diff --git a/backend/priv/repo/migrations/20190523122128_add_money_with_currency_type_to_postgres.exs b/backend/priv/repo/migrations/20190523122128_add_money_with_currency_type_to_postgres.exs new file mode 100644 index 0000000..bbbce4b --- /dev/null +++ b/backend/priv/repo/migrations/20190523122128_add_money_with_currency_type_to_postgres.exs @@ -0,0 +1,11 @@ +defmodule WeekBudget.DB.Repo.Migrations.AddMoneyWithCurrencyTypeToPostgres do + use Ecto.Migration + + def up do + execute("CREATE TYPE public.money_with_currency AS (currency_code char(3), amount numeric);") + end + + def down do + execute("DROP TYPE public.money_with_currency;") + end +end \ No newline at end of file diff --git a/backend/priv/repo/migrations/20190523122356_init.exs b/backend/priv/repo/migrations/20190523122356_init.exs new file mode 100644 index 0000000..e981b5c --- /dev/null +++ b/backend/priv/repo/migrations/20190523122356_init.exs @@ -0,0 +1,12 @@ +defmodule WeekBudget.DB.Repo.Migrations.Init do + use Ecto.Migration + + def change do + create table(:budgets, primary_key: false) do + add(:secret, :binary_id, primary_key: true) + add(:amount, :money_with_currency) + + timestamps() + end + end +end diff --git a/backend/priv/repo/migrations/20190523123548_add_events.exs b/backend/priv/repo/migrations/20190523123548_add_events.exs new file mode 100644 index 0000000..2306c84 --- /dev/null +++ b/backend/priv/repo/migrations/20190523123548_add_events.exs @@ -0,0 +1,18 @@ +defmodule WeekBudget.DB.Repo.Migrations.AddEvents do + use Ecto.Migration + + def change do + create table(:events) do + add(:amount, :money_with_currency, null: false) + add(:at, :utc_datetime, null: false) + + add( + :budget, + references("budgets", column: :secret, type: :binary_id, on_delete: :delete_all), + null: false + ) + end + + create(index("events", :budget)) + end +end diff --git a/backend/priv/repo/migrations/20190525204914_budget_fkey_name.exs b/backend/priv/repo/migrations/20190525204914_budget_fkey_name.exs new file mode 100644 index 0000000..59d1672 --- /dev/null +++ b/backend/priv/repo/migrations/20190525204914_budget_fkey_name.exs @@ -0,0 +1,7 @@ +defmodule WeekBudget.DB.Repo.Migrations.BudgetFkeyName do + use Ecto.Migration + + def change do + rename(table("events"), :budget, to: :budget_id) + end +end diff --git a/backend/test/test_helper.exs b/backend/test/test_helper.exs new file mode 100644 index 0000000..869559e --- /dev/null +++ b/backend/test/test_helper.exs @@ -0,0 +1 @@ +ExUnit.start() diff --git a/backend/test/week_budget_test.exs b/backend/test/week_budget_test.exs new file mode 100644 index 0000000..eba3ede --- /dev/null +++ b/backend/test/week_budget_test.exs @@ -0,0 +1,8 @@ +defmodule WeekBudgetTest do + use ExUnit.Case + doctest WeekBudget + + test "greets the world" do + assert WeekBudget.hello() == :world + end +end