Class: Campaign
- Inherits:
-
ApplicationRecord
- Object
- ActiveRecord::Base
- ApplicationRecord
- Campaign
- Includes:
- Models::Auditable, Models::LiquidMethods
- Defined in:
- app/models/campaign.rb
Overview
== Schema Information
Table name: campaigns
Database name: primary
id :integer not null, primary key
active_since :datetime
auto_assign_source :boolean
auto_assign_until :date
auto_enroll :boolean default(FALSE)
campaign_type :string
category :string
description :text
end_date :date
exclude_from_monthly_report :boolean default(FALSE), not null
name :string(255) not null
start_date :date
state :string
created_at :datetime
updated_at :datetime
creator_id :integer
customer_filter_id :integer
source_id :integer
updater_id :integer
Indexes
idx_campaign_type (campaign_type)
idx_campaigns_state (state)
index_campaigns_on_customer_filter_id (customer_filter_id)
index_campaigns_on_source_id (source_id)
Foreign Keys
fk_rails_... (customer_filter_id => customer_filters.id)
Defined Under Namespace
Classes: AddCustomerResult, AssignCustomersToAudience, AssignDripCampaigns, DripContext
Constant Summary collapse
- CATEGORIES =
Categories.
%w[announcements events newsletters promotions webinars].freeze
- CAMPAIGN_TYPES =
Recognised campaign types.
eventcampaigns represent a trade show. They point at an EXISTING source
(the show, underTrade Show > …) rather than generating one under
Campaigns > …, whichbefore_create :generate_sourcealready skips when
source_id is set. They exist so attendance is recordable as a touch —
see Marketing::RecordEventAttendance — and so shows appear in the influence
report, which keys on campaigns. %w[email_marketing outside_sales event].freeze
Constants included from Models::Auditable
Models::Auditable::ALWAYS_IGNORED
Constants included from Models::Schedulable
Models::Schedulable::SIMPLE_FORM_OPTIONS
Instance Attribute Summary collapse
-
#auto_assign_window_days ⇒ Object
readonly
Validates auto assign window days.
-
#campaign_type ⇒ Object
readonly
Validates name, campaign type.
-
#category ⇒ Object
readonly
Validates category.
-
#name ⇒ Object
readonly
Validates name, campaign type.
Belongs to collapse
-
#customer_filter ⇒ CustomerFilter?
The customer filter this record belongs to.
-
#source ⇒ Source?
The source this record belongs to.
Methods included from Models::Auditable
Has and belongs to many collapse
-
#audiences ⇒ ActiveRecord::Relation<Audience>
The associated audiences.
Has many collapse
-
#activities ⇒ ActiveRecord::Relation<Activity>
The associated activities.
-
#activity_chain_types ⇒ ActiveRecord::Relation<ActivityChainType>
The associated activity chain types.
-
#audience_members ⇒ ActiveRecord::Relation<AudienceMember>
The associated audience members.
-
#campaign_deliveries ⇒ ActiveRecord::Relation<CampaignDelivery>
The associated campaign deliveries.
-
#campaign_emails ⇒ ActiveRecord::Relation<CampaignEmail>
The associated campaign emails.
Class Method Summary collapse
-
.active ⇒ ActiveRecord::Relation<Campaign>
A relation of Campaigns that are active.
-
.active_options_for_select ⇒ Array<Array>
[label, id]pairs for select dropdowns. -
.merge_audiences(campaign_ids: nil) ⇒ Object
Merge audiences.
-
.options_for_select ⇒ Array<Array>
[label, id]pairs for select dropdowns. -
.outside_sales ⇒ ActiveRecord::Relation<Campaign>
A relation of Campaigns that are outside sales.
-
.states_for_select ⇒ Object
States for select.
-
.with_campaign_emails_count ⇒ ActiveRecord::Relation<Campaign>
A relation of Campaigns that are with campaign emails count.
-
.with_email_stats ⇒ ActiveRecord::Relation<Campaign>
A relation of Campaigns that are with email stats.
-
.with_last_created_at_email ⇒ ActiveRecord::Relation<Campaign>
A relation of Campaigns that are with last created at email.
Instance Method Summary collapse
-
#add_customer(customer) ⇒ Object
Add customer.
-
#audience_member_count ⇒ Object
Audience member count.
-
#audiences_all_customer ⇒ Object
Audiences all customer.
-
#audiences_all_email ⇒ Object
Audiences all email.
-
#campaign_source_name_available ⇒ Object
generate_source creates an active child Source named
nameunder the Email / Outside Sales parent. -
#can_be_deativated? ⇒ Boolean
Whether the record can be deativated.
-
#check_for_active_campaign_emails ⇒ Object
Check for active campaign emails.
-
#delivery_stats ⇒ Object
Delivery stats.
-
#email_marketing? ⇒ Boolean
Whether the record email marketing.
-
#generate_source ⇒ Object
Generate source.
-
#merge_audiences ⇒ Object
Merge audiences.
-
#name_and_type ⇒ Object
Name and type.
-
#outside_sales? ⇒ Boolean
Whether the record outside sales.
-
#source_assignment_window_open_for?(customer, at: Time.current) ⇒ Boolean
Whether the customer's per-member source-assignment window is open at the given time — i.e.
-
#to_s ⇒ String
String representation of the record.
-
#unique_audience_members ⇒ Object
Unique audience members.
-
#update_source_name ⇒ Object
Update source name.
Methods included from Models::Auditable
#all_skipped_columns, #audit_reference_data, #should_not_save_version, #stamp_record
Methods inherited from ApplicationRecord
ransackable_associations, ransackable_attributes, ransackable_scopes, ransortable_attributes, #to_relation
Methods included from Models::Schedulable
Methods included from Models::AfterCommittable
Methods included from Models::EventPublishable
Instance Attribute Details
#auto_assign_window_days ⇒ Object (readonly)
Validates auto assign window days.
Validations:
- Numericality ({ only_integer: true, greater_than: 0 })
91 |
# File 'app/models/campaign.rb', line 91 validates :auto_assign_window_days, numericality: { only_integer: true, greater_than: 0 } |
#campaign_type ⇒ Object (readonly)
Validates name, campaign type.
Validations:
89 |
# File 'app/models/campaign.rb', line 89 validates :name, :campaign_type, presence: true |
#category ⇒ Object (readonly)
Validates category.
Validations (if => proc { |c| c.campaign_type == 'email_marketing' } ):
Validations:
- Inclusion ({ in: CATEGORIES })
- Allow_nil
93 |
# File 'app/models/campaign.rb', line 93 validates :category, presence: true, if: proc { |c| c.campaign_type == 'email_marketing' } |
#name ⇒ Object (readonly)
Validates name, campaign type.
Validations:
89 |
# File 'app/models/campaign.rb', line 89 validates :name, :campaign_type, presence: true |
Class Method Details
.active ⇒ ActiveRecord::Relation<Campaign>
A relation of Campaigns that are active. Active Record Scope
103 |
# File 'app/models/campaign.rb', line 103 scope :active, -> { where(state: 'active') } |
.active_options_for_select ⇒ Array<Array>
Returns [label, id] pairs for select dropdowns.
216 217 218 |
# File 'app/models/campaign.rb', line 216 def self. where(state: 'active').order(:name).map { |c| [c.name_and_type, c.id] } end |
.merge_audiences(campaign_ids: nil) ⇒ Object
Merge audiences.
161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 |
# File 'app/models/campaign.rb', line 161 def self.merge_audiences(campaign_ids: nil) unshared_audiences = Audience .customers .where(%{ (select count(*) from audiences_campaigns csl where csl.audience_id = audiences.id) = 1}) campaigns = Campaign.joins(:audiences) .merge(unshared_audiences) .where(%{ (select count(*) from audiences_campaigns csl inner join audiences sl on sl.id = csl.audience_id where sl.list_type = 'customer' and csl.campaign_id = campaigns.id) > 1 }) campaigns = campaigns.where(id: campaign_ids) if campaign_ids.present? Campaign.transaction do merged_audience_members = [] audience_member_ids_to_delete = [] audience_ids_to_delete = [] campaigns.distinct.each do |campaign| first_list = campaign.audiences.order(:created_at).first puts "Consolidating campaign #{campaign.id} into audience #{first_list.id}" campaign.audiences.excluding(first_list).each do |sl| merged_audience_members += sl.audience_members.map do |sub| { audience_id: first_list.id, customer_id: sub.customer_id, state: sub.state, created_at: sub.created_at, updated_at: sub.updated_at, creator_id: sub.creator_id, updater_id: sub.updater_id } end audience_ids_to_delete << sl.id audience_member_ids_to_delete += sl.audience_member_ids end end msg = "Moving #{merged_audience_members.size} audience_members, deleting #{audience_ids_to_delete.size} lists" Rails.logger.info msg AudienceMember.insert_all(merged_audience_members) if merged_audience_members.any? AudienceMember.delete_by(id: audience_member_ids_to_delete) Audience.delete_by(id: audience_ids_to_delete) msg end end |
.options_for_select ⇒ Array<Array>
Returns [label, id] pairs for select dropdowns.
209 210 211 212 213 |
# File 'app/models/campaign.rb', line 209 def self. res = order(:name) res = yield(res) if block_given? res.order(:name).map { |c| [c.name_and_type, c.id] } end |
.outside_sales ⇒ ActiveRecord::Relation<Campaign>
A relation of Campaigns that are outside sales. Active Record Scope
104 |
# File 'app/models/campaign.rb', line 104 scope :outside_sales, -> { where(campaign_type: 'outside_sales') } |
.states_for_select ⇒ Object
States for select.
221 222 223 |
# File 'app/models/campaign.rb', line 221 def self.states_for_select state_machines[:state].states.map { |s| [s.human_name, s.value] } end |
.with_campaign_emails_count ⇒ ActiveRecord::Relation<Campaign>
A relation of Campaigns that are with campaign emails count. Active Record Scope
112 113 114 115 116 117 |
# File 'app/models/campaign.rb', line 112 scope :with_campaign_emails_count, -> { select('campaigns.*') .select('COUNT(campaign_actions.id) as emails_count') .left_joins(:campaign_emails) .group('campaigns.id') } |
.with_email_stats ⇒ ActiveRecord::Relation<Campaign>
A relation of Campaigns that are with email stats. Active Record Scope
119 120 121 122 123 124 125 |
# File 'app/models/campaign.rb', line 119 scope :with_email_stats, -> { select('campaigns.*') .select('MAX(campaign_actions.created_at) as last_email_created_at') .select('COUNT(campaign_actions.id) as emails_count') .left_joins(:campaign_emails) .group('campaigns.id') } |
.with_last_created_at_email ⇒ ActiveRecord::Relation<Campaign>
A relation of Campaigns that are with last created at email. Active Record Scope
106 107 108 109 110 111 |
# File 'app/models/campaign.rb', line 106 scope :with_last_created_at_email, -> { select('campaigns.*') .select('MAX(campaign_actions.created_at) as last_email_created_at') .left_joins(:campaign_emails) .group('campaigns.id') } |
Instance Method Details
#activities ⇒ ActiveRecord::Relation<Activity>
Returns the associated activities.
80 |
# File 'app/models/campaign.rb', line 80 has_many :activities |
#activity_chain_types ⇒ ActiveRecord::Relation<ActivityChainType>
Returns the associated activity chain types.
82 |
# File 'app/models/campaign.rb', line 82 has_many :activity_chain_types |
#add_customer(customer) ⇒ Object
Add customer.
232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 |
# File 'app/models/campaign.rb', line 232 def add_customer(customer) return AddCustomerResult.new(subscribed: false, message: 'Customer is GUEST and cannot be subscribed') if customer.guest? return AddCustomerResult.new(subscribed: false, message: 'Customer has no contact information') unless customer.contactable? audience = audiences.where(list_type: 'customer', name: "#{name} system added").first_or_create audience_member = audience.audience_members.where(customer_id: customer.id).first # Check customer against filter if customer_filter.nil? || customer_filter.applies_to_customer?(customer) if audience_member # make sure it is active and return audience_member.activate else begin # Joining an audience records a TOUCH; it no longer rewrites the # source of anything the customer already had. The touch is what # `campaign_influenced_invoices` joins against, so influence is # reported without destroying acquisition to record it. audience_member = audience.audience_members.create(customer_id: customer.id) rescue ActiveRecord::RecordNotUnique # Another process might have caused a race condition here end end res = AddCustomerResult.new(subscribed: true, audience_member: audience_member) else # Does not meet filter qualification # customer does not apply to filter in place, inactivate subscription if applicable if audience_member&.active? audience_member.deactivate 'Existing audience_member was de-activated due to customer filter' end res = AddCustomerResult.new(subscribed: false, audience_member: audience_member) end res end |
#audience_member_count ⇒ Object
Audience member count.
274 275 276 |
# File 'app/models/campaign.rb', line 274 def audience_member_count audiences.sum(&:audience_member_count) end |
#audience_members ⇒ ActiveRecord::Relation<AudienceMember>
Returns the associated audience members.
78 |
# File 'app/models/campaign.rb', line 78 has_many :audience_members, through: :audiences |
#audiences ⇒ ActiveRecord::Relation<Audience>
Returns the associated audiences.
71 |
# File 'app/models/campaign.rb', line 71 has_and_belongs_to_many :audiences |
#audiences_all_customer ⇒ Object
Audiences all customer.
359 360 361 362 363 |
# File 'app/models/campaign.rb', line 359 def audiences_all_customer return unless audiences.any? && audiences.any? { |sl| ['customer'].exclude?(sl.list_type) } errors.add(:audience_ids, 'Only customer audiences are allowed for outside_sales and event campaigns') end |
#audiences_all_email ⇒ Object
Audiences all email.
352 353 354 355 356 |
# File 'app/models/campaign.rb', line 352 def audiences_all_email return unless audiences.any? && audiences.any? { |sl| %w[static dynamic].exclude?(sl.list_type) } errors.add(:audience_ids, 'Only email audiences are allowed for email_marketing campaigns') end |
#campaign_deliveries ⇒ ActiveRecord::Relation<CampaignDelivery>
Returns the associated campaign deliveries.
76 |
# File 'app/models/campaign.rb', line 76 has_many :campaign_deliveries, through: :campaign_emails |
#campaign_emails ⇒ ActiveRecord::Relation<CampaignEmail>
Returns the associated campaign emails.
74 |
# File 'app/models/campaign.rb', line 74 has_many :campaign_emails, dependent: :destroy, inverse_of: :campaign |
#campaign_source_name_available ⇒ Object
generate_source creates an active child Source named name under the
Email / Outside Sales parent. The Source's ltree full_name
("Campaigns > Email > ") is built in an after_create and is uniquely
indexed among active sources (index_sources_on_full_name_unique_active), so
a duplicate campaign name would raise RecordNotUnique deep in that rebuild —
a 500. Reject it up front as a form error instead (AppSignal #5594).
319 320 321 322 323 324 325 326 327 |
# File 'app/models/campaign.rb', line 319 def campaign_source_name_available return if name.blank? parent_id = outside_sales? ? Source.outside_sales_campaign_id : Source.email_campaign_id return if parent_id.blank? return unless Source.where(parent_id: parent_id, name: name, visibility: 'active').exists? errors.add(:name, 'is already used by an active campaign source') end |
#can_be_deativated? ⇒ Boolean
Returns whether the record can be deativated.
290 291 292 293 294 295 296 |
# File 'app/models/campaign.rb', line 290 def can_be_deativated? return false if created_at < 1.month.ago last_transmitted = campaign_emails.where.not(last_transmitted: nil).maximum(:last_transmitted) campaign_emails.empty? || (last_transmitted && (last_transmitted > 1.month.ago)) end |
#check_for_active_campaign_emails ⇒ Object
Check for active campaign emails.
330 331 332 333 334 335 336 337 338 339 |
# File 'app/models/campaign.rb', line 330 def check_for_active_campaign_emails if campaign_emails.empty? true elsif campaign_emails.any? { |ce| !ce.can_be_destroyed? } errors.add :base, 'cannot delete campaign which has emails which have already been sent' false else true end end |
#customer_filter ⇒ CustomerFilter?
Returns the customer filter this record belongs to.
68 |
# File 'app/models/campaign.rb', line 68 belongs_to :customer_filter, optional: true |
#delivery_stats ⇒ Object
Delivery stats.
150 151 152 |
# File 'app/models/campaign.rb', line 150 def delivery_stats CampaignEmail.delivery_stats(campaign_deliveries) end |
#email_marketing? ⇒ Boolean
Returns whether the record email marketing.
347 348 349 |
# File 'app/models/campaign.rb', line 347 def email_marketing? campaign_type == 'email_marketing' end |
#generate_source ⇒ Object
Generate source.
299 300 301 302 303 304 305 306 307 308 309 310 311 |
# File 'app/models/campaign.rb', line 299 def generate_source s = Source.new s.parent_id = (campaign_type == 'outside_sales' ? Source.outside_sales_campaign_id : Source.email_campaign_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.}") throw :abort end end |
#merge_audiences ⇒ Object
Merge audiences.
155 156 157 |
# File 'app/models/campaign.rb', line 155 def merge_audiences self.class.merge_audiences(campaign_ids: id) end |
#name_and_type ⇒ Object
Name and type.
269 270 271 |
# File 'app/models/campaign.rb', line 269 def name_and_type "#{name} [#{campaign_type}]" end |
#outside_sales? ⇒ Boolean
Returns whether the record outside sales.
342 343 344 |
# File 'app/models/campaign.rb', line 342 def outside_sales? campaign_type == 'outside_sales' end |
#source ⇒ Source?
Returns the source this record belongs to.
66 |
# File 'app/models/campaign.rb', line 66 belongs_to :source, optional: true |
#source_assignment_window_open_for?(customer, at: Time.current) ⇒ Boolean
Whether the customer's per-member source-assignment window is open at
the given time — i.e. they joined the audience (or the campaign
activated) and their last campaign touch is within
auto_assign_window_days. Used by the creation-default path so a
dormant membership (e.g. a 2018 drip enrollment with no touches
since) stops claiming new opportunities/orders.
397 398 399 400 401 402 403 404 |
# File 'app/models/campaign.rb', line 397 def source_assignment_window_open_for?(customer, at: Time.current) joined_at = audience_members.where(customer_id: customer.id).active.minimum(:created_at) || active_since&.beginning_of_day at_time = at.in_time_zone return false unless joined_at && joined_at <= at_time.end_of_day source_assignment_window_end(customer, joined_at: joined_at, up_to: at_time.end_of_day) >= at_time end |
#to_s ⇒ String
Returns string representation of the record.
226 227 228 |
# File 'app/models/campaign.rb', line 226 def to_s "#{name} [#{id}]" end |
#unique_audience_members ⇒ Object
Unique audience members.
279 280 281 282 283 284 285 286 287 |
# File 'app/models/campaign.rb', line 279 def unique_audience_members Campaign.joins("LEFT OUTER JOIN audiences_campaigns ON audiences_campaigns.campaign_id = campaigns.id LEFT OUTER JOIN audiences ON audiences.id = audiences_campaigns.audience_id LEFT OUTER JOIN audience_members ON audience_members.audience_id = audiences.id LEFT OUTER JOIN view_customers ON audience_members.customer_id = view_customers.id AND view_customers.state != 'guest'") .where(campaigns: { id: id }) .count('distinct view_customers.id') end |
#update_source_name ⇒ Object
Update source name.
366 367 368 |
# File 'app/models/campaign.rb', line 366 def update_source_name source&.update(name: name) end |