activity_notification 2.4.0 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (253) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +25 -29
  3. data/app/jobs/activity_notification/cascading_notification_job.rb +123 -0
  4. data/docs/Functions.md +202 -12
  5. data/docs/Setup.md +34 -2
  6. data/docs/Testing.md +12 -1
  7. data/lib/activity_notification/apis/cascading_notification_api.rb +208 -0
  8. data/lib/activity_notification/apis/notification_api.rb +3 -0
  9. data/lib/activity_notification/config.rb +10 -0
  10. data/lib/activity_notification/mailers/helpers.rb +27 -1
  11. data/lib/activity_notification/models/concerns/swagger/subscription_schema.rb +1 -1
  12. data/lib/activity_notification/version.rb +1 -1
  13. data/lib/generators/templates/activity_notification.rb +8 -0
  14. metadata +45 -481
  15. data/.codeclimate.yml +0 -33
  16. data/.coveralls.yml +0 -1
  17. data/.github/ISSUE_TEMPLATE/bug_report.md +0 -22
  18. data/.github/ISSUE_TEMPLATE/feature_request.md +0 -17
  19. data/.github/pull_request_template.md +0 -13
  20. data/.github/workflows/build.yml +0 -101
  21. data/.gitignore +0 -74
  22. data/.rspec +0 -3
  23. data/.rubocop.yml +0 -1157
  24. data/.yardopts +0 -6
  25. data/CHANGELOG.md +0 -439
  26. data/Gemfile +0 -31
  27. data/Procfile +0 -2
  28. data/Rakefile +0 -28
  29. data/activity_notification.gemspec +0 -44
  30. data/ai-curated-specs/issues/172/design.md +0 -220
  31. data/ai-curated-specs/issues/172/tasks.md +0 -326
  32. data/ai-curated-specs/issues/188/design.md +0 -227
  33. data/ai-curated-specs/issues/188/requirements.md +0 -78
  34. data/ai-curated-specs/issues/188/tasks.md +0 -203
  35. data/ai-curated-specs/issues/188/upstream-contributions.md +0 -592
  36. data/ai-curated-specs/issues/50/design.md +0 -235
  37. data/ai-curated-specs/issues/50/requirements.md +0 -49
  38. data/ai-curated-specs/issues/50/tasks.md +0 -232
  39. data/bin/_dynamodblocal +0 -4
  40. data/bin/bundle_update.sh +0 -7
  41. data/bin/deploy_on_heroku.sh +0 -16
  42. data/bin/install_dynamodblocal.sh +0 -5
  43. data/bin/start_dynamodblocal.sh +0 -47
  44. data/bin/stop_dynamodblocal.sh +0 -34
  45. data/gemfiles/Gemfile.rails-5.0 +0 -25
  46. data/gemfiles/Gemfile.rails-5.1 +0 -25
  47. data/gemfiles/Gemfile.rails-5.2 +0 -24
  48. data/gemfiles/Gemfile.rails-6.0 +0 -23
  49. data/gemfiles/Gemfile.rails-6.1 +0 -22
  50. data/gemfiles/Gemfile.rails-7.0 +0 -25
  51. data/gemfiles/Gemfile.rails-7.1 +0 -23
  52. data/gemfiles/Gemfile.rails-7.2 +0 -23
  53. data/gemfiles/Gemfile.rails-8.0 +0 -24
  54. data/package.json +0 -8
  55. data/spec/channels/notification_api_channel_shared_examples.rb +0 -59
  56. data/spec/channels/notification_api_channel_spec.rb +0 -49
  57. data/spec/channels/notification_api_with_devise_channel_spec.rb +0 -76
  58. data/spec/channels/notification_channel_shared_examples.rb +0 -59
  59. data/spec/channels/notification_channel_spec.rb +0 -48
  60. data/spec/channels/notification_with_devise_channel_spec.rb +0 -97
  61. data/spec/concerns/apis/notification_api_spec.rb +0 -1627
  62. data/spec/concerns/apis/subscription_api_spec.rb +0 -474
  63. data/spec/concerns/common_spec.rb +0 -213
  64. data/spec/concerns/models/group_spec.rb +0 -61
  65. data/spec/concerns/models/notifiable_spec.rb +0 -782
  66. data/spec/concerns/models/notifier_spec.rb +0 -71
  67. data/spec/concerns/models/subscriber_spec.rb +0 -800
  68. data/spec/concerns/models/target_spec.rb +0 -1285
  69. data/spec/concerns/renderable_spec.rb +0 -129
  70. data/spec/config_spec.rb +0 -85
  71. data/spec/controllers/common_controller_spec.rb +0 -25
  72. data/spec/controllers/controller_spec_utility.rb +0 -100
  73. data/spec/controllers/dummy_common_controller.rb +0 -5
  74. data/spec/controllers/notifications_api_controller_shared_examples.rb +0 -619
  75. data/spec/controllers/notifications_api_controller_spec.rb +0 -19
  76. data/spec/controllers/notifications_api_with_devise_controller_spec.rb +0 -60
  77. data/spec/controllers/notifications_controller_shared_examples.rb +0 -743
  78. data/spec/controllers/notifications_controller_spec.rb +0 -11
  79. data/spec/controllers/notifications_with_devise_controller_spec.rb +0 -97
  80. data/spec/controllers/subscriptions_api_controller_shared_examples.rb +0 -750
  81. data/spec/controllers/subscriptions_api_controller_spec.rb +0 -19
  82. data/spec/controllers/subscriptions_api_with_devise_controller_spec.rb +0 -60
  83. data/spec/controllers/subscriptions_controller_shared_examples.rb +0 -946
  84. data/spec/controllers/subscriptions_controller_spec.rb +0 -11
  85. data/spec/controllers/subscriptions_with_devise_controller_spec.rb +0 -97
  86. data/spec/factories/admins.rb +0 -5
  87. data/spec/factories/articles.rb +0 -5
  88. data/spec/factories/comments.rb +0 -6
  89. data/spec/factories/dummy/dummy_group.rb +0 -4
  90. data/spec/factories/dummy/dummy_notifiable.rb +0 -4
  91. data/spec/factories/dummy/dummy_notifier.rb +0 -4
  92. data/spec/factories/dummy/dummy_subscriber.rb +0 -4
  93. data/spec/factories/dummy/dummy_target.rb +0 -4
  94. data/spec/factories/notifications.rb +0 -7
  95. data/spec/factories/subscriptions.rb +0 -8
  96. data/spec/factories/users.rb +0 -11
  97. data/spec/generators/controllers_generator_spec.rb +0 -85
  98. data/spec/generators/install_generator_spec.rb +0 -43
  99. data/spec/generators/migration/migration_generator_spec.rb +0 -66
  100. data/spec/generators/models_generator_spec.rb +0 -96
  101. data/spec/generators/views_generator_spec.rb +0 -195
  102. data/spec/helpers/polymorphic_helpers_spec.rb +0 -89
  103. data/spec/helpers/view_helpers_spec.rb +0 -547
  104. data/spec/jobs/notification_resilience_job_spec.rb +0 -167
  105. data/spec/jobs/notify_all_job_spec.rb +0 -23
  106. data/spec/jobs/notify_job_spec.rb +0 -23
  107. data/spec/jobs/notify_to_job_spec.rb +0 -23
  108. data/spec/mailers/mailer_spec.rb +0 -214
  109. data/spec/mailers/notification_resilience_spec.rb +0 -263
  110. data/spec/models/dummy/dummy_group_spec.rb +0 -10
  111. data/spec/models/dummy/dummy_notifiable_spec.rb +0 -10
  112. data/spec/models/dummy/dummy_notifier_spec.rb +0 -10
  113. data/spec/models/dummy/dummy_subscriber_spec.rb +0 -8
  114. data/spec/models/dummy/dummy_target_spec.rb +0 -10
  115. data/spec/models/notification_spec.rb +0 -472
  116. data/spec/models/subscription_spec.rb +0 -215
  117. data/spec/optional_targets/action_cable_api_channel_spec.rb +0 -34
  118. data/spec/optional_targets/action_cable_channel_spec.rb +0 -41
  119. data/spec/optional_targets/amazon_sns_spec.rb +0 -47
  120. data/spec/optional_targets/base_spec.rb +0 -45
  121. data/spec/optional_targets/slack_spec.rb +0 -44
  122. data/spec/orm/dynamoid_spec.rb +0 -115
  123. data/spec/rails_app/Rakefile +0 -15
  124. data/spec/rails_app/app/assets/config/manifest.js +0 -3
  125. data/spec/rails_app/app/assets/images/.keep +0 -0
  126. data/spec/rails_app/app/assets/javascripts/application.js +0 -3
  127. data/spec/rails_app/app/assets/javascripts/cable.js +0 -12
  128. data/spec/rails_app/app/assets/stylesheets/application.css +0 -15
  129. data/spec/rails_app/app/assets/stylesheets/reset.css +0 -85
  130. data/spec/rails_app/app/assets/stylesheets/style.css +0 -244
  131. data/spec/rails_app/app/controllers/admins_controller.rb +0 -21
  132. data/spec/rails_app/app/controllers/application_controller.rb +0 -5
  133. data/spec/rails_app/app/controllers/articles_controller.rb +0 -67
  134. data/spec/rails_app/app/controllers/comments_controller.rb +0 -36
  135. data/spec/rails_app/app/controllers/concerns/.keep +0 -0
  136. data/spec/rails_app/app/controllers/spa_controller.rb +0 -7
  137. data/spec/rails_app/app/controllers/users/notifications_controller.rb +0 -2
  138. data/spec/rails_app/app/controllers/users/notifications_with_devise_controller.rb +0 -2
  139. data/spec/rails_app/app/controllers/users/subscriptions_controller.rb +0 -2
  140. data/spec/rails_app/app/controllers/users/subscriptions_with_devise_controller.rb +0 -2
  141. data/spec/rails_app/app/controllers/users_controller.rb +0 -26
  142. data/spec/rails_app/app/helpers/application_helper.rb +0 -2
  143. data/spec/rails_app/app/helpers/devise_helper.rb +0 -2
  144. data/spec/rails_app/app/javascript/App.vue +0 -40
  145. data/spec/rails_app/app/javascript/components/DeviseTokenAuth.vue +0 -82
  146. data/spec/rails_app/app/javascript/components/Top.vue +0 -98
  147. data/spec/rails_app/app/javascript/components/notifications/Index.vue +0 -200
  148. data/spec/rails_app/app/javascript/components/notifications/Notification.vue +0 -133
  149. data/spec/rails_app/app/javascript/components/notifications/NotificationContent.vue +0 -122
  150. data/spec/rails_app/app/javascript/components/subscriptions/Index.vue +0 -279
  151. data/spec/rails_app/app/javascript/components/subscriptions/NewSubscription.vue +0 -112
  152. data/spec/rails_app/app/javascript/components/subscriptions/NotificationKey.vue +0 -141
  153. data/spec/rails_app/app/javascript/components/subscriptions/Subscription.vue +0 -226
  154. data/spec/rails_app/app/javascript/config/development.js +0 -5
  155. data/spec/rails_app/app/javascript/config/environment.js +0 -7
  156. data/spec/rails_app/app/javascript/config/production.js +0 -5
  157. data/spec/rails_app/app/javascript/config/test.js +0 -5
  158. data/spec/rails_app/app/javascript/packs/application.js +0 -18
  159. data/spec/rails_app/app/javascript/packs/spa.js +0 -14
  160. data/spec/rails_app/app/javascript/router/index.js +0 -73
  161. data/spec/rails_app/app/javascript/store/index.js +0 -37
  162. data/spec/rails_app/app/mailers/.keep +0 -0
  163. data/spec/rails_app/app/mailers/custom_notification_mailer.rb +0 -5
  164. data/spec/rails_app/app/models/admin.rb +0 -35
  165. data/spec/rails_app/app/models/article.rb +0 -54
  166. data/spec/rails_app/app/models/comment.rb +0 -81
  167. data/spec/rails_app/app/models/dummy/dummy_base.rb +0 -11
  168. data/spec/rails_app/app/models/dummy/dummy_group.rb +0 -23
  169. data/spec/rails_app/app/models/dummy/dummy_notifiable.rb +0 -15
  170. data/spec/rails_app/app/models/dummy/dummy_notifiable_target.rb +0 -27
  171. data/spec/rails_app/app/models/dummy/dummy_notifier.rb +0 -15
  172. data/spec/rails_app/app/models/dummy/dummy_subscriber.rb +0 -14
  173. data/spec/rails_app/app/models/dummy/dummy_target.rb +0 -16
  174. data/spec/rails_app/app/models/user.rb +0 -73
  175. data/spec/rails_app/app/views/activity_notification/mailer/dummy_subscribers/test_key.text.erb +0 -1
  176. data/spec/rails_app/app/views/activity_notification/notifications/default/article/_update.html.erb +0 -146
  177. data/spec/rails_app/app/views/activity_notification/notifications/default/custom/_path_test.html.erb +0 -1
  178. data/spec/rails_app/app/views/activity_notification/notifications/default/custom/_test.html.erb +0 -1
  179. data/spec/rails_app/app/views/activity_notification/notifications/users/_custom_index.html.erb +0 -1
  180. data/spec/rails_app/app/views/activity_notification/notifications/users/custom/_test.html.erb +0 -1
  181. data/spec/rails_app/app/views/activity_notification/notifications/users/overridden/custom/_test.html.erb +0 -1
  182. data/spec/rails_app/app/views/activity_notification/optional_targets/admins/amazon_sns/comment/_default.text.erb +0 -10
  183. data/spec/rails_app/app/views/articles/_form.html.erb +0 -24
  184. data/spec/rails_app/app/views/articles/edit.html.erb +0 -8
  185. data/spec/rails_app/app/views/articles/index.html.erb +0 -113
  186. data/spec/rails_app/app/views/articles/new.html.erb +0 -7
  187. data/spec/rails_app/app/views/articles/show.html.erb +0 -49
  188. data/spec/rails_app/app/views/layouts/_header.html.erb +0 -46
  189. data/spec/rails_app/app/views/layouts/application.html.erb +0 -15
  190. data/spec/rails_app/app/views/spa/index.html.erb +0 -2
  191. data/spec/rails_app/babel.config.js +0 -72
  192. data/spec/rails_app/bin/bundle +0 -3
  193. data/spec/rails_app/bin/rails +0 -4
  194. data/spec/rails_app/bin/rake +0 -4
  195. data/spec/rails_app/bin/setup +0 -29
  196. data/spec/rails_app/bin/webpack +0 -18
  197. data/spec/rails_app/bin/webpack-dev-server +0 -18
  198. data/spec/rails_app/config/application.rb +0 -54
  199. data/spec/rails_app/config/boot.rb +0 -5
  200. data/spec/rails_app/config/cable.yml +0 -8
  201. data/spec/rails_app/config/database.yml +0 -36
  202. data/spec/rails_app/config/dynamoid.rb +0 -13
  203. data/spec/rails_app/config/environment.rb +0 -26
  204. data/spec/rails_app/config/environments/development.rb +0 -60
  205. data/spec/rails_app/config/environments/production.rb +0 -85
  206. data/spec/rails_app/config/environments/test.rb +0 -53
  207. data/spec/rails_app/config/initializers/activity_notification.rb +0 -104
  208. data/spec/rails_app/config/initializers/assets.rb +0 -11
  209. data/spec/rails_app/config/initializers/backtrace_silencers.rb +0 -7
  210. data/spec/rails_app/config/initializers/cookies_serializer.rb +0 -3
  211. data/spec/rails_app/config/initializers/copy_it.aws.rb.template +0 -6
  212. data/spec/rails_app/config/initializers/devise.rb +0 -278
  213. data/spec/rails_app/config/initializers/devise_token_auth.rb +0 -55
  214. data/spec/rails_app/config/initializers/filter_parameter_logging.rb +0 -4
  215. data/spec/rails_app/config/initializers/inflections.rb +0 -16
  216. data/spec/rails_app/config/initializers/mime_types.rb +0 -4
  217. data/spec/rails_app/config/initializers/mysql.rb +0 -9
  218. data/spec/rails_app/config/initializers/session_store.rb +0 -3
  219. data/spec/rails_app/config/initializers/wrap_parameters.rb +0 -14
  220. data/spec/rails_app/config/initializers/zeitwerk.rb +0 -10
  221. data/spec/rails_app/config/locales/activity_notification.en.yml +0 -26
  222. data/spec/rails_app/config/locales/devise.en.yml +0 -62
  223. data/spec/rails_app/config/mongoid.yml +0 -13
  224. data/spec/rails_app/config/routes.rb +0 -50
  225. data/spec/rails_app/config/secrets.yml +0 -22
  226. data/spec/rails_app/config/webpack/development.js +0 -5
  227. data/spec/rails_app/config/webpack/environment.js +0 -7
  228. data/spec/rails_app/config/webpack/loaders/vue.js +0 -6
  229. data/spec/rails_app/config/webpack/production.js +0 -5
  230. data/spec/rails_app/config/webpack/test.js +0 -5
  231. data/spec/rails_app/config/webpacker.yml +0 -97
  232. data/spec/rails_app/config.ru +0 -4
  233. data/spec/rails_app/db/migrate/20160716000000_create_test_tables.rb +0 -42
  234. data/spec/rails_app/db/migrate/20181209000000_create_activity_notification_tables.rb +0 -33
  235. data/spec/rails_app/db/migrate/20191201000000_add_tokens_to_users.rb +0 -10
  236. data/spec/rails_app/db/schema.rb +0 -98
  237. data/spec/rails_app/db/seeds.rb +0 -95
  238. data/spec/rails_app/lib/custom_optional_targets/console_output.rb +0 -16
  239. data/spec/rails_app/lib/custom_optional_targets/raise_error.rb +0 -14
  240. data/spec/rails_app/lib/custom_optional_targets/wrong_target.rb +0 -13
  241. data/spec/rails_app/lib/mailer_previews/mailer_preview.rb +0 -29
  242. data/spec/rails_app/package.json +0 -23
  243. data/spec/rails_app/postcss.config.js +0 -12
  244. data/spec/rails_app/public/404.html +0 -67
  245. data/spec/rails_app/public/422.html +0 -67
  246. data/spec/rails_app/public/500.html +0 -66
  247. data/spec/rails_app/public/favicon.ico +0 -0
  248. data/spec/roles/acts_as_group_spec.rb +0 -30
  249. data/spec/roles/acts_as_notifiable_spec.rb +0 -432
  250. data/spec/roles/acts_as_notifier_spec.rb +0 -30
  251. data/spec/roles/acts_as_target_spec.rb +0 -36
  252. data/spec/spec_helper.rb +0 -56
  253. data/spec/version_spec.rb +0 -31
@@ -0,0 +1,208 @@
1
+ module ActivityNotification
2
+ # Defines API for cascading notifications included in Notification model.
3
+ # Cascading notifications enable sequential delivery through different channels
4
+ # based on read status, with configurable time delays between each step.
5
+ module CascadingNotificationApi
6
+ extend ActiveSupport::Concern
7
+
8
+ # Starts a cascading notification chain with the specified configuration.
9
+ # The chain will automatically check the read status before each step and
10
+ # only proceed if the notification remains unread.
11
+ #
12
+ # @example Simple cascade with Slack then email
13
+ # notification.cascade_notify([
14
+ # { delay: 10.minutes, target: :slack },
15
+ # { delay: 10.minutes, target: :email }
16
+ # ])
17
+ #
18
+ # @example Cascade with custom options for each target
19
+ # notification.cascade_notify([
20
+ # { delay: 5.minutes, target: :slack, options: { channel: '#alerts' } },
21
+ # { delay: 10.minutes, target: :amazon_sns, options: { subject: 'Urgent' } },
22
+ # { delay: 15.minutes, target: :email }
23
+ # ])
24
+ #
25
+ # @param [Array<Hash>] cascade_config Array of cascade step configurations
26
+ # @option cascade_config [ActiveSupport::Duration] :delay Required. Time to wait before this step
27
+ # @option cascade_config [Symbol, String] :target Required. Name of the optional target (e.g., :slack, :email)
28
+ # @option cascade_config [Hash] :options Optional. Parameters to pass to the optional target
29
+ # @param [Hash] options Additional options for cascade
30
+ # @option options [Boolean] :validate (true) Whether to validate the cascade configuration
31
+ # @option options [Boolean] :trigger_first_immediately (false) Whether to trigger the first target immediately without delay
32
+ # @return [Boolean] true if cascade was initiated successfully, false otherwise
33
+ # @raise [ArgumentError] if cascade_config is invalid and :validate is true
34
+ def cascade_notify(cascade_config, options = {})
35
+ validate = options.fetch(:validate, true)
36
+ trigger_first_immediately = options.fetch(:trigger_first_immediately, false)
37
+
38
+ # Validate configuration if requested
39
+ if validate
40
+ validation_result = validate_cascade_config(cascade_config)
41
+ unless validation_result[:valid]
42
+ raise ArgumentError, "Invalid cascade configuration: #{validation_result[:errors].join(', ')}"
43
+ end
44
+ end
45
+
46
+ # Return false if cascade config is empty
47
+ return false if cascade_config.blank?
48
+
49
+ # Return false if notification is already opened
50
+ return false if opened?
51
+
52
+ if defined?(ActiveJob) && defined?(ActivityNotification::CascadingNotificationJob) &&
53
+ ActivityNotification::CascadingNotificationJob.respond_to?(:perform_later)
54
+ if trigger_first_immediately && cascade_config.any?
55
+ # Trigger first target immediately
56
+ first_step = cascade_config.first
57
+ target_name = first_step[:target] || first_step['target']
58
+ target_options = first_step[:options] || first_step['options'] || {}
59
+
60
+ # Perform the first step synchronously
61
+ perform_cascade_step(target_name, target_options)
62
+
63
+ # Schedule remaining steps if any
64
+ if cascade_config.length > 1
65
+ remaining_config = cascade_config[1..-1]
66
+ first_delay = remaining_config.first[:delay] || remaining_config.first['delay']
67
+
68
+ if first_delay.present?
69
+ ActivityNotification::CascadingNotificationJob
70
+ .set(wait: first_delay)
71
+ .perform_later(id, remaining_config, 0)
72
+ end
73
+ end
74
+ else
75
+ # Schedule first step with its configured delay
76
+ first_step = cascade_config.first
77
+ first_delay = first_step[:delay] || first_step['delay']
78
+
79
+ if first_delay.present?
80
+ ActivityNotification::CascadingNotificationJob
81
+ .set(wait: first_delay)
82
+ .perform_later(id, cascade_config, 0)
83
+ else
84
+ # If no delay specified for first step, trigger immediately
85
+ ActivityNotification::CascadingNotificationJob
86
+ .perform_later(id, cascade_config, 0)
87
+ end
88
+ end
89
+
90
+ true
91
+ else
92
+ Rails.logger.error("ActiveJob or CascadingNotificationJob not available for cascading notifications")
93
+ false
94
+ end
95
+ end
96
+
97
+ # Validates a cascade configuration array
98
+ #
99
+ # @param [Array<Hash>] cascade_config The configuration to validate
100
+ # @return [Hash] Hash with :valid (Boolean) and :errors (Array<String>) keys
101
+ def validate_cascade_config(cascade_config)
102
+ errors = []
103
+
104
+ if cascade_config.nil?
105
+ errors << "cascade_config cannot be nil"
106
+ return { valid: false, errors: errors }
107
+ end
108
+
109
+ unless cascade_config.is_a?(Array)
110
+ errors << "cascade_config must be an Array"
111
+ return { valid: false, errors: errors }
112
+ end
113
+
114
+ if cascade_config.empty?
115
+ errors << "cascade_config cannot be empty"
116
+ end
117
+
118
+ cascade_config.each_with_index do |step, index|
119
+ unless step.is_a?(Hash)
120
+ errors << "Step #{index} must be a Hash"
121
+ next
122
+ end
123
+
124
+ # Check for required target parameter
125
+ target = step[:target] || step['target']
126
+ if target.nil?
127
+ errors << "Step #{index} missing required :target parameter"
128
+ elsif !target.is_a?(Symbol) && !target.is_a?(String)
129
+ errors << "Step #{index} :target must be a Symbol or String"
130
+ end
131
+
132
+ # Check for delay parameter (only required for steps after the first if not using trigger_first_immediately)
133
+ delay = step[:delay] || step['delay']
134
+ if delay.nil?
135
+ errors << "Step #{index} missing :delay parameter"
136
+ elsif !delay.respond_to?(:from_now) && !delay.is_a?(Numeric)
137
+ errors << "Step #{index} :delay must be an ActiveSupport::Duration or Numeric (seconds)"
138
+ end
139
+
140
+ # Check options if present
141
+ options = step[:options] || step['options']
142
+ if options.present? && !options.is_a?(Hash)
143
+ errors << "Step #{index} :options must be a Hash"
144
+ end
145
+ end
146
+
147
+ { valid: errors.empty?, errors: errors }
148
+ end
149
+
150
+ # Checks if a cascading notification is currently in progress for this notification
151
+ # This is a helper method that checks if there are scheduled jobs for this notification
152
+ #
153
+ # @return [Boolean] true if cascade jobs are scheduled (this is a best-effort check)
154
+ def cascade_in_progress?
155
+ # This is a best-effort check that returns false by default
156
+ # In production, you might want to track this state differently
157
+ # (e.g., in Redis, database flag, or by querying the job queue)
158
+ false
159
+ end
160
+
161
+ private
162
+
163
+ # Performs a single cascade step immediately (synchronously)
164
+ # @api private
165
+ # @param [Symbol, String] target_name Name of the optional target
166
+ # @param [Hash] options Options to pass to the optional target
167
+ # @return [Hash] Result of the operation
168
+ def perform_cascade_step(target_name, options = {})
169
+ target_name_sym = target_name.to_sym
170
+
171
+ # Get all configured optional targets for this notification
172
+ optional_targets = notifiable.optional_targets(
173
+ target.to_resources_name,
174
+ key
175
+ )
176
+
177
+ # Find the matching optional target
178
+ optional_target = optional_targets.find do |ot|
179
+ ot.to_optional_target_name == target_name_sym
180
+ end
181
+
182
+ if optional_target.nil?
183
+ Rails.logger.warn("Optional target '#{target_name}' not found for notification #{id}")
184
+ return { target_name_sym => :not_configured }
185
+ end
186
+
187
+ # Check subscription status
188
+ unless optional_target_subscribed?(target_name_sym)
189
+ Rails.logger.info("Target not subscribed to optional target '#{target_name}' for notification #{id}")
190
+ return { target_name_sym => :not_subscribed }
191
+ end
192
+
193
+ # Trigger the optional target
194
+ begin
195
+ optional_target.notify(self, options)
196
+ Rails.logger.info("Successfully triggered optional target '#{target_name}' for notification #{id}")
197
+ { target_name_sym => :success }
198
+ rescue => e
199
+ Rails.logger.error("Failed to trigger optional target '#{target_name}' for notification #{id}: #{e.message}")
200
+ if ActivityNotification.config.rescue_optional_target_errors
201
+ { target_name_sym => e }
202
+ else
203
+ raise e
204
+ end
205
+ end
206
+ end
207
+ end
208
+ end
@@ -1,7 +1,10 @@
1
+ require 'activity_notification/apis/cascading_notification_api'
2
+
1
3
  module ActivityNotification
2
4
  # Defines API for notification included in Notification model.
3
5
  module NotificationApi
4
6
  extend ActiveSupport::Concern
7
+ include CascadingNotificationApi
5
8
 
6
9
  included do
7
10
  # Defines store_notification as private clas method
@@ -82,6 +82,15 @@ module ActivityNotification
82
82
  # @return [String] Email address as sender of notification email.
83
83
  attr_accessor :mailer_sender
84
84
 
85
+ # @overload mailer_cc
86
+ # Returns carbon copy (CC) email address(es) for notification email
87
+ # @return [String, Array<String>, Proc] CC email address(es) for notification email.
88
+ # @overload mailer_cc=(value)
89
+ # Sets carbon copy (CC) email address(es) for notification email
90
+ # @param [String, Array<String>, Proc] mailer_cc The new mailer_cc
91
+ # @return [String, Array<String>, Proc] CC email address(es) for notification email.
92
+ attr_accessor :mailer_cc
93
+
85
94
  # @overload mailer
86
95
  # Returns mailer class for email notification
87
96
  # @return [String] Mailer class for email notification.
@@ -236,6 +245,7 @@ module ActivityNotification
236
245
  @subscribe_to_email_as_default = nil
237
246
  @subscribe_to_optional_targets_as_default = nil
238
247
  @mailer_sender = nil
248
+ @mailer_cc = nil
239
249
  @mailer = 'ActivityNotification::Mailer'
240
250
  @parent_mailer = 'ActionMailer::Base'
241
251
  @parent_job = 'ActiveJob::Base'
@@ -74,6 +74,7 @@ module ActivityNotification
74
74
  subject: :subject_for,
75
75
  from: :mailer_from,
76
76
  reply_to: :mailer_reply_to,
77
+ cc: :mailer_cc,
77
78
  message_id: nil
78
79
  }.each do |header_name, default_method|
79
80
  overridding_method_name = "overriding_notification_email_#{header_name.to_s}"
@@ -81,7 +82,12 @@ module ActivityNotification
81
82
  @notification.notifiable.send(overridding_method_name, @target, key).present?
82
83
  @notification.notifiable.send(overridding_method_name, @target, key)
83
84
  elsif default_method
84
- send(default_method, key)
85
+ # Special handling for methods that take target instead of key
86
+ if [:mailer_cc].include?(default_method)
87
+ send(default_method, @target)
88
+ else
89
+ send(default_method, key)
90
+ end
85
91
  else
86
92
  nil
87
93
  end
@@ -99,6 +105,26 @@ module ActivityNotification
99
105
  target.mailer_to
100
106
  end
101
107
 
108
+ # Returns carbon copy (CC) email address(es).
109
+ #
110
+ # @param [Object] target Target instance to notify
111
+ # @return [String, Array<String>, nil] CC email address(es) or nil
112
+ def mailer_cc(target)
113
+ if target.respond_to?(:mailer_cc)
114
+ target.mailer_cc
115
+ elsif ActivityNotification.config.mailer_cc.present?
116
+ if ActivityNotification.config.mailer_cc.is_a?(Proc)
117
+ # Get the notification key from current context
118
+ key = @notification ? @notification.key : nil
119
+ ActivityNotification.config.mailer_cc.call(key)
120
+ else
121
+ ActivityNotification.config.mailer_cc
122
+ end
123
+ else
124
+ nil
125
+ end
126
+ end
127
+
102
128
  # Returns sender email address as 'reply_to'.
103
129
  #
104
130
  # @param [String] key Key of the notification or batch notification email
@@ -85,7 +85,7 @@ module ActivityNotification
85
85
  },
86
86
  subscribed_at: {
87
87
  type: "string",
88
- format: "date-time"
88
+ nullable: true
89
89
  }
90
90
  }
91
91
  }
@@ -1,3 +1,3 @@
1
1
  module ActivityNotification
2
- VERSION = "2.4.0"
2
+ VERSION = "2.5.0"
3
3
  end
@@ -45,6 +45,14 @@ ActivityNotification.configure do |config|
45
45
  # note that it will be overwritten if you use your own mailer class with default "from" parameter.
46
46
  config.mailer_sender = 'please-change-me-at-config-initializers-activity_notification@example.com'
47
47
 
48
+ # Configure the carbon copy (CC) email address(es) for notification emails.
49
+ # You can set a single email address, an array of email addresses, or a Proc that returns either.
50
+ # Note that this can be overridden per target by defining a mailer_cc method in the target model,
51
+ # or per notification by defining overriding_notification_email_cc in the notifiable model.
52
+ # config.mailer_cc = 'admin@example.com'
53
+ # config.mailer_cc = ['admin@example.com', 'support@example.com']
54
+ # config.mailer_cc = ->(key){ key.include?('urgent') ? 'urgent@example.com' : nil }
55
+
48
56
  # Configure the class responsible to send e-mails.
49
57
  # config.mailer = "ActivityNotification::Mailer"
50
58