4.6 KiB
type, universe, status, consumes, produces
| type | universe | status | consumes | produces | ||||
|---|---|---|---|---|---|---|---|---|
| process | live | verified |
|
|
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
- Teacher edits entries;
GradeBooksController#updatewrites them inside one transaction (app/controllers/grade_books_controller.rb:9-23). TheautosaveStimulus controller PATCHes as they type. - Admin posts
finalize(config/routes.rb:26);authorize @grade_bookresolves tofinalize?→ admin only (app/controllers/grade_books_controller.rb:5-6,39-41). - Already
completed? Redirect and stop (:26-28). - Otherwise
@grade_book.verified!, thenDistributeEarnings.execute(@grade_book)(:30-31). - The service loads the previous quarter's gradebook entries, grouped by user
(
app/services/distribute_earnings.rb:34-42). - Per entry, it sums three buckets — attendance (days + perfect bonus), math (grade +
improvement), reading (grade + improvement) (
:54-73) — using the constants on grade-entry. - Each non-zero bucket becomes a
depositwith its ownreason(:44-52). Zero-value buckets are skipped, so a student with no earnings gets no row at all. @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 — makesimproved_grade?comparenilindices and raise (app/models/grade_entry.rb:64-67). The transaction rolls back and the whole classroom goes unpaid. DistributeEarningspaysentry.userregardless 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