alumni_lookup

Sub-Phase 1.14: Dynamic Community Creation

Champion Portal Development Sub-Phase 1.14

Estimated Effort: 1–2 weeks
Focus: Background detection and creation of emergent communities

Prerequisites: Phase 1.12 (Community Foundation) and Phase 1.13 (Landing Pages) complete

Status:COMPLETE — January 2026

Related Documents:


Completion Summary

Completed: January 2026

What Was Implemented

  1. Cp::CommunityDetectionService — Detects when threshold (3 Champions) is met for any community type (district, college, major, affinity, industry)
    • Uses type-specific lookup columns (district_id, college_code, major_code, affinity_code, industry)
    • Automatic Champion assignment to newly created communities
    • Notification generation for all members
  2. Cp::CommunityCreationJob — Background job triggered after verification and profile edits
    • Filters to specified community types for targeted detection
    • Only processes verified Champions
  3. Cp::CommunityNotification model — Tracks notifications per Champion per Community
    • notification_type enum (currently only threshold_reached, room for future types)
    • Read/unread tracking with timestamps
    • Scopes for unread and recent notifications
  4. Cp::CommunityNotificationsController — Dismiss endpoint for notifications
    • Single dismiss and dismiss-all actions
    • Activity tracking on dismiss
  5. Dashboard Integration — Shows notification banner for new communities
    • Prominent card when communities are pending
    • Links to community landing pages
    • Dismiss functionality via Turbo
  6. Hybrid Triggers — Community detection runs at multiple trigger points:
    • ChampionVerificationService#perform — After verification (college/major)
    • ProfileWizardController#update — After location, profession, affinities steps
    • ProfileController#update — After location, profession, affinities section edits

Files Created/Modified

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

Tests (55 total, all passing)

Deferred Items (Added to BACKLOG.md)

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

Table of Contents

  1. Overview
  2. Why This Sub-Phase Exists
  3. Design Decisions
  4. Internal Sub-Phases
  5. Detection Logic
  6. Notification System
  7. Scope
  8. Definition of Success
  9. Tests to Create
  10. Documentation Updates
  11. Questions Resolved

1. Overview

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:

  1. Champion #1 in “Music Industry” joins → No community yet
  2. Champion #2 in “Music Industry” joins → Still no community (below threshold)
  3. Champion #3 in “Music Industry” joins → 🎉 Community created automatically!
  4. All three Champions get notified: “You’re part of a new community!”

After Phase 1.14:


2. Why This Sub-Phase Exists

Manual Community Creation Doesn’t Scale

Without dynamic creation, admins would need to:

With 5 community types × hundreds of possible values, this is unsustainable.

The “Pleasant Surprise” Experience

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.

From JOBS-TO-BE-DONE.md:

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.


3. Design Decisions

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

4. Internal Sub-Phases

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

5. Detection Logic

When to Check for New Communities

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

Detection Service

# 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

Background Job

# 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

6. Notification System

Database Schema

# 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

Notification Model

# 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

Dashboard Integration

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 %>

7. Scope

In Scope

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

Out of Scope (Deferred)

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

8. Definition of Success

Functional Acceptance Criteria

Edge Cases Handled

Performance Criteria


9. Tests to Create

Service Tests

# 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

Job Tests

# 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

Model Tests

# 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

Controller Tests

# 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

10. Documentation Updates

After completing Phase 1.14:


11. Questions Resolved

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

Files to Create

New Files

Files to Modify


Appendix: Full Community Creation Flow

Champion 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!"    │
└──────────────────────────────┘