⚠️ PLANNING DOCUMENT — This describes features that are NOT YET IMPLEMENTED.
Related Documents:
- DECISIONS.md — Architectural decision log
- AVOIDING-DUPLICATION.md — Data overlap handling framework
- ../README.md — Champion Portal technical overview
The Champion Portal is designed mobile-first. Desktop is secondary.
Design Implications:
Future Vision: The Champion Portal may eventually become a native mobile app, so all architecture decisions should consider:
The same PostgreSQL database serves both portals, but data has clear ownership:
| Domain | Owner | Description |
|---|---|---|
| Administrative Data | Lookup Portal | Data imported/entered by Staff and Admins |
| User-Generated Data | Champion Portal | Data entered/created by Champions |
┌─────────────────────────────────────────────────────────────────┐
│ SHARED DATABASE │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────────┐ ┌──────────────────────────┐ │
│ │ LOOKUP PORTAL │ │ CHAMPION PORTAL │ │
│ │ (Administrative) │ │ (User-Generated) │ │
│ ├──────────────────────┤ ├──────────────────────────┤ │
│ │ │ │ │ │
│ │ alumni │◄──────┤ cp_champions │ │
│ │ degrees │ (via │ cp_profiles │ │
│ │ alumni_affinities │ BUID) │ cp_affinities │ │
│ │ engagement_activities│ │ cp_contributions │ │
│ │ users (Staff/Admin) │ │ cp_messages │ │
│ │ │ │ cp_posts │ │
│ └──────────────────────┘ └──────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
Owned by the Lookup Portal — data managed by Staff/Admins:
| Table | Purpose |
|---|---|
alumni |
Master alumni records (imported from CRM) |
degrees |
Degree records (imported from Registrar) |
affinities |
Master list of affinity groups (reference data) |
alumni_affinities |
Alumni-to-affinity links (imported/staff-managed) |
engagement_activities |
Tracked engagement (imported from CRM/systems) |
engagement_types |
Types of engagement activities (reference data) |
colleges |
College reference data |
majors |
Major reference data |
users |
Lookup Portal users (Staff, Admins) |
champion_signups |
Interest form submissions (processed by Staff) |
Use the cp_ prefix to clearly indicate ownership:
| Table | Purpose |
|---|---|
cp_champions |
Champion user accounts (self-registered) |
cp_profiles |
Champion profile data (user-editable) |
cp_affinities |
Champion-claimed affinities (self-reported) |
cp_contributions |
“What I can help with” offers |
cp_messages |
Champion-to-champion messages |
cp_message_threads |
Message conversation threads |
cp_posts |
Discussion board posts |
cp_comments |
Comments on posts |
cp_follows |
Follow relationships between champions |
Tables that bridge both domains:
| Table | Purpose |
|---|---|
cp_verifications |
Links cp_champions to alumni (BUID match) |
crm_data_changes |
Unified changelog for CRM export (all portals) |
crm_data_export_batches |
Track CRM export batches |
zip_codes |
ZIP code reference data |
districts |
Metro area groupings |
regions |
Large geographic areas |
Note:
crm_data_changesreplaces the originalcp_profile_changesdesign. See UNIFIED_DATA_SYNC.md for full architecture.
Planned for a separate Event Check-in project:
| Table | Purpose |
|---|---|
contacts |
Any person who attends events (alumni, parents, guests) |
events |
Event reference data |
registrants |
Event attendance/check-in records |
| Direction | Flow Type | Example |
|---|---|---|
| Administrative → Champion | Read-Only | Champions VIEW degree data |
| Champion → Administrative | Via Staff Review | Champion updates → Staff syncs to alumni |
| Champion-Only | Isolated | Messages, posts never flow to Lookup |
Administrative → Champion (Read-Only):
Champion → Administrative (Via Staff Review):
alumni_affinitiesalumni recordalumni as the “source of truth” with optional Champion enrichmentChampion-Only (Isolated):
cp_messagescp_postsKey Principle: Champion Portal activity does NOT create engagement_activities records. Those remain Staff-managed data from CRM imports.
| Layer | Table | Who Manages | Purpose |
|---|---|---|---|
| Reference | affinities |
Staff | Master list of valid affinities |
| Imported | alumni_affinities |
Staff | CRM-imported alumni-affinity links |
| Self-Reported | cp_affinities |
Champions | User-claimed affinities |
Why Separate?
alumni_affinities = verified, imported data (trusted)cp_affinities = self-reported (may need verification)cp_affinities Table Schema:
create_table :cp_affinities do |t|
t.bigint :cp_champion_id, null: false
t.string :affinity_code, null: false
t.timestamps
end
add_index :cp_affinities, [:cp_champion_id, :affinity_code], unique: true
add_foreign_key :cp_affinities, :cp_champions
add_foreign_key :cp_affinities, :affinities, column: :affinity_code, primary_key: :affinity_code
Export for Advancement Services:
crm_data_changes table for CRM exportFuture Enhancement:
alumni_affinities to their profile (see BACKLOG.md)| Type | Table | Source |
|---|---|---|
| System-Tracked | engagement_activities |
CRM imports, email clicks, event attendance |
| Champion-Generated | (not tracked here) | Portal activity is NOT engagement |
Decision: Champion Portal activity (logging in, posting, messaging) does NOT create engagement_activities records.
If we ever want to track Champion Portal activity, it would go in a separate table like cp_activity_log with clear separation.
| Data | Location | Who Edits | Visible To |
|---|---|---|---|
pref_name, maiden_name |
alumni |
Staff OR Champion (if verified) | Both portals |
| Photo (ID) | alumni.photo |
Staff only | Lookup Portal only |
| Photo (profile) | cp_champions.photo |
Champion only | Champion Portal only |
| Contact (CRM) | alumni.email/phone/city/state |
Staff only | Lookup Portal only |
| Contact (portal) | cp_champions.* |
Champion only | Champion Portal |
| Bio | cp_champions.bio |
Champion only | Champion Portal |
| Prospect notes | alumni.prospect_notes |
Staff only | Lookup Portal only |
| Degrees | degrees |
Staff only (imports) | Both (read-only for Champion) |
alumni record when verifiedZIP code is the primary location input — minimal friction, maximum value.
ZIP Hierarchy:
ZIP (37027)
→ City (Brentwood)
→ District (Nashville-Davidson--Murfreesboro--Franklin, TN)
→ Region (Southeast)
| Table | Purpose |
|---|---|
zip_codes |
Reference: ZIP → city, state |
districts |
Metro area groupings |
regions |
Large geographic areas |
cp_champions.zip_code |
Required |
cp_champions.city/state/street_address |
Optional, auto-populated or manual |
🔗 Full Documentation: See UNIFIED_DATA_SYNC.md for complete architecture.
A unified system tracks ALL changes that need export to Advancement Services (BruinQuest/Salesforce CRM), regardless of source:
| Source | Description |
|---|---|
| Champion Portal | Champions edit their profile (name, email, phone, address) |
| Lookup Portal (Manual) | Staff manually correct alumni records |
| Affinaquest Import | Our data is newer than the import file (recency conflicts) |
| Term | Definition |
|---|---|
| BUID | Belmont University ID (internal primary key) |
| BQID | BruinQuest ID (alumni.contact_id, format: C-000000000) |
Note: Exports include BOTH identifiers for data team compatibility.
crm_data_changesThis replaces the original cp_profile_changes design with a unified table:
create_table :crm_data_changes do |t|
# Identity - WHO is affected
t.string :buid
t.string :contact_id
t.bigint :alumni_id
t.bigint :cp_champion_id
# What changed
t.string :source_table, null: false # "alumni", "cp_champions"
t.string :field_name, null: false # e.g., "phone", "city", "pref_name"
t.text :old_value
t.text :new_value
# Context
t.string :change_source, null: false # "champion_portal", "lookup_manual", "affinaquest_conflict"
t.string :changed_by_type # "Cp::Champion", "User", "system"
t.bigint :changed_by_id
t.text :notes
# Export tracking
t.string :export_status, default: 'pending' # pending, exported, dismissed
t.datetime :exported_at
t.bigint :exported_by_id
t.bigint :export_batch_id
t.timestamps
end
add_index :crm_data_changes, :buid
add_index :crm_data_changes, :contact_id
add_index :crm_data_changes, :export_status
add_index :crm_data_changes, :change_source
add_index :crm_data_changes, :created_at
| Field | Table | Export Priority |
|---|---|---|
pref_name, maiden_name |
cp_champions |
High |
email, phone |
cp_champions |
High |
city, state, zip_code, street_address |
cp_champions |
High |
employer, job_title, industry |
cp_champions |
Medium |
bio |
cp_champions |
Low |
photo |
cp_champions |
Low (track changed_at only) |
When a Champion edits their profile:
# app/models/concerns/crm_tracked.rb
module CrmTracked
extend ActiveSupport::Concern
included do
after_update :log_crm_changes
end
def log_crm_changes
tracked_fields.each do |field|
if saved_change_to_attribute?(field)
CrmDataChange.create!(
buid: linked_alumni&.buid,
contact_id: linked_alumni&.contact_id,
alumni_id: linked_alumni&.id,
cp_champion_id: id,
source_table: 'cp_champions',
field_name: field.to_s,
old_value: attribute_before_last_save(field),
new_value: send(field),
change_source: 'champion_portal',
changed_by_type: 'Cp::Champion',
changed_by_id: id
)
end
end
end
end
class CrmDataExportService
def pending_changes
CrmDataChange.pending.recent
end
def changes_since(date)
CrmDataChange
.where('created_at >= ?', date)
.order(:created_at)
end
def export_batch(change_ids, user:)
CrmDataExportBatch.create_export!(
user: user,
change_ids: change_ids
)
end
def to_csv(changes)
# Unified format for BruinQuest/Salesforce import
# Columns: BQID, BUID, Field, Old Value, New Value, Source, Changed At
end
end
When converting to a native app:
cp_ tables may need separate indexes optimized for Champion queries| Document | Purpose |
|---|---|
| DECISIONS.md | Architectural decision log |
| AVOIDING-DUPLICATION.md | Data overlap handling |
| UNIFIED_DATA_SYNC.md | Complete CRM sync architecture |
| ../README.md | Champion Portal overview |
| ../../development/MODEL_RELATIONSHIPS.md | Existing model associations |