Skip to content

Commit 62c2af2

Browse files
committed
Refactor Notifier module into Notification namespace
The monolithic Notifier module was becoming unwieldy with 15+ notification types all in one file. This refactoring splits each notification into its own class under app/models/notification/, following the PORO pattern. Each notification class inherits from Notification::Base which provides: - notify/notify_later class methods for sync/async execution - mail_template DSL to declare the associated MailTemplate - mail_template_active? for efficient existence check - deliver_later helper for sending emails The scheduled jobs now enqueue each notification individually, providing better parallelization and failure isolation. Added MailTemplate.active_template? for efficient EXISTS queries when only checking template availability.
1 parent 7cbb4d3 commit 62c2af2

35 files changed

+1129
-792
lines changed

app/jobs/notification_job.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# frozen_string_literal: true
2+
3+
class NotificationJob < ApplicationJob
4+
queue_as :low
5+
6+
def perform(notification_class_name)
7+
notification_class_name.constantize.notify
8+
end
9+
end

app/jobs/scheduled/notifier_daily_job.rb

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,25 @@
22

33
module Scheduled
44
class NotifierDailyJob < BaseJob
5+
NOTIFICATIONS = [
6+
Notification::InvoiceOverdueNotice,
7+
Notification::AdminDeliveryList,
8+
Notification::AdminMembershipsRenewalPending,
9+
Notification::MembershipInitialBasket,
10+
Notification::MembershipFinalBasket,
11+
Notification::MembershipFirstBasket,
12+
Notification::MembershipLastBasket,
13+
Notification::MembershipSecondLastTrialBasket,
14+
Notification::MembershipLastTrialBasket,
15+
Notification::MembershipRenewalReminder,
16+
Notification::ActivityParticipationReminder,
17+
Notification::ActivityParticipationValidated,
18+
Notification::ActivityParticipationRejected,
19+
Notification::BiddingRoundOpenedReminder
20+
].freeze
21+
522
def perform
6-
Notifier.send_all_daily
23+
NOTIFICATIONS.each(&:notify_later)
724
end
825
end
926
end

app/jobs/scheduled/notifier_hourly_job.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,12 @@
22

33
module Scheduled
44
class NotifierHourlyJob < BaseJob
5+
NOTIFICATIONS = [
6+
Notification::AdminNewActivityParticipation
7+
].freeze
8+
59
def perform
6-
Notifier.send_all_hourly
10+
NOTIFICATIONS.each(&:notify_later)
711
end
812
end
913
end

app/models/mail_template.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,10 @@ def self.active_template(title)
9393
active.find_by(title: title)
9494
end
9595

96+
def self.active_template?(title)
97+
active.exists?(title: title)
98+
end
99+
96100
def self.create_all!
97101
TITLES.each do |title|
98102
find_or_create_by!(title: title)
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# frozen_string_literal: true
2+
3+
class Notification::ActivityParticipationRejected < Notification::Base
4+
mail_template :activity_participation_rejected
5+
6+
def notify
7+
return unless Current.org.feature?("activity")
8+
return unless mail_template_active?
9+
10+
ActivityParticipationGroup.group(eligible_participations).each do |group|
11+
deliver_later(activity_participation_ids: group.ids)
12+
group.touch(:review_sent_at)
13+
end
14+
end
15+
16+
private
17+
18+
def eligible_participations
19+
ActivityParticipation
20+
.where(rejected_at: 3.days.ago..)
21+
.review_not_sent
22+
.includes(:activity, :member)
23+
.select(&:can_send_email?)
24+
end
25+
end
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# frozen_string_literal: true
2+
3+
class Notification::ActivityParticipationReminder < Notification::Base
4+
mail_template :activity_participation_reminder
5+
6+
def notify
7+
return unless Current.org.feature?("activity")
8+
9+
ActivityParticipationGroup.group(eligible_participations).each do |group|
10+
deliver_later(activity_participation_ids: group.ids)
11+
group.touch(:latest_reminder_sent_at)
12+
end
13+
end
14+
15+
private
16+
17+
def eligible_participations
18+
ActivityParticipation
19+
.future
20+
.includes(:activity, :member)
21+
.select(&:reminderable?)
22+
.select(&:can_send_email?)
23+
end
24+
end
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# frozen_string_literal: true
2+
3+
class Notification::ActivityParticipationValidated < Notification::Base
4+
mail_template :activity_participation_validated
5+
6+
def notify
7+
return unless Current.org.feature?("activity")
8+
return unless mail_template_active?
9+
10+
ActivityParticipationGroup.group(eligible_participations).each do |group|
11+
deliver_later(activity_participation_ids: group.ids)
12+
group.touch(:review_sent_at)
13+
end
14+
end
15+
16+
private
17+
18+
def eligible_participations
19+
ActivityParticipation
20+
.where(validated_at: 3.days.ago..)
21+
.review_not_sent
22+
.includes(:activity, :member)
23+
.select(&:can_send_email?)
24+
end
25+
end
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# frozen_string_literal: true
2+
3+
class Notification::AdminDeliveryList < Notification::Base
4+
def notify
5+
return unless next_delivery
6+
return unless Date.current == (next_delivery.date - 1.day)
7+
8+
notify_depots
9+
notify_admins
10+
end
11+
12+
private
13+
14+
def next_delivery
15+
@next_delivery ||= Delivery.next
16+
end
17+
18+
def notify_depots
19+
next_delivery.depots.select(&:emails?).each do |depot|
20+
AdminMailer.with(
21+
depot: depot,
22+
delivery: next_delivery
23+
).depot_delivery_list_email.deliver_later
24+
end
25+
end
26+
27+
def notify_admins
28+
Admin.notify!(:delivery_list, delivery: next_delivery)
29+
end
30+
end
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# frozen_string_literal: true
2+
3+
class Notification::AdminMembershipsRenewalPending < Notification::Base
4+
def notify
5+
return unless notify_today?
6+
return unless pending_memberships.any? || opened_memberships.any?
7+
8+
Admin.notify!(:memberships_renewal_pending,
9+
pending_memberships: pending_memberships.to_a,
10+
opened_memberships: opened_memberships.to_a,
11+
pending_action_url: memberships_url(renewal_state_eq: :renewal_pending),
12+
opened_action_url: memberships_url(renewal_state_eq: :renewal_opened),
13+
action_url: memberships_url)
14+
end
15+
16+
private
17+
18+
def notify_today?
19+
Current.fiscal_year.end_of_year == 10.days.from_now.to_date
20+
end
21+
22+
def pending_memberships
23+
@pending_memberships ||= Membership.current_year.renewal_state_eq(:renewal_pending)
24+
end
25+
26+
def opened_memberships
27+
@opened_memberships ||= Membership.current_year.renewal_state_eq(:renewal_opened)
28+
end
29+
30+
def memberships_url(**options)
31+
Rails
32+
.application
33+
.routes
34+
.url_helpers
35+
.memberships_url(
36+
q: { during_year: Current.fy_year }.merge(options),
37+
scope: :all,
38+
host: Current.org.admin_url)
39+
end
40+
end
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# frozen_string_literal: true
2+
3+
class Notification::AdminNewActivityParticipation < Notification::Base
4+
def notify
5+
return unless Current.org.feature?("activity")
6+
7+
ActivityParticipationGroup.group(eligible_participations).each do |group|
8+
notify_admins(group)
9+
group.touch(:admins_notified_at)
10+
end
11+
end
12+
13+
private
14+
15+
def eligible_participations
16+
ActivityParticipation
17+
.where(created_at: 1.day.ago.., admins_notified_at: nil)
18+
.includes(:activity, :member, :session)
19+
end
20+
21+
def notify_admins(group)
22+
attrs = {
23+
activity_participation_ids: group.ids,
24+
skip: group.session&.admin
25+
}
26+
Admin.notify!(:new_activity_participation, **attrs)
27+
Admin.notify!(:new_activity_participation_with_note, **attrs) if group.note?
28+
end
29+
end

0 commit comments

Comments
 (0)