cover/Elixir.WalletTransfers.Commands.CompleteTransfer.html

1 defmodule WalletTransfers.Commands.CompleteTransfer do
2 @moduledoc """
3 Command handler for completing a reserved transfer.
4
5 Acquires a per-transfer lock, retrieves the transfer, applies the complete
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.TransferCompleted
12 alias WalletObservability.AuditEvent
13
14 @doc """
15 Executes the complete transfer 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 21 def execute(transfer_id, opts \\ []) do
25 21 correlation_id =
26 Keyword.get(opts, :correlation_id, WalletSharedKernel.Correlation.new_correlation_id())
27
28 21 case LockStore.acquire(transfer_id) do
29 9 {:error, :locked} ->
30 {:error, :locked}
31
32 :ok ->
33 12 result =
34 6 with {:ok, transfer} <- TransferStore.get(transfer_id),
35 11 {:ok, completed_transfer} <- Transfer.complete(transfer),
36 6 :ok <- TransferStore.update(completed_transfer) do
37 {:ok, completed_transfer}
38 end
39
40 12 LockStore.release(transfer_id)
41
42 12 case result do
43 {:ok, completed_transfer} ->
44 6 emit_event(
45 6 TransferCompleted.build(transfer_id, completed_transfer.user_id,
46 correlation_id: correlation_id,
47 6 amount: completed_transfer.amount,
48 6 currency: completed_transfer.currency,
49 6 completed_at: completed_transfer.completed_at
50 )
51 )
52
53 6 audit =
54 AuditEvent.build(
55 :financial,
56 "transfer_completed",
57 "transfer",
58 transfer_id,
59 :success,
60 6 actor_id: completed_transfer.user_id,
61 correlation_id: correlation_id,
62 metadata: %{
63 6 amount: completed_transfer.amount,
64 6 currency: completed_transfer.currency
65 }
66 )
67
68 6 :telemetry.execute([:wallet_transfers, :audit], %{}, audit)
69
70 {:ok, completed_transfer}
71
72 error ->
73 6 error
74 end
75 end
76 end
77
78 6 defp emit_event(event) do
79 6 pubsub = Application.get_env(:wallet_transfers, :pubsub, WalletWeb.PubSub)
80
81 6 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