Files
project-work/worker-toolkit-stocks-in-the-future/repo/docs/map/objects/identity/student.md

3.0 KiB

type, cluster, universe, status, entity
type cluster universe status entity
object identity live verified 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; 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 (guaranteed on create), order
  • owned-by: classroom — twice over, see Why
  • joins: classroom-enrollment
  • 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 and classroom — both rosters; portfolio if you touch ensure_portfolio; Admin::StudentsController and StudentsController; the import-students movement, which creates students by this exact path.
  • Does not hit: teacher. 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