cover/Elixir.WalletSettlement.SettlementBatch.html

1 defmodule WalletSettlement.SettlementBatch do
2 @moduledoc """
3 Settlement batch struct and lifecycle state machine.
4
5 A settlement batch aggregates completed transfers within a time window and
6 processes them against external counterparties or core banking.
7
8 Lifecycle:
9 :pending -> :running (via start/1)
10 :running -> :completed (via complete/2)
11 :running -> :partial (via partial/2 — some items settled, some failed)
12 :running -> :failed (via fail/2)
13 :completed / :partial / :failed are terminal states.
14
15 TypedId prefix: `stl_`
16 """
17
18 alias WalletSharedKernel.TypedId
19
20 @terminal_states [:completed, :partial, :failed]
21
22 @enforce_keys [
23 :batch_id,
24 :window_start,
25 :window_end,
26 :status,
27 :transfer_ids,
28 :created_at
29 ]
30
31
:-(
defstruct [
32 :batch_id,
33 :window_start,
34 :window_end,
35 :status,
36 :currency,
37 :correlation_id,
38 :started_at,
39 :completed_at,
40 :failure_reason,
41 :created_at,
42 transfer_ids: [],
43 settled_count: 0,
44 failed_count: 0,
45 exception_count: 0,
46 total_amount: 0,
47 metadata: %{}
48 ]
49
50 @type status :: :pending | :running | :completed | :partial | :failed
51
52 @type t :: %__MODULE__{
53 batch_id: String.t(),
54 window_start: DateTime.t(),
55 window_end: DateTime.t(),
56 status: status(),
57 transfer_ids: [String.t()],
58 settled_count: non_neg_integer(),
59 failed_count: non_neg_integer(),
60 exception_count: non_neg_integer(),
61 total_amount: non_neg_integer(),
62 currency: String.t() | nil,
63 correlation_id: String.t() | nil,
64 started_at: DateTime.t() | nil,
65 completed_at: DateTime.t() | nil,
66 failure_reason: String.t() | nil,
67 created_at: DateTime.t(),
68 metadata: map()
69 }
70
71 @doc "Creates a new pending settlement batch."
72 @spec new(
73 window_start :: DateTime.t(),
74 window_end :: DateTime.t(),
75 transfer_ids :: [String.t()],
76 opts :: keyword()
77 ) :: t()
78 16 def new(window_start, window_end, transfer_ids, opts \\ []) do
79 24 %__MODULE__{
80 batch_id: TypedId.generate("stl"),
81 window_start: window_start,
82 window_end: window_end,
83 status: :pending,
84 transfer_ids: transfer_ids,
85 total_amount: Keyword.get(opts, :total_amount, 0),
86 currency: Keyword.get(opts, :currency),
87 correlation_id: Keyword.get(opts, :correlation_id),
88 created_at: DateTime.utc_now(),
89 metadata: Keyword.get(opts, :metadata, %{})
90 }
91 end
92
93 @doc "Transitions a pending batch to running. Returns `{:ok, batch}` or `{:error, :invalid_transition}`."
94 @spec start(t()) :: {:ok, t()} | {:error, :invalid_transition}
95 14 def start(%__MODULE__{status: :pending} = batch) do
96 {:ok, %{batch | status: :running, started_at: DateTime.utc_now()}}
97 end
98 1 def start(%__MODULE__{}), do: {:error, :invalid_transition}
99
100 @doc "Completes a running batch with settled/failed counts."
101 @spec complete(t(), opts :: keyword()) :: {:ok, t()} | {:error, :invalid_transition}
102 3 def complete(batch, opts \\ [])
103 def complete(%__MODULE__{status: :running} = batch, opts) do
104 9 settled = Keyword.get(opts, :settled_count, length(batch.transfer_ids))
105 9 failed = Keyword.get(opts, :failed_count, 0)
106 9 exceptions = Keyword.get(opts, :exception_count, 0)
107
108 9 status = if failed > 0 or exceptions > 0, do: :partial, else: :completed
109
110 {:ok, %{batch |
111 status: status,
112 settled_count: settled,
113 failed_count: failed,
114 exception_count: exceptions,
115 completed_at: DateTime.utc_now()
116 }}
117 end
118 1 def complete(%__MODULE__{}, _opts), do: {:error, :invalid_transition}
119
120 @doc "Fails a running batch with a reason."
121 @spec fail(t(), reason :: String.t()) :: {:ok, t()} | {:error, :invalid_transition}
122 2 def fail(%__MODULE__{status: :running} = batch, reason) do
123 {:ok, %{batch | status: :failed, failure_reason: reason, completed_at: DateTime.utc_now()}}
124 end
125 1 def fail(%__MODULE__{}, _reason), do: {:error, :invalid_transition}
126
127 @doc "Returns true if the batch is in a terminal state."
128 @spec terminal?(t()) :: boolean()
129 4 def terminal?(%__MODULE__{status: status}), do: status in @terminal_states
130
131
:-(
def terminal_states, do: @terminal_states
132 end
Line Hits Source