| 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 |