cover/Elixir.WalletRisk.Commands.FlagRiskSignal.html

1 defmodule WalletRisk.Commands.FlagRiskSignal do
2 @moduledoc """
3 Flag a new risk signal for a user and update their risk profile with the new score.
4
5 Steps:
6 1. Build RiskSignal with score_contribution.
7 2. Store signal in RiskSignalStore.
8 3. Upsert RiskProfile for user, applying accumulated score.
9 4. Emit RiskScored event.
10 5. Emit audit event.
11
12 Returns `{:ok, %{signal: RiskSignal.t(), profile: RiskProfile.t(), score_result: map()}}`.
13 """
14
15 alias WalletRisk.{RiskSignal, RiskSignalStore, RiskProfileStore, RiskProfile, ScoringEngine}
16 alias WalletRisk.Events.RiskScored
17 alias WalletObservability.AuditEvent
18
19 @spec execute(user_id :: String.t(), signal_type :: atom(), opts :: keyword()) ::
20 {:ok, map()} | {:error, term()}
21 11 def execute(user_id, signal_type, opts \\ []) do
22 11 correlation_id =
23 Keyword.get(opts, :correlation_id, WalletSharedKernel.Correlation.new_correlation_id())
24
25 11 signal = RiskSignal.new(user_id, signal_type,
26 transfer_id: Keyword.get(opts, :transfer_id),
27 severity: Keyword.get(opts, :severity, :medium),
28 score_contribution: Keyword.get(opts, :score_contribution),
29 description: Keyword.get(opts, :description),
30 source: Keyword.get(opts, :source, :rule_engine),
31 correlation_id: correlation_id,
32 metadata: Keyword.get(opts, :metadata, %{})
33 )
34
35 11 with :ok <- RiskSignalStore.store(signal),
36 11 {:ok, profile} <- RiskProfileStore.upsert_for_user(user_id, fn p ->
37 # Recompute score from all stored signals + this new one
38 11 existing_signals = RiskSignalStore.list_by_user(user_id)
39 11 result = ScoringEngine.compute(existing_signals)
40 p
41 9 |> RiskProfile.apply_score(result.score)
42 9 |> RiskProfile.add_signal(signal.signal_id)
43 end, correlation_id: correlation_id) do
44 9 signals = RiskSignalStore.list_by_user(user_id)
45 9 score_result = ScoringEngine.compute(signals)
46
47 9 emit_event(RiskScored.build(profile.profile_id,
48 user_id: user_id,
49 9 score: score_result.score,
50 9 band: score_result.band,
51 9 signal_count: score_result.signal_count,
52 correlation_id: correlation_id
53 ))
54 9 emit_audit(signal.signal_id, user_id, correlation_id, score_result.score)
55
56 {:ok, %{signal: signal, profile: profile, score_result: score_result}}
57 end
58 rescue
59
:-(
error -> {:error, error}
60 end
61
62 9 defp emit_event(event) do
63 9 pubsub = Application.get_env(:wallet_risk, :pubsub, WalletWeb.PubSub)
64 9 apply(Phoenix.PubSub, :broadcast, [pubsub, "wallet_risk:events", {:domain_event, event}])
65 rescue
66
:-(
_ -> :ok
67 end
68
69 9 defp emit_audit(signal_id, user_id, correlation_id, score) do
70 9 audit = AuditEvent.build(:risk, "risk_signal_flagged", "risk_signal", signal_id, :success,
71 correlation_id: correlation_id,
72 metadata: %{user_id: user_id, computed_score: score}
73 )
74 9 :telemetry.execute([:wallet_risk, :audit], %{}, audit)
75 rescue
76
:-(
_ -> :ok
77 end
78 end
Line Hits Source