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)
endSolar NAM (TrueDesign) Flow:
- In Home Tour,
SolarNam.tsxrenders an "Add Main Service Panel"SelectableCard(line 611-634) - On submit,
SolarNamEstimateItemCreatorcreates the MSP as a childEstimateItem - Creates estimate items only -- no NAM path for creating MSP as a proposed change
Aurora / Legacy Flow:
SolarRoofing::SolarProposal::SolarItemServicehandles both paths- For submitted projects: calls
propose_main_panel_creation→ProposeChange(creation proposal) - For non-submitted projects: calls
update_solar_main_panel_estimate_item(direct creation)
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.
components/projects/app/views/projects/proposed_changes/_item_row.html.erb
Three gear dropdown states:
- Unchanged items (
item.action.blank?, lines 136-208): Haspossible_nested_configsat lines 201-204 - Creation proposals (
creation_proposal?, lines 209-248): Haspossible_nested_configsat lines 242-244 - 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?).
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.
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.
-
Aurora flow, estimate-level:
SolarItemServicecorrectly creates solar + MSP estimate items with parent-child linkage. -
Aurora flow, project-level (proposed changes):
SolarItemService#propose_main_panel_creationcorrectly creates a proposed creation for MSP. -
NAM flow, estimate-level:
SolarNamEstimateItemCreatorcorrectly creates MSP as a child estimate item. -
Child deletion cascade:
ProposeChange#propose_child_item_deletions!correctly proposes deletion for MSP children when the solar parent is deleted. -
ProposedItemChange nesting: The
child_itemsoverride onProposedItemChangecorrectly excludes children proposed for deletion when checking nested item limits. -
Controller flow:
ProjectItemsController#createalready usesProposeChangefor submitted projects, so any item created through the product configurator on a submitted project will be properly proposed.
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:
_item_row.html.erb(projects page) lines 322-326_item_row.html.erb(proposed changes) lines 201-204, 242-244
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
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
Location: db/migrate/ (umbrella app, data migration)
Create a data migration that:
-
Identifies the correct MSP-specific
ProductConfigID 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. -
Updates that
ProductConfigrecord to set:nested: truenested_item_limit: 1(only one MSP per solar design)nested_name: "Main Service Panel"(controls the display name in "Add {nested_display_name}")
-
Creates a
NestableProductConfigrecord:product_config_id: the MSP config identified abovenested_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 of809059..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.
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.
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."
File: components/projects/app/helpers/projects/project_items_helper.rb
Add a helper that checks:
- Item is a non-nested solar item
- Territory uses Solar NAM (
HomeTour::SolarNamRequirements.using_solar_nam?) - 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? }
endAlso 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")
endA) Data migration test:
- Verify
NestableProductConfigrecord exists with correct associations - Verify
ProductConfighasnested: true,nested_item_limit: 1,nested_name: "Main Service Panel"
B) can_add_solar_msp? helper tests:
- Returns
truewhen: solar NAM enabled, no MSP child exists - Returns
truewhen: solar NAM enabled, MSP child exists but is proposed for deletion - Returns
falsewhen: solar NAM enabled, MSP child exists and is not proposed for deletion - Returns
falsewhen: solar NAM disabled (Aurora territory) - Returns
falsewhen: item is nested (is itself an MSP) - Returns
falsewhen: 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
| 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 |
SolarRoofing::SolarProposal::SolarItemService-- untouchedHomeTour::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