This feature imports contact information from Affinaquest (CRM) CSV exports to keep alumni data synchronized for search and matching purposes.
The import process:
Alumni Model (new fields)
├── email_school # School email address
├── email_personal # Personal email address
├── email_business # Business email address
├── email_other # Other email address
├── zip # ZIP code for district lookup
├── affinaquest_updated_at # Last modified in Affinaquest
└── affinaquest_synced_at # Last sync timestamp
| CSV Column | Affinaquest Field | Alumni Field | Purpose |
|---|---|---|---|
email |
Email Address | email |
Primary email (already existed) |
email_school |
School Email | email_school |
.edu addresses |
email_personal |
Personal Email | email_personal |
Gmail, Yahoo, etc. |
email_business |
Business Email | email_business |
Work addresses |
email_other |
Alternate Email | email_other |
Catch-all |
Recency conflicts are logged to the unified crm_data_changes table for eventual export back to the CRM. This infrastructure is shared with the Champion Portal for self-service updates.
See: Unified CRM Sync Architecture
Navigate to Settings → Affinaquest Import (/settings/affinaquest)
The CSV must include these columns:
buid (required) - Belmont University ID (e.g., B00421123)contact_id - Affinaquest Contact ID (e.g., C-000220688)affinaquest_updated_at - Last modified timestamp in AffinaquestOptional contact fields:
email, email_personal, email_business, email_school, email_otherphonecity, state, zipbirthdatepref_name, maiden_nameClick Preview to see:
Click Commit to process the import. Large files (5000+ rows) run as background jobs.
After import, review:
For each field, the import compares timestamps:
alumni.updated_at for general fields, or field-specific timestamps if availableaffinaquest_updated_at from the CSVIf local data is newer, the import keeps local data and logs a CrmDataChange record for potential export back to the CRM.
CSV Upload → Preview → Commit → Background Job (if large)
↓
Process each row:
1. Find alumni by BUID
2. Check Contact ID match
3. Compare timestamps
4. Update if import is newer
5. Log conflicts if local is newer
| Table | Purpose |
|---|---|
alumni |
Main contact records |
affinaquest_import_batches |
Tracks each import batch |
affinaquest_import_conflicts |
ID mismatch conflicts |
crm_data_changes |
Field-level recency conflicts for export |
bin/rails alumni:check_duplicate_buids
If duplicates exist (shouldn’t happen with unique index):
bin/rails alumni:cleanup_duplicate_buids # Dry run
bin/rails alumni:cleanup_duplicate_buids CONFIRM=1 # Actually merge
If duplicate conflict records exist:
bin/rails alumni:cleanup_duplicate_crm_changes # Dry run
bin/rails alumni:cleanup_duplicate_crm_changes CONFIRM=1 # Actually delete
| File | Purpose |
|---|---|
app/services/csv/affinaquest_contact_importer.rb |
Main import logic |
app/jobs/affinaquest_import_job.rb |
Background job wrapper |
app/controllers/settings/affinaquest_controller.rb |
UI controller |
app/models/affinaquest_import_batch.rb |
Batch tracking |
app/models/affinaquest_import_conflict.rb |
ID conflicts |
app/models/crm_data_change.rb |
Recency conflict logging |
lib/tasks/cleanup_duplicate_buids.rake |
Maintenance tasks |
This indicates duplicate BUID records in the database. Run:
bin/rails alumni:check_duplicate_buids
If duplicates exist, merge them:
bin/rails alumni:cleanup_duplicate_buids CONFIRM=1
Background jobs require Sidekiq + Redis. Check:
redis-cli pingbundle exec sidekiqEnsure the CrmDataChange model matches the database schema. Check:
change_source (not source)export_status (not status)source_table is requiredFiles with 5000+ rows automatically use background jobs. Ensure Sidekiq is configured.
alumni.buid has a unique index - prevents duplicate BUID recordsalumni.contact_id has a unique index - prevents duplicate Contact IDscrm_data_changes deduplicates on (buid, field_name, change_source, export_status) for pending recordsThe imported contact data enhances search functionality:
The filter_by_any_email scope searches across all email fields:
email (primary)email_schoolemail_personalemail_businessemail_otherUsed in: Alumni search, batch search, event RSVP matching
ZIP codes enable district-based filtering:
GET /api/districts/autocomplete?query=...districts table via zip_codes lookupAlumni.zip → ZipCode.zip → ZipCode.district_id → District
→ ZipCode.region_id → Region
Methods available:
alumni.zip_district - Returns District recordalumni.zip_region - Returns Region recordcrm_data_changes back to Affinaquest