convert pdf to md, add OVERVIEW and docs

This commit is contained in:
2026-08-17 22:30:18 +00:00
parent 95d5787868
commit 4df62d2609
44 changed files with 3307 additions and 1 deletions

View File

@@ -0,0 +1,76 @@
---
type: object
cluster: gradebook
universe: live
status: verified
entity: app/models/grade_book.rb
---
# GradeBook
One [classroom](../org/classroom.md)'s grades for one [quarter](../org/quarter.md), and
the object whose status decides whether students get paid.
Verified 2026-08-16 against commit `63732df`.
## Why this shape
The model is tiny — two belongs-to, one has-many, one enum (`app/models/grade_book.rb`) —
but the enum is the payout gate.
`status` has three values: `draft → verified → completed` (`:8-12`). Read literally that
looks like a review workflow. **It is not.** `GradeBooksController#finalize` sets
`verified!` and calls `DistributeEarnings` on the very next line
(`app/controllers/grade_books_controller.rb:30-31`), so `verified` exists for a few
milliseconds. Its real job is to satisfy the service's own guard,
`return unless @grade_book.verified?` (`app/services/distribute_earnings.rb:14`), which
keeps the service safe to call from anywhere else.
**Double-payment is prevented by exactly one check** — the controller's
`if @grade_book.completed?` (`app/controllers/grade_books_controller.rb:26`). There is no
database constraint, no idempotency key on the resulting deposits, and
`DistributeEarnings` itself would happily pay twice if handed a `verified` book. Anything
new that finalizes a gradebook must repeat that check.
Gradebooks are never created by a controller: [classroom](../org/classroom.md) creates one
per quarter on `after_create` (`app/models/classroom.rb:29,112-116`).
## Shape
- Table `grade_books`, `db/schema.rb:98-107`; unique on `[quarter_id, classroom_id]`
(`db/schema.rb:105`) — one book per classroom per quarter
- `status` is a **string** column, default `"draft"`, `null: false` (`db/schema.rb:102`)
- `belongs_to :quarter`, `belongs_to :classroom` (`:4-5`)
- `has_many :grade_entries, dependent: :destroy` (`:6`)
## Connected to
- **owns:** [grade-entry](grade-entry.md)
- **owned-by:** [classroom](../org/classroom.md), [quarter](../org/quarter.md)
- **joins:** —
- **looks-like-but-is-not:** `verified` is not a human review state; see Why.
And a `GradeBook` is not a [grade-level](../org/grade-level.md).
## If you change this
- **Hits:** [portfolio-transaction](../money/portfolio-transaction.md) — finalizing mints
deposits; the [finalize-gradebook-earnings](../../processes/finalize-gradebook-earnings.md)
movement; [grade-entry](grade-entry.md) via `dependent: :destroy`;
`GradeBookPolicy`; the autosave Stimulus controller, which PATCHes entries into the
`update` action.
- **Does not hit:** [order](../trading/order.md) or any holding. Earnings arrive as cash
deposits only — finalizing never buys, sells, or touches
[portfolio-stock](../trading/portfolio-stock.md).
## Surfaces
| Surface | Role |
|---|---|
| `GradeBooksController` (`show`, `update`, `finalize`) | teacher reads/writes |
| `Classroom#create_gradebooks_for_quarters` | writes (creation) |
| `DistributeEarnings` | reads status, writes `completed!` |
## See
- Source: `app/models/grade_book.rb`, `db/schema.rb:98-107`
- As-built: `docs/gradebook-earnings.md`

View File

@@ -0,0 +1,90 @@
---
type: object
cluster: gradebook
universe: live
status: verified
entity: app/models/grade_entry.rb
---
# GradeEntry
One student's row in one [grade-book](grade-book.md): two letter grades, attendance days,
a perfect-attendance flag. **This is where every payout amount is defined.**
Verified 2026-08-16 against commit `63732df`.
## Why this shape
The payout table is five Ruby constants on this model, all in **cents**
(`app/models/grade_entry.rb:9-13`):
| Constant | Value | Meaning |
|---|---|---|
| `EARNINGS_PER_DAY_ATTENDANCE` | `20` | $0.20 per day present |
| `EARNINGS_FOR_A_GRADE` | `3_00` | $3.00 for any A |
| `EARNINGS_FOR_B_GRADE` | `2_00` | $2.00 for any B |
| `EARNINGS_FOR_IMPROVED_GRADE` | `2_00` | $2.00 for improving |
| `EARNINGS_FOR_PERFECT_ATTENDANCE` | `1_00` | $1.00 bonus |
They are not configuration. Changing what a student earns is a code change and a deploy —
there is no admin screen and no database row for these.
**`GRADE_OPTIONS` is ordered best-to-worst on purpose** (`:15`). `improved_grade?`
compares array *indices*, treating a lower index as better (`:64-67`). Reordering or
inserting into that array silently changes every improvement bonus in the app.
**There are no validations on this model at all** — grades are constrained only by the
`<select>` in `app/views/grade_books/_grade_entry.html.erb:8,17`, and the controller
permits the values straight through (`app/controllers/grade_books_controller.rb:53-57`).
A value outside `GRADE_OPTIONS` saves fine, then makes `improved_grade?` compare `nil`
indices and raise `NoMethodError` during the next quarter's payout. Grades C through F
earn nothing but are legal; anything not in the list is a latent failure.
## Shape
- Table `grade_entries`, `db/schema.rb:109-121`; unique on `[grade_book_id, user_id]`
(`db/schema.rb:118`) — one row per student per book
- `math_grade`, `reading_grade` — plain strings, nullable, unvalidated
- `attendance_days` — bigint, nullable; `earnings_for_attendance` returns 0 when blank
(`:17-21`)
- `is_perfect_attendance` — boolean, default false, `null: false` (`db/schema.rb:113`)
- `belongs_to :grade_book`, `belongs_to :user` (`:4-5`) — `user`, not `student`
- Earnings readers: `earnings_for_attendance`, `earnings_for_math`,
`earnings_for_reading`, `attendance_perfect_earnings`, `math_improvement_earnings`,
`reading_improvement_earnings` (`:17-49`)
Every earnings method is a **pure reader**. Nothing here writes money —
`DistributeEarnings` calls them and creates the deposits.
## Connected to
- **owns:** —
- **owned-by:** [grade-book](grade-book.md), [user](../identity/user.md)
- **joins:** —
- **looks-like-but-is-not:** `math_grade` is a **letter** (`"A+"`…`"F"`), unrelated to
[grade-level](../org/grade-level.md), which is 5–8.
## If you change this
- **Hits:** [portfolio-transaction](../money/portfolio-transaction.md) amounts — these
constants are the amounts; `DistributeEarnings`
(`app/services/distribute_earnings.rb:54-73`), which sums attendance + math + reading;
the [finalize-gradebook-earnings](../../processes/finalize-gradebook-earnings.md)
movement; `AttendanceEntryPresenter`.
- **Does not hit:** already-paid deposits. Editing an entry after finalize changes
nothing retroactively — the deposits are independent rows with no link back to the
entry that produced them (`db/schema.rb:170-179` has no `grade_entry_id`). Re-paying
would require re-running finalize, which the `completed?` guard blocks.
## Surfaces
| Surface | Role |
|---|---|
| `GradeBooksController#update` (+ autosave Stimulus controller) | teacher writes |
| `DistributeEarnings` | reads |
| `AttendanceEntryPresenter` | reads |
## See
- Source: `app/models/grade_entry.rb`, `db/schema.rb:109-121`
- As-built: `docs/gradebook-earnings.md`