8000 feat(notifications): RSVP emails (E1, E2, E3) — closes #116 by mwoods79 · Pull Request #132 · huddlz-hq/huddlz · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 3 additions & 22 deletions lib/huddlz/communities/group_member/changes/notify_joined.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,10 @@ defmodule Huddlz.Communities.GroupMember.Changes.NotifyJoined do

use Ash.Resource.Change

require Ash.Query

alias Huddlz.Accounts.User
alias Huddlz.Communities.Group
alias Huddlz.Communities.GroupMember
alias Huddlz.Notifications
alias Huddlz.Communities.Huddl.Changes.RecipientHelpers

@impl true
def change(changeset, _opts, _context) do
Expand All @@ -34,25 +32,8 @@ defmodule Huddlz.Communities.GroupMember.Changes.NotifyJoined do
}

group_id
|> recipient_user_ids(joiner_id)
|> Enum.each(&deliver_to(&1, payload))
end
end

defp deliver_to(user_id, payload) do
case Ash.get(User, user_id, authorize?: false) do
{:ok, recipient} -> Notifications.deliver_async(recipient, :group_member_joined, payload)
_ -> :noop
|> RecipientHelpers.group_organizer_user_ids(exclude: joiner_id)
|> RecipientHelpers.deliver_each(:group_member_joined, payload)
end
end

defp recipient_user_ids(group_id, joiner_id) do
GroupMember
|> Ash.Query.filter(group_id == ^group_id and role in [:owner, :organizer])
|> Ash.Query.select([:user_id])
|> Ash.read!(authorize?: false)
|> Enum.map(& &1.user_id)
|> Enum.uniq()
|> Enum.reject(&(&1 == joiner_id))
end
end
3 changes: 3 additions & 0 deletions lib/huddlz/communities/huddl.ex
Original file line number Diff line number Diff line change
Expand Up @@ -291,13 +291,16 @@ defmodule Huddlz.Communities.Huddl do
require_atomic? false

change Huddlz.Communities.Huddl.Changes.Rsvp
change Huddlz.Communities.Huddl.Changes.NotifyRsvpReceived
change Huddlz.Communities.Huddl.Changes.NotifyRsvpConfirmation
end

update :cancel_rsvp do
description "Cancel RSVP to this huddl as the current actor"
require_atomic? false

change Huddlz.Communities.Huddl.Changes.CancelRsvp
change Huddlz.Communities.Huddl.Changes.NotifyRsvpCancelled
end

read :due_for_24h_reminder do
Expand Down
2 changes: 1 addition & 1 deletion lib/huddlz/communities/huddl/changes/cancel_rsvp.ex
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ defmodule Huddlz.Communities.Huddl.Changes.CancelRsvp do

{:ok, attendee} ->
Ash.destroy!(attendee, authorize?: false)
changeset
Ash.Changeset.put_context(changeset, :rsvp_cancelled, true)

{:error, error} ->
Ash.Changeset.add_error(changeset, error)
Expand Down
9 changes: 1 addition & 8 deletions lib/huddlz/communities/huddl/changes/notify_cancelled.ex
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,7 @@ defmodule Huddlz.Communities.Huddl.Changes.NotifyCancelled do

use Ash.Resource.Change

alias Huddlz.Accounts.User
alias Huddlz.Communities.Huddl.Changes.RecipientHelpers
alias Huddlz.Notifications

@impl true
def change(changeset, _opts, _context) do
Expand Down Expand Up @@ -45,12 +43,7 @@ defmodule Huddlz.Communities.Huddl.Changes.NotifyCancelled do
recipients = cs.context[:huddl_cancelled_recipients] || []
payload = cs.context[:huddl_cancelled_payload] || %{}

for user_id <- recipients do
case Ash.get(User, user_id, authorize?: false) do
{:ok, user} -> Notifications.deliver_async(user, :huddl_cancelled, payload)
_ -> :noop
end
end
RecipientHelpers.deliver_each(recipients, :huddl_cancelled, payload)

{:ok, huddl}
end
Expand Down
15 changes: 2 additions & 13 deletions lib/huddlz/communities/huddl/changes/notify_meaningful_update.ex
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,8 @@ defmodule Huddlz.Communities.Huddl.Changes.NotifyMeaningfulUpdate do

require Ash.Query

alias Huddlz.Accounts.User
alias Huddlz.Communities.Huddl
alias Huddlz.Communities.Huddl.Changes.RecipientHelpers
alias Huddlz.Notifications

@meaningful_attrs [:title, :starts_at, :ends_at, :physical_location, :virtual_link]

Expand Down Expand Up @@ -60,7 +58,7 @@ defmodule Huddlz.Communities.Huddl.Changes.NotifyMeaningfulUpdate do

payload = base_payload(huddl, changed_fields)

deliver_each(recipients, :huddl_updated, payload)
RecipientHelpers.deliver_each(recipients, :huddl_updated, payload)

{:ok, huddl}
end
Expand All @@ -79,7 +77,7 @@ defmodule Huddlz.Communities.Huddl.Changes.NotifyMeaningfulUpdate do

payload = base_payload(next, changed_fields)

deliver_each(recipients, :huddl_series_updated, payload)
RecipientHelpers.deliver_each(recipients, :huddl_series_updated, payload)

{:ok, huddl}
end
Expand All @@ -105,13 +103,4 @@ defmodule Huddlz.Communities.Huddl.Changes.NotifyMeaningfulUpdate do
"changed_fields" => Enum.map(changed_fields, &Atom.to_string/1)
}
end

defp deliver_each(user_ids, trigger, payload) do
for user_id <- user_ids do
case Ash.get(User, user_id, authorize?: false) do
{:ok, user} -> Notifications.deliver_async(user, trigger, payload)
_ -> :noop
end
end
end
end
9 changes: 1 addition & 8 deletions lib/huddlz/communities/huddl/changes/notify_new_in_group.ex
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,8 @@ defmodule Huddlz.Communities.Huddl.Changes.NotifyNewInGroup do

require Ash.Query

alias Huddlz.Accounts.User
alias Huddlz.Communities.GroupMember
alias Huddlz.Communities.Huddl.Changes.RecipientHelpers
alias Huddlz.Notifications

@impl true
def change(changeset, _opts, _context) do
Expand Down Expand Up @@ -56,12 +54,7 @@ defmodule Huddlz.Communities.Huddl.Changes.NotifyNewInGroup do
"group_slug" => to_string(huddl.group.slug)
}

for user_id <- user_ids do
case Ash.get(User, user_id, authorize?: false) do
{:ok, user} -> Notifications.deliver_async(user, :huddl_new, payload)
_ -> :noop
end
end
RecipientHelpers.deliver_each(user_ids, :huddl_new, payload)

{:ok, huddl}
end
Expand Down
46 changes: 46 additions & 0 deletions lib/huddlz/communities/huddl/changes/notify_rsvp_cancelled.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
defmodule Huddlz.Communities.Huddl.Changes.NotifyRsvpCancelled do
@moduledoc """
Enqueues E2 (rsvp_cancelled) emails when a user cancels their RSVP
to a huddl. Sent to the group's owner and organizers, deduplicated,
with the actor (the rsvper) excluded.

No-ops when there was no actual RSVP to cancel: `Huddl.Changes.CancelRsvp`
only sets `cs.context[:rsvp_cancelled]` on the destroy path. If no
attendee row existed, the action still succeeds but no email fires.
"""

use Ash.Resource.Change

alias Huddlz.Communities.Huddl.Changes.RecipientHelpers

@impl true
def change(changeset, _opts, _context) do
Ash.Changeset.after_action(changeset, &notify/2)
end

defp notify(cs, huddl) do
with true <- cs.context[:rsvp_cancelled] == true,
%{id: _} = actor <- cs.context[:private][:actor] do
deliver(huddl, actor)
{:ok, huddl}
else
_ -> {:ok, huddl}
end
end

defp deliver(huddl, actor) do
huddl = Ash.load!(huddl, [:group], authorize?: false)

payload = %{
"huddl_id" => huddl.id,
"huddl_title" => to_string(huddl.title),
"group_name" => to_string(huddl.group.name),
"group_slug" => to_string(huddl.group.slug),
"rsvper_display_name" => to_string(actor.display_name)
}

huddl.group_id
|> RecipientHelpers.group_organizer_user_ids(exclude: actor.id)
|> RecipientHelpers.deliver_each(:rsvp_cancelled, payload)
end
end
30 changes: 30 additions & 0 deletions lib/huddlz/communities/huddl/changes/notify_rsvp_confirmation.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
defmodule Huddlz.Communities.Huddl.Changes.NotifyRsvpConfirmation do
@moduledoc """
Enqueues E3 (rsvp_confirmation) email when a user RSVPs to a huddl.
Sent to the actor (the rsvper) themselves, not to organizers — that's
E1's job.

No-ops on duplicate RSVPs: `Huddl.Changes.Rsvp` only sets
`cs.context[:rsvp_created]` on the create path. If the attendee row
already existed, the action still succeeds but no email fires.
"""

use Ash.Resource.Change

alias Huddlz.Notifications

@impl true
def change(changeset, _opts, _context) do
Ash.Changeset.after_action(changeset, &notify/2)
end

defp notify(cs, huddl) do
with true <- cs.context[:rsvp_created] == true,
%{id: _} = actor <- cs.context[:private][:actor] do
Notifications.deliver_async(actor, :rsvp_confirmation, %{"huddl_id" => huddl.id})
{:ok, huddl}
else
_ -> {:ok, huddl}
end
end
end
46 changes: 46 additions & 0 deletions lib/huddlz/communities/huddl/changes/notify_rsvp_received.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
defmodule Huddlz.Communities.Huddl.Changes.NotifyRsvpReceived do
@moduledoc """
Enqueues E1 (rsvp_received) emails when a user RSVPs to a huddl.
Sent to the group's owner and organizers, deduplicated, with the
actor (the rsvper) excluded.

No-ops on duplicate RSVPs: `Huddl.Changes.Rsvp` only sets
`cs.context[:rsvp_created]` on the create path. If the attendee row
already existed, the action still succeeds but no email fires.
"""

use Ash.Resource.Change

alias Huddlz.Communities.Huddl.Changes.RecipientHelpers

@impl true
def change(changeset, _opts, _context) do
Ash.Changeset.after_action(changeset, &notify/2)
end

defp notify(cs, huddl) do
with true <- cs.context[:rsvp_created] == true,
%{id: _} = actor <- cs.context[:private][:actor] do
deliver(huddl, actor)
{:ok, huddl}
else
_ -> {:ok, huddl}
end
end

defp deliver(huddl, actor) do
huddl = Ash.load!(huddl, [:group], authorize?: false)

payload = %{
"huddl_id" => huddl.id,
"huddl_title" => to_string(huddl.title),
"group_name" => to_string(huddl.group.name),
"group_slug" => to_string(huddl.group.slug),
"rsvper_display_name" => to_string(actor.display_name)
}

huddl.group_id
|> RecipientHelpers.group_organizer_user_ids(exclude: actor.id)
|> RecipientHelpers.deliver_each(:rsvp_received, payload)
end
end
38 changes: 38 additions & 0 deletions lib/huddlz/communities/huddl/changes/recipient_helpers.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ defmodule Huddlz.Communities.Huddl.Changes.RecipientHelpers do

require Ash.Query

alias Huddlz.Accounts.User
alias Huddlz.Communities.GroupMember
alias Huddlz.Communities.HuddlAttendee
alias Huddlz.Notifications

@doc """
Returns the user_ids of every RSVP on the given huddl, optionally
Expand All @@ -28,6 +31,24 @@ defmodule Huddlz.Communities.Huddl.Changes.RecipientHelpers do
|> Enum.reject(&(&1 == actor_id))
end

@doc """
Returns the user_ids of every owner and organizer of the given group,
deduplicated, optionally excluding the actor. Used by the E1/E2 RSVP
fanout notifiers.
"""
@spec group_organizer_user_ids(Ecto.UUID.t(), keyword()) :: [Ecto.UUID.t()]
def group_organizer_user_ids(group_id, opts \\ []) do
actor_id = Keyword.get(opts, :exclude)

GroupMember
|> Ash.Query.filter(group_id == ^group_id and role in [:owner, :organizer])
|> Ash.Query.select([:user_id])
|> Ash.read!(authorize?: false)
|> Enum.map(& &1.user_id)
|> Enum.uniq()
|> Enum.reject(&(&1 == actor_id))
end

@doc """
Pull the actor id out of an Ash.Changeset's private context. Returns
`nil` when no actor is present (e.g. system-driven actions).
Expand All @@ -39,4 +60,21 @@ defmodule Huddlz.Communities.Huddl.Changes.RecipientHelpers do
_ -> nil
end
end

@doc """
Fan a notification trigger out to a list of user_ids, fetching each
user with `authorize?: false` and skipping any that no longer exist
(e.g. raced deletion). Used by the C/E-series fanout notifiers.
"""
@spec deliver_each([Ecto.UUID.t()], atom(), map()) :: :ok
def deliver_each(user_ids, trigger, payload) do
for user_id <- user_ids do
case Ash.get(User, user_id, authorize?: false) do
{:ok, user} -> Notifications.deliver_async(user, trigger, payload)
_ -> :noop
end
end

:ok
end
end
2 changes: 1 addition & 1 deletion lib/huddlz/communities/huddl/changes/rsvp.ex
10BC8
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ defmodule Huddlz.Communities.Huddl.Changes.Rsvp do
Ash.Changeset.add_error(cs, "This huddl is full")
else
create_rsvp!(huddl.id, user_id)
cs
Ash.Changeset.put_context(cs, :rsvp_created, true)
end
end

Expand Down
9 changes: 1 addition & 8 deletions lib/huddlz/communities/huddl/changes/send_reminder.ex
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,7 @@ defmodule Huddlz.Communities.Huddl.Changes.SendReminder do

use Ash.Resource.Change

alias Huddlz.Accounts.User
alias Huddlz.Communities.Huddl.Changes.RecipientHelpers
alias Huddlz.Notifications

@impl true
def change(changeset, opts, _context) do
Expand All @@ -34,12 +32,7 @@ defmodule Huddlz.Communities.Huddl.Changes.SendReminder do

payload = %{"huddl_id" => huddl.id}

for user_id <- user_ids do
case Ash.get(User, user_id, authorize?: false) do
{:ok, user} -> Notifications.deliver_async(user, trigger, payload)
_ -> :noop
end
end
RecipientHelpers.deliver_each(user_ids, trigger, payload)

{:ok, huddl}
end
Expand Down
3 changes: 2 additions & 1 deletion lib/huddlz/notifications/senders/group_archived.ex
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ defmodule Huddlz.Notifications.Senders.GroupArchived do
import Swoosh.Email

alias Huddlz.Mailer
alias Huddlz.Notifications.Senders.HeaderSafe
alias Huddlz.Notifications.Senders.HtmlEscape

@impl true
Expand All @@ -27,7 +28,7 @@ defmodule Huddlz.Notifications.Senders.GroupArchived do
new()
|> from(Mailer.from())
|> to(to_string(user.email))
|> subject("#{group_name(payload)} has been deleted")
|> subject(HeaderSafe.safe("#{group_name(payload)} has been deleted"))
|> html_body("""
<p>Hi #{safe_name},</p>

Expand Down
Loading
Loading
0