convert pdf to md, add OVERVIEW and docs
This commit is contained in:
@@ -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)
|
||||
Reference in New Issue
Block a user