How to fix: `ash_double_entry` transfer throwing (Jason.EncodeError) invalid byte error

I am using the latest version of ash_double_entry but I am getting the following error while trying to transfer amount from one account to another.

I am not sure why Jason is unable to encode it. How can I solve it?

Transfer #=> MyApp.Ledger.Transfer
|> Ash.Changeset.for_create(:transfer, attrs, context) #=> #Ash.Changeset<
  domain: MyApp.Ledger,
  action_type: :create,
  action: :transfer,
  tenant: "test_org",
  attributes: %{
    id: "01JGM56T672FCQNXJ3JQ6PKZA6",
    amount: Money.new(:KES, "20"),
    from_account_id: "f0f1ce00-db80-401f-bae5-87623857ec4d",
    to_account_id: "225b3615-d5e9-4868-8fc6-c977342e7e90"
  },
  relationships: %{},
  errors: [],
  data: #MyApp.Ledger.Transfer<
    balances: #Ash.NotLoaded<:relationship, field: :balances>,
    to_account: #Ash.NotLoaded<:relationship, field: :to_account>,
    from_account: #Ash.NotLoaded<:relationship, field: :from_account>,
    paper_trail_versions: #Ash.NotLoaded<:relationship, field: :paper_trail_versions>,
    __meta__: #Ecto.Schema.Metadata<:built, "ledger_transfers">,
    id: nil,
    amount: nil,
    timestamp: nil,
    inserted_at: nil,
    from_account_id: nil,
    to_account_id: nil,
    aggregates: %{},
    calculations: %{},
    ...
  >,
  valid?: true
>
|> Ash.create() #=> {:error,
 %Ash.Error.Unknown{
   bread_crumbs: ["Error returned from: MyApp.Ledger.Transfer.transfer"], 
   changeset: "#Changeset<>", 
   errors: [
     %Ash.Error.Unknown.UnknownError{
       error: "** (Jason.EncodeError) invalid byte 0x94 in <<1, 148, 40, 83, 104, 199, 19, 217, 122, 246, 67, 149, 205, 105, 253, 70>>",
       field: nil,
       value: nil,
       splode: Ash.Error,
       bread_crumbs: ["Error returned from: MyApp.Ledger.Transfer.transfer"],
       vars: [],
       path: [],
       stacktrace: #Splode.Stacktrace<>,
       class: :unknown
     }
   ]
 }}

Are you also on the latest version of ash, ash_postgres and ash_sql? If not, please try updating those. Otherwise, please create a reproduction and I will address ASAP.

1 Like

Yes, I am using latest versions for all Ash packages.

ash_archival 1.0.4
ash_authentication 4.3.9
ash_authentication_phoenix 2.4.4
ash_double_entry 1.0.6
ash_money 0.1.15
ash_paper_trail 0.4.0
ash_postgres 2.4.20
ash_sql 0.2.43
ash 3.4.50 3.4.51
ash_phoenix  2.1.13

REPRODUCTION

Failing Tests

defmodule MyApp.Ledger.LedgerAccountsLiveTest do
  alias MyApp.Ledger.{Account, AccountType, Transfer}
  use MyAppWeb.ConnCase, async: false
  import LedgerCase

  def get_accounts(tenant) do
    case Ash.read(Account, tenant: tenant, authorize?: false) do
      {:ok, []} -> create_accounts(tenant)
      {:ok, accounts} -> accounts
    end
  end

  def create_accounts(tenant) do
    
    # Account type are automatically seeded when testing starts.
    account_type = Ash.read_first!(AccountType, tenant: tenant, authorize?: false)

    attrs = [
      %{
        identifier: "1000",
        account_number: "1000",
        name: "CASH and BANK",
        currency: :USD,
        account_type_id: account_type.id
      },
      %{
        identifier: "1010",
        account_number: "1010",
        currency: :USD,
        name: "CASH and BANK - NCBA",
        account_type_id: account_type.id
      },
      %{
        identifier: "1020",
        account_number: "1020",
        currency: :USD,
        name: "CASH and BANK - OPEN FLOAT",
        account_type_id: account_type.id
      }
    ]

    Ash.Seed.seed!(Account, attrs, tenant: tenant)
  end
  
  describe "Ledger Accounts" do
    test "Transfer between 2 accounts happens successfully", %{user: actor} do
      accounts = get_accounts(actor.current_tenant)
      account_1 = Enum.at(accounts, 1)
      account_2 = Enum.at(accounts, 2)

      attrs = %{
        amount: Money.new!(20, :USD),
        from_account_id: account_1.id,
        to_account_id: account_2.id
      }

      opts = [actor: actor, tenant: actor.current_tenant]

      Transfer
      |> Ash.Changeset.for_create(:transfer, attrs, opts)
      |> Ash.create()
      |> dbg()
    end
  end
end

Account Resource

defmodule MyApp.Ledger.Account do
  
  use Ash.Resource,
    domain: MyApp.Ledger,
    authorizers: [Ash.Policy.Authorizer],
    data_layer: AshPostgres.DataLayer,
    notifiers: [Ash.Notifier.PubSub],
    extensions: [AshPaperTrail.Resource, AshDoubleEntry.Account]

  postgres do
    table "ledger_accounts"
    repo MyApp.Repo
  end

  paper_trail do
    belongs_to_actor :user, MyApp.Auth.User, domain: MyApp.Auth
    store_action_name? true
  end

  account do
    transfer_resource(MyApp.Ledger.Transfer)
    balance_resource(MyApp.Ledger.Balance)
    open_action_accept([:account_number, :name, :account_type_id])
  end

  policies do
    policy always() do
      access_type :strict
      authorize_if MyApp.Auth.Checks.Can
    end
  end

  pub_sub do
    module MyAppWeb.Endpoint

    prefix "ledger_accounts"
    publish_all :update, [[:id, nil]]
    publish_all :create, [[:id, nil]]
    publish_all :destroy, [[:id, nil]]
  end

  preparations do
    prepare MyApp.Preparations.SetTenantFromActor
  end

  changes do
    change MyApp.Changes.SetTenantFromActor
  end

  multitenancy do
    strategy :context
  end

  attributes do
    attribute :account_number, :string, allow_nil?: false
    attribute :name, :string, allow_nil?: false
    attribute :tax_rate, :decimal, default: 0
  end

  relationships do
    belongs_to :account_type, MyApp.Ledger.AccountType do
      source_attribute :account_type_id
    end

    belongs_to :parent_account, MyApp.Ledger.Account do
      source_attribute :parent_account_id
      destination_attribute :id
      allow_nil? true
    end

    has_many :sub_accounts, MyApp.Ledger.Account do
      destination_attribute :parent_account_id
      source_attribute :id
    end
  end
end

Transfer resource

defmodule MyApp.Ledger.Transfer do
  use Ash.Resource,
    domain: MyApp.Ledger,
    authorizers: [Ash.Policy.Authorizer],
    data_layer: AshPostgres.DataLayer,
    notifiers: [Ash.Notifier.PubSub],
    extensions: [AshPaperTrail.Resource, AshDoubleEntry.Transfer]

  postgres do
    table "ledger_transfers"
    repo MyApp.Repo
  end

  paper_trail do
    belongs_to_actor :user, MyApp.Auth.User, domain: MyApp.Auth

    store_action_name? true
  end

  transfer do
    account_resource(MyApp.Ledger.Account)
    balance_resource(MyApp.Ledger.Balance)

    destroy_balances?(true)
  end

  policies do
    policy always() do
      access_type :strict
      authorize_if MyApp.Auth.Checks.Can
    end
  end

  pub_sub do
    module MyAppWeb.Endpoint

    prefix "ledger_transfers"
    publish_all :update, [[:id, nil]]
    publish_all :create, [[:id, nil]]
    publish_all :destroy, [[:id, nil]]
  end

  preparations do
    prepare MyApp.Preparations.SetTenantFromActor
  end

  changes do
    change MyApp.Changes.SetTenantFromActor
  end

  multitenancy do
    strategy :context
  end
end

Can you put this into a GitHub repository that I can clone? It takes much longer and leaves room for error if I have to set up a new project, like if the issue is somehow related to data layer setup etc.

1 Like

HI @zachdaniel,

I setup a repository but I could not reproduce this error in the fresh new install. Testing are successfully.

It must be something specific to my codes or a conflicting package. I am happy to invite you to my private repo, if you are open to that, otherwise, I need to dig deeper…

You can clone and test ash_debugger/test/transfer_test.exs at main · kamaroly/ash_debugger · GitHub.

@zachdaniel,

I have reproduced the error. The issue happens when you add ash_paper_trail to a project with ash_double_entry. It appears to happen while encoding changes into json for versions on the Transfer resource.

I updated the repository with a reproduction. See: Reproduced the error. Ash double entry incompatible with ash paper trail · kamaroly/ash_debugger@b426ad6 · GitHub

Excellent detective work :bowing_man: This looks to be an easy fix actually.

Mind trying main of ash_double_entry? It needed logic to handle the ULID value that it uses being encoded into a json map.

1 Like

Thanks @zachdaniel. The main branch solved the issue and my tests are passing now.

Are you planning to release the fix to hex.pm anytime soon? Just asking so that I don’t have to keep fetching the package from github.

Thanks and enjoy your weekend.

I will release it next week.

1 Like

I will be using the main branch for now. Once again, thank you for the amazing work as alwasy.

1 Like