AshDoubleEntry - How To `require_atomic? false` in the adjust_balance function

I am having the following warning from ash_double_entry extension. adjust_balance function is in the AshDoubleEntry extension. How do I set require_atomic to false?

`Kamaro.Ledger.Resources.Balance.adjust_balance` cannot be done atomically, because the changes `[Ash.Resource.Change.AfterAction]` cannot be done atomically

You must either address the issue or set `require_atomic? false` on `Kamaro.Ledger.Resources.Balance.adjust_balance`.

  lib/kamaro/ledger/resources/balance.ex:1

are you on the latest version of ash_double_entry? It defines an atomic callback and should be compatible with atomic updates.

I have the version recommended in the docs {:ash_double_entry, "~> 1.0.0-rc.0"}. I will install the latest version: 1.0.3 and try again.

Pushing up a fix to the docs.

1 Like

Even with the latest version I am still getting the same error.

warning: [Kamaro.Ledger.Resources.Balance]
actions -> adjust_balance:
  `Kamaro.Ledger.Resources.Balance.adjust_balance` cannot be done atomically, because the changes `[Ash.Resource.Change.AfterAction]` cannot be done atomically

You must either address the issue or set `require_atomic? false` on `Kamaro.Ledger.Resources.Balance.adjust_balance`.

  lib/kamaro/ledger/resources/balance.ex:1: Kamaro.Ledger.Resources.Balance.__verify_spark_dsl__/1

Compilation failed due to warnings while using the --warnings-as-errors option

:thinking: are you on the latest version of ash as well?

I’m using it atomically in a personal app of mine, so it should definitely be compatible…

Yes. I am on the latest version of ash. See my mix deps below. Unless if you are referring to ash main branch instead of 3.0.16

Have you added any global changes to your transaction resource?

I haven’t added any global changes to the transaction resource.
Here are the resources

defmodule Kamaro.Ledger.Resources.Transfer do
  use Ash.Resource,
    domain: Kamaro.Ledger,
    data_layer: AshPostgres.DataLayer,
    extensions: [AshDoubleEntry.Transfer]

  postgres do
    table "transfers"
    repo Kamaro.Repo
  end

  transfer do
    # configure the other resources it will interact with
    account_resource(Kamaro.Ledger.Resources.Account)
    balance_resource(Kamaro.Ledger.Resources.Balance)
  end
end

defmodule Kamaro.Ledger.Resources.Balance do
  use Ash.Resource,
    domain: Kamaro.Ledger,
    data_layer: AshPostgres.DataLayer,
    extensions: [AshDoubleEntry.Balance]

  postgres do
    table "balances"
    repo Kamaro.Repo
  end

  balance do
    # configure the other resources it will interact with
    transfer_resource(Kamaro.Ledger.Resources.Transfer)
    account_resource(Kamaro.Ledger.Resources.Account)
  end

  actions do
    read :read do
      primary? true
      # configure keyset pagination for streaming
      pagination keyset?: true, required?: false
    end
  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 |> Ash.load!(:account) |> Map.get(:account)

    if account.allow_zero_balance == false && Money.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

defmodule Kamaro.Ledger.Resources.Account do
  use Ash.Resource,
    domain: Kamaro.Ledger,
    data_layer: AshPostgres.DataLayer,
    extensions: [AshDoubleEntry.Account]

  postgres do
    table "accounts"
    repo Kamaro.Repo
  end

  account do
    # configure the other resources it will interact with
    transfer_resource(Kamaro.Ledger.Resources.Transfer)
    balance_resource(Kamaro.Ledger.Resources.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

  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 |> Ash.load!(:account) |> Map.get(:account)

    if account.allow_zero_balance == false && Money.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

that change does not have an atomic implementation, and therefore cannot be done atomically.

Here is an example from my app that does the same logic atomically:

defmodule Webuilt.Ledger.Balance.Validations.RequiresPositiveBalance do
  @moduledoc "Validates that an account requires a positive balance"
  use Ash.Resource.Validation

  @impl true
  def validate(changeset, _, _) do
    account_id = Ash.Changeset.get_attribute(changeset, :account_id)

    if Webuilt.Ledger.get_account!(account_id, authorize?: false).allow_negative_balance do
      {:error,
       Ash.Error.Changes.InvalidRelationship.exception(
         relationship: :account,
         message: "Account must require positive balance"
       )}
    else
      :ok
    end
  end

  @impl true
  def atomic(_changeset, _, _) do
    {:atomic, [:account], expr(account.allow_negative_balance == false),
     expr(
       error(Ash.Error.Changes.InvalidRelationship,
         relationship: :account,
         message: "Account must require positive balance"
       )
     )}
  end
end

  # in the balance resource
  validations do
    validate compare(:balance, greater_than_or_equal_to: 0),
      where: [Webuilt.Ledger.Balance.Validations.RequiresPositiveBalance],
      message: "balance cannot be negative"
  end

Just realized that you’re using an example from the guide. I will adjust to make the guide illustrate this

EDIT: I’ve published latest fixes and removal of that example. Better examples can be added later.

1 Like