# i-Learned — Implementation Tasks

Phase breakdown from `SPEC.md §39`. Updated at the end of every phase, as
required by `SPEC.md §45`.

**Status legend:** `[ ]` not started · `[~]` in progress · `[x]` done

**Gate for every phase:** `pnpm lint` · `pnpm typecheck` · `pnpm test` must all
pass, and the application must still run, before the phase is marked done.

---

## Progress

| Phase | Title | Status |
| --- | --- | --- |
| 0 | Foundation | `[x]` **complete** — gates passed 2026-08-25 |
| 1 | CRM and Admissions | `[x]` complete |
| 2 | Student Records and Enrollment | `[~]` complete except the dean and room calendar views |
| 3 | Finance MVP | `[x]` complete |
| 4 | Faculty and Grades | `[x]` complete |
| 5 | Student Services and Analytics | `[~]` complete except the report builder and profile-update approval |
| 6 | Hardening | `[~]` E2E and accessibility complete; API document outstanding |

---

## Phase 0 — Foundation

Monorepo, environment, database, component system, authentication, RBAC,
institution configuration, seed users, audit framework.

### 0.1 Repository and tooling
- [x] pnpm workspaces + Turborepo pipeline
- [x] Shared TypeScript configs (strict, `noUncheckedIndexedAccess`)
- [x] Shared ESLint flat configs (base + React)
- [x] Prettier, `.gitignore`, `.nvmrc`
- [x] `.env.example` with every variable documented
- [x] `docker-compose.yml` — PostgreSQL, optional Redis, optional MinIO
- [x] `ARCHITECTURE.md`, `ASSUMPTIONS.md`, `TASKS.md`
- [x] `tools/local-postgres` — Docker-free PostgreSQL for local development
- [x] `README.md` with setup, demo accounts and troubleshooting

### 0.2 Domain core (`packages/core`) — no framework imports
- [x] Typed domain errors and their HTTP mapping
- [x] Permission catalogue and `authorize()` resolution
- [x] `SecurityContext` and scope types (college, section, self)
- [x] Generic state-machine engine (`TransitionTable`, `transition()`)
- [x] Integer-centavo money arithmetic
- [x] Port interfaces: storage, mailer, jobs, exam provider, lead ads,
      payment gateway, captcha, malware scanner, rate limiter, clock
- [x] PII-redacting logger
- [x] Philippine formatting helpers (mobile normalization, PHP currency, dates)

### 0.3 Database (`packages/db`)
- [x] Full Prisma schema — all `SPEC.md §8` concepts
- [x] Initial migration
- [x] Migration adding append-only rules on `audit_log`
- [x] Prisma client singleton + transaction helper
- [x] Repository implementations of `core` ports
- [x] Deterministic seed: institution, branding, roles, permissions, demo users

### 0.4 Authentication and RBAC
- [x] Argon2id password hashing and verification
- [x] Opaque DB-backed sessions (hashed token, idle + absolute expiry)
- [x] Login, logout, session revocation
- [x] Rate limiting and account lockout on repeated failures
- [x] Password reset with expiring single-use token
- [x] `requireSession()` / `authorize()` guards for route handlers
- [x] Audit framework: append-only writer used by every sensitive action
- [x] Login and security-event logging

### 0.5 Application shell (`apps/web` + `packages/ui`)
- [x] Next.js 16 app with strict TypeScript
- [x] Tailwind 4 with branding-driven CSS custom properties
- [x] Glassmorphism surface treatment — brand-derived ambient wash, frosted chrome, cards, tables and dialogs, with measured contrast and solid fallbacks
- [x] UI primitives: button, input, select, dialog, badge, table, card, tabs,
      toast, breadcrumb, stepper, timeline, empty/error/loading states
- [x] Role-aware app shell: sidebar, top bar, breadcrumbs, mobile navigation
- [x] `/login`, `/forgot-password`, landing page
- [x] Role dashboard placeholders for all eight roles, correctly gated
- [x] `/admin/branding` — editable institution identity: system information, colours, footer and support contacts, logo upload, and an explicit provenance confirmation
- [x] `/admin/users` — add, edit, activate, deactivate, reset password and delete, each guarded by a rank check as well as a permission check
- [x] `/admin/colleges` and `/admin/programs` — add, edit, retire, reactivate and delete colleges, departments and programmes, with dependency guards in place of foreign-key errors
- [x] Data-subject rights (RA 10173): public privacy notice, `/account/data` with an audited portable export, access history by office, marketing-consent withdrawal, and a reported retention schedule
- [x] Standard API envelope + `withApiHandler` wrapper
- [ ] OpenAPI document generated from Zod schemas

### 0.6 Phase gate
- [x] `pnpm lint` clean
- [x] `pnpm typecheck` clean
- [x] `pnpm test` green (unit tests for permissions, transitions, money)
- [x] All eight demo roles can log in and reach their own dashboard only

---

## Phase 0 verification record

Recorded because "it builds" and "it works" are different claims, and
`SPEC.md §0.11` forbids overstating readiness.

**Gates, all passing:**

| Gate | Result |
| --- | --- |
| `pnpm lint` | clean across 5 packages |
| `pnpm typecheck` | clean across 7 targets, TypeScript strict |
| `pnpm test` | 267 passing — 230 unit, 37 integration |
| `pnpm build` | production build succeeds |
| `pnpm db:verify` | 10 seed invariants hold |

**Verified by running it, not by inspection:**

- All eight seeded roles sign in and land on their own console.
- A student is refused all six staff consoles; a cashier is refused the
  registrar, dean and admin consoles. Refusal is by permission, not by hidden
  navigation — the URLs were requested directly.
- A wrong password and an unknown account return byte-identical messages.
- A cross-origin `POST` to the login endpoint is refused with 403.
- Logout revokes the session server-side: the next request returns 401.
- `UPDATE` and `DELETE` on `audit_log` are refused by the database itself.
- A backwards meeting time is refused by a database check constraint.
- The seeded timetable has zero faculty, room or student schedule conflicts
  across 560 meetings.
- Every assessment total reconciles with its own lines.
- 165 unpublished grades exist and none is returned by the student grade query.

**Known gap carried into Phase 1:** the OpenAPI document is not yet generated
from the Zod schemas. The envelope, error mapping and validation it would
describe are all in place; only the emitted specification is missing.

---

## Phase 1 — CRM and Admissions

- [x] Lead model: sources, UTM capture, stages, tags, assignment, follow-ups
- [x] Lead list with filters, quick search, duplicate detection
- [x] Lead activity timeline and notes
- [x] Lead → applicant conversion (linked, not copied)
- [x] CSV import for historical Facebook leads
- [x] `LeadAdsProvider` port + mock adapter
- [x] Public `/apply` multi-step wizard with save-as-draft
- [x] Application reference number generation
- [x] Printable submission acknowledgment
- [x] Public `/apply/status` progress lookup
- [x] Configurable requirement definitions per applicant type and program
- [x] Requirement upload with versioning (no overwrite)
- [x] Requirement review: accept / reject with reason / needs-resubmission / waive
- [x] Exam scheduling: dates, slots, venue/online, capacity, booking, reschedule
- [x] Exam attendance and result encoding
- [x] `EntranceExamProvider` port + internal and external adapters
- [x] Application state machine covering all four admission tracks
- [x] Medicine track: NMAT screening against configurable threshold
- [x] Dean interview scheduling, rubric scoring, decision
- [x] Medicine panel interview
- [x] Admission decision and notice-of-acceptance status
- [x] Applicant communication log
- [x] Admissions dashboard with live funnel and conversion metrics
- [x] Dean applicant queue, scoped to assigned colleges
- [x] Notification triggers for admissions events
- [x] Phase gate: lint, typecheck, tests, update this file

### Phase 1 verification record

**Gates:** `pnpm lint` clean · `pnpm typecheck` clean · `pnpm test` 367 passing
(240 unit, 10 hashing, 117 integration) · `pnpm build` succeeds, 56 routes ·
`pnpm db:verify` all 10 invariants hold.

**Verified by running it:**

- A complete application submitted through the real browser UI:
  `APP-2026-000083`, with the checklist generated from configuration and the
  applicant able to follow it publicly.
- A Medicine applicant is offered only the MD programme; a freshman is offered
  the other six. The track is decided by the programme, not self-declared.
- The privacy notice cannot be skipped — the wizard refuses to advance.
- Reference numbers continue past pre-existing data instead of colliding.
- An illegal transition (`SUBMITTED → ENROLLED`) is refused; a legal one with
  an unmet precondition returns 412 naming what is outstanding.
- A rejection with no reason is refused at the API, the service and the UI.
- A student and a cashier are refused every admissions endpoint and screen.
- A Dean sees only applications in their college; one from another college
  reports "not found" rather than "forbidden".

**Three real defects found and fixed during this phase:**

1. **Permission checked after the record lookup.** An actor with no rights got
   a 404 from a bogus id instead of a 403, leaking whether the record existed.
   Permission is now checked before any lookup, across five services.
2. **Reference numbers collided with seeded data.** The seed wrote
   `APP-2026-000001` upward but left the allocator at zero, so the first real
   submission failed on the unique index. The seed now aligns every counter
   with the rows it created — the same step any bulk import must perform.
3. **Draft cleanup deadlocked submission.** The draft was deleted inside the
   serializable submission transaction while the wizard autosaved the same row,
   losing real submissions. Cleanup now happens after commit and cannot fail the
   submission. Only the browser test caught this; the API test had no concurrent
   writer.

**The remaining screens, verified the same way:**

- `/admissions/exams` lists sittings with seats remaining, and creates one.
- `/admissions/exams/:id` marks attendance and encodes scores per applicant.
  The pass/fail outcome is an explicit staff choice: no cut-off is configured,
  and none is inferred (ASSUMPTIONS.md C2).
- `/dean/applicants` shows the Dean's own queue; a Dean with no college
  assignment gets an explanation rather than an empty table.
- `/dean/interviews` and `/dean/interviews/:id` schedule and evaluate, with the
  weighted total shown as it is filled in.
- The Dean's interview screen shows requirement *status* only. The uploaded
  documents are deliberately not surfaced there (SPEC.md §15).
- A Nursing dean opening a Medicine interview gets 404, not 403.
- Narrative comments are genuinely mandatory: the button stays disabled, the
  service refuses it, and a test asserts each.

**The whole chain, driven end to end** for `APP-2026-000084`: public submission
→ under review → booked onto a sitting → score and outcome recorded →
requirements → interview scheduled → evaluation refused without comments, then
accepted with them → ACCEPTED, and the applicant sees "Decision" on the public
status page. Seventeen assertions, all green.

**No fourth product defect.** The chain run initially failed because the public
rate limiter (five submissions per hour per IP) blocked the test's submission —
the limiter doing exactly its job. The harness then searched on an empty
reference and asserted against the resulting unfiltered list. The fix was to the
test, not the product: a distinct client address per run, and an abort when the
reference comes back empty. Recorded here because "the test was wrong" is a
conclusion worth being able to audit later.

**Bulk lead import, the last item:** `/admissions/leads/import` reads a
Facebook Lead Ads export as-is. It is deliberately two-stage — the file is
parsed, cross-referenced against existing leads, and *nothing is written* until
someone has read what the dry run says. A bulk import is the one operation where
an unnoticed mistake is multiplied by a thousand, so the preview is the feature
rather than a courtesy.

Three policy decisions are recorded rather than assumed (ASSUMPTIONS.md C9–C11):

- **Consent is never invented.** A historical export carries no evidence that
  anyone agreed to be contacted, so rows import with consent *not given* unless
  the file states otherwise; an unreadable consent value counts as no consent.
  The count without consent is shown before and after the import.
- **Ambiguous dates are refused, not guessed.** `03/04/2026` is 3 April or
  4 March depending on the exporting locale; guessing would misdate a whole
  batch silently. The original enquiry date is otherwise preserved, because
  overwriting it would destroy the funnel history the import exists to recover.
- **Duplicates are reported, never merged** — the same stance the CRM takes
  everywhere else, in both directions (repeated within the file, and matching a
  lead already on record).

Verified against the running application: a four-row file producing one of every
outcome imported exactly two leads, the quoted comma in `"Santos, Maria"`
survived, `0917 123 9001` was stored as `+639171239001`, re-importing the same
file reported both as already on record and offered to create nothing, a file
with no name column was refused up front rather than row by row, and a student
and a cashier were refused the screen and both endpoints. Nineteen checks.

**A defect the tests caught:** the batch tag was only second-resolution, so two
imports in the same second shared a tag — and that tag is the documented way to
find or undo a specific batch. It now carries a random suffix.

**A second, found while running the phase gate:** `pnpm db:verify` had been
silently broken since the Prisma client output moved in Phase 0. It imported a
path that no longer existed, so the seed invariants had not actually run for
some time. Fixed; all ten hold.

**Phase 1 is complete.**

---

## Phase 2 — Student Records and Enrollment

- [x] Student master record; applicant → student conversion in one transaction
- [x] Student number assignment
- [x] Student status history, prior schools, emergency contacts, addresses
- [x] Colleges, programs, departments, rooms admin
- [x] Academic years and terms admin (single current year enforced)
- [x] Course catalogue with prerequisites
- [x] Curriculum versioning with effectivity dates
- [x] Section offerings and meeting schedules
- [x] Server-side conflict detection: faculty, room, section, capacity
- [x] Transferee subject evaluation (Dean) and final evaluation (Registrar)
- [x] Pre-registration drafting and Dean approval
- [x] Enrollment state machine and enrollment items with curriculum snapshot
- [x] Registrar validation and finalization
- [x] COR prototype (watermarked) — view and print
- [x] Registrar dashboard with live queues
- [~] Calendar views: student, faculty, dean, room — *student and faculty views built; dean and room calendars outstanding*
- [~] CSV/XLSX import framework: upload, validate, preview, confirm, report — *the framework exists and is proven by lead import; XLSX and other entity types outstanding*
- [x] Phase gate: lint, typecheck, tests, update this file

---

## Phase 3 — Finance MVP

- [x] Fee definitions: per-unit, fixed, per-term, percentage
- [x] Fee schedules bound to program + year + year level
- [x] Assessment generation with snapshotted lines
- [x] Discounts and scholarships as explicit lines
- [x] Payment plans with due dates
- [x] Payment posting with idempotency key
- [x] Payment allocation and balance computation
- [x] Service invoice reference recording
- [x] Void and reversal with mandatory reason and audit
- [x] Cashier dashboard: queues, collections, methods, partial payments
- [x] `PaymentGateway` port + null adapter
- [x] Assessment summary document (watermarked)
- [x] Phase gate: lint, typecheck, tests, update this file

---

## Phase 4 — Faculty and Grades

- [x] Faculty profiles and load assignment by Dean
- [x] Faculty load view: subject, section, units, hours, schedule, headcount
- [x] Class lists restricted to assigned sections and instruction-relevant fields
- [x] Configurable grade scales per program
- [x] Grade entry with draft saving and validation
- [x] Final submission with timestamp and lock
- [x] Registrar publication; students see published grades only
- [x] Grade change request and approval workflow
- [x] Full grade history retention
- [x] Dean grade-submission monitoring
- [x] Phase gate: lint, typecheck, tests, update this file

---

## Phase 5 — Student Services and Analytics

- [x] Student portal: home, profile, enrollment timeline, schedule, grades,
      finance, notifications
- [~] Profile update requests requiring Registrar approval — *requests are raised, validated and audited; the Registrar approval screen is outstanding*
- [x] Document request types with fees, SLA and issuing office
- [x] Document request lifecycle through to release
- [x] Clearance templates, steps and per-office sign-off
- [x] Continuing-student enrollment gated on clearance where configured
- [x] Analytics: admissions, enrollment, demographics, finance, academic ops
- [x] Small-cell suppression on demographic breakdowns
- [x] CSV export gated on permission, with audit events
- [~] Reports: applicant summary, funnel, enrollment summary, master list,
      class list, faculty load, collection summary, document requests —
      *the funnel, enrollment, collection and master-list views exist on the
      analytics and console screens; a dedicated report builder is outstanding*
- [x] Global role-scoped search
- [x] Phase gate: lint, typecheck, tests, update this file

---

## Phase 6 — Hardening

- [x] Playwright E2E — Scenario 1: Freshman
- [x] Playwright E2E — Scenario 2: Transferee
- [x] Playwright E2E — Scenario 3: Continuing student
- [x] Playwright E2E — Scenario 4: Doctor of Medicine
- [x] Playwright E2E — Scenario 5: Faculty grades → publication → student view
- [x] Playwright E2E — Scenario 6: Privacy denials
- [x] Accessibility review against WCAG 2.2 AA
- [x] Rate limiting and anti-bot on all public endpoints
- [~] Security review pass; dependency audit — *the SPEC.md §44 review is recorded in `docs/security-checklist.md`; an external dependency audit has not been run*
- [~] `docs/deployment.md`, `docs/security-checklist.md`, `docs/api.md` — *deployment and security checklist written; `docs/api.md` is not generated from the Zod schemas yet*
- [x] README finalization with tested commands
- [x] Phase gate: lint, typecheck, full test suite, update this file

---

### Phases 2-6 verification record

**Gates:** `pnpm lint` clean - `pnpm typecheck` clean - `pnpm test` 418 passing
(248 unit, 10 hashing, 160 integration) - `pnpm test:e2e` 45 passing in a real
browser - `pnpm build` succeeds, 92 routes - `pnpm db:verify` all 10 invariants
hold.

**What was built.** Enrollment with server-side conflict detection and Registrar
overrides; the watermarked Certificate of Registration; cashiering with
idempotent posting, allocation and void/reversal; grade encoding, submission,
publication and change requests; the student portal (schedule, finance, grades,
requests, clearance, notifications); document requests and clearance sign-off;
analytics with small-cell suppression and an audited CSV export.

**Verified by driving the running application**, not only by unit test:
147 checks across five smoke suites, plus 45 Playwright scenarios covering the
six journeys in SPEC.md 39 - including a freshman completing the whole public
wizard and then tracking the application publicly.

**Five real defects found and fixed.**

1. **A student could read another student's clearance.** `clearance.read` is
   held by the STUDENT role for the holder's *own* record, and the guard treated
   possession of it as authority over anybody's. Reaching someone else's
   clearance now requires the staff permission (ASSUMPTIONS.md G8). Found by an
   integration test written specifically to try it.
2. **Scope checks named only the college.** A student carries no college scope,
   so a college-only target passed `isInScope` unchallenged; the permission
   guard above it was doing all the work. Every check on a student-owned record
   now names the student too, so it is correct on its own (ASSUMPTIONS.md G9).
3. **The skip link did not skip.** `<main>` is not focusable by default, so
   activating "Skip to main content" scrolled the page and left focus on the
   link - the next Tab walked straight back into the navigation the user was
   trying to bypass. A genuine WCAG 2.4.1 failure, fixed on all six layouts.
4. **A reversal tried to post a negative payment.** The database refused it
   (`payment_amount_positive`) and the database was right: a payment is money a
   person handed over, and a negative one is not a payment. The reversal is now
   recorded as a status change plus a `PaymentReversal` row (ASSUMPTIONS.md E8).
5. **`payment.void` was held by no role at all.** The void/reversal workflow
   that SPEC.md 17 requires was reachable only by the break-glass superadmin.
   It is now held by ADMIN - never by the cashier, who must not be able to undo
   their own work (ASSUMPTIONS.md E7). Applying it needed no destructive reset:
   `pnpm db:sync-roles` reconciles seeded role permissions with the catalogue.

**One flaky test made deterministic.** The public-submission tests drew a
*random* client address from a 250-strong range, and the rate limiter is
in-memory for the life of the run - so once enough tests existed, two collided
and one was throttled for a reason unrelated to what it was checking. Addresses
are now allocated from a counter. Verified stable across three consecutive runs.

**An imprecise invariant corrected.** `pnpm db:verify` asserted that *every*
application carries status history. Drafts are the wizard's autosave slots and
have none by design - writing one per autosave would fill the audit trail with
keystrokes rather than decisions. The invariant now says what it means: every
*submitted* application has history.

**What is deliberately not finished** is listed against each phase above with
what remains. The largest gaps are the transferee two-stage evaluation screen,
the Dean's load-assignment and grade-monitoring screens, a dedicated report
builder, global cross-entity search, and `docs/api.md` generated from the Zod
schemas.

---

### Closing the Phase 2-5 gaps

**Gates:** `pnpm lint` clean - `pnpm typecheck` clean - `pnpm test` 432 passing
(248 unit, 10 hashing, 174 integration) - `pnpm test:e2e` 44 passing, 1 skipped -
`pnpm build` succeeds, 101 routes - `pnpm db:verify` all invariants hold.

**Transferee subject evaluation** (`/registrar/transfer-credit`). Two stages,
side by side, neither overwriting the other — so a Registrar who differs from a
Dean produces a *visible disagreement* rather than a silent correction, and the
screen marks each row where they differ. No equivalence is matched
automatically: crediting a subject taken elsewhere is an academic judgement
(ASSUMPTIONS.md C12-C14). Crediting more units than the subject carried is
refused outright, because that is arithmetic rather than judgement, and a
"partial" that credits the full value is refused as a contradiction.

**The Dean's two screens.** `/dean/faculty-load` assigns teaching, and refuses a
double-booking outright with no override — a person cannot be in two rooms at
once, so allowing it would only record a fiction (SPEC.md 33.12). The refusal
names who is already teaching and when. `/dean/grades` monitors submission
across the college and deliberately shows **counts, never marks**: knowing
whether a colleague has finished is a legitimate need; reading their students'
grades to find out is not.

**The assessment statement** (`/cashier/assessments/:id`), rendered from the
assessment's snapshotted lines rather than recomputed from the fee schedule, so
the paper in a student's hand cannot disagree with what they were asked to pay.
Gross, discount and net are all shown, so the figure reconciles against its own
visible components.

**Global search**, which obeys the same rules as the screens it searches. Each
entity is queried only when the caller holds the permission governing its own
list, filtered by the same scope. A search box that found records its user could
not otherwise open would be a privacy hole with a friendly interface.

**A judgement call worth recording.** The first version returned *nothing at
all* for a student, which leaks nothing but leaves the largest group of users
with a dead control. Students now search their own document requests, filtered
on their own user id.

**Two corrections to my own assumptions**, both found by writing the test:

1. I first gated the Dean's evaluation stage on `application.review`, which a
   Registrar does not hold — so the Registrar's office could not record a
   transcript, which is exactly what it does in practice. The catalogue already
   had a dedicated `transfer_credit.evaluate` held by *both* roles. The
   separation that genuinely exists runs the other way: a Dean cannot verify
   their own judgement, because `enrollment.approve` is the Registrar's alone.
2. The freshman end-to-end scenario scraped the reference number off the
   acknowledgment page. That worked until the number appeared in more than one
   place, after which it produced an empty string and failed three steps later
   for a reason with no connection to the cause. It now reads the reference
   from the submission response, and reports a rate-limited run as a skip
   rather than a mystery timeout.

**Accessibility** re-audited across 26 screens after the additions - all
structural checks pass.

---

## Deferred — out of MVP scope

Tracked so the boundary stays explicit (`SPEC.md §38`). Extension points exist;
implementations do not.

- Full LMS content delivery, assignments, quizzes
- Payroll, HR, procurement, inventory, library circulation, biometrics
- Clinic/EMR, dormitory, alumni
- Native mobile application
- Production Meta Lead Ads sync (port + mock only)
- Production online payment gateway (port + null adapter only)
- Official digital signatures and verification QR
- CHED or other external system integration
- Full general ledger and accounting ERP
