cover/Elixir.WalletSettlement.Workers.SettlementWorker.html

1 defmodule WalletSettlement.Workers.SettlementWorker do
2 @moduledoc """
3 Async worker for executing a settlement batch.
4
5 Implements `perform/1` compatible with both the internal `JobQueue` and Oban.
6
7 Expected args:
8 - `"batch_id"` — the settlement batch to process.
9 - `"correlation_id"` — propagated into events.
10
11 Retry policy: up to `max_attempts` configured on `:settlement_high` queue.
12 """
13
14 alias WalletSettlement.{SettlementBatch, SettlementStore}
15 alias WalletSettlement.Events.{SettlementBatchStarted, SettlementBatchCompleted, SettlementExceptionRaised}
16 alias WalletObservability.AuditEvent
17
18 @doc "Performs settlement batch execution. Returns `:ok` or `{:error, reason}`."
19 @spec perform(args :: map()) :: :ok | {:error, term()}
20 def perform(%{"batch_id" => batch_id} = args) do
21 8 correlation_id = Map.get(args, "correlation_id", WalletSharedKernel.Correlation.new_correlation_id())
22
23 8 with {:ok, batch} <- SettlementStore.get(batch_id),
24 4 {:ok, running} <- SettlementBatch.start(batch),
25 4 :ok <- SettlementStore.update(running) do
26
27 4 emit_event(SettlementBatchStarted.build(batch_id, correlation_id: correlation_id,
28 4 transfer_count: length(running.transfer_ids)))
29
30 4 emit_audit(:settlement_batch_started, batch_id, correlation_id, %{
31 4 transfer_count: length(running.transfer_ids),
32 4 window_start: running.window_start,
33 4 window_end: running.window_end
34 })
35
36 # Process transfers — each completed transfer is settled by marking it in ledger.
37 # In production this calls the CBS/payment rail adapter (Phase 6).
38 4 {settled, failed, exceptions} = process_transfers(running, correlation_id)
39
40 4 case SettlementBatch.complete(running, settled_count: settled, failed_count: failed, exception_count: exceptions) do
41 {:ok, completed} ->
42 4 SettlementStore.update(completed)
43
44 4 emit_event(SettlementBatchCompleted.build(batch_id, correlation_id: correlation_id,
45 settled_count: settled, failed_count: failed, exception_count: exceptions,
46 4 status: completed.status))
47
48 4 emit_audit(:settlement_batch_completed, batch_id, correlation_id, %{
49 settled_count: settled, failed_count: failed, exception_count: exceptions,
50 4 status: completed.status
51 })
52
53 :ok
54
55
:-(
{:error, reason} -> {:error, reason}
56 end
57 end
58 end
59
60 1 def perform(_args), do: {:error, :missing_batch_id}
61
62 # Processes all transfers in the batch. Returns {settled, failed, exceptions}.
63 # Phase 5: pure in-memory simulation; Phase 6 replaces with external adapter calls.
64 defp process_transfers(batch, correlation_id) do
65 4 Enum.reduce(batch.transfer_ids, {0, 0, 0}, fn transfer_id, {s, f, e} ->
66 # Stub: Simulate settlement — in practice calls external payment rail.
67 3 transfers_mod = Application.get_env(:wallet_settlement, :transfers_module, WalletTransfers.Queries.GetTransfer)
68
69 3 case apply(transfers_mod, :execute, [transfer_id]) do
70 {:ok, transfer} when transfer.status == :completed ->
71 # Completed transfer — count as settled
72 2 {s + 1, f, e}
73
74 {:ok, _transfer} ->
75 # Transfer not in completed state — raise exception
76
:-(
raise_exception(batch.batch_id, transfer_id, :status_mismatch, :high, correlation_id)
77
:-(
{s, f, e + 1}
78
79 {:error, :not_found} ->
80 # Missing transfer record — raise exception
81 1 raise_exception(batch.batch_id, transfer_id, :missing_transfer, :critical, correlation_id)
82 1 {s, f + 1, e + 1}
83 end
84 end)
85 end
86
87 defp raise_exception(batch_id, transfer_id, mismatch_type, severity, correlation_id) do
88 alias WalletSettlement.{ExceptionRecord, ExceptionStore}
89
90 1 exc = ExceptionRecord.new(batch_id, mismatch_type, severity,
91 transfer_id: transfer_id, correlation_id: correlation_id)
92
93 1 ExceptionStore.store(exc)
94
95 1 emit_event(SettlementExceptionRaised.build(exc.exception_id,
96 batch_id: batch_id, transfer_id: transfer_id,
97 mismatch_type: mismatch_type, severity: severity,
98 correlation_id: correlation_id))
99 end
100
101 9 defp emit_event(event) do
102 9 pubsub = Application.get_env(:wallet_settlement, :pubsub, WalletWeb.PubSub)
103 9 apply(Phoenix.PubSub, :broadcast, [pubsub, "wallet_settlement:events", {:domain_event, event}])
104 rescue
105
:-(
_ -> :ok
106 end
107
108 8 defp emit_audit(action, batch_id, correlation_id, metadata) do
109 8 audit = AuditEvent.build(:settlement, to_string(action), "settlement_batch", batch_id,
110 :success, correlation_id: correlation_id, metadata: metadata)
111 8 :telemetry.execute([:wallet_settlement, :audit], %{}, audit)
112 rescue
113
:-(
_ -> :ok
114 end
115 end
Line Hits Source