Files
project-work/worker-toolkit-stocks-in-the-future/repo/docs/map/processes/finalize-gradebook-earnings.md

4.6 KiB

type, universe, status, consumes, produces
type universe status consumes produces
process live verified
../objects/gradebook/grade-book.md
../objects/gradebook/grade-entry.md
../objects/org/quarter.md
../objects/money/portfolio-transaction.md

finalize-gradebook-earnings

Grades and attendance become SIF dollars. The only path by which students earn.

Verified 2026-08-16 against commit 63732df.

Input → Movement → Output

A teacher fills in a quarter's grade-entries — two letter grades and attendance per student. An admin then presses Finalize, which flips the grade-book to verified and hands it to DistributeEarnings. The service writes up to three deposit rows per student and marks the book completed.

Why this shape

Teachers enter, admins release. GradeBookPolicy#finalize? is user.admin? (app/policies/grade_book_policy.rb:12-14) while show? and update? also accept the classroom's teachers (:4-10). Money is never minted by the person who entered the numbers.

verified is a millisecond-long state. The controller sets verified! and calls the service on the next line (app/controllers/grade_books_controller.rb:30-31); the service refuses to run on anything else (app/services/distribute_earnings.rb:14). It is a handshake between the two, not a review queue.

One check prevents paying twice — if @grade_book.completed? in the controller (app/controllers/grade_books_controller.rb:26). Deposits carry no link back to the entry that produced them, so a double run cannot be detected afterwards and would have to be unwound by hand.

Improvement bonuses reach into the previous quarter, crossing school-year boundaries via Quarter#previous (app/services/distribute_earnings.rb:35-38, app/models/quarter.rb:20-24). If that returns nil — a first quarter with no prior year — the bonus silently pays zero and the run still succeeds.

Steps

  1. Teacher edits entries; GradeBooksController#update writes them inside one transaction (app/controllers/grade_books_controller.rb:9-23). The autosave Stimulus controller PATCHes as they type.
  2. Admin posts finalize (config/routes.rb:26); authorize @grade_book resolves to finalize? → admin only (app/controllers/grade_books_controller.rb:5-6,39-41).
  3. Already completed? Redirect and stop (:26-28).
  4. Otherwise @grade_book.verified!, then DistributeEarnings.execute(@grade_book) (:30-31).
  5. The service loads the previous quarter's gradebook entries, grouped by user (app/services/distribute_earnings.rb:34-42).
  6. Per entry, it sums three buckets — attendance (days + perfect bonus), math (grade + improvement), reading (grade + improvement) (:54-73) — using the constants on grade-entry.
  7. Each non-zero bucket becomes a deposit with its own reason (:44-52). Zero-value buckets are skipped, so a student with no earnings gets no row at all.
  8. @grade_book.completed! inside the same transaction (:16-19).

If you change this

  • Hits: portfolio-transaction — this is where deposits come from; every balance downstream; earnings-summary, which groups those deposits by reason; grade-book status.
  • Does not hit: order, portfolio-stock, or any holding. Earnings arrive purely as cash — finalizing never buys anything, and a student with no orders is affected exactly as much as one with many.

Failure modes seen in the code

  • A letter grade outside GradeEntry::GRADE_OPTIONS — possible, since the model has no validations — makes improved_grade? compare nil indices and raise (app/models/grade_entry.rb:64-67). The transaction rolls back and the whole classroom goes unpaid.
  • DistributeEarnings pays entry.user regardless of enrollment status (:25-31), so an unenrolled student with a lingering entry is still paid.

Surfaces

Surface Role
GradeBooksController#update + autosave Stimulus controller teacher writes
GradeBooksController#finalize admin triggers
DistributeEarnings writes deposits

See

  • Objects: grade-book, grade-entry
  • Source: app/services/distribute_earnings.rb, app/controllers/grade_books_controller.rb
  • As-built: docs/gradebook-earnings.md