Champion Portal Development Phase 1
Estimated Effort: 8–12 weeks
Focus: Auth, Profile, Directory, Regional structure, DashboardRelated Documents:
- ../README.md — Champion Portal overview
- ../development/DATA-ARCHITECTURE.md — Data ownership
- ../development/DECISIONS.md — Resolved decisions
- ../features/HOMEPAGE-DASHBOARD.md — Dashboard spec
Phase 1 establishes the foundation for the Champion Portal. Champions will be able to:
This phase delivers the core identity and discovery features that make Champions feel part of a community from day one.
Phase 1 is divided into sub-phases that can be implemented incrementally. Each sub-phase is a standalone prompt for an implementation agent.
| Sub-Phase | Name | Spec | Status | Est. Time |
|---|---|---|---|---|
| 1.1 | Database & Models | — | ✅ Complete | 1–2 days |
| 1.2 | Email Authentication | — | ✅ Complete | 1–2 days |
| 1.3 | SSO Authentication | — | ✅ Complete | 1–2 days |
| 1.4 | Profile & Directory | — | ✅ Complete | 2–3 days |
| 1.5 | Dashboard & Admin | — | ✅ Complete | 2–3 days |
| 1.6 | Dashboard, Directory & Metrics | — | ✅ Complete | 1–2 weeks |
| 1.7 | In-App Messaging | 1.7-messaging.md | ✅ Complete | 1–2 weeks |
| 1.8 | Staff Metrics Dashboard | 1.8-staff-metrics-dashboard.md | ✅ Complete | 1–2 weeks |
| 1.9 | Pre-Beta Polish | 1.9-pre-beta-polish.md | ✅ Complete | 2–3 weeks |
| 1.10 | Community News & Announcements | 1.10-community-news.md | ✅ Complete | 2–3 weeks |
| 1.11 | Admin Events | 1.11-admin-events.md | ✅ Complete | 1–2 weeks |
| 1.12 | Community Foundation | 1.12-community-foundation.md | ✅ Complete | 1–2 weeks |
| 1.13 | Community Landing Pages | 1.13-community-landing-pages.md | ✅ Complete | 1–2 weeks |
| 1.14 | Dynamic Community Creation | 1.14-dynamic-community-creation.md | ✅ Complete | 1–2 weeks |
| 1.15 | Community Enhancements | 1.15-community-enhancements.md | ✅ Complete | 1–2 weeks |
| 1.16 | Notifications System | 1.16-notifications.md | 🔲 Not Started | 3–4 weeks |
Goal: Create all database tables and models for Champion Portal foundations.
Status: ✅ COMPLETE (December 5, 2025)
Deliverables:
cp_champions table migration (all profile fields, verification_status enum)cp_profile_changes table migration (changelog for CRM export)
crm_data_changes table. See Affinaquest Import - UNIFIED_DATA_SYNC.mddistricts and regions tableszip_codes table with city/state lookupCp::Champion model with validations, Devise auth, associationsCp::ProfileChange model for CRM export trackingrails champion_portal:seed_geographic_dataImplementation Notes:
first_name, pref_first_name, college_last_name, last_nameMigrations Created:
20251204200000_create_champion_portal_foundation_tables.rb20251205194414_change_districts_csv_name_index_to_composite.rbDeployment: After deploying migrations, run seed task on staging/production:
heroku run rails champion_portal:seed_geographic_data --app alumni-lookup-staging
Acceptance Test:
# In rails console:
champion = Cp::Champion.create!(first_name: "Test", last_name: "User", email: "test@example.com", password: "password123")
champion.verification_status # => "unverified"
ZipCode.find_by(zip: "37027").district.name # => "Nashville"
ZipCode.find_by(zip: "37027").district.region.name # => "Southeast"
Goal: Champions can sign up with email, verify, and log in.
Status: ✅ COMPLETE (December 5, 2025)
Deliverables:
Cp::RegistrationsController — create account (name + email + zip code)Cp::ConfirmationsController — email verification with password settingCp::SessionsController — login/logoutCp::PasswordsController — forgot/reset passwordCp::DashboardController — authenticated landing pageCp::BaseController — feature flag checking, layoutCp::ChampionMailer — custom Devise mailer for champion subdomaindropdown_controller.js, mobile_menu_controller.jsTest Coverage (53 tests, 119 assertions):
test/controllers/cp/registrations_controller_test.rb (4 tests)test/controllers/cp/sessions_controller_test.rb (5 tests)test/controllers/cp/dashboard_controller_test.rb (4 tests)test/controllers/cp/confirmations_controller_test.rb (8 tests)test/controllers/cp/passwords_controller_test.rb (8 tests)test/models/cp/champion_test.rb (14 tests)test/mailers/cp/champion_mailer_test.rb (10 tests)Note: SSO buttons deferred to Phase 1.3 due to Devise multi-model OmniAuth path prefix conflict.
Files Created:
app/controllers/cp/base_controller.rbapp/controllers/cp/registrations_controller.rbapp/controllers/cp/sessions_controller.rbapp/controllers/cp/confirmations_controller.rbapp/controllers/cp/passwords_controller.rbapp/controllers/cp/dashboard_controller.rbapp/mailers/cp/champion_mailer.rbapp/views/cp/registrations/new.html.erbapp/views/cp/sessions/new.html.erbapp/views/cp/confirmations/show.html.erbapp/views/cp/confirmations/new.html.erbapp/views/cp/passwords/new.html.erbapp/views/cp/passwords/edit.html.erbapp/views/cp/dashboard/show.html.erbapp/views/layouts/champions.html.erbapp/views/layouts/champions/_header.html.erbapp/views/layouts/champions/_footer.html.erbapp/views/layouts/champions/_mobile_nav.html.erbapp/views/cp/champion_mailer/*.html.erb and .text.erbapp/javascript/controllers/dropdown_controller.jsapp/javascript/controllers/mobile_menu_controller.jstest/controllers/cp/*_controller_test.rbtest/models/cp/champion_test.rbtest/mailers/cp/champion_mailer_test.rbRoutes Added:
/signup → registrations#new/create/login → sessions#new/create/logout → sessions#destroy/confirm → confirmations#show/create/password/new → passwords#new/create/edit/update/dashboard → dashboard#showAcceptance Test:
champions.local.test/signup → enter name + email → submitGoal: Champions can sign up/log in with Google (Apple/Facebook deferred to future enhancement).
Status: ✅ COMPLETE (December 8, 2025)
Deliverables:
Cp::OmniauthCallbacksController)/profile/complete)google_uid column on cp_champions (existing from Phase 1.1)Implementation Notes:
/auth path prefix to work with multiple Devise modelsdevise_scope blocks for proper Devise mappingemail_verified status (email confirmed by provider)/profile/complete after loginGOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET env varsFiles Created:
app/controllers/cp/omniauth_callbacks_controller.rbapp/controllers/cp/profile_controller.rbapp/views/cp/profile/show.html.erbapp/views/cp/profile/edit.html.erbapp/views/cp/profile/complete.html.erbtest/controllers/cp/omniauth_callbacks_controller_test.rbtest/controllers/cp/profile_controller_test.rbFiles Modified:
config/initializers/devise.rb — Added path_prefix: '/auth' to Google OAuth configconfig/initializers/omniauth.rb — Added OmniAuth.config.path_prefix = '/auth'config/routes.rb — OmniAuth callbacks inside devise_scope blocksapp/views/cp/sessions/new.html.erb — Google button at topapp/views/cp/registrations/new.html.erb — Google button at topRoutes Added:
GET/POST /auth/google_oauth2/callback → cp/omniauth_callbacks#google_oauth2GET /auth/failure → cp/omniauth_callbacks#failureGET /profile → cp/profile#showGET /profile/edit → cp/profile#editPATCH /profile → cp/profile#updateGET /profile/complete → cp/profile#completePATCH /profile/complete → cp/profile#save_completeAcceptance Test:
dev.alumnichampions.com:3000/login → see “Sign in with Google” at topGoal: Champions can complete profiles and search for other Champions.
Deliverables:
Profile Completion Wizard:
Affinity Selection (Step 4 & Profile Edit):
cp_affinities table migration (champion_id, affinity_code, timestamps)Cp::Affinity model with associationscp_affinity_selector_controller.js for Champion-specific selection/api/affinities)Profile Edit Page:
Directory:
Cp::DirectoryController with searchrequire_champion_verified!)Cp::Champion.verified)Export Infrastructure:
Key UX Decisions:
Acceptance Test:
Goal: Champions see personalized dashboard; Staff can verify Champions.
Status: ✅ COMPLETE (December 19, 2025)
Prerequisites: Portal Admin role must be implemented (see AUTH_AND_ROLES_SYSTEM.md)
Portal Admin Role (December 2025): ✅ Complete
portal_admin role added to User modelportal_admin? helper method for authorizationensure_portal_admin! guard in ApplicationControllerensure_portal_admin!Deliverables:
/champions with dedicated layoutChampions::ChampionsController — Dashboard with stats, funnel, regional breakdownChampions::VerificationsController — Verification queue with approve/reject/search/linkChampions::StatsController — Placeholder for Phase 1.6/6 detailed statisticschampion_admin.html.erb layout with sidebar navigationCp::DashboardController — Champion-facing dashboard (completed in Phase 1.6)Note: The Champion-facing dashboard (Cp::DashboardController) is part of the Champion Portal itself (accessed via alumnichampions.com). This phase focused on the admin-side verification tools in the Lookup Portal. Champion-facing features continue in Phase 1.4 (Profile & Directory).
Implementation Notes:
pending_buid IS NOT NULL first, then by created_atbuid, clears pending_buid, sets verification_status = champion_verifiedpending_buid only, keeps champion in queue for manual searchFiles Created:
app/controllers/champions/base_controller.rbapp/controllers/champions/champions_controller.rbapp/controllers/champions/verifications_controller.rbapp/controllers/champions/stats_controller.rbapp/controllers/settings/districts_controller.rbapp/views/champions/**/*.html.erb (sidebar, dashboard, verification queue)app/views/settings/districts/**/*.html.erb (index, edit, partials)app/views/layouts/champion_admin.html.erbtest/controllers/champions/verifications_controller_test.rb (18 tests)test/controllers/settings/districts_controller_test.rb (23 tests)Routes Added:
/champions — Champions namespace root (dashboard)/champions/verifications — Verification queue/champions/verifications/:id — Review single champion/champions/verifications/:id/approve — Approve pending match/champions/verifications/:id/reject — Reject pending match/champions/verifications/:id/search_alumni — AJAX alumni search/champions/verifications/:id/link_buid — Manual BUID link/champions/stats — Statistics placeholder/settings/districts — District management/settings/districts/load_more — Turbo Stream paginationTest Coverage: 41 new tests (18 verifications + 23 districts)
Important: Pending BUID Verification (Security Feature)
As of December 2025, champions who confirm their education via name-based match (not email match) have their selected BUID stored in pending_buid instead of auto-linking. This creates two verification workflows:
| Scenario | Staff Action |
|---|---|
Champion has pending_buid |
Review pending match → Approve (link BUID) or Reject |
| Champion has no match | Search alumni manually → Link BUID |
The verification queue should:
pending_buid set, ordered by signup dateDatabase columns used:
pending_buid — BUID selected by champion during education confirmationpending_buid_match_type — How match was found (‘name’ typically)buid — Linked BUID (set after staff approval or email-based auto-link)Model helper:
champion.pending_alumni — Returns Alumni record for pending_buidAcceptance Test:
Goal: Complete the Staging MVP — champion-facing dashboard, enhanced directory filters, and activity tracking infrastructure.
Status: ✅ COMPLETE
Prerequisites: Phase 1.5 complete
Replaces: Phase 1B (Metrics Foundation) — absorbed and scoped for MVP
Related Documents:
Why This Sub-Phase Exists:
Phase 1B was originally planned as a separate metrics-only phase. We’ve absorbed its core infrastructure into Sub-Phase 1.6 because:
Jobs to Be Done Alignment:
| Job | How This Sub-Phase Addresses It |
|---|---|
| C1: Find My Tribe | Enhanced directory filters make it easier to find Champions with shared background |
| E2: See the Big Picture | Staff dashboard shows Champion activity metrics at a glance |
Internal Sub-Phases (1.6.1, 1.6.2, 1.6.3):
| Sub-Phase | Name | Est. Time |
|---|---|---|
| 1.6.1 | Activity Tracking Infrastructure | 1–2 days |
| 1.6.2 | Champion Dashboard Completion | 1–2 days |
| 1.6.3 | Directory Enhancements | 2–3 days |
Goal: Create the data infrastructure to track Champion activities from day one.
Deliverables:
cp_activity_events table migrationCp::ActivityEvent model with validationsCp::ActivityRecorder service for recording eventslogin, profile_edit, directory_search, profile_viewEvent Types (Initial):
| Event | Description | Recorded When |
|---|---|---|
login |
Champion logged in | On sign_in callback |
profile_edit |
Champion updated their profile | On profile#update |
directory_search |
Champion searched directory | On directory#index with params |
profile_view |
Champion viewed another profile | On directory#show |
Database Schema:
create_table :cp_activity_events do |t|
t.references :cp_champion, null: false, foreign_key: true
t.string :event_type, null: false
t.jsonb :metadata, default: {}
t.datetime :occurred_at, null: false
t.timestamps
end
add_index :cp_activity_events, [:event_type, :occurred_at]
add_index :cp_activity_events, [:cp_champion_id, :event_type, :occurred_at]
ActivityRecorder Service:
# Usage: Cp::ActivityRecorder.record(champion, :login)
# Usage: Cp::ActivityRecorder.record(champion, :profile_view, target_id: other_champion.id)
# Usage: Cp::ActivityRecorder.record(champion, :directory_search, filters: { district_id: 1 })
module Cp
class ActivityRecorder
VALID_EVENTS = %w[login profile_edit directory_search profile_view].freeze
def self.record(champion, event_type, metadata = {})
return unless VALID_EVENTS.include?(event_type.to_s)
champion.activity_events.create!(
event_type: event_type.to_s,
metadata: metadata,
occurred_at: Time.current
)
end
end
end
Acceptance Test:
loginprofile_editdirectory_search and filter metadataprofile_viewGoal: Complete the champion-facing dashboard with real community stats and district preview.
Status: ✅ COMPLETE
Current State:
Deliverables:
login activity event on dashboard loadDashboard Sections (Updated):
┌─────────────────────────────────────────────────────────────────────┐
│ Welcome Banner (existing) │
│ - Personalized greeting │
│ - Profile completion prompt (if incomplete) │
├─────────────────────────────────────────────────────────────────────┤
│ Community Snapshot (unified block) │
│ - District name + community counts │
│ - Alumni in your district preview │
│ (degree + class year shown, fallback: "Belmont Alum") │
│ - CTA: Browse all alumni in your district (or invite others) │
├─────────────────────────────────────────────────────────────────────┤
│ Quick Actions (existing) │
├─────────────────────────────────────────────────────────────────────┤
│ Recent Activity (improved placeholder) │
│ "Your activity will appear here as you explore and connect." │
│ [Browse the Directory →] [Complete Your Profile →] │
└─────────────────────────────────────────────────────────────────────┘
Controller Changes:
# app/controllers/cp/dashboard_controller.rb
def show
@champion = current_cp_champion
# Record login activity
Cp::ActivityRecorder.record_login_once_per_session(@champion, session)
# Community stats
@total_champions = Cp::Champion.verified.count
@district_champions_count = if @champion.district.present?
Cp::Champion.verified
.location_visible_to(@champion)
.where(district: @champion.district)
.count
else
0
end
# Preview cards (3 random alumni from district, excluding self)
@district_preview = if @champion.district.present?
Cp::Champion.verified
.location_visible_to(@champion)
.where(district: @champion.district)
.where.not(id: @champion.id)
.includes(alumni: { degrees: :major })
.order("RANDOM()")
.limit(3)
else
[]
end
end
Acceptance Test:
Goal: Add the most valuable directory filters and sorting options based on competitive analysis.
Current State:
Deliverables:
New Filters:
district_autocomplete_controller.js)Sorting:
Activity Tracking:
directory_search with filter metadataprofile_view when viewing Champion profileGraduation Year Filter (Hierarchical Dropdown):
The graduation year filter uses a hierarchical dropdown where selecting a decade header selects all years within it:
┌─────────────────────────────┐
│ Graduation Year ▼ │
├─────────────────────────────┤
│ ▶ All Years │
│ ▶ 2020s │
│ ├── 2024 │
│ ├── 2023 │
│ ├── 2022 │
│ ├── 2021 │
│ └── 2020 │
│ ▶ 2010s │
│ ├── 2019 │
│ ├── 2018 │
│ └── ... │
│ ▶ 2000s │
│ ▶ 1990s │
│ ▶ Earlier │
└─────────────────────────────┘
Behavior:
Filter Logic (Backend):
# Decade selection: ?grad_decade=2020
# Individual year: ?grad_year=2024
# Multiple years: ?grad_years[]=2022&grad_years[]=2023
if params[:grad_decade].present?
decade_start = params[:grad_decade].to_i
decade_end = decade_start + 9
query = query.joins(alumni: :degrees)
.where("EXTRACT(YEAR FROM degrees.degree_date) BETWEEN ? AND ?",
decade_start, decade_end)
.distinct
elsif params[:grad_year].present?
query = query.joins(alumni: :degrees)
.where("EXTRACT(YEAR FROM degrees.degree_date) = ?", params[:grad_year])
.distinct
elsif params[:grad_years].present?
query = query.joins(alumni: :degrees)
.where("EXTRACT(YEAR FROM degrees.degree_date) IN (?)", params[:grad_years])
.distinct
end
College Filter:
# Load colleges from degrees of verified champions
@colleges = College.joins(majors: { degrees: { alumni: :cp_champion } })
.where(cp_champions: { verification_status: :champion_verified })
.distinct
.order(:college_name)
District Filter (Autocomplete):
Important: With 820 districts, a dropdown would be terrible UX. Reuse the existing district_autocomplete_controller.js Stimulus controller, which is already used in the Lookup Portal alumni search.
<!-- Reuse the existing district autocomplete pattern from alumni/search.html.erb -->
<div data-controller="district-autocomplete" class="relative">
<label for="district_name">District</label>
<input type="hidden" name="district_id" id="district_id" value="<%= params[:district_id] %>"
data-district-autocomplete-target="districtCode">
<input type="text"
id="district_name"
data-district-autocomplete-target="input"
data-action="input->district-autocomplete#search keydown->district-autocomplete#navigate"
value="<%= @selected_district&.display_name %>"
placeholder="Type to search districts..."
autocomplete="off">
<% if params[:district_id].present? %>
<button type="button" data-action="district-autocomplete#clear">×</button>
<% end %>
<div data-district-autocomplete-target="results" class="absolute z-10 hidden"></div>
</div>
API Endpoint (existing): /api/districts?q= — Returns JSON with code, display_name, name, state, region
Sort Dropdown:
<select name="sort">
<option value="name" <%= 'selected' if params[:sort] == 'name' %>>Name A-Z</option>
<option value="recent" <%= 'selected' if params[:sort] == 'recent' %>>Recently Joined</option>
</select>
# Controller
case params[:sort]
when "recent"
query.order(created_at: :desc)
else
query.order(:first_name, :last_name)
end
Activity Tracking:
# In DirectoryController#index
def index
# ... existing filter logic ...
# Record search activity (only if authenticated and has filters)
if current_cp_champion && params.except(:controller, :action, :page).any?
Cp::ActivityRecorder.record(
current_cp_champion,
:directory_search,
filters: params.slice(:q, :district_id, :district, :industry, :role,
:affinity_code, :grad_years, :grad_year, :grad_decade,
:college_id, :college_code).to_h
)
end
end
# In DirectoryController#show
def show
@champion = Cp::Champion.verified.find(params[:id])
# Record profile view (don't record viewing own profile)
if current_cp_champion && current_cp_champion != @champion
Cp::ActivityRecorder.record(
current_cp_champion,
:profile_view,
target_champion_id: @champion.id
)
end
end
Acceptance Test:
Key Decisions (Sub-Phase 1.6):
| Decision | Choice | Rationale |
|---|---|---|
| Area terminology | “District” not “Region” | District = metro area (Nashville), Region = too broad (Southeast) |
| Graduation filter style | Hierarchical dropdown | Can select decade (all years) OR individual year |
| District filter style | Autocomplete | 820 districts — dropdown would be terrible UX; reuse existing district_autocomplete_controller.js |
| Initial sort options | Name A-Z, Recently Joined | Simple MVP; can add “Recently Active” after tracking is live |
| Dashboard preview | 3 cards + count + link | Shows community exists without overwhelming |
| Phase structure | Absorb 1B into 1.6 | Cleaner MVP grouping, no metrics-only phase |
Scope:
In Scope:
Out of Scope (Phase 6 / Future):
Definition of Success (Sub-Phase 1.6):
Staging MVP Readiness:
Tests to Create (Sub-Phase 1.6):
Model Tests (test/models/cp/activity_event_test.rb):
Service Tests (test/services/cp/activity_recorder_test.rb):
Controller Tests:
Documentation Updates (Sub-Phase 1.6):
Cp::ActivityEvent to ../../development/MODEL_RELATIONSHIPS.mdRelationship to Other Phases:
Builds on:
Enables:
Supersedes:
| Question | Decision | Source |
|---|---|---|
| What is the onboarding flow? | Progressive Account Creation (name + email first) | DECISIONS.md §2.1 |
| Is BUID required at signup? | No — Optional + Two-tier verification | DECISIONS.md §2.2 |
| What is the account data model? | Single cp_champions table with verification_status enum |
DECISIONS.md §2.3 |
| Which SSO providers? | Google, Apple, Facebook | DECISIONS.md §2.12 |
| How is location captured? | ZIP-first — ZIP → city/state/district/region | DECISIONS.md §2.10 |
| How are photos handled? | Separate: cp_champions.photo for Champion Portal |
DECISIONS.md §2.9 |
| Mobile or desktop first? | Mobile-first | DECISIONS.md §2.5 |
| Table naming convention? | cp_ prefix for Champion Portal tables |
DECISIONS.md §2.6 |
| Question | Decision | Notes |
|---|---|---|
| How should ZIP → District mapping be seeded? | CSV import — Seed from zip_district_region.csv |
Rake task: rails champion_portal:seed_geographic_data |
| What districts/regions exist initially? | 820 districts, 7 regions, with 8 highlighted | See data details below |
| How should verification queue be organized in Lookup? | New /champions namespace in Lookup Portal |
Implemented December 2025 with dedicated layout |
| Password requirements? | 8+ characters, at least 1 letter and 1 number | Balanced security/friction |
| Session duration? | 7 days default, 30 days with “Remember me” | Mobile-friendly with security option |
Geographic Data (Imported December 5, 2025):
| Question | Who Decides | Impact |
|---|---|---|
| Pilot group composition | Engagement Team | Which Champions get first access |
| Verification turnaround SLA | Engagement Team | User messaging about wait time |
| Profile fields required vs optional | Product/Engagement | Onboarding friction |
| Champion role quiz — required or optional? | Product | Onboarding flow |
| Area | Deliverables |
|---|---|
| Authentication | Email signup, password reset, email verification, Google/Apple/Facebook SSO |
| Account model | cp_champions table with verification status enum |
| Profile | Personal info, contact info, employment, affinities, photo upload |
| Privacy controls | Per-field visibility settings |
| Location | ZIP-based city/district/region assignment |
| Directory | Search by name, city, grad year, college, major, industry |
| Dashboard | Welcome, role display, quick actions, regional events (placeholder), regional posts (placeholder) |
| Admin: Verification | Engagement Team can search alumni, link BUID to Champion account |
| Regional structure | Districts and regions tables, ZIP mapping |
| Feature | Phase |
|---|---|
| Event submission/calendar | Phase 2 |
| Story submission | Phase 2 |
| Mentorship matching | Phase 2 |
| Discussion boards | Phase 3 |
| Direct messaging | Phase 4 |
| Visit mode | Phase 5 |
| Map view | Phase 5 |
| Reporting dashboards | Phase 6 |
| Criterion | Validation |
|---|---|
| Champion can sign up with email | End-to-end test passes |
| Champion can sign up with Google SSO | End-to-end test passes |
| Champion can sign up with Apple SSO | End-to-end test passes |
| Champion can sign up with Facebook SSO | End-to-end test passes |
| Email verification flow works | User receives email, clicks link, can set password |
| Password reset works | User can request and complete reset |
| Email Verified user can edit own profile | Access control enforced |
| Email Verified user cannot see directory | Access control enforced |
| Champion Verified user can browse directory | Directory search returns results |
| Champion Verified user appears in directory | Searchable by name, city, etc. |
| ZIP code → city/district/region assignment works | Auto-populated on profile |
| Profile changes are logged | crm_data_changes records created (unified CRM sync) |
| Staff can verify Champions in Lookup Portal | Verification queue functional |
| Staff can search alumni and link BUID | BUID linking works |
| Dashboard displays for logged-in Champion | Personalized greeting, role, region |
| Metric | Target |
|---|---|
| Signup completion rate (email verified) | >80% |
| Profile completion rate | >70% |
| Time from signup to Champion Verified | <1 business day |
| Directory searches per pilot user | 2+ during pilot |
| Model | Test Cases |
|---|---|
Cp::Champion |
Valid factory, validation errors, verification status transitions, associations |
Cp::Champion |
ZIP → city/state/district/region lookup |
Cp::Champion |
Photo attachment (ActiveStorage) |
Cp::ProfileChange |
Auto-creates on attribute changes |
Cp::ProfileChange |
Tracks field_name, old_value, new_value, changed_by |
| Controller | Test Cases |
|---|---|
Cp::RegistrationsController |
Create account with valid data, reject invalid email, reject duplicate email |
Cp::RegistrationsController |
SSO callback creates account, links existing account |
Cp::SessionsController |
Login with password, login with SSO, logout |
Cp::SessionsController |
Reject unverified users, reject invalid credentials |
Cp::ChampionsController |
Email Verified can edit own profile, cannot edit others |
Cp::ChampionsController |
Champion Verified can view own profile |
Cp::DirectoryController |
Champion Verified can search, Email Verified cannot |
Cp::DirectoryController |
Search by name, city, grad year, college, industry |
Cp::DashboardController |
Returns dashboard for logged-in Champion |
Admin::VerificationsController |
Staff can view queue, search alumni, link BUID |
Admin::VerificationsController |
Queue shows champions with pending_buid first |
Admin::VerificationsController |
Staff can approve pending_buid (links BUID, sets verified) |
Admin::VerificationsController |
Staff can reject pending_buid (clears pending_buid) |
| Feature | Test Cases |
|---|---|
| Email signup flow | Complete journey: signup → email → verify → set password → profile |
| Google SSO flow | Click Google → authorize → account created → profile completion |
| Apple SSO flow | Click Apple → authorize → account created → profile completion |
| Facebook SSO flow | Click Facebook → authorize → account created → profile completion |
| Password reset | Request reset → email → click link → new password → login |
| Directory search | Log in as Champion Verified → search by name → view profile |
| Verification workflow (pending) | Staff logs into Lookup → sees pending match → approves → BUID linked |
| Verification workflow (manual) | Staff logs into Lookup → searches alumni → links BUID → champion verified |
| Scenario | Expected Behavior |
|---|---|
| Unverified user tries to log in | Rejected with “verify your email” message |
| Email Verified user visits directory | Redirected with “verification pending” message |
| Email Verified user edits own profile | Allowed |
| Champion Verified user visits directory | Allowed |
| Non-staff user visits verification queue | Rejected (403) |
| Data | Test Cases |
|---|---|
| Unique, valid format, required | |
| ZIP code | Valid 5-digit format, exists in database/lookup |
| Password | Meets complexity requirements |
| Profile fields | Required fields enforced, optional fields nullable |
After completing Phase 1, update the following documents:
| Document | Changes |
|---|---|
| ../README.md | Mark Phase 1 as complete, update prerequisites |
| ../development/DATA-ARCHITECTURE.md | Finalize cp_champions schema, add actual column list |
| ../development/DECISIONS.md | Record any new decisions made during implementation |
| ../STAKEHOLDER-OVERVIEW.md | Update pilot status |
| ../../features/CHAMPION_PORTAL.md | Create implemented feature doc (move from planning) |
| Document | Purpose |
|---|---|
docs/features/CHAMPION_PORTAL.md |
Document implemented functionality |
docs/development/CHAMPION_VERIFICATION_WORKFLOW.md |
Staff workflow guide |
| Champion Portal user guide (for pilots) | End-user documentation |
| Fixture | Purpose |
|---|---|
cp_champions.yml |
Test champions in various verification states |
districts.yml |
Test districts (Nashville, etc.) |
regions.yml |
Test regions (Southeast, etc.) |
zip_codes.yml |
Test ZIP codes mapped to districts |
For detailed specifications on each feature area, see: