web47core 0.0.8 → 0.0.9

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +0 -14
  3. data/Gemfile.lock +30 -9
  4. data/README.md +27 -4
  5. data/lib/app/models/concerns/app47_logger.rb +175 -0
  6. data/lib/app/models/concerns/core_account.rb +51 -0
  7. data/lib/app/models/concerns/standard_model.rb +104 -6
  8. data/lib/app/models/email_notification.rb +253 -0
  9. data/lib/app/models/email_template.rb +6 -0
  10. data/lib/app/models/notification.rb +276 -0
  11. data/lib/app/models/notification_template.rb +20 -0
  12. data/lib/app/models/slack_notification.rb +89 -0
  13. data/lib/app/models/sms_notification.rb +56 -0
  14. data/lib/app/models/smtp_configuration.rb +148 -0
  15. data/lib/app/models/template.rb +21 -0
  16. data/lib/templates/email/notification_failure.liquid +10 -0
  17. data/lib/templates/email/notification_failure.subject.liquid +1 -0
  18. data/lib/templates/slack/error_message.liquid +1 -0
  19. data/lib/web47core.rb +10 -2
  20. data/test/factories/account_factories.rb +9 -0
  21. data/test/factories/notification_factories.rb +14 -0
  22. data/test/models/app47_logger_test.rb +88 -0
  23. data/test/models/concerns/{formable_test.rb → standard_model_test.rb} +24 -5
  24. data/test/models/email_notification_test.rb +297 -0
  25. data/test/models/notification_test.rb +127 -0
  26. data/test/models/slack_notification_test.rb +50 -0
  27. data/test/notification_test_helper.rb +146 -0
  28. data/test/rails_setup.rb +4 -0
  29. data/test/test_helper.rb +10 -4
  30. data/test/test_models_helper.rb +14 -0
  31. data/web47core.gemspec +5 -2
  32. metadata +87 -14
  33. data/lib/app/models/concerns/auto_clear_cache.rb +0 -34
  34. data/lib/app/models/concerns/formable.rb +0 -111
  35. data/test/models/concerns/auto_clear_cache_test.rb +0 -27
@@ -0,0 +1,253 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # EmailNotification is a concrete class of Notification, sending emails to both members
5
+ # and users, or in the case of eCommerce, customers.
6
+ #
7
+ # This object allows you to create an EmailNotification object, set it's property and
8
+ # tell it send_notification. It'll handle everything from there including
9
+ # 1. Running in the background thread
10
+ # 2. Error recovery
11
+ # 3. Retry on failure
12
+ #
13
+ # The Email object also uses the templating engine to allow the specification of a template
14
+ # and a set of local variables for dynamic content.
15
+ #
16
+ # Usage:
17
+ #
18
+ # email = EmailNotification.new
19
+ # email.to = 'user@abc.com'
20
+ # email.sender = 'me@abc.com'
21
+ # email.subject = 'Today is the day!'
22
+ # email.message = 'Day is today!'
23
+ # email.send_notification
24
+ #
25
+ # You are done! Go about your business of creating awesome software!
26
+ #
27
+ class EmailNotification < Notification
28
+ # The From: "Joe Smith <joe@abc.com>"
29
+ field :from, type: String
30
+ # The Reply-To: "no-reply@abc.com"
31
+ field :reply_to, type: String
32
+ # The sender: "joe@abc.com"
33
+ field :sender, type: String
34
+ # The cc: header
35
+ field :cc, type: String
36
+ # The subject
37
+ field :subject, type: String
38
+ #
39
+ # Validations
40
+ #
41
+ validates :subject, presence: true
42
+ validates :to, presence: true
43
+
44
+ # has_many :attachments, class_name: 'EmailNotificationAttachment', dependent: :destroy
45
+
46
+ def from_template(template_name, locals = {})
47
+ super
48
+ self.subject = subject_from_template(template_name, locals)
49
+ end
50
+
51
+ #
52
+ # Add a file as an attachment to this email notification.
53
+ #
54
+ # Expecting a path to the file, not the file or data itself.
55
+ # TODO consider how to handle a file object or data object.
56
+ #
57
+ # Once you have called this method, the file is stored via PaperClip and
58
+ # does not need to stay persistant through email delivery.
59
+ # You can delete it after telling the EmailNotification to send
60
+ # the notification.
61
+ #
62
+ # def add_file(file)
63
+ # # Make sure we are saved
64
+ # save! unless persisted?
65
+ # attachment = EmailNotificationAttachment.new email_notification: self
66
+ # attachment.file = open(file)
67
+ # attachment.save!
68
+ # end
69
+ def deliver_message!
70
+ mail = Mail.new
71
+ # Set the from line
72
+ address = from_address
73
+ mail.from = address
74
+ self.from = address
75
+ # Set the sender line
76
+ address = sender_address
77
+ mail.sender = address
78
+ self.sender = address
79
+ # Set the reply to
80
+ address = reply_to_address
81
+ if address.present?
82
+ self.reply_to = address
83
+ mail.reply_to = address
84
+ mail.sender = address
85
+ end
86
+
87
+ # Set the to address
88
+ mail.to = to
89
+
90
+ # Set the cc line
91
+ mail.cc = cc unless cc.nil?
92
+
93
+ # Set the subject line
94
+ mail.subject = subject
95
+
96
+ # set the message body and send
97
+ html_message = build_message
98
+ mail.html_part do
99
+ content_type 'text/html; charset=UTF-8'
100
+ body html_message
101
+ end
102
+
103
+ # Add the attachments, if there are any. If not, none are added.
104
+ add_attachments(attachments) if defined?(attachments)
105
+
106
+ # Setup the delivery method for this message only.
107
+ if 'test'.eql?(ENV['RAILS_ENV'])
108
+ mail.delivery_method :test
109
+ else
110
+ mail.delivery_method :smtp, smtp_configuration
111
+ end
112
+
113
+ # Deliver it
114
+ mail.deliver
115
+ rescue StandardError => error
116
+ # We are catching and rethrowing this only to allow the temp directory to be cleaned up in the ensure block
117
+ raise error
118
+ ensure
119
+ FileUtils.remove_entry_secure(tmp_dir) if defined?(tmp_dir) && tmp_dir.present?
120
+ end
121
+
122
+ def add_attachments(attachments = nil)
123
+ tmp_dir = Dir.mktmpdir
124
+ attachments.each do |attachment|
125
+ # Create a temp directory, it'll get cleaned up in the rescue.
126
+ tmp_file_path = "#{tmp_dir}/#{attachment.file_file_name}"
127
+ File.open(tmp_file_path, 'w') do |f|
128
+ f.write(URI.parse(attachment.file_url).open.read.force_encoding('utf-16').encode)
129
+ end
130
+ mail.attachments[attachment.file_file_name] = {
131
+ mime_type: attachment.file_content_type,
132
+ content: File.open(tmp_file_path).read.force_encoding('utf-16').encode
133
+ }
134
+ end
135
+ end
136
+
137
+ #
138
+ # sender address
139
+ #
140
+ def sender_address
141
+ address = SystemConfiguration.smtp_user_name
142
+ unless account.nil?
143
+ account_smtp = account.fetch_smtp_configuration
144
+ address = account_smtp.username if account_smtp.use?
145
+ end
146
+ address
147
+ end
148
+
149
+ #
150
+ # From address
151
+ #
152
+ def from_address
153
+ return from if from.present?
154
+
155
+ address = SystemConfiguration.default_email
156
+ if account.present?
157
+ account_smtp = account.fetch_smtp_configuration
158
+ address = account_smtp.email_address if account_smtp.use?
159
+ end
160
+ address
161
+ end
162
+
163
+ #
164
+ # Reply to address
165
+ #
166
+ def reply_to_address
167
+ return reply_to if reply_to.present?
168
+
169
+ address = nil
170
+ unless account.nil?
171
+ smtp = account.fetch_smtp_configuration
172
+ address = smtp.reply_to_address if smtp.use? && !smtp.reply_to_address.nil? && !smtp.reply_to_address.empty?
173
+ end
174
+ address
175
+ end
176
+
177
+ #
178
+ # SMTP Configuration
179
+ #
180
+ def smtp_configuration
181
+ account.get_smtp_configuration.use? ? account_smtp_configuration : default_smtp_configuration
182
+ rescue StandardError
183
+ default_smtp_configuration
184
+ end
185
+
186
+ def account_smtp_configuration
187
+ smtp = account.fetch_smtp_configuration
188
+
189
+ config = {
190
+ address: smtp.server_name,
191
+ port: smtp.port,
192
+ authentication: smtp.authentication_method.to_sym,
193
+ enable_starttls_auto: smtp.ssl.eql?(true)
194
+ }
195
+ config[:domain] = smtp.domain unless smtp.domain.nil? || smtp.domain.empty?
196
+ config[:user_name] = smtp.username unless smtp.username.nil? || smtp.username.empty?
197
+ config[:password] = smtp.password unless smtp.password.nil? || smtp.password.empty?
198
+ config
199
+ end
200
+
201
+ def default_smtp_configuration
202
+ SystemConfiguration.smtp_configuration
203
+ end
204
+
205
+ #
206
+ # HTMLize messages
207
+ #
208
+ def build_message
209
+ clean_message = message.strip
210
+ clean_message = "<html><body><pre>#{clean_message}</pre></body></html>" unless clean_message.start_with?('<')
211
+ add_notification_tracker(clean_message)
212
+ end
213
+
214
+ def add_notification_tracker(html_message)
215
+ tracker_url = "#{SystemConfiguration.base_url}/notifications/#{id}/img"
216
+ return html_message if html_message.include?(tracker_url)
217
+
218
+ image_tag = "<img style='height:0;width:0;border:none;display:none;' src='#{tracker_url}' alt=''/></body>"
219
+ html_message.sub(%r{<\/body>}, image_tag)
220
+ end
221
+
222
+ def subject_from_haml_text(haml_text, locals)
223
+ locals[:base_url] = SystemConfiguration.base_url
224
+ haml_engine = Haml::Engine.new(haml_text)
225
+ self.subject = haml_engine.render(Object.new, stringify_all(locals))
226
+ end
227
+
228
+ def subject_from_liquid_text(liquid_text, locals)
229
+ self.subject = render_liquid_text(liquid_text, locals)
230
+ end
231
+
232
+ def subject_from_template(template_name, locals)
233
+ subject = account_subject_template(template_name) ||
234
+ default_subject_template(template_name) ||
235
+ template_from_file(template_name, prefix: 'subject')
236
+ return subject_from_liquid_text(subject, locals) if subject.present?
237
+
238
+ subject = template_from_file(template_name, format: 'haml', prefix: 'subject')
239
+ subject_from_haml_text(subject, locals) if subject.present?
240
+ end
241
+
242
+ def account_subject_template(template_name)
243
+ self.account.templates.emails.find_by(name: template_name.to_s).subject
244
+ rescue StandardError
245
+ nil
246
+ end
247
+
248
+ def default_subject_template(template_name)
249
+ EmailTemplate.where(account: nil, name: template_name.to_s).template
250
+ rescue StandardError
251
+ nil
252
+ end
253
+ end
@@ -0,0 +1,6 @@
1
+ class EmailTemplate < Template
2
+
3
+ field :subject, type: String
4
+
5
+ validates_presence_of :subject
6
+ end
@@ -0,0 +1,276 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ #
5
+ class Notification
6
+ include StandardModel
7
+ #
8
+ # Constants
9
+ #
10
+ STATE_INVALID = 'invalid'.freeze unless defined? STATE_INVALID
11
+ STATE_NEW = 'new'.freeze unless defined? STATE_NEW
12
+ STATE_PROCESSED = 'processed'.freeze unless defined? STATE_PROCESSED
13
+ STATE_PROCESSING = 'processing'.freeze unless defined? STATE_PROCESSING
14
+ STATE_RESUBMITTED = 'resubmitted'.freeze unless defined? STATE_RESUBMITTED
15
+ STATE_RETRYING = 'retrying'.freeze unless defined? STATE_RETRYING
16
+ STATE_SUBMITTED = 'submitted'.freeze unless defined? STATE_SUBMITTED
17
+ STATE_VIEWED = 'viewed'.freeze unless defined? STATE_VIEWED
18
+ unless defined? ALL_STATES
19
+ ALL_STATES = [STATE_INVALID,
20
+ STATE_NEW,
21
+ STATE_PROCESSED,
22
+ STATE_PROCESSING,
23
+ STATE_RESUBMITTED,
24
+ STATE_RETRYING,
25
+ STATE_SUBMITTED,
26
+ STATE_VIEWED].freeze
27
+ end
28
+ #
29
+ # Channels
30
+ #
31
+ DELIVERY_EMAIL = 'email'.freeze unless defined? DELIVERY_EMAIL
32
+ DELIVERY_SLACK = 'slack'.freeze unless defined? DELIVERY_SLACK
33
+ DELIVERY_SMS = 'sms'.freeze unless defined? DELIVERY_SMS
34
+ #
35
+ # Fields
36
+ #
37
+ field :retries, type: Integer, default: 0
38
+ field :state, type: String, default: STATE_NEW
39
+ field :to, type: String
40
+ field :message, type: String
41
+ field :error_message, type: String
42
+ field :last_viewed_at, type: Time
43
+ field :viewed_count, type: Integer, default: 0
44
+ #
45
+ # Relationships
46
+ #
47
+ belongs_to :account, inverse_of: :notifications, optional: true
48
+ belongs_to :notification_template, inverse_of: :notifications, optional: true
49
+ #
50
+ # Validations
51
+ #
52
+ validates :state, presence: true, inclusion: { in: ALL_STATES }
53
+ validates :message, presence: true
54
+
55
+ #
56
+ # Was this notification successful sent
57
+ #
58
+ def successful?
59
+ [STATE_PROCESSED, STATE_VIEWED].include? state
60
+ end
61
+
62
+ #
63
+ # If this notification can be deleted, we don't want to delete one that is currently being processed.
64
+ #
65
+ def deletable?
66
+ [STATE_PROCESSED, STATE_INVALID, STATE_NEW, STATE_VIEWED].include? state
67
+ end
68
+
69
+ #
70
+ # If this notification can be resent, we don't want to resend one that is currently being processed.
71
+ #
72
+ def sendable?
73
+ [STATE_PROCESSED, STATE_INVALID, STATE_NEW, STATE_VIEWED].include? state
74
+ end
75
+
76
+ #
77
+ # Mark this as viewed
78
+ #
79
+ def viewed
80
+ set(state: STATE_VIEWED, last_viewed_at: Time.now.utc, viewed_count: viewed_count + 1)
81
+ end
82
+
83
+ #
84
+ # Start processing the notification
85
+ #
86
+ def start_processing
87
+ set state: STATE_PROCESSING
88
+ end
89
+
90
+ #
91
+ # Set to retrying
92
+ #
93
+ def retry_delivery(message)
94
+ set error_message: message, retries: retries + 1, state: STATE_RETRYING
95
+ end
96
+
97
+ #
98
+ # Finish processing the notification successfully
99
+ #
100
+ def finish_processing(message = nil)
101
+ if message.present?
102
+ set state: STATE_INVALID, error_message: message
103
+ else
104
+ set state: STATE_PROCESSED, error_message: ''
105
+ end
106
+ end
107
+
108
+ #
109
+ # Send the notification
110
+ #
111
+ def send_notification
112
+ if state.eql? STATE_NEW
113
+ self.state = STATE_SUBMITTED
114
+ else
115
+ self.retries = 0
116
+ self.state = STATE_RESUBMITTED
117
+ self.error_message = 'Retrying'
118
+ end
119
+
120
+ begin
121
+ deliver_message if save!
122
+ rescue StandardError => error
123
+ finish_processing error.message
124
+ end
125
+ end
126
+
127
+ def deliver_message!
128
+ raise 'Incomplete class, concrete implementation should implment #deliver_message!'
129
+ end
130
+
131
+ def deliver_message
132
+ start_processing
133
+ deliver_message!
134
+ finish_processing
135
+ rescue StandardError => error
136
+ if retries > 10
137
+ log_error "Unable to process notification id: #{id}, done retrying", error
138
+ finish_processing "Failed final attempt: #{error.message}"
139
+ notify_failure(error)
140
+ else
141
+ log_error "Unable to process notification id: #{id}, retrying!!", error
142
+ retry_delivery("Failed attempt # #{retries}: #{error.message}")
143
+ delay(run_at: 10.minutes.from_now).deliver
144
+ end
145
+ end
146
+
147
+ handle_asynchronously :deliver_message, priority: 100
148
+
149
+ def from_template(template_name, locals = {})
150
+ locals[:base_url] = SystemConfiguration.base_url
151
+ self.message = message_from_template(template_name, locals)
152
+ end
153
+
154
+ #
155
+ # Retrieve the template from the account
156
+ #
157
+ # If the account does exists or the template in the account does exist, we catch the error and return nil
158
+ #
159
+ def account_message_template(template_name)
160
+ account.templates.find_by(name: template_name.to_s, _type: "Account#{delivery_channel.humanize}Template").template
161
+ rescue StandardError
162
+ nil
163
+ end
164
+
165
+ #
166
+ # Get the default template stored in the database that is not associated with an account
167
+ #
168
+ def default_message_template(template_name)
169
+ Template.find_by(account: nil, name: template_name.to_s).template
170
+ rescue StandardError
171
+ nil
172
+ end
173
+
174
+ #
175
+ # Retrieve the template out of the project
176
+ #
177
+ def template_from_file(template_name, format: 'liquid', prefix: nil)
178
+ file_name = [template_name, prefix, format].compact.join('.')
179
+ if File.exist?(Rails.root.join('lib', 'templates', delivery_channel, file_name))
180
+ File.open(Rails.root.join('lib', 'templates', delivery_channel, file_name))
181
+ else
182
+ File.read(File.join(__dir__, '../../lib', 'templates', delivery_channel, file_name))
183
+ end.read
184
+ rescue StandardError
185
+ nil
186
+ end
187
+
188
+ #
189
+ # Default delivery channel is email, override for sms, SMTP or other channels
190
+ #
191
+ def delivery_channel
192
+ DELIVERY_EMAIL
193
+ end
194
+
195
+ def message_from_template(template_name, locals)
196
+ template = account_message_template(template_name) || template_from_file(template_name)
197
+ return message_from_liquid_text(template, locals) if template.present?
198
+
199
+ template = template_from_file(template_name, format: 'haml')
200
+ message_from_haml_text(template, locals) if template.present?
201
+ end
202
+
203
+ def message_from_haml_text(haml_text, locals)
204
+ locals[:base_url] = SystemConfiguration.base_url
205
+ engine = Haml::Engine.new(haml_text)
206
+ self.message = engine.render(Object.new, stringify_all(locals))
207
+ end
208
+
209
+ def message_from_haml_file(file_name, locals)
210
+ message_from_haml_text(File.read(file_name), locals)
211
+ end
212
+
213
+ def message_from_liquid_text(liquid_text, locals)
214
+ self.message = render_liquid_text(liquid_text, locals)
215
+ end
216
+
217
+ #
218
+ # Render the given liquid text
219
+ #
220
+ def render_liquid_text(liquid_text, locals = {})
221
+ locals[:base_url] = SystemConfiguration.base_url
222
+ Liquid::Template.parse(liquid_text).render(stringify_all(locals))
223
+ end
224
+
225
+ #
226
+ # Convert all keys and values to strings for liquid sanity
227
+ #
228
+ def stringify_all(obj)
229
+ case obj
230
+ when Hash
231
+ result = {}
232
+ obj.each { |key, value| result[key.to_s] = stringify_all(value) }
233
+ when Array
234
+ result = []
235
+ obj.each { |value| result << stringify_all(value) }
236
+ when FalseClass
237
+ result = false
238
+ when TrueClass
239
+ result = true
240
+ else
241
+ result = obj.to_s
242
+ end
243
+ result
244
+ end
245
+
246
+ private
247
+
248
+ #
249
+ # Only send a message if this message is associated with an account
250
+ # and that has an active smtp configuration
251
+ # otherwise send it to support for the environment
252
+ #
253
+ def notify_failure(error)
254
+ if account.nil? || !account.fetch_or_build_smtp_configuration.use?
255
+ error_email = EmailNotification.new
256
+ params = { company_name: SystemConfiguration.company_name,
257
+ error_message: error.message,
258
+ notification_url: "#{SystemConfiguration.base_url}/stack/notifications/#{id}" }
259
+ error_email.from_template(AccountEmailTemplate::EMAIL_NOTIFICATION_FAILED, params)
260
+ error_email.to = SystemConfiguration.support_email
261
+ error_email.save!
262
+ error_email.deliver
263
+ else
264
+ account.smtp_admins.each do |admin|
265
+ error_email = EmailNotification.new
266
+ params = { company_name: SystemConfiguration.company_name,
267
+ error_message: error.message,
268
+ notification_url: "#{SystemConfiguration.base_url}/notifications/#{id}" }
269
+ error_email.from_template(AccountEmailTemplate::EMAIL_NOTIFICATION_FAILED, params)
270
+ error_email.to = admin.email
271
+ error_email.save!
272
+ error_email.deliver
273
+ end
274
+ end
275
+ end
276
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Base class for notification
5
+ #
6
+ class NotificationTemplate < Template
7
+ include StandardModel
8
+ #
9
+ # Fields
10
+ #
11
+ field :draft, type: Boolean, default: true
12
+ #
13
+ # Relationships
14
+ #
15
+ has_many :notifications, dependent: :nullify
16
+ #
17
+ # Validations
18
+ #
19
+ validates_presence_of :draft
20
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # SlackNotification is a concrete class of Notification, sending notifications to the slack
5
+ # system. Initially this will be only used internally, however we could configure this
6
+ # to work with an account's slack configuration.
7
+ #
8
+ # This object allows you to create an EmailNotification object, set it's property and
9
+ # tell it send_notification. It'll handle everything from there including
10
+ # 1. Running in the background thread
11
+ # 2. Error recovery
12
+ # 3. Retry on failure
13
+ #
14
+ # Usage:
15
+ #
16
+ # SlackNotification.say 'Error getting data from resource'
17
+ #
18
+ # or
19
+ # To change the channel it's posted to...
20
+ #
21
+ # SlackNotification.say 'Error getting data from resource', to: 'another_channel'
22
+ #
23
+ # or
24
+ # To who the message is from, the default is the environment name
25
+ #
26
+ # SlackNotification.say 'Error getting data from resource', to: 'another_channel', from: 'someone else'
27
+ #
28
+ # You are done! Go about your business of creating awesome software!
29
+ #
30
+ class SlackNotification < Notification
31
+ # The slack username to use
32
+ field :from, type: String
33
+
34
+ def deliver_message!
35
+ return unless SystemConfiguration.slack_configured?
36
+
37
+ start_processing
38
+ payload = { text: message }
39
+ payload[:channel] = if to.present?
40
+ to
41
+ elsif SystemConfiguration.slack_support_channel.present?
42
+ SystemConfiguration.slack_support_channel
43
+ else
44
+ 'support'
45
+ end
46
+ # Use the environment as the default, otherwise set it as the from
47
+ payload[:username] = from.presence || Rails.env
48
+ # Setup the delivery method for this message only.
49
+ RestClient.post(SystemConfiguration.slack_api_url, payload.to_json)
50
+ finish_processing
51
+ rescue StandardError => error
52
+ App47Logger.log_debug '!!! Error sending SLACK notification !!!'
53
+ App47Logger.log_debug error.message
54
+ App47Logger.log_debug error.backtrace
55
+ App47Logger.log_debug '!!! Error sending SLACK notification !!!'
56
+ finish_processing error.message
57
+ end
58
+
59
+ #
60
+ # Limit the number of times the slack notification is retried
61
+ #
62
+ def max_retries
63
+ 1
64
+ end
65
+
66
+ #
67
+ # Default delivery channel is email, override for sms, SMTP or other channels
68
+ #
69
+ def delivery_channel
70
+ DELIVERY_SLACK
71
+ end
72
+
73
+ #
74
+ # Convenience method to say something when we see something
75
+ #
76
+ def self.say(message, to: nil, from: nil, template: nil)
77
+ return unless SystemConfiguration.slack_configured?
78
+
79
+ notification = SlackNotification.new(to: to, from: from)
80
+ notification.message = if template.present?
81
+ notification.message_from_template template, message
82
+ elsif message.is_a?(Array)
83
+ message.join("\n")
84
+ else
85
+ message
86
+ end
87
+ notification.send_notification
88
+ end
89
+ end
@@ -0,0 +1,56 @@
1
+ #
2
+ # SmsNotification is a concrete class of Notification, sending sms to both members
3
+ # and users, or in the case of eCommerce, customers.
4
+ #
5
+ # This object allows you to create an SmsNotification object, set it's property and
6
+ # tell it send_notification. It'll handle everything from there including
7
+ # 1. Running in the background thread
8
+ # 2. Error recovery
9
+ # 3. Retry on failure
10
+ #
11
+ # The SMS object also uses the templating engine to allow the specification of a template
12
+ # and a set of local variables for dynamic content.
13
+ #
14
+ # Usage:
15
+ #
16
+ # sms = SmsNotification.new
17
+ # sms.to = '5713326267'
18
+ # sms.message = 'Day is today!'
19
+ # sms.send_notification
20
+ #
21
+ # You are done! Go about your business of creating awesome software!
22
+ #
23
+ class SmsNotification < Notification
24
+ #
25
+ # Fields
26
+ #
27
+ field :sid, type: String
28
+
29
+ validates_format_of :to, with: %r{\A\+[1-9]{1}[0-9]{3,14}\z}, message: 'Invalid phone number'
30
+ validates_presence_of :to
31
+ validates_presence_of :account_id
32
+
33
+ def deliver_message!
34
+ return unless SystemConfiguration.twilio_configured?
35
+
36
+ config = SystemConfiguration.configuration
37
+ account_sid = config.twilio_account_id
38
+ auth_token = config.twilio_auth_token
39
+ client = Twilio::REST::Client.new account_sid, auth_token
40
+
41
+ message = client.account.messages.create(
42
+ body: self.message,
43
+ to: self.to,
44
+ from: config.twilio_phone_number
45
+ )
46
+ # We are saved in the calling class, no reason to save again
47
+ set sid: message.sid
48
+ end
49
+
50
+ #
51
+ # set the delivery channel for the templates
52
+ #
53
+ def delivery_channel
54
+ DELIVERY_SMS
55
+ end
56
+ end