How to create a deposit and a transfer funds function

I have been following this tutorial (https://lobotuerto.com/blog/building-a-json-api-in-elixir-with-phoenix/) to make my first JSON elixir app using Phoenix. I am doing so to create a mock banking app where users can create a profile with a starting amount and then deposit and transfer funds across to other accounts.

I am set to make a lot more functions but this is all new to me and I wasn’t able to find any decent tutorials that might help me - so I thought these two basic functions would be a good learning exercise if someone is willing to explain this to me?

I am using the pre-made create function to make the deposit one as shown below:

def create(conn, %{"user" => user_params}) do
    with {:ok, %User{} = user} <- Account.create_user(user_params) do
      conn
      |> put_status(:created)
      |> put_resp_header("location", Routes.user_path(conn, :show, user))
      |> render("show.json", user: user)
    end
  end

  def deposit(conn, %{"id" => id, "amount" => amount}) do
    user = Account.get_user!(id)
    with {:ok, %User{} = user} <- Account.deposit(user, amount) do
      render(conn, "show.json", user: user)
    end
  end

Is that right so far?

Then in the account.ex file, I am wondering how I will be able to ‘add’ the current amount to the deposited amount:

def create_user(attrs \\ %{}) do
    %User{}
    |> User.changeset(attrs)
    |> Repo.insert()
  end


  """
  def deposit(%User{} = user, attrs) do
    user
    |> User.changeset(attrs)
    |> Repo.update()
  end

Finally, this is how the schema is storing information as returned from a GET on the localhost:4000/api/users request:


{"data":[{"amount":200.5,"id":1,"name":"Tester"},{"amount":0.0,"id":2,"name":"Arisha Barron"},{"amount":0.0,"id":3,"name":"Branden Gibson"},{"amount":0.0,"id":4,"name":"Rhonda Church"},{"amount":0.0,"id":5,"name":"Georgina Hazel"},{"amount":5.0,"id":6,"name":"test2"}]}

Really quite confused on this so any help would be muchly appreciated!

The best way in my opinion is NOT TO ADD the amount to the users account, but to CREATE a transaction (not the database transaction) under the specific user’s account. In this way, you can easily keep tracking all the operations on all of the users’ accounts and spot on abnormality if needed, and you can worry less about race conditions when multiple deposits/withdrawals happen on a same account at the same time.

For doing that, you’ll need another table and the corresponding schema module:

defmodule MyApp.Account.Transaction do
  use Ecto.Schema
  import Ecto.Changeset
  alias MyApp.Account.User

  schema "transactions" do
    belongs_to :user, User
    field :amount, :decimal  # positive number for deposit, negative for withdrawal
    timestamps()
  end

  def changeset(transaction, params) do
    transaction
    |> cast(params, [:user_id, :amount])
    |> assoc_constraint(:user)
    |> validate_required([:user_id, :amount])
  end
end

You also need to modify your User module:

defmodule MyApp.Account.User do
  ...
  alias MyApp.Account.Transaction

  schema "users" do
     ...
    has_many :transactions, Transaction
  end
end

And in the Account module (I guess it’s a context?), you can write this:

defmodule MyApp.Account do
  ...
  alias MyApp.Account.Transaction

  @spec deposit(MyApp.Account.User.t(), number()) :: 
        {:ok, MyApp.Account.Transaction.t()} |
        {:error, Ecto.Changeset.t()}
  def deposit(user, amount) do
    user
    |> Ecto.build_assoc(:transactions)
    |> Transaction.changeset(%{amount: amount})
    |> Repo.insert()
  end
end
2 Likes

Hi there, many thanks for your reply. I am trying to migrate after making the proposed changes but I am getting an error for the following file:

defmodule SpBank.Account.User do
  use Ecto.Schema
  import Ecto.Changeset
  alias MyApp.Account.Transaction
  schema "users" do
    field :amount, :float
    field :name, :string
    has_many: transactions , Transaction 
    timestamps()
  end

  @doc false
  def changeset(user, attrs) do
    user
    |> cast(attrs, [:name, :amount])
    |> validate_required([:name, :amount])
  end
end
== Compilation error in file lib/sp_bank/account/user.ex ==
** (SyntaxError) lib/sp_bank/account/user.ex:8:30: syntax error before: 'Transaction'
    (elixir 1.11.2) lib/kernel/parallel_compiler.ex:314: anonymous fn/4 in Kernel.ParallelCompiler.spawn_workers/7
Marcs-MBP:sp_bank marcwatts$

It looks correct according to other sources - what would be causing this syntax error?

has_many: transactions , Transaction
you need has_many :transactions, Transaction.

1 Like

Ok so now I have tackled all the errors, I have not been successful in actually creating the route it seems.
sp_bank_web/controllers/user_controller:

defmodule SpBankWeb.UserController do
  use SpBankWeb, :controller

  alias SpBank.Account
  alias SpBank.Account.User

  action_fallback SpBankWeb.FallbackController

  def index(conn, _params) do
    users = Account.list_users()
    render(conn, "index.json", users: users)
  end

  def create(conn, %{"user" => user_params}) do
    with {:ok, %User{} = user} <- Account.create_user(user_params) do
      conn
      |> put_status(:created)
      |> put_resp_header("location", Routes.user_path(conn, :show, user))
      |> render("show.json", user: user)
    end
  end

  def deposit(conn, %{"id" => id, "amount" => amount}) do
    user = Account.get_user!(id)
    with {:ok, %User{} = user} <- Account.deposit(user, amount) do
      render(conn, "show.json", user: user)
    end
  end

sp_bank/account.ex:


defmodule SpBank.Account do
  @moduledoc """
  The Account context.
  """

  import Ecto.Query, warn: false
  alias SpBank.Repo

  alias SpBank.Account.User
  alias SpBank.Account.Transaction


  @spec deposit(SpBank.Account.User.t(), number()) :: 
  {:ok, SpBank.Account.Transaction.t()} |
  {:error, Ecto.Changeset.t()}
  def deposit(user, amount) do
  user
  |> Ecto.build_assoc(:transactions)
  |> Transaction.changeset(%{amount: amount})
  |> Repo.insert()
  end

sp_bank/account/transaction.ex:

defmodule SpBank.Account.Transaction do
  use Ecto.Schema
  import Ecto.Changeset
  alias SpBank.Account.User

  schema "transactions" do
    belongs_to :user, User
    field :amount, :decimal

    timestamps()
  end

  @doc false
  def changeset(transaction, attrs) do
    transaction
    |> cast(attrs, [:id, :amount])
    |> assoc_constraint(:user)
    |> validate_required([:id, :amount])
  end
end

router.ex:


defmodule SpBankWeb.Router do
  use SpBankWeb, :router

  pipeline :api do
    plug :accepts, ["json"]
  end

  scope "/api", SpBankWeb do
    pipe_through :api
    resources "/users", UserController, except: [:new, :edit]
  
  end

Why are all the other routes showing (‘show’, ‘create’ etc) but not the deposit route?

This was solved by adding the post route to the /api part of the router:


scope "/api", SpBankWeb do
    pipe_through :api
    resources "/users", UserController, except: [:new, :edit]
    post "/deposit", UserController, :deposit
  end

However, when I actually try to run the deposit function in my api, it seems to be giving it parameters in the form of an ‘amount’ rather than ‘user’, which is odd considering the api request (see below):


##### Phoenix.ActionClauseError <small>at POST</small> <small>/api/deposit</small>

# no function clause matching in SpBankWeb.UserController.deposit/2

The following arguments were given to SpBankWeb.UserController.deposit/2:

# 2 %{"user" => %{"amount" => "90", "name" => "test2"}}



* lib/sp_bank_web/controllers/user_controller.ex:23SpBankWeb.UserController.deposit/2
* lib/sp_bank_web/controllers/user_controller.ex:1SpBankWeb.UserController.action/2
* lib/sp_bank_web/controllers/user_controller.ex:1SpBankWeb.UserController.phoenix_controller_pipeline/2
* phoenix lib/phoenix/router.ex:352Phoenix.Router.__call__/2
* lib/sp_bank_web/endpoint.ex:1SpBankWeb.Endpoint.plug_builder_call/2
* lib/plug/debugger.ex:132SpBankWeb.Endpoint."call (overridable 3)"/2
* lib/sp_bank_web/endpoint.ex:1SpBankWeb.Endpoint.call/2
* phoenix lib/phoenix/endpoint/cowboy2_handler.ex:65Phoenix.Endpoint.Cowboy2Handler.init/4
* cowboy /Users/marcwatts/VSProjects/saltpay-back-end-elixir-engineer-ybyguv/sp_bank/deps/cowboy/src/cowboy_handler.erl:37:cowboy_handler.execute/2
* cowboy /Users/marcwatts/VSProjects/saltpay-back-end-elixir-engineer-ybyguv/sp_bank/deps/cowboy/src/cowboy_stream_h.erl:300:cowboy_stream_h.execute/3
* cowboy /Users/marcwatts/VSProjects/saltpay-back-end-elixir-engineer-ybyguv/sp_bank/deps/cowboy/src/cowboy_stream_h.erl:291:cowboy_stream_h.request_process/3
* stdlib proc_lib.erl:226:proc_lib.init_p_do_apply/3

Params

user

%{"amount" => "90", "name" => "test2"}

API request:
http://localhost:4000/api/deposit
request body:
{"user":{"name":"test2","amount":"90"}}

This has been very fiddly so far, hoping that my naivety with phoenix will serve as an error to error step by step guide to someone facing similar issues!

My method above of adding post "/deposit", UserController, :deposit to the router.ex has left me slightly more confused.

Why does the method not show up alongside the other UserController methods from this line resources "/users", UserController, except: [:new, :edit] as the deposit method is inside the UserController too?

WHat is the difference between this resources line and my POST line for the deposit method on its own? Either way, it’s still not working when I try to use it in POSTman:

http://localhost:4000/api/depositno function clause matching in SpBankWeb.UserController.deposit/2
The following arguments were given to SpBankWeb.UserController.deposit/2:


    # 1    # 2
    %{"user" => %{"amount" => "100.0", "id" => 2}}
[UserController deposit method:]
  def deposit(conn, %{"id" => id, "amount" => amount}) do
    user = Account.get_user!(id)
    with {:ok, %User{} = user} <- Account.deposit(user, amount) do
      render(conn, "show.json", user: user)
    end
  end

Routes:


          user_path  GET     /api/users              SpBankWeb.UserController :index
          user_path  GET     /api/users/:id          SpBankWeb.UserController :show
          user_path  POST    /api/users              SpBankWeb.UserController :create
          user_path  PATCH   /api/users/:id          SpBankWeb.UserController :update
                     PUT     /api/users/:id          SpBankWeb.UserController :update
          user_path  DELETE  /api/users/:id          SpBankWeb.UserController :delete
          user_path  GET     /api/transactions       SpBankWeb.UserController :index
          user_path  GET     /api/transactions/:id   SpBankWeb.UserController :show
          user_path  POST    /api/transactions       SpBankWeb.UserController :create
          user_path  PATCH   /api/transactions/:id   SpBankWeb.UserController :update
                     PUT     /api/transactions/:id   SpBankWeb.UserController :update
          user_path  DELETE  /api/transactions/:id   SpBankWeb.UserController :delete
          user_path  POST    /api/deposit            SpBankWeb.UserController :deposit

Any help here would be much appreciated!