Class: Warranty

Inherits:
ApplicationRecord show all
Includes:
Models::Auditable
Defined in:
app/models/warranty.rb,
app/models/warranty/product.rb

Overview

== Schema Information

Table name: warranty_products

id :bigint not null, primary key
details :jsonb not null
product_line :enum not null
valid_until :date
created_at :datetime not null
updated_at :datetime not null
line_item_id :bigint
warranty_id :bigint not null

Indexes

index_warranty_products_on_line_item_id (line_item_id)
index_warranty_products_on_warranty_id (warranty_id)

Foreign Keys

fk_rails_... (line_item_id => line_items.id)
fk_rails_... (warranty_id => warranties.id)

Warranty model containing products.

Defined Under Namespace

Classes: InstallationRecordMigrator, OrderLookup, Product, PublicRegistration, RegistrantResolver, RegistrationParams, ReviewPrompt

Constant Summary collapse

PRODUCT_LINES =

Registered product lines — also the labels of the native
warranty_product_line PG enum backing the column. Mirrors the eleven
legacy /contact/warranty-<slug>/ forms.

%w[
  floor_heating
  snow_melting
  deicing
  deicing_kit
  pipe_tracing
  towel_warmer
  infrared_heating_panel
  countertop_heating
  mirror_defogger
  led_mirror
  shower_waterproofing
].freeze
PRODUCT_LINE_LABELS =

Human labels for the product-line keys (forms + CRM display).

{
  'floor_heating' => 'Floor Heating & Controls',
  'snow_melting' => 'Snow Melting & Controls',
  'deicing' => 'Freeform Deicing Cable & Controls',
  'deicing_kit' => 'DIY Deicing Kits',
  'pipe_tracing' => 'PRO-Tect Pipe Freeze Protection & Controls',
  'towel_warmer' => 'Towel Warmers & Controls',
  'infrared_heating_panel' => 'Infrared Heating Panels & Controls',
  'countertop_heating' => 'Countertop Heaters',
  'mirror_defogger' => 'Mirror Defoggers',
  'led_mirror' => 'LED Mirrors',
  'shower_waterproofing' => 'Shower Kits'
}.freeze
ROOM_TYPES =

Per-product-line extra installation fields on the public registration
form, mirroring what the legacy per-product mail_form forms captured
(ohm/megohm readings prove the element was electrically intact at
install time — the crux of later claim disputes). Kit-picker fields the
legacy forms needed (which pan kit, which grate, which mirror model, …)
are deliberately absent: on a matched order the line item itself answers
that. Entries: { key:, label:, collection: (select when present) }.

['Bathroom', 'Kitchen', 'Bedroom', 'Basement', 'Sunroom', 'Living Room', 'Other'].freeze
FLOOR_TYPES =

Floor surface options for floor-heating registrations.

%w[Tile Carpet Marble Hardwood Laminate Vinyl PVC Other].freeze
OUTDOOR_AREAS =

Outdoor area options for snow-melting registrations.

['Driveway', 'Ramp', 'Walkway/Path', 'Outdoor Stairs', 'Patios', 'Other'].freeze
INSTALLED_UNDER =

Subsurface options for snow-melting installations.

%w[Concrete Asphalt Pavers Other].freeze
PIPE_CABLE_LENGTHS =

Pipe-tracing cable length options.

['3 ft.', '12 ft.', '30 ft.', 'Other'].freeze
CONTROL_SOURCES =

Control purchase-source options.

['WarmlyYours', 'Amazon', 'Costco', 'Home Depot', "Lowe's", 'Other'].freeze
OHM_STAGES =

Ohm-reading installation stages.

['Before Install', 'During Install', 'After Install'].freeze
OHM_FIELDS =

Ohm-reading field definitions per stage.

OHM_STAGES.each_with_index.map { |stage, i| { key: :"ohm_#{i + 1}", label: "Ohm Reading — #{stage}" } }.freeze
MEGOHM_FIELDS =

Megohm-reading field definitions per stage.

OHM_STAGES.each_with_index.map { |stage, i| { key: :"megohm_#{i + 1}", label: "MegOhm Reading — #{stage}" } }.freeze
LINE_EXTRAS =

Per-product-line extra installation fields.

{
  'floor_heating' => [
    # freeform: rendered as a text input with the collection as
    # type-ahead suggestions — registrants describe the room/floor in
    # their own words (tech-team feedback, Jeff 2026-07-17).
    { key: :room_type, label: 'Room / Area', collection: ROOM_TYPES, freeform: true },
    { key: :floor_type, label: 'Floor / Surface Type', collection: FLOOR_TYPES, freeform: true },
    *OHM_FIELDS
  ],
  'snow_melting' => [
    { key: :room_type, label: 'Area', collection: OUTDOOR_AREAS, freeform: true },
    { key: :installed_under, label: 'Installed Under', collection: INSTALLED_UNDER },
    { key: :pavers_thickness, label: 'Paver Thickness',
      hint: 'If installed under pavers — 2.5" maximum' },
    { key: :material_depth_below, label: 'Depth Below Element',
      hint: 'Material depth below the heating element — 2" minimum' },
    { key: :material_depth_above, label: 'Depth Above Element',
      hint: 'Material depth above the heating element — 1.5" minimum' },
    *OHM_FIELDS,
    *MEGOHM_FIELDS
  ],
  'pipe_tracing' => [
    { key: :installation_length, label: 'Cable Length', collection: PIPE_CABLE_LENGTHS }
  ],
  'towel_warmer' => [
    { key: :control_purchase_source, label: 'Control Purchased From', collection: CONTROL_SOURCES }
  ],
  'shower_waterproofing' => [
    { key: :thinset, label: 'Thinset Used' }
  ]
}.freeze
PRODUCT_MODEL_OPTIONS =

Manual-flow product-name suggestions per line, carried over from the
retired per-product forms' model dropdowns. Rendered as datalists, so
anything not listed can still be typed free-form.

{
  'floor_heating' => ['TempZone™ for Tile, Stone and Hardwood', 'Environ II™ for Carpet and Laminate',
                      'Slab Heating for Concrete Slab'],
  'snow_melting' => ['Snow Melting Cable', 'Snow Melting Mat'],
  'towel_warmer' => %w[Infinity Riviera Sierra Barcelona Elements Metropolitan Studio Huron
                       Vida Maui Sydney Summit Malta Palma Ibiza Vancouver London Paris Milan
                       Maple Bellagio Rome Venice] +
                    ['Tahoe 6', 'Tahoe 7', 'Tahoe 8', 'Tahoe 10', 'Ontario XL', 'Grande 10', 'Grande 12'],
  'infrared_heating_panel' => ['LAVA® Glass (250W)', 'LAVA® Glass (500W)', 'LAVA® Glass (750W)', 'LAVA® Glass (1000W)',
                               'LAVA® Mirror (250W)', 'LAVA® Mirror (500W)', 'LAVA® Mirror (750W)', 'LAVA® Mirror (1000W)',
                               'LAVA® Light (350W)', 'LAVA® Light (500W)',
                               'LAVA® Crystal (350W)', 'LAVA® Crystal (500W)', 'LAVA® Crystal (1000W)',
                               'Ember Mirror (600W)', 'Ember Glass (600W)', 'Ember Glass (800W)',
                               'Ember Flex (300W)', 'Ember Flex (500W)', 'Ember Flex (700W)'],
  'mirror_defogger' => ['Rectangle (12" x 18")', 'Rectangle (18" x 24")', 'Rectangle (24" x 32")', 'Rectangle (30" x 40")',
                        'Circle (15")', 'Circle (20")', 'Circle (25")', 'Oval (18" x 26")', 'Oval (24" x 32")'],
  'deicing_kit' => ['30 ft. (ETC120-5W-030-WY)', '60 ft. (ETC120-5W-060-WY)',
                    '80 ft. (ETC120-5W-080-WY)', '100 ft. (ETC120-5W-100-WY)']
}.freeze
TERMS =

Default warranty duration (years) per product line — single source of
truth for valid_until, seeded from the legacy form/marketing copy
(TempZone 25yr, snow melting 10yr, deicing cable 3yr, plug-in kits 2yr,
PRO-Tect 10yr, towel warmers 2/5yr by model, Ember panels 5yr,
countertop element 3yr, defoggers 1yr, LED mirrors 2/3/5yr by
collection, TempZone shower mat 25yr). Sub-model variations (Environ
5yr, Marquee 2yr, …) are handled per-row via the term_years detail
override until the business decides on a terms table (open questions
#2/#6 in the task doc).

{
  'floor_heating' => 25,
  'snow_melting' => 10,
  'deicing' => 3,
  'deicing_kit' => 2,
  'pipe_tracing' => 10,
  'towel_warmer' => 5,
  'infrared_heating_panel' => 5,
  'countertop_heating' => 3,
  'mirror_defogger' => 1,
  'led_mirror' => 5,
  'shower_waterproofing' => 25
}.freeze
WARRANTABLE_CATEGORY_PATHS =

Product-category roots (items.pc_path_slugs) whose items are the
registrable product itself — the big categories the legacy per-product
forms covered. Thermostats/controls, accessories, underlayment, spare
parts, tools, power, sensors and shipping lines are deliberately absent:
they ride along on the system registration (the form asks for the
thermostat model separately) rather than being registered themselves.

[
  LtreePaths::PC_HEATING_ELEMENTS,
  LtreePaths::PC_TOWEL_WARMERS,
  LtreePaths::PC_INFRARED_HEATING_PANELS,
  LtreePaths::PC_MIRROR_DEFOGGERS,
  LtreePaths::PC_MIRRORS,
  'goods.shower_pan_kits',
  'goods.shower_floor_heating_kits'
].freeze
PRODUCT_LINE_BY_PL_ROOT =

Product-line root (items.primary_pl_path_slugs first label) →
warranty product-line enum label.

{
  'floor_heating' => 'floor_heating',
  'snow_melting' => 'snow_melting',
  'roof_gutter_deicing' => 'deicing',
  'pipe_freeze_protection' => 'pipe_tracing',
  'towel_warmer' => 'towel_warmer',
  'infrared_heating_panels' => 'infrared_heating_panel',
  'countertop_heater' => 'countertop_heating',
  'mirror_defogger' => 'mirror_defogger',
  'led_mirror' => 'led_mirror',
  'shower_kits' => 'shower_waterproofing'
}.freeze
DEICING_KIT_PL_PREFIX =

Pre-assembled plug-in deicing kits have their own (shorter) terms and
their own legacy form — split them off the freeform deicing cable line.

'roof_gutter_deicing.constant_wattage'
NON_DISPLAY_CATEGORY_ROOTS =

Category roots hidden from the public registration screen entirely.

%w[shipping_and_handling].freeze

Constants included from Models::Auditable

Models::Auditable::ALWAYS_IGNORED

Constants included from Models::Schedulable

Models::Schedulable::SIMPLE_FORM_OPTIONS

Instance Attribute Summary collapse

Belongs to collapse

Methods included from Models::Auditable

#creator, #updater

Has many collapse

Class Method Summary collapse

Instance Method Summary collapse

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

config

Methods included from Models::AfterCommittable

#after_commit

Methods included from Models::EventPublishable

#publish_event

Instance Attribute Details

#installed_onObject (readonly)

A product can't be installed (or bought) in the future — these dates
anchor #coverage_starts_on, so a future date would postpone coverage.

Validations:

  • Comparison ({ less_than_or_equal_to: -> { Date.current }, message: 'cannot be in the future' })
  • Allow_nil


396
397
398
# File 'app/models/warranty.rb', line 396

validates :installed_on, :purchased_on,
comparison: { less_than_or_equal_to: -> { Date.current }, message: 'cannot be in the future' },
allow_nil: true

#purchased_onObject (readonly)

A product can't be installed (or bought) in the future — these dates
anchor #coverage_starts_on, so a future date would postpone coverage.

Validations:

  • Comparison ({ less_than_or_equal_to: -> { Date.current }, message: 'cannot be in the future' })
  • Allow_nil


396
397
398
# File 'app/models/warranty.rb', line 396

validates :installed_on, :purchased_on,
comparison: { less_than_or_equal_to: -> { Date.current }, message: 'cannot be in the future' },
allow_nil: true

#submitted_atObject (readonly)

When the registration was submitted (web) or keyed (crm); defaulted to
now before validation, so presence only fails on explicit nil-ing.

Validations:



392
# File 'app/models/warranty.rb', line 392

validates :submitted_at, presence: true

Class Method Details

.displayable_line_items(order) ⇒ Array<LineItem>

Everything the registration screen lists — all positive-qty lines except
shipping, in the customer-facing quote order (heating elements →
thermostats → accessories, priciest first). Lines that aren't registrable
on their own (thermostats, sensors, accessories) render as informational
"covered with the system" rows rather than input panels.

Parameters:

Returns:



295
296
297
298
299
300
301
302
303
304
305
306
307
# File 'app/models/warranty.rb', line 295

def self.displayable_line_items(order)
  return [] unless order

  # NOTE: the coverage views call item_warranty_years per accessory item,
  # which runs Item::SpecificationsRetriever (a memoized service, not an
  # association — it cannot be eager-loaded here). One retriever query
  # per accessory line is acceptable at order sizes.
  lines = order.line_items.with_positive_qty.includes(:item).reject do |line_item|
    root = line_item.item&.pc_path_slugs.to_s.split('.').first
    line_item.item.nil? || NON_DISPLAY_CATEGORY_ROOTS.include?(root)
  end
  LineItem.sort_for_room_display(lines)
end

.extra_fields_for(product_line) ⇒ Array<Hash>

Returns extra field definitions for the line (empty when none).

Parameters:

  • product_line (String, nil)

Returns:

  • (Array<Hash>)

    extra field definitions for the line (empty when none)



181
182
183
# File 'app/models/warranty.rb', line 181

def self.extra_fields_for(product_line)
  LINE_EXTRAS.fetch(product_line, [])
end

.installer_identity_matchingActiveRecord::Relation<Warranty>

A relation of Warranties that are installer identity matching. Active Record Scope

Returns:

See Also:



446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
# File 'app/models/warranty.rb', line 446

scope :installer_identity_matching, lambda { |name, company|
  entry_match = lambda do |name_sql, company_sql|
    "(LOWER(COALESCE(NULLIF(TRIM(#{name_sql}), ''), '')) = LOWER(:name) AND " \
      "LOWER(COALESCE(NULLIF(TRIM(#{company_sql}), ''), '')) = LOWER(:company))"
  end

  where(<<~SQL.squish, name: name.to_s.strip, company: company.to_s.strip)
    #{entry_match.call("details->>'installer_name'", "details->>'installer_company'")}
    OR #{entry_match.call(
      "CONCAT_WS(' ', NULLIF(TRIM(details->>'installer1_first_name'), ''), NULLIF(TRIM(details->>'installer1_last_name'), ''))",
      "details->>'installer1_company'"
    )}
    OR #{entry_match.call(
      "CONCAT_WS(' ', NULLIF(TRIM(details->>'installer2_first_name'), ''), NULLIF(TRIM(details->>'installer2_last_name'), ''))",
      "details->>'installer2_company'"
    )}
    OR EXISTS (
      SELECT 1 FROM jsonb_array_elements(
        CASE WHEN jsonb_typeof(details->'installers') = 'array' THEN details->'installers' ELSE '[]'::jsonb END
      ) inst
      WHERE COALESCE(inst->>'self_install', 'false') NOT IN ('true', '1')
        AND #{entry_match.call("inst->>'installer_name'", "inst->>'installer_company'")}
    )
  SQL
}

.installer_matchingActiveRecord::Relation<Warranty>

A relation of Warranties that are installer matching. Active Record Scope

Returns:

See Also:



420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
# File 'app/models/warranty.rb', line 420

scope :installer_matching, lambda { |term|
  pattern = "%#{sanitize_sql_like(term.to_s.strip)}%"
  where(<<~SQL.squish, pattern: pattern)
    details->>'installer_name' ILIKE :pattern
    OR details->>'installer_company' ILIKE :pattern
    OR details->>'installer_phone' ILIKE :pattern
    OR CONCAT_WS(' ', details->>'installer1_first_name', details->>'installer1_last_name') ILIKE :pattern
    OR details->>'installer1_company' ILIKE :pattern
    OR details->>'installer1_phone' ILIKE :pattern
    OR CONCAT_WS(' ', details->>'installer2_first_name', details->>'installer2_last_name') ILIKE :pattern
    OR details->>'installer2_company' ILIKE :pattern
    OR EXISTS (
      SELECT 1 FROM jsonb_array_elements(
        CASE WHEN jsonb_typeof(details->'installers') = 'array' THEN details->'installers' ELSE '[]'::jsonb END
      ) inst
      WHERE inst->>'installer_name' ILIKE :pattern
         OR inst->>'installer_company' ILIKE :pattern
         OR inst->>'installer_phone' ILIKE :pattern
    )
  SQL
}

.installer_statsArray<Hash>

Aggregated installer activity for the CRM installer report: one row per
distinct installer (case-insensitive name+company), with registration
counts and first/last submission, unioned across all storage shapes.

Returns:

  • (Array<Hash>)


477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
# File 'app/models/warranty.rb', line 477

def self.installer_stats
  ActiveRecord::Base.with_connection { |conn| conn.select_all(<<~SQL.squish).to_a }
    WITH entries AS (
      SELECT w.id, w.submitted_at,
             NULLIF(TRIM(inst->>'installer_name'), '') AS name,
             NULLIF(TRIM(inst->>'installer_company'), '') AS company,
             NULLIF(TRIM(inst->>'installer_phone'), '') AS phone,
             NULLIF(inst->>'installer_party_id', '')::bigint AS party_id
      FROM warranties w,
           jsonb_array_elements(CASE WHEN jsonb_typeof(w.details->'installers') = 'array'
                                     THEN w.details->'installers' ELSE '[]'::jsonb END) inst
      WHERE COALESCE(inst->>'self_install', 'false') NOT IN ('true', '1')
      UNION ALL
      SELECT w.id, w.submitted_at,
             NULLIF(TRIM(w.details->>'installer_name'), ''),
             NULLIF(TRIM(w.details->>'installer_company'), ''),
             NULLIF(TRIM(w.details->>'installer_phone'), ''),
             NULL::bigint
      FROM warranties w
      UNION ALL
      SELECT w.id, w.submitted_at,
             NULLIF(TRIM(CONCAT_WS(' ', NULLIF(TRIM(w.details->>'installer1_first_name'), ''),
                                        NULLIF(TRIM(w.details->>'installer1_last_name'), ''))), ''),
             NULLIF(TRIM(w.details->>'installer1_company'), ''),
             NULLIF(TRIM(w.details->>'installer1_phone'), ''),
             NULL::bigint
      FROM warranties w
      UNION ALL
      SELECT w.id, w.submitted_at,
             NULLIF(TRIM(CONCAT_WS(' ', NULLIF(TRIM(w.details->>'installer2_first_name'), ''),
                                        NULLIF(TRIM(w.details->>'installer2_last_name'), ''))), ''),
             NULLIF(TRIM(w.details->>'installer2_company'), ''),
             NULLIF(TRIM(w.details->>'installer2_phone'), ''),
             NULL::bigint
      FROM warranties w
    )
    SELECT MAX(name) AS installer_name,
           MAX(company) AS installer_company,
           MAX(phone) AS installer_phone,
           CASE WHEN COUNT(DISTINCT party_id) = 1 THEN MAX(party_id) END AS installer_party_id,
           COUNT(DISTINCT id) AS registrations,
           MIN(submitted_at) AS first_submitted_at,
           MAX(submitted_at) AS last_submitted_at
    FROM entries
    WHERE name IS NOT NULL OR company IS NOT NULL
    GROUP BY LOWER(COALESCE(name, '')), LOWER(COALESCE(company, ''))
    ORDER BY registrations DESC, MAX(submitted_at) DESC
    LIMIT 500
  SQL
end

.item_warranty_years(item) ⇒ Integer?

The item's own published warranty term in years, parsed from its
"Warranty" product specification ("Limited 3-year warranty…",
"2 years"). Accessories and controls carry their own printed terms,
distinct from the heating system they ship with (nSpire 3yr, snow
controls/relays/sensors 2yr, …).

Parameters:

Returns:

  • (Integer, nil)


331
332
333
334
335
336
# File 'app/models/warranty.rb', line 331

def self.item_warranty_years(item)
  return nil unless item

  blurb = item.product_specifications.detect { |spec| spec.name == 'Warranty' }&.text_blurb
  blurb && blurb[/(\d+)[\s-]*year/i, 1]&.to_i
end

.matchedActiveRecord::Relation<Warranty>

A relation of Warranties that are matched. Active Record Scope

Returns:

See Also:



414
# File 'app/models/warranty.rb', line 414

scope :matched, -> { where.not(order_id: nil) }

.order_placed_on(order) ⇒ Date?

The order's placed date (checkout completion, EDI order date for
marketplace orders, then record creation) — the warranty clock anchor
and the earliest credible installation date.

Parameters:

Returns:

  • (Date, nil)


281
282
283
284
285
# File 'app/models/warranty.rb', line 281

def self.order_placed_on(order)
  return nil unless order

  (order.completion_date || order.edi_order_date || order.created_at)&.to_date
end

.order_thermostat_names(order) ⇒ Array<String>

Names of thermostat/control items on the order. When present, the
public form skips asking for the control model and the created rows
are stamped with these instead.

Parameters:

Returns:

  • (Array<String>)


315
316
317
318
319
320
321
# File 'app/models/warranty.rb', line 315

def self.order_thermostat_names(order)
  return [] unless order

  order.line_items.with_positive_qty.includes(:item)
       .select { |line_item| line_item.item&.is_thermostat? }
       .map(&:name).compact_blank.uniq
end

.product_line_for_item(item) ⇒ String?

The warranty product line an item registers under, or nil when the item
isn't itself registrable (controls, accessories, shipping, services, …).

Parameters:

Returns:



248
249
250
251
252
253
254
255
256
257
258
# File 'app/models/warranty.rb', line 248

def self.product_line_for_item(item)
  return if item.nil?

  pc_path = item.pc_path_slugs.to_s
  return unless WARRANTABLE_CATEGORY_PATHS.any? { |root| pc_path == root || pc_path.start_with?("#{root}.") }

  pl_path = item.primary_pl_path_slugs.to_s
  return 'deicing_kit' if pl_path.start_with?(DEICING_KIT_PL_PREFIX)

  PRODUCT_LINE_BY_PL_ROOT[pl_path.split('.').first]
end

.registrable_line_items(order) ⇒ Array<LineItem>

The order's line items a customer can register — positive-qty lines
whose item maps to a warranty product line via product_line_for_item.

Parameters:

Returns:



265
266
267
268
269
270
# File 'app/models/warranty.rb', line 265

def self.registrable_line_items(order)
  return [] unless order

  order.line_items.with_positive_qty.includes(:item)
       .select { |line_item| product_line_for_item(line_item.item).present? }
end

.unmatchedActiveRecord::Relation<Warranty>

A relation of Warranties that are unmatched. Active Record Scope

Returns:

See Also:



413
# File 'app/models/warranty.rb', line 413

scope :unmatched, -> { where(order_id: nil) }

Instance Method Details

#coverage_starts_onDate?

The date the warranty clock starts. Business rule (2026-07-15): the
warranty starts the day the order is placed — the claimed order date
on manual registrations (purchased_on), the placed date on matched
orders, and the submission date as the last resort.

Returns:

  • (Date, nil)


561
562
563
# File 'app/models/warranty.rb', line 561

def coverage_starts_on
  purchased_on || order_placed_on || &.to_date
end

#created_byParty

The rep who keyed a mailed-in card; nil for public web submissions.

Returns:

See Also:



344
# File 'app/models/warranty.rb', line 344

belongs_to :created_by, class_name: 'Party', optional: true

#customerCustomer

The end-consumer Party the warranty belongs to.

Returns:

See Also:



342
# File 'app/models/warranty.rb', line 342

belongs_to :customer

#expired?Boolean

(valid_until is synced to the LATEST product expiry)

Returns:

  • (Boolean)

    whether every product's coverage has ended



567
568
569
# File 'app/models/warranty.rb', line 567

def expired?
  valid_until.present? && valid_until < Date.current
end

#installer_linesArray<String>

Human lines for whoever installed — one per submitted installer, falling
back to the singular legacy fields (CRM-keyed / backfilled rows).

Returns:

  • (Array<String>)


540
541
542
543
544
545
546
547
548
549
550
551
552
553
# File 'app/models/warranty.rb', line 540

def installer_lines
  lines = Array(installers).filter_map do |entry|
    entry = entry.stringify_keys
    if ActiveModel::Type::Boolean.new.cast(entry['self_install'])
      'Self-installed'
    else
      [entry['installer_name'], entry['installer_company'],
       entry['installer_phone']].compact_blank.join('').presence || 'Professional installer'
    end
  end
  return lines if lines.any?

  [[installer_name, installer_company, installer_phone].compact_blank.join('').presence].compact
end

#orderOrder

The matched WarmlyYours order; nil until a rep reconciles an
unmatched web registration.

Returns:

See Also:



340
# File 'app/models/warranty.rb', line 340

belongs_to :order, optional: true

#product_line_labelsArray<String>

Human labels of the registered product lines, one per distinct line —
"Snow Melting & Controls", or several for mixed submissions.

Returns:

  • (Array<String>)


532
533
534
# File 'app/models/warranty.rb', line 532

def product_line_labels
  products.map(&:product_line_label).uniq
end

#productsActiveRecord::Relation<Warranty::Product>

The registered units — one per roll/cable/mat, each with its own line,
term, expiry, serials, and readings.

Returns:

See Also:



349
# File 'app/models/warranty.rb', line 349

has_many :products, class_name: 'Warranty::Product', inverse_of: :warranty, dependent: :destroy

#uploadsActiveRecord::Relation<Upload>

Scanned warranty cards and photos (category warranty_card).

Returns:

  • (ActiveRecord::Relation<Upload>)

See Also:



346
# File 'app/models/warranty.rb', line 346

has_many :uploads, as: :resource, dependent: :destroy