AE
Ash Elixir•7d ago
Failz

ash_double_entry different currency

Working with USD and creating transfers from AshPhoenix.Form.submit() works as expected. I added additional accounts with a different currency, EUR, and this fails with:
* ** (ArgumentError) Cannot subtract two monies with different currencies. Received :EUR and :USD.
* ** (ArgumentError) Cannot subtract two monies with different currencies. Received :EUR and :USD.
which makes sense, but where did the :USD come from if both accounts have :EUR currencies? I added some IO.inspects to AshDoubleEntry.Transfer.Changes.VerifyTransfer and the changeset comes in with :USD
changeset: #Ash.Changeset<
domain: App.Ledger,
action_type: :create,
action: :create,
attributes: %{
id: "01JW8T49T67BCRY1PNFVMJM4Z7",
timestamp: ~U[2025-05-27 12:05:29.031480Z],
category: "uncategorized",
amount: Money.new(:USD, "4"),
note: nil,
updated_at: ~U[2025-05-27 12:05:29.031513Z],
inserted_at: ~U[2025-05-27 12:05:29.031513Z],
to_account_id: "0197119e-1d53-74b5-a958-8847d6b412e4",
from_account_id: "0197119d-e91b-7566-ab42-3b626b5fdd6e",
merchant: "unknown"
}
>
changeset: #Ash.Changeset<
domain: App.Ledger,
action_type: :create,
action: :create,
attributes: %{
id: "01JW8T49T67BCRY1PNFVMJM4Z7",
timestamp: ~U[2025-05-27 12:05:29.031480Z],
category: "uncategorized",
amount: Money.new(:USD, "4"),
note: nil,
updated_at: ~U[2025-05-27 12:05:29.031513Z],
inserted_at: ~U[2025-05-27 12:05:29.031513Z],
to_account_id: "0197119e-1d53-74b5-a958-8847d6b412e4",
from_account_id: "0197119d-e91b-7566-ab42-3b626b5fdd6e",
merchant: "unknown"
}
>
Creating a transfer with :EUR works from iex:
App.Ledger.Transfer
|> Ash.Changeset.for_create(:create, %{
amount: Money.new!(:EUR, 20),
from_account_id: ac1.id,
to_account_id: ac2.id})
|> Ash.create!()
App.Ledger.Transfer
|> Ash.Changeset.for_create(:create, %{
amount: Money.new!(:EUR, 20),
from_account_id: ac1.id,
to_account_id: ac2.id})
|> Ash.create!()
So where is an appropriate place to take our form_params.amount and create an appropriate instance of Money with the correct currency? Would it be in the App.Ledger.Transfer.create code interface with a custom_input or somewhere else?
form_params: %{
"amount" => "4",
"category" => "uncategorized",
"from_account_id" => "0197119d-e91b-7566-ab42-3b626b5fdd6e",
"merchant" => "unknown",
"note" => "",
"to_account_id" => "0197119e-1d53-74b5-a958-8847d6b412e4"
}
form_params: %{
"amount" => "4",
"category" => "uncategorized",
"from_account_id" => "0197119d-e91b-7566-ab42-3b626b5fdd6e",
"merchant" => "unknown",
"note" => "",
"to_account_id" => "0197119e-1d53-74b5-a958-8847d6b412e4"
}
6 Replies
ZachDaniel
ZachDaniel•7d ago
🤔 what do the params look like going into the form? You could do it by modifying the params in the form itself potentially
Failz
FailzOP•7d ago
form: #AshPhoenix.Form<
resource: App.Ledger.Transfer,
action: :create,
type: :create,
params: %{},
source: #Ash.Changeset<
domain: App.Ledger,
action_type: :create,
action: :create,
attributes: %{
id: "01JW8V8XR233KJFTH7RTZH8PEC",
category: "uncategorized",
merchant: "unknown"
},
relationships: %{},
errors: [
%Ash.Error.Changes.Required{
field: :amount,
type: :attribute,
resource: App.Ledger.Transfer,
splode: nil,
bread_crumbs: [],
vars: [],
path: [],
stacktrace: #Splode.Stacktrace<>,
class: :invalid
}
],
data: %App.Ledger.Transfer{
balances: #Ash.NotLoaded<:relationship, field: :balances>,
to_account: #Ash.NotLoaded<:relationship, field: :to_account>,
from_account: #Ash.NotLoaded<:relationship, field: :from_account>,
__meta__: #Ecto.Schema.Metadata<:built, "ledger_transfers">,
id: nil,
amount: nil,
category: "uncategorized",
merchant: "unknown",
note: nil,
inserted_at: nil,
updated_at: nil,
timestamp: nil,
from_account_id: nil,
to_account_id: nil
},
valid?: false
>,
name: "form",
data: nil,
form_keys: [],
forms: %{},
domain: App.Ledger,
method: "post",
submit_errors: nil,
id: "form",
transform_errors: nil,
original_data: nil,
transform_params: nil,
prepare_params: nil,
prepare_source: nil,
raw_params: %{},
warn_on_unhandled_errors?: true,
any_removed?: false,
added?: false,
changed?: true,
touched_forms: MapSet.new([]),
valid?: false,
errors: nil,
submitted_once?: false,
just_submitted?: false,
...
>
form: #AshPhoenix.Form<
resource: App.Ledger.Transfer,
action: :create,
type: :create,
params: %{},
source: #Ash.Changeset<
domain: App.Ledger,
action_type: :create,
action: :create,
attributes: %{
id: "01JW8V8XR233KJFTH7RTZH8PEC",
category: "uncategorized",
merchant: "unknown"
},
relationships: %{},
errors: [
%Ash.Error.Changes.Required{
field: :amount,
type: :attribute,
resource: App.Ledger.Transfer,
splode: nil,
bread_crumbs: [],
vars: [],
path: [],
stacktrace: #Splode.Stacktrace<>,
class: :invalid
}
],
data: %App.Ledger.Transfer{
balances: #Ash.NotLoaded<:relationship, field: :balances>,
to_account: #Ash.NotLoaded<:relationship, field: :to_account>,
from_account: #Ash.NotLoaded<:relationship, field: :from_account>,
__meta__: #Ecto.Schema.Metadata<:built, "ledger_transfers">,
id: nil,
amount: nil,
category: "uncategorized",
merchant: "unknown",
note: nil,
inserted_at: nil,
updated_at: nil,
timestamp: nil,
from_account_id: nil,
to_account_id: nil
},
valid?: false
>,
name: "form",
data: nil,
form_keys: [],
forms: %{},
domain: App.Ledger,
method: "post",
submit_errors: nil,
id: "form",
transform_errors: nil,
original_data: nil,
transform_params: nil,
prepare_params: nil,
prepare_source: nil,
raw_params: %{},
warn_on_unhandled_errors?: true,
any_removed?: false,
added?: false,
changed?: true,
touched_forms: MapSet.new([]),
valid?: false,
errors: nil,
submitted_once?: false,
just_submitted?: false,
...
>
and that's from AshPhoenix.Form.for_create(App.Ledger.Transfer, :create) this is working, although I wonder if someplace in the domain/resource is better because this logic isn't applied to things like JSON or GraphQL APIs
defp convert_amount_to_account_currency(form_params, accounts) do
case form_params do
%{"from_account_id" => from_account_id, "amount" => amount}
when is_binary(amount) ->
case Enum.find(accounts, &(&1.id == from_account_id)) do
%{currency: currency} ->
money = Money.new(amount, currency)
Map.put(form_params, "amount", money)

nil ->
form_params
end

_ ->
form_params
end
end
defp convert_amount_to_account_currency(form_params, accounts) do
case form_params do
%{"from_account_id" => from_account_id, "amount" => amount}
when is_binary(amount) ->
case Enum.find(accounts, &(&1.id == from_account_id)) do
%{currency: currency} ->
money = Money.new(amount, currency)
Map.put(form_params, "amount", money)

nil ->
form_params
end

_ ->
form_params
end
end
ZachDaniel
ZachDaniel•7d ago
Over those APIs I believe they accept an amount and a currency That is pretty strange though, I think you could maybe do it as part of your template like in the html include the currency and amount fields as nested fields
Failz
FailzOP•7d ago
Which API's specifically? I'm still the newbie 👶
ZachDaniel
ZachDaniel•7d ago
I mean AshGraphql and AshMoney accept both a currency and an amount in their generated APIs so they don't suffer from the same issue that your UI does where its just providing a number and so therefore using the default configured currency
Failz
FailzOP•7d ago
ah ok. I haven't turned on yet but good to know

Did you find this page helpful?