2023-08-06 16:27:33 +12:00
|
|
|
defmodule AshDoubleEntry.Transfer.Changes.VerifyTransfer do
|
2023-09-27 02:46:07 +13:00
|
|
|
# Verify a transfer and update all related balances of the accounts involved.
|
2023-08-06 16:27:33 +12:00
|
|
|
|
2023-09-27 02:46:07 +13:00
|
|
|
# This operation locks the accounts involved, serializing all transfers between
|
|
|
|
# relevant accounts.
|
|
|
|
|
|
|
|
@moduledoc false
|
2023-08-06 16:27:33 +12:00
|
|
|
use Ash.Resource.Change
|
|
|
|
require Ash.Query
|
|
|
|
|
|
|
|
def change(changeset, _opts, context) do
|
2023-12-13 02:12:44 +13:00
|
|
|
if changeset.action.type == :update and
|
|
|
|
Enum.any?(
|
|
|
|
[:from_account_id, :to_account_id, :id],
|
2023-12-11 07:20:49 +13:00
|
|
|
&Ash.Changeset.changing_attribute?(changeset, &1)
|
|
|
|
) do
|
2023-12-13 02:12:44 +13:00
|
|
|
Ash.Changeset.add_error(
|
|
|
|
changeset,
|
|
|
|
"Cannot modify a transfer's from_account_id, to_account_id, or id"
|
|
|
|
)
|
2023-12-11 07:20:49 +13:00
|
|
|
else
|
|
|
|
changeset
|
|
|
|
|> Ash.Changeset.before_action(fn changeset ->
|
2023-12-13 02:12:44 +13:00
|
|
|
if changeset.action.type == :create do
|
|
|
|
timestamp = Ash.Changeset.get_attribute(changeset, :timestamp)
|
2023-08-06 16:27:33 +12:00
|
|
|
|
2023-12-13 02:12:44 +13:00
|
|
|
timestamp =
|
|
|
|
case timestamp do
|
|
|
|
nil -> System.system_time(:millisecond)
|
|
|
|
timestamp -> DateTime.to_unix(timestamp, :millisecond)
|
|
|
|
end
|
2023-08-06 16:27:33 +12:00
|
|
|
|
2023-12-13 02:12:44 +13:00
|
|
|
ulid = AshDoubleEntry.ULID.generate(timestamp)
|
2023-12-11 07:20:49 +13:00
|
|
|
|
2023-12-13 02:12:44 +13:00
|
|
|
Ash.Changeset.force_change_attribute(changeset, :id, ulid)
|
|
|
|
else
|
|
|
|
changeset
|
|
|
|
end
|
2023-12-11 07:20:49 +13:00
|
|
|
end)
|
|
|
|
|> maybe_destroy_balances(context)
|
|
|
|
|> Ash.Changeset.after_action(fn changeset, result ->
|
|
|
|
from_account_id = Ash.Changeset.get_attribute(changeset, :from_account_id)
|
|
|
|
to_account_id = Ash.Changeset.get_attribute(changeset, :to_account_id)
|
2024-02-19 16:11:32 +13:00
|
|
|
new_amount = Ash.Changeset.get_attribute(changeset, :amount)
|
|
|
|
|
|
|
|
old_amount =
|
|
|
|
if changeset.action.type == :destroy do
|
|
|
|
Money.new!(0, new_amount.currency)
|
|
|
|
else
|
|
|
|
changeset.data.amount || Money.new!(0, new_amount.currency)
|
|
|
|
end
|
2023-12-11 07:20:49 +13:00
|
|
|
|
2023-12-13 02:12:44 +13:00
|
|
|
amount_delta =
|
2024-02-19 16:11:32 +13:00
|
|
|
Money.sub!(new_amount, old_amount)
|
2023-12-13 02:12:44 +13:00
|
|
|
|
2023-12-11 07:20:49 +13:00
|
|
|
accounts =
|
|
|
|
changeset.resource
|
|
|
|
|> AshDoubleEntry.Transfer.Info.transfer_account_resource!()
|
|
|
|
|> Ash.Query.filter(id in ^[from_account_id, to_account_id])
|
2024-06-19 08:50:23 +12:00
|
|
|
|> Ash.Query.set_context(%{ash_double_entry?: true})
|
2024-04-02 10:21:49 +13:00
|
|
|
|> Ash.Query.for_read(
|
|
|
|
:lock_accounts,
|
|
|
|
%{},
|
2024-06-18 13:36:50 +12:00
|
|
|
Ash.Context.to_opts(context,
|
|
|
|
authorize?: authorize?(changeset.domain),
|
|
|
|
domain: changeset.domain
|
|
|
|
)
|
2024-04-02 10:21:49 +13:00
|
|
|
)
|
2023-12-11 07:20:49 +13:00
|
|
|
|> Ash.Query.load(balance_as_of_ulid: %{ulid: result.id})
|
2024-04-02 10:21:49 +13:00
|
|
|
|> Ash.read!()
|
2023-12-11 07:20:49 +13:00
|
|
|
|
|
|
|
from_account = Enum.find(accounts, &(&1.id == from_account_id))
|
|
|
|
to_account = Enum.find(accounts, &(&1.id == to_account_id))
|
|
|
|
|
|
|
|
new_from_account_balance =
|
|
|
|
Money.sub!(
|
|
|
|
from_account.balance_as_of_ulid || Money.new!(0, from_account.currency),
|
2023-12-13 02:12:44 +13:00
|
|
|
amount_delta
|
2023-12-11 07:20:49 +13:00
|
|
|
)
|
|
|
|
|
|
|
|
new_to_account_balance =
|
2023-12-13 02:12:44 +13:00
|
|
|
Money.add!(
|
|
|
|
to_account.balance_as_of_ulid || Money.new!(0, to_account.currency),
|
|
|
|
amount_delta
|
|
|
|
)
|
2023-12-11 07:20:49 +13:00
|
|
|
|
2024-04-30 07:40:06 +12:00
|
|
|
balance_resource =
|
|
|
|
AshDoubleEntry.Transfer.Info.transfer_balance_resource!(changeset.resource)
|
2023-12-11 07:20:49 +13:00
|
|
|
|
2024-04-30 07:40:06 +12:00
|
|
|
unless changeset.action.type == :destroy do
|
|
|
|
Ash.bulk_create!(
|
|
|
|
[
|
|
|
|
%{
|
|
|
|
account_id: from_account.id,
|
|
|
|
transfer_id: result.id,
|
|
|
|
balance: new_from_account_balance
|
|
|
|
},
|
|
|
|
%{
|
|
|
|
account_id: to_account.id,
|
|
|
|
transfer_id: result.id,
|
|
|
|
balance: new_to_account_balance
|
|
|
|
}
|
|
|
|
],
|
|
|
|
balance_resource,
|
2023-12-11 07:20:49 +13:00
|
|
|
:upsert_balance,
|
2024-04-02 10:21:49 +13:00
|
|
|
Ash.Context.to_opts(context,
|
|
|
|
domain: changeset.domain,
|
2024-04-30 07:40:06 +12:00
|
|
|
upsert_fields: [:balance],
|
|
|
|
return_errors?: true,
|
|
|
|
stop_on_error?: true
|
2024-04-02 10:21:49 +13:00
|
|
|
)
|
2023-12-11 07:20:49 +13:00
|
|
|
)
|
|
|
|
end
|
2023-08-06 16:27:33 +12:00
|
|
|
|
2024-04-30 07:40:06 +12:00
|
|
|
amount_delta =
|
|
|
|
if changeset.action.type == :destroy do
|
|
|
|
Money.mult!(amount_delta, -1)
|
2023-12-11 07:20:49 +13:00
|
|
|
else
|
2024-04-30 07:40:06 +12:00
|
|
|
amount_delta
|
2023-12-11 07:20:49 +13:00
|
|
|
end
|
2024-04-30 07:40:06 +12:00
|
|
|
|
|
|
|
Ash.bulk_update!(
|
|
|
|
balance_resource,
|
|
|
|
:adjust_balance,
|
|
|
|
%{
|
|
|
|
from_account_id: from_account.id,
|
|
|
|
to_account_id: to_account.id,
|
|
|
|
transfer_id: result.id,
|
|
|
|
delta: amount_delta
|
|
|
|
},
|
2024-04-02 10:21:49 +13:00
|
|
|
Ash.Context.to_opts(context,
|
|
|
|
domain: changeset.domain,
|
2024-04-30 07:40:06 +12:00
|
|
|
strategy: [:atomic, :stream, :atomic_batches],
|
2023-12-11 07:20:49 +13:00
|
|
|
return_errors?: true,
|
2024-04-30 07:40:06 +12:00
|
|
|
stop_on_error?: true
|
2023-12-11 07:20:49 +13:00
|
|
|
)
|
2024-06-18 13:36:50 +12:00
|
|
|
|> Keyword.update(
|
|
|
|
:context,
|
|
|
|
%{ash_double_entry?: true},
|
|
|
|
&Map.put(&1, :ash_double_entry?, true)
|
|
|
|
)
|
2023-12-11 07:16:02 +13:00
|
|
|
)
|
2023-08-06 16:27:33 +12:00
|
|
|
|
2023-12-11 07:20:49 +13:00
|
|
|
{:ok, result}
|
2023-08-06 16:27:33 +12:00
|
|
|
end)
|
2023-12-11 07:20:49 +13:00
|
|
|
end
|
2023-08-06 16:27:33 +12:00
|
|
|
end
|
|
|
|
|
2023-12-11 07:16:02 +13:00
|
|
|
defp maybe_destroy_balances(changeset, context) do
|
|
|
|
if changeset.action.type == :destroy do
|
|
|
|
balance_resource =
|
|
|
|
changeset.resource
|
|
|
|
|> AshDoubleEntry.Transfer.Info.transfer_balance_resource!()
|
|
|
|
|
|
|
|
destroy_action = Ash.Resource.Info.primary_action(balance_resource, :destroy)
|
|
|
|
|
|
|
|
if !destroy_action do
|
|
|
|
raise "Must configure a primary destroy action for #{inspect(balance_resource)} to destroy transactions"
|
|
|
|
end
|
|
|
|
|
|
|
|
Ash.Changeset.before_action(changeset, fn changeset ->
|
|
|
|
balance_resource
|
|
|
|
|> Ash.Query.filter(transfer_id == ^changeset.data.id)
|
2024-06-18 13:36:50 +12:00
|
|
|
|> Ash.Query.set_context(%{ash_double_entry?: true})
|
2024-04-30 07:40:06 +12:00
|
|
|
|> Ash.bulk_destroy!(
|
|
|
|
destroy_action,
|
|
|
|
%{},
|
|
|
|
Ash.Context.to_opts(context,
|
2024-06-18 13:36:50 +12:00
|
|
|
authorize?: authorize?(changeset.domain),
|
2024-04-30 07:40:06 +12:00
|
|
|
domain: changeset.domain,
|
|
|
|
strategy: [:stream, :atomic, :atomic_batches]
|
2023-12-11 07:16:02 +13:00
|
|
|
)
|
2024-04-30 07:40:06 +12:00
|
|
|
)
|
2023-12-11 07:16:02 +13:00
|
|
|
|
|
|
|
changeset
|
|
|
|
end)
|
|
|
|
else
|
|
|
|
changeset
|
|
|
|
end
|
|
|
|
end
|
2024-06-18 13:36:50 +12:00
|
|
|
|
|
|
|
defp authorize?(domain), do: Ash.Domain.Info.authorize(domain) == :always
|
2023-08-06 16:27:33 +12:00
|
|
|
end
|