--- 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`