Champion Portal Development Sub-Phase 1.14
Estimated Effort: 1–2 weeks
Focus: Background detection and creation of emergent communitiesPrerequisites: Phase 1.12 (Community Foundation) and Phase 1.13 (Landing Pages) complete
Status: ✅ COMPLETE — January 2026
Related Documents:
- 1.12-community-foundation.md — Schema and threshold logic
- 1.13-community-landing-pages.md — UI that displays communities
- ../../JOBS-TO-BE-DONE.md — Job C1: Find My Tribe
Completed: January 2026
Cp::CommunityDetectionService — Detects when threshold (3 Champions) is met for any community type (district, college, major, affinity, industry)
district_id, college_code, major_code, affinity_code, industry)Cp::CommunityCreationJob — Background job triggered after verification and profile edits
Cp::CommunityNotification model — Tracks notifications per Champion per Community
notification_type enum (currently only threshold_reached, room for future types)Cp::CommunityNotificationsController — Dismiss endpoint for notifications
ChampionVerificationService#perform — After verification (college/major)ProfileWizardController#update — After location, profession, affinities stepsProfileController#update — After location, profession, affinities section edits| File | Purpose |
|---|---|
db/migrate/20260109003249_create_cp_community_notifications.rb |
Migration |
app/models/cp/community_notification.rb |
Notification model |
app/models/cp/community.rb |
Added has_many :community_notifications |
app/services/cp/community_detection_service.rb |
Detection logic |
app/jobs/cp/community_creation_job.rb |
Background job |
app/controllers/cp/community_notifications_controller.rb |
Dismiss actions |
app/views/cp/dashboard/_community_notifications.html.erb |
Dashboard partial |
app/views/cp/dashboard/show.html.erb |
Integrated notification partial |
app/controllers/cp/dashboard_controller.rb |
Load notifications |
config/routes.rb |
Added notification routes |
app/services/cp/champion_verification_service.rb |
Added job trigger |
app/controllers/cp/profile_wizard_controller.rb |
Added job trigger |
app/controllers/cp/profile_controller.rb |
Added job trigger |
test/models/cp/community_notification_test.rb — 19 teststest/services/cp/community_detection_service_test.rb — 16 teststest/jobs/cp/community_creation_job_test.rb — 11 teststest/controllers/cp/community_notifications_controller_test.rb — 9 tests| Feature | Reason |
|---|---|
| Email notifications for new communities | In-app sufficient for MVP |
threshold_near notification type |
Nice-to-have; would require checking when count = THRESHOLD - 1 |
| Custom communities auto-creation | Not needed; custom communities are staff-created |
Phase 1.14 completes the “emergent community” model by implementing automatic community creation when Champions organically form groups that meet the threshold.
The Vision:
Communities aren’t pre-created for every possible combination. Instead, they emerge naturally:
After Phase 1.14:
Without dynamic creation, admins would need to:
With 5 community types × hundreds of possible values, this is unsustainable.
Dynamic creation enables a delightful user experience:
“I joined as the only person from my major. Three months later, I got a notification that there were now 3 of us — and I had a community page to visit!”
This creates a sense of the platform being alive and growing.
Job C1: Find My Tribe
“When I move to a new city or want to expand my Belmont network, I want to find other Champions/alumni nearby…”
Dynamic communities mean Champions find their tribe automatically, even for niche interests.
| Decision | Choice | Rationale |
|---|---|---|
| Detection timing | Background job after Champion assignment | Don’t slow down verification flow |
| Notification method | In-app (Dashboard prompt) only for MVP | Email notifications deferred to Backlog |
| Batch vs. real-time | Real-time (job runs after each verification) | Communities should form immediately |
| Notification persistence | Database-backed (seen/unseen tracking) | Champions shouldn’t miss notifications |
| Who gets notified? | All members of newly created community | Everyone should know they’re part of something new |
| Sub-Phase | Name | Est. Time |
|---|---|---|
| 1.14.1 | Threshold Detection Service | 2–3 days |
| 1.14.2 | Community Creation Job | 1–2 days |
| 1.14.3 | In-App Notification System | 2–3 days |
| 1.14.4 | Dashboard Integration | 1–2 days |
| 1.14.5 | Testing & Edge Cases | 1–2 days |
Detection happens after a Champion is verified and assigned to communities (via CommunityAssignmentService from Phase 1.12).
# In verification flow (simplified)
class ChampionVerificationService
def verify(champion)
# ... existing verification logic ...
# Assign to existing communities
CommunityAssignmentService.assign_all(champion)
# Check if any new communities should be created
CommunityCreationJob.perform_later(champion.id)
end
end
# app/services/cp/community_detection_service.rb
module Cp
class CommunityDetectionService
THRESHOLD = 3
def initialize(champion)
@champion = champion
end
def detect_and_create
new_communities = []
# Check each community type
new_communities += check_district_communities
new_communities += check_college_communities
new_communities += check_major_communities
new_communities += check_affinity_communities
new_communities += check_industry_communities
new_communities.compact
end
private
def check_district_communities
return [] unless @champion.district_id
# Count Champions with this district who aren't in a district community yet
district_count = Cp::Champion.verified
.where(district_id: @champion.district_id)
.count
# Check if community already exists
existing = Cp::Community.find_by(community_type: :district, district_id: @champion.district_id)
if district_count >= THRESHOLD && existing.nil?
[create_district_community(@champion.district)]
else
[]
end
end
def check_college_communities
return [] unless @champion.alumni
@champion.alumni.degrees.map do |degree|
next unless degree.major&.college_code
college_code = degree.major.college_code
existing = Cp::Community.find_by(community_type: :college, college_code: college_code)
next if existing
# Count Champions with degrees from this college
college_count = count_champions_with_college(college_code)
if college_count >= THRESHOLD
create_college_community(college_code)
end
end.compact
end
# Similar methods for major, affinity, industry...
def create_district_community(district)
community = Cp::Community.create!(
name: district.name,
slug: district.name.parameterize,
community_type: :district,
district: district,
threshold: THRESHOLD,
active: true
)
# Assign all qualifying Champions
assign_champions_to_community(community)
community
end
def assign_champions_to_community(community)
qualifying_champions = find_qualifying_champions(community)
qualifying_champions.each do |champion|
Cp::ChampionCommunity.find_or_create_by!(
champion: champion,
community: community
)
end
# Notify all members
community.champions.each do |champion|
Cp::CommunityNotification.create!(
champion: champion,
community: community,
notification_type: :new_community
)
end
end
end
end
# app/jobs/cp/community_creation_job.rb
module Cp
class CommunityCreationJob < ApplicationJob
queue_as :default
def perform(champion_id)
champion = Cp::Champion.find(champion_id)
service = CommunityDetectionService.new(champion)
new_communities = service.detect_and_create
if new_communities.any?
Rails.logger.info "Created #{new_communities.size} new communities for Champion #{champion_id}"
end
end
end
end
# db/migrate/XXXXXX_create_cp_community_notifications.rb
create_table :cp_community_notifications do |t|
t.references :champion, null: false, foreign_key: { to_table: :cp_champions }
t.references :community, null: false, foreign_key: { to_table: :cp_communities }
t.integer :notification_type, null: false, default: 0 # new_community, threshold_near, etc.
t.boolean :read, default: false
t.datetime :read_at
t.timestamps
t.index [:champion_id, :read]
t.index [:champion_id, :community_id, :notification_type], unique: true, name: 'idx_cp_community_notif_unique'
end
# app/models/cp/community_notification.rb
module Cp
class CommunityNotification < ApplicationRecord
self.table_name = 'cp_community_notifications'
belongs_to :champion, class_name: 'Cp::Champion'
belongs_to :community, class_name: 'Cp::Community'
enum :notification_type, {
new_community: 0, # You're part of a new community!
threshold_near: 1 # Future: "1 more person and X becomes a community"
}
scope :unread, -> { where(read: false) }
scope :recent, -> { order(created_at: :desc) }
def mark_as_read!
update!(read: true, read_at: Time.current)
end
end
end
The Dashboard will show unread community notifications as a prompt:
<%# app/views/cp/dashboard/_community_notifications.html.erb %>
<% if @community_notifications.any? %>
<div class="bg-gradient-to-r from-skyblue/10 to-belmontblue/10 border border-skyblue/20 rounded-xl p-6 mb-6">
<div class="flex items-start gap-4">
<div class="flex-shrink-0">
<span class="inline-flex items-center justify-center w-10 h-10 rounded-full bg-skyblue/20">
🎉
</span>
</div>
<div class="flex-1">
<h3 class="font-semibold text-gray-900">New Community Alert!</h3>
<% @community_notifications.each do |notification| %>
<p class="text-gray-600 mt-1">
You're now part of <strong><%= notification.community.name %></strong>
with <%= notification.community.member_count %> fellow Champions!
</p>
<% end %>
<div class="mt-3 flex gap-2">
<%= link_to "View Community", cp_community_path(@community_notifications.first.community),
class: "inline-flex items-center px-4 py-2 bg-belmontblue text-white rounded-lg hover:bg-fountainblue" %>
<%= button_to "Dismiss", dismiss_cp_community_notifications_path,
method: :post,
class: "inline-flex items-center px-4 py-2 text-gray-600 hover:text-gray-800" %>
</div>
</div>
</div>
</div>
<% end %>
| Feature | Description |
|---|---|
CommunityDetectionService |
Detect when threshold is met for any community type |
CommunityCreationJob |
Background job to create communities |
cp_community_notifications table |
Track notifications for Champions |
Cp::CommunityNotification model |
Notification model with read tracking |
| Dashboard prompt | Show “New Community” notification on Dashboard |
| Dismiss functionality | Champions can dismiss/acknowledge notifications |
| Auto-assignment | All qualifying Champions assigned when community created |
| Feature | Reason | Target Phase |
|---|---|---|
| Email notifications | Complexity; in-app sufficient for MVP | Backlog |
| In-app notification dropdown | Requires broader notification system | Backlog |
| “Almost a community” alerts | Nice-to-have | Backlog |
| Champion opt-out of auto-join | Edge case | Backlog |
| Admin approval for new communities | Trust the threshold | Future if needed |
# test/services/cp/community_detection_service_test.rb
class Cp::CommunityDetectionServiceTest < ActiveSupport::TestCase
test "creates district community when threshold met"
test "creates college community when threshold met"
test "creates major community when threshold met"
test "creates affinity community when threshold met"
test "creates industry community when threshold met"
test "does not create duplicate community"
test "assigns all qualifying champions to new community"
test "creates notification for each member"
test "handles champion with no matching attributes"
test "handles champion at threshold boundary"
end
# test/jobs/cp/community_creation_job_test.rb
class Cp::CommunityCreationJobTest < ActiveJob::TestCase
test "enqueues job on champion verification"
test "job creates communities via detection service"
test "job handles missing champion gracefully"
test "job is idempotent (safe to run multiple times)"
end
# test/models/cp/community_notification_test.rb
class Cp::CommunityNotificationTest < ActiveSupport::TestCase
test "validates champion presence"
test "validates community presence"
test "validates uniqueness of champion + community + type"
test "scope unread returns only unread"
test "mark_as_read! updates read and read_at"
end
# test/controllers/cp/community_notifications_controller_test.rb
class Cp::CommunityNotificationsControllerTest < ActionDispatch::IntegrationTest
test "dismiss marks notifications as read"
test "dismiss requires authenticated champion"
test "dismiss only affects current champion's notifications"
end
After completing Phase 1.14:
app/controllers/champions/roadmap_controller.rb statusdocs/features/champion_portal/| Question | Answer | Date |
|---|---|---|
| Notification method? | In-app only for MVP (Dashboard prompt) | Jan 7, 2026 |
| Email notifications? | Deferred to Backlog | Jan 7, 2026 |
| When to run detection? | Background job after Champion assignment | Jan 7, 2026 |
| Who gets notified? | All members of newly created community | Jan 7, 2026 |
| Admin approval for new communities? | No — trust the threshold | Jan 7, 2026 |
db/migrate/XXXXXX_create_cp_community_notifications.rbapp/models/cp/community_notification.rbapp/services/cp/community_detection_service.rbapp/jobs/cp/community_creation_job.rbapp/controllers/cp/community_notifications_controller.rbapp/views/cp/dashboard/_community_notifications.html.erbtest/models/cp/community_notification_test.rbtest/services/cp/community_detection_service_test.rbtest/jobs/cp/community_creation_job_test.rbtest/controllers/cp/community_notifications_controller_test.rbtest/fixtures/cp/community_notifications.ymlconfig/routes.rb — Add notification dismiss routeapp/controllers/cp/dashboard_controller.rb — Load unread notificationsapp/views/cp/dashboard/show.html.erb — Include notification partialapp/services/cp/champion_verification_service.rb — Trigger creation jobChampion Verified
│
▼
┌──────────────────────────────┐
│ CommunityAssignmentService │
│ .assign_all(champion) │
│ │
│ Assigns to EXISTING │
│ communities only │
└──────────────────────────────┘
│
▼
┌──────────────────────────────┐
│ CommunityCreationJob │
│ .perform_later(champion_id) │
│ │
│ Queued for background │
└──────────────────────────────┘
│
▼ (async)
┌──────────────────────────────┐
│ CommunityDetectionService │
│ .detect_and_create │
│ │
│ For each community type: │
│ 1. Count qualifying Champions│
│ 2. Check if community exists │
│ 3. If count >= 3 && !exists: │
│ └─► Create community │
│ └─► Assign all members │
│ └─► Create notifications │
└──────────────────────────────┘
│
▼
┌──────────────────────────────┐
│ Champion's Next Dashboard │
│ Load │
│ │
│ Shows notification: │
│ "🎉 New Community Alert!" │
└──────────────────────────────┘