Class: Warranty
- Inherits:
-
ApplicationRecord
- Object
- ActiveRecord::Base
- ApplicationRecord
- Warranty
- 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_linePG 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 forvalid_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 theterm_yearsdetail
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
-
#installed_on ⇒ Object
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.
-
#purchased_on ⇒ Object
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.
-
#submitted_at ⇒ Object
readonly
When the registration was submitted (web) or keyed (crm); defaulted to now before validation, so presence only fails on explicit nil-ing.
Belongs to collapse
-
#created_by ⇒ Party
The rep who keyed a mailed-in card; nil for public web submissions.
-
#customer ⇒ Customer
The end-consumer Party the warranty belongs to.
-
#order ⇒ Order
The matched WarmlyYours order; nil until a rep reconciles an unmatched web registration.
Methods included from Models::Auditable
Has many collapse
-
#products ⇒ ActiveRecord::Relation<Warranty::Product>
The registered units — one per roll/cable/mat, each with its own line, term, expiry, serials, and readings.
-
#uploads ⇒ ActiveRecord::Relation<Upload>
Scanned warranty cards and photos (category
warranty_card).
Class Method Summary collapse
-
.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).
-
.extra_fields_for(product_line) ⇒ Array<Hash>
Extra field definitions for the line (empty when none).
-
.installer_identity_matching ⇒ ActiveRecord::Relation<Warranty>
A relation of Warranties that are installer identity matching.
-
.installer_matching ⇒ ActiveRecord::Relation<Warranty>
A relation of Warranties that are installer matching.
-
.installer_stats ⇒ Array<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.
-
.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").
-
.matched ⇒ ActiveRecord::Relation<Warranty>
A relation of Warranties that are matched.
-
.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.
-
.order_thermostat_names(order) ⇒ Array<String>
Names of thermostat/control items on the order.
-
.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, …).
-
.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 Warranty.product_line_for_item.
-
.unmatched ⇒ ActiveRecord::Relation<Warranty>
A relation of Warranties that are unmatched.
Instance Method Summary collapse
-
#coverage_starts_on ⇒ Date?
The date the warranty clock starts.
-
#expired? ⇒ Boolean
(
valid_untilis synced to the LATEST product expiry). -
#installer_lines ⇒ Array<String>
Human lines for whoever installed — one per submitted installer, falling back to the singular legacy fields (CRM-keyed / backfilled rows).
-
#product_line_labels ⇒ Array<String>
Human labels of the registered product lines, one per distinct line — "Snow Melting & Controls", or several for mixed submissions.
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
#installed_on ⇒ Object (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_on ⇒ Object (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_at ⇒ Object (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.
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).
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_matching ⇒ ActiveRecord::Relation<Warranty>
A relation of Warranties that are installer identity matching. Active Record Scope
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_matching ⇒ ActiveRecord::Relation<Warranty>
A relation of Warranties that are installer matching. Active Record Scope
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_stats ⇒ Array<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.
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, …).
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 |
.matched ⇒ ActiveRecord::Relation<Warranty>
A relation of Warranties that are matched. Active Record Scope
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.
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.
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, …).
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.
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 |
.unmatched ⇒ ActiveRecord::Relation<Warranty>
A relation of Warranties that are unmatched. Active Record Scope
413 |
# File 'app/models/warranty.rb', line 413 scope :unmatched, -> { where(order_id: nil) } |
Instance Method Details
#coverage_starts_on ⇒ Date?
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.
561 562 563 |
# File 'app/models/warranty.rb', line 561 def coverage_starts_on purchased_on || order_placed_on || submitted_at&.to_date end |
#created_by ⇒ Party
The rep who keyed a mailed-in card; nil for public web submissions.
344 |
# File 'app/models/warranty.rb', line 344 belongs_to :created_by, class_name: 'Party', optional: true |
#customer ⇒ Customer
The end-consumer Party the warranty belongs to.
342 |
# File 'app/models/warranty.rb', line 342 belongs_to :customer |
#expired? ⇒ Boolean
(valid_until is synced to the LATEST product expiry)
567 568 569 |
# File 'app/models/warranty.rb', line 567 def expired? valid_until.present? && valid_until < Date.current end |
#installer_lines ⇒ Array<String>
Human lines for whoever installed — one per submitted installer, falling
back to the singular legacy fields (CRM-keyed / backfilled rows).
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 |
#order ⇒ Order
The matched WarmlyYours order; nil until a rep reconciles an
unmatched web registration.
340 |
# File 'app/models/warranty.rb', line 340 belongs_to :order, optional: true |
#product_line_labels ⇒ Array<String>
Human labels of the registered product lines, one per distinct line —
"Snow Melting & Controls", or several for mixed submissions.
532 533 534 |
# File 'app/models/warranty.rb', line 532 def product_line_labels products.map(&:product_line_label).uniq end |
#products ⇒ ActiveRecord::Relation<Warranty::Product>
The registered units — one per roll/cable/mat, each with its own line,
term, expiry, serials, and readings.
349 |
# File 'app/models/warranty.rb', line 349 has_many :products, class_name: 'Warranty::Product', inverse_of: :warranty, dependent: :destroy |
#uploads ⇒ ActiveRecord::Relation<Upload>
Scanned warranty cards and photos (category warranty_card).
346 |
# File 'app/models/warranty.rb', line 346 has_many :uploads, as: :resource, dependent: :destroy |