Repository navigation
feat(notifications): huddl lifecycle and reminders (C1-C4, D1-D2) - #131
Merged
Merged
Conversation
Wire huddl_cancelled to Huddl.:destroy. The NotifyCancelled change captures attendee user_ids and the huddl's display fields in before_action (the HuddlAttendee rows cascade-delete with the huddl and the row itself disappears) and fans out notifications in after_action once the destroy commits. The actor (typically the owner or organizer cancelling) is excluded from the recipients. Promotes :destroy to a primary explicit action so the change can hang off it, replacing defaults [:destroy]. Also lays down test/huddlz/notifications/huddl_lifecycle_notifications_test.exs as the integration-test home for the C-series and D-series triggers, mirroring the shape of the existing group_membership_notifications_test.
Wire 24h and 1h reminders to the AshOban scheduler. Two new triggers on Huddl run a candidate read every 2 minutes and dispatch a per-record update action that stamps the corresponding sent-at column and fans out emails to current RSVPs. Schema: - Add reminder_24h_sent_at, reminder_1h_sent_at columns on huddlz (utc_datetime_usec, nullable). Migration generated via mix ash.codegen. Read actions due_for_24h_reminder / due_for_1h_reminder filter by starts_at within the window AND is_nil(reminder_*_sent_at), so already-sent huddlz drop out and the action is idempotent at the filter level. Both use keyset pagination as required by AshOban. Update actions send_24h_reminder / send_1h_reminder run the SendReminder change with kind: :h24 or :h1. The change stamps the column and enqueues DeliverWorker jobs per attendee — recipients resolved at fire time so RSVPs added after the huddl was created still get the reminder. Reminder dispatch policy authorizes always() since the scheduler runs without an actor. ResetReminderStamps attaches to :update and clears both columns whenever starts_at changes. That is the entire reschedule story: the cron picks the huddl up again at its new time. Cancellation is automatic — destroyed huddlz drop out of the read filter. Senders HuddlReminder24h and HuddlReminder1h are Activity (footer included) and attach an .ics calendar file via Notifications.ICS.event_for/1. Each sender re-fetches the huddl by id from the payload — documented exception to the stateless-sender rule, since the .ics requires the live struct and embedding binary in JSONB is awkward. The 1h sender leads with the virtual link prominently when one is set.
Wire huddl_new to Huddl.:create. The NotifyNewInGroup change runs in after_action once the huddl row exists, reads every member of the huddl's group, excludes the actor (the creator), and fans out a :huddl_new notification per member. Recipients without an active preference fall through to the registry default (true), so members get emailed unless they opted out via /profile/notifications. Sender HuddlNew is Activity (footer included), naming the group and the huddl in the subject and linking to the huddl page.
Wire huddl_updated to Huddl.:update. NotifyMeaningfulUpdate detects
changes to title, starts_at, ends_at, physical_location, or
virtual_link in before_action; if none of those are in the changeset
the change is a no-op. Otherwise after_action fans out :huddl_updated
to current RSVPs (excluding the actor) with a "changed_fields" list
the sender humanizes for the body ("the start time", "the location",
etc.). Cosmetic edits (description, thumbnail, max_attendees, etc.)
do not trigger a notification.
Sender HuddlUpdated is Activity (footer included).
Lifts a small RecipientHelpers module out of inline change-module
code now that two changes (NotifyCancelled and NotifyMeaningfulUpdate)
need to read RSVP user_ids and pull the actor id off the changeset.
SendReminder also adopts rsvp_user_ids/1 for symmetry.
Layer the recurring-series branch into NotifyMeaningfulUpdate. When the editor passes edit_type: "all", EditRecurringHuddlz regenerates every future instance in the series; we then resolve the *next* upcoming instance by huddl_template_id + smallest starts_at > now() and fan out :huddl_series_updated to its RSVPs. Subsequent instances rely on their own D1/D2 reminders to surface the new schedule, per docs/notifications.md C4. Sender HuddlSeriesUpdated is Activity (footer included), naming the next instance and explicitly noting that later instances will be covered by their own reminders. Also tightens NotifyNewInGroup to skip C1 when there is no actor on the changeset. RecurrenceHelper creates each child instance via a system-driven Ash.create with no actor, so without this gate the series-regeneration path would fan out a fresh "new huddl scheduled" email for every regenerated occurrence — exactly what C4 was added to avoid.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #115.
Adds the six remaining huddl-related email triggers from
docs/notifications.md:edit_type: "all").ics).ics)Summary
C3 wires
huddl_cancelledtoHuddl.:destroyviaNotifyCancelled. Captures attendees + display fields inbefore_action(the rows cascade-delete with the huddl) and fans out inafter_action. Promotes the previously-default:destroyto a primary explicit action. Mirrors the B-seriesNotifyArchivedpattern exactly.D1/D2 use AshOban cron triggers + two new sent-at columns on
huddlz(reminder_24h_sent_at,reminder_1h_sent_at). Read actions filter onstarts_at > now() and starts_at < from_now(N, :hour) and is_nil(reminder_*_sent_at)so the action is idempotent and rescheduled huddls are picked up automatically onceResetReminderStampsclears the column. Cancellation is free (destroyed huddls drop out of the read filter). The senders attach an.icscalendar event via the existingNotifications.ICS.event_for/1. The 1h sender leads with the virtual link prominently when one is set.C1 wires
huddl_newtoHuddl.:createviaNotifyNewInGroup. Skips when the create runs with no actor (system-driven path used byRecurrenceHelperfor child instances of a series — those don't need a fresh "new huddl" email per occurrence).C2 wires
huddl_updatedtoHuddl.:updateviaNotifyMeaningfulUpdate. Detects changes totitle,starts_at,ends_at,physical_location, orvirtual_link; cosmetic edits are silent. Lifts a smallHuddl.Changes.RecipientHelpersmodule out of inline code now that two changes need RSVP user-id lookup.C4 layers the recurring-series branch into
NotifyMeaningfulUpdate. AfterEditRecurringHuddlzregenerates the series, resolve the next-upcoming instance byhuddl_template_idand fan out:huddl_series_updatedto its RSVPs only.The trigger atoms (
:huddl_new,:huddl_updated,:huddl_cancelled,:huddl_series_updated,:huddl_reminder_24h,:huddl_reminder_1h) were already registered inHuddlz.Notifications.Triggersfrom the foundation work; this PR ships the senders and change modules at those module paths.Commits
feat(notifications): send C3 huddl_cancelled emailfeat(notifications): send D1/D2 huddl reminder emails on cronfeat(notifications): send C1 huddl_new emailfeat(notifications): send C2 huddl_updated emailfeat(notifications): send C4 huddl_series_updated emailEach commit lands its trigger end-to-end (sender + change module + sender unit test + integration test section) with
mix precommitclean.Test plan
mix precommitclean (981 tests, credo clean).icsattached for D1/D2.Oban.drain_queue(:notifications)withassert_email_sent+ actor exclusion + skip-when-no-meaningful-change.due_for_24h_reminderread filter; updatingstarts_atre-nils the column./dev/mailboxwalkthrough of all six flows.