Class: Customer::NewCustomer

Inherits:
Object
  • Object
show all
Includes:
ActiveModel::API, ActiveModel::Attributes, ActiveModel::Validations, ActiveModel::Validations::Callbacks, Heatwave::AttributeNormalizing, StripAttributes
Defined in:
app/services/customer/new_customer.rb

Overview

Service object: new customer.
rubocop:disable Metrics/ClassLength -- pre-existing god-class (330+ lines on master); decompose separately

Constant Summary collapse

REP_ASSIGN_METHODS =

Rep assign methods.

{ aa: 'Auto Assign', sa: 'Self Assign', ma: 'Manually Assign', dna: 'Do Not Assign' }.with_indifferent_access

Instance Attribute Summary collapse

Delegated Instance Attributes collapse

Instance Method Summary collapse

Methods included from Heatwave::AttributeNormalizing

normalize

Instance Attribute Details

#cellObject (readonly)



72
# File 'app/services/customer/new_customer.rb', line 72

validates :phone, :phone2, :cell, :fax, phone_format: true

#emailObject (readonly)

We always validate these fields no matter what step we're in

Validations:

  • Email_format


71
# File 'app/services/customer/new_customer.rb', line 71

validates :email, :email2, email_format: true

#email2Object (readonly)

We always validate these fields no matter what step we're in

Validations:

  • Email_format


71
# File 'app/services/customer/new_customer.rb', line 71

validates :email, :email2, email_format: true

#faxObject (readonly)



72
# File 'app/services/customer/new_customer.rb', line 72

validates :phone, :phone2, :cell, :fax, phone_format: true

#phoneObject (readonly)



72
# File 'app/services/customer/new_customer.rb', line 72

validates :phone, :phone2, :cell, :fax, phone_format: true

#phone2Object (readonly)



72
# File 'app/services/customer/new_customer.rb', line 72

validates :phone, :phone2, :cell, :fax, phone_format: true

#websiteObject (readonly)



73
# File 'app/services/customer/new_customer.rb', line 73

validates :website, url: { allow_nil: true, allow_blank: true }

Instance Method Details

#address_complete?Boolean

Bare mininum information we need for a valid address

Returns:

  • (Boolean)


339
340
341
# File 'app/services/customer/new_customer.rb', line 339

def address_complete?
  %i[street1 city postal_code country_iso].all? { |f| send(f).presence }
end

#allow_disablement_of_address_validation?Boolean

Accessor method to determine if we can allow disabling the address validation

Returns:

  • (Boolean)


407
408
409
# File 'app/services/customer/new_customer.rb', line 407

def allow_disablement_of_address_validation?
  @address_carrier_error
end

#assignment_step?Boolean

Returns:

  • (Boolean)


108
109
110
# File 'app/services/customer/new_customer.rb', line 108

def assignment_step?
  step == :assignment
end

#build_address(address) ⇒ Object

Copy over address attributes



307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
# File 'app/services/customer/new_customer.rb', line 307

def build_address(address)
  address.street1 = street1
  address.street2 = street2
  address.city = city
  address.state_code = state_code
  address.zip = postal_code
  address.country_iso3 = Country.find_by(iso: country_iso)&.iso3
  address.is_residential = residential_address.nil? ? false : residential_address
  address.lat = lat
  address.lng = lng
  address.has_loading_dock = has_loading_dock
  address.is_construction_site = is_construction_site
  address.requires_inside_delivery = requires_inside_delivery
  address.requires_liftgate = requires_liftgate
  address.limited_access = limited_access
  address.requires_appointment = requires_appointment
  address.disable_address_correction = disable_address_correction.nil? ? false : disable_address_correction
  address
end

#build_and_save_customer(check_for_spam: false) ⇒ Object

Builds, qualify, create lead and save customer



348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
# File 'app/services/customer/new_customer.rb', line 348

def build_and_save_customer(check_for_spam: false)
  customer = build_customer
  if check_for_spam && is_spam?
    customer.errors.add(:base, 'Spam customer detected')
  else
    customer.state = 'lead_qualify'
    customer.state_event = :complete_qualification unless start_in_lead_qualify?

    if customer.save
      # Update the return path
      self.return_path = return_path && customer.id && return_path.gsub('new_party_id', customer.id.to_s)
      NewLead::LeadActivityAssigner.new.process(customer, notes: notes) # unless customer.primary_sales_rep_id.nil?
      # record email preferences
      set_email_preferences
      # Broadcast product interest if any
      customer.broadcast_product_interest_changed(product_line_ids_added: customer.product_line_ids)
      # Broadcast buying groups if any
      customer.broadcast_buying_group_changed(buying_group_id_added: customer.buying_group_id)
      # Queue profile image lookup (logo from Logo.dev's image CDN)
      PartyProfileImageWorker.perform_async(customer.id)
      # Cheap domain-keyed brand pull (social profiles + description).
      # Complements — doesn't replace — the full Apollo/PDL/Gemini run
      # that fires later when the lead reaches `lead_qualify`: this one
      # needs nothing but a company domain, so a business lead lands
      # with its socials already proposed for review. No-ops for
      # homeowners and webmail-only leads.
      CurrentScope.without_job_tracking do
        PartyResearch::AutoEnricher.enqueue(customer, only: %w[logo_dev])
      end
    end
  end
  customer
end

#build_contact_points(party) ⇒ Object

Build the contact points into the party



328
329
330
331
332
333
334
335
336
# File 'app/services/customer/new_customer.rb', line 328

def build_contact_points(party)
  party.contact_points.build(detail: phone, category: ContactPoint::PHONE) if phone.present?
  party.contact_points.build(detail: phone2, category: ContactPoint::PHONE) if phone2.present?
  party.contact_points.build(detail: cell, category: ContactPoint::CELL) if cell.present?
  party.contact_points.build(detail: fax, category: ContactPoint::FAX) if fax.present?
  party.contact_points.build(detail: email, category: ContactPoint::EMAIL) if email.present?
  party.contact_points.build(detail: email2, category: ContactPoint::EMAIL) if email2.present?
  party.contact_points.build(detail: website, category: ContactPoint::WEBSITE) if website.present?
end

#build_customerObject

Instantiate a new customer translating all attributes from newcustomer



266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
# File 'app/services/customer/new_customer.rb', line 266

def build_customer
  cu = Customer.new
  cu.profile_id = profile_id
  cu.buying_group_id = buying_group_id
  cu.source_id = source_id
  cu.source_info = source_info.presence
  cu.preferred_language = preferred_language
  cu.catalog_id = catalog_id
  cu.tier2_program_pricing_id = tier2_program_pricing_id
  cu.gclid = gclid.presence
  # Split the person name, we'll reuse that later
  pnp = split_person_name
  if company_name.blank?
    pnp.to_party(cu)
  else # If not company, build a person
    cu.full_name = company_name
    if pnp.valid? # A contact is present
      cnt = cu.contacts.build
      pnp.to_party(cnt)
      cnt.job_title = job_title
    end
  end
  # Build the contact points into the contact if present otherwise customer
  build_contact_points(cnt || cu)
  cu.primary_sales_rep_id = primary_sales_rep_id
  cu.secondary_sales_rep_id = secondary_sales_rep_id
  cu.local_sales_rep_id = local_sales_rep_id
  cu.service_rep_id = service_rep_id
  cu.do_not_auto_assign = (rep_assignment_method.to_s.to_sym == :dna)
  cu.creation_method = creation_method

  # Build the address if we have all the elements
  if address_complete?
    a = build_address(cu.addresses.new)
    cu.shipping_address = cu.billing_address = cu.mailing_address = a
  end
  cu.product_line_ids = (product_line_ids || []).compact.uniq
  cu
end

#buying_groupObject

Accessor method to retrieve buying group



230
231
232
# File 'app/services/customer/new_customer.rb', line 230

def buying_group
  BuyingGroup.find(buying_group_id.presence) if buying_group_id.presence
end

#buying_group_options_for_selectObject

Retrieves a list of buying available for the customer company



235
236
237
# File 'app/services/customer/new_customer.rb', line 235

def buying_group_options_for_select
  BuyingGroup.options_for_select(company_id: company_id)
end

#catalogObject

Catalog is based on country in this model



240
241
242
243
# File 'app/services/customer/new_customer.rb', line 240

def catalog
  # Always return a catalog; default to US when country is not provided
  Catalog.default_for_country(country_iso)
end

#catalog_idObject



245
246
247
# File 'app/services/customer/new_customer.rb', line 245

def catalog_id
  catalog.try(:id)
end

#catalog_nameObject



249
250
251
# File 'app/services/customer/new_customer.rb', line 249

def catalog_name
  catalog.try(:name)
end

#company_idObject

Alias for Catalog#company_id

Returns:

  • (Object)

    Catalog#company_id

See Also:



253
# File 'app/services/customer/new_customer.rb', line 253

delegate :company_id, to: :catalog

#company_name_partsObject

Split company name in parts for highlighting on screen



180
181
182
# File 'app/services/customer/new_customer.rb', line 180

def company_name_parts
  company_name.to_s.split(/[ ,.]/).filter_map(&:presence)
end

#contact_point_detailsObject



170
171
172
# File 'app/services/customer/new_customer.rb', line 170

def contact_point_details
  [email, phone, fax].compact.uniq
end

#country_must_existObject

build_address looks the Country row up by iso — an unknown value
(tampered or legacy form posts) must fail validation here instead of
crashing on nil.iso3 later (AppSignal #6410).



138
139
140
141
142
# File 'app/services/customer/new_customer.rb', line 138

def country_must_exist
  return if country_iso.blank? || Country.exists?(iso: country_iso)

  errors.add(:country_iso, 'is not a supported country')
end

#default_pricing_programObject



192
193
194
195
196
197
198
# File 'app/services/customer/new_customer.rb', line 192

def default_pricing_program
  if is_individual?
    Coupon::Tier2ProgramPricing.msrp_tier2_pricing_coupon
  else
    Coupon::Tier2ProgramPricing.customer_default_trade
  end
end

#default_profileObject



200
201
202
203
204
205
206
# File 'app/services/customer/new_customer.rb', line 200

def default_profile
  if is_individual?
    Profile.homeowner_profile
  else
    Profile.unknown_trade
  end
end

#find_duplicatesObject



157
158
159
160
# File 'app/services/customer/new_customer.rb', line 157

def find_duplicates
  cf = Query::CustomerFinder.new(attributes)
  cf.find_duplicates
end

#full_addressObject



343
344
345
# File 'app/services/customer/new_customer.rb', line 343

def full_address
  %i[street1 city postal_code country_iso].map { |f| send(f).presence }.join(' ')
end

#has_address?Boolean

Returns:

  • (Boolean)


118
119
120
# File 'app/services/customer/new_customer.rb', line 118

def has_address?
  street1.present?
end

#identify_step?Boolean

Returns:

  • (Boolean)


100
101
102
# File 'app/services/customer/new_customer.rb', line 100

def identify_step?
  step == :identify
end

#is_individual?Boolean

Returns:

  • (Boolean)


184
185
186
# File 'app/services/customer/new_customer.rb', line 184

def is_individual?
  company_name.blank?
end

#is_organization?Boolean

Returns:

  • (Boolean)


188
189
190
# File 'app/services/customer/new_customer.rb', line 188

def is_organization?
  company_name.present?
end

#is_spam?Boolean

Returns:

  • (Boolean)


411
412
413
414
415
416
# File 'app/services/customer/new_customer.rb', line 411

def is_spam?
  SpamCheck.new.process(
     author_email: email.presence,
     phone_number: phone.presence || phone2.presence || cell.presence || fax.presence
   )
end

#name_presenceObject

Check for name presence and normalize the person name using our name parser
e.g Smith, John III will be John Smith III



124
125
126
127
128
129
130
131
132
133
# File 'app/services/customer/new_customer.rb', line 124

def name_presence
  if person_name.present?
    pnp = split_person_name
    self.person_name = pnp.full_name
  end
  return if company_name.present? || person_name.present?

  errors.add :company_name, 'must be specified if no person name present'
  errors.add :person_name, 'must be specified if no company name present'
end

#person_first_nameObject



162
163
164
# File 'app/services/customer/new_customer.rb', line 162

def person_first_name
  pnp.first
end

#person_last_nameObject



166
167
168
# File 'app/services/customer/new_customer.rb', line 166

def person_last_name
  pnp.last
end

#person_name_partsObject

Split person name in parts for highlighting on screen



175
176
177
# File 'app/services/customer/new_customer.rb', line 175

def person_name_parts
  person_name.to_s.scan(/(\w+)/).flatten
end

#pricing_program_options_for_selectObject

Used in the final step to determine the pricing offered



209
210
211
# File 'app/services/customer/new_customer.rb', line 209

def pricing_program_options_for_select
  Coupon::Tier2ProgramPricing.select_options_for_customer(build_customer)
end

#profileObject

Accessor method to retrieve profile



214
215
216
# File 'app/services/customer/new_customer.rb', line 214

def profile
  Profile.find(profile_id) if profile_id.present?
end

#profile_collection_for_selectObject

List of profiles for selection, we assume an individual is always a homeowner
only, otherwise we pull up every trade



220
221
222
223
224
225
226
227
# File 'app/services/customer/new_customer.rb', line 220

def profile_collection_for_select
  profiles = if is_individual?
               Profile.homeowner
             else
               Profile.trades
             end
  profiles.order(:name).pluck(:name, :id)
end

#rep_assignment_method_nameObject



96
97
98
# File 'app/services/customer/new_customer.rb', line 96

def rep_assignment_method_name
  REP_ASSIGN_METHODS[rep_assignment_method]
end

#set_default_email_preferencesObject



255
256
257
258
259
260
261
262
263
# File 'app/services/customer/new_customer.rb', line 255

def set_default_email_preferences
  # If it's for US, subscribe to everything by default
  # Canada will therefore be unsubscribed by default
  default_value = company_id == 1
  %i[subscribe_to_announcements subscribe_to_events subscribe_to_newsletters
     subscribe_to_product_review_requests subscribe_to_promotions subscribe_to_webinars].each do |field|
    public_send(:"#{field}=", default_value) if public_send(field).nil?
  end
end

#set_email_preferencesObject

Creates or updates email preferences if an email address was provided



383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
# File 'app/services/customer/new_customer.rb', line 383

def set_email_preferences
  return true if email.blank?

  ep = EmailPreference.find_or_initialize_by(email: email&.downcase)
  ep.disable_announcements = subscribe_to_announcements != true
  ep.disable_events = subscribe_to_events != true
  ep.disable_newsletters = subscribe_to_newsletters != true
  ep.disable_promotions = subscribe_to_promotions != true
  ep.disable_webinars = subscribe_to_webinars != true

  # Default new FR/IT signups to tracking-disabled (CNIL/Garante 2026 rules on
  # open/click pixels for marketing). country_iso is required, so no fallback
  # needed. Only set on new records to preserve existing opt-in/opt-out choices.
  ep.disable_email_tracking = %w[FR IT FRA ITA].include?(country_iso.to_s.upcase) if ep.new_record?

  ep.save
end

#setup_or_assignment_step?Boolean

Returns:

  • (Boolean)


112
113
114
# File 'app/services/customer/new_customer.rb', line 112

def setup_or_assignment_step?
  (setup_step? or assignment_step?)
end

#setup_step?Boolean

Returns:

  • (Boolean)


104
105
106
# File 'app/services/customer/new_customer.rb', line 104

def setup_step?
  step == :setup
end

#split_person_nameObject

Calls the person name parser to split a name, returns a hash with :first_name, :last_name, :middle_name, :suffix, :prefix



402
403
404
# File 'app/services/customer/new_customer.rb', line 402

def split_person_name
  PersonNameParser.new(person_name)
end

#start_in_lead_qualify?Boolean

Returns:

  • (Boolean)


116
# File 'app/services/customer/new_customer.rb', line 116

def start_in_lead_qualify? = start_in_lead_qualify

#website_url_to_base_urlObject



144
145
146
147
148
149
150
151
152
153
154
155
# File 'app/services/customer/new_customer.rb', line 144

def website_url_to_base_url
  return if website.blank?

  pv = begin
    Addressable::URI.parse(website)
  rescue StandardError
    nil
  end
  return unless pv

  self.website = "#{pv.scheme || 'https'}://#{pv.host}"
end