├── .ctags
├── .gitignore
├── .iex.exs
├── .travis.yml
├── LICENSE.md
├── Procfile
├── README.md
├── app.json
├── apps
├── auth
│ ├── .gitignore
│ ├── README.md
│ ├── config
│ │ ├── config.exs
│ │ ├── dev.exs
│ │ ├── prod.exs
│ │ └── test.exs
│ ├── lib
│ │ ├── auth.ex
│ │ └── auth
│ │ │ ├── account.ex
│ │ │ ├── application.ex
│ │ │ └── repo.ex
│ ├── mix.exs
│ ├── priv
│ │ └── repo
│ │ │ └── migrations
│ │ │ └── 20160731211450_create_accounts.exs
│ └── test
│ │ ├── auth_test.exs
│ │ └── test_helper.exs
├── backoffice
│ ├── .gitignore
│ ├── README.md
│ ├── brunch-config.js
│ ├── config
│ │ ├── config.exs
│ │ ├── dev.exs
│ │ ├── prod.exs
│ │ └── test.exs
│ ├── lib
│ │ ├── backoffice.ex
│ │ └── backoffice
│ │ │ ├── application.ex
│ │ │ └── endpoint.ex
│ ├── mix.exs
│ ├── package.json
│ ├── priv
│ │ ├── gettext
│ │ │ ├── en
│ │ │ │ └── LC_MESSAGES
│ │ │ │ │ └── errors.po
│ │ │ └── errors.pot
│ │ └── repo
│ │ │ └── seeds.exs
│ ├── test
│ │ ├── controllers
│ │ │ └── page_controller_test.exs
│ │ ├── support
│ │ │ ├── conn_case.ex
│ │ │ └── model_case.ex
│ │ ├── test_helper.exs
│ │ └── views
│ │ │ ├── error_view_test.exs
│ │ │ ├── layout_view_test.exs
│ │ │ └── page_view_test.exs
│ └── web
│ │ ├── admin
│ │ ├── auth
│ │ │ └── account.ex
│ │ ├── bank
│ │ │ ├── customer.ex
│ │ │ └── ledger
│ │ │ │ ├── account.ex
│ │ │ │ └── entry.ex
│ │ └── dashboard.ex
│ │ ├── controllers
│ │ └── page_controller.ex
│ │ ├── gettext.ex
│ │ ├── router.ex
│ │ ├── static
│ │ ├── assets
│ │ │ ├── favicon.ico
│ │ │ ├── fonts
│ │ │ │ ├── FontAwesome.otf
│ │ │ │ ├── fontawesome-webfont.eot
│ │ │ │ ├── fontawesome-webfont.svg
│ │ │ │ ├── fontawesome-webfont.ttf
│ │ │ │ ├── fontawesome-webfont.woff
│ │ │ │ ├── fontawesome-webfont.woff2
│ │ │ │ ├── ionicons.eot
│ │ │ │ ├── ionicons.svg
│ │ │ │ ├── ionicons.ttf
│ │ │ │ └── ionicons.woff
│ │ │ ├── images
│ │ │ │ ├── ex_admin
│ │ │ │ │ ├── admin_notes_icon.png
│ │ │ │ │ ├── datepicker
│ │ │ │ │ │ ├── datepicker-header-bg.png
│ │ │ │ │ │ ├── datepicker-input-icon.png
│ │ │ │ │ │ ├── datepicker-next-link-icon.png
│ │ │ │ │ │ ├── datepicker-nipple.png
│ │ │ │ │ │ └── datepicker-prev-link-icon.png
│ │ │ │ │ ├── glyphicons-halflings-white.png
│ │ │ │ │ ├── glyphicons-halflings.png
│ │ │ │ │ └── orderable.png
│ │ │ │ └── phoenix.png
│ │ │ └── robots.txt
│ │ ├── css
│ │ │ ├── app.css
│ │ │ └── phoenix.css
│ │ ├── js
│ │ │ ├── app.js
│ │ │ └── socket.js
│ │ └── vendor
│ │ │ ├── active_admin.css.css
│ │ │ ├── active_admin.css.css.map
│ │ │ ├── admin_lte2.css
│ │ │ ├── admin_lte2.css.map
│ │ │ ├── admin_lte2.js
│ │ │ ├── admin_lte2.js.map
│ │ │ ├── ex_admin_common.js
│ │ │ ├── ex_admin_common.js.map
│ │ │ ├── jquery.min.js
│ │ │ └── jquery.min.js.map
│ │ ├── templates
│ │ ├── layout
│ │ │ └── app.html.eex
│ │ └── page
│ │ │ └── index.html.eex
│ │ ├── views
│ │ ├── error_helpers.ex
│ │ ├── error_view.ex
│ │ ├── layout_view.ex
│ │ └── page_view.ex
│ │ └── web.ex
├── bank
│ ├── .gitignore
│ ├── .iex.exs
│ ├── README.md
│ ├── config
│ │ ├── config.exs
│ │ ├── dev.exs
│ │ ├── prod.exs
│ │ └── test.exs
│ ├── lib
│ │ ├── bank.ex
│ │ └── bank
│ │ │ ├── application.ex
│ │ │ ├── customer.ex
│ │ │ ├── customer_registration.ex
│ │ │ ├── deposit.ex
│ │ │ ├── iex_helpers.ex
│ │ │ ├── ledger.ex
│ │ │ ├── ledger
│ │ │ ├── account.ex
│ │ │ └── entry.ex
│ │ │ ├── model.ex
│ │ │ ├── repo.ex
│ │ │ └── transfer.ex
│ ├── mix.exs
│ ├── priv
│ │ └── repo
│ │ │ ├── migrations
│ │ │ ├── 20160527233040_create_account.exs
│ │ │ ├── 20160527233041_create_customer.exs
│ │ │ └── 20160528001304_create_entries.exs
│ │ │ └── seeds.exs
│ └── test
│ │ ├── bank
│ │ ├── customer_test.exs
│ │ ├── ledger
│ │ │ └── account_test.exs
│ │ ├── ledger_test.exs
│ │ ├── transaction_test.exs
│ │ └── transfer_test.exs
│ │ ├── bank_test.exs
│ │ ├── support
│ │ └── case.ex
│ │ └── test_helper.exs
├── bank_web
│ ├── .gitignore
│ ├── .iex.exs
│ ├── README.md
│ ├── brunch-config.js
│ ├── config
│ │ ├── config.exs
│ │ ├── dev.exs
│ │ ├── prod.exs
│ │ └── test.exs
│ ├── lib
│ │ ├── bank_web.ex
│ │ └── bank_web
│ │ │ ├── application.ex
│ │ │ └── endpoint.ex
│ ├── mix.exs
│ ├── package.json
│ ├── priv
│ │ └── gettext
│ │ │ ├── en
│ │ │ └── LC_MESSAGES
│ │ │ │ └── errors.po
│ │ │ └── errors.pot
│ ├── test
│ │ ├── controllers
│ │ │ ├── account_controller_test.exs
│ │ │ ├── page_controller_test.exs
│ │ │ └── transfer_controller_test.exs
│ │ ├── support
│ │ │ ├── channel_case.ex
│ │ │ ├── conn_case.ex
│ │ │ └── model_case.ex
│ │ ├── test_helper.exs
│ │ └── views
│ │ │ ├── account_view_test.exs
│ │ │ ├── error_view_test.exs
│ │ │ ├── layout_view_test.exs
│ │ │ └── page_view_test.exs
│ └── web
│ │ ├── controllers
│ │ ├── account_controller.ex
│ │ ├── authentication.ex
│ │ ├── page_controller.ex
│ │ ├── session_controller.ex
│ │ └── transfer_controller.ex
│ │ ├── gettext.ex
│ │ ├── router.ex
│ │ ├── static
│ │ ├── assets
│ │ │ ├── favicon.ico
│ │ │ ├── images
│ │ │ │ └── phoenix.png
│ │ │ └── robots.txt
│ │ ├── css
│ │ │ └── app.css
│ │ ├── js
│ │ │ ├── app.js
│ │ │ └── socket.js
│ │ └── vendor
│ │ │ └── css
│ │ │ └── bootstrap-yeti.min.css
│ │ ├── templates
│ │ ├── account
│ │ │ └── show.html.eex
│ │ ├── layout
│ │ │ └── app.html.eex
│ │ ├── page
│ │ │ └── index.html.eex
│ │ ├── session
│ │ │ └── new.html.eex
│ │ └── transfer
│ │ │ └── new.html.eex
│ │ ├── views
│ │ ├── account_view.ex
│ │ ├── error_helpers.ex
│ │ ├── error_view.ex
│ │ ├── layout_view.ex
│ │ ├── page_view.ex
│ │ ├── session_view.ex
│ │ └── transfer_view.ex
│ │ └── web.ex
├── master_proxy
│ ├── .gitignore
│ ├── README.md
│ ├── config
│ │ └── config.exs
│ ├── lib
│ │ ├── master_proxy.ex
│ │ └── master_proxy
│ │ │ ├── application.ex
│ │ │ └── plug.ex
│ ├── mix.exs
│ └── test
│ │ ├── master_proxy_test.exs
│ │ └── test_helper.exs
├── messenger
│ ├── .gitignore
│ ├── .iex.exs
│ ├── README.md
│ ├── config
│ │ └── config.exs
│ ├── lib
│ │ ├── messenger.ex
│ │ └── messenger
│ │ │ ├── application.ex
│ │ │ ├── logger.ex
│ │ │ └── test.ex
│ ├── mix.exs
│ └── test
│ │ ├── messenger_test.exs
│ │ └── test_helper.exs
└── money
│ ├── .gitignore
│ ├── .iex.exs
│ ├── README.md
│ ├── config
│ └── config.exs
│ ├── lib
│ ├── money.ex
│ └── money
│ │ ├── ecto.ex
│ │ └── phoenix.ex
│ ├── mix.exs
│ └── test
│ ├── money_test.exs
│ └── test_helper.exs
├── compile
├── config
└── config.exs
├── docs
├── diagram.png
└── main.md
├── elixir_buildpack.config
├── mix.exs
├── mix.lock
├── phoenix_static_buildpack.config
└── script
└── diagram.exs
/.ctags:
--------------------------------------------------------------------------------
1 | --exclude=.git
2 | --exclude=node_modules
3 | --exclude=doc
4 | --exclude=deps
5 | --exclude=_build
6 |
7 | --langdef=Elixir
8 | --langmap=Elixir:.ex.exs
9 | --regex-Elixir=/^[ \t]*def(p?)[ \t]+([a-z_][a-zA-Z0-9_?!]*)/\2/f,functions,functions (def ...)/
10 | --regex-Elixir=/^[ \t]*defcallback[ \t]+([a-z_][a-zA-Z0-9_?!]*)/\1/c,callbacks,callbacks (defcallback ...)/
11 | --regex-Elixir=/^[ \t]*defdelegate[ \t]+([a-z_][a-zA-Z0-9_?!]*)/\1/d,delegates,delegates (defdelegate ...)/
12 | --regex-Elixir=/^[ \t]*defexception[ \t]+([A-Z][a-zA-Z0-9_]*\.)*([A-Z][a-zA-Z0-9_?!]*)/\2/e,exceptions,exceptions (defexception ...)/
13 | --regex-Elixir=/^[ \t]*defimpl[ \t]+([A-Z][a-zA-Z0-9_]*\.)*([A-Z][a-zA-Z0-9_?!]*)/\2/i,implementations,implementations (defimpl ...)/
14 | --regex-Elixir=/^[ \t]*defmacro(p?)[ \t]+([a-z_][a-zA-Z0-9_?!]*)\(/\2/a,macros,macros (defmacro ...)/
15 | --regex-Elixir=/^[ \t]*defmacro(p?)[ \t]+([a-zA-Z0-9_?!]+)?[ \t]+([^ \tA-Za-z0-9_]+)[ \t]*[a-zA-Z0-9_!?!]/\3/o,operators,operators (e.g. "defmacro a <<< b")/
16 | --regex-Elixir=/^[ \t]*defmodule[ \t]+([A-Z][a-zA-Z0-9_]*\.)*([A-Z][a-zA-Z0-9_?!]*)/\2/m,modules,modules (defmodule ...)/
17 | --regex-Elixir=/^[ \t]*defprotocol[ \t]+([A-Z][a-zA-Z0-9_]*\.)*([A-Z][a-zA-Z0-9_?!]*)/\2/p,protocols,protocols (defprotocol...)/
18 | --regex-Elixir=/^[ \t]*Record\.defrecord[ \t]+:([a-zA-Z0-9_]+)/\1/r,records,records (defrecord...)/
19 | --regex-Elixir=/^[ \t]*test[ \t]+\"([a-z_][a-zA-Z0-9_?! ]*)\"*/\1/t,tests,tests (test ...)/
20 |
21 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | tmp/
2 | # The directory Mix will write compiled artifacts to.
3 | /_build
4 |
5 | # If you run "mix test --cover", coverage assets end up here.
6 | /cover
7 |
8 | # The directory Mix downloads your dependencies sources to.
9 | /deps
10 |
11 | # Where 3rd-party dependencies like ExDoc output generated docs.
12 | doc
13 |
14 | # If the VM crashes, it generates a dump, let's ignore it too.
15 | erl_crash.dump
16 |
17 | # Also ignore archive artifacts (built via "mix archive.build").
18 | *.ez
19 |
20 | app_tree.dot
21 | app_tree.png
22 | deps_tree.dot
23 | deps_tree.png
24 | tags
25 | /diagram.dot
26 |
--------------------------------------------------------------------------------
/.iex.exs:
--------------------------------------------------------------------------------
1 | import_if_available Money, only: [sigil_M: 2]
2 | import_if_available Bank.IExHelpers
3 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: elixir
2 | elixir: 1.4.2
3 | otp_release: 19.3
4 | sudo: false
5 | addons:
6 | postgresql: 9.4
7 | before_script:
8 | - MIX_ENV=test mix compile
9 | - MIX_ENV=test mix ecto.create
10 | script:
11 | - mix test
12 | - echo "Re-running tests for each app" ; for app in apps/**; do cd $app && mix test; cd ../.. ; done
13 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Wojciech Mach
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
10 |
--------------------------------------------------------------------------------
/Procfile:
--------------------------------------------------------------------------------
1 | web: cd apps/master_proxy && MIX_ENV=prod mix run --no-halt
2 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Bank Platform
2 |
3 | [](https://travis-ci.org/wojtekmach/acme_bank)
4 |
5 |
6 | [](https://heroku.com/deploy)
7 |
8 | Acme Bank is an example project to explore and experiment with building modular and maintainable Elixir/Phoenix applications. For some more context, see ElixirConf USA 2016 talk "Building an Umbrella Project":
9 |
10 | - [Slides](https://speakerdeck.com/wojtekmach/building-an-umbrella-project)
11 | - [Video](https://www.youtube.com/watch?v=6NTmUQClHrU)
12 |
13 | See README of each application to learn more about what it does. Most apps contain a "wishlist" of features that might
14 | makes sense for that app. The idea is not to implement these features but to show how each particular app can grow
15 | in complexity and thus how important it is to keep it separate.
16 |
17 | ## Apps
18 |
19 | - [Auth](apps/auth)
20 | - [Backoffice](apps/backoffice)
21 | - [BankWeb](apps/bank_web)
22 | - [Bank](apps/bank)
23 | - [MasterProxy](apps/master_proxy)
24 | - [Messenger](apps/messenger)
25 | - [Money](apps/money)
26 |
27 | 
28 |
29 | ## Setup
30 |
31 | $ git clone https://github.com/wojtekmach/acme_bank
32 | $ cd acme_bank
33 | $ mix deps.get
34 | $ mix ecto.setup
35 | $ (cd apps/bank_web && npm install)
36 | $ (cd apps/backoffice && npm install)
37 | $ mix phoenix.server
38 | $ open http://localhost:4000
39 | $ open http://localhost:4001/backoffice
40 |
41 | ## Deployment
42 |
43 | Acme Bank can be deployed to Heroku, see the Heroku Deploy button at the beginning of the README.
44 | [MasterProxy](apps/master_proxy) is used as the main entry point to the application: it binds
45 | to the port exposed by Heroku, and forwards requests to web apps.
46 |
47 | ## License
48 |
49 | MIT
50 |
--------------------------------------------------------------------------------
/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Bank",
3 | "description": "A simple bank written in Elixir.",
4 | "repository": "https://github.com/wojtekmach/bank",
5 | "keywords": ["elixir", "umbrella", "phoenix", "ecto"],
6 | "buildpacks": [
7 | {
8 | "url": "https://github.com/HashNuke/heroku-buildpack-elixir.git"
9 | },
10 | {
11 | "url": "https://github.com/gjaldon/heroku-buildpack-phoenix-static.git"
12 | }
13 | ],
14 | "env": {
15 | "SECRET_KEY_BASE": {
16 | "description": "A secret key for verifying the integrity of signed cookies.",
17 | "generator": "secret"
18 | },
19 | "POOL_SIZE": {
20 | "description": "Database pool size",
21 | "value": "10"
22 | }
23 | },
24 | "addons": [
25 | "heroku-postgresql"
26 | ],
27 | "scripts": {
28 | "postdeploy": "MIX_ENV=prod mix ecto.migrate"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/apps/auth/.gitignore:
--------------------------------------------------------------------------------
1 | # The directory Mix will write compiled artifacts to.
2 | /_build
3 |
4 | # If you run "mix test --cover", coverage assets end up here.
5 | /cover
6 |
7 | # The directory Mix downloads your dependencies sources to.
8 | /deps
9 |
10 | # Where 3rd-party dependencies like ExDoc output generated docs.
11 | /doc
12 |
13 | # If the VM crashes, it generates a dump, let's ignore it too.
14 | erl_crash.dump
15 |
16 | # Also ignore archive artifacts (built via "mix archive.build").
17 | *.ez
18 |
--------------------------------------------------------------------------------
/apps/auth/README.md:
--------------------------------------------------------------------------------
1 | # Auth
2 |
3 | Authentication system for the platform.
4 |
5 | Right now, `Auth` just handles a simple email/password combination.
6 |
7 | In the future this could be extended to support:
8 |
9 | - different authentication strategies, e.g.: username+password for customers, LDAP for operators, OAuth for partners
10 | - 2FA
11 | - tracking sign in count
12 | - tracking sign in attempts and locking down
13 | - tracking IP
14 | - tracking sign ins from multiple devices
15 | - showing active sessions across devices (using Phoenix Presence)
16 |
--------------------------------------------------------------------------------
/apps/auth/config/config.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
3 | ## Logger
4 | config :logger, level: :debug
5 |
6 | ## Repo
7 | config :auth, ecto_repos: [Auth.Repo]
8 |
9 | import_config "#{Mix.env}.exs"
10 |
--------------------------------------------------------------------------------
/apps/auth/config/dev.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
3 | config :auth, Auth.Repo,
4 | adapter: Ecto.Adapters.Postgres,
5 | username: "postgres",
6 | password: "postgres",
7 | database: "bank_platform_#{Mix.env}",
8 | hostname: "localhost"
9 |
--------------------------------------------------------------------------------
/apps/auth/config/prod.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
3 | config :logger, level: :info
4 |
5 | config :auth, Auth.Repo,
6 | adapter: Ecto.Adapters.Postgres,
7 | url: System.get_env("DATABASE_URL"),
8 | pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"),
9 | ssl: true
10 |
--------------------------------------------------------------------------------
/apps/auth/config/test.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
3 | ## Repo
4 | config :auth, Auth.Repo,
5 | adapter: Ecto.Adapters.Postgres,
6 | username: "postgres",
7 | password: "postgres",
8 | database: "bank_platform_#{Mix.env}",
9 | hostname: "localhost",
10 | pool: Ecto.Adapters.SQL.Sandbox
11 |
12 | ## Comeonin
13 | config :comeonin, :bcrypt_log_rounds, 4
14 |
--------------------------------------------------------------------------------
/apps/auth/lib/auth.ex:
--------------------------------------------------------------------------------
1 | defmodule Auth do
2 | @moduledoc ~S"""
3 | Authentication system for the platform.
4 |
5 | See `register/1` for creating an account and `sign_in/2` for signing in.
6 | """
7 |
8 | alias Auth.{Account, Repo}
9 |
10 | def register(params) do
11 | Account.build(params)
12 | |> Repo.insert()
13 | end
14 |
15 | def sign_in(email, password) do
16 | account = Repo.get_by(Account, email: email)
17 | do_sign_in(account, password)
18 | end
19 |
20 | defp do_sign_in(%Account{password_hash: password_hash} = account, password) do
21 | if Comeonin.Bcrypt.checkpw(password, password_hash) do
22 | {:ok, account}
23 | else
24 | {:error, :unauthorized}
25 | end
26 | end
27 | defp do_sign_in(nil, _) do
28 | Comeonin.Bcrypt.dummy_checkpw()
29 | {:error, :not_found}
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/apps/auth/lib/auth/account.ex:
--------------------------------------------------------------------------------
1 | defmodule Auth.Account do
2 | use Ecto.Schema
3 | import Ecto.Changeset
4 |
5 | schema "auth_accounts" do
6 | field :email, :string
7 | field :password_hash, :string
8 | field :password, :string, virtual: true
9 |
10 | timestamps()
11 | end
12 |
13 | def build(params) do
14 | changeset(%Auth.Account{}, params)
15 | end
16 |
17 | def changeset(account, params \\ %{}) do
18 | cast(account, params, ~w(email password))
19 | |> validate_required([:email, :password])
20 | |> validate_format(:email, ~r/.*@.*/)
21 | |> validate_length(:password, min: 8)
22 | |> unique_constraint(:email)
23 | |> put_password_hash()
24 | end
25 |
26 | defp put_password_hash(%{changes: %{password: password}} = changeset) do
27 | put_change(changeset, :password_hash, Comeonin.Bcrypt.hashpwsalt(password))
28 | end
29 | defp put_password_hash(%{changes: %{}} = changeset), do: changeset
30 | end
31 |
--------------------------------------------------------------------------------
/apps/auth/lib/auth/application.ex:
--------------------------------------------------------------------------------
1 | defmodule Auth.Application do
2 | use Application
3 |
4 | def start(_type, _args) do
5 | import Supervisor.Spec, warn: false
6 |
7 | children = [
8 | supervisor(Auth.Repo, [])
9 | ]
10 |
11 | opts = [strategy: :one_for_one, name: Auth.Supervisor]
12 | Supervisor.start_link(children, opts)
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/apps/auth/lib/auth/repo.ex:
--------------------------------------------------------------------------------
1 | defmodule Auth.Repo do
2 | use Ecto.Repo, otp_app: :auth
3 | end
4 |
--------------------------------------------------------------------------------
/apps/auth/mix.exs:
--------------------------------------------------------------------------------
1 | defmodule Auth.Mixfile do
2 | use Mix.Project
3 |
4 | def project do
5 | [app: :auth,
6 | version: "0.1.0",
7 | build_path: "../../_build",
8 | config_path: "../../config/config.exs",
9 | deps_path: "../../deps",
10 | lockfile: "../../mix.lock",
11 | elixir: "~> 1.4.2",
12 | build_embedded: Mix.env == :prod,
13 | start_permanent: Mix.env == :prod,
14 | deps: deps(),
15 | docs: [main: "Auth"]]
16 | end
17 |
18 | def application do
19 | [applications: [:logger, :ecto, :postgrex],
20 | mod: {Auth.Application, []}]
21 | end
22 |
23 | defp deps do
24 | [{:ecto, "~> 2.0"},
25 | {:postgrex, ">= 0.0.0"},
26 | {:comeonin, "~> 2.5"}]
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/apps/auth/priv/repo/migrations/20160731211450_create_accounts.exs:
--------------------------------------------------------------------------------
1 | defmodule Auth.Repo.Migrations.CreateAccounts do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:auth_accounts) do
6 | add :email, :string
7 | add :password_hash, :string
8 |
9 | timestamps()
10 | end
11 |
12 | create unique_index(:auth_accounts, [:email])
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/apps/auth/test/auth_test.exs:
--------------------------------------------------------------------------------
1 | defmodule AuthTest do
2 | use ExUnit.Case
3 |
4 | setup _tags do
5 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(Auth.Repo, [])
6 | :ok
7 | end
8 |
9 | test "register: success" do
10 | assert {:ok, alice} = Auth.register(%{email: "alice@example.com", password: "secret12"})
11 |
12 | assert alice.email == "alice@example.com"
13 | assert Comeonin.Bcrypt.checkpw("secret12", alice.password_hash)
14 | end
15 |
16 | test "register: failure" do
17 | {:ok, _} = Auth.register(%{email: "alice@example.com", password: "secret12"})
18 | {:error, _} = Auth.register(%{email: "alice@example.com", password: "secret12"})
19 | {:error, _} = Auth.register(%{email: "bob@example.com", password: ""})
20 | end
21 |
22 | test "sign in" do
23 | {:ok, alice} = Auth.register(%{email: "alice@example.com", password: "secret12"})
24 | id = alice.id
25 |
26 | assert {:ok, %{id: ^id}} = Auth.sign_in("alice@example.com", "secret12")
27 | assert {:error, :unauthorized} = Auth.sign_in("alice@example.com", "bad")
28 | assert {:error, :not_found} = Auth.sign_in("bad@example.com", "bad")
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/apps/auth/test/test_helper.exs:
--------------------------------------------------------------------------------
1 | ExUnit.start()
2 |
3 | Ecto.Adapters.SQL.Sandbox.mode(Auth.Repo, :manual)
4 |
--------------------------------------------------------------------------------
/apps/backoffice/.gitignore:
--------------------------------------------------------------------------------
1 | # App artifacts
2 | /_build
3 | /db
4 | /deps
5 | /*.ez
6 |
7 | # Generated on crash by the VM
8 | erl_crash.dump
9 |
10 | # Static artifacts
11 | /node_modules
12 |
13 | # Since we are building assets from web/static,
14 | # we ignore priv/static. You may want to comment
15 | # this depending on your deployment strategy.
16 | /priv/static/
17 |
18 | # The config/prod.secret.exs file by default contains sensitive
19 | # data and you should not commit it into version control.
20 | #
21 | # Alternatively, you may comment the line below and commit the
22 | # secrets file as long as you replace its contents by environment
23 | # variables.
24 | /config/prod.secret.exs
25 |
--------------------------------------------------------------------------------
/apps/backoffice/README.md:
--------------------------------------------------------------------------------
1 | # Backoffice
2 |
3 | Backoffice provides administrative capabilities to the operators of the platform.
4 |
5 | Built using [ExAdmin](https://github.com/smpallen99/ex_admin).
6 |
7 | ## Setup
8 |
9 | To start your Phoenix app:
10 |
11 | * Install dependencies with `mix deps.get`
12 | * Create and migrate your database with `mix ecto.setup`
13 | * Install Node.js dependencies with `npm install`
14 | * Start Phoenix endpoint with `mix phoenix.server`
15 |
16 | Now you can visit [`localhost:4001`](http://localhost:4001) from your browser.
17 |
--------------------------------------------------------------------------------
/apps/backoffice/brunch-config.js:
--------------------------------------------------------------------------------
1 | exports.config = {
2 | // See http://brunch.io/#documentation for docs.
3 | files: {
4 | javascripts: {
5 | joinTo: {
6 | "js/app.js": /^(web\/static\/js)|(node_modules)/,
7 | "js/ex_admin_common.js": ["web/static/vendor/ex_admin_common.js"],
8 | "js/admin_lte2.js": ["web/static/vendor/admin_lte2.js"],
9 | "js/jquery.min.js": ["web/static/vendor/jquery.min.js"],
10 | }
11 | },
12 | stylesheets: {
13 | joinTo: {
14 | "css/app.css": /^(web\/static\/css)/,
15 | "css/admin_lte2.css": ["web/static/vendor/admin_lte2.css"],
16 | "css/active_admin.css": ["web/static/vendor/active_admin.css.css"],
17 | },
18 | order: {
19 | after: ["web/static/css/app.css"] // concat app.css last
20 | }
21 | },
22 | templates: {
23 | joinTo: "js/app.js"
24 | }
25 | },
26 |
27 | conventions: {
28 | // This option sets where we should place non-css and non-js assets in.
29 | // By default, we set this to "/web/static/assets". Files in this directory
30 | // will be copied to `paths.public`, which is "priv/static" by default.
31 | assets: /^(web\/static\/assets)/
32 | },
33 |
34 | // Phoenix paths configuration
35 | paths: {
36 | // Dependencies and current project directories to watch
37 | watched: [
38 | "web/static",
39 | "test/static"
40 | ],
41 |
42 | // Where to compile files to
43 | public: "priv/static"
44 | },
45 |
46 | // Configure your plugins
47 | plugins: {
48 | babel: {
49 | // Do not use ES6 compiler in vendor code
50 | ignore: [/web\/static\/vendor/]
51 | }
52 | },
53 |
54 | modules: {
55 | autoRequire: {
56 | "js/app.js": ["web/static/js/app"]
57 | }
58 | },
59 |
60 | npm: {
61 | enabled: true
62 | }
63 | };
64 |
--------------------------------------------------------------------------------
/apps/backoffice/config/config.exs:
--------------------------------------------------------------------------------
1 | # This file is responsible for configuring your application
2 | # and its dependencies with the aid of the Mix.Config module.
3 | #
4 | # This configuration file is loaded before any dependency and
5 | # is restricted to this project.
6 | use Mix.Config
7 |
8 | # General application configuration
9 | config :backoffice,
10 | ecto_repos: []
11 |
12 | # Configures the endpoint
13 | config :backoffice, Backoffice.Endpoint,
14 | url: [host: "localhost", path: "/"],
15 | static_url: [host: "localhost", path: "/backoffice"],
16 | secret_key_base: "oqP7+8kIDrpIetIPddC3hf7pmNxOZbTjqUpWkR6nsvJelI7kfR7bhaBE0PP9pdDa",
17 | render_errors: [view: Backoffice.ErrorView, accepts: ~w(html json)],
18 | pubsub: [name: Backoffice.PubSub,
19 | adapter: Phoenix.PubSub.PG2]
20 |
21 | # Configures Elixir's Logger
22 | config :logger, :console,
23 | format: "$time $metadata[$level] $message\n",
24 | metadata: [:request_id]
25 |
26 |
27 | import_config "#{Mix.env}.exs"
28 |
29 | config :xain, :after_callback, {Phoenix.HTML, :raw}
30 |
31 | config :ex_admin,
32 | # TODO: for now, all DB operations go through Bank.Repo, even managing
33 | # Auth.Account etc (which should be done through Auth.Repo).
34 | # This works, because both Repos use the same DB. (which happens
35 | # to be also useful for Heroku deployment.)
36 | #
37 | # See: https://github.com/smpallen99/ex_admin/issues/138 for a
38 | # proper multi-repo support in ExAdmin.
39 | repo: Bank.Repo,
40 |
41 | module: Backoffice,
42 | modules: [
43 | Backoffice.ExAdmin.Dashboard,
44 |
45 | Backoffice.ExAdmin.Bank.Customer,
46 | Backoffice.ExAdmin.Bank.LedgerAccount,
47 | Backoffice.ExAdmin.Bank.Ledger.Entry,
48 |
49 | Backoffice.ExAdmin.AuthAccount,
50 | ]
51 |
--------------------------------------------------------------------------------
/apps/backoffice/config/dev.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
3 | # For development, we disable any cache and enable
4 | # debugging and code reloading.
5 | #
6 | # The watchers configuration can be used to run external
7 | # watchers to your application. For example, we use it
8 | # with brunch.io to recompile .js and .css sources.
9 | config :backoffice, Backoffice.Endpoint,
10 | http: [port: 4001],
11 | debug_errors: true,
12 | code_reloader: true,
13 | check_origin: false,
14 | watchers: [node: ["node_modules/brunch/bin/brunch", "watch", "--stdin",
15 | cd: Path.expand("../", __DIR__)]]
16 |
17 |
18 | # Watch static and templates for browser reloading.
19 | config :backoffice, Backoffice.Endpoint,
20 | live_reload: [
21 | patterns: [
22 | ~r{priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$},
23 | ~r{priv/gettext/.*(po)$},
24 | ~r{web/views/.*(ex)$},
25 | ~r{web/templates/.*(eex)$}
26 | ]
27 | ]
28 |
29 | # Do not include metadata nor timestamps in development logs
30 | config :logger, :console, format: "[$level] $message\n"
31 |
32 | # Set a higher stacktrace during development. Avoid configuring such
33 | # in production as building large stacktraces may be expensive.
34 | config :phoenix, :stacktrace_depth, 20
35 |
--------------------------------------------------------------------------------
/apps/backoffice/config/prod.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
3 | # For production, we configure the host to read the PORT
4 | # from the system environment. Therefore, you will need
5 | # to set PORT=80 before running your server.
6 | #
7 | # You should also configure the url host to something
8 | # meaningful, we use this information when generating URLs.
9 | #
10 | # Finally, we also include the path to a manifest
11 | # containing the digested version of static files. This
12 | # manifest is generated by the mix phoenix.digest task
13 | # which you typically run after static files are built.
14 | config :backoffice, Backoffice.Endpoint,
15 | # http: [port: {:system, "PORT"}],
16 | url: [scheme: "https", host: "acme-bank.herokuapp.com", port: 443],
17 | force_ssl: [rewrite_on: [:x_forwarded_proto]],
18 | cache_static_manifest: "priv/static/manifest.json",
19 | secret_key_base: System.get_env("SECRET_KEY_BASE")
20 |
21 | # Do not print debug messages in production
22 | config :logger, level: :info
23 |
24 | # ## SSL Support
25 | #
26 | # To get SSL working, you will need to add the `https` key
27 | # to the previous section and set your `:url` port to 443:
28 | #
29 | # config :backoffice, Backoffice.Endpoint,
30 | # ...
31 | # url: [host: "example.com", port: 443],
32 | # https: [port: 443,
33 | # keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"),
34 | # certfile: System.get_env("SOME_APP_SSL_CERT_PATH")]
35 | #
36 | # Where those two env variables return an absolute path to
37 | # the key and cert in disk or a relative path inside priv,
38 | # for example "priv/ssl/server.key".
39 | #
40 | # We also recommend setting `force_ssl`, ensuring no data is
41 | # ever sent via http, always redirecting to https:
42 | #
43 | # config :backoffice, Backoffice.Endpoint,
44 | # force_ssl: [hsts: true]
45 | #
46 | # Check `Plug.SSL` for all available options in `force_ssl`.
47 |
48 | # ## Using releases
49 | #
50 | # If you are doing OTP releases, you need to instruct Phoenix
51 | # to start the server for all endpoints:
52 | #
53 | # config :phoenix, :serve_endpoints, true
54 | #
55 | # Alternatively, you can configure exactly which server to
56 | # start per endpoint:
57 | #
58 | # config :backoffice, Backoffice.Endpoint, server: true
59 | #
60 | # You will also need to set the application root to `.` in order
61 | # for the new static assets to be served after a hot upgrade:
62 | #
63 | # config :backoffice, Backoffice.Endpoint, root: "."
64 |
65 | # Finally import the config/prod.secret.exs
66 | # which should be versioned separately.
67 | # import_config "prod.secret.exs"
68 |
--------------------------------------------------------------------------------
/apps/backoffice/config/test.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
3 | # We don't run a server during test. If one is required,
4 | # you can enable the server option below.
5 | config :backoffice, Backoffice.Endpoint,
6 | http: [port: 4001],
7 | server: false
8 |
9 | # Print only warnings and errors during test
10 | config :logger, level: :warn
11 |
--------------------------------------------------------------------------------
/apps/backoffice/lib/backoffice.ex:
--------------------------------------------------------------------------------
1 | defmodule Backoffice do
2 | @moduledoc """
3 | Backoffice provides administrative capabilities to the operators of the platform.
4 |
5 | Built with `ExAdmin`.
6 | """
7 | end
8 |
--------------------------------------------------------------------------------
/apps/backoffice/lib/backoffice/application.ex:
--------------------------------------------------------------------------------
1 | defmodule Backoffice.Application do
2 | @moduledoc false
3 |
4 | use Application
5 |
6 | def start(_type, _args) do
7 | import Supervisor.Spec
8 |
9 | children = [
10 | supervisor(Backoffice.Endpoint, []),
11 | ]
12 |
13 | opts = [strategy: :one_for_one, name: Backoffice.Supervisor]
14 | Supervisor.start_link(children, opts)
15 | end
16 |
17 | def config_change(changed, _new, removed) do
18 | Backoffice.Endpoint.config_change(changed, removed)
19 | :ok
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/apps/backoffice/lib/backoffice/endpoint.ex:
--------------------------------------------------------------------------------
1 | defmodule Backoffice.Endpoint do
2 | use Phoenix.Endpoint, otp_app: :backoffice
3 |
4 | # Serve at "/" the static files from "priv/static" directory.
5 | #
6 | # You should set gzip to true if you are running phoenix.digest
7 | # when deploying your static files in production.
8 | plug Plug.Static,
9 | at: "/backoffice", from: :backoffice, gzip: false,
10 | only: ~w(css fonts images js favicon.ico robots.txt)
11 |
12 | # Code reloading can be explicitly enabled under the
13 | # :code_reloader configuration of your endpoint.
14 | if code_reloading? do
15 | socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket
16 | plug Phoenix.LiveReloader
17 | plug Phoenix.CodeReloader
18 | end
19 |
20 | plug Plug.RequestId
21 | plug Plug.Logger
22 |
23 | plug Plug.Parsers,
24 | parsers: [:urlencoded, :multipart, :json],
25 | pass: ["*/*"],
26 | json_decoder: Poison
27 |
28 | plug Plug.MethodOverride
29 | plug Plug.Head
30 |
31 | # The session will be stored in the cookie and signed,
32 | # this means its contents can be read but not tampered with.
33 | # Set :encryption_salt if you would also like to encrypt it.
34 | plug Plug.Session,
35 | store: :cookie,
36 | key: "_backoffice_key",
37 | signing_salt: "ipXaUny2"
38 |
39 | plug Backoffice.Router
40 | end
41 |
--------------------------------------------------------------------------------
/apps/backoffice/mix.exs:
--------------------------------------------------------------------------------
1 | defmodule Backoffice.Mixfile do
2 | use Mix.Project
3 |
4 | def project do
5 | [app: :backoffice,
6 | version: "0.0.1",
7 | build_path: "../../_build",
8 | config_path: "../../config/config.exs",
9 | deps_path: "../../deps",
10 | lockfile: "../../mix.lock",
11 | elixir: "~> 1.4.2",
12 | elixirc_paths: elixirc_paths(Mix.env),
13 | compilers: [:phoenix, :gettext] ++ Mix.compilers,
14 | build_embedded: Mix.env == :prod,
15 | start_permanent: Mix.env == :prod,
16 | aliases: aliases(),
17 | deps: deps()]
18 | end
19 |
20 | def application do
21 | [mod: {Backoffice.Application, []},
22 | applications: [:phoenix, :phoenix_pubsub, :phoenix_html, :cowboy, :logger, :gettext,
23 | :phoenix_ecto, :postgrex, :bank]]
24 | end
25 |
26 | defp elixirc_paths(:test), do: ["lib", "web", "test/support"]
27 | defp elixirc_paths(_), do: ["lib", "web"]
28 |
29 | defp deps do
30 | [{:phoenix, "~> 1.2.0"},
31 | {:phoenix_pubsub, "~> 1.0"},
32 | {:phoenix_ecto, "~> 3.0"},
33 | {:postgrex, ">= 0.0.0"},
34 | {:phoenix_html, "~> 2.6"},
35 | {:phoenix_live_reload, "~> 1.0", only: :dev},
36 | {:gettext, "~> 0.11"},
37 | {:cowboy, "~> 1.0"},
38 | {:ex_admin, github: "wojtekmach/ex_admin", branch: "wm-customize-resource-name"},
39 | {:ex_queb, github: "E-MetroTel/ex_queb", override: true},
40 |
41 | {:bank, in_umbrella: true},
42 | ]
43 | end
44 |
45 | defp aliases do
46 | ["ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
47 | "ecto.reset": ["ecto.drop", "ecto.setup"],
48 | "test": ["ecto.create --quiet", "ecto.migrate", "test"]]
49 | end
50 | end
51 |
--------------------------------------------------------------------------------
/apps/backoffice/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "repository": {},
3 | "license": "MIT",
4 | "scripts": {
5 | "deploy": "brunch build --production",
6 | "watch": "brunch watch --stdin"
7 | },
8 | "dependencies": {
9 | "phoenix": "file:../../deps/phoenix",
10 | "phoenix_html": "file:../../deps/phoenix_html"
11 | },
12 | "devDependencies": {
13 | "babel-brunch": "~6.0.0",
14 | "brunch": "2.7.4",
15 | "clean-css-brunch": "~2.0.0",
16 | "css-brunch": "~2.0.0",
17 | "javascript-brunch": "~2.0.0",
18 | "uglify-js-brunch": "~2.0.1"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/apps/backoffice/priv/gettext/en/LC_MESSAGES/errors.po:
--------------------------------------------------------------------------------
1 | ## `msgid`s in this file come from POT (.pot) files.
2 | ##
3 | ## Do not add, change, or remove `msgid`s manually here as
4 | ## they're tied to the ones in the corresponding POT file
5 | ## (with the same domain).
6 | ##
7 | ## Use `mix gettext.extract --merge` or `mix gettext.merge`
8 | ## to merge POT files into PO files.
9 | msgid ""
10 | msgstr ""
11 | "Language: en\n"
12 |
13 | ## From Ecto.Changeset.cast/4
14 | msgid "can't be blank"
15 | msgstr ""
16 |
17 | ## From Ecto.Changeset.unique_constraint/3
18 | msgid "has already been taken"
19 | msgstr ""
20 |
21 | ## From Ecto.Changeset.put_change/3
22 | msgid "is invalid"
23 | msgstr ""
24 |
25 | ## From Ecto.Changeset.validate_format/3
26 | msgid "has invalid format"
27 | msgstr ""
28 |
29 | ## From Ecto.Changeset.validate_subset/3
30 | msgid "has an invalid entry"
31 | msgstr ""
32 |
33 | ## From Ecto.Changeset.validate_exclusion/3
34 | msgid "is reserved"
35 | msgstr ""
36 |
37 | ## From Ecto.Changeset.validate_confirmation/3
38 | msgid "does not match confirmation"
39 | msgstr ""
40 |
41 | ## From Ecto.Changeset.no_assoc_constraint/3
42 | msgid "is still associated to this entry"
43 | msgstr ""
44 |
45 | msgid "are still associated to this entry"
46 | msgstr ""
47 |
48 | ## From Ecto.Changeset.validate_length/3
49 | msgid "should be %{count} character(s)"
50 | msgid_plural "should be %{count} character(s)"
51 | msgstr[0] ""
52 | msgstr[1] ""
53 |
54 | msgid "should have %{count} item(s)"
55 | msgid_plural "should have %{count} item(s)"
56 | msgstr[0] ""
57 | msgstr[1] ""
58 |
59 | msgid "should be at least %{count} character(s)"
60 | msgid_plural "should be at least %{count} character(s)"
61 | msgstr[0] ""
62 | msgstr[1] ""
63 |
64 | msgid "should have at least %{count} item(s)"
65 | msgid_plural "should have at least %{count} item(s)"
66 | msgstr[0] ""
67 | msgstr[1] ""
68 |
69 | msgid "should be at most %{count} character(s)"
70 | msgid_plural "should be at most %{count} character(s)"
71 | msgstr[0] ""
72 | msgstr[1] ""
73 |
74 | msgid "should have at most %{count} item(s)"
75 | msgid_plural "should have at most %{count} item(s)"
76 | msgstr[0] ""
77 | msgstr[1] ""
78 |
79 | ## From Ecto.Changeset.validate_number/3
80 | msgid "must be less than %{number}"
81 | msgstr ""
82 |
83 | msgid "must be greater than %{number}"
84 | msgstr ""
85 |
86 | msgid "must be less than or equal to %{number}"
87 | msgstr ""
88 |
89 | msgid "must be greater than or equal to %{number}"
90 | msgstr ""
91 |
92 | msgid "must be equal to %{number}"
93 | msgstr ""
94 |
--------------------------------------------------------------------------------
/apps/backoffice/priv/gettext/errors.pot:
--------------------------------------------------------------------------------
1 | ## This file is a PO Template file.
2 | ##
3 | ## `msgid`s here are often extracted from source code.
4 | ## Add new translations manually only if they're dynamic
5 | ## translations that can't be statically extracted.
6 | ##
7 | ## Run `mix gettext.extract` to bring this file up to
8 | ## date. Leave `msgstr`s empty as changing them here as no
9 | ## effect: edit them in PO (`.po`) files instead.
10 |
11 | ## From Ecto.Changeset.cast/4
12 | msgid "can't be blank"
13 | msgstr ""
14 |
15 | ## From Ecto.Changeset.unique_constraint/3
16 | msgid "has already been taken"
17 | msgstr ""
18 |
19 | ## From Ecto.Changeset.put_change/3
20 | msgid "is invalid"
21 | msgstr ""
22 |
23 | ## From Ecto.Changeset.validate_format/3
24 | msgid "has invalid format"
25 | msgstr ""
26 |
27 | ## From Ecto.Changeset.validate_subset/3
28 | msgid "has an invalid entry"
29 | msgstr ""
30 |
31 | ## From Ecto.Changeset.validate_exclusion/3
32 | msgid "is reserved"
33 | msgstr ""
34 |
35 | ## From Ecto.Changeset.validate_confirmation/3
36 | msgid "does not match confirmation"
37 | msgstr ""
38 |
39 | ## From Ecto.Changeset.no_assoc_constraint/3
40 | msgid "is still associated to this entry"
41 | msgstr ""
42 |
43 | msgid "are still associated to this entry"
44 | msgstr ""
45 |
46 | ## From Ecto.Changeset.validate_length/3
47 | msgid "should be %{count} character(s)"
48 | msgid_plural "should be %{count} character(s)"
49 | msgstr[0] ""
50 | msgstr[1] ""
51 |
52 | msgid "should have %{count} item(s)"
53 | msgid_plural "should have %{count} item(s)"
54 | msgstr[0] ""
55 | msgstr[1] ""
56 |
57 | msgid "should be at least %{count} character(s)"
58 | msgid_plural "should be at least %{count} character(s)"
59 | msgstr[0] ""
60 | msgstr[1] ""
61 |
62 | msgid "should have at least %{count} item(s)"
63 | msgid_plural "should have at least %{count} item(s)"
64 | msgstr[0] ""
65 | msgstr[1] ""
66 |
67 | msgid "should be at most %{count} character(s)"
68 | msgid_plural "should be at most %{count} character(s)"
69 | msgstr[0] ""
70 | msgstr[1] ""
71 |
72 | msgid "should have at most %{count} item(s)"
73 | msgid_plural "should have at most %{count} item(s)"
74 | msgstr[0] ""
75 | msgstr[1] ""
76 |
77 | ## From Ecto.Changeset.validate_number/3
78 | msgid "must be less than %{number}"
79 | msgstr ""
80 |
81 | msgid "must be greater than %{number}"
82 | msgstr ""
83 |
84 | msgid "must be less than or equal to %{number}"
85 | msgstr ""
86 |
87 | msgid "must be greater than or equal to %{number}"
88 | msgstr ""
89 |
90 | msgid "must be equal to %{number}"
91 | msgstr ""
92 |
--------------------------------------------------------------------------------
/apps/backoffice/priv/repo/seeds.exs:
--------------------------------------------------------------------------------
1 | # Script for populating the database. You can run it as:
2 | #
3 | # mix run priv/repo/seeds.exs
4 | #
5 | # Inside the script, you can read and write to any of your
6 | # repositories directly:
7 | #
8 | # Backoffice.Repo.insert!(%Backoffice.SomeModel{})
9 | #
10 | # We recommend using the bang functions (`insert!`, `update!`
11 | # and so on) as they will fail if something goes wrong.
12 |
--------------------------------------------------------------------------------
/apps/backoffice/test/controllers/page_controller_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Backoffice.PageControllerTest do
2 | use Backoffice.ConnCase
3 |
4 | test "GET /", %{conn: conn} do
5 | conn = get conn, "/backoffice"
6 | assert html_response(conn, 200) =~ "Welcome to Acme Bank Backoffice"
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/apps/backoffice/test/support/conn_case.ex:
--------------------------------------------------------------------------------
1 | defmodule Backoffice.ConnCase do
2 | @moduledoc """
3 | This module defines the test case to be used by
4 | tests that require setting up a connection.
5 |
6 | Such tests rely on `Phoenix.ConnTest` and also
7 | import other functionality to make it easier
8 | to build and query models.
9 |
10 | Finally, if the test case interacts with the database,
11 | it cannot be async. For this reason, every test runs
12 | inside a transaction which is reset at the beginning
13 | of the test unless the test case is marked as async.
14 | """
15 |
16 | use ExUnit.CaseTemplate
17 |
18 | using do
19 | quote do
20 | # Import conveniences for testing with connections
21 | use Phoenix.ConnTest
22 |
23 | import Ecto
24 | import Ecto.Changeset
25 | import Ecto.Query
26 |
27 | import Backoffice.Router.Helpers
28 |
29 | # The default endpoint for testing
30 | @endpoint Backoffice.Endpoint
31 | end
32 | end
33 |
34 | setup _tags do
35 | {:ok, conn: Phoenix.ConnTest.build_conn()}
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/apps/backoffice/test/support/model_case.ex:
--------------------------------------------------------------------------------
1 | defmodule Backoffice.ModelCase do
2 | @moduledoc """
3 | This module defines the test case to be used by
4 | model tests.
5 |
6 | You may define functions here to be used as helpers in
7 | your model tests. See `errors_on/2`'s definition as reference.
8 |
9 | Finally, if the test case interacts with the database,
10 | it cannot be async. For this reason, every test runs
11 | inside a transaction which is reset at the beginning
12 | of the test unless the test case is marked as async.
13 | """
14 |
15 | use ExUnit.CaseTemplate
16 |
17 | using do
18 | quote do
19 | import Ecto
20 | import Ecto.Changeset
21 | import Ecto.Query
22 | import Backoffice.ModelCase
23 | end
24 | end
25 |
26 | setup _tags do
27 | :ok
28 | end
29 |
30 | @doc """
31 | Helper for returning list of errors in a struct when given certain data.
32 |
33 | ## Examples
34 |
35 | Given a User schema that lists `:name` as a required field and validates
36 | `:password` to be safe, it would return:
37 |
38 | iex> errors_on(%User{}, %{password: "password"})
39 | [password: "is unsafe", name: "is blank"]
40 |
41 | You could then write your assertion like:
42 |
43 | assert {:password, "is unsafe"} in errors_on(%User{}, %{password: "password"})
44 |
45 | You can also create the changeset manually and retrieve the errors
46 | field directly:
47 |
48 | iex> changeset = User.changeset(%User{}, password: "password")
49 | iex> {:password, "is unsafe"} in changeset.errors
50 | true
51 | """
52 | def errors_on(struct, data) do
53 | struct.__struct__.changeset(struct, data)
54 | |> Ecto.Changeset.traverse_errors(&Backoffice.ErrorHelpers.translate_error/1)
55 | |> Enum.flat_map(fn {key, errors} -> for msg <- errors, do: {key, msg} end)
56 | end
57 | end
58 |
--------------------------------------------------------------------------------
/apps/backoffice/test/test_helper.exs:
--------------------------------------------------------------------------------
1 | ExUnit.start
2 |
3 |
--------------------------------------------------------------------------------
/apps/backoffice/test/views/error_view_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Backoffice.ErrorViewTest do
2 | use Backoffice.ConnCase, async: true
3 |
4 | # Bring render/3 and render_to_string/3 for testing custom views
5 | import Phoenix.View
6 |
7 | test "renders 404.html" do
8 | assert render_to_string(Backoffice.ErrorView, "404.html", []) ==
9 | "Page not found"
10 | end
11 |
12 | test "render 500.html" do
13 | assert render_to_string(Backoffice.ErrorView, "500.html", []) ==
14 | "Internal server error"
15 | end
16 |
17 | test "render any other" do
18 | assert render_to_string(Backoffice.ErrorView, "505.html", []) ==
19 | "Internal server error"
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/apps/backoffice/test/views/layout_view_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Backoffice.LayoutViewTest do
2 | use Backoffice.ConnCase, async: true
3 | end
4 |
--------------------------------------------------------------------------------
/apps/backoffice/test/views/page_view_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Backoffice.PageViewTest do
2 | use Backoffice.ConnCase, async: true
3 | end
4 |
--------------------------------------------------------------------------------
/apps/backoffice/web/admin/auth/account.ex:
--------------------------------------------------------------------------------
1 | defmodule Backoffice.ExAdmin.AuthAccount do
2 | use ExAdmin.Register
3 |
4 | register_resource Auth.Account do
5 | menu label: "Auth Accounts"
6 | options resource_name: "auth_account", controller_route: "auth_accounts"
7 |
8 | filter [:id, :email]
9 |
10 | index do
11 | column :id
12 | column :email
13 | end
14 |
15 | show _account do
16 | attributes_table do
17 | row :id
18 | row :email
19 | row :inserted_at
20 | row :updated_at
21 | end
22 | end
23 |
24 | form account do
25 | inputs do
26 | input account, :email
27 |
28 | unless account.id do
29 | input account, :password
30 | end
31 | end
32 | end
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/apps/backoffice/web/admin/bank/customer.ex:
--------------------------------------------------------------------------------
1 | defmodule Backoffice.ExAdmin.Bank.Customer do
2 | use ExAdmin.Register
3 |
4 | register_resource Bank.Customer do
5 | menu label: "Bank Customers"
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/apps/backoffice/web/admin/bank/ledger/account.ex:
--------------------------------------------------------------------------------
1 | defmodule Backoffice.ExAdmin.Bank.LedgerAccount do
2 | use ExAdmin.Register
3 |
4 | register_resource Bank.Ledger.Account do
5 | menu label: "Bank Accounts"
6 | options resource_name: "bank_account", controller_route: "bank_accounts"
7 |
8 | index do
9 | column :id
10 | column :type
11 | column :name
12 | column :currency
13 | column :balance, fn account ->
14 | Bank.Ledger.balance(account) |> Money.to_string
15 | end
16 | end
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/apps/backoffice/web/admin/bank/ledger/entry.ex:
--------------------------------------------------------------------------------
1 | defmodule Backoffice.ExAdmin.Bank.Ledger.Entry do
2 | use ExAdmin.Register
3 |
4 | register_resource Bank.Ledger.Entry do
5 | menu label: "Bank Entries"
6 |
7 | actions :all, only: [:index]
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/apps/backoffice/web/admin/dashboard.ex:
--------------------------------------------------------------------------------
1 | defimpl ExAdmin.Render, for: Money do
2 | defdelegate to_string(money), to: Money
3 | end
4 |
5 | defmodule Backoffice.ExAdmin.Dashboard do
6 | use ExAdmin.Register
7 |
8 | register_page "Dashboard" do
9 | menu priority: 1, label: "Dashboard"
10 | content do
11 | div ".blank_slate_container#dashboard_default_message" do
12 | span ".blank_slate" do
13 | span "Welcome to Acme Bank Backoffice"
14 | end
15 | end
16 | end
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/apps/backoffice/web/controllers/page_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule Backoffice.PageController do
2 | use Backoffice.Web, :controller
3 |
4 | def index(conn, _params) do
5 | render conn, "index.html"
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/apps/backoffice/web/gettext.ex:
--------------------------------------------------------------------------------
1 | defmodule Backoffice.Gettext do
2 | @moduledoc """
3 | A module providing Internationalization with a gettext-based API.
4 |
5 | By using [Gettext](https://hexdocs.pm/gettext),
6 | your module gains a set of macros for translations, for example:
7 |
8 | import Backoffice.Gettext
9 |
10 | # Simple translation
11 | gettext "Here is the string to translate"
12 |
13 | # Plural translation
14 | ngettext "Here is the string to translate",
15 | "Here are the strings to translate",
16 | 3
17 |
18 | # Domain-based translation
19 | dgettext "errors", "Here is the error message to translate"
20 |
21 | See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage.
22 | """
23 | use Gettext, otp_app: :backoffice
24 | end
25 |
--------------------------------------------------------------------------------
/apps/backoffice/web/router.ex:
--------------------------------------------------------------------------------
1 | defmodule Backoffice.Router do
2 | use Backoffice.Web, :router
3 |
4 | pipeline :browser do
5 | plug :accepts, ["html"]
6 | plug :fetch_session
7 | plug :fetch_flash
8 | plug :protect_from_forgery
9 | plug :put_secure_browser_headers
10 | end
11 |
12 | use ExAdmin.Router
13 |
14 | scope "/backoffice", ExAdmin do
15 | pipe_through :browser
16 | admin_routes
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/apps/backoffice/web/static/assets/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wojtekmach/acme_bank/cee19a490d2b3c04465273b7a6212d7e6a81f736/apps/backoffice/web/static/assets/favicon.ico
--------------------------------------------------------------------------------
/apps/backoffice/web/static/assets/fonts/FontAwesome.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wojtekmach/acme_bank/cee19a490d2b3c04465273b7a6212d7e6a81f736/apps/backoffice/web/static/assets/fonts/FontAwesome.otf
--------------------------------------------------------------------------------
/apps/backoffice/web/static/assets/fonts/fontawesome-webfont.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wojtekmach/acme_bank/cee19a490d2b3c04465273b7a6212d7e6a81f736/apps/backoffice/web/static/assets/fonts/fontawesome-webfont.eot
--------------------------------------------------------------------------------
/apps/backoffice/web/static/assets/fonts/fontawesome-webfont.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wojtekmach/acme_bank/cee19a490d2b3c04465273b7a6212d7e6a81f736/apps/backoffice/web/static/assets/fonts/fontawesome-webfont.ttf
--------------------------------------------------------------------------------
/apps/backoffice/web/static/assets/fonts/fontawesome-webfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wojtekmach/acme_bank/cee19a490d2b3c04465273b7a6212d7e6a81f736/apps/backoffice/web/static/assets/fonts/fontawesome-webfont.woff
--------------------------------------------------------------------------------
/apps/backoffice/web/static/assets/fonts/fontawesome-webfont.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wojtekmach/acme_bank/cee19a490d2b3c04465273b7a6212d7e6a81f736/apps/backoffice/web/static/assets/fonts/fontawesome-webfont.woff2
--------------------------------------------------------------------------------
/apps/backoffice/web/static/assets/fonts/ionicons.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wojtekmach/acme_bank/cee19a490d2b3c04465273b7a6212d7e6a81f736/apps/backoffice/web/static/assets/fonts/ionicons.eot
--------------------------------------------------------------------------------
/apps/backoffice/web/static/assets/fonts/ionicons.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wojtekmach/acme_bank/cee19a490d2b3c04465273b7a6212d7e6a81f736/apps/backoffice/web/static/assets/fonts/ionicons.ttf
--------------------------------------------------------------------------------
/apps/backoffice/web/static/assets/fonts/ionicons.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wojtekmach/acme_bank/cee19a490d2b3c04465273b7a6212d7e6a81f736/apps/backoffice/web/static/assets/fonts/ionicons.woff
--------------------------------------------------------------------------------
/apps/backoffice/web/static/assets/images/ex_admin/admin_notes_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wojtekmach/acme_bank/cee19a490d2b3c04465273b7a6212d7e6a81f736/apps/backoffice/web/static/assets/images/ex_admin/admin_notes_icon.png
--------------------------------------------------------------------------------
/apps/backoffice/web/static/assets/images/ex_admin/datepicker/datepicker-header-bg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wojtekmach/acme_bank/cee19a490d2b3c04465273b7a6212d7e6a81f736/apps/backoffice/web/static/assets/images/ex_admin/datepicker/datepicker-header-bg.png
--------------------------------------------------------------------------------
/apps/backoffice/web/static/assets/images/ex_admin/datepicker/datepicker-input-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wojtekmach/acme_bank/cee19a490d2b3c04465273b7a6212d7e6a81f736/apps/backoffice/web/static/assets/images/ex_admin/datepicker/datepicker-input-icon.png
--------------------------------------------------------------------------------
/apps/backoffice/web/static/assets/images/ex_admin/datepicker/datepicker-next-link-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wojtekmach/acme_bank/cee19a490d2b3c04465273b7a6212d7e6a81f736/apps/backoffice/web/static/assets/images/ex_admin/datepicker/datepicker-next-link-icon.png
--------------------------------------------------------------------------------
/apps/backoffice/web/static/assets/images/ex_admin/datepicker/datepicker-nipple.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wojtekmach/acme_bank/cee19a490d2b3c04465273b7a6212d7e6a81f736/apps/backoffice/web/static/assets/images/ex_admin/datepicker/datepicker-nipple.png
--------------------------------------------------------------------------------
/apps/backoffice/web/static/assets/images/ex_admin/datepicker/datepicker-prev-link-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wojtekmach/acme_bank/cee19a490d2b3c04465273b7a6212d7e6a81f736/apps/backoffice/web/static/assets/images/ex_admin/datepicker/datepicker-prev-link-icon.png
--------------------------------------------------------------------------------
/apps/backoffice/web/static/assets/images/ex_admin/glyphicons-halflings-white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wojtekmach/acme_bank/cee19a490d2b3c04465273b7a6212d7e6a81f736/apps/backoffice/web/static/assets/images/ex_admin/glyphicons-halflings-white.png
--------------------------------------------------------------------------------
/apps/backoffice/web/static/assets/images/ex_admin/glyphicons-halflings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wojtekmach/acme_bank/cee19a490d2b3c04465273b7a6212d7e6a81f736/apps/backoffice/web/static/assets/images/ex_admin/glyphicons-halflings.png
--------------------------------------------------------------------------------
/apps/backoffice/web/static/assets/images/ex_admin/orderable.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wojtekmach/acme_bank/cee19a490d2b3c04465273b7a6212d7e6a81f736/apps/backoffice/web/static/assets/images/ex_admin/orderable.png
--------------------------------------------------------------------------------
/apps/backoffice/web/static/assets/images/phoenix.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wojtekmach/acme_bank/cee19a490d2b3c04465273b7a6212d7e6a81f736/apps/backoffice/web/static/assets/images/phoenix.png
--------------------------------------------------------------------------------
/apps/backoffice/web/static/assets/robots.txt:
--------------------------------------------------------------------------------
1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
2 | #
3 | # To ban all spiders from the entire site uncomment the next two lines:
4 | # User-agent: *
5 | # Disallow: /
6 |
--------------------------------------------------------------------------------
/apps/backoffice/web/static/css/app.css:
--------------------------------------------------------------------------------
1 | /* This file is for your main application css. */
--------------------------------------------------------------------------------
/apps/backoffice/web/static/js/app.js:
--------------------------------------------------------------------------------
1 | // Brunch automatically concatenates all files in your
2 | // watched paths. Those paths can be configured at
3 | // config.paths.watched in "brunch-config.js".
4 | //
5 | // However, those files will only be executed if
6 | // explicitly imported. The only exception are files
7 | // in vendor, which are never wrapped in imports and
8 | // therefore are always executed.
9 |
10 | // Import dependencies
11 | //
12 | // If you no longer want to use a dependency, remember
13 | // to also remove its path from "config.paths.watched".
14 | import "phoenix_html"
15 |
16 | // Import local files
17 | //
18 | // Local files can be imported directly using relative
19 | // paths "./socket" or full ones "web/static/js/socket".
20 |
21 | // import socket from "./socket"
22 |
--------------------------------------------------------------------------------
/apps/backoffice/web/static/js/socket.js:
--------------------------------------------------------------------------------
1 | // NOTE: The contents of this file will only be executed if
2 | // you uncomment its entry in "web/static/js/app.js".
3 |
4 | // To use Phoenix channels, the first step is to import Socket
5 | // and connect at the socket path in "lib/my_app/endpoint.ex":
6 | import {Socket} from "phoenix"
7 |
8 | let socket = new Socket("/socket", {params: {token: window.userToken}})
9 |
10 | // When you connect, you'll often need to authenticate the client.
11 | // For example, imagine you have an authentication plug, `MyAuth`,
12 | // which authenticates the session and assigns a `:current_user`.
13 | // If the current user exists you can assign the user's token in
14 | // the connection for use in the layout.
15 | //
16 | // In your "web/router.ex":
17 | //
18 | // pipeline :browser do
19 | // ...
20 | // plug MyAuth
21 | // plug :put_user_token
22 | // end
23 | //
24 | // defp put_user_token(conn, _) do
25 | // if current_user = conn.assigns[:current_user] do
26 | // token = Phoenix.Token.sign(conn, "user socket", current_user.id)
27 | // assign(conn, :user_token, token)
28 | // else
29 | // conn
30 | // end
31 | // end
32 | //
33 | // Now you need to pass this token to JavaScript. You can do so
34 | // inside a script tag in "web/templates/layout/app.html.eex":
35 | //
36 | //
37 | //
38 | // You will need to verify the user token in the "connect/2" function
39 | // in "web/channels/user_socket.ex":
40 | //
41 | // def connect(%{"token" => token}, socket) do
42 | // # max_age: 1209600 is equivalent to two weeks in seconds
43 | // case Phoenix.Token.verify(socket, "user socket", token, max_age: 1209600) do
44 | // {:ok, user_id} ->
45 | // {:ok, assign(socket, :user, user_id)}
46 | // {:error, reason} ->
47 | // :error
48 | // end
49 | // end
50 | //
51 | // Finally, pass the token on connect as below. Or remove it
52 | // from connect if you don't care about authentication.
53 |
54 | socket.connect()
55 |
56 | // Now that you are connected, you can join channels with a topic:
57 | let channel = socket.channel("topic:subtopic", {})
58 | channel.join()
59 | .receive("ok", resp => { console.log("Joined successfully", resp) })
60 | .receive("error", resp => { console.log("Unable to join", resp) })
61 |
62 | export default socket
63 |
--------------------------------------------------------------------------------
/apps/backoffice/web/templates/layout/app.html.eex:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | Hello Backoffice!
11 | ">
12 |
13 |
14 |
15 |
16 |
24 |
25 |
<%= get_flash(@conn, :info) %>
26 |
<%= get_flash(@conn, :error) %>
27 |
28 |
29 | <%= render @view_module, @view_template, assigns %>
30 |
31 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/apps/backoffice/web/templates/page/index.html.eex:
--------------------------------------------------------------------------------
1 |
2 |
<%= gettext "Welcome to %{name}", name: "Phoenix!" %>
3 |
A productive web framework that
does not compromise speed and maintainability.
4 |
5 |
6 |
37 |
--------------------------------------------------------------------------------
/apps/backoffice/web/views/error_helpers.ex:
--------------------------------------------------------------------------------
1 | defmodule Backoffice.ErrorHelpers do
2 | @moduledoc """
3 | Conveniences for translating and building error messages.
4 | """
5 |
6 | use Phoenix.HTML
7 |
8 | @doc """
9 | Generates tag for inlined form input errors.
10 | """
11 | def error_tag(form, field) do
12 | if error = form.errors[field] do
13 | content_tag :span, translate_error(error), class: "help-block"
14 | end
15 | end
16 |
17 | @doc """
18 | Translates an error message using gettext.
19 | """
20 | def translate_error({msg, opts}) do
21 | # Because error messages were defined within Ecto, we must
22 | # call the Gettext module passing our Gettext backend. We
23 | # also use the "errors" domain as translations are placed
24 | # in the errors.po file.
25 | # Ecto will pass the :count keyword if the error message is
26 | # meant to be pluralized.
27 | # On your own code and templates, depending on whether you
28 | # need the message to be pluralized or not, this could be
29 | # written simply as:
30 | #
31 | # dngettext "errors", "1 file", "%{count} files", count
32 | # dgettext "errors", "is invalid"
33 | #
34 | if count = opts[:count] do
35 | Gettext.dngettext(Backoffice.Gettext, "errors", msg, msg, count, opts)
36 | else
37 | Gettext.dgettext(Backoffice.Gettext, "errors", msg, opts)
38 | end
39 | end
40 | end
41 |
--------------------------------------------------------------------------------
/apps/backoffice/web/views/error_view.ex:
--------------------------------------------------------------------------------
1 | defmodule Backoffice.ErrorView do
2 | use Backoffice.Web, :view
3 |
4 | def render("404.html", _assigns) do
5 | "Page not found"
6 | end
7 |
8 | def render("500.html", _assigns) do
9 | "Internal server error"
10 | end
11 |
12 | # In case no render clause matches or no
13 | # template is found, let's render it as 500
14 | def template_not_found(_template, assigns) do
15 | render "500.html", assigns
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/apps/backoffice/web/views/layout_view.ex:
--------------------------------------------------------------------------------
1 | defmodule Backoffice.LayoutView do
2 | use Backoffice.Web, :view
3 | end
4 |
--------------------------------------------------------------------------------
/apps/backoffice/web/views/page_view.ex:
--------------------------------------------------------------------------------
1 | defmodule Backoffice.PageView do
2 | use Backoffice.Web, :view
3 | end
4 |
--------------------------------------------------------------------------------
/apps/backoffice/web/web.ex:
--------------------------------------------------------------------------------
1 | defmodule Backoffice.Web do
2 | @moduledoc """
3 | A module that keeps using definitions for controllers,
4 | views and so on.
5 |
6 | This can be used in your application as:
7 |
8 | use Backoffice.Web, :controller
9 | use Backoffice.Web, :view
10 |
11 | The definitions below will be executed for every view,
12 | controller, etc, so keep them short and clean, focused
13 | on imports, uses and aliases.
14 |
15 | Do NOT define functions inside the quoted expressions
16 | below.
17 | """
18 |
19 | def model do
20 | quote do
21 | use Ecto.Schema
22 |
23 | import Ecto
24 | import Ecto.Changeset
25 | import Ecto.Query
26 | end
27 | end
28 |
29 | def controller do
30 | quote do
31 | use Phoenix.Controller
32 |
33 | import Ecto
34 | import Ecto.Query
35 |
36 | import Backoffice.Router.Helpers
37 | import Backoffice.Gettext
38 | end
39 | end
40 |
41 | def view do
42 | quote do
43 | use Phoenix.View, root: "web/templates"
44 |
45 | # Import convenience functions from controllers
46 | import Phoenix.Controller, only: [get_csrf_token: 0, get_flash: 2, view_module: 1]
47 |
48 | # Use all HTML functionality (forms, tags, etc)
49 | use Phoenix.HTML
50 |
51 | import Backoffice.Router.Helpers
52 | import Backoffice.ErrorHelpers
53 | import Backoffice.Gettext
54 | end
55 | end
56 |
57 | def router do
58 | quote do
59 | use Phoenix.Router
60 | end
61 | end
62 |
63 | @doc """
64 | When used, dispatch to the appropriate controller/view/etc.
65 | """
66 | defmacro __using__(which) when is_atom(which) do
67 | apply(__MODULE__, which, [])
68 | end
69 | end
70 |
--------------------------------------------------------------------------------
/apps/bank/.gitignore:
--------------------------------------------------------------------------------
1 | # The directory Mix will write compiled artifacts to.
2 | /_build
3 |
4 | # If you run "mix test --cover", coverage assets end up here.
5 | /cover
6 |
7 | # The directory Mix downloads your dependencies sources to.
8 | /deps
9 |
10 | # Where 3rd-party dependencies like ExDoc output generated docs.
11 | /doc
12 |
13 | # If the VM crashes, it generates a dump, let's ignore it too.
14 | erl_crash.dump
15 |
16 | # Also ignore archive artifacts (built via "mix archive.build").
17 | *.ez
18 |
--------------------------------------------------------------------------------
/apps/bank/.iex.exs:
--------------------------------------------------------------------------------
1 | import_file "../../.iex.exs"
2 |
--------------------------------------------------------------------------------
/apps/bank/README.md:
--------------------------------------------------------------------------------
1 | # Bank
2 |
3 | Contains main business logic of the platform.
4 |
5 | See:
6 | - [`Bank`](lib/bank.ex)
7 | - [`Bank.Ledger`](lib/bank/ledger.ex)
8 |
--------------------------------------------------------------------------------
/apps/bank/config/config.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
3 | ## Logger
4 | config :logger, level: :debug
5 |
6 | ## Repo
7 | config :bank, ecto_repos: [Bank.Repo]
8 |
9 | import_config "#{Mix.env}.exs"
10 |
--------------------------------------------------------------------------------
/apps/bank/config/dev.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
3 | # TODO: rename DB to bank_*
4 | config :bank, Bank.Repo,
5 | adapter: Ecto.Adapters.Postgres,
6 | username: "postgres",
7 | password: "postgres",
8 | database: "bank_platform_#{Mix.env}",
9 | hostname: "localhost"
10 |
11 |
--------------------------------------------------------------------------------
/apps/bank/config/prod.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
3 | config :logger, level: :info
4 |
5 | config :bank, Bank.Repo,
6 | adapter: Ecto.Adapters.Postgres,
7 | url: System.get_env("DATABASE_URL"),
8 | pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"),
9 | ssl: true
10 |
--------------------------------------------------------------------------------
/apps/bank/config/test.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
3 | config :bank, Bank.Repo,
4 | adapter: Ecto.Adapters.Postgres,
5 | username: "postgres",
6 | password: "postgres",
7 | database: "bank_platform_#{Mix.env}",
8 | hostname: "localhost",
9 | pool: Ecto.Adapters.SQL.Sandbox
10 |
11 |
--------------------------------------------------------------------------------
/apps/bank/lib/bank.ex:
--------------------------------------------------------------------------------
1 | defmodule Bank do
2 | @moduledoc ~S"""
3 | Contains main business logic of the project.
4 |
5 | `Bank` is used by `BankWeb` and `Backoffice` Phoenix apps.
6 |
7 | See `Bank.Ledger` for a double-entry accounting system implementation.
8 | """
9 |
10 | use Bank.Model
11 |
12 | ## Customers
13 |
14 | def create_customer!(username, email) do
15 | Customer.build(%{username: username, email: email})
16 | |> Repo.insert!
17 | end
18 |
19 | def register_customer(username, email, password) do
20 | Bank.CustomerRegistration.create(username, email, password)
21 | end
22 |
23 | def find_customer!(clauses) do
24 | Repo.get_by!(Customer, clauses)
25 | |> Repo.preload(:wallet)
26 | end
27 |
28 | def customers do
29 | Repo.all(Customer)
30 | end
31 |
32 | ## Deposits
33 |
34 | def create_deposit!(account, amount) do
35 | {:ok, result} =
36 | Deposit.build(account, amount)
37 | |> Ledger.write
38 | result
39 | end
40 |
41 | ## Ledger
42 |
43 | @doc ~S"""
44 | Returns balance of the customer's wallet account
45 | """
46 | def balance(%Customer{wallet: wallet}), do: Ledger.balance(wallet)
47 |
48 | @doc ~S"""
49 | Returns transactions of the customer's wallet account.
50 | """
51 | def transactions(%Customer{wallet: wallet}), do: Ledger.entries(wallet)
52 |
53 | ## Transfers
54 |
55 | def build_transfer(customer) do
56 | Transfer.changeset(customer, %Transfer{})
57 | end
58 |
59 | def create_transfer(customer, params) do
60 | Transfer.create(customer, params)
61 | end
62 | end
63 |
--------------------------------------------------------------------------------
/apps/bank/lib/bank/application.ex:
--------------------------------------------------------------------------------
1 | defmodule Bank.Application do
2 | @moduledoc false
3 |
4 | use Application
5 |
6 | # See http://elixir-lang.org/docs/stable/elixir/Application.html
7 | # for more information on OTP Applications
8 | def start(_type, _args) do
9 | import Supervisor.Spec, warn: false
10 |
11 | # Define workers and child supervisors to be supervised
12 | children = [
13 | supervisor(Bank.Repo, [])
14 | ]
15 |
16 | # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html
17 | # for other strategies and supported options
18 | opts = [strategy: :one_for_one, name: Bank.Supervisor]
19 | Supervisor.start_link(children, opts)
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/apps/bank/lib/bank/customer.ex:
--------------------------------------------------------------------------------
1 | defmodule Bank.Customer do
2 | use Bank.Model
3 |
4 | schema "bank_customers" do
5 | field :username, :string
6 | field :email, :string
7 | field :auth_account_id, :integer
8 |
9 | belongs_to :wallet, Ledger.Account
10 |
11 | timestamps()
12 | end
13 |
14 | @doc """
15 | Builds a changeset based on the `struct` and `params`.
16 | """
17 | def changeset(struct, params \\ %{}) do
18 | struct
19 | |> cast(params, ~w(username email)a)
20 | |> validate_required(~w(username email)a)
21 | |> unique_constraint(:username)
22 | end
23 |
24 | def build(%{username: username} = params) do
25 | changeset(%Customer{}, params)
26 | |> put_assoc(:wallet, Ledger.Account.build_wallet("Wallet: #{username}"))
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/apps/bank/lib/bank/customer_registration.ex:
--------------------------------------------------------------------------------
1 | defmodule Bank.CustomerRegistration do
2 | use Bank.Model
3 |
4 | def create(username, email, password) do
5 | Ecto.Multi.new
6 | |> Ecto.Multi.insert(:customer, Customer.build(%{username: username, email: email}))
7 | |> Ecto.Multi.run(:account, fn _ ->
8 | Auth.register(%{email: email, password: password})
9 | end)
10 | |> Ecto.Multi.run(:update, fn %{customer: customer, account: account} ->
11 | Ecto.Changeset.change(customer, auth_account_id: account.id)
12 | |> Repo.update
13 | end)
14 | |> Repo.transaction()
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/apps/bank/lib/bank/deposit.ex:
--------------------------------------------------------------------------------
1 | defmodule Bank.Deposit do
2 | use Bank.Model
3 |
4 | def build(%Customer{wallet: wallet}, %Money{} = amount) do
5 | build(wallet, amount)
6 | end
7 | def build(%Ledger.Account{} = wallet, %Money{} = amount) do
8 | description = "Deposit"
9 |
10 | [
11 | {:debit, Ledger.deposits_account, description, amount},
12 | {:credit, wallet, description, amount},
13 | ]
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/apps/bank/lib/bank/iex_helpers.ex:
--------------------------------------------------------------------------------
1 | defmodule Bank.IExHelpers do
2 | def alice do
3 | Bank.find_customer!(username: "alice")
4 | end
5 |
6 | def bob do
7 | Bank.find_customer!(username: "bob")
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/apps/bank/lib/bank/ledger.ex:
--------------------------------------------------------------------------------
1 | defmodule Bank.Ledger do
2 | @moduledoc ~S"""
3 | A simple implementation of double-entry accounting system.
4 |
5 | Basically, we store every `Bank.Ledger.Entry` twice - once for each account affected.
6 | Thus, if Alice transfers $10.00 to Bob we'll have two entries:
7 |
8 | - debit Alice's account for $10.00
9 | - credit Bob's account for $10.00
10 |
11 | `Bank.Ledger.Entry` can be a credit or a debit. Depending on `Bank.Ledger.Account`'s type,
12 | a credit can result in the increase (or decrease) of that accounts' balance.
13 | See `balance/1`.
14 |
15 | Double-entry accounting system implementation is usually required for
16 | compliance with other financial institutions.
17 |
18 | See [Wikipedia entry for more information](https://en.wikipedia.org/wiki/Double-entry_bookkeeping_system#Debits_and_credits)
19 | """
20 |
21 | use Bank.Model
22 |
23 | alias Bank.Ledger.{Account, Entry}
24 |
25 | @doc ~S"""
26 | Creates a wallet account for a given `username`.
27 | """
28 | def create_wallet!(username) do
29 | Account.build_wallet(username)
30 | |> Repo.insert!
31 | end
32 |
33 | @doc ~S"""
34 | Returns account's balance as `Money`.
35 |
36 | We calculate balance over all account's entry.
37 | Balance increases or decreases are based on `Bank.Account`'s type
38 | and `Bank.Entry`'s type according to this table:
39 |
40 | | Debit | Credit
41 | ----------|----------|---------
42 | Asset | Increase | Decrease
43 | Liability | Decrease | Increase
44 |
45 | """
46 | def balance(%Account{id: id, type: type, currency: currency}) do
47 | q = from(t in Entry,
48 | select: fragment("SUM(CASE WHEN b0.type = 'credit' THEN (b0.amount).cents ELSE -(b0.amount).cents END)"),
49 | where: t.account_id == ^id)
50 |
51 | balance = Repo.one(q) || 0
52 | balance = do_balance(balance, type)
53 | %Money{cents: balance, currency: currency}
54 | end
55 | defp do_balance(balance, "liability"), do: +balance
56 | defp do_balance(balance, "asset"), do: -balance
57 |
58 | def deposits_account do
59 | Repo.get_by(Account, name: "Deposits") ||
60 | Repo.insert!(Account.build_asset("Deposits"))
61 | end
62 |
63 | def entries(%Account{id: id}) do
64 | Repo.all(from t in Entry, where: t.account_id == ^id)
65 | end
66 |
67 | def write(entries) do
68 | Repo.transaction_with_isolation(fn ->
69 | with :ok <- same_currencies(entries),
70 | {:ok, persisted_entries} <- insert(entries),
71 | :ok <- credits_equal_debits(),
72 | :ok <- sufficient_funds(persisted_entries) do
73 | persisted_entries
74 | else
75 | {:error, reason} ->
76 | Repo.rollback(reason)
77 | end
78 | end, level: :serializable)
79 | end
80 |
81 | defp same_currencies(entries) do
82 | {_, _, _, %Money{currency: currency}} = hd(entries)
83 | currencies =
84 | Enum.flat_map(entries, fn {_, %Account{currency: a}, _, %Money{currency: b}} -> [a, b] end)
85 |
86 | if Enum.uniq(currencies) == [currency] do
87 | :ok
88 | else
89 | {:error, :different_currencies}
90 | end
91 | end
92 |
93 | defp insert(entries) do
94 | entries =
95 | Enum.map(entries, fn tuple ->
96 | Entry.from_tuple(tuple)
97 | |> Repo.insert!
98 | end)
99 | {:ok, entries}
100 | end
101 |
102 | defp credits_equal_debits do
103 | q = from e in Entry, select: fragment("SUM((b0.amount).cents)")
104 | credits = Repo.one!(from(e in q, where: e.type == "credit"))
105 | debits = Repo.one!(from(e in q, where: e.type == "debit"))
106 |
107 | if credits == debits do
108 | :ok
109 | else
110 | {:error, :credits_not_equal_debits}
111 | end
112 | end
113 |
114 | defp sufficient_funds(entries) do
115 | accounts = Enum.map(entries, & &1.account)
116 |
117 | if Enum.all?(accounts, fn account -> balance(account).cents >= 0 end) do
118 | :ok
119 | else
120 | {:error, :insufficient_funds}
121 | end
122 | end
123 | end
124 |
--------------------------------------------------------------------------------
/apps/bank/lib/bank/ledger/account.ex:
--------------------------------------------------------------------------------
1 | defmodule Bank.Ledger.Account do
2 | use Bank.Model
3 |
4 | @account_types ~w(asset liability)
5 |
6 | schema "bank_accounts" do
7 | field :type, :string
8 | field :name, :string
9 | field :currency, :string
10 |
11 | timestamps()
12 | end
13 |
14 | def build_asset(name) do
15 | changeset(%Ledger.Account{}, Map.put(%{type: "asset", currency: "USD"}, :name, name))
16 | end
17 |
18 | def build_wallet(name) do
19 | changeset(%Ledger.Account{}, Map.put(%{type: "liability", currency: "USD"}, :name, name))
20 | end
21 |
22 | @doc """
23 | Builds a changeset based on the `struct` and `params`.
24 | """
25 | def changeset(struct, params \\ %{}) do
26 | struct
27 | |> cast(params, [:type, :name, :currency])
28 | |> validate_required([:type, :name, :currency])
29 | |> unique_constraint(:name)
30 | |> validate_inclusion(:type, @account_types)
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/apps/bank/lib/bank/ledger/entry.ex:
--------------------------------------------------------------------------------
1 | defmodule Bank.Ledger.Entry do
2 | use Bank.Model
3 |
4 | @entry_types [:credit, :debit]
5 |
6 | schema "bank_entries" do
7 | field :type, :string
8 | field :description, :string
9 | field :amount, Money.Ecto
10 | belongs_to :account, Ledger.Account
11 |
12 | timestamps()
13 | end
14 |
15 | def from_tuple({type, %Ledger.Account{} = account, description, %Money{} = amount})
16 | when type in @entry_types and is_binary(description) do
17 |
18 | %Ledger.Entry{
19 | type: Atom.to_string(type),
20 | account: account,
21 | description: description,
22 | amount: amount,
23 | }
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/apps/bank/lib/bank/model.ex:
--------------------------------------------------------------------------------
1 | defmodule Bank.Model do
2 | @moduledoc false
3 |
4 | defmacro __using__(_) do
5 | quote do
6 | use Ecto.Schema
7 | import Ecto.Changeset
8 | import Ecto.Query
9 |
10 | alias Bank.{
11 | Account,
12 | Customer,
13 | Deposit,
14 | Ledger,
15 | Repo,
16 | Transfer,
17 | Entry
18 | }
19 | end
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/apps/bank/lib/bank/repo.ex:
--------------------------------------------------------------------------------
1 | defmodule Bank.Repo do
2 | @moduledoc false
3 |
4 | use Ecto.Repo, otp_app: :bank
5 | use Scrivener, page_size: 10
6 |
7 | def transaction_with_isolation(fun_or_multi, opts) do
8 | false = Bank.Repo.in_transaction?
9 | level = Keyword.fetch!(opts, :level)
10 |
11 | transaction(fn ->
12 | {:ok, _} = Ecto.Adapters.SQL.query(Bank.Repo, "SET TRANSACTION ISOLATION LEVEL #{level}", [])
13 |
14 | case transaction(fun_or_multi, opts) do
15 | {:ok, result} -> {:ok, result}
16 | {:error, reason} -> Bank.Repo.rollback(reason)
17 | end
18 | |> unwrap_transaction_result
19 | end, opts)
20 | end
21 |
22 | defp unwrap_transaction_result({:ok, result}), do: result
23 | defp unwrap_transaction_result(other), do: other
24 | end
25 |
--------------------------------------------------------------------------------
/apps/bank/lib/bank/transfer.ex:
--------------------------------------------------------------------------------
1 | defmodule Bank.Transfer do
2 | use Bank.Model
3 |
4 | embedded_schema do
5 | field :amount_string, :string
6 | field :amount, Money.Ecto
7 | field :destination_username, :string
8 | field :description, :string
9 |
10 | embeds_one :source_customer, Customer
11 | embeds_one :destination_customer, Customer
12 | end
13 |
14 | def changeset(customer, struct, params \\ %{}) do
15 | struct
16 | |> cast(params, [:amount_string, :destination_username, :description])
17 | |> validate_required([:amount_string, :destination_username, :description])
18 | |> validate_format(:amount_string, ~r/\A\d+\.\d{2}\Z/, message: "is invalid")
19 | |> put_embed(:source_customer, customer)
20 | |> put_destination_customer(customer)
21 | end
22 |
23 | defp put_destination_customer(%{valid?: false} = changeset, _), do: changeset
24 | defp put_destination_customer(changeset, source_customer) do
25 | username = get_change(changeset, :destination_username)
26 |
27 | if username == source_customer.username do
28 | add_error(changeset, :destination_username, "cannot transfer to the same account")
29 | else
30 | case Repo.one(from c in Customer, where: c.username == ^username, preload: :wallet) do
31 | %Customer{} = customer ->
32 | put_embed(changeset, :destination_customer, customer)
33 | nil ->
34 | add_error(changeset, :destination_username, "is invalid")
35 | end
36 | end
37 | end
38 |
39 | def create(customer, params) do
40 | changeset = changeset(customer, %Transfer{}, params)
41 |
42 | if changeset.valid? do
43 | transfer = apply_changes(changeset)
44 | source_account = customer.wallet
45 | destination_account = transfer.destination_customer.wallet
46 |
47 | amount = Money.new(transfer.amount_string <> " " <> destination_account.currency)
48 | transfer = %{transfer | amount: amount}
49 | transactions = build_transactions(source_account, destination_account, transfer.description, amount)
50 |
51 | case Ledger.write(transactions) do
52 | {:ok, _} ->
53 | :ok = send_message(transfer)
54 | {:ok, transfer}
55 | {:error, :insufficient_funds} ->
56 | changeset = add_error(changeset, :amount_string, "insufficient funds")
57 | {:error, changeset}
58 | end
59 | else
60 | {:error, changeset}
61 | end
62 | end
63 |
64 | defp build_transactions(source, destination, description, amount) do
65 | [
66 | {:debit, source, description, amount},
67 | {:credit, destination, description, amount},
68 | ]
69 | end
70 |
71 | defp send_message(transfer) do
72 | subject = "You've received #{transfer.amount} from #{transfer.source_customer.username}"
73 | :ok = Messenger.deliver_email(transfer.destination_username, subject, subject)
74 | end
75 | end
76 |
--------------------------------------------------------------------------------
/apps/bank/mix.exs:
--------------------------------------------------------------------------------
1 | defmodule Bank.Mixfile do
2 | use Mix.Project
3 |
4 | def project do
5 | [app: :bank,
6 | version: "0.1.0",
7 | build_path: "../../_build",
8 | config_path: "../../config/config.exs",
9 | deps_path: "../../deps",
10 | lockfile: "../../mix.lock",
11 | elixir: ">= 1.4.2",
12 | elixirc_paths: elixirc_paths(Mix.env),
13 | build_embedded: Mix.env == :prod,
14 | start_permanent: Mix.env == :prod,
15 | aliases: aliases(),
16 | deps: deps(),
17 | docs: [main: "Bank"]]
18 | end
19 |
20 | # Configuration for the OTP application
21 | #
22 | # Type "mix help compile.app" for more information
23 | def application do
24 | [applications: [:logger, :ecto, :postgrex, :auth, :money],
25 | mod: {Bank.Application, []}]
26 | end
27 |
28 | defp elixirc_paths(:test), do: ["lib", "test/support"]
29 | defp elixirc_paths(_), do: ["lib"]
30 |
31 | defp deps do
32 | [{:ecto, "~> 2.0"},
33 | {:postgrex, ">= 0.0.0"},
34 | {:scrivener_ecto, github: "drewolson/scrivener_ecto"},
35 |
36 | {:auth, in_umbrella: true},
37 | {:messenger, in_umbrella: true},
38 | {:money, in_umbrella: true}]
39 | end
40 |
41 | defp aliases do
42 | ["ecto.setup": ["ecto.create", "ecto.migrate", "ecto.seed"],
43 | "ecto.seed": ["run priv/repo/seeds.exs"],
44 | "ecto.reset": ["ecto.drop", "ecto.setup"],
45 | "test": ["ecto.create --quiet", "ecto.migrate", "test"]]
46 | end
47 | end
48 |
--------------------------------------------------------------------------------
/apps/bank/priv/repo/migrations/20160527233040_create_account.exs:
--------------------------------------------------------------------------------
1 | defmodule Bank.Repo.Migrations.CreateAccount do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:bank_accounts) do
6 | add :type, :string, null: false
7 | add :name, :string, null: false
8 | add :currency, :string, null: false
9 |
10 | timestamps()
11 | end
12 | create index(:bank_accounts, [:name], unique: true)
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/apps/bank/priv/repo/migrations/20160527233041_create_customer.exs:
--------------------------------------------------------------------------------
1 | defmodule Bank.Repo.Migrations.CreateCustomer do
2 | use Ecto.Migration
3 |
4 | def change do
5 | create table(:bank_customers) do
6 | add :username, :string
7 | add :email, :string
8 | add :wallet_id, references(:bank_accounts, on_delete: :nothing)
9 | add :auth_account_id, :integer
10 |
11 | timestamps()
12 | end
13 |
14 | create index(:bank_customers, [:username], unique: true)
15 | create index(:bank_customers, [:email], unique: true)
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/apps/bank/priv/repo/migrations/20160528001304_create_entries.exs:
--------------------------------------------------------------------------------
1 | defmodule Bank.Repo.Migrations.CreateEntry do
2 | use Ecto.Migration
3 |
4 | def change do
5 | execute """
6 | CREATE TYPE moneyz AS (
7 | cents integer,
8 | currency varchar
9 | );
10 | """
11 |
12 | create table(:bank_entries) do
13 | add :type, :string
14 | add :description, :string
15 | add :amount, :moneyz
16 | add :account_id, references(:bank_accounts, on_delete: :nothing)
17 |
18 | timestamps()
19 | end
20 | create index(:bank_entries, [:account_id])
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/apps/bank/priv/repo/seeds.exs:
--------------------------------------------------------------------------------
1 | if Mix.env == :dev do
2 |
3 | import Money
4 |
5 | {:ok, %{customer: alice}} = Bank.register_customer("alice", "alice@example.com", "secret12")
6 | Bank.create_deposit!(alice, ~M"10 USD")
7 |
8 | {:ok, %{customer: bob}} = Bank.register_customer("bob", "bob@example.com", "secret12")
9 |
10 | end
11 |
--------------------------------------------------------------------------------
/apps/bank/test/bank/customer_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Bank.CustomerTest do
2 | use Bank.Case
3 |
4 | @valid_attrs %{username: "alice", email: "alice@example.com"}
5 | @invalid_attrs %{}
6 |
7 | test "changeset with valid attributes" do
8 | changeset = Customer.changeset(%Customer{}, @valid_attrs)
9 | assert changeset.valid?
10 | end
11 |
12 | test "changeset with invalid attributes" do
13 | changeset = Customer.changeset(%Customer{}, @invalid_attrs)
14 | refute changeset.valid?
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/apps/bank/test/bank/ledger/account_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Bank.Ledger.AccountTest do
2 | use Bank.Case
3 | alias Bank.Ledger.Account
4 |
5 | @valid_attrs %{type: "liability", name: "some content", currency: "USD"}
6 | @invalid_attrs %{}
7 |
8 | test "changeset with valid attributes" do
9 | changeset = Account.changeset(%Account{}, @valid_attrs)
10 | assert changeset.valid?
11 | end
12 |
13 | test "changeset with invalid attributes" do
14 | changeset = Account.changeset(%Account{}, @invalid_attrs)
15 | refute changeset.valid?
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/apps/bank/test/bank/ledger_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Bank.LedgerTest do
2 | use Bank.Case
3 |
4 | @moduletag isolation: :serializable
5 |
6 | setup _tags do
7 | alice = Ledger.create_wallet!("alice")
8 | bob = Ledger.create_wallet!("bob")
9 | {:ok, _} = Deposit.build(alice, ~M"100 USD") |> Ledger.write
10 |
11 | {:ok, %{alice: alice, bob: bob}}
12 | end
13 |
14 | test "write: success", %{alice: alice, bob: bob} do
15 | transactions = [
16 | {:debit, alice, "", ~M"10 USD"},
17 | {:credit, bob, "", ~M"10 USD"},
18 | ]
19 |
20 | assert {:ok, [%Ledger.Entry{}, %Ledger.Entry{}]} = Ledger.write(transactions)
21 |
22 | assert Ledger.balance(alice) == ~M"90 USD"
23 | assert Ledger.balance(bob) == ~M"10 USD"
24 | end
25 |
26 | test "write: different currencies", %{alice: alice, bob: bob} do
27 | transactions = [
28 | {:debit, alice, "", ~M"10 USD"},
29 | {:credit, bob, "", ~M"10 EUR"},
30 | ]
31 |
32 | assert {:error, :different_currencies} = Ledger.write(transactions)
33 | end
34 |
35 | test "write: credits not equal debits", %{alice: alice, bob: bob} do
36 | transactions = [
37 | {:debit, alice, "", ~M"10 USD"},
38 | {:credit, bob, "", ~M"9 USD"},
39 | ]
40 |
41 | assert {:error, :credits_not_equal_debits} = Ledger.write(transactions)
42 | assert Ledger.balance(alice) == ~M"100 USD"
43 | assert Ledger.balance(bob) == ~M"0 USD"
44 | end
45 |
46 | test "write: insufficient funds", %{alice: alice, bob: bob} do
47 | transactions = [
48 | {:debit, alice, "", ~M"900 USD"},
49 | {:credit, bob, "", ~M"900 USD"},
50 | ]
51 |
52 | assert {:error, :insufficient_funds} = Ledger.write(transactions)
53 | end
54 | end
55 |
--------------------------------------------------------------------------------
/apps/bank/test/bank/transaction_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Bank.EntryTest do
2 | use Bank.Case
3 | end
4 |
--------------------------------------------------------------------------------
/apps/bank/test/bank/transfer_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Bank.TransferTest do
2 | use Bank.Case
3 |
4 | @moduletag isolation: :serializable
5 |
6 | @valid_params %{
7 | amount_string: "2.01",
8 | destination_username: "bob",
9 | description: "Lunch money"
10 | }
11 |
12 | setup do
13 | :ok = Messenger.Local.setup()
14 |
15 | alice = Bank.create_customer!("alice", "alice@example.com")
16 | bob = Bank.create_customer!("bob", "bob@example.com")
17 | Bank.create_deposit!(alice, ~M"10 USD")
18 |
19 | {:ok, %{alice: alice, bob: bob}}
20 | end
21 |
22 | test "create: success", %{alice: alice, bob: bob} do
23 | assert {:ok, _} = Transfer.create(alice, @valid_params)
24 | assert Ledger.balance(alice.wallet) == ~M"7.99 USD"
25 | assert Ledger.balance(bob.wallet) == ~M"2.01 USD"
26 |
27 | assert Messenger.Local.subjects_for("bob") == ["You've received 2.01 USD from alice"]
28 | end
29 |
30 | test "create: invalid amount", %{alice: alice} do
31 | assert {:error, %{errors: [amount_string: {"is invalid", _}]}} =
32 | Transfer.create(alice, %{@valid_params | amount_string: "invalid"})
33 |
34 | assert {:error, %{errors: [amount_string: {"is invalid", _}]}} =
35 | Transfer.create(alice, %{@valid_params | amount_string: "-2.00"})
36 | end
37 |
38 | test "create: insufficient funds", %{alice: alice} do
39 | assert {:error, %{errors: [amount_string: {"insufficient funds", _}]}} =
40 | Transfer.create(alice, %{@valid_params | amount_string: "999.00"})
41 | end
42 |
43 | test "create: blank destination", %{alice: alice} do
44 | assert {:error, %{errors: [destination_username: {"can't be blank", _}]}} =
45 | Transfer.create(alice, %{@valid_params | destination_username: ""})
46 | end
47 |
48 | test "create: cannot transfer to the same account", %{alice: alice} do
49 | assert {:error, %{errors: [destination_username: {"cannot transfer to the same account", _}]}} =
50 | Transfer.create(alice, %{@valid_params | destination_username: "alice"})
51 | end
52 |
53 | test "create: invalid destination", %{alice: alice} do
54 | assert {:error, %{errors: [destination_username: {"is invalid", _}]}} =
55 | Transfer.create(alice, %{@valid_params | destination_username: "invalid"})
56 | end
57 | end
58 |
--------------------------------------------------------------------------------
/apps/bank/test/bank_test.exs:
--------------------------------------------------------------------------------
1 | defmodule BankTest do
2 | use ExUnit.Case
3 |
4 | test "the truth" do
5 | assert 1 + 1 == 2
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/apps/bank/test/support/case.ex:
--------------------------------------------------------------------------------
1 | defmodule Bank.Case do
2 | use ExUnit.CaseTemplate
3 |
4 | using do
5 | quote do
6 | alias Bank.Repo
7 |
8 | import Ecto
9 | import Ecto.Changeset
10 | import Ecto.Query
11 | import Bank.Case
12 |
13 | import Money, only: [sigil_M: 2]
14 | use Bank.Model
15 | end
16 | end
17 |
18 | setup tags do
19 | opts = tags |> Map.take([:isolation]) |> Enum.to_list()
20 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(Bank.Repo, opts)
21 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(Auth.Repo, opts)
22 |
23 | unless tags[:async] do
24 | Ecto.Adapters.SQL.Sandbox.mode(Auth.Repo, {:shared, self()})
25 | Ecto.Adapters.SQL.Sandbox.mode(Bank.Repo, {:shared, self()})
26 | end
27 |
28 | :ok
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/apps/bank/test/test_helper.exs:
--------------------------------------------------------------------------------
1 | ExUnit.start()
2 |
3 | Ecto.Adapters.SQL.Sandbox.mode(Bank.Repo, :manual)
4 |
--------------------------------------------------------------------------------
/apps/bank_web/.gitignore:
--------------------------------------------------------------------------------
1 | # App artifacts
2 | /_build
3 | /db
4 | /deps
5 | /*.ez
6 |
7 | # Generate on crash by the VM
8 | erl_crash.dump
9 |
10 | # Static artifacts
11 | /node_modules
12 |
13 | # Since we are building assets from web/static,
14 | # we ignore priv/static. You may want to comment
15 | # this depending on your deployment strategy.
16 | /priv/static/
17 |
18 | # The config/prod.secret.exs file by default contains sensitive
19 | # data and you should not commit it into version control.
20 | #
21 | # Alternatively, you may comment the line below and commit the
22 | # secrets file as long as you replace its contents by environment
23 | # variables.
24 | /config/prod.secret.exs
25 |
--------------------------------------------------------------------------------
/apps/bank_web/.iex.exs:
--------------------------------------------------------------------------------
1 | import_file "../../.iex.exs"
2 |
--------------------------------------------------------------------------------
/apps/bank_web/README.md:
--------------------------------------------------------------------------------
1 | # BankWeb
2 |
3 | Web interface to the platform.
4 |
5 | See `Bank` for the main business logic.
6 |
7 | ## Setup
8 |
9 | To start your Phoenix app:
10 |
11 | * Install dependencies with `mix deps.get`
12 | * Create and migrate your database with `mix ecto.setup`
13 | * Install Node.js dependencies with `npm install`
14 | * Start Phoenix endpoint with `mix phoenix.server`
15 |
16 | Now you can visit [`localhost:4000`](http://localhost:4000) from your browser.
17 |
--------------------------------------------------------------------------------
/apps/bank_web/brunch-config.js:
--------------------------------------------------------------------------------
1 | exports.config = {
2 | // See http://brunch.io/#documentation for docs.
3 | files: {
4 | javascripts: {
5 | joinTo: "js/app.js"
6 |
7 | // To use a separate vendor.js bundle, specify two files path
8 | // https://github.com/brunch/brunch/blob/master/docs/config.md#files
9 | // joinTo: {
10 | // "js/app.js": /^(web\/static\/js)/,
11 | // "js/vendor.js": /^(web\/static\/vendor)|(deps)/
12 | // }
13 | //
14 | // To change the order of concatenation of files, explicitly mention here
15 | // https://github.com/brunch/brunch/tree/master/docs#concatenation
16 | // order: {
17 | // before: [
18 | // "web/static/vendor/js/jquery-2.1.1.js",
19 | // "web/static/vendor/js/bootstrap.min.js"
20 | // ]
21 | // }
22 | },
23 | stylesheets: {
24 | joinTo: "css/app.css",
25 | order: {
26 | after: ["web/static/css/app.css"] // concat app.css last
27 | }
28 | },
29 | templates: {
30 | joinTo: "js/app.js"
31 | }
32 | },
33 |
34 | conventions: {
35 | // This option sets where we should place non-css and non-js assets in.
36 | // By default, we set this to "/web/static/assets". Files in this directory
37 | // will be copied to `paths.public`, which is "priv/static" by default.
38 | assets: /^(web\/static\/assets)/
39 | },
40 |
41 | // Phoenix paths configuration
42 | paths: {
43 | // Dependencies and current project directories to watch
44 | watched: [
45 | "web/static",
46 | "test/static"
47 | ],
48 |
49 | // Where to compile files to
50 | public: "priv/static"
51 | },
52 |
53 | // Configure your plugins
54 | plugins: {
55 | babel: {
56 | // Do not use ES6 compiler in vendor code
57 | ignore: [/web\/static\/vendor/]
58 | }
59 | },
60 |
61 | modules: {
62 | autoRequire: {
63 | "js/app.js": ["web/static/js/app"]
64 | }
65 | },
66 |
67 | npm: {
68 | enabled: true
69 | }
70 | };
71 |
--------------------------------------------------------------------------------
/apps/bank_web/config/config.exs:
--------------------------------------------------------------------------------
1 | # This file is responsible for configuring your application
2 | # and its dependencies with the aid of the Mix.Config module.
3 | #
4 | # This configuration file is loaded before any dependency and
5 | # is restricted to this project.
6 | use Mix.Config
7 |
8 | # General application configuration
9 | config :bank_web, ecto_repos: []
10 |
11 | # Configures the endpoint
12 | config :bank_web, BankWeb.Endpoint,
13 | url: [host: "localhost"],
14 | secret_key_base: "YhKiB9ImkAB8TdOqawXLwAwVbFklXxnzjqHFRCOeyR2qbwxlmh90E/oxeG894gfu",
15 | render_errors: [view: BankWeb.ErrorView, accepts: ~w(html json)],
16 | pubsub: [name: BankWeb.PubSub,
17 | adapter: Phoenix.PubSub.PG2]
18 |
19 | # Configures Elixir's Logger
20 | config :logger, :console,
21 | format: "$time $metadata[$level] $message\n",
22 | metadata: [:request_id]
23 |
24 | # Import environment specific config. This must remain at the bottom
25 | # of this file so it overrides the configuration defined above.
26 | import_config "#{Mix.env}.exs"
27 |
--------------------------------------------------------------------------------
/apps/bank_web/config/dev.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
3 | # For development, we disable any cache and enable
4 | # debugging and code reloading.
5 | #
6 | # The watchers configuration can be used to run external
7 | # watchers to your application. For example, we use it
8 | # with brunch.io to recompile .js and .css sources.
9 | config :bank_web, BankWeb.Endpoint,
10 | http: [port: 4000],
11 | debug_errors: true,
12 | code_reloader: true,
13 | check_origin: false,
14 | watchers: [node: ["node_modules/brunch/bin/brunch", "watch", "--stdin",
15 | cd: Path.expand("../", __DIR__)]]
16 |
17 |
18 | # Watch static and templates for browser reloading.
19 | config :bank_web, BankWeb.Endpoint,
20 | live_reload: [
21 | patterns: [
22 | ~r{priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$},
23 | ~r{priv/gettext/.*(po)$},
24 | ~r{web/views/.*(ex)$},
25 | ~r{web/templates/.*(eex)$}
26 | ]
27 | ]
28 |
29 | # Do not include metadata nor timestamps in development logs
30 | config :logger, :console, format: "[$level] $message\n"
31 |
32 | # Set a higher stacktrace during development. Avoid configuring such
33 | # in production as building large stacktraces may be expensive.
34 | config :phoenix, :stacktrace_depth, 20
35 |
--------------------------------------------------------------------------------
/apps/bank_web/config/prod.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
3 | # For production, we configure the host to read the PORT
4 | # from the system environment. Therefore, you will need
5 | # to set PORT=80 before running your server.
6 | #
7 | # You should also configure the url host to something
8 | # meaningful, we use this information when generating URLs.
9 | #
10 | # Finally, we also include the path to a manifest
11 | # containing the digested version of static files. This
12 | # manifest is generated by the mix phoenix.digest task
13 | # which you typically run after static files are built.
14 | # config :bank_web, BankWeb.Endpoint,
15 | # http: [port: {:system, "PORT"}],
16 | # url: [host: "example.com", port: 80],
17 | # cache_static_manifest: "priv/static/manifest.json"
18 |
19 | config :bank_web, BankWeb.Endpoint,
20 | # http: [port: {:system, "PORT"}],
21 | url: [scheme: "https", host: "acme-bank.herokuapp.com", port: 443],
22 | force_ssl: [rewrite_on: [:x_forwarded_proto]],
23 | cache_static_manifest: "priv/static/manifest.json",
24 | secret_key_base: System.get_env("SECRET_KEY_BASE")
25 |
26 | # Do not print debug messages in production
27 | config :logger, level: :info
28 |
29 | # ## SSL Support
30 | #
31 | # To get SSL working, you will need to add the `https` key
32 | # to the previous section and set your `:url` port to 443:
33 | #
34 | # config :bank_web, BankWeb.Endpoint,
35 | # ...
36 | # url: [host: "example.com", port: 443],
37 | # https: [port: 443,
38 | # keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"),
39 | # certfile: System.get_env("SOME_APP_SSL_CERT_PATH")]
40 | #
41 | # Where those two env variables return an absolute path to
42 | # the key and cert in disk or a relative path inside priv,
43 | # for example "priv/ssl/server.key".
44 | #
45 | # We also recommend setting `force_ssl`, ensuring no data is
46 | # ever sent via http, always redirecting to https:
47 | #
48 | # config :bank_web, BankWeb.Endpoint,
49 | # force_ssl: [hsts: true]
50 | #
51 | # Check `Plug.SSL` for all available options in `force_ssl`.
52 |
53 | # ## Using releases
54 | #
55 | # If you are doing OTP releases, you need to instruct Phoenix
56 | # to start the server for all endpoints:
57 | #
58 | # config :phoenix, :serve_endpoints, true
59 | #
60 | # Alternatively, you can configure exactly which server to
61 | # start per endpoint:
62 | #
63 | # config :bank_web, BankWeb.Endpoint, server: true
64 | #
65 | # You will also need to set the application root to `.` in order
66 | # for the new static assets to be served after a hot upgrade:
67 | #
68 | # config :bank_web, BankWeb.Endpoint, root: "."
69 |
70 | # Finally import the config/prod.secret.exs
71 | # which should be versioned separately.
72 | # import_config "prod.secret.exs"
73 |
--------------------------------------------------------------------------------
/apps/bank_web/config/test.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
3 | # We don't run a server during test. If one is required,
4 | # you can enable the server option below.
5 | config :bank_web, BankWeb.Endpoint,
6 | http: [port: 4001],
7 | server: false
8 |
9 | # Print only warnings and errors during test
10 | config :logger, level: :warn
11 |
--------------------------------------------------------------------------------
/apps/bank_web/lib/bank_web.ex:
--------------------------------------------------------------------------------
1 | defmodule BankWeb do
2 | @moduledoc """
3 | Web interface to the platform built using the `Phoenix` Web framework.
4 | """
5 | end
6 |
--------------------------------------------------------------------------------
/apps/bank_web/lib/bank_web/application.ex:
--------------------------------------------------------------------------------
1 | defmodule BankWeb.Application do
2 | @modulefoc false
3 | use Application
4 |
5 | def start(_type, _args) do
6 | import Supervisor.Spec
7 |
8 | children = [
9 | supervisor(BankWeb.Endpoint, []),
10 | ]
11 |
12 | opts = [strategy: :one_for_one, name: BankWeb.Supervisor]
13 | Supervisor.start_link(children, opts)
14 | end
15 |
16 | def config_change(changed, _new, removed) do
17 | BankWeb.Endpoint.config_change(changed, removed)
18 | :ok
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/apps/bank_web/lib/bank_web/endpoint.ex:
--------------------------------------------------------------------------------
1 | defmodule BankWeb.Endpoint do
2 | use Phoenix.Endpoint, otp_app: :bank_web
3 |
4 | # Serve at "/" the static files from "priv/static" directory.
5 | #
6 | # You should set gzip to true if you are running phoenix.digest
7 | # when deploying your static files in production.
8 | plug Plug.Static,
9 | at: "/", from: :bank_web, gzip: false,
10 | only: ~w(css fonts images js favicon.ico robots.txt)
11 |
12 | # Code reloading can be explicitly enabled under the
13 | # :code_reloader configuration of your endpoint.
14 | if code_reloading? do
15 | socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket
16 | plug Phoenix.LiveReloader
17 | plug Phoenix.CodeReloader
18 | end
19 |
20 | plug Plug.RequestId
21 | plug Plug.Logger
22 |
23 | plug Plug.Parsers,
24 | parsers: [:urlencoded, :multipart, :json],
25 | pass: ["*/*"],
26 | json_decoder: Poison
27 |
28 | plug Plug.MethodOverride
29 | plug Plug.Head
30 |
31 | # The session will be stored in the cookie and signed,
32 | # this means its contents can be read but not tampered with.
33 | # Set :encryption_salt if you would also like to encrypt it.
34 | plug Plug.Session,
35 | store: :cookie,
36 | key: "_bank_web_key",
37 | signing_salt: "um/pBJag"
38 |
39 | plug BankWeb.Router
40 | end
41 |
--------------------------------------------------------------------------------
/apps/bank_web/mix.exs:
--------------------------------------------------------------------------------
1 | defmodule BankWeb.Mixfile do
2 | use Mix.Project
3 |
4 | def project do
5 | [app: :bank_web,
6 | version: "0.0.1",
7 | build_path: "../../_build",
8 | config_path: "../../config/config.exs",
9 | deps_path: "../../deps",
10 | lockfile: "../../mix.lock",
11 | elixir: ">= 1.4.2",
12 | elixirc_paths: elixirc_paths(Mix.env),
13 | compilers: [:phoenix, :gettext] ++ Mix.compilers,
14 | build_embedded: Mix.env == :prod,
15 | start_permanent: Mix.env == :prod,
16 | docs: [main: "readme", extras: ["README.md"]],
17 | aliases: aliases(),
18 | deps: deps()]
19 | end
20 |
21 | # Configuration for the OTP application.
22 | #
23 | # Type `mix help compile.app` for more information.
24 | def application do
25 | [mod: {BankWeb.Application, []},
26 | applications: [:phoenix, :phoenix_pubsub, :phoenix_html, :cowboy, :logger, :gettext,
27 | :phoenix_ecto, :bank, :messenger, :auth]]
28 | end
29 |
30 | # Specifies which paths to compile per environment.
31 | defp elixirc_paths(:test), do: ["lib", "web", "test/support"]
32 | defp elixirc_paths(_), do: ["lib", "web"]
33 |
34 | # Specifies your project dependencies.
35 | #
36 | # Type `mix help deps` for examples and options.
37 | defp deps do
38 | [{:phoenix, "~> 1.2"},
39 | {:phoenix_pubsub, "~> 1.0.0"},
40 | {:phoenix_ecto, "~> 3.0"},
41 | {:phoenix_html, "~> 2.5"},
42 | {:phoenix_live_reload, "~> 1.0", only: :dev},
43 | {:gettext, "~> 0.11"},
44 | {:cowboy, "~> 1.0"},
45 |
46 | {:bank, in_umbrella: true}]
47 | end
48 |
49 | # Aliases are shortcuts or tasks specific to the current project.
50 | # For example, to create, migrate and run the seeds file at once:
51 | #
52 | # $ mix ecto.setup
53 | #
54 | # See the documentation for `Mix` for more info on aliases.
55 | defp aliases do
56 | []
57 | end
58 | end
59 |
--------------------------------------------------------------------------------
/apps/bank_web/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "repository": {},
3 | "license": "MIT",
4 | "scripts": {
5 | "deploy": "brunch build --production",
6 | "watch": "brunch watch --stdin"
7 | },
8 | "dependencies": {
9 | "phoenix": "file:../../deps/phoenix",
10 | "phoenix_html": "file:../../deps/phoenix_html"
11 | },
12 | "devDependencies": {
13 | "babel-brunch": "~6.0.0",
14 | "brunch": "2.7.4",
15 | "clean-css-brunch": "~2.0.0",
16 | "css-brunch": "~2.0.0",
17 | "javascript-brunch": "~2.0.0",
18 | "uglify-js-brunch": "~2.0.1"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/apps/bank_web/priv/gettext/en/LC_MESSAGES/errors.po:
--------------------------------------------------------------------------------
1 | ## `msgid`s in this file come from POT (.pot) files.
2 | ##
3 | ## Do not add, change, or remove `msgid`s manually here as
4 | ## they're tied to the ones in the corresponding POT file
5 | ## (with the same domain).
6 | ##
7 | ## Use `mix gettext.extract --merge` or `mix gettext.merge`
8 | ## to merge POT files into PO files.
9 | msgid ""
10 | msgstr ""
11 | "Language: en\n"
12 |
13 | ## From Ecto.Changeset.cast/4
14 | msgid "can't be blank"
15 | msgstr ""
16 |
17 | ## From Ecto.Changeset.unique_constraint/3
18 | msgid "has already been taken"
19 | msgstr ""
20 |
21 | ## From Ecto.Changeset.put_change/3
22 | msgid "is invalid"
23 | msgstr ""
24 |
25 | ## From Ecto.Changeset.validate_format/3
26 | msgid "has invalid format"
27 | msgstr ""
28 |
29 | ## From Ecto.Changeset.validate_subset/3
30 | msgid "has an invalid entry"
31 | msgstr ""
32 |
33 | ## From Ecto.Changeset.validate_exclusion/3
34 | msgid "is reserved"
35 | msgstr ""
36 |
37 | ## From Ecto.Changeset.validate_confirmation/3
38 | msgid "does not match confirmation"
39 | msgstr ""
40 |
41 | ## From Ecto.Changeset.no_assoc_constraint/3
42 | msgid "is still associated to this entry"
43 | msgstr ""
44 |
45 | msgid "are still associated to this entry"
46 | msgstr ""
47 |
48 | ## From Ecto.Changeset.validate_length/3
49 | msgid "should be %{count} character(s)"
50 | msgid_plural "should be %{count} character(s)"
51 | msgstr[0] ""
52 | msgstr[1] ""
53 |
54 | msgid "should have %{count} item(s)"
55 | msgid_plural "should have %{count} item(s)"
56 | msgstr[0] ""
57 | msgstr[1] ""
58 |
59 | msgid "should be at least %{count} character(s)"
60 | msgid_plural "should be at least %{count} character(s)"
61 | msgstr[0] ""
62 | msgstr[1] ""
63 |
64 | msgid "should have at least %{count} item(s)"
65 | msgid_plural "should have at least %{count} item(s)"
66 | msgstr[0] ""
67 | msgstr[1] ""
68 |
69 | msgid "should be at most %{count} character(s)"
70 | msgid_plural "should be at most %{count} character(s)"
71 | msgstr[0] ""
72 | msgstr[1] ""
73 |
74 | msgid "should have at most %{count} item(s)"
75 | msgid_plural "should have at most %{count} item(s)"
76 | msgstr[0] ""
77 | msgstr[1] ""
78 |
79 | ## From Ecto.Changeset.validate_number/3
80 | msgid "must be less than %{number}"
81 | msgstr ""
82 |
83 | msgid "must be greater than %{number}"
84 | msgstr ""
85 |
86 | msgid "must be less than or equal to %{number}"
87 | msgstr ""
88 |
89 | msgid "must be greater than or equal to %{number}"
90 | msgstr ""
91 |
92 | msgid "must be equal to %{number}"
93 | msgstr ""
94 |
--------------------------------------------------------------------------------
/apps/bank_web/priv/gettext/errors.pot:
--------------------------------------------------------------------------------
1 | ## This file is a PO Template file.
2 | ##
3 | ## `msgid`s here are often extracted from source code.
4 | ## Add new translations manually only if they're dynamic
5 | ## translations that can't be statically extracted.
6 | ##
7 | ## Run `mix gettext.extract` to bring this file up to
8 | ## date. Leave `msgstr`s empty as changing them here as no
9 | ## effect: edit them in PO (`.po`) files instead.
10 |
11 | ## From Ecto.Changeset.cast/4
12 | msgid "can't be blank"
13 | msgstr ""
14 |
15 | ## From Ecto.Changeset.unique_constraint/3
16 | msgid "has already been taken"
17 | msgstr ""
18 |
19 | ## From Ecto.Changeset.put_change/3
20 | msgid "is invalid"
21 | msgstr ""
22 |
23 | ## From Ecto.Changeset.validate_format/3
24 | msgid "has invalid format"
25 | msgstr ""
26 |
27 | ## From Ecto.Changeset.validate_subset/3
28 | msgid "has an invalid entry"
29 | msgstr ""
30 |
31 | ## From Ecto.Changeset.validate_exclusion/3
32 | msgid "is reserved"
33 | msgstr ""
34 |
35 | ## From Ecto.Changeset.validate_confirmation/3
36 | msgid "does not match confirmation"
37 | msgstr ""
38 |
39 | ## From Ecto.Changeset.no_assoc_constraint/3
40 | msgid "is still associated to this entry"
41 | msgstr ""
42 |
43 | msgid "are still associated to this entry"
44 | msgstr ""
45 |
46 | ## From Ecto.Changeset.validate_length/3
47 | msgid "should be %{count} character(s)"
48 | msgid_plural "should be %{count} character(s)"
49 | msgstr[0] ""
50 | msgstr[1] ""
51 |
52 | msgid "should have %{count} item(s)"
53 | msgid_plural "should have %{count} item(s)"
54 | msgstr[0] ""
55 | msgstr[1] ""
56 |
57 | msgid "should be at least %{count} character(s)"
58 | msgid_plural "should be at least %{count} character(s)"
59 | msgstr[0] ""
60 | msgstr[1] ""
61 |
62 | msgid "should have at least %{count} item(s)"
63 | msgid_plural "should have at least %{count} item(s)"
64 | msgstr[0] ""
65 | msgstr[1] ""
66 |
67 | msgid "should be at most %{count} character(s)"
68 | msgid_plural "should be at most %{count} character(s)"
69 | msgstr[0] ""
70 | msgstr[1] ""
71 |
72 | msgid "should have at most %{count} item(s)"
73 | msgid_plural "should have at most %{count} item(s)"
74 | msgstr[0] ""
75 | msgstr[1] ""
76 |
77 | ## From Ecto.Changeset.validate_number/3
78 | msgid "must be less than %{number}"
79 | msgstr ""
80 |
81 | msgid "must be greater than %{number}"
82 | msgstr ""
83 |
84 | msgid "must be less than or equal to %{number}"
85 | msgstr ""
86 |
87 | msgid "must be greater than or equal to %{number}"
88 | msgstr ""
89 |
90 | msgid "must be equal to %{number}"
91 | msgstr ""
92 |
--------------------------------------------------------------------------------
/apps/bank_web/test/controllers/account_controller_test.exs:
--------------------------------------------------------------------------------
1 | defmodule BankWeb.AccountControllerTest do
2 | use BankWeb.ConnCase
3 |
4 | @moduletag isolation: :serializable
5 |
6 | test "show", %{conn: conn} do
7 | {:ok, %{customer: alice}} = Bank.register_customer("alice", "alice@example.com", "secret12")
8 | Bank.create_deposit!(alice, ~M"10 USD")
9 |
10 | conn = post conn, "/sign_in", %{session: %{email: "alice@example.com", password: "secret12"}}
11 |
12 | conn = get conn, "/account"
13 | assert conn.status == 200
14 | assert conn.resp_body =~ "Account balance
\n\n$10.00"
15 | assert conn.resp_body =~ "Deposit"
16 | end
17 |
18 | test "unauthenticated", %{conn: conn} do
19 | conn = get conn, "/account"
20 | assert conn.status == 302
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/apps/bank_web/test/controllers/page_controller_test.exs:
--------------------------------------------------------------------------------
1 | defmodule BankWeb.PageControllerTest do
2 | use BankWeb.ConnCase
3 |
4 | setup do
5 | Bank.create_customer!("alice", "alice@example.com")
6 | :ok
7 | end
8 |
9 | test "index: unauthenticated", %{conn: conn} do
10 | conn = get conn, "/"
11 | assert html_response(conn, 302) =~ ~r{"/sign_in"}
12 | end
13 |
14 | test "index: authenticated", %{conn: conn} do
15 | conn = post conn, "/sign_in_as/alice"
16 | assert html_response(conn, 302) =~ ~r{"/"}
17 |
18 | conn = get conn, "/"
19 | assert html_response(conn, 200) =~ "Signed in as alice"
20 |
21 | conn = get conn, "/sign_out"
22 | assert html_response(conn, 302) =~ ~r{"/"}
23 |
24 | conn = get conn, "/"
25 | assert html_response(conn, 302) =~ ~r{"/sign_in"}
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/apps/bank_web/test/controllers/transfer_controller_test.exs:
--------------------------------------------------------------------------------
1 | defmodule BankWeb.TransferControllerTest do
2 | use BankWeb.ConnCase
3 |
4 | @moduletag isolation: :serializable
5 |
6 | setup do
7 | :ok = Messenger.Local.setup()
8 |
9 | alice = Bank.create_customer!("alice", "alice@example.com")
10 | bob = Bank.create_customer!("bob", "bob@example.com")
11 | Bank.create_deposit!(alice, ~M"10 USD")
12 |
13 | conn = assign(build_conn(), :current_customer, alice)
14 |
15 | {:ok, %{conn: conn, alice: alice, bob: bob}}
16 | end
17 |
18 | test "create: success", %{conn: conn} do
19 | params = %{
20 | amount_string: "2.01",
21 | destination_username: "bob",
22 | description: "Lunch money"
23 | }
24 | conn = post conn, "/transfers", %{"transfer" => params}
25 | assert html_response(conn, 302)
26 | end
27 |
28 | test "create: invalid amount", %{conn: conn} do
29 | conn = post conn, "/transfers", %{"transfer" => %{amount_string: "bad"}}
30 | assert html_response(conn, 200) =~ "is invalid"
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/apps/bank_web/test/support/channel_case.ex:
--------------------------------------------------------------------------------
1 | defmodule BankWeb.ChannelCase do
2 | @moduledoc """
3 | This module defines the test case to be used by
4 | channel tests.
5 |
6 | Such tests rely on `Phoenix.ChannelTest` and also
7 | import other functionality to make it easier
8 | to build and query models.
9 |
10 | Finally, if the test case interacts with the database,
11 | it cannot be async. For this reason, every test runs
12 | inside a transaction which is reset at the beginning
13 | of the test unless the test case is marked as async.
14 | """
15 |
16 | use ExUnit.CaseTemplate
17 |
18 | using do
19 | quote do
20 | # Import conveniences for testing with channels
21 | use Phoenix.ChannelTest
22 |
23 | alias Bank.Repo
24 | import Ecto
25 | import Ecto.Changeset
26 | import Ecto.Query
27 |
28 |
29 | # The default endpoint for testing
30 | @endpoint BankWeb.Endpoint
31 | end
32 | end
33 |
34 | setup tags do
35 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(Bank.Repo)
36 |
37 | unless tags[:async] do
38 | Ecto.Adapters.SQL.Sandbox.mode(Bank.Repo, {:shared, self()})
39 | end
40 |
41 | :ok
42 | end
43 | end
44 |
--------------------------------------------------------------------------------
/apps/bank_web/test/support/conn_case.ex:
--------------------------------------------------------------------------------
1 | defmodule BankWeb.ConnCase do
2 | @moduledoc """
3 | This module defines the test case to be used by
4 | tests that require setting up a connection.
5 |
6 | Such tests rely on `Phoenix.ConnTest` and also
7 | import other functionality to make it easier
8 | to build and query models.
9 |
10 | Finally, if the test case interacts with the database,
11 | it cannot be async. For this reason, every test runs
12 | inside a transaction which is reset at the beginning
13 | of the test unless the test case is marked as async.
14 | """
15 |
16 | use ExUnit.CaseTemplate
17 |
18 | using do
19 | quote do
20 | # Import conveniences for testing with connections
21 | use Phoenix.ConnTest
22 |
23 | alias Bank.Repo
24 | import Ecto
25 | import Ecto.Changeset
26 | import Ecto.Query
27 |
28 | import BankWeb.Router.Helpers
29 |
30 | import Money, only: [sigil_M: 2]
31 |
32 | # The default endpoint for testing
33 | @endpoint BankWeb.Endpoint
34 | end
35 | end
36 |
37 | setup tags do
38 | opts = tags |> Map.take([:isolation]) |> Enum.to_list()
39 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(Bank.Repo, opts)
40 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(Auth.Repo, opts)
41 |
42 | unless tags[:async] do
43 | Ecto.Adapters.SQL.Sandbox.mode(Bank.Repo, {:shared, self()})
44 | Ecto.Adapters.SQL.Sandbox.mode(Auth.Repo, {:shared, self()})
45 | end
46 |
47 | {:ok, conn: Phoenix.ConnTest.build_conn()}
48 | end
49 | end
50 |
--------------------------------------------------------------------------------
/apps/bank_web/test/support/model_case.ex:
--------------------------------------------------------------------------------
1 | defmodule BankWeb.ModelCase do
2 | @moduledoc """
3 | This module defines the test case to be used by
4 | model tests.
5 |
6 | You may define functions here to be used as helpers in
7 | your model tests. See `errors_on/2`'s definition as reference.
8 |
9 | Finally, if the test case interacts with the database,
10 | it cannot be async. For this reason, every test runs
11 | inside a transaction which is reset at the beginning
12 | of the test unless the test case is marked as async.
13 | """
14 |
15 | use ExUnit.CaseTemplate
16 |
17 | using do
18 | quote do
19 | alias Bank.Repo
20 |
21 | import Ecto
22 | import Ecto.Changeset
23 | import Ecto.Query
24 | import BankWeb.ModelCase
25 |
26 | import Money, only: [sigil_M: 2]
27 | end
28 | end
29 |
30 | setup tags do
31 | opts = tags |> Map.take([:isolation]) |> Enum.to_list()
32 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(Bank.Repo, opts)
33 | #
34 | # unless tags[:async] do
35 | # Ecto.Adapters.SQL.Sandbox.mode(Bank.Repo, {:shared, self()})
36 | # end
37 | #
38 | # if level = tags[:isolation] do
39 | # Bank.Repo.isolation(level)
40 | # end
41 |
42 | :ok
43 | end
44 |
45 | @doc """
46 | Helper for returning list of errors in a struct when given certain data.
47 |
48 | ## Examples
49 |
50 | Given a User schema that lists `:name` as a required field and validates
51 | `:password` to be safe, it would return:
52 |
53 | iex> errors_on(%User{}, %{password: "password"})
54 | [password: "is unsafe", name: "is blank"]
55 |
56 | You could then write your assertion like:
57 |
58 | assert {:password, "is unsafe"} in errors_on(%User{}, %{password: "password"})
59 |
60 | You can also create the changeset manually and retrieve the errors
61 | field directly:
62 |
63 | iex> changeset = User.changeset(%User{}, password: "password")
64 | iex> {:password, "is unsafe"} in changeset.errors
65 | true
66 | """
67 | def errors_on(struct, data) do
68 | struct.__struct__.changeset(struct, data)
69 | |> Ecto.Changeset.traverse_errors(&BankWeb.ErrorHelpers.translate_error/1)
70 | |> Enum.flat_map(fn {key, errors} -> for msg <- errors, do: {key, msg} end)
71 | end
72 | end
73 |
--------------------------------------------------------------------------------
/apps/bank_web/test/test_helper.exs:
--------------------------------------------------------------------------------
1 | ExUnit.start
2 |
3 | Ecto.Adapters.SQL.Sandbox.mode(Bank.Repo, :manual)
4 | Ecto.Adapters.SQL.Sandbox.mode(Auth.Repo, :manual)
5 |
--------------------------------------------------------------------------------
/apps/bank_web/test/views/account_view_test.exs:
--------------------------------------------------------------------------------
1 | defmodule BankWeb.AccountViewTest do
2 | use BankWeb.ConnCase, async: true
3 | doctest BankWeb.AccountView
4 | end
5 |
--------------------------------------------------------------------------------
/apps/bank_web/test/views/error_view_test.exs:
--------------------------------------------------------------------------------
1 | defmodule BankWeb.ErrorViewTest do
2 | use BankWeb.ConnCase, async: true
3 |
4 | # Bring render/3 and render_to_string/3 for testing custom views
5 | import Phoenix.View
6 |
7 | test "renders 404.html" do
8 | assert render_to_string(BankWeb.ErrorView, "404.html", []) ==
9 | "Page not found"
10 | end
11 |
12 | test "render 500.html" do
13 | assert render_to_string(BankWeb.ErrorView, "500.html", []) ==
14 | "Internal server error"
15 | end
16 |
17 | test "render any other" do
18 | assert render_to_string(BankWeb.ErrorView, "505.html", []) ==
19 | "Internal server error"
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/apps/bank_web/test/views/layout_view_test.exs:
--------------------------------------------------------------------------------
1 | defmodule BankWeb.LayoutViewTest do
2 | use BankWeb.ConnCase, async: true
3 | end
4 |
--------------------------------------------------------------------------------
/apps/bank_web/test/views/page_view_test.exs:
--------------------------------------------------------------------------------
1 | defmodule BankWeb.PageViewTest do
2 | use BankWeb.ConnCase, async: true
3 | end
4 |
--------------------------------------------------------------------------------
/apps/bank_web/web/controllers/account_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule BankWeb.AccountController do
2 | use BankWeb.Web, :controller
3 | plug :require_authenticated
4 |
5 | def show(conn, _params) do
6 | customer = conn.assigns.current_customer
7 | balance = Bank.balance(customer)
8 | transactions = Bank.transactions(customer)
9 |
10 | render conn, "show.html", balance: balance, transactions: transactions
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/apps/bank_web/web/controllers/authentication.ex:
--------------------------------------------------------------------------------
1 | defmodule BankWeb.Authentication do
2 | import Plug.Conn
3 |
4 | def init([]), do: []
5 |
6 | def call(%{assigns: %{current_customer: %{}}} = conn, _opts), do: conn
7 | def call(conn, _opts) do
8 | id = get_session(conn, :customer_id)
9 | customer = if id, do: Bank.find_customer!(id: id)
10 |
11 | conn
12 | |> assign(:current_customer, customer)
13 | end
14 |
15 | def require_authenticated(%{assigns: %{current_customer: %{}}} = conn, _opts), do: conn
16 | def require_authenticated(conn, _opts) do
17 | conn
18 | |> Phoenix.Controller.put_flash(:alert, "You must be signed in to access that page")
19 | |> Phoenix.Controller.redirect(to: "/sign_in")
20 | |> halt()
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/apps/bank_web/web/controllers/page_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule BankWeb.PageController do
2 | use BankWeb.Web, :controller
3 | plug :require_authenticated
4 |
5 | def index(conn, _params) do
6 | render conn, "index.html"
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/apps/bank_web/web/controllers/session_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule BankWeb.SessionController do
2 | use BankWeb.Web, :controller
3 |
4 | def new(conn, _params) do
5 | customers = Bank.customers()
6 | render conn, "new.html", customers: customers
7 | end
8 |
9 | def create(conn, %{"session" => %{"email" => email, "password" => password}}) do
10 | case Auth.sign_in(email, password) do
11 | {:ok, account} ->
12 | customer = Bank.find_customer!(auth_account_id: account.id)
13 |
14 | conn
15 | |> put_session(:customer_id, customer.id)
16 | |> redirect(to: "/")
17 | {:error, _} ->
18 | customers = Bank.customers()
19 | render(conn, "new.html", customers: customers)
20 | end
21 | end
22 |
23 | def sign_in_as(conn, %{"username" => username}) do
24 | customer = Bank.find_customer!(username: username)
25 |
26 | conn
27 | |> put_session(:customer_id, customer.id)
28 | |> redirect(to: "/")
29 | end
30 |
31 | def sign_out(conn, _params) do
32 | conn
33 | |> clear_session()
34 | |> redirect(to: "/")
35 | end
36 | end
37 |
--------------------------------------------------------------------------------
/apps/bank_web/web/controllers/transfer_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule BankWeb.TransferController do
2 | use BankWeb.Web, :controller
3 | plug :require_authenticated
4 |
5 | def new(conn, _params) do
6 | customer = conn.assigns.current_customer
7 | transfer = Bank.build_transfer(customer)
8 | render conn, "new.html", transfer: transfer
9 | end
10 |
11 | def create(conn, %{"transfer" => transfer_params}) do
12 | customer = conn.assigns.current_customer
13 |
14 | case Bank.create_transfer(customer, transfer_params) do
15 | {:ok, _transfer} ->
16 | redirect conn, to: account_path(conn, :show)
17 | {:error, changeset} ->
18 | changeset = %{changeset | action: :transfer}
19 | render conn, "new.html", transfer: changeset
20 | end
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/apps/bank_web/web/gettext.ex:
--------------------------------------------------------------------------------
1 | defmodule BankWeb.Gettext do
2 | @moduledoc """
3 | A module providing Internationalization with a gettext-based API.
4 |
5 | By using [Gettext](https://hexdocs.pm/gettext),
6 | your module gains a set of macros for translations, for example:
7 |
8 | import BankWeb.Gettext
9 |
10 | # Simple translation
11 | gettext "Here is the string to translate"
12 |
13 | # Plural translation
14 | ngettext "Here is the string to translate",
15 | "Here are the strings to translate",
16 | 3
17 |
18 | # Domain-based translation
19 | dgettext "errors", "Here is the error message to translate"
20 |
21 | See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage.
22 | """
23 | use Gettext, otp_app: :bank_web
24 | end
25 |
--------------------------------------------------------------------------------
/apps/bank_web/web/router.ex:
--------------------------------------------------------------------------------
1 | defmodule BankWeb.Router do
2 | use BankWeb.Web, :router
3 |
4 | pipeline :browser do
5 | plug :accepts, ["html"]
6 | plug :fetch_session
7 | plug :fetch_flash
8 | plug :protect_from_forgery
9 | plug :put_secure_browser_headers
10 | plug BankWeb.Authentication
11 | end
12 |
13 | pipeline :api do
14 | plug :accepts, ["json"]
15 | end
16 |
17 | scope "/", BankWeb do
18 | pipe_through :browser # Use the default browser stack
19 |
20 | get "/", PageController, :index
21 |
22 | get "/sign_in", SessionController, :new
23 | post "/sign_in", SessionController, :create
24 | post "/sign_in_as/:username", SessionController, :sign_in_as
25 | get "/sign_out", SessionController, :sign_out
26 |
27 | get "/account", AccountController, :show
28 |
29 | get "/transfers/new", TransferController, :new
30 | post "/transfers", TransferController, :create
31 | end
32 |
33 | # Other scopes may use custom stacks.
34 | # scope "/api", BankWeb do
35 | # pipe_through :api
36 | # end
37 | end
38 |
--------------------------------------------------------------------------------
/apps/bank_web/web/static/assets/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wojtekmach/acme_bank/cee19a490d2b3c04465273b7a6212d7e6a81f736/apps/bank_web/web/static/assets/favicon.ico
--------------------------------------------------------------------------------
/apps/bank_web/web/static/assets/images/phoenix.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wojtekmach/acme_bank/cee19a490d2b3c04465273b7a6212d7e6a81f736/apps/bank_web/web/static/assets/images/phoenix.png
--------------------------------------------------------------------------------
/apps/bank_web/web/static/assets/robots.txt:
--------------------------------------------------------------------------------
1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
2 | #
3 | # To ban all spiders from the entire site uncomment the next two lines:
4 | # User-agent: *
5 | # Disallow: /
6 |
--------------------------------------------------------------------------------
/apps/bank_web/web/static/css/app.css:
--------------------------------------------------------------------------------
1 | /* This file is for your main application css. */
--------------------------------------------------------------------------------
/apps/bank_web/web/static/js/app.js:
--------------------------------------------------------------------------------
1 | // Brunch automatically concatenates all files in your
2 | // watched paths. Those paths can be configured at
3 | // config.paths.watched in "brunch-config.js".
4 | //
5 | // However, those files will only be executed if
6 | // explicitly imported. The only exception are files
7 | // in vendor, which are never wrapped in imports and
8 | // therefore are always executed.
9 |
10 | // Import dependencies
11 | //
12 | // If you no longer want to use a dependency, remember
13 | // to also remove its path from "config.paths.watched".
14 | import "phoenix_html"
15 |
16 | // Import local files
17 | //
18 | // Local files can be imported directly using relative
19 | // paths "./socket" or full ones "web/static/js/socket".
20 |
21 | // import socket from "./socket"
22 |
--------------------------------------------------------------------------------
/apps/bank_web/web/static/js/socket.js:
--------------------------------------------------------------------------------
1 | // NOTE: The contents of this file will only be executed if
2 | // you uncomment its entry in "web/static/js/app.js".
3 |
4 | // To use Phoenix channels, the first step is to import Socket
5 | // and connect at the socket path in "lib/my_app/endpoint.ex":
6 | import {Socket} from "phoenix"
7 |
8 | let socket = new Socket("/socket", {params: {token: window.userToken}})
9 |
10 | // When you connect, you'll often need to authenticate the client.
11 | // For example, imagine you have an authentication plug, `MyAuth`,
12 | // which authenticates the session and assigns a `:current_user`.
13 | // If the current user exists you can assign the user's token in
14 | // the connection for use in the layout.
15 | //
16 | // In your "web/router.ex":
17 | //
18 | // pipeline :browser do
19 | // ...
20 | // plug MyAuth
21 | // plug :put_user_token
22 | // end
23 | //
24 | // defp put_user_token(conn, _) do
25 | // if current_user = conn.assigns[:current_user] do
26 | // token = Phoenix.Token.sign(conn, "user socket", current_user.id)
27 | // assign(conn, :user_token, token)
28 | // else
29 | // conn
30 | // end
31 | // end
32 | //
33 | // Now you need to pass this token to JavaScript. You can do so
34 | // inside a script tag in "web/templates/layout/app.html.eex":
35 | //
36 | //
37 | //
38 | // You will need to verify the user token in the "connect/2" function
39 | // in "web/channels/user_socket.ex":
40 | //
41 | // def connect(%{"token" => token}, socket) do
42 | // # max_age: 1209600 is equivalent to two weeks in seconds
43 | // case Phoenix.Token.verify(socket, "user socket", token, max_age: 1209600) do
44 | // {:ok, user_id} ->
45 | // {:ok, assign(socket, :user, user_id)}
46 | // {:error, reason} ->
47 | // :error
48 | // end
49 | // end
50 | //
51 | // Finally, pass the token on connect as below. Or remove it
52 | // from connect if you don't care about authentication.
53 |
54 | socket.connect()
55 |
56 | // Now that you are connected, you can join channels with a topic:
57 | let channel = socket.channel("topic:subtopic", {})
58 | channel.join()
59 | .receive("ok", resp => { console.log("Joined successfully", resp) })
60 | .receive("error", resp => { console.log("Unable to join", resp) })
61 |
62 | export default socket
63 |
--------------------------------------------------------------------------------
/apps/bank_web/web/templates/account/show.html.eex:
--------------------------------------------------------------------------------
1 | Account balance
2 |
3 | <%= format_money(@balance) %>
4 |
5 | Transactions
6 |
7 |
8 |
9 |
10 | Description |
11 | Time |
12 | Amount |
13 |
14 |
15 |
16 | <%= for transaction <- @transactions do %>
17 |
18 | <%= transaction.description %> |
19 | <%= transaction.inserted_at %> |
20 | <%= format_amount(transaction) %> |
21 |
22 | <% end %>
23 |
24 |
25 |
26 | <%= link "New transfer", to: transfer_path(@conn, :new), class: "btn btn-default" %>
27 |
--------------------------------------------------------------------------------
/apps/bank_web/web/templates/layout/app.html.eex:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | Hello BankWeb!
11 | ">
12 |
13 |
14 |
15 |
35 |
36 |
37 | <% if flash = get_flash(@conn, :info) do %>
38 |
<%= flash %>
39 | <% end %>
40 |
41 | <% if flash = get_flash(@conn, :error) do %>
42 |
<%= flash %>
43 | <% end %>
44 |
45 |
46 | <%= render @view_module, @view_template, assigns %>
47 |
48 |
49 |
50 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/apps/bank_web/web/templates/page/index.html.eex:
--------------------------------------------------------------------------------
1 | Welcome
2 |
--------------------------------------------------------------------------------
/apps/bank_web/web/templates/session/new.html.eex:
--------------------------------------------------------------------------------
1 | Sign in
2 |
3 |
4 |
5 | <%= form_for @conn, session_path(@conn, :create), [as: :session], fn f -> %>
6 |
7 | <%= label f, :email %>
8 | <%= text_input f, :email, class: "form-control" %>
9 | <%= error_tag f, :email %>
10 |
11 |
12 |
13 | <%= label f, :password %>
14 | <%= password_input f, :password, class: "form-control" %>
15 | <%= error_tag f, :password %>
16 |
17 |
18 |
19 | <%= submit "Sign in", class: "btn btn-primary" %>
20 |
21 | <% end %>
22 |
23 |
24 |
25 | [Debug] Sign in
26 |
27 | <%= for customer <- @customers do %>
28 | <%= link "Sign in as #{customer.username}", to: session_path(@conn, :sign_in_as, customer.username), method: :post, class: "btn btn-default" %>
29 | <% end %>
30 |
--------------------------------------------------------------------------------
/apps/bank_web/web/templates/transfer/new.html.eex:
--------------------------------------------------------------------------------
1 | New transfer
2 |
3 |
4 |
5 |
6 | <%= form_for @transfer, transfer_path(@conn, :create), fn f -> %>
7 |
8 | <%= label f, :amount_string, "Amount" %>
9 | <%= text_input f, :amount_string, class: "form-control" %>
10 | <%= error_tag f, :amount_string %>
11 |
12 |
13 |
14 | <%= label f, :destination_username, "Destination username" %>
15 | <%= text_input f, :destination_username, class: "form-control" %>
16 | <%= error_tag f, :destination_username %>
17 |
18 |
19 |
20 | <%= label f, :description %>
21 | <%= textarea f, :description, class: "form-control" %>
22 | <%= error_tag f, :description %>
23 |
24 |
25 |
26 | <%= submit "Submit", class: "btn btn-primary" %>
27 |
28 | <% end %>
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/apps/bank_web/web/views/account_view.ex:
--------------------------------------------------------------------------------
1 | defmodule BankWeb.AccountView do
2 | use BankWeb.Web, :view
3 |
4 | def format_amount(%Bank.Ledger.Entry{type: type, amount: amount}) do
5 | sign(type) <> format_money(amount)
6 | end
7 |
8 | defp sign("credit"), do: "+"
9 | defp sign("debit"), do: "-"
10 |
11 | def format_money(%Money{} = money) do
12 | [value, currency] = to_string(money) |> String.split(" ", trim: true)
13 | do_format_money(value, currency)
14 | end
15 |
16 | defp do_format_money(value, "USD"), do: "$" <> value
17 | end
18 |
--------------------------------------------------------------------------------
/apps/bank_web/web/views/error_helpers.ex:
--------------------------------------------------------------------------------
1 | defmodule BankWeb.ErrorHelpers do
2 | @moduledoc """
3 | Conveniences for translating and building error messages.
4 | """
5 |
6 | use Phoenix.HTML
7 |
8 | @doc """
9 | Generates tag for inlined form input errors.
10 | """
11 | def error_tag(form, field) do
12 | if error = form.errors[field] do
13 | content_tag :span, translate_error(error), class: "help-block"
14 | end
15 | end
16 |
17 | @doc """
18 | Translates an error message using gettext.
19 | """
20 | def translate_error({msg, opts}) do
21 | # Because error messages were defined within Ecto, we must
22 | # call the Gettext module passing our Gettext backend. We
23 | # also use the "errors" domain as translations are placed
24 | # in the errors.po file.
25 | # Ecto will pass the :count keyword if the error message is
26 | # meant to be pluralized.
27 | # On your own code and templates, depending on whether you
28 | # need the message to be pluralized or not, this could be
29 | # written simply as:
30 | #
31 | # dngettext "errors", "1 file", "%{count} files", count
32 | # dgettext "errors", "is invalid"
33 | #
34 | if count = opts[:count] do
35 | Gettext.dngettext(BankWeb.Gettext, "errors", msg, msg, count, opts)
36 | else
37 | Gettext.dgettext(BankWeb.Gettext, "errors", msg, opts)
38 | end
39 | end
40 | end
41 |
--------------------------------------------------------------------------------
/apps/bank_web/web/views/error_view.ex:
--------------------------------------------------------------------------------
1 | defmodule BankWeb.ErrorView do
2 | use BankWeb.Web, :view
3 |
4 | def render("404.html", _assigns) do
5 | "Page not found"
6 | end
7 |
8 | def render("500.html", _assigns) do
9 | "Internal server error"
10 | end
11 |
12 | # In case no render clause matches or no
13 | # template is found, let's render it as 500
14 | def template_not_found(_template, assigns) do
15 | render "500.html", assigns
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/apps/bank_web/web/views/layout_view.ex:
--------------------------------------------------------------------------------
1 | defmodule BankWeb.LayoutView do
2 | use BankWeb.Web, :view
3 |
4 | def nav_link(conn, title, opts) do
5 | active? = conn.request_path == Keyword.fetch!(opts, :to)
6 | class = if active?, do: "active", else: ""
7 |
8 | content_tag(:li, link(title, opts), class: class)
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/apps/bank_web/web/views/page_view.ex:
--------------------------------------------------------------------------------
1 | defmodule BankWeb.PageView do
2 | use BankWeb.Web, :view
3 | end
4 |
--------------------------------------------------------------------------------
/apps/bank_web/web/views/session_view.ex:
--------------------------------------------------------------------------------
1 | defmodule BankWeb.SessionView do
2 | use BankWeb.Web, :view
3 | end
4 |
--------------------------------------------------------------------------------
/apps/bank_web/web/views/transfer_view.ex:
--------------------------------------------------------------------------------
1 | defmodule BankWeb.TransferView do
2 | use BankWeb.Web, :view
3 | end
4 |
--------------------------------------------------------------------------------
/apps/bank_web/web/web.ex:
--------------------------------------------------------------------------------
1 | defmodule BankWeb.Web do
2 | @moduledoc """
3 | A module that keeps using definitions for controllers,
4 | views and so on.
5 |
6 | This can be used in your application as:
7 |
8 | use BankWeb.Web, :controller
9 | use BankWeb.Web, :view
10 |
11 | The definitions below will be executed for every view,
12 | controller, etc, so keep them short and clean, focused
13 | on imports, uses and aliases.
14 |
15 | Do NOT define functions inside the quoted expressions
16 | below.
17 | """
18 |
19 | def model do
20 | quote do
21 | use Ecto.Schema
22 |
23 | import Ecto
24 | import Ecto.Changeset
25 | import Ecto.Query
26 | end
27 | end
28 |
29 | def controller do
30 | quote do
31 | use Phoenix.Controller
32 |
33 | alias Bank.Repo
34 | import Ecto
35 | import Ecto.Query
36 |
37 | import BankWeb.Router.Helpers
38 | import BankWeb.Gettext
39 |
40 | import BankWeb.Authentication, only: [require_authenticated: 2]
41 | end
42 | end
43 |
44 | def view do
45 | quote do
46 | use Phoenix.View, root: "web/templates"
47 |
48 | # Import convenience functions from controllers
49 | import Phoenix.Controller, only: [get_csrf_token: 0, get_flash: 2, view_module: 1]
50 |
51 | # Use all HTML functionality (forms, tags, etc)
52 | use Phoenix.HTML
53 |
54 | import BankWeb.Router.Helpers
55 | import BankWeb.ErrorHelpers
56 | import BankWeb.Gettext
57 | end
58 | end
59 |
60 | def router do
61 | quote do
62 | use Phoenix.Router
63 | end
64 | end
65 |
66 | def channel do
67 | quote do
68 | use Phoenix.Channel
69 |
70 | alias Bank.Repo
71 | import Ecto
72 | import Ecto.Query
73 | import BankWeb.Gettext
74 | end
75 | end
76 |
77 | @doc """
78 | When used, dispatch to the appropriate controller/view/etc.
79 | """
80 | defmacro __using__(which) when is_atom(which) do
81 | apply(__MODULE__, which, [])
82 | end
83 | end
84 |
--------------------------------------------------------------------------------
/apps/master_proxy/.gitignore:
--------------------------------------------------------------------------------
1 | # The directory Mix will write compiled artifacts to.
2 | /_build
3 |
4 | # If you run "mix test --cover", coverage assets end up here.
5 | /cover
6 |
7 | # The directory Mix downloads your dependencies sources to.
8 | /deps
9 |
10 | # Where 3rd-party dependencies like ExDoc output generated docs.
11 | /doc
12 |
13 | # If the VM crashes, it generates a dump, let's ignore it too.
14 | erl_crash.dump
15 |
16 | # Also ignore archive artifacts (built via "mix archive.build").
17 | *.ez
18 |
--------------------------------------------------------------------------------
/apps/master_proxy/README.md:
--------------------------------------------------------------------------------
1 | # MasterProxy
2 |
3 | Proxies requests to Web apps that are part of the platform.
4 | Useful for Heroku deployment when just one web port is exposed.
5 |
6 | This application is based on a gist shared by @Gazler, thanks Gary!
7 |
--------------------------------------------------------------------------------
/apps/master_proxy/config/config.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
3 | config :master_proxy, ecto_repos: []
4 |
--------------------------------------------------------------------------------
/apps/master_proxy/lib/master_proxy.ex:
--------------------------------------------------------------------------------
1 | defmodule MasterProxy do
2 | @moduledoc """
3 | Proxies requests to Web apps that are part of the platform.
4 | Useful for Heroku deployment when just one web port is exposed.
5 |
6 | See `MasterProxy.Plug`.
7 | """
8 | end
9 |
--------------------------------------------------------------------------------
/apps/master_proxy/lib/master_proxy/application.ex:
--------------------------------------------------------------------------------
1 | defmodule MasterProxy.Application do
2 | @moduledoc false
3 | use Application
4 |
5 | def start(_type, _args) do
6 | import Supervisor.Spec, warn: false
7 |
8 | port = (System.get_env("PORT") || "3333") |> String.to_integer
9 | cowboy = Plug.Adapters.Cowboy.child_spec(:http, MasterProxy.Plug, [], [port: port])
10 |
11 | children = [
12 | cowboy
13 | ]
14 |
15 | opts = [strategy: :one_for_one, name: MasterProxy.Supervisor]
16 | Supervisor.start_link(children, opts)
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/apps/master_proxy/lib/master_proxy/plug.ex:
--------------------------------------------------------------------------------
1 | defmodule MasterProxy.Plug do
2 | def init(options) do
3 | options
4 | end
5 |
6 | def call(conn, _opts) do
7 | cond do
8 | conn.request_path =~ ~r{/backoffice} ->
9 | Backoffice.Endpoint.call(conn, [])
10 | true ->
11 | BankWeb.Endpoint.call(conn, [])
12 | end
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/apps/master_proxy/mix.exs:
--------------------------------------------------------------------------------
1 | defmodule MasterProxy.Mixfile do
2 | use Mix.Project
3 |
4 | def project do
5 | [app: :master_proxy,
6 | version: "0.1.0",
7 | build_path: "../../_build",
8 | config_path: "../../config/config.exs",
9 | deps_path: "../../deps",
10 | lockfile: "../../mix.lock",
11 | elixir: "~> 1.4.2",
12 | build_embedded: Mix.env == :prod,
13 | start_permanent: Mix.env == :prod,
14 | deps: deps()]
15 | end
16 |
17 | def application do
18 | [applications: [:logger, :plug_cowboy, :bank_web, :backoffice],
19 | mod: {MasterProxy.Application, []}]
20 | end
21 |
22 | defp deps do
23 | [{:plug_cowboy, "~> 1.0"},
24 | {:bank_web, in_umbrella: true},
25 | {:backoffice, in_umbrella: true}]
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/apps/master_proxy/test/master_proxy_test.exs:
--------------------------------------------------------------------------------
1 | defmodule MasterProxyTest do
2 | use ExUnit.Case
3 | doctest MasterProxy
4 |
5 | test "the truth" do
6 | assert 1 + 1 == 2
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/apps/master_proxy/test/test_helper.exs:
--------------------------------------------------------------------------------
1 | ExUnit.start()
2 |
--------------------------------------------------------------------------------
/apps/messenger/.gitignore:
--------------------------------------------------------------------------------
1 | # The directory Mix will write compiled artifacts to.
2 | /_build
3 |
4 | # If you run "mix test --cover", coverage assets end up here.
5 | /cover
6 |
7 | # The directory Mix downloads your dependencies sources to.
8 | /deps
9 |
10 | # Where 3rd-party dependencies like ExDoc output generated docs.
11 | /doc
12 |
13 | # If the VM crashes, it generates a dump, let's ignore it too.
14 | erl_crash.dump
15 |
16 | # Also ignore archive artifacts (built via "mix archive.build").
17 | *.ez
18 |
--------------------------------------------------------------------------------
/apps/messenger/.iex.exs:
--------------------------------------------------------------------------------
1 | import_file "../../.iex.exs"
2 |
--------------------------------------------------------------------------------
/apps/messenger/README.md:
--------------------------------------------------------------------------------
1 | # Messenger
2 |
3 | Provides messaging services to the platform.
4 |
5 | Right now, `Messenger` is a dummy, stateless service that just stores the messages in the filesystem.
6 |
7 | In the future this could be extended to support:
8 |
9 | - SMS
10 | - rate-limiting
11 | - bouncing
12 | - idempotence
13 | - failure handling using e.g. retrying with backoff, circuit breakers, fallback providers
14 | - analytics
15 |
16 | See [`Messenger`](lib/messenger.ex) for docs.
17 |
--------------------------------------------------------------------------------
/apps/messenger/config/config.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
3 | config :messenger, adapter: Messenger.Local
4 |
--------------------------------------------------------------------------------
/apps/messenger/lib/messenger.ex:
--------------------------------------------------------------------------------
1 | defmodule Messenger do
2 | @moduledoc ~S"""
3 | Messenger delivers messages.
4 |
5 | Available adapters:
6 |
7 | - `Messenger.Local`
8 | - `Messenger.Logger`
9 | """
10 |
11 | @type email :: String.t
12 | @type subject :: String.t
13 | @type body :: String.t
14 |
15 | @callback deliver_email(email, subject, body) :: :ok
16 |
17 | def deliver_email(email, subject, body) do
18 | adapter.deliver_email(email, subject, body)
19 | end
20 |
21 | defp adapter do
22 | Application.get_env(:messenger, :adapter)
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/apps/messenger/lib/messenger/application.ex:
--------------------------------------------------------------------------------
1 | defmodule Messenger.Application do
2 | @moduledoc false
3 |
4 | use Application
5 |
6 | def start(_type, _args) do
7 | import Supervisor.Spec, warn: false
8 |
9 | children = []
10 |
11 | opts = [strategy: :one_for_one, name: Messenger.Supervisor]
12 | Supervisor.start_link(children, opts)
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/apps/messenger/lib/messenger/logger.ex:
--------------------------------------------------------------------------------
1 | defmodule Messenger.Logger do
2 | require Logger
3 |
4 | def deliver_email(email, subject, body) do
5 | Logger.info("Delivered email:#{inspect email} subject:#{inspect subject} body:#{inspect body}")
6 | :ok
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/apps/messenger/lib/messenger/test.ex:
--------------------------------------------------------------------------------
1 | defmodule Messenger.Local do
2 | @moduledoc ~S"""
3 | Test adapter for `Messenger`.
4 |
5 | Stores messages on the filesystem. Run `setup/0` before each test to
6 | ensure clean state.
7 | """
8 |
9 | @root "tmp/messenger"
10 |
11 | def setup do
12 | File.rm_rf!(@root)
13 | :ok
14 | end
15 |
16 | def deliver_email(username, subject, body) do
17 | File.mkdir_p!(@root)
18 |
19 | uniq_id = :erlang.unique_integer()
20 | File.write!("#{@root}/#{username}-#{uniq_id}", "#{subject}\n\n#{body}")
21 | :ok
22 | end
23 |
24 | def messages_for(username) do
25 | File.mkdir_p!(@root)
26 |
27 | Path.wildcard("#{@root}/#{username}-*")
28 | |> Enum.map(fn path ->
29 | File.read!(path)
30 | |> String.split("\n\n", parts: 2)
31 | |> List.to_tuple
32 | end)
33 | end
34 |
35 | def subjects_for(username) do
36 | messages_for(username)
37 | |> Enum.map(&elem(&1, 0))
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/apps/messenger/mix.exs:
--------------------------------------------------------------------------------
1 | defmodule Messenger.Mixfile do
2 | use Mix.Project
3 |
4 | def project do
5 | [app: :messenger,
6 | version: "0.1.0",
7 | build_path: "../../_build",
8 | config_path: "../../config/config.exs",
9 | deps_path: "../../deps",
10 | lockfile: "../../mix.lock",
11 | elixir: ">= 1.4.2",
12 | build_embedded: Mix.env == :prod,
13 | start_permanent: Mix.env == :prod,
14 | deps: deps(),
15 | docs: [main: "Messenger"]]
16 | end
17 |
18 | def application do
19 | [applications: [:logger],
20 | mod: {Messenger.Application, []}]
21 | end
22 |
23 | defp deps do
24 | []
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/apps/messenger/test/messenger_test.exs:
--------------------------------------------------------------------------------
1 | defmodule MessengerTest do
2 | use ExUnit.Case
3 | doctest Messenger
4 |
5 | setup do
6 | :ok = Messenger.Local.setup()
7 | :ok
8 | end
9 |
10 | test "messenger" do
11 | :ok = Messenger.deliver_email("alice", "subject 1", "body 1")
12 | :ok = Messenger.deliver_email("alice", "subject 2", "body 2")
13 | :ok = Messenger.deliver_email("bob", "subject 3", "body 3")
14 |
15 | assert Messenger.Local.subjects_for("alice") == ["subject 2", "subject 1"]
16 | assert Messenger.Local.messages_for("alice") == [
17 | {"subject 2", "body 2"},
18 | {"subject 1", "body 1"},
19 | ]
20 |
21 | assert Messenger.Local.subjects_for("bob") == ["subject 3"]
22 | assert Messenger.Local.messages_for("bob") == [
23 | {"subject 3", "body 3"},
24 | ]
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/apps/messenger/test/test_helper.exs:
--------------------------------------------------------------------------------
1 | ExUnit.start()
2 |
--------------------------------------------------------------------------------
/apps/money/.gitignore:
--------------------------------------------------------------------------------
1 | # The directory Mix will write compiled artifacts to.
2 | /_build
3 |
4 | # If you run "mix test --cover", coverage assets end up here.
5 | /cover
6 |
7 | # The directory Mix downloads your dependencies sources to.
8 | /deps
9 |
10 | # Where 3rd-party dependencies like ExDoc output generated docs.
11 | /doc
12 |
13 | # If the VM crashes, it generates a dump, let's ignore it too.
14 | erl_crash.dump
15 |
16 | # Also ignore archive artifacts (built via "mix archive.build").
17 | *.ez
18 |
--------------------------------------------------------------------------------
/apps/money/.iex.exs:
--------------------------------------------------------------------------------
1 | import_file "../../.iex.exs"
2 |
--------------------------------------------------------------------------------
/apps/money/README.md:
--------------------------------------------------------------------------------
1 | # Money
2 |
3 | Functions to work with monetary values in currencies. See:
4 |
5 | - [`Money`](lib/money.ex)
6 | - [Phoenix integration](lib/money/phoenix.ex)
7 | - [Ecto integration](lib/money/ecto.ex)
8 |
--------------------------------------------------------------------------------
/apps/money/config/config.exs:
--------------------------------------------------------------------------------
1 | # This file is responsible for configuring your application
2 | # and its dependencies with the aid of the Mix.Config module.
3 | use Mix.Config
4 |
5 | config :money, ecto_repos: []
6 |
7 | # This configuration is loaded before any dependency and is restricted
8 | # to this project. If another project depends on this project, this
9 | # file won't be loaded nor affect the parent project. For this reason,
10 | # if you want to provide default values for your application for
11 | # 3rd-party users, it should be done in your "mix.exs" file.
12 |
13 | # You can configure for your application as:
14 | #
15 | # config :money, key: :value
16 | #
17 | # And access this configuration in your application as:
18 | #
19 | # Application.get_env(:money, :key)
20 | #
21 | # Or configure a 3rd-party app:
22 | #
23 | # config :logger, level: :info
24 | #
25 |
26 | # It is also possible to import configuration files, relative to this
27 | # directory. For example, you can emulate configuration per environment
28 | # by uncommenting the line below and defining dev.exs, test.exs and such.
29 | # Configuration from the imported file will override the ones defined
30 | # here (which is why it is important to import them last).
31 | #
32 | # import_config "#{Mix.env}.exs"
33 |
--------------------------------------------------------------------------------
/apps/money/lib/money.ex:
--------------------------------------------------------------------------------
1 | defmodule Money do
2 | @moduledoc ~S"""
3 | `Money` represents some monetary value (stored in cents) in a given currency.
4 |
5 | See `Money.Ecto` for a custom type implementation that can be used in schemas.
6 |
7 | In order to use the `~M` sigil, import the module:
8 |
9 | import Money
10 |
11 | ## Examples
12 |
13 | iex> Money.new("10.00 USD")
14 | ~M"10.00 USD"
15 |
16 | iex> ~M"10.00 USD".currency
17 | "USD"
18 | iex> ~M"10.01 USD".cents
19 | 1001
20 |
21 | iex> Money.add(~M"10 USD", ~M"20 USD")
22 | ~M"30.00 USD"
23 |
24 | iex> Kernel.to_string(~M"-10.50 USD")
25 | "-10.50 USD"
26 |
27 | iex> inspect(~M"10 USD")
28 | "~M\"10.00 USD\""
29 |
30 | iex> Money.parse("10 USD")
31 | {:ok, ~M"10 USD"}
32 | iex> Money.parse("10.1 USD")
33 | :error
34 |
35 | iex> ~M"10.001 USD"
36 | ** (ArgumentError) invalid string: "10.001 USD"
37 |
38 | """
39 |
40 | defstruct cents: 0, currency: nil
41 |
42 | def new(str) when is_binary(str) do
43 | case parse(str) do
44 | {:ok, money} -> money
45 | :error -> raise ArgumentError, "invalid string: #{inspect str}"
46 | end
47 | end
48 |
49 | def parse(str) when is_binary(str) do
50 | case Regex.run(~r/\A(-?)(\d+)(\.(\d{2}))?\ ([A-Z]{3})\z/, str) do
51 | [_, sign, dollars, _, cents, currency] ->
52 | do_parse(sign, dollars, cents, currency)
53 | _ -> :error
54 | end
55 | end
56 |
57 | defp do_parse(sign, dollars, "", currency),
58 | do: do_parse(sign, dollars, "00", currency)
59 | defp do_parse(sign, dollars, cents, currency) do
60 | sign = if sign == "-", do: -1, else: 1
61 | cents = sign * (String.to_integer(dollars) * 100 + String.to_integer(cents))
62 | {:ok, %Money{cents: cents, currency: currency}}
63 | end
64 |
65 | def sigil_M(str, _opts),
66 | do: new(str)
67 |
68 | def add(%Money{cents: left_cents, currency: currency},
69 | %Money{cents: right_cents, currency: currency}) do
70 | %Money{cents: left_cents + right_cents, currency: currency}
71 | end
72 |
73 | def to_string(%Money{cents: cents, currency: currency}) when cents >= 0 do
74 | {dollars, cents} = {div(cents, 100), rem(cents, 100)}
75 | cents = :io_lib.format("~2..0B", [cents]) |> IO.iodata_to_binary
76 | "#{dollars}.#{cents} #{currency}"
77 | end
78 | def to_string(%Money{cents: cents, currency: currency}) do
79 | "-" <> Money.to_string(%Money{cents: -cents, currency: currency})
80 | end
81 | end
82 |
83 | defimpl Inspect, for: Money do
84 | def inspect(money, _opts),
85 | do: "~M\"#{money}\""
86 | end
87 |
88 | defimpl String.Chars, for: Money do
89 | defdelegate to_string(data), to: Money
90 | end
91 |
--------------------------------------------------------------------------------
/apps/money/lib/money/ecto.ex:
--------------------------------------------------------------------------------
1 | if Code.ensure_loaded?(Ecto.Type) do
2 | defmodule Money.Ecto do
3 | @moduledoc ~S"""
4 |
5 | Provides custom Ecto type to use `Money`.
6 |
7 | It might work with different adapters, but it has only been tested
8 | on PostgreSQL as a composite type.
9 |
10 | ## Usage:
11 |
12 | Schema:
13 |
14 | defmodule Item do
15 | use Ecto.Schema
16 |
17 | schema "items" do
18 | field :name, :string
19 | field :price, Money.Ecto
20 | end
21 | end
22 |
23 | Migration:
24 |
25 | def change do
26 | execute "
27 | CREATE TYPE moneyz AS (
28 | cents integer,
29 | currency varchar
30 | );
31 | "
32 |
33 | create table(:items) do
34 | add :name, :string
35 | add :price, :moneyz
36 | end
37 | end
38 |
39 | """
40 |
41 | @behaviour Ecto.Type
42 |
43 | def type, do: :moneyz
44 |
45 | # TODO:
46 | def cast(_), do: :error
47 |
48 | def load({cents, currency}) when is_integer(cents) and is_binary(currency) do
49 | {:ok, %Money{cents: cents, currency: currency}}
50 | end
51 | def load(_), do: :error
52 |
53 | def dump(%Money{cents: cents, currency: currency}) do
54 | {:ok, {cents, currency}}
55 | end
56 | def dump(_), do: :error
57 | end
58 | end
59 |
--------------------------------------------------------------------------------
/apps/money/lib/money/phoenix.ex:
--------------------------------------------------------------------------------
1 | if Code.ensure_loaded?(Phoenix.HTML.Safe) do
2 | defimpl Phoenix.HTML.Safe, for: Money do
3 | defdelegate to_iodata(data), to: Money, as: :to_string
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/apps/money/mix.exs:
--------------------------------------------------------------------------------
1 | defmodule Money.Mixfile do
2 | use Mix.Project
3 |
4 | def project do
5 | [app: :money,
6 | version: "0.1.0",
7 | build_path: "../../_build",
8 | config_path: "../../config/config.exs",
9 | deps_path: "../../deps",
10 | lockfile: "../../mix.lock",
11 | elixir: ">= 1.4.2",
12 | build_embedded: Mix.env == :prod,
13 | start_permanent: Mix.env == :prod,
14 | deps: deps(),
15 | docs: [main: "Money"]]
16 | end
17 |
18 | def application do
19 | [applications: [:logger]]
20 | end
21 |
22 | defp deps do
23 | [{:ecto, "~> 2.0", optional: true},
24 | {:phoenix_html, "~> 2.6", optional: true},
25 | {:plug, "~> 1.2", optional: true}]
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/apps/money/test/money_test.exs:
--------------------------------------------------------------------------------
1 | defmodule MoneyTest do
2 | use ExUnit.Case
3 | doctest Money, import: true
4 | import Money, only: [sigil_M: 2]
5 |
6 | test "works" do
7 | assert ~M"10 USD" == %Money{cents: 10_00, currency: "USD"}
8 | assert ~M"10.00 USD" == %Money{cents: 10_00, currency: "USD"}
9 | assert ~M"-5.10 USD" == %Money{cents: -5_10, currency: "USD"}
10 |
11 | assert to_string(~M"10 USD") == "10.00 USD"
12 | assert to_string(~M"10.00 USD") == "10.00 USD"
13 | assert to_string(~M"-5.10 USD") == "-5.10 USD"
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/apps/money/test/test_helper.exs:
--------------------------------------------------------------------------------
1 | ExUnit.start()
2 |
--------------------------------------------------------------------------------
/compile:
--------------------------------------------------------------------------------
1 | # apps/bank_web (default)
2 | brunch build --production
3 | mix phoenix.digest
4 |
5 | # apps/backoffice
6 | cd ../backoffice
7 | npm install
8 | brunch build --production
9 | mix phoenix.digest
10 |
--------------------------------------------------------------------------------
/config/config.exs:
--------------------------------------------------------------------------------
1 | # This file is responsible for configuring your application
2 | # and its dependencies with the aid of the Mix.Config module.
3 | use Mix.Config
4 |
5 | # By default, the umbrella project as well as each child
6 | # application will require this configuration file, ensuring
7 | # they all use the same configuration. While one could
8 | # configure all applications here, we prefer to delegate
9 | # back to each application for organization purposes.
10 | import_config "../apps/*/config/config.exs"
11 |
12 | # Sample configuration (overrides the imported configuration above):
13 | #
14 | # config :logger, :console,
15 | # level: :info,
16 | # format: "$date $time [$level] $metadata$message\n",
17 | # metadata: [:user_id]
18 |
--------------------------------------------------------------------------------
/docs/diagram.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wojtekmach/acme_bank/cee19a490d2b3c04465273b7a6212d7e6a81f736/docs/diagram.png
--------------------------------------------------------------------------------
/docs/main.md:
--------------------------------------------------------------------------------
1 | # Acme Bank
2 |
3 | Acme Bank is an example project to explore and experiment with building modular and maintainable Elixir/Phoenix applications.
4 |
5 | See [README on GitHub](https://github.com/wojtekmach/acme_bank) for more information, including setup instructions.
6 |
7 | ## Apps
8 |
9 | - `Auth`
10 | - `Backoffice`
11 | - `BankWeb`
12 | - `Bank`
13 | - `MasterProxy`
14 | - `Messenger`
15 | - `Money`
16 |
17 | 
18 |
--------------------------------------------------------------------------------
/elixir_buildpack.config:
--------------------------------------------------------------------------------
1 | erlang_version=19.3
2 | elixir_version=1.4.2
3 |
4 | config_vars_to_export=(DATABASE_URL)
5 |
--------------------------------------------------------------------------------
/mix.exs:
--------------------------------------------------------------------------------
1 | defmodule BankPlatform.Mixfile do
2 | use Mix.Project
3 |
4 | def project do
5 | [apps_path: "apps",
6 | build_embedded: Mix.env == :prod,
7 | start_permanent: Mix.env == :prod,
8 | version: "1.0.0-dev",
9 | source_url: "https://github.com/wojtekmach/acme_bank",
10 | name: "Acme Bank",
11 | docs: [source_ref: "HEAD", main: "main", assets: "docs", extras: ["docs/main.md"]],
12 | deps: deps(),
13 | aliases: aliases()]
14 | end
15 |
16 | defp deps do
17 | [{:ex_doc, github: "elixir-lang/ex_doc", branch: "master", only: :dev}]
18 | end
19 |
20 | defp aliases do
21 | ["ecto.setup": ["ecto.create", "ecto.migrate", "ecto.seed"],
22 | "ecto.seed": ["run apps/bank/priv/repo/seeds.exs"],
23 | "ecto.reset": ["ecto.drop", "ecto.setup"],
24 | "test": ["ecto.create --quiet", "ecto.migrate", "test"]]
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/mix.lock:
--------------------------------------------------------------------------------
1 | %{
2 | "comeonin": {:hex, :comeonin, "2.5.3", "ccd70ebf465eaf4e11fc5a13fd0f5be2538b6b4975d4f7a13d571670b31da060", [:make, :mix], []},
3 | "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], []},
4 | "cowboy": {:hex, :cowboy, "1.1.2", "61ac29ea970389a88eca5a65601460162d370a70018afe6f949a29dca91f3bb0", [:rebar3], [{:cowlib, "~> 1.0.2", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3.2", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm"},
5 | "cowlib": {:hex, :cowlib, "1.0.2", "9d769a1d062c9c3ac753096f868ca121e2730b9a377de23dec0f7e08b1df84ee", [:make], []},
6 | "csvlixir": {:hex, :csvlixir, "1.0.0", "e9fd30abfca2d312390060e86bb7ec52487c813824dcccad45bb13e85ecad6b1", [:mix], []},
7 | "db_connection": {:hex, :db_connection, "1.1.3", "89b30ca1ef0a3b469b1c779579590688561d586694a3ce8792985d4d7e575a61", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm"},
8 | "decimal": {:hex, :decimal, "1.8.0", "ca462e0d885f09a1c5a342dbd7c1dcf27ea63548c65a65e67334f4b61803822e", [:mix], [], "hexpm"},
9 | "earmark": {:hex, :earmark, "1.0.2", "a0b0904d74ecc14da8bd2e6e0248e1a409a2bc91aade75fcf428125603de3853", [:mix], []},
10 | "ecto": {:hex, :ecto, "2.0.5", "7f4c79ac41ffba1a4c032b69d7045489f0069c256de606523c65d9f8188e502d", [:mix], [{:db_connection, "~> 1.0-rc.4", [hex: :db_connection, optional: true]}, {:decimal, "~> 1.1.2 or ~> 1.2", [hex: :decimal, optional: false]}, {:mariaex, "~> 0.7.7", [hex: :mariaex, optional: true]}, {:poison, "~> 1.5 or ~> 2.0", [hex: :poison, optional: true]}, {:poolboy, "~> 1.5", [hex: :poolboy, optional: false]}, {:postgrex, "~> 0.12.0", [hex: :postgrex, optional: true]}, {:sbroker, "~> 1.0-beta", [hex: :sbroker, optional: true]}]},
11 | "ex_admin": {:git, "https://github.com/wojtekmach/ex_admin.git", "5912810cfd048df6cf483ffeccf4c05355a2cb5e", [branch: "wm-customize-resource-name"]},
12 | "ex_doc": {:git, "https://github.com/elixir-lang/ex_doc.git", "316a7fb475c51fc8949f7b41472d14973dd8d6ab", [branch: "master"]},
13 | "ex_queb": {:git, "https://github.com/E-MetroTel/ex_queb.git", "1a03bdc6bd4df6d79381f180a3d7600292efb48d", []},
14 | "exactor": {:hex, :exactor, "2.2.2", "90b27d72c05614801a60f8400afd4e4346dfc33ea9beffe3b98a794891d2ff96", [:mix], []},
15 | "fs": {:hex, :fs, "0.9.2", "ed17036c26c3f70ac49781ed9220a50c36775c6ca2cf8182d123b6566e49ec59", [:rebar], []},
16 | "gettext": {:hex, :gettext, "0.11.0", "80c1dd42d270482418fa158ec5ba073d2980e3718bacad86f3d4ad71d5667679", [:mix], []},
17 | "inflex": {:hex, :inflex, "1.7.0", "4466a34b7d8e871d8164619ba0f3b8410ec782e900f0ae1d3d27a5875a29532e", [:mix], []},
18 | "mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm"},
19 | "phoenix": {:hex, :phoenix, "1.2.1", "6dc592249ab73c67575769765b66ad164ad25d83defa3492dc6ae269bd2a68ab", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, optional: true]}, {:phoenix_pubsub, "~> 1.0", [hex: :phoenix_pubsub, optional: false]}, {:plug, "~> 1.1", [hex: :plug, optional: false]}, {:poison, "~> 1.5 or ~> 2.0", [hex: :poison, optional: false]}]},
20 | "phoenix_ecto": {:hex, :phoenix_ecto, "3.0.1", "42eb486ef732cf209d0a353e791806721f33ff40beab0a86f02070a5649ed00a", [:mix], [{:ecto, "~> 2.0", [hex: :ecto, optional: false]}, {:phoenix_html, "~> 2.6", [hex: :phoenix_html, optional: true]}, {:plug, "~> 1.0", [hex: :plug, optional: false]}]},
21 | "phoenix_html": {:hex, :phoenix_html, "2.13.3", "850e292ff6e204257f5f9c4c54a8cb1f6fbc16ed53d360c2b780a3d0ba333867", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
22 | "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.0.5", "829218c4152ba1e9848e2bf8e161fcde6b4ec679a516259442561d21fde68d0b", [:mix], [{:fs, "~> 0.9.1", [hex: :fs, optional: false]}, {:phoenix, "~> 1.0 or ~> 1.2-rc", [hex: :phoenix, optional: false]}]},
23 | "phoenix_pubsub": {:hex, :phoenix_pubsub, "1.0.1", "c10ddf6237007c804bf2b8f3c4d5b99009b42eca3a0dfac04ea2d8001186056a", [:mix], []},
24 | "plug": {:hex, :plug, "1.8.3", "12d5f9796dc72e8ac9614e94bda5e51c4c028d0d428e9297650d09e15a684478", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm"},
25 | "plug_cowboy": {:hex, :plug_cowboy, "1.0.0", "2e2a7d3409746d335f451218b8bb0858301c3de6d668c3052716c909936eb57a", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
26 | "plug_crypto": {:hex, :plug_crypto, "1.0.0", "18e49317d3fa343f24620ed22795ec29d4a5e602d52d1513ccea0b07d8ea7d4d", [:mix], [], "hexpm"},
27 | "poison": {:hex, :poison, "2.2.0", "4763b69a8a77bd77d26f477d196428b741261a761257ff1cf92753a0d4d24a63", [:mix], []},
28 | "poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm"},
29 | "postgrex": {:hex, :postgrex, "0.12.2", "13cfd784a148da78f0edddd433bcef5c98388fdb2963ba25ed1fa7265885e6bf", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 1.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm"},
30 | "ranch": {:hex, :ranch, "1.3.2", "e4965a144dc9fbe70e5c077c65e73c57165416a901bd02ea899cfd95aa890986", [:rebar3], [], "hexpm"},
31 | "scrivener": {:hex, :scrivener, "2.1.1", "eb52c8b7d283e8999edd6fd50d872ab870669d1f4504134841d0845af11b5ef3", [:mix], []},
32 | "scrivener_ecto": {:git, "https://github.com/drewolson/scrivener_ecto.git", "df2c5720f763b002c48be1f132fbaaa594cfbeae", []},
33 | "xain": {:hex, :xain, "0.6.0", "5b61cfe3ffc17904759ee30a699f9e0b1aefd943e996ee4cafea76e5b2f59e3a", [:mix], []},
34 | }
35 |
--------------------------------------------------------------------------------
/phoenix_static_buildpack.config:
--------------------------------------------------------------------------------
1 | phoenix_relative_path=apps/bank_web
2 |
--------------------------------------------------------------------------------
/script/diagram.exs:
--------------------------------------------------------------------------------
1 | deps = Mix.Dep.loaded([]) |> Enum.filter(& &1.top_level)
2 |
3 | fun = fn dep, deps ->
4 | dep = Enum.find(deps, & &1.app == dep.app)
5 | children = Enum.filter(dep.deps, & Keyword.get(&1.opts, :in_umbrella))
6 | {{dep.app, nil}, children}
7 | end
8 |
9 | dep = Enum.at(deps, -1)
10 |
11 | Mix.Utils.print_tree([dep], fn dep -> fun.(dep, deps) end)
12 | Mix.Utils.write_dot_graph!("tmp/diagram.dot", "deps", [dep], fn dep -> fun.(dep, deps) end)
13 |
14 | System.cmd("dot", ["-Tpng", "tmp/diagram.dot", "-odocs/diagram.png"])
15 |
--------------------------------------------------------------------------------