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
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sincerely
4
+ class SendController < ApplicationController
5
+ MAX_RECIPIENTS = 50
6
+
7
+ def new
8
+ @templates = Sincerely::Templates::NotificationTemplate.order(:name)
9
+ end
10
+
11
+ def create
12
+ recipients = parse_recipients(params[:recipients])
13
+
14
+ if recipients.empty?
15
+ render json: { success: false, error: 'No valid recipients provided' }, status: :unprocessable_entity
16
+ return
17
+ end
18
+
19
+ if recipients.size > MAX_RECIPIENTS
20
+ render json: { success: false, error: "Maximum #{MAX_RECIPIENTS} recipients allowed" },
21
+ status: :unprocessable_entity
22
+ return
23
+ end
24
+
25
+ template = Sincerely::Templates::NotificationTemplate.find_by(id: params[:template_id])
26
+ unless template
27
+ render json: { success: false, error: 'Template not found' }, status: :unprocessable_entity
28
+ return
29
+ end
30
+
31
+ template_data = params[:template_data]&.to_unsafe_h || {}
32
+
33
+ results = { sent: 0, failed: 0, errors: [] }
34
+
35
+ recipients.each do |recipient|
36
+ notification = notification_model.new(
37
+ recipient:,
38
+ notification_type: 'email',
39
+ template_id: template.id,
40
+ delivery_options: { template_data: }
41
+ )
42
+
43
+ if notification.save
44
+ begin
45
+ notification.deliver
46
+ results[:sent] += 1
47
+ rescue StandardError => e
48
+ results[:failed] += 1
49
+ results[:errors] << { recipient:, error: e.message }
50
+ end
51
+ else
52
+ results[:failed] += 1
53
+ results[:errors] << { recipient:, error: notification.errors.full_messages.join(', ') }
54
+ end
55
+ end
56
+
57
+ render json: {
58
+ success: true,
59
+ sent: results[:sent],
60
+ failed: results[:failed],
61
+ errors: results[:errors].first(5) # Limit error details
62
+ }
63
+ end
64
+
65
+ def template_variables
66
+ template = Sincerely::Templates::NotificationTemplate.find_by(id: params[:id])
67
+
68
+ unless template
69
+ render json: { variables: [] }
70
+ return
71
+ end
72
+
73
+ variables = extract_liquid_variables(template)
74
+ render json: { variables: variables.uniq.sort }
75
+ end
76
+
77
+ private
78
+
79
+ def parse_recipients(input)
80
+ return [] if input.blank?
81
+
82
+ # Split by comma, semicolon, space, or newline
83
+ input.split(/[\s,;\n]+/)
84
+ .map(&:strip)
85
+ .compact_blank
86
+ .grep(/\A[^@\s]+@[^@\s]+\z/)
87
+ .uniq
88
+ .first(MAX_RECIPIENTS)
89
+ end
90
+
91
+ def extract_liquid_variables(template)
92
+ content = [
93
+ template.subject,
94
+ template.html_content,
95
+ template.text_content
96
+ ].compact.join(' ')
97
+
98
+ # Extract {{ variable }} and {{ variable.property }} patterns
99
+ content.scan(/\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*)\s*\}\}/)
100
+ .flatten
101
+ .map { |v| v.split('.').first } # Get root variable name
102
+ .uniq
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sincerely
4
+ class TemplatesController < ApplicationController
5
+ before_action :set_template, only: %i[show edit update preview]
6
+
7
+ def index
8
+ @pagination = paginate(Sincerely::Templates::NotificationTemplate.order(created_at: :desc))
9
+ @templates = @pagination[:records]
10
+ end
11
+
12
+ def show
13
+ @notification_count = notification_model.where(template_id: @template.id).count
14
+ end
15
+
16
+ def new
17
+ @template = Sincerely::Templates::EmailLiquidTemplate.new
18
+ end
19
+
20
+ def edit; end
21
+
22
+ def create
23
+ @template = Sincerely::Templates::EmailLiquidTemplate.new(template_params)
24
+
25
+ if @template.save
26
+ redirect_to template_path(@template), notice: 'Template created successfully'
27
+ else
28
+ render :new, status: :unprocessable_entity
29
+ end
30
+ end
31
+
32
+ def update
33
+ if @template.update(template_params)
34
+ redirect_to template_path(@template), notice: 'Template updated successfully'
35
+ else
36
+ render :edit, status: :unprocessable_entity
37
+ end
38
+ end
39
+
40
+ def preview
41
+ sample_data = params[:sample_data].present? ? JSON.parse(params[:sample_data]) : {}
42
+ @rendered_subject = @template.renderer.render(@template.subject || '', { 'template_data' => sample_data })
43
+ @rendered_html = @template.render(:html, { 'template_data' => sample_data })
44
+ @rendered_text = @template.render(:text, { 'template_data' => sample_data })
45
+
46
+ render layout: false
47
+ rescue JSON::ParserError
48
+ render plain: 'Invalid JSON in sample data', status: :bad_request
49
+ end
50
+
51
+ private
52
+
53
+ def set_template
54
+ @template = Sincerely::Templates::NotificationTemplate.find(params[:id])
55
+ end
56
+
57
+ def template_params
58
+ params.require(:template).permit(:name, :subject, :sender, :html_content, :text_content)
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sincerely
4
+ module ApplicationHelper
5
+ def state_badge_class(state)
6
+ case state.to_s
7
+ when 'draft' then 'badge-draft'
8
+ when 'accepted' then 'badge-accepted'
9
+ when 'delivered' then 'badge-delivered'
10
+ when 'opened' then 'badge-opened'
11
+ when 'clicked' then 'badge-clicked'
12
+ when 'bounced' then 'badge-bounced'
13
+ when 'complained' then 'badge-complained'
14
+ when 'rejected' then 'badge-rejected'
15
+ when 'delayed' then 'badge-delayed'
16
+ else 'badge-default'
17
+ end
18
+ end
19
+
20
+ def format_timestamp(time)
21
+ return '-' unless time
22
+
23
+ time.strftime('%b %d, %Y %H:%M:%S')
24
+ end
25
+
26
+ def event_type_badge_class(event_type)
27
+ case event_type.to_s.downcase
28
+ when 'delivery', 'send' then 'badge-delivered'
29
+ when 'open' then 'badge-opened'
30
+ when 'click' then 'badge-clicked'
31
+ when 'bounce', 'hard_bounce', 'soft_bounce' then 'badge-bounced'
32
+ when 'complaint', 'spam_complaint' then 'badge-complained'
33
+ when 'reject' then 'badge-rejected'
34
+ when 'delay' then 'badge-delayed'
35
+ else 'badge-default'
36
+ end
37
+ end
38
+ end
39
+ end