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,72 @@
---
type: object
cluster: identity
universe: live
status: verified
entity: app/models/student.rb
---
# Student
A `User` with `type: "Student"` — the only user kind that owns a portfolio and can trade.
Verified 2026-08-16 against commit `63732df`.
## Why this shape
Every money path downstream assumes a portfolio exists, so `Student` guarantees one on
create rather than letting callers remember (`app/models/student.rb:9,78-80`). Nothing in
the trading code null-checks for a missing portfolio because of this hook.
The class also carries the **roster bridge**. A student's classroom is reachable two ways
and `Student` is where they meet: `primary_classroom` prefers the enrollment record and
falls back to the legacy `classroom_id` column (`:37-43`). On create it writes both — but
`create_initial_enrollment` fires **only if `classroom_id` is present** (`:10,82-86`), so
a student created without it has no enrollment either. Read the roster trap in
`../../CONTEXT.md` before touching this.
## Shape
- STI subclass of [user](user.md); no table of its own
- `has_many :classroom_enrollments`, `has_many :classrooms, through:` (`:4-5`)
- Callbacks: `set_default_email` forces blank → `nil` (`:8,74-76`);
`ensure_portfolio` (`:9,78-80`); `create_initial_enrollment` (`:10,82-86`)
- Reads: `current_enrollments` (`:17-19`), `current_classrooms` (`:24-28`),
`primary_enrollment` (`:33-35`), `primary_classroom` (`:41-43`)
- Writes: `enroll_in!` (`:51-59`), `unenroll_from!` (`:66-70`)
`enroll_in!` always creates the row with `primary: false` and then promotes it via
`make_primary!` (`:52-57`) — the promotion is what enforces one-primary, not the insert.
## Connected to
- **owns:** [portfolio](../money/portfolio.md) (guaranteed on create),
[order](../trading/order.md)
- **owned-by:** [classroom](../org/classroom.md) — twice over, see Why
- **joins:** [classroom-enrollment](../org/classroom-enrollment.md)
- **looks-like-but-is-not:** `student.classrooms` (through enrollments) is **not**
`student.classroom` (the `classroom_id` column). They can disagree.
## If you change this
- **Hits:** [classroom-enrollment](../org/classroom-enrollment.md) and
[classroom](../org/classroom.md) — both rosters; [portfolio](../money/portfolio.md) if
you touch `ensure_portfolio`; `Admin::StudentsController` and `StudentsController`;
the [import-students](../../processes/import-students.md) movement, which creates
students by this exact path.
- **Does not hit:** [teacher](teacher.md). Same table, but no shared callbacks — `Teacher`
runs `sync_username_from_email` instead and shares none of the hooks above.
## Surfaces
| Surface | Role |
|---|---|
| `StudentsController` (nested under classroom) | teacher creates/edits, resets passwords |
| `Admin::StudentsController` | admin CRUD, CSV import, restore, manual transactions |
| `ImportStudentService` | writes |
| student's own portfolio + orders pages | reads |
## See
- Source: `app/models/student.rb`
- Base class: [user](user.md)