Skip to content

Instantly share code, notes, and snippets.

@garettarrowood
Created March 25, 2026 10:55
Show Gist options
  • Select an option

  • Save garettarrowood/67d141cc553e7dadcd3e0915e2ef3b03 to your computer and use it in GitHub Desktop.

Select an option

Save garettarrowood/67d141cc553e7dadcd3e0915e2ef3b03 to your computer and use it in GitHub Desktop.
Main Service Panel Report

Main Service Panel: Report and Implementation Plan

Part 1: How the Main Service Panel Works Today

Data Model

The Main Service Panel (MSP) is a child line item under the solar parent item. Both share product_id (Solar) but are distinguished by:

  • Solar parent: location: "Roof", parent_item_id: nil
  • MSP child: location: "House", parent_item_id: <solar_item_id>

Detection is via HomeTour::SolarMainPanel:

# components/home_tour/app/services/home_tour/solar_main_panel.rb
def self.main_panel?(estimate)
  solar_item_ids = estimate.estimate_items
                           .where(product_id: solar_product.id, parent_item_id: nil)
                           .pluck(:id)
  estimate.estimate_items.exists?(parent_item_id: solar_item_ids)
end

Two Creation Flows

Solar NAM (TrueDesign) Flow:

  • In Home Tour, SolarNam.tsx renders an "Add Main Service Panel" SelectableCard (line 611-634)
  • On submit, SolarNamEstimateItemCreator creates the MSP as a child EstimateItem
  • Creates estimate items only -- no NAM path for creating MSP as a proposed change

Aurora / Legacy Flow:

  • SolarRoofing::SolarProposal::SolarItemService handles both paths
  • For submitted projects: calls propose_main_panel_creationProposeChange (creation proposal)
  • For non-submitted projects: calls update_solar_main_panel_estimate_item (direct creation)

Gear Dropdown on the Projects Page

components/nitro_component_transition/app/views/bootstrap/projects/_item_row.html.erb

The gear dropdown already has a mechanism for adding nested items via possible_nested_configs (lines 322-326):

<% unless item.nested? %>
  <% item.possible_nested_configs.each do |config| %>
    <li><%= link_to(icon("sticky-note", "Add #{config.nested_display_name}", ...),
                    add_nested_item_path(item, config), ...) %></li>
  <% end %>
<% end %>

This iterates over NestableProductConfig records linked to the item's config. No NestableProductConfig record currently exists for MSP under solar, so "Add Main Service Panel" never appears.

Gear Dropdown on the Proposed Changes Page

components/projects/app/views/projects/proposed_changes/_item_row.html.erb

Three gear dropdown states:

  1. Unchanged items (item.action.blank?, lines 136-208): Has possible_nested_configs at lines 201-204
  2. Creation proposals (creation_proposal?, lines 209-248): Has possible_nested_configs at lines 242-244
  3. Deletion proposals (deletion_proposal?, lines 250-260): Only shows "Restore" -- no nested configs

The solar edit path is always set to the Aurora solar proposal path (lines 18-21). There is no Solar NAM branch (unlike the projects page which checks using_solar_nam?).

Controller: ProjectItemsController#create

components/nitro_component_transition/app/controllers/project_items_controller.rb

The create action already handles proposed changes correctly. It calls propose_item_change (line 35) which delegates to ProposeChange.new(item, :creation, ...). The parent_item_id is correctly extracted from params (line 20-24). So adding items from the product configurator on submitted projects automatically creates proposed changes.

ProposedItemChange Handles Deletion-Aware Nesting

components/projects/app/models/projects/proposed_item_change.rb overrides child_items (lines 314-319) and sibling_count (lines 304-312) to use ItemSorter#child_items_without_deletions. This means possible_nested_configs on a ProposedItemChange correctly accounts for proposed deletions when checking the nested item limit.

However, ProjectItem (used for unchanged items on the proposed changes page) does not have this override and will check raw child_items without filtering proposed deletions.


Part 2: Where It IS Working

  1. Aurora flow, estimate-level: SolarItemService correctly creates solar + MSP estimate items with parent-child linkage.

  2. Aurora flow, project-level (proposed changes): SolarItemService#propose_main_panel_creation correctly creates a proposed creation for MSP.

  3. NAM flow, estimate-level: SolarNamEstimateItemCreator correctly creates MSP as a child estimate item.

  4. Child deletion cascade: ProposeChange#propose_child_item_deletions! correctly proposes deletion for MSP children when the solar parent is deleted.

  5. ProposedItemChange nesting: The child_items override on ProposedItemChange correctly excludes children proposed for deletion when checking nested item limits.

  6. Controller flow: ProjectItemsController#create already uses ProposeChange for submitted projects, so any item created through the product configurator on a submitted project will be properly proposed.


Part 3: Where It IS NOT Working

Issue 1: No "Add Main Service Panel" in the gear dropdown (NAM flow)

There is no NestableProductConfig record linking the MSP product config to the solar product config. The possible_nested_configs mechanism returns an empty result for solar items, so "Add Main Service Panel" never appears in the gear dropdown on either page.

Affected files:

Issue 2: Unchanged solar items don't account for MSP deletion proposals

On the proposed changes page, when a solar item is unchanged (a ProjectItem, not a ProposedItemChange), its possible_nested_configs uses the default children_with_config which checks raw child_items. If the MSP exists as a ProjectItem child but is proposed for deletion, nested_item_limit: 1 would still hide "Add Main Service Panel" because the child physically exists.

Affected file: components/core_models/lib/core_models/nitro_item/item.rb lines 238-248

Issue 3: No Solar NAM edit path on the proposed changes page

The proposed changes page always uses the Aurora solar proposal path for editing solar items (lines 18-21 of _item_row.html.erb). There is no HomeTour::SolarNamRequirements.using_solar_nam? check, unlike the projects page (line 218).

Affected file: _item_row.html.erb (proposed changes) lines 18-21


Part 4: Implementation Plan

Step 1: Data Migration -- NestableProductConfig Setup

Location: db/migrate/ (umbrella app, data migration)

Create a data migration that:

  1. Identifies the correct MSP-specific ProductConfig ID to use as the nestable config. The MSP config list is [809034, 809039, 809049, 809054, 809590, 809595, 809600, 809605, 809610, 809615, 809620]. The config that diverges from the solar parent's list is at 809590 -- this or a parent container config in the MSP subtree should be the one marked as nested.

  2. Updates that ProductConfig record to set:

    • nested: true
    • nested_item_limit: 1 (only one MSP per solar design)
    • nested_name: "Main Service Panel" (controls the display name in "Add {nested_display_name}")
  3. Creates a NestableProductConfig record:

    • product_config_id: the MSP config identified above
    • nested_under_product_config_id: a config ID from the solar parent's config list that is not shared with the MSP list (e.g., one of 809059..809089) to ensure MSP can only be nested under solar parent items, not under other MSP items

Verification needed: Query the database to confirm the correct config IDs and their hierarchy. The ProductConfig tree structure must support the configurator traversal from the nested config root to produce the full MAIN_PANEL_CONFIG_LIST.

Step 2: Projects Page -- Gate MSP Option Behind NAM Check

File: components/nitro_component_transition/app/views/bootstrap/projects/_item_row.html.erb

Modify the possible_nested_configs block (lines 322-326) to only show the MSP option for NAM solar items. This leaves Aurora behavior unchanged (Aurora handles MSP through the solar proposal comparison page, not the gear dropdown).

Replace:

<% unless item.nested? %>
  <% item.possible_nested_configs.each do |config| %>
    <li><%= link_to(icon("sticky-note", "Add #{config.nested_display_name}", class: "fa-fw"),
                    add_nested_item_path(item, config), ...) %></li>
  <% end %>
<% end %>

With logic that gates solar nested configs behind the NAM check:

<% unless item.nested? %>
  <% item.possible_nested_configs.each do |config| %>
    <% next if item.solar? && !using_solar_nam_for_item?(item) %>
    <li><%= link_to(icon("sticky-note", "Add #{config.nested_display_name}", class: "fa-fw"),
                    add_nested_item_path(item, config), ...) %></li>
  <% end %>
<% end %>

Add a helper method (or inline the check) that calls HomeTour::SolarNamRequirements.using_solar_nam? with the item's territory and current user, similar to the existing check at line 218.

Step 3: Proposed Changes Page -- Solar MSP with Proposed-Change Awareness

File: components/projects/app/views/projects/proposed_changes/_item_row.html.erb

The challenge: For unchanged solar items (ProjectItem), possible_nested_configs does not account for proposed MSP deletions. We need a supplemental check.

Approach: For solar items specifically, replace the generic possible_nested_configs rendering with a dedicated helper that checks proposed change state. For non-solar items, keep the generic mechanism.

A) For unchanged items (inside item.action.blank? block, around lines 201-204):

<% unless item.nested? %>
  <% if item.solar? %>
    <% if can_add_solar_msp?(item) %>
      <li><%= link_to(icon("sticky-note", "Add Main Service Panel", class: "fa-fw"),
                      add_nested_item_path(item, solar_msp_config), ...) %></li>
    <% end %>
  <% else %>
    <% item.possible_nested_configs.each do |config| %>
      <li><%= link_to(...) %></li>
    <% end %>
  <% end %>
<% end %>

B) For creation proposals (inside creation_proposal? block, around lines 242-244):

Same pattern. ProposedItemChange#possible_nested_configs already filters deletions via child_items_without_deletions, but we still want the NAM gate.

C) For deletion proposals (lines 250-260):

No change needed. Deletion proposals only show "Restore", so "Add Main Service Panel" never appears. This satisfies the requirement: "On the proposed changes page, [don't show the option] if ... the only design is action Deletion."

Step 4: Helper Method -- can_add_solar_msp?

File: components/projects/app/helpers/projects/project_items_helper.rb

Add a helper that checks:

  1. Item is a non-nested solar item
  2. Territory uses Solar NAM (HomeTour::SolarNamRequirements.using_solar_nam?)
  3. No active MSP child exists, accounting for proposed deletions
def can_add_solar_msp?(item)
  return false unless item.solar? && !item.nested?

  territory_id = item.project.territory_id
  return false unless HomeTour::SolarNamRequirements.using_solar_nam?(territory_id, current_user_record)

  item_sorter = Projects::Calculators::ItemSorter.new(item.project)
  parent_id = item.respond_to?(:project_item_id) ? item.project_item_id : item.id
  children = item_sorter.child_items_and_proposed_changes(product: item.product, parent_item_id: parent_id)

  # Allow adding MSP if no active MSP children exist
  # (all existing MSP children are proposed for deletion or none exist)
  children.none? || children.all? { |c| c.respond_to?(:deletion?) && c.deletion? }
end

Also add a method to retrieve the MSP ProductConfig:

def solar_msp_config
  # Cache the lookup; the config ID comes from the NestableProductConfig setup
  @solar_msp_config ||= ProductConfig.find_by(nested: true, nested_name: "Main Service Panel")
end

Step 5: Tests

A) Data migration test:

  • Verify NestableProductConfig record exists with correct associations
  • Verify ProductConfig has nested: true, nested_item_limit: 1, nested_name: "Main Service Panel"

B) can_add_solar_msp? helper tests:

  • Returns true when: solar NAM enabled, no MSP child exists
  • Returns true when: solar NAM enabled, MSP child exists but is proposed for deletion
  • Returns false when: solar NAM enabled, MSP child exists and is not proposed for deletion
  • Returns false when: solar NAM disabled (Aurora territory)
  • Returns false when: item is nested (is itself an MSP)
  • Returns false when: item is not solar

C) View tests (projects page):

  • "Add Main Service Panel" appears in gear dropdown for NAM solar items without MSP
  • "Add Main Service Panel" does NOT appear for Aurora solar items
  • "Add Main Service Panel" does NOT appear when MSP already exists

D) View tests (proposed changes page):

  • "Add Main Service Panel" appears for unchanged NAM solar items without MSP
  • "Add Main Service Panel" appears when MSP is proposed for deletion
  • "Add Main Service Panel" does NOT appear when solar item is proposed for deletion
  • "Add Main Service Panel" does NOT appear when MSP exists and is not proposed for deletion

Summary of Files to Change

File Change
db/migrate/<timestamp>_setup_solar_msp_nestable_config.rb Data migration: create NestableProductConfig, update ProductConfig
components/nitro_component_transition/app/views/bootstrap/projects/_item_row.html.erb Gate solar nested configs behind NAM check
components/projects/app/views/projects/proposed_changes/_item_row.html.erb Replace generic nested configs with can_add_solar_msp? for solar items
components/projects/app/helpers/projects/project_items_helper.rb Add can_add_solar_msp? and solar_msp_config helpers
Spec files for helper, views, and migration Test all conditions

What Does NOT Change (Aurora flow preserved)

  • SolarRoofing::SolarProposal::SolarItemService -- untouched
  • HomeTour::SolarNam.tsx / Solar.tsx -- untouched (Home Tour MSP card UI)
  • SolarNamEstimateItemCreator -- untouched
  • Aurora solar proposal comparison flow -- untouched
  • The NAM gate ensures "Add Main Service Panel" in the gear dropdown only appears for NAM territories
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment