cover/Elixir.WalletTransfers.Commands.CancelTransfer.html

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