cover/Elixir.WalletTransfers.Commands.InitiateTransfer.html

1 defmodule WalletTransfers.Commands.InitiateTransfer do
2 @moduledoc """
3 Command handler for initiating a new transfer.
4
5 Creates a transfer struct, persists it to TransferStore, emits domain and
6 audit events. Returns `{:ok, transfer}` on success.
7
8 Idempotency: if an `idempotency_key` is provided, TransferStore atomically
9 checks for an existing transfer with that key for the same user and returns
10 it directly (replay semantics per ADR 0004/0005). No duplicate is created.
11 """
12
13 alias WalletTransfers.{Transfer, TransferStore}
14 alias WalletTransfers.Events.TransferInitiated
15 alias WalletObservability.AuditEvent
16
17 @doc """
18 Executes the initiate transfer command.
19
20 Options:
21 - `idempotency_key` — caller-supplied key for safe retries.
22 - `type` — transfer type (`:internal` | `:p2p` | `:external`), default `:internal`.
23 - `reference` — unique business reference; auto-generated if not provided.
24 - `correlation_id` — propagated into events.
25 - `fee_amount` — fee in minor units, default 0.
26 - `metadata` — arbitrary metadata map.
27
28 Returns `{:ok, transfer}` or `{:error, reason}`.
29 """
30 @spec execute(
31 user_id :: String.t(),
32 from_account_id :: String.t(),
33 to_account_id :: String.t(),
34 amount :: pos_integer(),
35 currency :: String.t(),
36 opts :: keyword()
37 ) :: {:ok, Transfer.t()} | {:error, term()}
38 84 def execute(user_id, from_account_id, to_account_id, amount, currency, opts \\ []) do
39 71 correlation_id =
40 Keyword.get(opts, :correlation_id, WalletSharedKernel.Correlation.new_correlation_id())
41
42 71 idempotency_key = Keyword.get(opts, :idempotency_key)
43
44 71 transfer =
45 Transfer.new(user_id, from_account_id, to_account_id, amount, currency,
46 idempotency_key:
47 71 idempotency_key || WalletSharedKernel.Correlation.new_request_id(),
48 type: Keyword.get(opts, :type, :internal),
49 reference: Keyword.get(opts, :reference, WalletSharedKernel.TypedId.generate("ref")),
50 fee_amount: Keyword.get(opts, :fee_amount, 0),
51 correlation_id: correlation_id,
52 metadata: Keyword.get(opts, :metadata, %{})
53 )
54
55 71 case TransferStore.store(transfer) do
56 # Idempotency replay: store returned an existing transfer for this key/user pair.
57 16 {:ok, existing_transfer} ->
58 {:ok, existing_transfer}
59
60 # New transfer stored successfully — emit domain and audit events.
61 :ok ->
62 47 emit_event(
63 47 TransferInitiated.build(transfer.transfer_id, user_id,
64 correlation_id: correlation_id,
65 amount: amount,
66 currency: currency,
67 47 type: transfer.type
68 )
69 )
70
71 47 audit =
72 AuditEvent.build(
73 :financial,
74 "transfer_initiated",
75 "transfer",
76 47 transfer.transfer_id,
77 :success,
78 actor_id: user_id,
79 correlation_id: correlation_id,
80 metadata: %{
81 from_account_id: from_account_id,
82 to_account_id: to_account_id,
83 amount: amount,
84 currency: currency,
85 47 type: transfer.type,
86 47 reference: transfer.reference
87 }
88 )
89
90 47 :telemetry.execute([:wallet_transfers, :audit], %{}, audit)
91
92 {:ok, transfer}
93
94 8 {:error, reason} ->
95 {:error, reason}
96 end
97 rescue
98
:-(
error -> {:error, error}
99 end
100
101 47 defp emit_event(event) do
102 47 pubsub = Application.get_env(:wallet_transfers, :pubsub, WalletWeb.PubSub)
103
104 47 apply(Phoenix.PubSub, :broadcast, [
105 pubsub,
106 "wallet_transfers:events",
107 {:domain_event, event}
108 ])
109 rescue
110
:-(
_ -> :ok
111 end
112 end
Line Hits Source