sincerely 1.0.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 (63) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +38 -0
  4. data/CHANGELOG.md +12 -0
  5. data/README.md +202 -0
  6. data/Rakefile +12 -0
  7. data/app/controllers/sincerely/application_controller.rb +81 -0
  8. data/app/controllers/sincerely/dashboard_controller.rb +112 -0
  9. data/app/controllers/sincerely/delivery_events_controller.rb +31 -0
  10. data/app/controllers/sincerely/engagement_events_controller.rb +32 -0
  11. data/app/controllers/sincerely/notifications_controller.rb +69 -0
  12. data/app/controllers/sincerely/send_controller.rb +105 -0
  13. data/app/controllers/sincerely/templates_controller.rb +61 -0
  14. data/app/helpers/sincerely/application_helper.rb +39 -0
  15. data/app/views/layouts/sincerely/application.html.erb +593 -0
  16. data/app/views/sincerely/dashboard/index.html.erb +382 -0
  17. data/app/views/sincerely/delivery_events/index.html.erb +97 -0
  18. data/app/views/sincerely/engagement_events/index.html.erb +97 -0
  19. data/app/views/sincerely/notifications/index.html.erb +91 -0
  20. data/app/views/sincerely/notifications/show.html.erb +98 -0
  21. data/app/views/sincerely/send/new.html.erb +592 -0
  22. data/app/views/sincerely/shared/_pagination.html.erb +19 -0
  23. data/app/views/sincerely/templates/_form.html.erb +226 -0
  24. data/app/views/sincerely/templates/edit.html.erb +11 -0
  25. data/app/views/sincerely/templates/index.html.erb +59 -0
  26. data/app/views/sincerely/templates/new.html.erb +11 -0
  27. data/app/views/sincerely/templates/preview.html.erb +48 -0
  28. data/app/views/sincerely/templates/show.html.erb +69 -0
  29. data/config/routes.rb +21 -0
  30. data/lib/config/application_config.rb +18 -0
  31. data/lib/config/sincerely_config.rb +31 -0
  32. data/lib/generators/sincerely/aws_ses_webhook_controller_generator.rb +31 -0
  33. data/lib/generators/sincerely/events_generator.rb +45 -0
  34. data/lib/generators/sincerely/install_generator.rb +18 -0
  35. data/lib/generators/sincerely/migration_generator.rb +65 -0
  36. data/lib/generators/templates/aws_ses_webhook_controller.rb.erb +3 -0
  37. data/lib/generators/templates/events/delivery_event_model.rb.erb +5 -0
  38. data/lib/generators/templates/events/delivery_events_create.rb.erb +20 -0
  39. data/lib/generators/templates/events/engagement_event_model.rb.erb +5 -0
  40. data/lib/generators/templates/events/engagement_events_create.rb.erb +21 -0
  41. data/lib/generators/templates/notification_model.rb.erb +3 -0
  42. data/lib/generators/templates/notifications_create.rb.erb +21 -0
  43. data/lib/generators/templates/notifications_update.rb.erb +16 -0
  44. data/lib/generators/templates/sincerely.yml +21 -0
  45. data/lib/generators/templates/templates_create.rb.erb +15 -0
  46. data/lib/sincerely/delivery_systems/email_aws_ses.rb +69 -0
  47. data/lib/sincerely/engine.rb +7 -0
  48. data/lib/sincerely/mixins/notification_model.rb +94 -0
  49. data/lib/sincerely/mixins/webhooks/aws_ses_events.rb +74 -0
  50. data/lib/sincerely/renderers/liquid.rb +14 -0
  51. data/lib/sincerely/services/events/aws_ses_bounce_event.rb +26 -0
  52. data/lib/sincerely/services/events/aws_ses_click_event.rb +25 -0
  53. data/lib/sincerely/services/events/aws_ses_complaint_event.rb +25 -0
  54. data/lib/sincerely/services/events/aws_ses_delivery_event.rb +17 -0
  55. data/lib/sincerely/services/events/aws_ses_event.rb +56 -0
  56. data/lib/sincerely/services/events/aws_ses_open_event.rb +25 -0
  57. data/lib/sincerely/services/process_delivery_event.rb +72 -0
  58. data/lib/sincerely/templates/email_liquid_template.rb +13 -0
  59. data/lib/sincerely/templates/notification_template.rb +22 -0
  60. data/lib/sincerely/version.rb +5 -0
  61. data/lib/sincerely.rb +20 -0
  62. data/sincerely.gemspec +37 -0
  63. metadata +187 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 3126012f05e46f992478bb05e997605aef323820d2fd72c95dfe3fa49105c33c
4
+ data.tar.gz: 5c5c1b66fc5e956506a634b8c4da25869019f741bb66e8517e28a08eef32c209
5
+ SHA512:
6
+ metadata.gz: 0b093a942bbb2d4ae549e049e47595ca42e645211b1b6d1a92be1e035cac3aa3c690c1f829505bba4e0ae61fbbfa950e08f25a9bc1c4507be2a3e2f5a2555225
7
+ data.tar.gz: 4f75a74cd95ec7025a04c6d549591129f6736c9303f07f866fe4213f74ba72a2fe677fc699c668efc405271e09b5b0437679bcb61a19439450400f45323fd58c
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --order random
2
+ --profile 10
3
+ --color
data/.rubocop.yml ADDED
@@ -0,0 +1,38 @@
1
+ require:
2
+ - rubocop-rspec
3
+ - rubocop-rails
4
+
5
+ AllCops:
6
+ NewCops: enable
7
+ TargetRubyVersion: 3.1
8
+
9
+ Style/Documentation:
10
+ Enabled: false
11
+ Layout/LineLength:
12
+ Max: 120
13
+ RSpec/NestedGroups:
14
+ Enabled: false
15
+
16
+ RSpec/MultipleExpectations:
17
+ Enabled: false
18
+
19
+ # Controller methods can be complex; these metrics produce false positives
20
+ Metrics/AbcSize:
21
+ Enabled: false
22
+
23
+ Metrics/MethodLength:
24
+ Enabled: false
25
+
26
+ Metrics/CyclomaticComplexity:
27
+ Enabled: false
28
+
29
+ Metrics/PerceivedComplexity:
30
+ Enabled: false
31
+
32
+ # Engines don't typically use i18n for simple flash messages
33
+ Rails/I18nLocaleTexts:
34
+ Enabled: false
35
+
36
+ # Keep :unprocessable_entity for Rails < 7.1 compatibility
37
+ Rails/ResponseParsedBody:
38
+ Enabled: false
data/CHANGELOG.md ADDED
@@ -0,0 +1,12 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ ## [1.0.0] - 2024-07-31
6
+
7
+ ### Added
8
+ - Initial release
9
+ - Amazon SES email sending
10
+ - Amazon SES email sending events
11
+ - liquid email templates
12
+ - notification statuses
data/README.md ADDED
@@ -0,0 +1,202 @@
1
+ # Sincerely
2
+ ## Introduction
3
+ Sincerely is a versatile Ruby gem designed for creating, delivering and monitoring email, sms or push notifications.
4
+
5
+ ## Table of Contents
6
+ 1. [Installation](#installation)
7
+ 2. [Getting Started](#getting-started)
8
+ 3. [Configuration](#configuration)
9
+ 4. [Usage](#usage)
10
+ 5. [Notification model](#notification-model)
11
+ 6. [Notification states](#notification-states)
12
+ 7. [Callbacks](#callbacks)
13
+ 8. [How to Run Tests](#how-to-run-tests)
14
+ 9. [Guide for Contributing](#guide-for-contributing)
15
+ 10. [How to Contact Us](#how-to-contact-us)
16
+ 11. [License](#license)
17
+
18
+ ## Installation
19
+ Sincerely 1.0 works with Rails 6.0 onwards. Run:
20
+ ```bash
21
+ bundle add sincerely
22
+ ```
23
+
24
+ ## Getting Started
25
+
26
+ 1. First, you need to run the install generator, which will create the `config/sincerely.yml` initializer file for you:
27
+ ```bash
28
+ rails g sincerely:install
29
+ ```
30
+
31
+ 2. You need to generate and run a migration to create the `notifications` and `notification_templates` tables and the `Notification` and the `NotificationTemplate` model:
32
+ ```bash
33
+ rails g sincerely:migration Notification
34
+
35
+ rails db:migrate
36
+ ```
37
+
38
+ 3. If you want to enable event callbacks you need to run the `events` task to create the `sincerely_delivery_events` and `sincerely_engagements_events` tables and the `Sincerely::DeliveryEvent` and `Sincerely::EngagementEvent` models:
39
+ ```bash
40
+ rails g sincerely:events
41
+
42
+ rails db:migrate
43
+ ```
44
+
45
+ ## Configuration
46
+ 1. You need to set the notification model generated in the previous step by setting the `notification_model_name` option in the `config/sincerely.yml` file:
47
+
48
+ ```
49
+ # config/sincerely.yml
50
+ defaults: &defaults
51
+ notification_model_name: Notification
52
+ ```
53
+
54
+ 2. You need to set the delivery system for each delivery methods (email/sms/push) by setting the `delivery_methods` option in the `config/sincerely.yml` file. Please note that right now the gem supports only AWS SES email notification (`Sincerely::DeliverySystems::EmailAwsSes` module).
55
+
56
+ ```
57
+ # config/sincerely.yml
58
+ defaults: &defaults
59
+ delivery_methods:
60
+ email:
61
+ delivery_system: Sincerely::DeliverySystems::EmailAwsSes
62
+ options:
63
+ region: region
64
+ access_key_id: your_access_key_id
65
+ secret_access_key: your_secret_access_key
66
+ configuration_set_name: config_set
67
+ sms:
68
+ ```
69
+
70
+ * `region`: the AWS region to connect to
71
+ * `access_key_id`: AWS access key id
72
+ * `secret_access_key`: AWS secret access key
73
+ * `configuration_set_name`: the name of the configuration set to use when sending the email, you don't need to specify the configuration set option if you don't want to handle SES email sending events
74
+
75
+ Sincerely uses the `aws-sdk-sesv2` gem to send emails. See [sesv2](https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/SESV2.html) for details.
76
+
77
+ Furthermore you need to configure Amazon SES to be able to send emails, see [SES](https://docs.aws.amazon.com/ses/latest/dg/Welcome.html) for details.
78
+
79
+ ## Usage
80
+
81
+ 1. Once the email delivery method is configured you need to create an email template which is used to generate the email content:
82
+
83
+ ```
84
+ Sincerely::Templates::EmailLiquidTemplate.create(
85
+ name: 'test',
86
+ subject: 'template for sending messages',
87
+ sender: 'john.doe@gmail.com'
88
+ html_content: 'hi {{name}}',
89
+ text_content: 'hi {{name}}'
90
+ )
91
+ ```
92
+
93
+ * `name`: unique id of the template
94
+ * `subject`: subject of the generated email (can be overwritten in the notification)
95
+ * `sender`: email address of the sender
96
+ * `html_content`: html content of the generated email, you can use the liquid syntax to use variables in the content, see [liquid](https://github.com/Shopify/liquid) for details
97
+ * `text_content`: text content of the generated email, you can use the liquid syntax to use variables in the content, see [liquid](https://github.com/Shopify/liquid) for details
98
+
99
+ 2. You need to create the notification:
100
+
101
+ ```
102
+ Notification.create(
103
+ recipient: 'jane.doe@gmail.com',
104
+ notification_type: 'email',
105
+ template: Sincerely::Templates::EmailLiquidTemplate.first,
106
+ delivery_options: {
107
+ template_data: {
108
+ name: 'John Doe'
109
+ },
110
+ subject: 'subject'
111
+ }
112
+ )
113
+ ```
114
+ * `recipient`: recipient of the notification
115
+ * `notification_type`: email/sms/push (please note that right now the gem supports only AWS SES email notifications)
116
+ * `template`: template to generate the notification content
117
+ * `delivery_options`: options for the associated template (`EmailLiquidTemplate` supports (i) the `template_data` option which contains the parameter hash for the liquid template defined in the html_content field of the associated template, (ii) the `subject` option which overwrites the subject defined in the associated template)
118
+
119
+
120
+ 3. You need to send the notification:
121
+
122
+ ```
123
+ notification.deliver
124
+ ```
125
+
126
+ ## Notification model
127
+ * `recipient`: recipient (email or phone number) of the notification
128
+ * `notification_type`: email | sms | push
129
+ * `delivery_options`: options for the associated template
130
+ * `delivery_system`: associated delivery system
131
+ * `delivery_state`: state of the notification
132
+ * `template_id`: associated template
133
+ * `sent_at`: timestamp of the delivery
134
+ * `message_id`: message id generated by the delivery system
135
+ * `error_message`: error message if the notification is rejected by the delivery system
136
+
137
+ ## Notification states
138
+ * `draft`: default state, notification is not yet sent
139
+ * `accepted`: the send request was successful and the delivery system will attempt to deliver the message to the recipient’s mail server
140
+ * `rejected`: notification is rejected by the delivery system
141
+ * `delivered`: notification is successfully delivered by the delivery system
142
+ * `bounced`: notification is bounced (ie when an email cannot be delivered to the recipient)
143
+ * `opened`: notification is opened
144
+ * `clicked`: notification is clicked
145
+ * `complained`: notification is complained (ie when the recipient of your email marks your email as spam)
146
+
147
+ Please note that the notification state is updated only if the event callback is configured.
148
+
149
+ ## Callbacks
150
+
151
+ `EmailAwsSes` delivery system handles SES email sending events. To enable it make sure:
152
+ * you have run the `events` task described in the `Getting Started` section
153
+ * you have set the `configuration_set_name` option described in the `Configuration` section
154
+ * you have run the following task that generates the webhook controller for you
155
+ ```bash
156
+ rails g sincerely:aws_ses_webhook_controller SES_WEBHOOK
157
+ ```
158
+
159
+ Furthermore you need to configure Amazon SES to monitor email sending, see [SES email sending events](https://docs.aws.amazon.com/ses/latest/dg/monitor-using-event-publishing.html) for details.
160
+
161
+ Once the webhook controller is in place, it will:
162
+ * update the state of the notifications
163
+ * create event records for the bounce/complaint/open/click events
164
+
165
+ Delivery events (delivery/bounce) fields:
166
+ * `message_id`: message id generated by the delivery system
167
+ * `delivery_system`: associated delivery system
168
+ * `event_type`: delivery | bounce
169
+ * `recipient`: recipient of the notification
170
+ * `delivery_event_type`: bounce type
171
+ * `delivery_event_subtype`: bounce subtype
172
+ * `options`: other event specific options (if any)
173
+ * `timestamp`: timestamp of the event
174
+
175
+ Engagement events (open/click/compaint) fields:
176
+ * `message_id`: message id generated by the delivery system
177
+ * `delivery_system`: associated delivery system
178
+ * `event_type`: open | click | compaint
179
+ * `recipient`: recipient of the notification
180
+ * `ip_address`: the recipient's IP address
181
+ * `user_agent`: the user agent of the device or email client
182
+ * `link`: the URL of the link that the recipient clicked
183
+ * `feedback_type`: complaint feedback type
184
+ * `options`: other event specific options (if any)
185
+ * `timestamp`: timestamp of the event
186
+
187
+
188
+ ## How to Run Tests
189
+ You can run unit tests for RubyBlok with the following command:
190
+ ```
191
+ bundle exec rspec
192
+ ```
193
+
194
+ ## Guide for Contributing
195
+ Contributions are made to this repository via Issues and Pull Requests (PRs).
196
+ Issues should be used to report bugs, request a new feature, or to discuss potential changes before a PR is created.
197
+
198
+ ## How to Contact Us
199
+ For any inquiries, reach out to us at: info@rubyblok.com
200
+
201
+ ## License
202
+ RubyBlok is released under the MIT License.
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require 'rubocop/rake_task'
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sincerely
4
+ class ApplicationController < ActionController::Base
5
+ protect_from_forgery with: :exception
6
+
7
+ layout 'sincerely/application'
8
+
9
+ before_action :authenticate_user!
10
+
11
+ helper_method :notification_model
12
+
13
+ private
14
+
15
+ def authenticate_user!
16
+ return unless Sincerely.authenticate_with
17
+
18
+ instance_exec(&Sincerely.authenticate_with)
19
+ end
20
+
21
+ def notification_model
22
+ @notification_model ||= Sincerely.notification_model
23
+ end
24
+
25
+ def paginate(collection, per_page: 25)
26
+ page = (params[:page] || 1).to_i
27
+ offset = (page - 1) * per_page
28
+ total = collection.count
29
+
30
+ {
31
+ records: collection.offset(offset).limit(per_page),
32
+ total_count: total,
33
+ current_page: page,
34
+ per_page:,
35
+ total_pages: (total.to_f / per_page).ceil
36
+ }
37
+ end
38
+
39
+ def time_filter_start
40
+ period = params[:period]
41
+ period = '24h' unless %w[1h 24h 7d 30d 3m all].include?(period)
42
+
43
+ case period
44
+ when '1h' then 1.hour.ago
45
+ when '24h' then 24.hours.ago
46
+ when '7d' then 7.days.ago
47
+ when '30d' then 30.days.ago
48
+ when '3m' then 3.months.ago
49
+ when 'all' then nil
50
+ end
51
+ end
52
+
53
+ def apply_time_filter(collection, column: :created_at)
54
+ start_time = time_filter_start
55
+ return collection unless start_time
56
+
57
+ collection.where("#{column} >= ?", start_time)
58
+ end
59
+
60
+ def apply_notification_filter(scope)
61
+ filter = Sincerely.config.filter_notifications_by
62
+ return scope unless filter.respond_to?(:call)
63
+
64
+ conditions = instance_exec(&filter)
65
+ return scope if conditions.blank?
66
+
67
+ scope.where(conditions)
68
+ end
69
+
70
+ def apply_event_filter(scope)
71
+ filter = Sincerely.config.filter_notifications_by
72
+ return scope unless filter.respond_to?(:call)
73
+
74
+ conditions = instance_exec(&filter)
75
+ return scope if conditions.blank?
76
+
77
+ message_ids = notification_model.where(conditions).where.not(message_id: nil).pluck(:message_id)
78
+ scope.where(message_id: message_ids)
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sincerely
4
+ class DashboardController < ApplicationController
5
+ def index
6
+ filtered_notifications = apply_time_filter(notification_model)
7
+ filtered_notifications = apply_notification_filter(filtered_notifications)
8
+
9
+ @total_notifications = filtered_notifications.count
10
+ @notifications_by_state = filtered_notifications.group(:delivery_state).count
11
+ @recent_notifications = filtered_notifications.order(created_at: :desc).limit(10)
12
+
13
+ # Delivery rates
14
+ total_sent = filtered_notifications.where.not(delivery_state: %w[draft rejected]).count
15
+ delivered = filtered_notifications.where(delivery_state: %w[delivered opened clicked]).count
16
+ @delivery_rate = total_sent.positive? ? (delivered.to_f / total_sent * 100).round(1) : 0
17
+
18
+ # Engagement rates
19
+ delivered_count = filtered_notifications.where(delivery_state: %w[delivered opened clicked]).count
20
+ engaged = filtered_notifications.where(delivery_state: %w[opened clicked]).count
21
+ @open_rate = delivered_count.positive? ? (engaged.to_f / delivered_count * 100).round(1) : 0
22
+
23
+ # Recent events (filtered by user's notifications)
24
+ user_message_ids = filtered_notifications.where.not(message_id: nil).pluck(:message_id)
25
+ @recent_delivery_events = apply_time_filter(Sincerely::DeliveryEvent)
26
+ .where(message_id: user_message_ids)
27
+ .order(created_at: :desc).limit(5)
28
+ @recent_engagement_events = apply_time_filter(Sincerely::EngagementEvent)
29
+ .where(message_id: user_message_ids)
30
+ .order(created_at: :desc).limit(5)
31
+
32
+ # Bounce rate
33
+ bounced = filtered_notifications.where(delivery_state: 'bounced').count
34
+ @bounce_rate = total_sent.positive? ? (bounced.to_f / total_sent * 100).round(1) : 0
35
+
36
+ # Current period for display
37
+ @current_period = params[:period] || '24h'
38
+
39
+ @notifications_timeline = build_timeline_data(filtered_notifications)
40
+ end
41
+
42
+ private
43
+
44
+ def build_timeline_data(notifications)
45
+ bucket_config = timeline_bucket_config
46
+ buckets = generate_time_buckets(bucket_config)
47
+ interval = bucket_config[:interval]
48
+ series = {}
49
+
50
+ notifications.find_each do |notification|
51
+ bucket_index = find_bucket_index(notification.created_at, buckets, interval)
52
+ next if bucket_index.nil?
53
+
54
+ state = notification.delivery_state
55
+ series[state] ||= Array.new(buckets.length, 0)
56
+ series[state][bucket_index] += 1
57
+ end
58
+
59
+ labels = buckets.map { |b| format_bucket_label(b, bucket_config) }
60
+
61
+ { labels:, series:, buckets: }
62
+ end
63
+
64
+ def timeline_bucket_config
65
+ period = params[:period]
66
+ period = '24h' unless %w[1h 24h 7d 30d 3m all].include?(period)
67
+
68
+ case period
69
+ when '1h'
70
+ { interval: 5.minutes, format: '%H:%M', start: 1.hour.ago }
71
+ when '24h'
72
+ { interval: 1.hour, format: '%H:%M', start: 24.hours.ago }
73
+ when '7d'
74
+ { interval: 6.hours, format: '%a %H:%M', start: 7.days.ago }
75
+ when '30d'
76
+ { interval: 1.day, format: '%b %d', start: 30.days.ago }
77
+ when '3m'
78
+ { interval: 1.week, format: '%b %d', start: 3.months.ago }
79
+ when 'all'
80
+ { interval: 1.month, format: '%b %Y', start: 1.year.ago }
81
+ end
82
+ end
83
+
84
+ def generate_time_buckets(config)
85
+ buckets = []
86
+ current = config[:start].beginning_of_hour
87
+ now = Time.current
88
+
89
+ while current <= now
90
+ buckets << current
91
+ current += config[:interval]
92
+ end
93
+
94
+ buckets
95
+ end
96
+
97
+ def find_bucket_index(time, buckets, interval)
98
+ return nil if buckets.empty?
99
+
100
+ buckets.each_with_index do |bucket_start, index|
101
+ bucket_end = bucket_start + interval
102
+ return index if time >= bucket_start && time < bucket_end
103
+ end
104
+
105
+ buckets.length - 1 if time >= buckets.last && time < buckets.last + interval
106
+ end
107
+
108
+ def format_bucket_label(bucket, config)
109
+ bucket.strftime(config[:format])
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sincerely
4
+ class DeliveryEventsController < ApplicationController
5
+ def index
6
+ @events = apply_time_filter(Sincerely::DeliveryEvent).order(created_at: :desc)
7
+ @events = apply_event_filter(@events)
8
+
9
+ @events = @events.where(event_type: params[:event_type]) if params[:event_type].present?
10
+ @events = @events.where('recipient LIKE ?', "%#{params[:recipient]}%") if params[:recipient].present?
11
+
12
+ if params[:template_id].present? || params[:notification_type].present?
13
+ notification_scope = notification_model.all
14
+ if params[:template_id].present?
15
+ notification_scope = notification_scope.where(template_id: params[:template_id])
16
+ end
17
+ if params[:notification_type].present?
18
+ notification_scope = notification_scope.where(notification_type: params[:notification_type])
19
+ end
20
+ message_ids = notification_scope.pluck(:message_id).compact
21
+ @events = @events.where(message_id: message_ids)
22
+ end
23
+
24
+ @pagination = paginate(@events)
25
+ @events = @pagination[:records]
26
+
27
+ @event_types = Sincerely::DeliveryEvent.distinct.pluck(:event_type).compact
28
+ @templates = Sincerely::Templates::NotificationTemplate.all
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sincerely
4
+ class EngagementEventsController < ApplicationController
5
+ def index
6
+ @events = apply_time_filter(Sincerely::EngagementEvent).order(created_at: :desc)
7
+ @events = apply_event_filter(@events)
8
+
9
+ @events = @events.where(event_type: params[:event_type]) if params[:event_type].present?
10
+ @events = @events.where('recipient LIKE ?', "%#{params[:recipient]}%") if params[:recipient].present?
11
+
12
+ # Filter by notification properties via message_id
13
+ if params[:template_id].present? || params[:notification_type].present?
14
+ notification_scope = notification_model.all
15
+ if params[:template_id].present?
16
+ notification_scope = notification_scope.where(template_id: params[:template_id])
17
+ end
18
+ if params[:notification_type].present?
19
+ notification_scope = notification_scope.where(notification_type: params[:notification_type])
20
+ end
21
+ message_ids = notification_scope.pluck(:message_id).compact
22
+ @events = @events.where(message_id: message_ids)
23
+ end
24
+
25
+ @pagination = paginate(@events)
26
+ @events = @pagination[:records]
27
+
28
+ @event_types = Sincerely::EngagementEvent.distinct.pluck(:event_type).compact
29
+ @templates = Sincerely::Templates::NotificationTemplate.all
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sincerely
4
+ class NotificationsController < ApplicationController
5
+ def index
6
+ @notifications = apply_time_filter(notification_model).order(created_at: :desc)
7
+
8
+ # Apply host app filter (e.g., filter by logged-in user's email)
9
+ @notifications = apply_notification_filter(@notifications)
10
+
11
+ # Apply filters
12
+ @notifications = @notifications.where(delivery_state: params[:status]) if params[:status].present?
13
+ @notifications = @notifications.where(template_id: params[:template_id]) if params[:template_id].present?
14
+ if params[:recipient].present?
15
+ @notifications = @notifications.where('recipient LIKE ?',
16
+ "%#{params[:recipient]}%")
17
+ end
18
+ if params[:notification_type].present?
19
+ @notifications = @notifications.where(notification_type: params[:notification_type])
20
+ end
21
+
22
+ @notifications = @notifications.where(created_at: Date.parse(params[:date_from])..) if params[:date_from].present?
23
+ if params[:date_to].present?
24
+ @notifications = @notifications.where(created_at: ..Date.parse(params[:date_to]).end_of_day)
25
+ end
26
+
27
+ @pagination = paginate(@notifications)
28
+ @notifications = @pagination[:records]
29
+ @templates = Sincerely::Templates::NotificationTemplate.all
30
+ @states = notification_model.aasm.states.map(&:name)
31
+ end
32
+
33
+ def show
34
+ @notification = apply_notification_filter(notification_model).find(params[:id])
35
+ @template = @notification.template
36
+ @timeline = build_event_timeline(@notification)
37
+ end
38
+
39
+ private
40
+
41
+ def build_event_timeline(notification)
42
+ events = []
43
+
44
+ Sincerely::DeliveryEvent.where(message_id: notification.message_id).find_each do |event|
45
+ events << {
46
+ type: event.event_type,
47
+ timestamp: event.timestamp || event.created_at,
48
+ details: format_event_details(event.options)
49
+ }
50
+ end
51
+
52
+ Sincerely::EngagementEvent.where(message_id: notification.message_id).find_each do |event|
53
+ events << {
54
+ type: event.event_type,
55
+ timestamp: event.timestamp || event.created_at,
56
+ details: format_event_details(event.options)
57
+ }
58
+ end
59
+
60
+ events.sort_by { |e| e[:timestamp] }.reverse
61
+ end
62
+
63
+ def format_event_details(data)
64
+ return nil if data.blank?
65
+
66
+ data.is_a?(Hash) ? data.map { |k, v| "#{k}: #{v}" }.join(', ') : data.to_s
67
+ end
68
+ end
69
+ end