Class: CampaignEmail
Overview
== Schema Information
Table name: campaign_actions
Database name: primary
id :integer not null, primary key
description :text
frequency :integer
last_transmitted :datetime
name :string
scheduled_time :datetime
sender_email :string
sequence :integer
state :string
type :string
created_at :datetime not null
updated_at :datetime not null
campaign_id :integer
creator_id :integer
email_template_id :integer
sender_id :integer
source_id :integer
updater_id :integer
Indexes
campaign_actions_campaign_id_idx (campaign_id)
campaign_actions_email_template_id_idx (email_template_id)
idx_type (type)
Foreign Keys
fk_rails_... (campaign_id => campaigns.id)
fk_rails_... (email_template_id => email_templates.id)
Constant Summary
collapse
- FREQUENCIES =
{ 'daily' => 86_400, 'weekly' => 604_800 }.freeze
- SPECIAL_SENDERS =
{ 'Social' => 'social@warmlyyours.com' }.freeze
- STATES =
:percentage_for_processed,
:percentage_for_deferred,
:percentage_for_delivered,
:percentage_for_open,
:percentage_for_click,
:percentage_for_bounce,
:percentage_for_dropped,
:percentage_for_spamreport,
:percentage_for_unsubscribe,
%w[
pending
queued
exception
suppressed
duplicate
deferred
dropped
bounced
sent
processed
delivered
opened
clicked
spammed
unsubscribed
].freeze
Models::Auditable::ALWAYS_IGNORED
Models::Schedulable::SIMPLE_FORM_OPTIONS
Instance Attribute Summary collapse
#creator, #updater
Has and belongs to many
collapse
Class Method Summary
collapse
Instance Method Summary
collapse
#all_skipped_columns, #audit_reference_data, #should_not_save_version, #stamp_record
ransackable_associations, ransackable_attributes, ransackable_scopes, ransortable_attributes, #to_relation
config
#after_commit
#publish_event
Instance Attribute Details
#clone_email_template_id ⇒ Object
Returns the value of attribute clone_email_template_id.
42
43
44
|
# File 'app/models/campaign_email.rb', line 42
def clone_email_template_id
@clone_email_template_id
end
|
#name ⇒ Object
before_destroy :can_be_destroyed?
Validations:
95
|
# File 'app/models/campaign_email.rb', line 95
validates :email_template, :name, presence: true
|
#sender_email ⇒ Object
96
|
# File 'app/models/campaign_email.rb', line 96
validates :sender_email, presence: true
|
Class Method Details
.combined_states_for_select ⇒ Object
221
222
223
|
# File 'app/models/campaign_email.rb', line 221
def self.combined_states_for_select
STATES.map { |state| [state.to_s.titleize, state] }
end
|
.frequencies_select ⇒ Object
217
218
219
|
# File 'app/models/campaign_email.rb', line 217
def self.frequencies_select
FREQUENCIES.map { |text, seconds| [text, seconds] }
end
|
.ready_to_be_transmitted ⇒ ActiveRecord::Relation<CampaignEmail>
A relation of CampaignEmails that are ready to be transmitted. Active Record Scope
109
|
# File 'app/models/campaign_email.rb', line 109
scope :ready_to_be_transmitted, -> { joins(:campaign).where(campaigns: { state: 'active' }, state: 'scheduled').where(CampaignEmail[:scheduled_time].lteq(Time.current)) }
|
.send_monthly_summary_email(date_start: nil, date_end: nil) ⇒ Object
205
206
207
208
209
210
211
212
213
214
215
|
# File 'app/models/campaign_email.rb', line 205
def self.send_monthly_summary_email(date_start: nil, date_end: nil)
date_start ||= Date.current.beginning_of_month
date_end ||= date_start.end_of_month
date_range = (date_start..date_end)
campaign_emails = CampaignEmail.where(last_transmitted: date_range).joins(:campaign).where('last_transmitted between ? and ? and exclude_from_monthly_report = false', Date.current.last_month.beginning_of_month.beginning_of_day,
Date.current.last_month.end_of_month.end_of_day).order(last_transmitted: :asc).to_a
return 'No emails sent last month' if campaign_emails.blank?
InternalReportsMailer.campaign_summary(campaign_emails).deliver
end
|
.sender_options ⇒ Object
199
200
201
202
203
|
# File 'app/models/campaign_email.rb', line 199
def self.sender_options
(Employee.includes(:employee_account).active_employees.map(&:email_with_name) + CampaignEmail::SPECIAL_SENDERS.map { |name, email| "#{name} <#{email}>" }).compact_blank.sort_by { |s| s[0] }
end
|
Instance Method Details
#audiences ⇒ ActiveRecord::Relation<Audience>
Optional per-email recipient override. When any list is attached, this email
sends to those lists instead of the campaign's own lists (see
#recipient_audiences). The join is on the STI campaign_actions table.
83
84
85
86
|
# File 'app/models/campaign_email.rb', line 83
has_and_belongs_to_many :audiences,
join_table: :audiences_campaign_actions,
foreign_key: :campaign_action_id,
association_foreign_key: :audience_id
|
77
|
# File 'app/models/campaign_email.rb', line 77
belongs_to :campaign, inverse_of: :campaign_emails, optional: true
|
#campaign_deliveries ⇒ ActiveRecord::Relation<CampaignDelivery>
87
|
# File 'app/models/campaign_email.rb', line 87
has_many :campaign_deliveries
|
#can_be_destroyed? ⇒ Boolean
277
278
279
280
281
282
283
284
|
# File 'app/models/campaign_email.rb', line 277
def can_be_destroyed?
if (unscheduled? || scheduled?) && campaign_deliveries.empty?
true
else
errors.add :base, 'cannot delete campaign email which has already been sent'
false
end
end
|
#can_be_sent? ⇒ Boolean
331
332
333
|
# File 'app/models/campaign_email.rb', line 331
def can_be_sent?
unscheduled? or scheduled?
end
|
#can_be_transmitted? ⇒ Boolean
327
328
329
|
# File 'app/models/campaign_email.rb', line 327
def can_be_transmitted?
scheduled_time.present? and scheduled_time <= Time.current
end
|
#communication_recipients ⇒ ActiveRecord::Relation<CommunicationRecipient>
88
|
# File 'app/models/campaign_email.rb', line 88
has_many :communication_recipients, through: :campaign_deliveries
|
#cost ⇒ Object
286
287
288
|
# File 'app/models/campaign_email.rb', line 286
def cost
communication_recipients.count * 0.0008 end
|
#delivery_funnel_counts ⇒ Object
Returns a monotonic funnel of counts for easier human interpretation.
Keys are: :total_recipients, :suppressed, :processed, :bounced, :delivered, :opened, :clicked, :unsubscribed, :spammed, :tracking_disabled
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
|
# File 'app/models/campaign_email.rb', line 363
def delivery_funnel_counts
suppressed = campaign_deliveries.where(state: 'suppressed').count
processed_total = communication_recipients.count
total_recipients = suppressed + processed_total
bounced_total = communication_recipients.where(state: 'bounced').count
delivered_total = communication_recipients.where(state: %w[delivered opened clicked spammed unsubscribed]).count
opened_total = communication_recipients.where(state: %w[opened clicked]).count
machine_opened_total = communication_recipients.where(state: %w[opened clicked], machine_open: true).count
human_opened_total = opened_total - machine_opened_total
clicked_total = communication_recipients.where(state: 'clicked').count
machine_clicked_total = communication_recipients.where(state: 'clicked', machine_clicked: true).count
human_clicked_total = clicked_total - machine_clicked_total
unsubscribed_total = communication_recipients.where(state: 'unsubscribed').count
spammed_total = communication_recipients.where(state: 'spammed').count
tracking_disabled = communication_recipients.where(tracking_disabled: true).count
{
total_recipients: total_recipients,
suppressed: suppressed,
processed: processed_total,
bounced: bounced_total,
delivered: delivered_total,
opened: opened_total,
machine_opened: machine_opened_total,
human_opened: human_opened_total,
clicked: clicked_total,
machine_clicked: machine_clicked_total,
human_clicked: human_clicked_total,
unsubscribed: unsubscribed_total,
spammed: spammed_total,
tracking_disabled: tracking_disabled
}
end
|
#delivery_percentages ⇒ Object
346
347
348
349
350
351
352
353
354
355
356
357
358
|
# File 'app/models/campaign_email.rb', line 346
def delivery_percentages
hsh = {}
total = total_deliveries_count
delivery_stats.each do |state, counter|
percentage = if counter > 0 && total > 0
((counter.to_f / total) * 100).round(2)
else
0.0
end
hsh[state] = percentage
end
hsh
end
|
#delivery_stats ⇒ Object
335
336
337
338
|
# File 'app/models/campaign_email.rb', line 335
def delivery_stats
hsh = view_campaign_deliveries.group(:combined_state).count
hsh.slice(*STATES) end
|
Validations:
78
|
# File 'app/models/campaign_email.rb', line 78
belongs_to :email_template, optional: true
|
#email_template_not_used_by_another_campaign ⇒ Object
424
425
426
427
428
429
430
431
432
433
434
|
# File 'app/models/campaign_email.rb', line 424
def email_template_not_used_by_another_campaign
clash = CampaignEmail.where(email_template_id: email_template_id)
clash = clash.where.not(id: id) if persisted?
clash = clash.where.not(campaign_id: campaign_id).includes(:campaign).first
return unless clash
errors.add(:email_template,
"is already used by campaign '#{clash.campaign&.name || clash.campaign_id}' " \
"(campaign email ##{clash.id}) — a template can only be linked to one campaign. " \
'Clone the template instead.')
end
|
#estimated_audience_size ⇒ Integer
Estimated audience size before transmission, mirroring the audience_members
used by #prepare_campaign_deliveries. Does not include audience_members
that would be added by generate_dynamic_audience_members at send time.
266
267
268
|
# File 'app/models/campaign_email.rb', line 266
def estimated_audience_size
recipient_audience_members.active.distinct.count
end
|
#frequency_description ⇒ Object
297
298
299
|
# File 'app/models/campaign_email.rb', line 297
def frequency_description
frequency.nil? ? 'One time' : FREQUENCIES.find { |_k, v| v == frequency }[0]
end
|
#generate_dynamic_audience_members ⇒ Object
305
306
307
|
# File 'app/models/campaign_email.rb', line 305
def generate_dynamic_audience_members
recipient_audiences.where(list_type: 'dynamic').find_each(&:generate_audience_members)
end
|
#generate_source ⇒ Object
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
|
# File 'app/models/campaign_email.rb', line 436
def generate_source
return if campaign&.source_id.nil?
s = Source.new
s.parent_id = campaign.source_id
s.name = name
s.referral_code = s.generate_ref_code
if s.save
self.source_id = s.id
true
else
errors.add(:base, "Unable to create source. Error: #{s.errors.full_messages}")
false
end
end
|
230
231
232
|
# File 'app/models/campaign_email.rb', line 230
def last_transmitted_formatted
last_transmitted.to_fs(:compact)
end
|
#prepare_campaign_deliveries ⇒ Object
309
310
311
312
313
314
315
316
317
318
319
320
|
# File 'app/models/campaign_email.rb', line 309
def prepare_campaign_deliveries
generate_dynamic_audience_members
records = recipient_audience_members.active.ids.map do |audience_member_id|
{
campaign_email_id: id,
audience_member_id: audience_member_id,
state: 'pending'
}
end
CampaignDelivery.insert_all(records, unique_by: %i[campaign_email_id audience_member_id]) if records.any?
end
|
#recipient_audience_members ⇒ ActiveRecord::Relation<AudienceMember>
256
257
258
|
# File 'app/models/campaign_email.rb', line 256
def recipient_audience_members
AudienceMember.where(audience_id: recipient_audiences.select(:id))
end
|
#recipient_audiences ⇒ ActiveRecord::Relation<Audience>
The audiences this email actually sends to: its own override lists
when any are attached, otherwise the campaign's lists. Override lists need
not belong to the campaign — that's the point (e.g. a webinar-attendee
thank-you list separate from the invitee lists).
244
245
246
|
# File 'app/models/campaign_email.rb', line 244
def recipient_audiences
audiences.any? ? audiences : campaign.audiences
end
|
#recipient_count ⇒ Object
234
235
236
|
# File 'app/models/campaign_email.rb', line 234
def recipient_count
communication_recipients.count
end
|
#recipient_override? ⇒ Boolean
Returns whether a per-email recipient override is in effect.
249
250
251
|
# File 'app/models/campaign_email.rb', line 249
def recipient_override?
audiences.any?
end
|
#roi ⇒ Object
291
292
293
294
295
|
# File 'app/models/campaign_email.rb', line 291
def roi
return profit if cost.zero?
((profit - cost) / cost) * 100
end
|
#scheduled_time_description ⇒ Object
301
302
303
|
# File 'app/models/campaign_email.rb', line 301
def scheduled_time_description
frequency.nil? ? 'Scheduled Time' : 'Next Scheduled Time'
end
|
#send_emails ⇒ Object
322
323
324
325
|
# File 'app/models/campaign_email.rb', line 322
def send_emails
prepare_campaign_deliveries if campaign_deliveries.blank?
CampaignDeliveryWorker.perform_async
end
|
#sender ⇒ Object
225
226
227
228
|
# File 'app/models/campaign_email.rb', line 225
def sender
email = Mail::Address.new(sender_email).address
email.nil? ? nil : Employee.joins(:employee_account).where(accounts: { email: email }).first
end
|
#set_new_scheduled_time ⇒ Object
270
271
272
273
274
275
|
# File 'app/models/campaign_email.rb', line 270
def set_new_scheduled_time
return if frequency.blank?
update(scheduled_time: last_transmitted + frequency)
reschedule
end
|
79
|
# File 'app/models/campaign_email.rb', line 79
belongs_to :source, optional: true
|
#total_deliveries_count ⇒ Object
341
342
343
|
# File 'app/models/campaign_email.rb', line 341
def total_deliveries_count
delivery_stats.values.sum
end
|
#update_email_template ⇒ Object
418
419
420
421
422
|
# File 'app/models/campaign_email.rb', line 418
def update_email_template
return unless email_template&.new_record?
email_template.description = "Template for campaign email #{name}"
end
|
#view_campaign_deliveries ⇒ ActiveRecord::Relation<ViewCampaignDelivery>
89
|
# File 'app/models/campaign_email.rb', line 89
has_many :view_campaign_deliveries
|