Skip to main content

Onboarding Plan-Module Integration

Question

"Is this implemented in the code, when on onboarding step 2 (Modules & Plan), the list of modules selected/unselected changes based on the type of company's plan?"

Answer

It is NOW implemented (after the fix).

The frontend code was already written to handle dynamic module selection based on plan, but the backend was returning empty module lists due to a bug.


How It Works

Frontend Implementation (ModulesStep.tsx)

The frontend has the logic to auto-select modules based on the selected plan:

Lines 151-159:

// Update selected modules when plan changes
useEffect(() => {
const selectedPlanData = plans.find(p => p.code === selectedPlan);
if (selectedPlanData && selectedPlanData.modules) {
// Auto-select modules that are included in the plan
const planModuleCodes = selectedPlanData.modules.map(m => m.code);
setSelectedModules(planModuleCodes);
}
}, [selectedPlan, plans]);

Lines 171-187:

const toggleModule = (moduleCode: string) => {
// Check if this module is required by the selected plan
const selectedPlanData = plans.find(p => p.code === selectedPlan);
const isIncludedInPlan = selectedPlanData?.modules.some(m => m.code === moduleCode);

// Don't allow unselecting modules included in the plan
if (isIncludedInPlan && selectedModules.includes(moduleCode)) {
toast('This module is included in your selected plan', { icon: 'ℹ️' });
return;
}

setSelectedModules(prev =>
prev.includes(moduleCode)
? prev.filter(m => m !== moduleCode)
: [...prev, moduleCode]
);
};

Backend Implementation (PlanService)

BEFORE (Broken):

# Old code that didn't work
for entitlement in entitlements:
if entitlement.entitlement.code.startswith('module_'):
module_code = entitlement.entitlement.code.replace('module_', '')
module_codes.append(module_code)

Issues with old code:

  1. Checked for 'module_' (lowercase) but entitlements are 'MODULE_' (uppercase)
  2. Didn't use the module_id foreign key we added to plan_entitlements
  3. Would return empty modules array for all plans

AFTER (Fixed):

# New code that works correctly
modules_query = (
self.db.query(Module)
.join(PlanEntitlement, Module.module_id == PlanEntitlement.module_id)
.filter(
PlanEntitlement.plan_version_id == active_version.plan_version_id,
PlanEntitlement.feature_enabled == True,
PlanEntitlement.module_id.isnot(None),
Module.category == 'USER_OPTION',
Module.is_visible == True
)
.order_by(Module.code)
)

What the fix does:

  1. ✅ Uses the module_id foreign key relationship
  2. ✅ Only returns USER_OPTION modules (excludes REQUIRED and UNDERLYING)
  3. ✅ Only returns visible modules
  4. ✅ Properly joins plan_entitlementsmodules

Expected Behavior

When User Selects STARTER Plan:

Auto-selected modules:

  • CHEMIQ (basic)
  • PLAN (basic)
  • LABELS (basic)
  • SAFEPATH (basic)

User can add (but not remove auto-selected ones):

  • INCIDENTIQ
  • ECOLOG
  • SAFEENTRY
  • FIFRA

When User Selects STANDARD Plan:

Auto-selected modules:

  • CHEMIQ (standard)
  • PLAN (standard)
  • LABELS (standard)
  • SAFEPATH (standard)
  • INCIDENTIQ (standard)
  • ECOLOG (standard)
  • SAFEENTRY (standard)

User can add:

  • FIFRA

When User Selects PRO Plan:

Auto-selected modules (all available):

  • CHEMIQ (advanced)
  • PLAN (advanced)
  • LABELS (advanced)
  • SAFEPATH (advanced)
  • INCIDENTIQ (advanced)
  • ECOLOG (advanced)
  • SAFEENTRY (advanced)
  • FIFRA (advanced)

User cannot add any more (all USER_OPTION modules are included)


Testing

Test Backend API

Start the backend:

cd tellus-ehs-hazcom-service
source venv/bin/activate
python -m app.main

Test the endpoint:

curl http://localhost:8000/api/v1/plans | jq

Expected output:

[
{
"plan_id": "...",
"code": "STARTER",
"name": "Starter",
"description": "...",
"modules": [
{"module_id": "...", "code": "CHEMIQ", "name": "Chemical Inventory (SDS Binder)", "description": "..."},
{"module_id": "...", "code": "PLAN", "name": "HazCom Plan Builder", "description": "..."},
{"module_id": "...", "code": "LABELS", "name": "GHS / Secondary Labeling", "description": "..."},
{"module_id": "...", "code": "SAFEPATH", "name": "Safety Training & LMS", "description": "..."}
]
},
{
"plan_id": "...",
"code": "STANDARD",
"name": "Standard",
"description": "...",
"modules": [
{"module_id": "...", "code": "CHEMIQ", "name": "Chemical Inventory (SDS Binder)", "description": "..."},
{"module_id": "...", "code": "ECOLOG", "name": "Waste & Sustainability", "description": "..."},
{"module_id": "...", "code": "INCIDENTIQ", "name": "Incident & Spill Management", "description": "..."},
{"module_id": "...", "code": "LABELS", "name": "GHS / Secondary Labeling", "description": "..."},
{"module_id": "...", "code": "PLAN", "name": "HazCom Plan Builder", "description": "..."},
{"module_id": "...", "code": "SAFEENTRY", "name": "Contractor & Visitor Safety", "description": "..."},
{"module_id": "...", "code": "SAFEPATH", "name": "Safety Training & LMS", "description": "..."}
]
},
{
"plan_id": "...",
"code": "PRO",
"name": "Pro",
"description": "...",
"modules": [
{"module_id": "...", "code": "CHEMIQ", "name": "Chemical Inventory (SDS Binder)", "description": "..."},
{"module_id": "...", "code": "ECOLOG", "name": "Waste & Sustainability", "description": "..."},
{"module_id": "...", "code": "FIFRA", "name": "Pesticide Label Compliance", "description": "..."},
{"module_id": "...", "code": "INCIDENTIQ", "name": "Incident & Spill Management", "description": "..."},
{"module_id": "...", "code": "LABELS", "name": "GHS / Secondary Labeling", "description": "..."},
{"module_id": "...", "code": "PLAN", "name": "HazCom Plan Builder", "description": "..."},
{"module_id": "...", "code": "SAFEENTRY", "name": "Contractor & Visitor Safety", "description": "..."},
{"module_id": "...", "code": "SAFEPATH", "name": "Safety Training & LMS", "description": "..."}
]
}
]

Test Frontend

  1. Start frontend:

    cd tellus-ehs-hazcom-ui
    npm run dev
  2. Navigate to onboarding:

    • Go to http://localhost:5174/onboarding
    • Select an industry type (e.g., "Manufacturing")
    • Click "Continue" to Step 2
  3. Test plan switching:

    • Click "STARTER" plan → Should see 4 modules auto-selected
    • Click "STANDARD" plan → Should see 7 modules auto-selected
    • Click "PRO" plan → Should see 8 modules auto-selected
    • Try to uncheck a module included in the plan → Should show toast "This module is included in your selected plan"

Files Changed

Backend

  • app/services/plan_service.py - Fixed get_plans_with_modules() method

Frontend

  • No changes needed - Frontend was already correctly implemented

Summary

YES, it is now implemented - When users select different plans in onboarding Step 2, the modules list automatically updates to show which modules are included in that plan.

The frontend prevents users from deselecting modules that are included in their plan, and auto-selects all plan-included modules when switching plans.