cover/Elixir.WalletTransfers.Commands.ReserveTransferFunds.html

1 defmodule WalletTransfers.Commands.ReserveTransferFunds do
2 @moduledoc """
3 Command handler for reserving funds on an initiated transfer.
4
5 Acquires a per-transfer lock, retrieves the transfer, applies the reserve
6 state transition, persists the updated transfer, releases the lock, and
7 emits domain and audit events.
8 """
9
10 alias WalletTransfers.{Transfer, TransferStore, LockStore}
11 alias WalletTransfers.Events.TransferReserved
12 alias WalletObservability.AuditEvent
13
14 @doc """
15 Executes the reserve transfer funds command.
16
17 Options:
18 - `correlation_id` — propagated into events.
19
20 Returns `{:ok, transfer}` or `{:error, :locked | :not_found | :invalid_transition}`.
21 """
22 @spec execute(transfer_id :: String.t(), opts :: keyword()) ::
23 {:ok, Transfer.t()} | {:error, :locked | :not_found | :invalid_transition}
24 37 def execute(transfer_id, opts \\ []) do
25 37 correlation_id =
26 Keyword.get(opts, :correlation_id, WalletSharedKernel.Correlation.new_correlation_id())
27
28 37 case LockStore.acquire(transfer_id) do
29 15 {:error, :locked} ->
30 {:error, :locked}
31
32 :ok ->
33 22 result =
34 5 with {:ok, transfer} <- TransferStore.get(transfer_id),
35 21 {:ok, reserved_transfer} <- Transfer.reserve(transfer),
36 17 :ok <- TransferStore.update(reserved_transfer) do
37 {:ok, reserved_transfer}
38 end
39
40 22 LockStore.release(transfer_id)
41
42 22 case result do
43 {:ok, reserved_transfer} ->
44 17 emit_event(
45 17 TransferReserved.build(transfer_id, reserved_transfer.user_id,
46 correlation_id: correlation_id,
47 17 amount: reserved_transfer.amount,
48 17 currency: reserved_transfer.currency,
49 17 reserved_at: reserved_transfer.reserved_at
50 )
51 )
52
53 17 audit =
54 AuditEvent.build(
55 :financial,
56 "transfer_reserved",
57 "transfer",
58 transfer_id,
59 :success,
60 17 actor_id: reserved_transfer.user_id,
61 correlation_id: correlation_id,
62 metadata: %{
63 17 amount: reserved_transfer.amount,
64 17 currency: reserved_transfer.currency
65 }
66 )
67
68 17 :telemetry.execute([:wallet_transfers, :audit], %{}, audit)
69
70 {:ok, reserved_transfer}
71
72 error ->
73 5 error
74 end
75 end
76 end
77
78 17 defp emit_event(event) do
79 17 pubsub = Application.get_env(:wallet_transfers, :pubsub, WalletWeb.PubSub)
80
81 17 apply(Phoenix.PubSub, :broadcast, [
82 pubsub,
83 "wallet_transfers:events",
84 {:domain_event, event}
85 ])
86 rescue
87
:-(
_ -> :ok
88 end
89 end
Line Hits Source