diff --git a/documentation/dsls/DSL:-AshDoubleEntry.Transfer.md b/documentation/dsls/DSL:-AshDoubleEntry.Transfer.md index bfa5db1..149f55d 100644 --- a/documentation/dsls/DSL:-AshDoubleEntry.Transfer.md +++ b/documentation/dsls/DSL:-AshDoubleEntry.Transfer.md @@ -21,7 +21,7 @@ An extension for creating a double entry ledger transfer. See the getting starte | [`account_resource`](#transfer-account_resource){: #transfer-account_resource .spark-required} | `module` | | The resource to use for account balances | | [`pre_check_identities_with`](#transfer-pre_check_identities_with){: #transfer-pre_check_identities_with } | `module` | | A domain to use to precheck generated identities. Required by certain data layers. | | [`balance_resource`](#transfer-balance_resource){: #transfer-balance_resource } | `module` | | The resource being used for balances | -| [`create_accept`](#transfer-create_accept){: #transfer-create_accept } | `atom \| list(atom)` | | Additional attributes to accept when creating a transfer | +| [`create_accept`](#transfer-create_accept){: #transfer-create_accept } | `atom \| list(atom)` | `[]` | Additional attributes to accept when creating a transfer | diff --git a/documentation/tutorials/getting-started-with-ash-double-entry.md b/documentation/tutorials/getting-started-with-ash-double-entry.md index 7561efe..5160e6d 100644 --- a/documentation/tutorials/getting-started-with-ash-double-entry.md +++ b/documentation/tutorials/getting-started-with-ash-double-entry.md @@ -91,6 +91,11 @@ defmodule YourApp.Ledger.Transfer do # configure the other resources it will interact with account_resource YourApp.Ledger.Account balance_resource YourApp.Ledger.Balance + + # you only need this if you are using `postgres` + # and so cannot add the `references` block shown below + + # destroy_balances? true end end ``` @@ -122,6 +127,10 @@ defmodule YourApp.Ledger.Balance do postgres do table "balances" repo YourApp.Repo + + references do + reference :transfer, on_delete: :delete + end end balance do @@ -161,6 +170,13 @@ defmodule YourApp.Ledger.Balance do end ``` +> ### cascading destroys {: .warning} +> +> If you are not using a data layer capable of automatic cascade +> deletion, you must add `destroy_balances? true` to the `transfer` +> resource! We do this with the `references` block in `ash_postgres` +> as shown above. + #### What does this extension do? - Adds the following attributes: diff --git a/lib/transfer/changes/verify_transfer.ex b/lib/transfer/changes/verify_transfer.ex index 0a889b0..640a69f 100644 --- a/lib/transfer/changes/verify_transfer.ex +++ b/lib/transfer/changes/verify_transfer.ex @@ -8,6 +8,14 @@ defmodule AshDoubleEntry.Transfer.Changes.VerifyTransfer do use Ash.Resource.Change require Ash.Query + def atomic(changeset, opts, context) do + if AshDoubleEntry.Transfer.Info.transfer_destroy_balances?(changeset.resource) do + {:error, "Cannot destroy a transfer atomically if balances must be destroyed manually"} + else + {:ok, change(changeset, opts, context)} + end + end + def change(changeset, _opts, context) do if changeset.action.type == :update and Enum.any?( @@ -19,24 +27,21 @@ defmodule AshDoubleEntry.Transfer.Changes.VerifyTransfer do "Cannot modify a transfer's from_account_id, to_account_id, or id" ) else - changeset - |> Ash.Changeset.before_action(fn changeset -> - if changeset.action.type == :create do - timestamp = Ash.Changeset.get_attribute(changeset, :timestamp) - - timestamp = - case timestamp do - nil -> System.system_time(:millisecond) - timestamp -> DateTime.to_unix(timestamp, :millisecond) - end + if changeset.action.type == :create do + timestamp = Ash.Changeset.get_attribute(changeset, :timestamp) - ulid = AshDoubleEntry.ULID.generate(timestamp) + timestamp = + case timestamp do + nil -> System.system_time(:millisecond) + timestamp -> DateTime.to_unix(timestamp, :millisecond) + end - Ash.Changeset.force_change_attribute(changeset, :id, ulid) - else - changeset - end - end) + ulid = AshDoubleEntry.ULID.generate(timestamp) + + Ash.Changeset.force_change_attribute(changeset, :id, ulid) + else + changeset + end |> maybe_destroy_balances(context) |> Ash.Changeset.after_action(fn changeset, result -> from_account_id = Ash.Changeset.get_attribute(changeset, :from_account_id) @@ -192,7 +197,7 @@ defmodule AshDoubleEntry.Transfer.Changes.VerifyTransfer do ) |> case do %Ash.BulkResult{status: :success} -> changeset - %Ash.BulkResult{errors: errors} -> {:error, Ash.Changeset.add_error(changeset, errors)} + %Ash.BulkResult{errors: errors} -> Ash.Changeset.add_error(changeset, errors) end end) else diff --git a/lib/transfer/transfer.ex b/lib/transfer/transfer.ex index 7a485ae..509e446 100644 --- a/lib/transfer/transfer.ex +++ b/lib/transfer/transfer.ex @@ -23,6 +23,11 @@ defmodule AshDoubleEntry.Transfer do type: {:wrap_list, :atom}, default: [], doc: "Additional attributes to accept when creating a transfer" + ], + destroy_balances?: [ + type: :boolean, + doc: "Whether or not balances must be manually destroyed. See the getting started guide for more.", + default: false ] ] } diff --git a/lib/transfer/transformers/add_structure.ex b/lib/transfer/transformers/add_structure.ex index cc88495..2fccb71 100644 --- a/lib/transfer/transformers/add_structure.ex +++ b/lib/transfer/transformers/add_structure.ex @@ -37,6 +37,11 @@ defmodule AshDoubleEntry.Transfer.Transformers.AddStructure do AshDoubleEntry.Transfer.Info.transfer_account_resource!(dsl), attribute_writable?: true ) + |> Ash.Resource.Builder.add_new_relationship( + :has_many, + :balances, + AshDoubleEntry.Transfer.Info.transfer_balance_resource!(dsl) + ) |> Ash.Resource.Builder.add_action(:create, :transfer, accept: [:amount, :timestamp, :from_account_id, :to_account_id] ++ @@ -46,6 +51,7 @@ defmodule AshDoubleEntry.Transfer.Transformers.AddStructure do pagination: Ash.Resource.Builder.build_pagination(keyset?: true) ) |> Ash.Resource.Builder.add_change({AshDoubleEntry.Transfer.Changes.VerifyTransfer, []}, + only_when_valid?: true, on: [:create, :update, :destroy] ) end