alumni_lookup

Phase 25 — Signup Data in the Lookup Portal

Status: Complete — shipped in v1.0.80 (July 29, 2026) Effort Class: Medium Prerequisites: Phase 19 (v2 signup flow), Phase 22 (CRM feedback loop), Phase 24 (employments) Surface: Lookup Portal core (alumni controller/views/helpers, AlumniFilterService, Csv::AlumniExporter) + shared models, reading from the signup ecosystem. No writes to Cp:: or app/controllers/champions/.


The problem

The v2 signup flow (Phase 19) has been collecting location, interest, affinity and return-visit data for months. None of it reaches the people who need it. Staff open alumni#show and see a v1-shaped “Alumni Champion Status” card that talks about roles and a five-step wizard that no longer exists; they search for alumni in a district and get CRM addresses only, missing every alum who told us they moved; they export a CSV that has one champion column and no idea what the alum actually said.

Three concrete gaps:

  1. The profile doesn’t show what v2 collected. Location, interest leaning, self-reported affinities, and whether the alum ever came back are all invisible.
  2. Search can’t see signup data. filter_by_district matches LEFT(alumni.zip, 5); filter_by_affinities matches alumni_affinities only.
  3. The “Alumni Champion Filter” describes a dead state machine. Six options built around v1’s five-step wizard, four of which v2 can no longer produce.

The fact that reframes #3

In v2, handle_who_you_are requires zip_code, and ChampionSignup#calculate_status returns 5 (:zip_code, labeled “Completed”) whenever a ZIP is present. Every v2 signup is complete the moment it is created. champion_signups.completed already means exactly “filled out the v2 basic contact info” — the scope needed no change. What needed changing was the six-option dropdown built on top of it, where “Show Signups In Progress” can now only ever surface a v1 legacy straggler.

The fact that shapes #1 and #2

ChampionSignupCrmLogger only reads alumni_affinities to diff against; nothing writes signup affinities into it. Signup data reaches the CRM by being logged to crm_data_changes, exported, and keyed in by Advancement Services — after which it flows back into alumni on the next BruinQuest sync. So a signup value is pending truth, and the alumni columns catch up to it later. That is the argument for never overwriting alumni.zip and instead resolving an effective value at read time.


Decisions (planning interview, July 29 2026)

Question Decision
Location precedence Effective location with precedence. Newest active signup’s ZIP wins over alumni.zip for display and district search. alumni.zip is never written.
CRM catch-up Auto-converge on match. When both ZIPs resolve to the same 5 digits, one location renders with no provenance badge. Only divergence shows the dual display.
Affinity search Included by default, badged. filter_by_affinities matches alumni_affinities OR signup affinity_codes. Profile shows one equal-weight list with a source line; search chips use one border and differ by fill.
Interest leaning Replaces Champion Role, keeps the seal art. top_interest_area drives the existing seal/label (area keys already match role keys), plus a 4-bar spectrum on the profile.
Filter dropdown 3 options, with prospect_status split out to its own checkbox. Rendered as a radio inside a grouped filter block (post-review revision).
Canonical signup Most recent by created_at among active signups. One signup drives everything.
Profile history Version + signup date; Returned / Added info stamps.
CSV additions Effective location block; interest leaning; self-reported affinities.

Fill-in decisions made during implementation

  1. No leaning → no seal. interest_spectrum returns {} when there are no quiz answers, so a v2 signup that skipped the quiz has no leaning. Fallback order: spectrum → explicitly selected final_role → render nothing.
  2. location_source in the CSV reads signup only when the signup ZIP is driving and differs from alumni.zip; otherwise crm. Consistent with auto-converge.
  3. List views use stored interest_scores only. Recomputing a v1 quiz through ChampionQuizService 50× per search page is not worth the leaning chip. The profile page, which handles one alum, uses the full computed spectrum.

Sub-phases

25.1 — Signup resolver (model/service layer)

25.2 — alumni#show

25.4 — CSV export

25.5 — Filter cleanup


Explicitly deferred (→ BACKLOG)

Item Why
Duplicate-signup warning on the profile Not selected in planning; canonical-by-created_at makes acting on stale data unlikely
signup_version / signup_date / last_return_visit / last_enriched CSV columns Not selected; they land on the profile page only
Consent + held-CRM-update count on the profile Not selected; already visible in Signup Admin
Free-text city/ZIP search (as opposed to district) Out of scope — the location search surface is the district autocomplete

Post-review revisions (July 29, 2026)

Four changes after the first walkthrough:

  1. Filters grouped; the signup filter is two checkboxes. Five checkboxes and a select in one flat row read as one undifferentiated pile and let the two surfaces blur. Now three labeled fieldsets — Alumni Champions, Alumni Network, Alumni Record — with “Completed Form” and “Manually flagged prospect” under the first.

    “Has not completed the form” is not offered. It was briefly built as a three-state radio on the reasoning that “who in this district have we NOT reached” is the outreach question the filter exists for. It is not a question this project has reached yet, and the option returns ~60,000 rows — the wrong thing to put one mis-click away. The no_form value stays in filter_by_alumni_status so ALUMNI_FILTER_ALIASES has a target and legacy bookmarks resolve; a view test asserts the form never renders it.

  2. Icon, not seal, on alumni#show. Matches the public landing page and the spectrum bars, and stops double-framing a badge that already has its label in the artwork.
  3. One affinity list. Two sections with different treatments made a self-report look like a lesser kind of fact. Now equal weight, differentiated by a Source: line. In the search-result chips, provenance moved from the border to the fill — every chip gets the same solid border, tinted for BruinQuest and white for self-reported. Dashed borders read as broken rather than provisional, which is obvious once six of them sit in one cell.
  4. “Affinaquest” and “Salesforce” removed from every staff-visible surface. The CRM is BruinQuest. The URL moved to /settings/bruinquest with a 301 from the old path. Ruby identifiers, tables, columns and the affinaquest_import change-source value keep the old spelling — renaming those is a migration with no visible benefit, and a half-renamed identifier is worse than a consistent one.

Not changed, deliberately: app/views/cp/policies/privacy.html.erb lists “Heroku (Salesforce)” in its sub-processor table. That is Heroku’s corporate parent in a legal disclosure, not CRM branding — and app/views/cp/** is a held surface.

Spec deviations

None. Three things outside the original scope were fixed on the way through because the phase touched the same code and would otherwise have shipped on top of them:

  1. The search form’s affinity <option> values were Affinity#id while filter_by_affinities matched affinity_code. Submitting without changing the category filtered a number against a column of codes and returned nothing. Masked because the affinity-selector Stimulus controller replaces the option list with real codes as soon as a category is picked — the normal path through the form.
  2. shared/_engagement_breakdown interpolated a role color into class names (bg-#{color}/10, text-<%= color %>), which Tailwind’s JIT never compiles, so that card had been rendering unstyled since it was written. Rebuilt on LEAN_SURFACE_CLASSES.
  3. _search_tags had three dead alumni_filter cases (champions, prospectives, all_champions) that the form has never emitted, so the tag never rendered.

Test coverage

Layer File Count
Resolver test/services/alumni/signup_profile_test.rb 23
Scopes + filter test/models/alumni_test.rb (Phase 25 sections) 17
Profile, search, filters test/controllers/alumni_controller_test.rb (Phase 25 section) 14
Export test/services/csv/alumni_exporter_test.rb (Phase 25.4 section) 7

Fixtures added: alumni(:relocated_alumni) (CRM Nashville / signup Madison), champion_signups(:relocated_v2), alumni_affinities(:relocated_sga), zip_codes(:zip_37212). champion_signups(:completed_linked).answers was changed from placeholder words to real quiz letters (a/a/c) — ChampionQuizService.normalize_answers discards anything that is not a valid letter, so the old fixture could never produce a spectrum and silently modeled a v1 signup that had never taken the quiz.