Champion Portal Development Phase 1C
Estimated Effort: 1–2 weeks
Focus: Role Selection UI, Quiz Integration, Service ExtractionPrerequisites: Phase 1.4 complete (Profile Wizard & Edit)
Related Documents:
- ../README.md — Champion Portal overview
- ../../features/CHAMPION_SIGNUP_SYSTEM.md — Existing signup flow
- PHASE-1.md — Phase 1 foundations
- ../features/02-PROFILE-MANAGEMENT.md — Profile spec
Phase 1C integrates Champion Role Selection and the “Find Your Role” quiz into the Champion Portal’s profile wizard and profile edit flows. This enables new champions to discover and select their role as part of onboarding, while existing champions can revisit their role or retake the quiz anytime.
After Phase 1C, Champions can:
Key Principle: Reuse existing code from Champion Signup. Extract to services so both the public signup flow (champions.bualum.co) and the portal profile wizard share the same logic and data.
The current profile wizard (Phase 1.4) collects profile data but does not capture Champion role. The role selection and quiz currently only exist in the public signup flow at champions.bualum.co, which is designed for anonymous prospects—not authenticated portal users.
Champions care about their role identity. It helps them:
“What kind of Champion are you?” should be a welcoming, optional, fun step—not a barrier. The quiz is a lighthearted way to discover your role, not a test.
Phase 1C has 3 sub-phases:
| Sub-Phase | Name | Est. Time | |
|---|---|---|---|
| 1C.1 | Service Extraction | 1–2 days | ✅ Complete |
| 1C.2 | Profile Wizard Integration | 2–3 days | ✅ Complete |
| 1C.3 | Profile Edit Integration | 1–2 days | 🚧 Pending |
Completed: December 12, 2025
Goal: Extract quiz logic and role data from ChampionSignupsHelper into reusable services that both the public signup flow and the portal can use.
Rationale: The existing Champion Signup flow must continue working unchanged until the portal is fully launched. By extracting logic to services and having both flows call those services, we:
Deliverables:
ChampionRoleService — Role definitions, colors, descriptions, narrativesChampionQuizService — Questions, scoring, result calculationChampionSignupsHelper to delegate to servicesCp::ChampionRolesHelper that delegates to the same servicesService Design:
# app/services/champion_role_service.rb
class ChampionRoleService
ROLES = {
'connection_advisor' => {
letter: 'a',
color: 'fountainblue',
emoji: '🤝',
title: 'Connection Advisor',
seal: 'connection-advisor-seal',
description: 'You excel at connecting people and resources...',
short_desc: 'You love connecting people and resources.',
detailed: {
tagline: 'Support the network. Open doors.',
activities: ['Share job leads', 'Make introductions', 'Offer career advice'],
summary: 'You help Bruins grow professionally...'
}
},
# ... other roles
}.freeze
def self.all_roles
ROLES
end
def self.role(key)
ROLES[key.to_s]
end
def self.role_by_letter(letter)
ROLES.find { |_, r| r[:letter] == letter }&.first || 'community_builder'
end
def self.role_color(key)
ROLES.dig(key.to_s, :color) || 'gray'
end
def self.role_title(key)
ROLES.dig(key.to_s, :title) || key.to_s.humanize.titleize
end
def self.valid_role?(key)
ROLES.key?(key.to_s)
end
def self.narrative_for(roles)
# Returns the combined narrative for one or more roles
# ... (existing logic from helper)
end
end
# app/services/champion_quiz_service.rb
class ChampionQuizService
QUESTIONS = [
{
text: "A friend you admire takes a bold leap — what's your response?",
options: {
'a' => 'Introduce them to someone who's walked a similar path.',
'b' => 'Share their journey to inspire others.',
'c' => 'Organize a small gathering to cheer them on.',
'd' => 'Ask how you can practically support their next step.'
}
},
# ... 6 more questions
].freeze
def self.questions
QUESTIONS
end
def self.question_count
QUESTIONS.length
end
# Calculate the winning role(s) from quiz answers
# @param answers [Hash] { 'q0' => 'a', 'q1' => 'c', ... }
# @return [String] role key (e.g., 'connection_advisor')
def self.calculate_result(answers)
return 'community_builder' if answers.blank?
counts = Hash.new(0)
answers.each_value { |letter| counts[letter] += 1 }
result_letter = counts.max_by { |_, count| count }&.first
ChampionRoleService.role_by_letter(result_letter)
end
# Get all top roles (may be multiple if tied)
# @return [Array<Array>] [[role_key, color], ...]
def self.calculate_top_roles(answers)
return [['community_builder', 'belmontblue']] if answers.blank?
counts = role_counts(answers)
max = counts.values.map { |v| v[:count] }.max
counts.select { |_, v| v[:count] == max }
.map { |role, v| [role, v[:color]] }
end
def self.role_counts(answers)
counts = Hash.new { |h, k| h[k] = { count: 0, color: ChampionRoleService.role_color(k) } }
answers.each_value do |letter|
role = ChampionRoleService.role_by_letter(letter)
counts[role][:count] += 1 if role
end
counts
end
end
Migration Path:
ChampionSignupsHelper to delegate to services:
def champion_roles
ChampionRoleService.all_roles
end
def champion_questions
ChampionQuizService.questions
end
def champion_result_role(answers)
ChampionQuizService.calculate_result(answers)
end
Cp::ChampionRolesHelper with same delegation patternAcceptance Test:
# Existing signup flow still works
ChampionSignupsHelper.new.champion_roles.keys
# => ['connection_advisor', 'digital_ambassador', 'community_builder', 'giving_advocate']
# New service works identically
ChampionRoleService.all_roles.keys
# => ['connection_advisor', 'digital_ambassador', 'community_builder', 'giving_advocate']
# Quiz calculation works
ChampionQuizService.calculate_result({ 'q0' => 'a', 'q1' => 'a', 'q2' => 'b' })
# => 'connection_advisor'
Completed: December 12, 2025
Goal: Add a “Champion Role” step to the profile wizard with role selection and optional quiz.
Deliverables:
champion_role step to ProfileWizardController::STEPS_step_champion_role.html.erb partialWizard Step Position:
The champion role step now comes after basic_info and before location_contact:
| Step | Name | Required |
|---|---|---|
| 1 | basic_info | Yes |
| 2 | champion_role | No (new) |
| 3 | location_contact | No |
| 4 | professional | No |
| 5 | affinities | No |
| 6 | photo | No |
Step Design:
┌─────────────────────────────────────────────────────────────────┐
│ Step 2 of 6 [Skip for now] │
├─────────────────────────────────────────────────────────────────┤
│ │
│ What kind of Champion are you? │
│ │
│ Select the role that best fits how you like to contribute: │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 🤝 Connection Advisor ○ │ │
│ │ You love connecting people and resources. │ │
│ │ [More info ▼] │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 🙌 Digital Ambassador ○ │ │
│ │ You share stories and celebrate others online. │ │
│ │ [More info ▼] │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 👋 Community Builder ○ │ │
│ │ You bring Bruins together and create community. │ │
│ │ [More info ▼] │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 🫶 Giving Advocate ○ │ │
│ │ You inspire generosity and giving back. │ │
│ │ [More info ▼] │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ───────────────────────────────────────────────────────────── │
│ Not sure? Take our fun quiz to discover your role! → │
│ │
│ [Back] [Next] │
└─────────────────────────────────────────────────────────────────┘
Quiz Flow (within wizard):
When user clicks “Take our fun quiz”:
/profile/wizard/quiz/1 (question 1)Database Migration:
class AddQuizAnswersToCpChampions < ActiveRecord::Migration[7.1]
def change
add_column :cp_champions, :quiz_answers, :jsonb, default: {}
add_column :cp_champions, :quiz_completed_at, :datetime
end
end
Controller Changes:
# app/controllers/cp/profile_wizard_controller.rb
STEPS = %w[basic_info champion_role location_contact professional affinities photo].freeze
# Add quiz actions
def quiz
@question_index = params[:question].to_i - 1
@question = ChampionQuizService.questions[@question_index]
if @question.nil?
redirect_to cp_profile_wizard_path(step: 'champion_role')
return
end
@total_questions = ChampionQuizService.question_count
end
def submit_quiz_answer
@question_index = params[:question].to_i - 1
session[:quiz_answers] ||= {}
session[:quiz_answers]["q#{@question_index}"] = params[:answer]
next_question = @question_index + 2 # 1-indexed
if next_question > ChampionQuizService.question_count
redirect_to cp_profile_wizard_quiz_results_path
else
redirect_to cp_profile_wizard_quiz_path(question: next_question)
end
end
def quiz_results
@answers = session[:quiz_answers] || {}
@recommended_role = ChampionQuizService.calculate_result(@answers)
@top_roles = ChampionQuizService.calculate_top_roles(@answers)
@role_counts = ChampionQuizService.role_counts(@answers)
end
def select_quiz_role
role = params[:role]
if ChampionRoleService.valid_role?(role)
@champion.update(
primary_role: role,
quiz_answers: session[:quiz_answers] || {},
quiz_completed_at: Time.current
)
session.delete(:quiz_answers)
end
redirect_to cp_profile_wizard_path(step: 'location_contact')
end
Routes Addition:
# In champions subdomain routes
scope module: 'cp' do
# Existing wizard routes
get 'profile/wizard/:step', to: 'profile_wizard#show', as: :cp_profile_wizard
patch 'profile/wizard/:step', to: 'profile_wizard#update'
# Quiz routes (within wizard flow)
get 'profile/wizard/quiz/:question', to: 'profile_wizard#quiz', as: :cp_profile_wizard_quiz
post 'profile/wizard/quiz/:question', to: 'profile_wizard#submit_quiz_answer'
get 'profile/wizard/quiz-results', to: 'profile_wizard#quiz_results', as: :cp_profile_wizard_quiz_results
post 'profile/wizard/select-role', to: 'profile_wizard#select_quiz_role', as: :cp_profile_wizard_select_role
end
Goal: Allow champions to view/change their role and retake the quiz from profile edit.
Deliverables:
Profile Edit Section Design:
┌─────────────────────────────────────────────────────────────────┐
│ Champion Role │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Your Current Role: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 🤝 Connection Advisor │ │
│ │ "You help Bruins grow professionally by making the │ │
│ │ right connections at the right time." │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ [Change Role] [Retake Quiz to Discover Your Role] │
│ │
└─────────────────────────────────────────────────────────────────┘
Controller Changes:
Add to Cp::ProfileController:
def role
@current_role = @champion.primary_role
@all_roles = ChampionRoleService.all_roles
end
def update_role
if ChampionRoleService.valid_role?(params[:role])
@champion.update(primary_role: params[:role])
redirect_to cp_profile_edit_path, notice: "Your Champion role has been updated!"
else
redirect_to cp_profile_role_path, alert: "Please select a valid role."
end
end
def quiz
# Separate quiz flow for profile edit (not wizard)
@question_index = params[:question].to_i - 1
@question = ChampionQuizService.questions[@question_index]
# ... similar to wizard quiz
end
Routes Addition:
# Profile role management
get 'profile/role', to: 'profile#role', as: :cp_profile_role
patch 'profile/role', to: 'profile#update_role'
get 'profile/role/quiz/:question', to: 'profile#quiz', as: :cp_profile_role_quiz
post 'profile/role/quiz/:question', to: 'profile#submit_quiz_answer'
get 'profile/role/quiz-results', to: 'profile#quiz_results', as: :cp_profile_role_quiz_results
post 'profile/role/select-role', to: 'profile#select_quiz_role', as: :cp_profile_role_select
Both the existing Champion Signup flow and the new Portal flows should use the same service classes for:
These partials can be shared (or extracted to shared location):
| Partial | Current Location | Shared Usage |
|---|---|---|
| Role card | champions/champion_signups/steps/_role.html.erb |
Extract to shared/_champion_role_card.html.erb |
| Quiz question | champions/champion_signups/steps/_question.html.erb |
Extract to shared/_champion_quiz_question.html.erb |
| Quiz results | champions/champion_signups/steps/_results.html.erb |
Extract to shared/_champion_quiz_results.html.erb |
| Seal SVG | champions/champion_signups/_seal.svg.erb |
Already reusable |
| Score chart | champions/champion_signups/steps/_score_chart.html.erb |
Extract to shared |
Wizard Flow:
primary_role fieldProfile Edit Flow:
Service Extraction:
ChampionRoleService works identically to helper methodsChampionQuizService works identically to helper methods| Metric | Measurement | |——–|————-| | Role selection rate | % of champions with primary_role set | | Quiz completion rate | % of champions with quiz_completed_at | | Role distribution | Count per role type | | Quiz-driven selections | % who used quiz vs direct selection |
test/services/champion_role_service_test.rb
test/services/champion_quiz_service_test.rb
test/controllers/cp/profile_wizard_controller_test.rb (additions)
test/controllers/cp/profile_controller_test.rb (additions)
test/integration/cp/champion_role_wizard_test.rb
test/controllers/champions/champion_signups_controller_test.rb
After Phase 1C completion, update:
docs/CHANGELOG.md — Add Phase 1C featuresdocs/features/CHAMPION_SIGNUP_SYSTEM.md — Note service extractiondocs/planning/champion-portal/features/02-PROFILE-MANAGEMENT.md — Add role sectiondocs/planning/champion-portal/README.md — Update phase referencesdocs/planning/champion-portal/phases/README.md — Add Phase 1C rowapp/controllers/champions/roadmap_controller.rb — Add 1C subphasedocs/development/MODEL_RELATIONSHIPS.md — Document quiz_answers fieldapp/helpers/champion_signups_helper.rb — Role definitions, questions, scoringapp/services/champion_role_service.rbapp/services/champion_quiz_service.rbapp/helpers/cp/champion_roles_helper.rbapp/views/cp/profile_wizard/_step_champion_role.html.erbapp/views/cp/profile_wizard/quiz.html.erbapp/views/cp/profile_wizard/quiz_results.html.erbapp/views/cp/profile/_role_section.html.erbapp/views/shared/_champion_role_card.html.erb (optional extraction)app/views/shared/_champion_quiz_question.html.erb (optional extraction)db/migrate/XXXXXX_add_quiz_answers_to_cp_champions.rbapp/helpers/champion_signups_helper.rb — Delegate to servicesapp/controllers/cp/profile_wizard_controller.rb — Add role step and quizapp/controllers/cp/profile_controller.rb — Add role managementconfig/routes.rb — Add quiz routesapp/models/cp/champion.rb — Add quiz_answers attribute