cover/Elixir.WalletNotifications.Workers.NotificationWorker.html

1 defmodule WalletNotifications.Workers.NotificationWorker do
2 @moduledoc """
3 Async worker for dispatching a queued notification.
4
5 Implements `perform/1` compatible with `WalletSettlement.JobQueue` and Oban.
6
7 Expected args:
8 - `"notification_id"` — the notification to dispatch.
9 - `"correlation_id"` — propagated into events.
10
11 Delivery flow:
12 1. Load notification from store.
13 2. Check user preference for this channel — skip (not fail) if disabled.
14 3. Mark as :sending.
15 4. Dispatch via channel adapter (stub in Phase 5; real adapter in Phase 6).
16 5. Mark as :sent or :failed; update store.
17 6. Emit event and audit record.
18
19 Retry: up to `max_attempts` on notification; job re-queued on failure.
20 """
21
22 alias WalletNotifications.{Notification, NotificationStore, PreferenceStore}
23 alias WalletNotifications.Events.{NotificationSent, NotificationFailed}
24 alias WalletObservability.AuditEvent
25
26 @doc "Performs notification dispatch. Returns `:ok` or `{:error, reason}`."
27 @spec perform(args :: map()) :: :ok | {:error, term()}
28 def perform(%{"notification_id" => notif_id} = args) do
29 18 correlation_id = Map.get(args, "correlation_id", WalletSharedKernel.Correlation.new_correlation_id())
30
31 18 with {:ok, notif} <- NotificationStore.get(notif_id) do
32 # Check preference
33 18 case check_preference(notif) do
34 :suppressed ->
35 # Soft-suppress — not a failure, mark as sent to avoid infinite retries
36 2 {:ok, sent} = Notification.mark_sending(notif)
37 2 {:ok, sent} = Notification.mark_sent(sent)
38 2 NotificationStore.update(sent)
39 2 emit_event(NotificationSent.build(notif_id,
40 2 user_id: notif.user_id, channel: notif.channel,
41 suppressed: true, correlation_id: correlation_id))
42 :ok
43
44 :allowed ->
45 16 with {:ok, sending} <- Notification.mark_sending(notif),
46 16 :ok <- NotificationStore.update(sending) do
47 # Dispatch via channel adapter (Phase 5: stub always succeeds)
48 16 channel_mod = channel_adapter(notif.channel)
49 16 case apply(channel_mod, :deliver, [sending]) do
50 :ok ->
51 9 {:ok, sent} = Notification.mark_sent(sending)
52 9 NotificationStore.update(sent)
53 9 emit_event(NotificationSent.build(notif_id,
54 9 user_id: notif.user_id, channel: notif.channel,
55 correlation_id: correlation_id))
56 9 emit_audit(:notification_sent, notif_id, notif.user_id, correlation_id, :success)
57 :ok
58
59 {:error, reason} ->
60 7 {:ok, failed} = Notification.mark_failed(sending, inspect(reason))
61 7 NotificationStore.update(failed)
62 7 emit_event(NotificationFailed.build(notif_id,
63 7 user_id: notif.user_id, channel: notif.channel,
64 reason: inspect(reason), correlation_id: correlation_id))
65 7 emit_audit(:notification_failed, notif_id, notif.user_id, correlation_id, :failure)
66 {:error, reason}
67 end
68 end
69 end
70 end
71 end
72
73
:-(
def perform(_args), do: {:error, :missing_notification_id}
74
75 defp check_preference(notif) do
76 18 case PreferenceStore.get(notif.user_id, notif.channel) do
77 {:ok, pref} ->
78 3 if WalletNotifications.Preference.allowed?(pref, notif.type), do: :allowed, else: :suppressed
79 15 {:error, :not_found} ->
80 # No preference set — default allow
81 :allowed
82 end
83 end
84
85 defp channel_adapter(channel) do
86 16 Application.get_env(:wallet_notifications, :"#{channel}_adapter",
87 WalletNotifications.Adapters.StubAdapter)
88 end
89
90 18 defp emit_event(event) do
91 18 pubsub = Application.get_env(:wallet_notifications, :pubsub, WalletWeb.PubSub)
92 18 apply(Phoenix.PubSub, :broadcast, [pubsub, "wallet_notifications:events", {:domain_event, event}])
93 rescue
94
:-(
_ -> :ok
95 end
96
97 16 defp emit_audit(action, notif_id, user_id, correlation_id, outcome) do
98 16 audit = AuditEvent.build(:notifications, to_string(action), "notification", notif_id,
99 outcome, actor_id: user_id, correlation_id: correlation_id, metadata: %{})
100 16 :telemetry.execute([:wallet_notifications, :audit], %{}, audit)
101 rescue
102
:-(
_ -> :ok
103 end
104 end
Line Hits Source