First of all, I am a total noob with Elixir. So this may be an obvious error on my part.
I am using SQLite as the database for a test project so that I can learn Elixir. I have an accounts table, and have created a test case to insert data to it. Here is my code:
defmodule Corebank.Accounts.Account do
@moduledoc """
Represents an account in the banking system.
This module defines the Ecto schema and related functions for account management.
"""
use Ecto.Schema
import Ecto.Changeset
@primary_key {:id, :binary_id, autogenerate: true}
schema "accounts" do
field :name, :string
field :account_type, :string
field :account_number, :string
field :currency, :string
field :balance, :decimal, default: 0.0
timestamps()
end
def changeset(account, attrs) do
account
|> cast(attrs, [:name, :account_number, :currency, :balance, :account_type])
|> validate_required([:name, :account_number, :currency, :account_type])
|> validate_length(:account_number, min: 6, max: 12)
|> validate_number(:balance, greater_than_or_equal_to: 0)
|> unique_constraint(:account_number)
end
This is my migration that I used to create the table:
defmodule Corebank.Repo.Migrations.CreateAccountsTable do
use Ecto.Migration
def change do
create table(:accounts) do
add :name, :string, null: false
add :account_type, :string, null: false
add :account_number, :string, null: false
add :currency, :string, null: false
add :balance, :decimal, null: false, default: 0.0
#timestamps() # Adds `inserted_at` and `updated_at` fields
end
create unique_index(:accounts, [:account_number]) # Ensures unique account numbers
end
end
This is my AccountManager module that creates a GenServer to do the account management:
defmodule Corebank.Accounts.AccountManageange do
use GenServer
alias Corebank.Accounts.Account
alias Corebank.Repo
## Client API
def start_link(_) do
GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
end
def create_account(attrs) do
IO.inspect(attrs)
GenServer.call(__MODULE__, {:create_account, attrs})
end
def get_balance(account_id) do
GenServer.call(__MODULE__, {:get_balance, account_id})
end
def deposit(account_id, amount) do
GenServer.call(__MODULE__, {:deposit, account_id, amount})
end
def withdraw(account_id, amount) do
GenServer.call(__MODULE__, {:withdraw, account_id, amount})
end
def transfer(from_account_id, to_account_id, amount) do
GenServer.call(__MODULE__, {:transfer, from_account_id, to_account_id, amount})
end
## Server Callbacks
def init(state) do
{:ok, state}
end
def handle_call({:create_account, attrs}, _from, state) do
changeset = Account.changeset(%Account{}, attrs)
IO.inspect(changeset)
case Repo.insert(changeset) do
{:ok, account} ->
{:reply, {:ok, account}, state}
{:error, changeset} ->
IO.warn("Account insert error")
{:reply, {:error, changeset}, state}
end
end
def handle_call({:get_balance, account_id}, _from, state) do
case Repo.get(Account, account_id) do
nil ->
{:reply, {:error, :account_not_found}, state}
account ->
{:reply, {:ok, account.balance}, state}
end
end
def handle_call({:deposit, account_id, amount}, _from, state) do
case Repo.get(Account, account_id) do
nil ->
{:reply, {:error, :account_not_found}, state}
account ->
new_balance = Decimal.add(account.balance, Decimal.new(amount))
changeset = Ecto.Changeset.change(account, balance: new_balance)
case Repo.update(changeset) do
{:ok, updated_account} ->
send_notification(:deposit, updated_account, amount)
{:reply, {:ok, updated_account}, state}
{:error, _} ->
{:reply, {:error, :update_failed}, state}
end
end
end
def handle_call({:withdraw, account_id, amount}, _from, state) do
case Repo.get(Account, account_id) do
nil ->
{:reply, {:error, :account_not_found}, state}
account when account.balance < amount ->
{:reply, {:error, :insufficient_funds}, state}
account ->
new_balance = Decimal.sub(account.balance, Decimal.new(amount))
changeset = Ecto.Changeset.change(account, balance: new_balance)
case Repo.update(changeset) do
{:ok, updated_account} ->
send_notification(:withdrawal, updated_account, amount)
{:reply, {:ok, updated_account}, state}
{:error, _} ->
{:reply, {:error, :update_failed}, state}
end
end
end
def handle_call({:transfer, from_account_id, to_account_id, amount}, _from, state) do
Repo.transaction(fn ->
case withdraw(from_account_id, amount) do
{:ok, _from_account} ->
case deposit(to_account_id, amount) do
{:ok, _to_account} ->
{:ok, :transfer_completed}
{:error, reason} ->
Repo.rollback(reason)
end
{:error, reason} ->
Repo.rollback(reason)
end
end)
|> case do
{:ok, :transfer_completed} ->
{:reply, {:ok, :transfer_successful}, state}
{:error, reason} ->
{:reply, {:error, reason}, state}
end
end
## Helper Functions
defp send_notification(:deposit, account, amount) do
# Emit deposit notification to PubSub or an external service.
IO.puts("Deposit of #{amount} to account #{account.id}")
end
defp send_notification(:withdrawal, account, amount) do
# Emit withdrawal notification to PubSub or an external service.
IO.puts("Withdrawal of #{amount} from account #{account.id}")
end
end
and finally my test:
defmodule Corebank.Accounts.AccountManagerTest do
use Corebank.DataCase
alias Corebank.Accounts.AccountManager
test "creates an account and retrieves its balance" do
attrs = %{
name: "Test Savings",
account_type: "SAVINGS",
account_number: "1234567",
currency: "USD",
balance: Decimal.new("1000.0"),
}
{:ok, _pid} = AccountManager.start_link(nil)
result = AccountManager.create_account(attrs)
IO.inspect(result)
case result do
{:ok, account} ->
IO.inspect(result)
{:ok, balance} = AccountManager.get_balance(account.id)
assert balance == Decimal.new("1000.00")
{:error, changeset} ->
IO.puts("Create account error")
IO.inspect(changeset)
flunk "Failed to create account: #{inspect(changeset)}"
end
end
end
The issue I am having is that when the test code attempts to insert into the accounts table, an exception is raised:
Running ExUnit with seed: 191072, max_cases: 16
....%{
name: "Test Savings",
balance: Decimal.new("1000.0"),
currency: "USD",
account_type: "SAVINGS",
account_number: "1234567"
}
#Ecto.Changeset<
action: nil,
changes: %{
name: "Test Savings",
balance: Decimal.new("1000.0"),
currency: "USD",
account_type: "SAVINGS",
account_number: "1234567"
},
errors: [],
data: #Corebank.Accounts.Account<>,
valid?: true,
...
>
16:27:33.354 [error] GenServer Corebank.Accounts.AccountManager terminating
** (Exqlite.Error) table accounts has no column named inserted_at
INSERT INTO "accounts" ("name","balance","currency","account_type","account_number","inserted_at","updated_at","id") VALUES (?1,?2,?3,?4,?5,?6,?7,?8)
(ecto_sql 3.12.1) lib/ecto/adapters/sql.ex:1096: Ecto.Adapters.SQL.raise_sql_call_error/1
(ecto 3.12.5) lib/ecto/repo/schema.ex:837: Ecto.Repo.Schema.apply/4
(ecto 3.12.5) lib/ecto/repo/schema.ex:416: anonymous fn/15 in Ecto.Repo.Schema.do_insert/4
(corebank 0.1.0) lib/corebank/Accounts/AccountManager.ex:44: Corebank.Accounts.AccountManager.handle_call/3
(stdlib 6.1.2) gen_server.erl:2381: :gen_server.try_handle_call/4
(stdlib 6.1.2) gen_server.erl:2410: :gen_server.handle_msg/6
(stdlib 6.1.2) proc_lib.erl:329: :proc_lib.init_p_do_apply/3
Last message (from #PID<0.346.0>): {:create_account, %{name: "Test Savings", balance: Decimal.new("1000.0"), currency: "USD", account_type: "SAVINGS", account_number: "1234567"}}
1) test creates an account and retrieves its balance (Corebank.Accounts.AccountManagerTest)
test/corebank/accounts/account_manager_test.exs:6
** (EXIT from #PID<0.346.0>) an exception was raised:
** (Exqlite.Error) table accounts has no column named inserted_at
INSERT INTO "accounts" ("name","balance","currency","account_type","account_number","inserted_at","updated_at","id") VALUES (?1,?2,?3,?4,?5,?6,?7,?8)
(ecto_sql 3.12.1) lib/ecto/adapters/sql.ex:1096: Ecto.Adapters.SQL.raise_sql_call_error/1
(ecto 3.12.5) lib/ecto/repo/schema.ex:837: Ecto.Repo.Schema.apply/4
(ecto 3.12.5) lib/ecto/repo/schema.ex:416: anonymous fn/15 in Ecto.Repo.Schema.do_insert/4
(corebank 0.1.0) lib/corebank/Accounts/AccountManager.ex:44: Corebank.Accounts.AccountManager.handle_call/3
(stdlib 6.1.2) gen_server.erl:2381: :gen_server.try_handle_call/4
(stdlib 6.1.2) gen_server.erl:2410: :gen_server.handle_msg/6
(stdlib 6.1.2) proc_lib.erl:329: :proc_lib.init_p_do_apply/3
.
Finished in 0.2 seconds (0.1s async, 0.1s sync)
6 tests, 1 failure
For the life of me, I canât figure out what the data type mismatch might be. I have been able to manually insert into the accounts table, using the exact same values, using the sqlite3 cli. But I canât see what the issue is with my code that would prevent the insert from succeeding.
Any help is much appreciated.
thanks