Class: Seams::Generators::NotificationsGenerator

Inherits:
Rails::Generators::Base
  • Object
show all
Includes:
EjectAware, HostInjector
Defined in:
lib/generators/seams/notifications/notifications_generator.rb

Overview

Generates a canonical Notifications engine on top of the generic engine scaffold.

Notifications uses STI: a single Notifications::Notification base with three concrete subclasses under Strategies — InApp, Email, Sms — each implementing its own #dispatch!. The schedule lives in a jsonb column populated by ice_cube; next_delivery_at is the indexed cache the recurring sweeper reads from.

Run with: bin/rails generate seams:notifications rubocop:disable Metrics/ClassLength

Constant Summary collapse

ENGINE_NAME =
"notifications"
DEFAULT_CHANNELS =
%w[in_app email sms].freeze
BILLING_TEMPLATES =
%w[
  subscription_started
  subscription_updated
  subscription_canceled
  invoice_paid
  invoice_failed
  lifetime_granted
  lifetime_purchased
].freeze

Constants included from EjectAware

EjectAware::EJECT_HEADER_PREFIX

Instance Method Summary collapse

Methods included from EjectAware

#ejected?, #template_unless_ejected

Methods included from HostInjector

#host_inject_gem, #host_inject_include_in_application_controller, #host_inject_include_in_user, #host_inject_mount, #host_uninject_gem, #host_uninject_include, #host_uninject_mount, #routes_draw_anchor

Instance Method Details

#create_adaptersObject



74
75
76
77
78
79
80
81
82
83
84
# File 'lib/generators/seams/notifications/notifications_generator.rb', line 74

def create_adapters
  template_unless_ejected "lib/adapters/abstract.rb.tt",
                          engine_path("lib/notifications/adapters/abstract.rb")
  template_unless_ejected "lib/adapters/action_mailer.rb.tt",
                          engine_path("lib/notifications/adapters/action_mailer.rb")
  # SMS adapter only ships when the sms channel is enabled.
  return unless channels.include?("sms")

  template_unless_ejected "lib/adapters/null_sms.rb.tt",
                          engine_path("lib/notifications/adapters/null_sms.rb")
end

#create_base_engineObject



46
47
48
# File 'lib/generators/seams/notifications/notifications_generator.rb', line 46

def create_base_engine
  EngineGenerator.start([ENGINE_NAME], destination_root: destination_root)
end

#create_channel_and_stimulusObject



145
146
147
148
149
150
# File 'lib/generators/seams/notifications/notifications_generator.rb', line 145

def create_channel_and_stimulus
  template_unless_ejected "app/channels/notification_channel.rb.tt",
                          engine_path("app/channels/notifications/notification_channel.rb")
  template_unless_ejected "app/javascript/controllers/notification_bell_controller.js.tt",
                          engine_path("app/javascript/notifications/controllers/notification_bell_controller.js")
end

#create_concernObject



86
87
88
89
# File 'lib/generators/seams/notifications/notifications_generator.rb', line 86

def create_concern
  template_unless_ejected "lib/concerns/notifiable.rb.tt",
                          engine_path("lib/notifications/concerns/notifiable.rb")
end

#create_configurationObject



65
66
67
68
69
70
71
72
# File 'lib/generators/seams/notifications/notifications_generator.rb', line 65

def create_configuration
  template_unless_ejected "lib/configuration.rb.tt",
                          engine_path("lib/notifications/configuration.rb")
  template_unless_ejected "lib/type_registry.rb.tt",
                          engine_path("lib/notifications/type_registry.rb")
  # lib/notifications.rb stays framework-managed (root require file).
  template "lib/notifications.rb.tt", engine_path("lib/notifications.rb"), force: true
end

#create_controllersObject



131
132
133
134
135
136
# File 'lib/generators/seams/notifications/notifications_generator.rb', line 131

def create_controllers
  template_unless_ejected "app/controllers/notifications_controller.rb.tt",
                          engine_path("app/controllers/notifications/notifications_controller.rb")
  template_unless_ejected "app/controllers/preferences_controller.rb.tt",
                          engine_path("app/controllers/notifications/preferences_controller.rb")
end

#create_default_templatesObject



159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
# File 'lib/generators/seams/notifications/notifications_generator.rb', line 159

def create_default_templates
  # Each notification template ships in two formats — .text.erb
  # for SMS / plain-text email + .html.erb for HTML email and
  # in-app rendering. Hosts override either or both by dropping
  # files at app/views/notifications/templates/<name>.<format>.erb.
  %i[text html].each do |format|
    template_unless_ejected "app/views/templates/default.#{format}.erb.tt",
                            engine_path("app/views/notifications/templates/default.#{format}.erb")
    template_unless_ejected "app/views/templates/welcome.#{format}.erb.tt",
                            engine_path("app/views/notifications/templates/welcome.#{format}.erb")
    BILLING_TEMPLATES.each do |name|
      template_unless_ejected "app/views/templates/billing/#{name}.#{format}.erb.tt",
                              engine_path("app/views/notifications/templates/billing/#{name}.#{format}.erb")
    end
  end

  # Mailer layout — wraps every notification email so hosts get
  # consistent header/footer chrome without per-template repetition.
  template_unless_ejected "app/views/layouts/notifications/mailer.html.erb.tt",
                          engine_path("app/views/layouts/notifications/mailer.html.erb")
  template_unless_ejected "app/views/layouts/notifications/mailer.text.erb.tt",
                          engine_path("app/views/layouts/notifications/mailer.text.erb")
end

#create_dummy_appObject



204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
# File 'lib/generators/seams/notifications/notifications_generator.rb', line 204

def create_dummy_app
  Seams::Generators::DummyAppWriter.write!(
    engine_path: File.join(destination_root, "engines", ENGINE_NAME),
    engine_module: "Notifications",
    mount_at: "/notifications",
    schema: dummy_schema,
    host_user: dummy_host_user,
    host_user_path: "app/models/auth/identity.rb"
  )
  # Wire the runtime spec templates into the generator output —
  # they were orphaned in templates/ pre-Wave-12 (the integration
  # test silently skipped them).
  template "spec/runtime/boot_spec.rb.tt",
           engine_path("spec/runtime/notifications_boot_spec.rb")
  template "spec/runtime/schedule_round_trip_spec.rb.tt",
           engine_path("spec/runtime/notifications_schedule_round_trip_spec.rb")
  # The BillingSubscriber emits a deliberately-loud
  # `notifications.billing_subscriber.skip` warn line when a
  # billing event arrives without an `account_id` — that's the
  # only signal a host operator gets that notifications are
  # silently failing for that tenant. Spec it.
  template "spec/runtime/billing_subscriber_skip_spec.rb.tt",
           engine_path("spec/runtime/notifications_billing_subscriber_skip_spec.rb")
  # Phase 2B (3/3) — bell + ActionCable broadcast verification.
  return unless channels.include?("in_app")

  template "spec/runtime/bell_broadcast_spec.rb.tt",
           engine_path("spec/runtime/notifications_bell_broadcast_spec.rb")
end

#create_initializerObject



248
249
250
251
252
253
254
255
256
257
# File 'lib/generators/seams/notifications/notifications_generator.rb', line 248

def create_initializer
  # Host-side initializer — documents the configure-adapters
  # entry point AND the optional Notifiable concern include
  # patterns. Wave 9 dropped the auto-include into the host
  # User; hosts now opt in by uncommenting one of the patterns
  # below (or leave the concern uninvoked — the polymorphic
  # owner column doesn't require it).
  template "config/initializers/notifications.rb.tt",
           File.join(destination_root, "config/initializers/notifications.rb")
end

#create_jobsObject



113
114
115
116
117
118
119
120
121
122
# File 'lib/generators/seams/notifications/notifications_generator.rb', line 113

def create_jobs
  template_unless_ejected "app/jobs/application_job.rb.tt",
                          engine_path("app/jobs/notifications/application_job.rb")
  template_unless_ejected "app/jobs/send_due_notifications_job.rb.tt",
                          engine_path("app/jobs/notifications/send_due_notifications_job.rb")
  template_unless_ejected "app/jobs/send_notification_job.rb.tt",
                          engine_path("app/jobs/notifications/send_notification_job.rb")
  template_unless_ejected "app/jobs/create_notification_job.rb.tt",
                          engine_path("app/jobs/notifications/create_notification_job.rb")
end

#create_mailerObject



152
153
154
155
156
157
# File 'lib/generators/seams/notifications/notifications_generator.rb', line 152

def create_mailer
  template_unless_ejected "app/mailers/application_mailer.rb.tt",
                          engine_path("app/mailers/notifications/application_mailer.rb")
  template_unless_ejected "app/mailers/notification_mailer.rb.tt",
                          engine_path("app/mailers/notifications/notification_mailer.rb")
end

#create_migrationsObject



183
184
185
186
187
188
189
190
# File 'lib/generators/seams/notifications/notifications_generator.rb', line 183

def create_migrations
  template "db/migrate/create_notifications.rb.tt",
           engine_path("db/migrate/#{timestamp(0)}_create_notifications.rb")
  template "db/migrate/create_notification_preferences.rb.tt",
           engine_path("db/migrate/#{timestamp(1)}_create_notification_preferences.rb")
  template "db/migrate/create_notification_deliveries.rb.tt",
           engine_path("db/migrate/#{timestamp(2)}_create_notification_deliveries.rb")
end

#create_modelsObject



91
92
93
94
95
96
97
98
99
100
101
# File 'lib/generators/seams/notifications/notifications_generator.rb', line 91

def create_models
  template_unless_ejected "app/models/application_record.rb.tt",
                          engine_path("app/models/notifications/application_record.rb")
  template_unless_ejected "app/models/notification.rb.tt",
                          engine_path("app/models/notifications/notification.rb")
  template_unless_ejected "app/models/notification_preference.rb.tt",
                          engine_path("app/models/notifications/notification_preference.rb")
  template_unless_ejected "app/models/delivery.rb.tt",
                          engine_path("app/models/notifications/delivery.rb")
  create_strategy_models
end

#create_specsObject



192
193
194
195
196
197
198
199
200
201
202
# File 'lib/generators/seams/notifications/notifications_generator.rb', line 192

def create_specs
  # Phase 2B finish — coverage for the engine's three core models.
  template_unless_ejected "spec/factories/notifications.rb.tt",
                          engine_path("spec/factories/notifications.rb")
  template_unless_ejected "spec/models/notification_spec.rb.tt",
                          engine_path("spec/models/notifications/notification_spec.rb")
  template_unless_ejected "spec/models/delivery_spec.rb.tt",
                          engine_path("spec/models/notifications/delivery_spec.rb")
  template_unless_ejected "spec/models/notification_preference_spec.rb.tt",
                          engine_path("spec/models/notifications/notification_preference_spec.rb")
end

#create_strategy_modelsObject



103
104
105
106
107
108
109
110
111
# File 'lib/generators/seams/notifications/notifications_generator.rb', line 103

def create_strategy_models
  # STI strategy subclasses ship per --channels selection.
  # STRATEGY_CLASSES in the Notifiable concern is conditionally
  # rendered (in_app/email/sms) to match.
  channels.each do |channel|
    template_unless_ejected "app/models/strategies/#{channel}.rb.tt",
                            engine_path("app/models/notifications/strategies/#{channel}.rb")
  end
end

#create_subscriberObject



124
125
126
127
128
129
# File 'lib/generators/seams/notifications/notifications_generator.rb', line 124

def create_subscriber
  template_unless_ejected "app/subscribers/auth_subscriber.rb.tt",
                          engine_path("app/subscribers/notifications/auth_subscriber.rb")
  template_unless_ejected "app/subscribers/billing_subscriber.rb.tt",
                          engine_path("app/subscribers/notifications/billing_subscriber.rb")
end

#create_viewsObject



138
139
140
141
142
143
# File 'lib/generators/seams/notifications/notifications_generator.rb', line 138

def create_views
  template_unless_ejected "app/views/notifications/_bell.html.erb.tt",
                          engine_path("app/views/notifications/notifications/_bell.html.erb")
  template_unless_ejected "app/views/notifications/index.html.erb.tt",
                          engine_path("app/views/notifications/notifications/index.html.erb")
end

#overwrite_engine_entry_pointObject



50
51
52
53
# File 'lib/generators/seams/notifications/notifications_generator.rb', line 50

def overwrite_engine_entry_point
  # engine.rb stays framework-managed.
  template "lib/engine.rb.tt", engine_path("lib/notifications/engine.rb"), force: true
end

#overwrite_readmeObject



234
235
236
# File 'lib/generators/seams/notifications/notifications_generator.rb', line 234

def overwrite_readme
  template "README.md.tt", engine_path("README.md"), force: true
end

#overwrite_routesObject



55
56
57
58
59
60
61
62
63
# File 'lib/generators/seams/notifications/notifications_generator.rb', line 55

def overwrite_routes
  # Routes ship with an explicit Engine.routes.draw block — the
  # generic engine scaffold leaves it empty, but notifications
  # needs the canonical /notifications + /preferences surface +
  # the Wave 10 `notifications.routes.after_preferences`
  # insertion-point marker so follow-up generators can splice
  # admin-side digest / opt-out routes.
  template_unless_ejected "config/routes.rb.tt", engine_path("config/routes.rb"), force: true
end

#report_summaryObject



271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
# File 'lib/generators/seams/notifications/notifications_generator.rb', line 271

def report_summary
  say ""
  say "  Notifications engine generated at engines/notifications/", :green
  say ""
  say "  Next steps:", :yellow
  say "    1. bundle install   (picks up ice_cube)"
  say "    2. bin/rails db:migrate"
  say "    3. Schedule the sweeper. With Solid Queue, add to config/recurring.yml:"
  say "         notifications_dispatcher:"
  say "           class: Notifications::SendDueNotificationsJob"
  say "           schedule: every minute"
  say "    4. Configure adapters in config/initializers/notifications.rb"
  say "    5. (optional) Include Notifications::Notifiable on Auth::Identity"
  say "       via the same initializer — see the file's comments."
  say ""
  say "  Subscribed to: identity.signed_up.auth (creates an InApp + Email Notification)"
  say ""
end

#update_exposed_concernsObject



238
239
240
241
242
243
244
245
246
# File 'lib/generators/seams/notifications/notifications_generator.rb', line 238

def update_exposed_concerns
  rubocop_path = engine_path(".rubocop.yml")
  return unless File.exist?(rubocop_path)

  contents = File.read(rubocop_path)
  replacement = "  ExposedConcerns:\n    - Notifications::Notifiable"
  contents.sub!(/^  ExposedConcerns: \[\]$/, replacement)
  File.write(rubocop_path, contents)
end

#wire_into_hostObject



259
260
261
262
263
264
265
266
267
268
269
# File 'lib/generators/seams/notifications/notifications_generator.rb', line 259

def wire_into_host
  host_inject_gem("ice_cube", ">= 0.16")
  # factory_bot_rails powers the engine's spec/factories/*. Lives
  # in the host's test group only.
  host_inject_gem("factory_bot_rails", "~> 6.4", group: :test)
  host_inject_mount(engine_class: "Notifications::Engine", at: "/notifications")
  # Wave 9: no auto-include into a host User. The canonical demo
  # has no host User after Wave 9 (the "human" is Auth::Identity).
  # Hosts wire `Notifications::Notifiable` themselves via the
  # initializer template — see config/initializers/notifications.rb.
end