cover/Elixir.WalletIntegrations.IntegrationException.html

1 defmodule WalletIntegrations.IntegrationException do
2 @moduledoc """
3 Reconciliation exception record for provider-wallet state mismatches (ADR 0008).
4
5 Created when:
6 - Provider returns unknown outcome.
7 - Callback validation fails irrecoverably.
8 - Provider state does not match wallet state during reconciliation.
9
10 Links: provider_reference → transfer_id for traceability.
11
12 States:
13 - `:open` — newly detected; needs investigation.
14 - `:investigating` — under review.
15 - `:escalated` — escalated for manual/ops resolution.
16 - `:resolved` — investigation complete; resolution recorded.
17 """
18
19 alias WalletSharedKernel.TypedId
20
21 @terminal_states [:resolved]
22 @exception_types [:mismatch, :timeout, :unknown_outcome, :callback_failure, :duplicate_charge]
23
24 @enforce_keys [:exception_id, :type, :provider, :status, :detected_at]
25
:-(
defstruct [
26 :exception_id,
27 :type,
28 :provider,
29 :provider_reference,
30 :transfer_id,
31 :payment_request_id,
32 :status,
33 :description,
34 :resolution,
35 :correlation_id,
36 :detected_at,
37 :resolved_at,
38 metadata: %{}
39 ]
40
41 @type exception_type :: :mismatch | :timeout | :unknown_outcome | :callback_failure | :duplicate_charge
42 @type status :: :open | :investigating | :escalated | :resolved
43
44 @type t :: %__MODULE__{
45 exception_id: String.t(),
46 type: exception_type(),
47 provider: atom(),
48 provider_reference: String.t() | nil,
49 transfer_id: String.t() | nil,
50 payment_request_id: String.t() | nil,
51 status: status(),
52 description: String.t() | nil,
53 resolution: String.t() | nil,
54 correlation_id: String.t() | nil,
55 detected_at: DateTime.t(),
56 resolved_at: DateTime.t() | nil,
57 metadata: map()
58 }
59
60 @doc "Build a new integration exception record."
61 @spec new(type :: exception_type(), provider :: atom(), opts :: keyword()) :: t()
62 18 def new(type, provider, opts \\ []) when type in @exception_types do
63 29 %__MODULE__{
64 exception_id: TypedId.generate("iex"),
65 type: type,
66 provider: provider,
67 provider_reference: Keyword.get(opts, :provider_reference),
68 transfer_id: Keyword.get(opts, :transfer_id),
69 payment_request_id: Keyword.get(opts, :payment_request_id),
70 status: :open,
71 description: Keyword.get(opts, :description),
72 correlation_id: Keyword.get(opts, :correlation_id),
73 detected_at: DateTime.utc_now(),
74 metadata: Keyword.get(opts, :metadata, %{})
75 }
76 end
77
78 @spec investigate(t(), note :: String.t()) :: {:ok, t()} | {:error, :invalid_transition}
79
:-(
def investigate(ex, note \\ "")
80 5 def investigate(%__MODULE__{status: s} = ex, note) when s in [:open, :escalated] do
81 {:ok, %{ex | status: :investigating, description: note}}
82 end
83
:-(
def investigate(%__MODULE__{}, _note), do: {:error, :invalid_transition}
84
85 @spec escalate(t()) :: {:ok, t()} | {:error, :invalid_transition}
86 2 def escalate(%__MODULE__{status: s} = ex) when s in [:open, :investigating] do
87 {:ok, %{ex | status: :escalated}}
88 end
89
:-(
def escalate(%__MODULE__{}), do: {:error, :invalid_transition}
90
91 @spec resolve(t(), resolution :: String.t()) :: {:ok, t()} | {:error, :invalid_transition | :already_resolved}
92 1 def resolve(%__MODULE__{status: :resolved}, _), do: {:error, :already_resolved}
93 7 def resolve(%__MODULE__{status: s} = ex, resolution) when s in [:open, :investigating, :escalated] do
94 {:ok, %{ex | status: :resolved, resolution: resolution, resolved_at: DateTime.utc_now()}}
95 end
96
:-(
def resolve(%__MODULE__{}), do: {:error, :invalid_transition}
97
98 @spec terminal?(t()) :: boolean()
99 3 def terminal?(%__MODULE__{status: s}), do: s in @terminal_states
100
101 @spec terminal_states() :: [atom()]
102
:-(
def terminal_states, do: @terminal_states
103 end
Line Hits Source