Class: Campaign::AssignDripCampaigns

Inherits:
BaseService show all
Defined in:
app/services/campaign/assign_drip_campaigns.rb

Defined Under Namespace

Classes: Result

Constant Summary collapse

PRO_SM_DRIP_LAUNCH =
'PRO_SM_DRIP_LAUNCH'
PRO_EFH_DRIP_LAUNCH =
'PRO_EFH_DRIP_LAUNCH'
HOM_EFH_DRIP_LAUNCH =
'HOM_EFH_DRIP_LAUNCH'
HO_SM_DRIP_LAUNCH =
'HOM_SM_DRIP_LAUNCH'
PRO_SS_LAUNCH =
'PRO_SS_LAUNCH'
BG_DRIP_LAUNCH =
'BG_DRIP_LAUNCH'
PRO_CUSTOMER_FILTER_ID =

Pros w/out DB/ETL/HD/COSTCO

170
HOM_CUSTOMER_FILTER_ID =

Homeowner

40

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from BaseService

#log_debug, #log_error, #log_info, #log_warning, #logger, #options, #tagged_logger

Constructor Details

#initialize(options = {}) ⇒ AssignDripCampaigns

Returns a new instance of AssignDripCampaigns.



19
20
21
22
23
24
# File 'app/services/campaign/assign_drip_campaigns.rb', line 19

def initialize(options = {})
  # Initialize our filters
  @cf_pro = CustomerFilter.find(PRO_CUSTOMER_FILTER_ID)
  @cf_ho = CustomerFilter.find(HOM_CUSTOMER_FILTER_ID)
  super
end

Instance Attribute Details

#cf_bgObject (readonly)

Returns the value of attribute cf_bg.



17
18
19
# File 'app/services/campaign/assign_drip_campaigns.rb', line 17

def cf_bg
  @cf_bg
end

#cf_hoObject (readonly)

Returns the value of attribute cf_ho.



17
18
19
# File 'app/services/campaign/assign_drip_campaigns.rb', line 17

def cf_ho
  @cf_ho
end

#cf_proObject (readonly)

Returns the value of attribute cf_pro.



17
18
19
# File 'app/services/campaign/assign_drip_campaigns.rb', line 17

def cf_pro
  @cf_pro
end

Class Method Details

.compact_valid_parties(valid_parties, logger: nil) ⇒ Object



237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
# File 'app/services/campaign/assign_drip_campaigns.rb', line 237

def self.compact_valid_parties(valid_parties, logger: nil)
  curated_parties = valid_parties
  # If the Customer and Contact share the same email, the Contact prevails, the Customer is eliminated
  if customer = curated_parties.detect { |p| p.is_a?(Customer) }
    if customer.email
      contact_parties = curated_parties.select { |p| p.is_a?(Contact) }
      contact_emails = contact_parties.filter_map(&:email).uniq
      # Does any of the contact have this customer email already?
      if customer.email.in?(contact_emails)
        # Remove the customer from valid parties
        logger.warn "Customer #{customer.reference_number} was removed from valid parties because a contact already contained the same email"
        curated_parties = curated_parties.where.not(id: customer.id)
      end
    end
  end
  curated_parties
end

Instance Method Details

#create_buying_group_drips(_customer, valid_parties, _buying_group_id) ⇒ Object

Create buying group drip campaign activity, for now it's just any buying group



104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
# File 'app/services/campaign/assign_drip_campaigns.rb', line 104

def create_buying_group_drips(_customer, valid_parties, _buying_group_id)
  activities_created = []
  drip_activity_type = ActivityType.active.where(task_type: BG_DRIP_LAUNCH).first
  if drip_activity_type
    # If the parties previously had this activity open or closed, we skip
    valid_parties = valid_parties.where.not('exists(select 1 from activities where party_id = parties.id and activity_type_id = ?)', drip_activity_type.id)
    # Finally, if we have any parties left, they get this activity, since we manually checked for existence skip_if_exists can be false
    valid_parties.each do |p|
      append_drip_activity!(activities_created, p, drip_activity_type.task_type)
    end
  else
    # Report the activity type is missing
    msg = "Missing drip activity #{BG_DRIP_LAUNCH}"
    log_error msg
    ErrorReporting.error(msg)
  end

  activities_created
end

#create_product_interest_drips(customer, valid_parties, product_line_ids) ⇒ Object

def create_service_drips(_customer, valid_parties, message)
activities_created = []
drip_activity_type = ActivityType.active.where(task_type: PRO_SS_LAUNCH).first
if drip_activity_type
# If the parties previously had this activity open or closed, we skip
valid_parties = valid_parties.where.not('exists(select 1 from activities where party_id = parties.id and activity_type_id = ?)', drip_activity_type.id)
# Finally, if we have any parties left, they get this activity, since we manually checked for existence skip_if_exists can be false
valid_parties.each do |p|
activities_created << p.create_activity(drip_activity_type.task_type, skip_if_exists: false, notes: message)
end
else
msg = "Missing drip activity #PRO_SS_LAUNCH"
log_error msg
ErrorReporting.error(msg)
end
activities_created
end



142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
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
# File 'app/services/campaign/assign_drip_campaigns.rb', line 142

def create_product_interest_drips(customer, valid_parties, product_line_ids)
  activities_created = []
  # Use product_line_url (exact) for ltree-based hierarchy matching
  drips = [
    {
      customer_filter: cf_pro,
      product_line_ids: product_line_ids,
      product_line_url: 'snow-melting',
      task_type: PRO_SM_DRIP_LAUNCH
    },
    {
      customer_filter: cf_pro,
      product_line_ids: product_line_ids,
      product_line_url: 'floor-heating',
      task_type: PRO_EFH_DRIP_LAUNCH
    },
    {
      customer_filter: cf_ho,
      product_line_ids: product_line_ids,
      product_line_url: 'floor-heating',
      task_type: HOM_EFH_DRIP_LAUNCH
    },
    {
      customer_filter: cf_ho,
      product_line_ids: product_line_ids,
      product_line_url: 'snow-melting',
      task_type: HO_SM_DRIP_LAUNCH
    }
  ]

  # Loop through our drips
  drips.each do |drip_params|
    # Add customer and valid parties which are always the same to each drip
    merged_drip_params = {
      customer: customer,
      valid_parties: valid_parties
    }.merge(drip_params)
    # Now run the evaluation and assign method
    res = evaluate_for_filter_and_drip(**merged_drip_params)
    # Aggregate activities created, if it was an error this would be an empty array
    activities_created += res.activities_created
  end
  activities_created
end

#evaluate_for_filter_and_drip(customer:, valid_parties:, customer_filter:, product_line_ids:, product_line_url:, task_type:) ⇒ Object

This is our main method where we evaluate a customer against a drip criteria



188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
# File 'app/services/campaign/assign_drip_campaigns.rb', line 188

def evaluate_for_filter_and_drip(customer:,
                                 valid_parties:,
                                 customer_filter:,
                                 product_line_ids:,
                                 product_line_url:,
                                 task_type:)
  return Result.new(error: 'Customer filter missing') unless customer_filter

  # Qualify our customer and parties which have an email
  unless customer_filter.applies_to_customer?(customer)
    msg = "Customer #{customer.reference_number} does not match Filter #{customer_filter.id}"
    log_warning msg
    return Result.new(error: msg)
  end

  activities_created = []

  # Use ltree for precise hierarchy matching instead of URL LIKE
  root_pl = ProductLine.find_by(slug_ltree: LtreePaths.slug_ltree_from_legacy_hyphen_url(product_line_url)) ||
            ProductLine.find_by(slug_ltree: product_line_url)
  product_lines_added = if root_pl
                          ProductLine.where(id: product_line_ids).where(ProductLine[:ltree_path_ids].ltree_descendant(root_pl.ltree_path_ids))
                        else
                          ProductLine.none
                        end

  if product_lines_added.present?
    # Engage and assign the activity to our parties
    drip_activity_type = ActivityType.active.where(task_type: task_type).first
    if drip_activity_type
      # If the parties previously had this activity open or closed, we skip
      valid_parties = valid_parties.where.not('exists(select 1 from activities where party_id = parties.id and activity_type_id = ?)', drip_activity_type.id)
      # Finally, if we have any parties left, they get this activity, since we manually checked for existence skip_if_exists can be false
      valid_parties.each do |p|
        append_drip_activity!(activities_created, p, drip_activity_type.task_type)
      end
    else
      # Report the activity type is missing
      msg = "Missing drip activity #{task_type}"
      log_error msg
      ErrorReporting.error(msg)
    end
  else
    log_info "No qualifying product lines added matching #{product_line_url}. Product Line ids added are: #{product_line_ids.join(', ')}"
  end

  Result.new(activities_created: activities_created)
end

#process(params) ⇒ Object

params is expected to be an object responding to

  • party
  • product_line_ids_added
  • product_line_ids_removed
  • buying_group_id_added
  • buying_group_id_removed
  • latitude
  • longitude


34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'app/services/campaign/assign_drip_campaigns.rb', line 34

def process(params)
  party = params.party
  product_line_ids_added = params.product_line_ids_added
  buying_group_id_added = params.buying_group_id_added

  parties = []
  case party
  when Customer
    customer = party
    parties = [customer, *customer.contacts.active]
  when Contact
    customer = party.customer
    parties = [party]
  end

  unless customer
    msg = "No customer present for party id #{party.id} for drip assignment"
    log_warning msg
    return Result.new(error: msg)
  end

  # We have to check that we have the right profile for this customer and that a snow melt interest was added
  activities_created = []
  # Prequalify our parties, can they receive announcements for instance?
  valid_parties = Party.distinct.where(id: parties.map(&:id)).joins(:contact_points).merge(ContactPoint.emails).where.not('exists(select 1 from email_preferences ep where ep.email = contact_points.detail and disable_announcements = true)')
  if (excluded_parties = (parties.to_a - valid_parties.to_a)).present?
    log_info "Parties #{excluded_parties.map(&:id).join(', ')} were excluded from drip because they had no email or their email were unsubscribed from announcements emails"
  end

  # remove customer when we are have duplicate emails
  valid_parties = self.class.compact_valid_parties(valid_parties, logger: logger)

  if valid_parties.blank?
    msg = 'No valid parties are present for drip assignment'
    log_warning msg
    return Result.new(error: msg)
  end
  # At some point we might have unsubscribe if interests are removed
  activities_created += create_product_interest_drips(customer, valid_parties, product_line_ids_added) if product_line_ids_added.present?

  # At some point we might have unsubscribe if buying groups are removed
  activities_created += create_buying_group_drips(customer, valid_parties, buying_group_id_added) if buying_group_id_added.present?

  # if params.latitude && params.longitude

  #   # What's our warehouse lat and long
  #   store_address = Store.find(1).warehouse_address
  #   service_center_latitude = store_address.lat
  #   service_center_longitude = store_address.lng
  #   # What's the distance (miles default)
  #   d = Geocoder::Calculations.distance_between([service_center_latitude, service_center_longitude], [params.latitude, params.longitude])
  #   d = d.round(2)
  #   # are we within service limits?
  #   if d < SMART_SERVICES_MAX_DISTANCE
  #     activities_created += create_service_drips(customer, valid_parties, "Coordinates within #{d} miles of service center")
  #   end
  # end

  Result.new(activities_created: activities_created)
rescue StandardError => e
  msg = "Exception #{e} raised in drip campaign assigner"
  log_error msg
  raise e unless Rails.env.production?

  ErrorReporting.error(e)
  Result.new(error: msg)
  # Render exception right away
end