convert pdf to md, add OVERVIEW and docs
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
---
|
||||
type: process
|
||||
universe: live
|
||||
status: verified
|
||||
consumes: ["../objects/gradebook/grade-book.md", "../objects/gradebook/grade-entry.md", "../objects/org/quarter.md"]
|
||||
produces: ["../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](../objects/gradebook/grade-entry.md) —
|
||||
two letter grades and attendance per student. An **admin** then presses Finalize, which
|
||||
flips the [grade-book](../objects/gradebook/grade-book.md) 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](../objects/gradebook/grade-entry.md).
|
||||
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](../objects/money/portfolio-transaction.md) — this is
|
||||
where deposits come from; every balance downstream;
|
||||
[earnings-summary](../objects/money/earnings-summary.md), which groups those deposits
|
||||
by reason; [grade-book](../objects/gradebook/grade-book.md) status.
|
||||
- **Does not hit:** [order](../objects/trading/order.md),
|
||||
[portfolio-stock](../objects/trading/portfolio-stock.md), 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](../objects/gradebook/grade-book.md),
|
||||
[grade-entry](../objects/gradebook/grade-entry.md)
|
||||
- Source: `app/services/distribute_earnings.rb`,
|
||||
`app/controllers/grade_books_controller.rb`
|
||||
- As-built: `docs/gradebook-earnings.md`
|
||||
Reference in New Issue
Block a user