improvement: wrap up initial implementaiton, add guides

This commit is contained in:
Zach Daniel 2023-08-18 23:49:09 -04:00
parent 8c7cf55008
commit 54608d5c76
11 changed files with 210 additions and 121 deletions

View file

@ -1,11 +1,13 @@
# AshDoubleEntry # AshDoubleEntry
**TODO: Add description** An extensible double entry system built using [Ash](ash-hq.org) resources.
See the [getting-started-guide](github.com/ash-project/ash_double_entry.git) to
setup the project!
## Installation ## Installation
If [available in Hex](https://hex.pm/docs/publish), the package can be installed The package can be installed by adding `ash_double_entry` to your list of dependencies in `mix.exs`:
by adding `ash_double_entry` to your list of dependencies in `mix.exs`:
```elixir ```elixir
def deps do def deps do
@ -14,8 +16,3 @@ def deps do
] ]
end end
``` ```
Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
be found at <https://hexdocs.pm/ash_double_entry>.

View file

@ -0,0 +1,155 @@
# Getting Started with Ash Double Entry
Ash Double Entry is implemented as a set of Ash resource extensions. You build the resources yourself, and the extensions add the attributes, relationships, actions and validations required for them to constitute a double entry system.
## What makes it special?
1. Account balances are updated automatically as transfers are introduced.
2. Arbitrary custom validations and behavior by virtue of modifying your own resources.
3. Transactions can be entered in the past, and all future balances are updated (and therefore validated).
## Setup
### Define your account resource
#### Example
```elixir
defmodule YourApp.Account do
use Ash.Resource,
data_layer: AshPostgres.DataLayer,
extensions: [AshDoubleEntry.Account]
postgres do
table "accounts"
repo YourApp.Repo
end
account do
# configure the other resources it will interact with
transfer_resource YourApp.Transfer
balance_resource YourApp.Balance
# accept custom attributes in the autogenerated `open` create action
open_action_accept [:account_number]
end
attributes do
# Add custom attributes
attribute :account_number, :string do
allow_nil? false
end
end
end
```
#### What does this extension do?
- Adds the following attributes:
- `:id`, a `:uuid` primary key
- `:currency`, a `:string` representing the currency of the transfer
- `:inserted_at`, a `:utc_datetime_usec` timestamp
- `:identifier`, a `:string` and a unique identifier for the account
- Adds the following actions:
- A primary read called `:read`, unless a primary read action already exists.
- A create action called `open`, that accepts `identifier`, `currency`, and the attributes in `open_action_accept`
- A read action called `:lock_accounts` that can be used to lock a list of accounts while in a transaction(for data layers that support it)
- Adds a `has_many` relationship called `balances`, referring to all related balances of an account
- Adds an aggregate called `balance`, referring to the latest balance as a `decimal` for that account
- Adds the following calculations:
- A `balance_as_of_ulid` calculation that takes an argument called `ulid`, which corresponds to a transfer id and returns the balance as a
decimal.
- A `balance_as_of` calculation that takes a `utc_datetime_usec` and returns the balance as of that datetime.
- Adds an identity called `unique_identifier` that ensures `identifier` is unique.
### Define your transfer resource
#### Example
```elixir
defmodule YourApp.Transfer do
use Ash.Resource,
data_layer: AshPostgres.DataLayer,
extensions: [AshDoubleEntry.Transfer]
postgres do
table "transfers"
repo YourApp.Repo
end
transfer do
# configure the other resources it will interact with
account_resource YourApp.Account
balance_resource YourApp.Balance
end
end
```
#### What does this extension do?
- Adds the following attributes
- `:id`, a `AshDoubleEntry.ULID` primary key which is sortable based on the `timestamp` of the transfer.
- `:amount`, a `:decimal` representing the amount of the transfer
- `:timestamp`, a `:utc_datetime_usec` representing when the transfer occurred
- `:inserted_at`, a `:utc_datetime_usec` timestamp
- Adds the following relationships
- `:from_account`, a `belongs_to` relationship of the account the transfer is from
- `:to_account`, a `belongs_to` relationship of the account the transfer is to
- Adds a `:read` action called `:read_transfers` with keyset pagination enabled. Required for streaming transfers, used for validating balances.
- Adds a change that runs on all create and update actions that reifies the balances table. It inserts a balance for the transfer, and updates any affected future balances.
### Define your balance resource
#### Example
```elixir
defmodule YourApp.Balance do
use Ash.Resource,
data_layer: AshPostgres.DataLayer,
extensions: [AshDoubleEntry.Balance]
postgres do
table "balances"
repo YourApp.Repo
end
balance do
# configure the other resources it will interact with
transfer_resource Transfer
account_resource Account
end
changes do
# add custom behavior. In this case, we're preventing certain balances from being less than zero
change after_action(&validate_balance/2)
end
defp validate_balance(changeset, result) do
account = result |> changeset.api.load!(:account) |> Map.get(:account)
if account.allow_zero_balance == false && Decimal.negative?(result.balance) do
{:error,
Ash.Error.Changes.InvalidAttribute.exception(
value: result.balance,
field: :balance,
message: "balance cannot be negative"
)}
else
{:ok, result}
end
end
end
```
#### What does this extension do?
- Adds the following attributes:
- `:id`, a `:uuid` primary key
- `:balance`, the balance as a decimal of the account at the time of the related transfer
- Adds the following relationships:
- `:transfer` a `:belongs_to` relationship, pointing to the transfer that this balance is as of.
- `:account` a `:belongs_to` relationship, pointing to the account the balance is for
- Adds the following actions:
- a primary read action called `:read`, if a priamry read action doesn't
exist
- a create action caleld `:upsert_balance`, which will create or update the relevant balance, by `transfer_id` and `account_id`
- Adds an identity that ensures that `account_id` and `transfer_id` are unique

View file

@ -8,29 +8,19 @@ defmodule AshDoubleEntry.Account.Transformers.AddStructure do
def transform(dsl) do def transform(dsl) do
dsl dsl
|> add_primary_read_action() |> add_primary_read_action()
|> add_balance_as_of_ulid_calculation() |> Ash.Resource.Builder.add_attribute(:id, :uuid,
|> add_balance_as_of_calculation() primary_key?: true,
|> Ash.Resource.Builder.add_attribute(:identifier, :string, allow_nil?: false) writable?: false,
|> Ash.Resource.Builder.add_aggregate(:balance, :first, [:balances], generated?: true,
field: :balance, allow_nil?: false,
default: Decimal.new(0), default: &Ash.UUID.generate/0
sort: [transfer_id: :desc]
) )
|> Ash.Resource.Builder.add_attribute(:identifier, :string, allow_nil?: false)
|> Ash.Resource.Builder.add_attribute( |> Ash.Resource.Builder.add_attribute(
:currency, :currency,
:string, :string,
allow_nil?: false allow_nil?: false
) )
|> Ash.Resource.Builder.add_attribute(
:must_be_positive,
:boolean,
allow_nil?: false,
default: true
)
|> Ash.Resource.Builder.add_attribute(:inserted_at, :utc_datetime_usec,
allow_nil?: false,
default: &DateTime.utc_now/0
)
|> Ash.Resource.Builder.add_action(:create, :open, |> Ash.Resource.Builder.add_action(:create, :open,
accept: accept:
Enum.uniq( Enum.uniq(
@ -44,15 +34,25 @@ defmodule AshDoubleEntry.Account.Transformers.AddStructure do
) )
] ]
) )
|> Ash.Resource.Builder.add_identity(:unique_identifier, [:identifier], |> Ash.Resource.Builder.add_attribute(:inserted_at, :utc_datetime_usec,
pre_check_with: pre_check_with(dsl) allow_nil?: false,
default: &DateTime.utc_now/0
)
|> Ash.Resource.Builder.add_aggregate(:balance, :first, [:balances],
field: :balance,
default: Decimal.new(0),
sort: [transfer_id: :desc]
) )
|> Ash.Resource.Builder.add_relationship( |> Ash.Resource.Builder.add_relationship(
:has_many, :has_many,
:balances, :balances,
AshDoubleEntry.Account.Info.account_balance_resource!(dsl), AshDoubleEntry.Account.Info.account_balance_resource!(dsl),
destination_attribute: :account_id, destination_attribute: :account_id
source_attribute: :id )
|> add_balance_as_of_ulid_calculation()
|> add_balance_as_of_calculation()
|> Ash.Resource.Builder.add_identity(:unique_identifier, [:identifier],
pre_check_with: pre_check_with(dsl)
) )
end end
@ -92,19 +92,6 @@ defmodule AshDoubleEntry.Account.Transformers.AddStructure do
) )
end end
defbuilder add_balance_aggregate(dsl) do
Ash.Resource.Builder.add_aggregate(
dsl,
:balance,
:first,
[:balances],
field: :balance,
query: [
sort: [ulid: :desc]
]
)
end
defbuilder add_primary_read_action(dsl) do defbuilder add_primary_read_action(dsl) do
if Ash.Resource.Info.primary_action(dsl, :read) do if Ash.Resource.Info.primary_action(dsl, :read) do
{:ok, dsl} {:ok, dsl}

View file

@ -1,2 +1,3 @@
defmodule AshDoubleEntry do defmodule AshDoubleEntry do
@moduledoc false
end end

View file

@ -7,7 +7,6 @@ defmodule AshDoubleEntry.Balance.Transformers.AddStructure do
def transform(dsl) do def transform(dsl) do
dsl dsl
|> add_primary_read_action()
|> Ash.Resource.Builder.add_attribute(:id, :uuid, |> Ash.Resource.Builder.add_attribute(:id, :uuid,
primary_key?: true, primary_key?: true,
writable?: false, writable?: false,
@ -15,6 +14,7 @@ defmodule AshDoubleEntry.Balance.Transformers.AddStructure do
allow_nil?: false, allow_nil?: false,
default: &Ash.UUID.generate/0 default: &Ash.UUID.generate/0
) )
|> Ash.Resource.Builder.add_attribute(:balance, :decimal, allow_nil?: false)
|> Ash.Resource.Builder.add_relationship( |> Ash.Resource.Builder.add_relationship(
:belongs_to, :belongs_to,
:transfer, :transfer,
@ -30,15 +30,15 @@ defmodule AshDoubleEntry.Balance.Transformers.AddStructure do
allow_nil?: false, allow_nil?: false,
attribute_writable?: true attribute_writable?: true
) )
|> Ash.Resource.Builder.add_attribute(:balance, :decimal, allow_nil?: false) |> add_primary_read_action()
|> Ash.Resource.Builder.add_identity(:unique_references, [:account_id, :transfer_id],
pre_check_with: pre_check_with(dsl)
)
|> Ash.Resource.Builder.add_action(:create, :upsert_balance, |> Ash.Resource.Builder.add_action(:create, :upsert_balance,
accept: [:balance, :account_id, :transfer_id], accept: [:balance, :account_id, :transfer_id],
upsert?: true, upsert?: true,
upsert_identity: :unique_references upsert_identity: :unique_references
) )
|> Ash.Resource.Builder.add_identity(:unique_references, [:account_id, :transfer_id],
pre_check_with: pre_check_with(dsl)
)
end end
defbuilder add_primary_read_action(dsl) do defbuilder add_primary_read_action(dsl) do

View file

@ -1,34 +0,0 @@
defmodule AshDoubleEntry.Transfer.Changes.SetAmounts do
use Ash.Resource.Change
def change(changeset, _, _) do
case Ash.Changeset.fetch_argument(changeset, :amount) do
{:ok, amount} when not is_nil(amount) ->
changeset
|> Ash.Changeset.force_change_new_attribute(:to_amount, amount)
|> Ash.Changeset.force_change_new_attribute(:from_amount, amount)
_ ->
cond do
is_nil(Ash.Changeset.get_attribute(changeset, :to_amount)) ->
{:error,
Ash.Error.Changes.Required.exception(
field: :to_amount,
type: :attribute,
resource: changeset.resource
)}
is_nil(Ash.Changeset.get_attribute(changeset, :from_amount)) ->
{:error,
Ash.Error.Changes.Required.exception(
field: :from_amount,
type: :attribute,
resource: changeset.resource
)}
true ->
changeset
end
end
end
end

View file

@ -13,8 +13,7 @@ defmodule AshDoubleEntry.Transfer.Changes.VerifyTransfer do
|> Ash.Changeset.before_action(fn changeset -> |> Ash.Changeset.before_action(fn changeset ->
from_account_id = Ash.Changeset.get_attribute(changeset, :from_account_id) from_account_id = Ash.Changeset.get_attribute(changeset, :from_account_id)
to_account_id = Ash.Changeset.get_attribute(changeset, :to_account_id) to_account_id = Ash.Changeset.get_attribute(changeset, :to_account_id)
from_amount = Ash.Changeset.get_attribute(changeset, :from_amount) amount = Ash.Changeset.get_attribute(changeset, :amount)
to_amount = Ash.Changeset.get_attribute(changeset, :to_amount)
timestamp = Ash.Changeset.get_attribute(changeset, :timestamp) timestamp = Ash.Changeset.get_attribute(changeset, :timestamp)
timestamp = timestamp =
@ -26,27 +25,21 @@ defmodule AshDoubleEntry.Transfer.Changes.VerifyTransfer do
ulid = AshDoubleEntry.ULID.generate(timestamp) ulid = AshDoubleEntry.ULID.generate(timestamp)
accounts = accounts =
try do
changeset.resource changeset.resource
|> AshDoubleEntry.Transfer.Info.transfer_account_resource!() |> AshDoubleEntry.Transfer.Info.transfer_account_resource!()
|> Ash.Query.filter(id in ^[from_account_id, to_account_id]) |> Ash.Query.filter(id in ^[from_account_id, to_account_id])
|> Ash.Query.for_read(:lock_accounts) |> Ash.Query.for_read(:lock_accounts)
|> Ash.Query.load(balance_as_of_ulid: %{ulid: ulid}) |> Ash.Query.load(balance_as_of_ulid: %{ulid: ulid})
|> changeset.api.read!(authorize?: false, tracer: context[:tracer]) |> changeset.api.read!(authorize?: false, tracer: context[:tracer])
rescue
e ->
IO.puts(Exception.format(:error, e, __STACKTRACE__))
[]
end
from_account = Enum.find(accounts, &(&1.id == from_account_id)) from_account = Enum.find(accounts, &(&1.id == from_account_id))
to_account = Enum.find(accounts, &(&1.id == to_account_id)) to_account = Enum.find(accounts, &(&1.id == to_account_id))
new_from_account_balance = new_from_account_balance =
Decimal.sub(from_account.balance_as_of_ulid, from_amount) Decimal.sub(from_account.balance_as_of_ulid, amount)
new_to_account_balance = new_to_account_balance =
Decimal.add(to_account.balance_as_of_ulid, to_amount) Decimal.add(to_account.balance_as_of_ulid, amount)
changeset.resource changeset.resource
|> AshDoubleEntry.Transfer.Info.transfer_balance_resource!() |> AshDoubleEntry.Transfer.Info.transfer_balance_resource!()
@ -80,8 +73,7 @@ defmodule AshDoubleEntry.Transfer.Changes.VerifyTransfer do
|> Ash.Changeset.set_context(%{ |> Ash.Changeset.set_context(%{
from_account: from_account, from_account: from_account,
to_account: to_account, to_account: to_account,
from_amount: from_amount, amount: amount
to_amount: to_amount
}) })
end) end)
|> Ash.Changeset.after_action(fn changeset, result -> |> Ash.Changeset.after_action(fn changeset, result ->
@ -98,17 +90,17 @@ defmodule AshDoubleEntry.Transfer.Changes.VerifyTransfer do
%{ %{
account_id: balance.account_id, account_id: balance.account_id,
transfer_id: balance.transfer_id, transfer_id: balance.transfer_id,
balance: Decimal.sub(balance.balance, changeset.context.from_amount) balance: Decimal.sub(balance.balance, changeset.context.amount)
} }
else else
%{ %{
account_id: balance.account_id, account_id: balance.account_id,
transfer_id: balance.transfer_id, transfer_id: balance.transfer_id,
balance: Decimal.add(balance.balance, changeset.context.to_amount) balance: Decimal.add(balance.balance, changeset.context.amount)
} }
end end
end) end)
|> changeset.api.bulk_create( |> changeset.api.bulk_create!(
AshDoubleEntry.Transfer.Info.transfer_balance_resource!(changeset.resource), AshDoubleEntry.Transfer.Info.transfer_balance_resource!(changeset.resource),
:upsert_balance, :upsert_balance,
context_to_opts(context, context_to_opts(context,
@ -119,7 +111,6 @@ defmodule AshDoubleEntry.Transfer.Changes.VerifyTransfer do
) )
{:ok, result} {:ok, result}
# do verification logic
end) end)
end end

View file

@ -2,6 +2,8 @@ defmodule AshDoubleEntry.Transfer.Transformers.AddStructure do
use Spark.Dsl.Transformer use Spark.Dsl.Transformer
def before?(Ash.Resource.Transformers.CachePrimaryKey), do: true def before?(Ash.Resource.Transformers.CachePrimaryKey), do: true
def before?(Ash.Resource.Transformers.BelongsToSourceField), do: true
def before?(Ash.Resource.Transformers.BelongsToAttribute), do: true
def before?(_), do: false def before?(_), do: false
def transform(dsl) do def transform(dsl) do
@ -12,8 +14,7 @@ defmodule AshDoubleEntry.Transfer.Transformers.AddStructure do
default: &AshDoubleEntry.ULID.generate/0, default: &AshDoubleEntry.ULID.generate/0,
generated?: false generated?: false
) )
|> Ash.Resource.Builder.add_attribute(:from_amount, :decimal, allow_nil?: false) |> Ash.Resource.Builder.add_attribute(:amount, :decimal, allow_nil?: false)
|> Ash.Resource.Builder.add_attribute(:to_amount, :decimal, allow_nil?: false)
|> Ash.Resource.Builder.add_attribute(:timestamp, :utc_datetime_usec, |> Ash.Resource.Builder.add_attribute(:timestamp, :utc_datetime_usec,
allow_nil?: false, allow_nil?: false,
default: &DateTime.utc_now/0 default: &DateTime.utc_now/0
@ -34,19 +35,12 @@ defmodule AshDoubleEntry.Transfer.Transformers.AddStructure do
AshDoubleEntry.Transfer.Info.transfer_account_resource!(dsl), AshDoubleEntry.Transfer.Info.transfer_account_resource!(dsl),
attribute_writable?: true attribute_writable?: true
) )
|> Ash.Resource.Builder.add_change({AshDoubleEntry.Transfer.Changes.VerifyTransfer, []})
|> Ash.Resource.Builder.add_action(:create, :transfer, |> Ash.Resource.Builder.add_action(:create, :transfer,
accept: [:to_amount, :from_amount, :timestamp, :from_account_id, :to_account_id], accept: [:amount, :timestamp, :from_account_id, :to_account_id]
allow_nil_input: [:to_amount, :from_amount],
arguments: [
Ash.Resource.Builder.build_action_argument(:amount, :decimal)
],
changes: [
Ash.Resource.Builder.build_action_change({AshDoubleEntry.Transfer.Changes.SetAmounts, []})
]
) )
|> Ash.Resource.Builder.add_action(:read, :read_transfers, |> Ash.Resource.Builder.add_action(:read, :read_transfers,
pagination: Ash.Resource.Builder.build_pagination(keyset?: true) pagination: Ash.Resource.Builder.build_pagination(keyset?: true)
) )
|> Ash.Resource.Builder.add_change({AshDoubleEntry.Transfer.Changes.VerifyTransfer, []})
end end
end end

View file

@ -1,10 +1,12 @@
defmodule AshDoubleEntry.MixProject do defmodule AshDoubleEntry.MixProject do
use Mix.Project use Mix.Project
@version "0.1.0"
def project do def project do
[ [
app: :ash_double_entry, app: :ash_double_entry,
version: "0.1.0", version: @version,
elixir: "~> 1.15", elixir: "~> 1.15",
start_permanent: Mix.env() == :prod, start_permanent: Mix.env() == :prod,
consolidate_protocols: Mix.env() != :test, consolidate_protocols: Mix.env() != :test,
@ -33,9 +35,7 @@ defmodule AshDoubleEntry.MixProject do
# Run "mix help deps" to learn about dependencies. # Run "mix help deps" to learn about dependencies.
defp deps do defp deps do
[ [
{:ash, github: "ash-project/ash"} {:ash, "~> 2.14"}
# {:dep_from_hexpm, "~> 0.3.0"},
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
] ]
end end
end end

View file

@ -1,5 +1,5 @@
%{ %{
"ash": {:git, "https://github.com/ash-project/ash.git", "6daae630f446441a314af162dae23154502e9fb8", []}, "ash": {:hex, :ash, "2.14.2", "111829b1db52e43c28c0660b2da8f1bfc76199dd8e15289f86f5998c07fb8d36", [:mix], [{:comparable, "~> 1.0", [hex: :comparable, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:earmark, "~> 1.4", [hex: :earmark, repo: "hexpm", optional: true]}, {:ecto, "~> 3.7", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8.0", [hex: :ets, repo: "hexpm", optional: false]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: false]}, {:plug, ">= 0.0.0", [hex: :plug, repo: "hexpm", optional: true]}, {:spark, ">= 1.1.20 and < 2.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}, {:stream_data, "~> 0.5.0", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e6e5924ccb3f8b0ab5de15b22006a4a18c0f002aadadd7aa9dfa39d53cff0b9a"},
"comparable": {:hex, :comparable, "1.0.0", "bb669e91cedd14ae9937053e5bcbc3c52bb2f22422611f43b6e38367d94a495f", [:mix], [{:typable, "~> 0.1", [hex: :typable, repo: "hexpm", optional: false]}], "hexpm", "277c11eeb1cd726e7cd41c6c199e7e52fa16ee6830b45ad4cdc62e51f62eb60c"}, "comparable": {:hex, :comparable, "1.0.0", "bb669e91cedd14ae9937053e5bcbc3c52bb2f22422611f43b6e38367d94a495f", [:mix], [{:typable, "~> 0.1", [hex: :typable, repo: "hexpm", optional: false]}], "hexpm", "277c11eeb1cd726e7cd41c6c199e7e52fa16ee6830b45ad4cdc62e51f62eb60c"},
"decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"},
"ecto": {:hex, :ecto, "3.10.3", "eb2ae2eecd210b4eb8bece1217b297ad4ff824b4384c0e3fdd28aaf96edd6135", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "44bec74e2364d491d70f7e42cd0d690922659d329f6465e89feb8a34e8cd3433"}, "ecto": {:hex, :ecto, "3.10.3", "eb2ae2eecd210b4eb8bece1217b297ad4ff824b4384c0e3fdd28aaf96edd6135", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "44bec74e2364d491d70f7e42cd0d690922659d329f6465e89feb8a34e8cd3433"},

View file

@ -15,8 +15,6 @@ defmodule AshDoubleEntryTest do
end end
attributes do attributes do
uuid_primary_key :id
attribute :allow_zero_balance, :boolean do attribute :allow_zero_balance, :boolean do
default true default true
end end