Ash FrameworkAF
Ash Frameworkโ€ข6mo agoโ€ข
5 replies
zimt28

Show live changes depending on nested data

I have an Invoice resource that has_many InvoiceItems. Every InvoiceItem gets some data (quantity, tax_rate, unit_price) and calculates the item's total amounts in a change.

# Invoice Resource
change manage_relationship(:items, type: :create)

change __MODULE__.Changes.SetIdentifier
# Runs the after_action hook
change __MODULE__.Changes.AggregateTaxes


In the Invoice-create_invoice action I want to aggregate all the taxes, this means iterating all items and building sums. The way I currently do this is by running the action and then updating the invoice in an after_action hook, as I could not find a way to access the InvoiceItem's data before.

This works fine, the problem is that it breaks live updating forms โ€“ I'd like to show the calculated amounts immediately using the after_action hook breaks this. Is there a way to work around this?

I think I need a way to run the nested items' changes first or at least make sure the inputs to my nested resource are valid, in this case I could calculate the value in the Invoice's changeset. I feel like there must be a way to do this, I just could not figure it out yet ๐Ÿ™‚
Solution
I couldn't really find out how to use transform_params here. As far as I can understand it's helpful for updating the parameters that get passed to the form, but my "calculated" fields are not actually part of the form.

I was able to figure out another way โ€“ maybe it's helpful for someone else, so I'll share it. The solution was quite easy:

1) Run the necessary calculations in the top level changeset (also):
From this
@impl true
def change(changeset, _opts, context) do
  Ash.Changeset.after_action(changeset, fn _changeset, record ->
    calculate_totals(record, context)
  end)
end

defp calculate_totals(record, context) do
  tax_items =
    record.items
    |> ...

  record =
    record
    |> Ash.Changeset.for_update(:update, %{}, tenant: context.tenant)
    |> Ash.Changeset.force_change_attributes(%{
      tax_items: tax_items
    })
    |> Ash.update!()

  {:ok, record}
end

to this
@impl true
def change(changeset, _opts, _context) do
  items = Ash.Changeset.get_argument(changeset, :items) || []

  valid_items =
    for item <- items,
        {:ok, record} <- [maybe_invoice_item(item)] do
      record
    end

  {subtotal, tax_total, total} =
    Enum.reduce(valid_items, {0, 0, 0}, fn item, {subtotal, tax_total, total} ->
      {
        Decimal.add(subtotal, item.subtotal),
        Decimal.add(tax_total, item.tax_amount),
        Decimal.add(total, item.total)
      }
    end)

  Ash.Changeset.force_change_attributes(changeset, %{
    subtotal: subtotal,
    tax_total: tax_total,
    total: total
  })
end

defp maybe_invoice_item(item) do
  InvoiceItem
  |> Ash.Changeset.for_create(:create_invoice_item, item)
  |> Ash.Changeset.apply_attributes()
end

The "trick" here is not to wait for the actual results but calculate them using the children's changeset.

2) Use @form.source.attributes in the LiveView to get the updated values:
Total: {@form.source.attributes.total}
Was this page helpful?