Why is my Deposit function not working when I POST to it?

I am trying to create a rudimentary online bank where customers can create accounts and make deposits into their accounts. This is the first time I’ve used Phoenix and Elixir and I’m having a lot of trouble with the deposit function. My thinking is that all a customer would need to deposit money is their account ID and the amount of money. I have therefore made a function for this in the user_controller which inputs the ID alongside the amount, however, I am getting the dreaded no function clause matching in SpBankWeb.UserController.deposit/2 error in postman when I try to post into it… perhaps due to how I am posting to it or how my logic is structured - or perhaps both of these things.

My POST request contains this as the body {"id" :{"amount":200.0}} and is correctly to the route http://localhost:4000/api/deposit (do I need to make any changes to this?). This results in the input, alongside the no function matching error,

# 2
%{"id" => %{"amount" => 200.0}} 
  1. This is the first thing that is confusing me, as my function takes in an ID and an amount, which is what is shown above - why is this not matching? The corresponding user function is below:

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
  1. Or, does the problem arise instead due to the router as below. Why, for instance, did I need to specify the route of POST here when for the other user_controller methods these simply came with the ‘resources’ of /users?

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]
    resources "/transactions", UserController, except: [:new, :edit]
    post "/deposit", UserController, :deposit
  end

Account.Transactions.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

Account.user.ex:

defmodule SpBank.Account.User do
  use Ecto.Schema
  import Ecto.Changeset
  alias SpBank.Account.Transaction
  schema "users" do
    # field :id 
    field :amount, :decimal
    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

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

your function is receiving two parameters a conn and a map…so its failing because in postman you are only sending one parameter

Hi thanks for your help! What would therefore be appropriate for sending it then? I haave tried many variations of the POST body including {"user" :{"id": 2,"amount":200.0}} which is two parameters, yet I still get an unmatching function error

It’s not the number of parameters that’s wrong it’s the shape of your map.

You are sending {"user" :{"id": 2,"amount":200.0}} which will be decoded into %{"user" => %{"id" => 2,"amount" => 200.0}} but you are expecting %{"id" => 2,"amount" => 200.0} so you should set the body to {"id": 2,"amount":200.0}.

Note that you could also define your route like so :

post "/deposit/:id", UserController, :deposit

And then post to http://localhost:4000/api/deposit/2 with the body {"amount":200.0}

Many thanks, I’ve realised I did try this alreaady and it brought back the error :

FunctionClauseError <small>at POST</small> <small>/api/deposit</small>

# no function clause matching in SpBankWeb.FallbackController.call/2

Show only app frames

Which I didn’t realise was different since it is looking for something in the FallBackController - I assume this means it is something wrong with the logic, any idea what it could be?

Indeed there’s an issue here

If Account.deposit/2 returns anything but {:ok, %User{} } the deposit function will return that thing (eg {:error, something}). In turn the fallback action will be called with the conn passed to the deposit function and that thing (eg {:error, something})

You could either change your action eg

def deposit(conn, %{"id" => id, "amount" => amount}) do
    user = Account.get_user!(id)
    case Account.deposit(user, amount) do
   {:ok, %User{} = user} ->
      render(conn, "show.json", user: user)
    _ -> send_resp(conn, 500, “Something went wrong”)
    end
  end

Or you could adapt your fallback action so that it can handle what Account.deposit returns.