cover/Elixir.WalletRisk.Commands.EscalateCase.html

1 defmodule WalletRisk.Commands.EscalateCase do
2 @moduledoc """
3 Open a RiskCase escalation for a user whose profile exceeds the review threshold.
4
5 Steps:
6 1. Fetch or validate RiskProfile for user_id.
7 2. Build RiskCase with score_at_creation.
8 3. Store RiskCase in RiskCaseStore.
9 4. Optionally assign to an investigator.
10 5. Optionally escalate to compliance if score exceeds block threshold.
11 6. Emit RiskEscalated event.
12 7. Emit audit event.
13
14 Returns `{:ok, RiskCase.t()}` or `{:error, reason}`.
15 """
16
17 alias WalletRisk.{RiskCase, RiskCaseStore, RiskProfileStore, ScoringEngine}
18 alias WalletRisk.Events.RiskEscalated
19 alias WalletObservability.AuditEvent
20
21 @spec execute(user_id :: String.t(), opts :: keyword()) ::
22 {:ok, RiskCase.t()} | {:error, term()}
23 9 def execute(user_id, opts \\ []) do
24 6 correlation_id =
25 Keyword.get(opts, :correlation_id, WalletSharedKernel.Correlation.new_correlation_id())
26
27 6 with {:ok, profile} <- get_or_error_profile(user_id) do
28 6 risk_case = RiskCase.new(user_id, profile.profile_id, profile.score,
29 correlation_id: correlation_id,
30 metadata: Keyword.get(opts, :metadata, %{})
31 )
32
33 6 risk_case =
34 if investigator = Keyword.get(opts, :investigator_id) do
35 1 {:ok, updated} = RiskCase.assign(risk_case, investigator)
36 1 updated
37 else
38 5 risk_case
39 end
40
41 6 risk_case =
42 6 if ScoringEngine.exceeds_block_threshold?(profile.score) and
43 1 Keyword.get(opts, :auto_escalate_to_compliance, false) do
44 1 reason = Keyword.get(opts, :escalation_reason, "score_exceeded_block_threshold")
45 1 {:ok, escalated} = RiskCase.escalate_to_compliance(risk_case, reason)
46 1 escalated
47 else
48 5 risk_case
49 end
50
51 6 with :ok <- RiskCaseStore.store(risk_case) do
52 6 RiskProfileStore.upsert_for_user(user_id, fn p ->
53 6 WalletRisk.RiskProfile.add_case(p, risk_case.case_id)
54 end, [])
55
56 6 emit_event(RiskEscalated.build(risk_case.case_id,
57 user_id: user_id,
58 6 profile_id: profile.profile_id,
59 6 score_at_creation: risk_case.score_at_creation,
60 6 escalation_reason: risk_case.escalation_reason,
61 correlation_id: correlation_id
62 ))
63 6 emit_audit(risk_case.case_id, user_id, correlation_id, :success)
64 {:ok, risk_case}
65 end
66 end
67 rescue
68
:-(
error -> {:error, error}
69 end
70
71 defp get_or_error_profile(user_id) do
72 6 case WalletRisk.RiskProfileStore.get_by_user(user_id) do
73 1 {:ok, p} -> {:ok, p}
74 {:error, :not_found} ->
75 # Create a baseline profile if none exists
76 5 profile = WalletRisk.RiskProfile.new(user_id)
77 5 WalletRisk.RiskProfileStore.store(profile)
78 {:ok, profile}
79 end
80 end
81
82 6 defp emit_event(event) do
83 6 pubsub = Application.get_env(:wallet_risk, :pubsub, WalletWeb.PubSub)
84 6 apply(Phoenix.PubSub, :broadcast, [pubsub, "wallet_risk:events", {:domain_event, event}])
85 rescue
86
:-(
_ -> :ok
87 end
88
89 6 defp emit_audit(case_id, user_id, correlation_id, outcome) do
90 6 audit = AuditEvent.build(:risk, "risk_case_escalated", "risk_case", case_id, outcome,
91 correlation_id: correlation_id,
92 metadata: %{user_id: user_id}
93 )
94 6 :telemetry.execute([:wallet_risk, :audit], %{}, audit)
95 rescue
96
:-(
_ -> :ok
97 end
98 end
Line Hits Source