Skip to content

Financial Aid: teach the big programs, and flag every school's own aid guarantee - #58

Merged
DanialBeg merged 3 commits into
mainfrom
feature/financial-aid-overview-refresh
Oct 2, 2026
Merged

DanialBeg merged 3 commits into
mainfrom
feature/financial-aid-overview-refresh

Conversation

@ZubairQazi

@ZubairQazi ZubairQazi commented Oct 1, 2026 •

Copy link
Copy Markdown
Collaborator

Apply the migrations before deploying

Two migrations must be on the remote project before this deploys:

Financial Aid's college queries select the new aid_guarantee* columns, and since #57 the
shared college search columns include the fee columns too. Deploying first makes those
queries fail. In Financial Aid, Overview shows "Couldn't check your schools", Deadlines and
Aid Compare show their failed state, and aid college search returns nothing. This is the
same failure #42 found with student_body.

It also reaches the dashboard, not just Financial Aid: ApplicationsContext wraps the
whole dashboard and copies each saved school's guarantee on every load. That path degrades
rather than breaks, but expect a failing colleges query from there too. If you are
debugging one, don't only look in the Financial Aid module.

supabase db push          # applies both migrations
supabase migration list   # verify local ⇄ remote are in sync

Until both are applied, a local dev server pointed at the remote project shows these same
failure states. That is expected.

What changed

Financial Aid Overview

A Know your aid block above the checklist: four collapsible rows in the checklist's own
shape, closed by default and color-coded by where the money comes from.

Row Color What's in it
The big programs blue Pell (considered via the FAFSA), QuestBridge, ROTC (per-branch links), GI Bill transfer
Your schools' own aid applications amber, "Don't skip" Guarantees at schools on the student's list, then every school with one
State grants green Cal Grant, NY TAP (with the Cornell contract-college rate), TEXAS Grant, Bright Futures, HOPE/Zell Miller. The student's state, from their ZIP, sorts first
Five kinds of scholarships purple Merit, need, institutional, national, specialized, each as its own colored chip

Each row ends with an Edvifi can… strip (logo, one line, a button) that opens the tool
that does the job: FAFSA prep, Aid Compare, Deadlines, Aid Engine.

Guarantees everywhere a school appears

  • Deadlines: a badge on every school with a guarantee, residency included ("Go Blue
    Guarantee · Michigan residents"). Opening any four-year school shows its guarantee and
    what to file, or a reminder to check its own aid application.
  • Application Tracking: every four-year school, and any other school with a guarantee (Ohio State's regional campuses, Emory Oxford), gets "Submit the school's financial aid
    application", named after its guarantee. Public schools whose guarantee needs the CSS
    Profile (Michigan, UNC, UVA, Georgia Tech) also get the CSS task, carrying over the tick
    if the profile is already filed for another school. Lists saved before this change gain
    both tasks while the application is still in progress; submitted or decided ones are
    left alone.

Rename

Scholarships becomes My Scholarships (tab, heading, guided tour, checklist copy).

Collapsible Overview rail in every module

The deadline rail beside a module's Overview (Financial Aid, Application Tracking, Essays) now
collapses like the dashboard's: same column, divider and edge toggle (one shared component).
It shares the dashboard's open/closed state. On phones it stacks under the checklist and
always shows.

Scrollbars

Thin, quiet scrollbars app-wide instead of the platform default. The page-level scroll areas
next to the rail show their thumb only on hover.

The guarantee data

The colleges table is the only source: Deadlines, the Overview and the full list read
it directly, and each saved school keeps a short copy (ApplicationEntry.aidGuarantee) so
tasks can be built without a lookup. That copy is refreshed once per school per session, so
a later correction reaches lists saved before it. To change a guarantee, write a new
migration.

No public dataset carries these (not the Scorecard, not the Common Data Set). Each of the 40
was read off the school's own site, re-verified page by page, and stamped 2026-09-30. CSS
Profile requirements come from College Board's 2027–28 participating-institutions list.
Where a threshold changes for students entering in fall 2027 (UChicago, Vanderbilt, Rice,
W&L), the row carries the fall 2027 figure, because that is this year's seniors.
Northwestern's $150K figure is a fundraising goal, not a promise, so it is listed by its
current commitment (pay nothing under $70K).

The migration adds aid_guarantee, _residents, _needs_css, _detail, _action,
_url and _checked_on, with a CHECK that the four text fields are set together. It
backfills 54 rows keyed on scorecard_id. Campuses a program covers are in (all six Ohio
State campuses, Emory Oxford). Graduate and online rows that share a parent's domain are
out (Teachers College, Weill Cornell, TAMU Health Science Center, UF Online). It was applied
twice against a local Postgres to confirm it is clean and re-runnable, and the CHECK refuses
a partial update.

Reviewed

Three code-review passes. Fixes that came out of them include:

  • a phone-layout CSS ordering bug
  • the collapsed toggle overflowing
  • residents-only programs shown as if they applied to everyone
  • 4-digit ZIPs mapping to the wrong state
  • an article link that silently changed checklist progress
  • stale rows on reopening the module
  • task counts that disagreed between the calendar and the school page
  • a single source for the guarantee label and the rail toggle

tsc, eslint, 543 tests and the production build all pass.

Not in scope

  • The Overview copy still says "Junior year…" regardless of the student's year. That was
    there before this change.
  • Mount Holyoke, Brandeis, UMass and Wayne State guarantees are not yet verified.

The Financial Aid Overview gains a "Know your aid" block above the
checklist: the big programs (Pell, QuestBridge, ROTC, the GI Bill), state
grants with the student's own state first, the school's own aid
application, and the five kinds of scholarships. Each row is colour-coded
by where the money comes from and ends with what Edvifi can do about it.

More and more colleges promise free tuition below a family income, but
only to students who finish that school's aid steps on time. Forty of
them, checked against each school's own site, now live on the colleges
table, the only source. Deadlines badges them, residency included, and
Application Tracking turns each into a task, adding the CSS Profile to
public schools whose guarantee runs through it. The migration has to be
applied before this deploys: the aid queries select its new columns.

Scholarships is renamed My Scholarships.

The deadline rail beside each module's Overview now collapses like the
dashboard's, sharing its state and its toggle, and scrollbars are thin
and quiet instead of the platform default.
@vercel

vercel Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
webapp Ready Ready Preview Oct 2, 2026 3:46am UTC

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Deploying timeline-prototype with  Cloudflare Pages  Cloudflare Pages

Latest commit: 3a5215a
Status: ✅  Deploy successful!
Preview URL: https://667e25e2.timeline-prototype.pages.dev
Branch Preview URL: https://feature-financial-aid-overvi.timeline-prototype.pages.dev

View logs

@DanialBeg

Copy link
Copy Markdown
Member

Reviewed on the branch — typecheck clean, 529 passing there. The migration is the most carefully sourced thing in this repo: every row with its own URL and a checked date, and the satellite-campus reasoning written down rather than left to be rediscovered.

Verified

I checked all 54 scorecard_ids against the live table, since a wrong key in a hand-written backfill silently attaches Harvard's guarantee to someone else. All 54 resolve, and every one maps to the school its label names. The five non-four-year rows are exactly the campuses your comment says you meant to include.

Also worth calling out: the context effect writes from dataRef.current rather than the closed-over data. That is the exact trap from #48, avoided deliberately.

One finding

Six schools get the guarantee but not the task that wins it.

aid-app is pushed inside if (fourYear), so a two-year campus never gets it:

2yr has aid-app: false | 4yr has aid-app: true
2yr tasks: account,fee,transcript,submit,portal,confirm

The affected rows are the ones you went out of your way to include — the five Ohio State regional campuses (204796, 204662, 204671, 204699, 204705) and Emory's Oxford College — all carrying Buckeye Opportunity / Emory guarantees. The migration's own header says it:

Every one of them depends on the student finishing that school's aid steps on time, so the app shows the guarantee next to the school and turns it into a task

The badge shows; the task does not. Reads as if (fourYear || app.aidGuarantee), which also future-proofs it against the next two-year school that announces one. None of the six has needsCss: true, so the CSS branch is unaffected.

Minor

JSON.stringify(copy) === JSON.stringify(a.aidGuarantee) is key-order dependent. It is correct today because both sides come from the same expression, but reordering that object literal later would make every entry compare unequal and rewrite the whole list on every load. A field compare would not care.

The conflict

My side is as clean as I can make it now: the isActive guard you asked for is on main (#59), so you are merging your two additions — the aid-task label refresh and the WeakMap cache — rather than reconciling two versions of the same idea. I left both alone on purpose.

One thing to expect on rebase: #57 put applicationFeeCents on ApplicationEntry and the waiver task is gated on it rather than school type, so defaultTasksFor now has a chargesAFee(app) branch your aid-app push lands beside. Your withSharedTasks also covers a gap I left open and flagged on #59 — a reopened school asking again for a waiver already granted.

Happy to take the rebase off your hands if you would rather not, since three of the four conflicting commits are mine.

@DanialBeg DanialBeg left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two things pinned to lines. The first is the one I would not ship without.

Comment thread web/src/data/applicationTasks.ts Outdated
if (isPrivate(app)) tasks.push(t('css', 'Complete the CSS Profile', 'submit'))
// Public schools whose guarantee runs through the CSS Profile need it too.
if (isPrivate(app) || app.aidGuarantee?.needsCss) tasks.push(t('css', 'Complete the CSS Profile', 'submit'))
tasks.push(t('aid-app', aidAppLabel(app), 'submit'))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Six schools get a guarantee here but never this task.

Inside if (fourYear), so a two-year campus never reaches it:

2yr has aid-app: false | 4yr has aid-app: true
2yr tasks: account,fee,transcript,submit,portal,confirm

The affected rows are the ones the migration deliberately includes — the five Ohio State regional campuses (204796, 204662, 204671, 204699, 204705) and Emory's Oxford College — each carrying a Buckeye Opportunity or Emory guarantee. The migration header is explicit that this is the point:

Every one of them depends on the student finishing that school's aid steps on time, so the app shows the guarantee next to the school and turns it into a task

The badge shows; the task does not. if (fourYear || app.aidGuarantee) covers it, and covers the next two-year school to announce one. None of the six has needsCss: true, so the CSS branch above is unaffected.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, fixed in 3a5215a: the task now goes to fourYear || app.aidGuarantee, with a test for a two-year campus that has a guarantee.

.map((a) => a.collegeId)
if (due.length === 0) return
for (const id of due) { guaranteeChecked.current.add(id); guaranteeInFlight.current.add(id) }
fetchAidColleges(due)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth adding to the deploy warning at the top of the PR: this runs for every student on every dashboard load, not only inside Financial Aid. ApplicationsContext wraps the whole dashboard, and fetchAidColleges selects the aid_guarantee* columns.

The .catch means it degrades rather than breaks — guarantees never get copied, the aid task reads generically, nothing throws. So the blast radius of a pre-migration deploy is wider than the description says but not worse in kind. Mentioning it because "Financial Aid breaks" sets the expectation that the dashboard is fine, and someone debugging a failing colleges query from the dashboard would be looking in the wrong module.

The retry is sound, incidentally: a failure clears the id from guaranteeChecked, and the effect re-runs on the next change to the list rather than spinning.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. Added to the deploy warning in the description, along with #57's migration, since the shared search columns now need it too.

if (!found.has(a.collegeId) && !none.has(a.collegeId)) return a
const g = found.get(a.collegeId)
const copy = g ? { headline: g.headline, ...(g.residents ? { residents: g.residents } : {}), needsCss: g.needsCss } : null
if (JSON.stringify(copy) === JSON.stringify(a.aidGuarantee)) return a

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor, and only a trap for later: this is key-order dependent. It is correct today because both sides are built by the expression two lines up, so the keys always come out in the same order — including the conditional residents, which is absent on both sides or present on both.

Reorder that literal in a future edit, though, and every entry compares unequal on every load, so the whole college list gets rewritten each time the dashboard opens. Comparing the three fields directly would not care.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 3a5215a: it now compares headline, residents and needsCss directly.

Merges #56, #57 and #59. tasksForEntry keeps main's version (any missing
default task, in its own phase, only while the application is still being
worked on) and adds the aid task's label refresh and the per-entry cache.

From review on #58: the aid-application task now goes to any school with
a guarantee, not just four-year ones, so Ohio State's regional campuses and
Emory's Oxford College get the task that wins theirs. The guarantee copy is
compared field by field rather than as JSON, which depended on key order.
@ZubairQazi

Copy link
Copy Markdown
Collaborator Author

Thanks for the review. Merged main (#56, #57, #59) into the branch: tasksForEntry keeps your version and adds the label refresh and cache. All three review points are addressed in 3a5215a. Tests: 543 passing.

@ZubairQazi
ZubairQazi requested a review from DanialBeg October 2, 2026 03:59
@DanialBeg DanialBeg closed this Oct 2, 2026
@DanialBeg
DanialBeg deleted the feature/financial-aid-overview-refresh branch October 2, 2026 06:51
@DanialBeg
DanialBeg restored the feature/financial-aid-overview-refresh branch October 2, 2026 06:52
@DanialBeg DanialBeg reopened this Oct 2, 2026
@ZubairQazi
ZubairQazi marked this pull request as ready for review October 2, 2026 11:06

@DanialBeg DanialBeg left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving. All three findings addressed, and the merge is the combined version you proposed.

Verified since:

  • if (fourYear || app.aidGuarantee) — the six guarantee campuses get the task now
  • sameGuarantee() replaces the JSON.stringify compare
  • the deploy warning covers the dashboard path, and names the 20261001 fee migration as a second prerequisite — which I had missed on my own PR
  • computeTasks lands where you said it should: any missing default spliced into its own phase, inside the isActive guard, with the label refresh and the cache kept
  • 543 passing, typecheck clean, and b5ecf13 is in its history so it is merging against current main

I also read AidGuideSections.tsx, which I had skipped. Nothing to flag: every external link carries rel="noopener noreferrer", the SVGs are aria-hidden, the collapsibles have aria-expanded, and the guarantee fetch is lazy with a cancellation guard and a real 'failed' state that retries on reopen rather than an empty list pretending to be an answer.

The migration is already applied. I pushed 20260930000000_college_aid_guarantees to the remote yesterday so the ordering your description asks for is satisfied before the merge rather than after. 54 schools carry a guarantee; spot-checked Harvard, Michigan, Berkeley and Ohio State against their rows. It needed --include-all, since 20260930 sorts before the already-applied 20261001; I checked the two are disjoint first — zero references to the fee columns — so applying out of order lands the same state.

So nothing stands between this and merging.

One apology. You will have seen this PR close and reopen yesterday with no explanation: I tried to merge it without noticing it was a draft, and the branch deletion I had chained with ; ran anyway when the merge was refused, which closed it. Restored at the same SHA within a minute and nothing was lost, but that was careless with someone else's branch and I am sorry for the noise.

@DanialBeg
DanialBeg merged commit f36ba8f into main Oct 2, 2026
7 checks passed
@DanialBeg
DanialBeg deleted the feature/financial-aid-overview-refresh branch October 2, 2026 22:04

This branch was successfully deployed

1 active deployment
Preview — 3a5215a1 Deployed Oct 2, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants