# SIS — Student Information System

The student records engine. Built directly on top of admissions so the
moment an applicant accepts an offer + pays, they flow into the SIS
with no data re-entry and no copy-paste. Designed to be the single
source of truth for everything a registrar, lecturer, finance officer,
or the student themselves needs to know.

---

## Design pillars

1. **Single source of truth.** Biodata lives once on a `Person` entity,
   not duplicated across Applicant, Student, Alumni. Querying "what's
   this student's date of birth" never returns two answers.

2. **Normalised contacts + identifications.** A Person has many emails,
   phones, addresses, government IDs. Each in its own row with
   kind/verified_at/effective dates so we never overwrite history.

3. **Append-only state.** `student_states` is event-sourced: each
   transition is a new row with effective_from + effective_until. Ask
   "what was this student's status on 14-Mar-2025?" → one query.

4. **Computed classifications.** Year level + standing (freshman,
   senior, postgrad year 2…) is derived from credits earned + programme
   duration, refreshed by a nightly command. Never manually edited;
   never drifts.

5. **Decoupled cards.** Card lifecycle (printed → issued → active →
   lost → replaced) is its own table with versions, not a status enum
   on Student.

6. **Reuse what works.** Encrypted document storage, activity logs,
   permission catalog, impersonation, RBAC — all already shipped. SIS
   plugs into them, doesn't rebuild them.

---

## Phase plan

| # | Name | Scope | Status |
|---|---|---|---|
| 1 | **Foundation** | `people` + normalised contact/ID/relationship tables; Person model; bridge from Application via `UpsertPersonFromApplication`; backfill existing students | **NOW** |
| 2 | **State engine + classifications** | `student_states` append-only; `student_classifications` computed; transition action + nightly recompute command | Pending |
| 3 | **Cards** | `student_cards` with version lifecycle; QR payload signing; reissue flow | Pending |
| 4 | **Mobility** | `student_leaves`, `student_transfers`, `student_withdrawals`; request/approve workflow | Pending |
| 5 | **Documents** | `student_documents` typed + verified + versioned; reuses encrypted storage | Pending |
| 6 | **Registrar UI** | Student directory + search; full profile page; quick actions panel | Pending |
| 7 | **Student self-service UI** | Personal dashboard; editable contacts; document downloads; card status | Pending |
| 8 | **Search & directory** | Postgres full-text index; permission-gated tenant directory | Pending |
| 9 | **Billing & fees** | Fee schedules, invoice generation, payment matching, statement export — its own substantial module | Pending |

Each phase ends with a commit, a CHANGELOG entry, and a check-in
before the next phase starts.

---

## Phase 1 — Foundation (the part being built now)

### Schema

```
people
  id, tenant_id, uuid, family_name, given_names, middle_names,
  preferred_name, salutation, gender, date_of_birth, nationality_code,
  marital_status, blood_group, religion, deceased_at,
  source_application_id (nullable — provenance only),
  created_at, updated_at, deleted_at

person_emails       (id, person_id, address, kind, is_primary, verified_at,
                     valid_from, valid_until)
person_phones       (id, person_id, e164_number, kind, is_primary, verified_at,
                     valid_from, valid_until)
person_addresses    (id, person_id, line1, line2, locality, region,
                     country_code, postal_code, kind, is_primary,
                     valid_from, valid_until)
person_identifications (id, person_id, kind, value, issuer, country_code,
                        issued_on, expires_on, verified_at)
person_relationships (id, person_id, related_person_id, kind,
                      is_emergency_contact, lives_with, financially_supports,
                      notes)

students (existing — add column)
  person_id → people.id (one-to-one; biodata source)
```

### Bridge from admissions

`App\Actions\SIS\UpsertPersonFromApplication`

* Called from `EnrolStudent` after Student is created.
* Idempotent. Dedupes by (tenant_id, national_id) primarily, then
  (tenant_id, lower(email)). Creates Person if no match; otherwise
  attaches Student to existing Person.
* Copies biodata from Application → Person.
* Creates normalised rows: email (verified, primary), phone if
  present, nationality, etc.

### Backfill

One-shot migration walks every existing Student row and:

* Tries to match a Person by email/national_id.
* Creates a Person from the Student's existing (denormalised) fields.
* Sets `students.person_id` to the new Person.id.

After this completes, the existing biodata columns on `students`
(full_name, date_of_birth, gender, nationality, region, phone, email)
are flagged as deprecated — they stay in place for back-compat but the
canonical read path is `$student->person->...`. They get dropped in a
later cleanup migration once all read sites are migrated.

### What's NOT in Phase 1

* No state machine yet (status column on students stays as-is for now)
* No card lifecycle
* No mobility events
* No new UI — Phase 1 is data plumbing; the UI lights up in Phase 6+

The point of Phase 1 is to land the SSOT foundation so every later
phase has somewhere correct to read biodata from.
