alumni_lookup

Phase 30 — Unified CRM Imports

Status: ✅ Complete — September 24, 2026 (30.1, 30.2, 30.3). Model handoff: the user chose to continue implementation on the planning model (“proceed”), September 24, 2026. Surface: Alumni Lookup (staff settings). Nothing touches cp/ or alumni_network/.

Completion Summary

Contacts, Educations, Areas of Study and Employment now run one flow at /settings/imports/:type: upload → scan → review → import → results on one status-driven batch page, with one poller, one controller (Settings::ImportsController), one state machine (ImportBatch) and a registry for the per-type differences (Imports::Type). Every type has a Commit confirm, a working Cancel, drag-and-drop upload, streamed manifests and heartbeat stale detection. The legacy Alumni CSV, Degrees CSV, Banner and /people importers are deleted.

Bugs fixed on the way: the scan page that needed a manual refresh (batches created pending; Contacts’ inline-script pollers under Turbo), a finished import redirecting staff who had left the page, Contacts results mixing batch #12 with #123, and the phone rake task and contact import undoing each other.

Deferred (BACKLOG → Unified CRM Imports): Giving/engagement imports onto the framework; the orphan report reading the legacy degrees table; merging the batch tables (won’t build).

Deploys: three pushes to staging (969f573a, 5ceaa6a7, 79a0b7a8); no migrations and no post-deploy rake tasks, so no launch guide. Suite at wrap: 6071 runs, 0 failures. Details per sub-phase in §9.

1. The Question This Phase Answers

Four imports do the same job: upload a CRM export, scan it in the background, review what will change, apply it in the background, and report the results. They are Contacts (BruinQuest), Educations, Areas of Study and Employment. They share a job pipeline but not a UI, so each one behaves slightly differently, and two of those differences are bugs:

The rest are plain inconsistencies (§2). The phase makes all four imports one flow with one UI, moves them under /settings/imports/:type, and removes the legacy importers that the CRM imports replaced.

2. Current State

  Contacts Educations / Areas of Study Employment
Pages 4: scanning → preview → processing → results 1 status-driven page 1 status-driven page
Polling Inline scripts (two different loops) import-progress Stimulus controller same
Batch created as scanning ✅ pending ❌ pending ❌
Scan progress None (spinner, “worker may not be running” after 2 min) Live bar Live bar
Commit confirm None None None, even though commit deletes rows
Cancel Scan + apply Route exists, no UI links to it; apply never checks for a cancel same
Stale auto-fail Apply only (v1.0.92) Heartbeat, 10 min Heartbeat, 10 min
Manifest ManifestStore, one JSON document StreamingManifest (v1.0.92) StreamingManifest
Results Separate page with conflicts and CRM changes Inline Inline
History “View All” page Recent-batches partial Its own recent-batches partial
URL /settings/bruinquest/… /settings/alumni/… /settings/alumni/…

Contacts’ apply still parses its whole manifest (ManifestStore.fetch), which is the same out-of-memory risk that killed a 66K-row education import on staging in September 2026. It has succeeded so far only because its rows are smaller.

3. Decisions

Decision A — One page per batch, driven by status

Every import uses a single batch page that renders the section for its current state:

Scanning ──► Review ──► Importing ──► Results
   │            │           │
   └────────────┴───────────┴──► Failed / Cancelled

Contacts’ four pages collapse into this. Its preview content (ID conflicts, recency conflicts, field-change stats) becomes the Review section, and its results page (conflicts and CRM changes from the batch) becomes the Results section.

Decision B — /settings/imports/:type

Route Purpose
GET /settings/imports Hub: one card per type, with its last run and status
GET /settings/imports/:type Upload form + that type’s batch history
POST /settings/imports/:type Upload → create batch as scanning → enqueue scan → redirect
GET /settings/imports/:type/batches/:id The single batch page
GET /settings/imports/:type/batches/:id/status JSON for the poller
POST /settings/imports/:type/batches/:id/commit Flip to running, enqueue apply
POST /settings/imports/:type/batches/:id/cancel Cancel during scan, review or apply
GET /settings/imports/:type/batches/:id/issues.csv Gap/issue CSV where the type has one

:type is one of contacts, educations, areas-of-study or employment. Batch ids are per table, and Educations and Areas of Study share education_import_batches, so a batch requested under the wrong type redirects to the right one rather than 404ing.

Old URLs redirect permanently (301) to their new equivalents, including batch pages. Before removing an old route, grep for persisted paths (StaffNotification#url, ActionItemsService). The debug skill has this happening twice on the 23.x renames.

Decision C — BruinQuest’s Conflicts and CRM Changes pages stay at /settings/bruinquest

They are CRM data tools that outlive any single import, not steps of the import flow. The Contacts Results section links to them, filtered to the batch.

Decision D — One “Imports” entry in the Settings sidebar

It replaces the four separate links and opens the hub. All four upload pages share one set of type tabs.

Decision E — One implementation, with a per-type adapter

One controller (Settings::ImportsController) serves every type. The differences between types live in a registry that maps each type slug to:

Shared pieces:

The batch tables stay separate. Merging them is a migration with no user-facing benefit.

Decision F — Every import gets the same controls

Decision G — Legacy importers are deleted, not hidden

Flow Entry point Replaced by
Alumni CSV settings/alumni#upload_alumni, #import; Csv::AlumniImporter Contacts
Degrees CSV #upload_degrees, #import_degrees_preview/commit, #import_degrees Educations
Banner (JH-0026c) #upload_banner, #import_banner_preview/commit; Csv::BannerImporter Educations
People PeopleController, app/views/people/, resources :people Contacts

The routes, actions, views, services and tests are all deleted, with no redirects: nothing should keep calling these. Three things are kept:

4. Sub-Phases

# Sub-phase Ships
30.1 Shared framework + Educations, Areas of Study, Employment Registry, controller, routes and redirects, concern, partials, poller; the scan-refresh fix; commit confirm; cancel UI and cancellation in the apply jobs; hub and sidebar
30.2 Contacts onto the framework Single-page Contacts, inline scripts deleted, scan progress, StreamingManifest for Contacts, old /settings/bruinquest flow URLs redirected
30.3 Remove legacy importers Decision G

Each sub-phase deploys on its own. None has a migration.

5. Acceptance Tests

Framework (run for every type)

Contacts

Legacy removal

6. Activity Event Tracking

None. This is a staff-only admin tool. Each batch row is already its own audit record (imported_by, counts, timestamps, error).

7. Documentation Updates (required before wrap)

8. Out of Scope

9. What Was Implemented

30.1 — Shared framework ✅ (September 24, 2026)

Files

Spec deviations

Tests: imports_controller_test.rb (17 tests: framework behavior across every type, legacy redirects, retired helpers); the two old controller test files were ported to imports_educations_test.rb and imports_employment_test.rb; cancellation tests for ChunkedCommit, the apply job and the scan job. The regression test for the manual-refresh bug was sabotage-verified by reverting the upload to pending.

30.2 — Contacts onto the framework ✅ (September 24, 2026)

Files

Spec deviations

Bugs found while porting

Tests: imports_contacts_test.rb (10, including a full upload → scan → commit round trip on one URL, the review reading without fetch, and every old URL’s redirect). The framework tests in imports_controller_test.rb now run across all four types. Tests for deleted actions were removed from affinaquest_controller_test.rb; model and ManifestStore tests updated and extended.

30.3 — Remove legacy importers ✅ (September 24, 2026)

Deleted: Csv::AlumniImporter, Csv::BannerImporter, PeopleController and its views, nine Settings::AlumniController actions (index, the three upload pages, import, the three degree import actions and the two banner actions) and their six views, the routes, lib/tasks/phone_formatting.rake, and the importers’ test files.

Spec deviations (approved in the 30.3 interview, September 24, 2026)

Found, not fixed (BACKLOG): the orphan report reads the legacy degrees table.

Tests: legacy_importers_removed_test.rb (the endpoints 404, helpers and classes gone, the redirect, the sidebar), phone_number_test.rb, test/tasks/alumni_rake_test.rb.