Skip to content

Store what a school charges to apply, and whether it waives it - #57

Merged
DanialBeg merged 1 commit into
mainfrom
feat/application-fees
Oct 1, 2026
Merged

DanialBeg merged 1 commit into
mainfrom
feat/application-fees

Conversation

@DanialBeg

Copy link
Copy Markdown
Member

Follows #56 — based on that branch, not main, because the waiver task this gates was added there. Merge #56 first and this retargets cleanly.

The data was already in the file

The Common App grid the deadline ingest reads carries three columns beside the round dates — US fee, international fee, and whether a waiver is taken. We were parsing straight past them.

Schools with a fee listed 1,128
Charging $0 611 (54%)
Median / p90 / max $0 / $70 / $150
Waiver accepted 540
US only 90
Not accepted 29

A student looking at ten schools and bracing for several hundred dollars of fees is usually wrong, and has no way to find out short of opening ten admissions pages.

What's here

A migration adding application_fee_cents, fee_waiver_policy and fees_verified_at, with a check constraint on the three policy values the grid actually publishes — anything else is a parse bug, not a new policy, and should fail loudly.

Cents, matching every other money column. Null and 0 stay distinguishable: "we don't know" and "it costs nothing" are very different things to tell someone.

The parser reads both columns, per page, off the same calibrated header as the dates.

The school page says it plainly — "Free to apply", or "$85 · Fee waivers accepted". Where no figure is on file it says that, rather than guessing: an invented $0 is a promise, an invented fee is a reason not to apply.

The waiver task follows the real figure instead of the school's type. A known $0 removes it; a known fee adds it, including at the two-year schools that do charge. Only where nothing is on file does it fall back to type — which is most of the 6,273 colleges, since the grid covers Common App members and little else.

One rule that differs from deadlines

Fees refresh on every ingest; deadlines only ever fill a blank. Nobody curates fees by hand, the grid is their only source, and a school that drops its fee should stop being shown as charging one. Deadlines keep the opposite rule for the same reason in reverse — 46 of them are hand-checked.

A bug worth naming

"Not Accepted" contains "Accepted". Matching the short label first turned all 29 schools that refuse waivers into schools that take them — exactly backwards, and the kind of thing that reads fine in review. Longest label first now, with a test pinning it.

Applying it

  1. Run the migration (supabase db push, or paste it in the SQL editor)
  2. node scripts/ingest-deadlines/emit-sql.mjs fees.sql
  3. Paste that into the SQL editor

Step 2 fails until step 1 lands, since it selects the new columns. The ingest writes with the anon key, which colleges is read-only to, so it goes through the SQL editor as owner — same path as the September deadline run.

Also noticed

The grid was republished 2026-09-25, the day after the last ingest, and Yale is no longer in it. Everything else checks out (Princeton, Columbia, Brown, Cornell, Dartmouth all present, 1,127 rows parsed against 1,127 before). The source is live and this is what re-runnable is for — but worth knowing a school can leave it mid-cycle, and worth a look before the next apply.

Testing

527 passing plus 11 parser tests, lint and typecheck clean, build succeeds. New coverage: free vs unknown vs priced, each waiver policy, that the real figure overrides the type guess in both directions, and the Not Accepted substring case.

The ingest itself is unrun — the migration has to land first, so nothing here has touched real data.

The grid the deadline ingest already reads carries three more columns beside
the round dates — the US fee, the international fee, and whether a waiver is
taken. We were parsing past them.

Worth having because the answer is so often "nothing". Of the 1,128 schools
the grid lists a fee for, 611 charge $0. The median is $0, the 90th percentile
$70, the highest $150. A student looking at ten schools and bracing for $700
is usually wrong, and has no way to find out short of opening ten admissions
pages.

So the school page says it: "Free to apply", or "$85 · Fee waivers accepted".
Where no figure is on file it says that too rather than guessing — an invented
$0 is a promise, and an invented fee is a reason not to apply.

The waiver task now follows the real figure instead of the school's type. A
known $0 removes it; a known fee adds it, including at the two-year schools
that do charge. Only where nothing is on file does it fall back to the type,
which is most of the 6,273 colleges, since the grid covers Common App members
and little else.

Fees refresh on every ingest rather than only filling blanks. Nobody curates
them by hand, the grid is their only source, and a school that drops its fee
should stop being shown as charging one. Deadlines keep the opposite rule, for
the same reason in reverse.

One parser bug worth naming: "Not Accepted" contains "Accepted", so matching
the short label first turned all 29 schools that refuse waivers into schools
that take them. Longest label first now, and a test pins it.
@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 1, 2026 5:34pm UTC

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying timeline-prototype with  Cloudflare Pages  Cloudflare Pages

Latest commit: 25e7639
Status: ✅  Deploy successful!
Preview URL: https://727f3903.timeline-prototype.pages.dev
Branch Preview URL: https://feat-application-fees.timeline-prototype.pages.dev

View logs

@DanialBeg
DanialBeg changed the base branch from feat/fee-waiver-lead-time to main October 1, 2026 20:36
@DanialBeg
DanialBeg merged commit 32e9acd into main Oct 1, 2026
5 checks passed
@DanialBeg
DanialBeg deleted the feat/application-fees branch October 1, 2026 20:37
ZubairQazi added a commit that referenced this pull request Oct 2, 2026
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.

This branch was successfully deployed

1 active deployment
Preview — 25e76397 Deployed Oct 1, 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.

1 participant