# Product Requirements Document
## University Management System (UMS)

**Version:** 1.0
**Date:** July 10, 2026
**Owner:** Samir
**Status:** Draft
**Stack:** Laravel (full-stack monolith)

---

# 1. Overview

A web-based University Management System built as a Laravel monolith with two distinct surfaces:

1. **Student Portal** — public-facing. Prospective students sign up, apply, enroll in courses, and pay fees online.
2. **Administration Console** — internal. Staff manage students, teachers, subjects, classes, attendance, grades, and finance.

**Core principle:** *Enrollment is the atom.* A student enrolled in a class section is the single record that attendance, grades, and invoices all hang off. Everything else is a lookup table around it.

---

# 2. Problem Statement

University operations today are fragmented across spreadsheets, paper forms, and manual fee collection. This causes:

- No single source of truth for student headcount, teacher load, or class capacity
- Students must physically visit the registrar to enroll or pay
- Fee reconciliation is manual and error-prone
- No visibility into at-risk students until it's too late

---

# 3. Goals & Success Metrics

| Goal | Metric | Target |
|---|---|---|
| Centralize university data | Single DB of record for students, staff, courses | 100% of records migrated |
| Self-service enrollment | % of enrollments completed without staff intervention | > 85% |
| Online fee collection | % of fees collected digitally | > 70% in first term |
| Operational visibility | Time to answer "how many active students?" | < 2 seconds (dashboard) |
| Reduce registrar workload | Support tickets during registration week | −50% vs baseline |

### Non-Goals (v1)

Library management, hostel/dormitory allocation, alumni portal, multi-campus tenancy, live video classes, native mobile apps.

---

# 4. User Roles & Permissions

| Role | Description | Key Permissions |
|---|---|---|
| **Guest** | Unauthenticated visitor | View landing page, programs, apply |
| **Applicant** | Signed up, not yet accepted | View application status, upload documents |
| **Student** | Accepted and enrolled | Enroll in classes, view grades/attendance, pay fees |
| **Teacher** | Faculty member | View assigned classes, mark attendance, enter grades |
| **Registrar** | Academic staff | CRUD students, classes, terms; approve applications |
| **Finance** | Accounts staff | View/generate invoices, reconcile payments, issue refunds |
| **Admin** | Superuser | All of the above + user management, roles, system settings |

**Implementation:** `spatie/laravel-permission`. Roles carry permissions; permissions are checked in Policies, never inline in controllers.

**Permission matrix (abridged):**

| Permission | Student | Teacher | Registrar | Finance | Admin |
|---|:-:|:-:|:-:|:-:|:-:|
| `student.view.own` | ✅ | — | — | — | ✅ |
| `student.view.any` | — | roster only | ✅ | ✅ | ✅ |
| `student.create` | — | — | ✅ | — | ✅ |
| `enrollment.create.own` | ✅ | — | — | — | ✅ |
| `enrollment.create.any` | — | — | ✅ | — | ✅ |
| `attendance.mark` | — | ✅ (own classes) | ✅ | — | ✅ |
| `grade.enter` | — | ✅ (own classes) | — | — | ✅ |
| `grade.publish` | — | ✅ | ✅ | — | ✅ |
| `invoice.view.own` | ✅ | — | — | — | ✅ |
| `invoice.generate` | — | — | ✅ | ✅ | ✅ |
| `payment.refund` | — | — | — | ✅ | ✅ |
| `class.create` | — | — | ✅ | — | ✅ |
| `user.manage` | — | — | — | — | ✅ |

---

# 5. Feature Specification

## 5.1 Public Landing Page

**Route:** `GET /`

**Sections:**
- Hero with university name, tagline, primary CTA ("Apply Now"), secondary CTA ("Student Login")
- Programs offered — card grid, each linking to a program detail page
- Admission requirements + deadlines (pulled from active `Term`)
- Faculty highlights
- Campus stats (students, faculty, programs) — cached counters
- Contact + footer

**Requirements:**
- Fully mobile-responsive
- Public program pages are SEO-indexable (server-rendered Blade)
- Page load < 1.5s on 3G

---

## 5.2 Sign Up & Application

**Route:** `GET|POST /register`

**Fields:**

| Field | Type | Validation |
|---|---|---|
| Full name | string | required, max 255 |
| Email | string | required, email, unique:users |
| Phone | string | required, valid phone |
| Password | string | required, min 8, confirmed, uncompromised |
| Date of birth | date | required, before 15 years ago |
| National ID / Passport | string | required, unique |
| Program of interest | select | required, exists:programs,id |

**Flow:**
1. Submit form → `User` created with role `applicant`, status `pending_verification`
2. Verification email sent (queued job)
3. Click link → email verified, status → `pending_documents`
4. Upload documents (transcript, ID scan, photo) → status → `submitted`
5. Registrar reviews in admin console → `accepted` or `rejected`
6. On acceptance: `Student` record created, `student_number` generated, role changed to `student`, acceptance email sent

**Application status machine:**

```
draft → pending_verification → pending_documents → submitted
                                                      ├→ under_review → accepted → enrolled
                                                      └→ rejected
```

**Requirements:**
- Email verification is mandatory before document upload
- Applicant cannot enroll in classes until status is `accepted`
- OAuth signup via Google supported (skips email verification)
- Rate limit: 5 registration attempts per IP per hour

---

## 5.3 Login & Session

**Route:** `GET|POST /login`

- Email + password
- "Remember me" (30-day cookie)
- Password reset via emailed signed link (60-min expiry)
- Rate limit: 5 failed attempts per email+IP, then 60s lockout, escalating
- Session invalidated on password change
- 2FA — **Phase 3**

---

## 5.4 Student Dashboard

**Route:** `GET /dashboard` *(auth, role:student)*

**Widgets:**

| Widget | Content |
|---|---|
| Current term summary | Term name, weeks remaining, registration window status |
| My classes | Subject, teacher, schedule, room, next session |
| Fees | Outstanding balance, next due date, **Pay Now** button |
| Grades | Current-term grades per class, cumulative GPA |
| Attendance | % per class, warning if below threshold (default 75%) |
| Announcements | Latest 5, filtered by audience |
| Documents | Enrollment letter (PDF), request transcript |

**Requirements:**
- All data scoped to `auth()->user()->student` — enforced via Policy, never by query param
- Dashboard cached 60s per student, invalidated on enrollment/payment/grade write

---

## 5.5 Course Catalog & Enrollment

**Route:** `GET /courses` · `POST /enroll/{class}` · `DELETE /enroll/{enrollment}`

**Catalog view:**
- Lists `ClassSection` records for the active `Term`
- Filterable: department, subject, day of week, teacher, seats available
- Each card shows: subject code + title, credits, teacher, schedule, room, `enrolled_count / capacity`

**Enrollment rules (enforced server-side in `EnrollmentService`):**

1. **Registration window** — `now()` between `term.reg_open` and `term.reg_close`
2. **Student status** — must be `accepted` or `enrolled`, not `suspended`
3. **Prerequisites** — every subject in `subject.prerequisites[]` must have a passing grade in the student's history
4. **Seat availability** — `enrolled_count < capacity` (locked via `SELECT ... FOR UPDATE` inside a transaction)
5. **Schedule conflict** — no overlapping timeslot with an existing enrollment this term
6. **Credit cap** — total enrolled credits ≤ `program.max_credits_per_term` (default 21)
7. **No duplicates** — student cannot enroll in two sections of the same subject in one term
8. **Outstanding balance** — if `invoice.status = overdue` from a prior term, block enrollment

**On successful enrollment:**
- `Enrollment` created with status `enrolled`
- `class_section.enrolled_count` incremented (atomically)
- Line item added to student's term `Invoice` (created if none exists)
- Confirmation email queued

**Drop rules:**
- Allowed only during registration window → full refund, invoice line item removed
- After window closes but before 50% of term elapsed → status `withdrawn`, 50% refund, `W` on transcript
- After 50% of term → drop denied, must petition registrar

---

## 5.6 Payments

**Routes:**
`GET /billing` · `POST /billing/pay` · `POST /webhooks/{gateway}`

### Gateways

| Gateway | Package | Methods | Currency | Webhook Event |
|---|---|---|---|---|
| **Stripe** | `laravel/cashier` | Card, Apple/Google Pay | BDT, USD | `payment_intent.succeeded`, `.payment_failed`, `charge.refunded` |
| **bKash** | Custom HTTP client | Mobile wallet | BDT | Execute-payment callback + status poll |
| **PayPal** | `srmklive/paypal` | Balance, card | USD | `PAYMENT.CAPTURE.COMPLETED`, `.DENIED`, `.REFUNDED` |

### Gateway abstraction

```php
interface PaymentGateway
{
    public function createPayment(Invoice $invoice, array $options = []): PaymentSession;
    public function verifyWebhook(Request $request): bool;
    public function parseWebhook(Request $request): WebhookEvent;
    public function refund(Payment $payment, int $amountMinor): RefundResult;
}
```

Implementations: `StripeGateway`, `BkashGateway`, `PayPalGateway`. Resolved from a `PaymentGatewayManager` by `$gateway` string.

### Payment flow

1. Student clicks **Pay Now** → chooses gateway and amount (full or installment)
2. `PaymentController@pay` creates a `Payment` row with status `pending`, generates an **idempotency key** = `sha1(invoice_id . amount . student_id . date)`
3. Gateway returns redirect URL or client secret → student completes on gateway
4. Gateway fires webhook → `WebhookController` verifies signature → dispatches `ProcessPaymentWebhook` job
5. Job (inside DB transaction):
   - Finds `Payment` by `gateway_txn_id`
   - Updates status to `succeeded` / `failed`
   - Recalculates `Invoice.amount_paid` and `Invoice.status`
   - Fires `PaymentSucceeded` event → receipt email + PDF

### Hard rules

- ❌ **Never** mark an invoice paid based on a client-side redirect. Only a verified webhook mutates payment state.
- ❌ **Never** store raw card data. Tokenize at the gateway. Application is PCI SAQ-A.
- ✅ Every `Payment` stores `gateway`, `gateway_txn_id`, `raw_payload` (JSON, for audit)
- ✅ Webhook endpoints are idempotent — replaying the same event is a no-op
- ✅ Webhook signature verification failure → 401, log, alert
- ✅ Currency is fixed per `Program` at v1. No FX conversion.
- ✅ Reconciliation job runs nightly, comparing gateway ledger to local `payments` table, flags mismatches

### Installments

Admin defines an installment plan per program/term: `[{pct: 50, due: term.start}, {pct: 50, due: term.start + 45d}]`. Invoice is split into `InvoiceInstallment` rows. Overdue installment → student flagged, blocks next-term enrollment.

---

## 5.7 Admin Dashboard

**Route:** `GET /admin` *(auth, role:registrar|finance|admin)*

**Counters (cached 5 min, invalidated on relevant writes):**

- Total students — active / suspended / graduated / withdrawn
- Total teachers, total staff
- Classes running this term
- Subjects offered
- Term revenue: collected vs outstanding
- Applications pending review

**Charts:**
- Enrollment trend (last 8 terms)
- Revenue by gateway (current term)
- Class fill rate distribution
- Applications by status (funnel)

**Alert panel:**
- Students below attendance threshold
- Overdue invoices > 30 days
- Classes below minimum enrollment (candidates for cancellation)
- Teachers over workload cap

---

## 5.8 Student Management

**Routes:** `/admin/students/*`

- Full CRUD; list with search + filters (program, year, status, admission date, outstanding balance)
- Detail view tabs: Profile · Enrollments · Grades · Attendance · Invoices · Documents · Audit Log
- Bulk CSV import (`maatwebsite/excel`) with dry-run validation and per-row error report
- Status transitions with reason + actor logged
- Export to CSV / PDF

**Status machine:**
```
applicant → enrolled ⇄ suspended
                ├→ graduated
                └→ withdrawn
```
Suspended students: cannot enroll, can still view records and pay outstanding fees.

---

## 5.9 Teacher Management

**Routes:** `/admin/teachers/*`

- CRUD teacher records: employee number, department, hire date, employment type
- **Qualified subjects** — many-to-many. A teacher may only be assigned to a `ClassSection` whose subject is in their qualified list. This is a hard constraint at the DB and validation layer.
- **Workload view** — total teaching hours per term, against `max_hours_per_term` (default 18 contact hours/week)
- **Conflict detection** — teacher cannot be assigned two class sections with overlapping timeslots in the same term
- Teacher portal (`/teaching`): assigned classes, rosters, attendance marker, grade entry

---

## 5.10 Staff Management

**Routes:** `/admin/staff/*`

- CRUD staff, assign to `Department`
- Assign role (`registrar`, `finance`, `admin`)
- Deactivate → immediate session invalidation
- Every role change is written to the audit log

---

## 5.11 Subject Management

**Routes:** `/admin/subjects/*`

| Field | Notes |
|---|---|
| `code` | e.g. `CSE-101`, unique |
| `title` | |
| `credits` | integer, 1–6 |
| `department_id` | |
| `description` | |
| `prerequisites[]` | self-referencing many-to-many |
| `qualified_teachers[]` | many-to-many |
| `is_active` | soft-disable without deleting history |

**Prerequisite validation:** cycle detection on save (a subject cannot transitively require itself).

---

## 5.12 Class Management

**Routes:** `/admin/classes/*`

A **ClassSection** is: `Subject + Teacher + Term + Room + Schedule + Capacity`.

**Creation validation:**
1. Teacher must be in `subject.qualified_teachers`
2. Teacher must not have a timeslot conflict in this term
3. Room must not have a timeslot conflict in this term
4. Room capacity ≥ section capacity
5. Term must not be closed

**`ConflictDetector` service** runs all checks and returns a structured conflict list, surfaced inline in the form before save.

**Schedule format:** array of `{day: MON, start: "09:00", end: "10:30"}`. Stored as JSON. Overlap check = `(a.start < b.end) && (b.start < a.end)` on matching day.

**Roster view:** enrolled students, waitlist (Phase 2), export to CSV.

---

## 5.13 Academic Terms

**Routes:** `/admin/terms/*`

| Field | Notes |
|---|---|
| `name` | e.g. "Fall 2026" |
| `start_date`, `end_date` | |
| `reg_open`, `reg_close` | enrollment window |
| `is_active` | **exactly one** term active at a time — enforced by a DB partial unique index |
| `status` | `planning → registration → in_progress → grading → closed` |

Closing a term: locks enrollments, finalizes grades, rolls GPA, archives.

---

## 5.14 Attendance

**Teacher route:** `/teaching/classes/{class}/attendance`

- Teacher selects a session date → marks each enrolled student `present` / `absent` / `late` / `excused`
- Bulk "mark all present" then adjust exceptions
- Attendance is per `Enrollment` + `session_date` (unique composite)
- Editable for 7 days after session, then locked (registrar can override, logged)

**Student view:** attendance % per class, with a warning banner below the threshold.

**Admin:** at-risk report — all students below `attendance_threshold` (configurable, default 75%).

---

## 5.15 Grading

**Teacher route:** `/teaching/classes/{class}/grades`

- Teacher defines assessment components per class: `{name: "Midterm", weight: 30}`. Weights must sum to 100.
- Enter raw scores per student per component
- Final grade = `Σ (score/max × weight)`, mapped to a letter grade via a configurable scale

**Default grade scale:**

| Range | Letter | Points |
|---|---|---|
| 80–100 | A | 4.00 |
| 75–79 | A− | 3.75 |
| 70–74 | B+ | 3.50 |
| 65–69 | B | 3.00 |
| 60–64 | B− | 2.75 |
| 55–59 | C+ | 2.50 |
| 50–54 | C | 2.25 |
| 45–49 | D | 2.00 |
| < 45 | F | 0.00 |

- **Draft vs Published** — grades are invisible to students until the teacher publishes. Publishing is irreversible without registrar override.
- **GPA** = `Σ(grade_points × credits) / Σ(credits)`, computed by `GpaCalculator`, cached on `students.cgpa`, recomputed on grade publish.
- Every grade change after publish writes an audit row with old value, new value, actor, reason.

---

## 5.16 Finance

**Routes:** `/admin/finance/*`

- **Fee structure** — per program per term: tuition, lab fee, library fee, registration fee. Some are per-credit, some flat.
- **Invoice generation** — auto on enrollment; manual bulk generation per term
- **Invoice states:** `draft → issued → partially_paid → paid` / `overdue` / `cancelled`
- **Payment ledger** — every payment row, filterable by gateway, date, status
- **Refunds** — finance staff initiates → approval required → gateway refund API called → `Payment` marked `refunded`, credit note issued
- **Reports:** outstanding balance aging (0–30, 31–60, 61–90, 90+), revenue by gateway, revenue by program, refunds issued

---

## 5.17 Announcements

- Admin/registrar creates announcement: title, body (rich text), audience (`all` / `program:X` / `year:N` / `class:Y`), publish date, expiry
- Delivered to dashboard + optional email blast (queued, batched)

---

## 5.18 Reporting & Exports

| Report | Filters | Formats |
|---|---|---|
| Enrollment by program | term, status | CSV, PDF |
| Revenue by term | gateway, program | CSV, PDF |
| Teacher workload | term, department | CSV |
| At-risk students | threshold, term | CSV, PDF |
| Class fill rates | term | CSV |
| Payment reconciliation | date range, gateway | CSV |

Large exports run as queued jobs → email download link (signed URL, 24h expiry).

---

# 6. Data Model

```
users
  id, name, email, email_verified_at, password, phone, status, created_at

departments
  id, name, code, head_teacher_id

programs
  id, name, code, department_id, total_credits, duration_terms,
  max_credits_per_term, currency, is_active

students
  id, user_id, student_number (unique), program_id, admission_date,
  status, cgpa, total_credits_earned, national_id

teachers
  id, user_id, employee_number (unique), department_id,
  hire_date, employment_type, max_hours_per_term

staff
  id, user_id, employee_number, department_id, job_title

subjects
  id, code (unique), title, credits, department_id, description, is_active

subject_prerequisites        [pivot, self-ref]
  subject_id, prerequisite_subject_id

subject_teacher              [pivot]
  subject_id, teacher_id

terms
  id, name, start_date, end_date, reg_open, reg_close,
  status, is_active   -- partial unique index on is_active WHERE is_active = true

rooms
  id, building, number, capacity, type

class_sections
  id, subject_id, teacher_id, term_id, room_id, section_code,
  schedule (json), capacity, enrolled_count, status

enrollments                  ← THE JOIN. Everything hangs off this.
  id, student_id, class_section_id, term_id, status,
  final_score, letter_grade, grade_points, published_at, enrolled_at
  UNIQUE (student_id, class_section_id)

attendances
  id, enrollment_id, session_date, status, marked_by, marked_at
  UNIQUE (enrollment_id, session_date)

assessment_components
  id, class_section_id, name, max_score, weight

grades
  id, enrollment_id, assessment_component_id, score, entered_by, entered_at
  UNIQUE (enrollment_id, assessment_component_id)

fee_structures
  id, program_id, term_id, fee_type, amount_minor, is_per_credit

invoices
  id, student_id, term_id, invoice_number (unique), currency,
  subtotal_minor, amount_paid_minor, status, due_date, issued_at

invoice_items
  id, invoice_id, description, enrollment_id (nullable),
  fee_structure_id (nullable), amount_minor

invoice_installments
  id, invoice_id, sequence, amount_minor, due_date, status

payments
  id, invoice_id, gateway, gateway_txn_id (unique), idempotency_key (unique),
  amount_minor, currency, status, raw_payload (json), paid_at, refunded_at

announcements
  id, title, body, audience_type, audience_id, published_at, expires_at, created_by

audit_logs
  id, auditable_type, auditable_id, action, old_values (json),
  new_values (json), actor_id, ip, user_agent, created_at
```

### Modeling notes

- **All money is stored as integer minor units** (`amount_minor`, i.e. poisha/cents). Never floats.
- `enrolled_count` on `class_sections` is a denormalized counter — updated inside the enrollment transaction with a row lock, reconciled nightly.
- `enrollments` is the pivot everything references. Attendance, grades, and invoice items all point at `enrollment_id`, not at `student_id + class_id`.
- `terms.is_active` uses a partial unique index (Postgres) or a trigger (MySQL) to guarantee exactly one active term.

---

# 7. Technical Stack

## 7.1 Stack table

| Layer | Choice |
|---|---|
| Framework | **Laravel 11** (full-stack) |
| Views | **Blade + Livewire 3** |
| Interactivity | Alpine.js (Livewire dependency) |
| Styling | Tailwind CSS |
| Build | Vite |
| Database | **PostgreSQL 16** (partial indexes, JSONB, CTEs) — MySQL 8 acceptable fallback |
| ORM | Eloquent |
| Auth | Laravel Fortify |
| RBAC | `spatie/laravel-permission` |
| Admin console | **Filament v3** — gives you resource CRUD, tables, filters, forms, widgets out of the box |
| Payments | `laravel/cashier` (Stripe) · `srmklive/paypal` · custom `BkashGateway` |
| Queue | Redis + **Laravel Horizon** |
| Cache | Redis |
| Scheduler | Laravel Scheduler (cron) |
| Files | S3-compatible (Laravel Filesystem) |
| Email | Laravel Mail + Mailgun / SES |
| PDF | `barryvdh/laravel-dompdf` |
| Excel/CSV | `maatwebsite/excel` |
| Audit | `owen-it/laravel-auditing` |
| Search | Postgres full-text (`tsvector`), Meilisearch only if needed |
| Testing | **Pest** + Laravel factories |
| Static analysis | Larastan (level 6+) |
| Style | Laravel Pint |

> **Note on Filament:** Section 5.7–5.17 (the entire admin console) maps almost 1:1 onto Filament Resources, Widgets, and Actions. Adopting it removes an estimated 30–40% of admin build time. The student portal stays hand-built Livewire, because it needs custom UX.

## 7.2 Application structure

```
routes/
├── web.php              → public + student portal
├── teaching.php         → teacher portal
├── api.php              → webhooks (Stripe, PayPal, bKash)
└── console.php          → scheduled commands
   (Filament registers /admin routes automatically)

app/
├── Models/
│   ├── User.php  Student.php  Teacher.php  Staff.php
│   ├── Department.php  Program.php  Subject.php  Term.php  Room.php
│   ├── ClassSection.php  Enrollment.php
│   ├── Attendance.php  AssessmentComponent.php  Grade.php
│   ├── Invoice.php  InvoiceItem.php  InvoiceInstallment.php  Payment.php
│   └── Announcement.php
│
├── Services/
│   ├── Enrollment/
│   │   ├── EnrollmentService.php        (orchestrates the 8 rules)
│   │   ├── PrerequisiteChecker.php
│   │   ├── SeatAllocator.php            (row-locked capacity check)
│   │   └── ScheduleConflictDetector.php
│   ├── Payment/
│   │   ├── PaymentGateway.php           (interface)
│   │   ├── PaymentGatewayManager.php
│   │   ├── StripeGateway.php
│   │   ├── BkashGateway.php
│   │   ├── PayPalGateway.php
│   │   └── InvoiceReconciler.php
│   ├── Academic/
│   │   ├── GpaCalculator.php
│   │   ├── GradeScale.php
│   │   └── TermCloser.php
│   └── Scheduling/
│       └── ConflictDetector.php         (teacher + room)
│
├── Http/
│   ├── Controllers/
│   │   ├── Student/  DashboardController  CatalogController
│   │   │              EnrollmentController  BillingController
│   │   ├── Teaching/ ClassController  AttendanceController  GradeController
│   │   └── Webhook/  StripeWebhookController  PayPalWebhookController
│   │                 BkashWebhookController
│   └── Middleware/
│       ├── EnsureRegistrationWindowOpen.php
│       ├── EnsureStudentNotSuspended.php
│       └── VerifyWebhookSignature.php
│
├── Livewire/
│   ├── Student/  CourseCatalog  EnrollmentCart  PaymentCheckout
│   └── Teaching/ AttendanceMarker  GradeEntryTable
│
├── Filament/
│   ├── Resources/  StudentResource  TeacherResource  SubjectResource
│   │               ClassSectionResource  TermResource  InvoiceResource
│   └── Widgets/    StatsOverview  EnrollmentTrendChart  RevenueChart
│                   AtRiskStudentsTable
│
├── Policies/
│   └── StudentPolicy  EnrollmentPolicy  GradePolicy  InvoicePolicy
│
├── Jobs/
│   ├── ProcessPaymentWebhook.php
│   ├── GenerateTermInvoices.php
│   ├── SendAnnouncementEmails.php
│   ├── ReconcileGatewayLedger.php
│   └── RecalculateStudentGpa.php
│
├── Events/     EnrollmentCreated  PaymentSucceeded  GradePublished
├── Listeners/  SendEnrollmentConfirmation  IssueReceipt  UpdateGpa
└── Rules/      NoScheduleConflict  MeetsPrerequisites  WeightsSumTo100
```

## 7.3 Key implementation constraints

**Seat allocation — must be race-safe:**
```php
DB::transaction(function () use ($student, $section) {
    $section = ClassSection::lockForUpdate()->findOrFail($section->id);
    if ($section->enrolled_count >= $section->capacity) {
        throw new SectionFullException;
    }
    $enrollment = Enrollment::create([...]);
    $section->increment('enrolled_count');
    return $enrollment;
});
```

**Webhook idempotency:**
```php
Payment::where('gateway_txn_id', $event->txnId)
       ->where('status', 'pending')
       ->firstOr(fn () => throw new AlreadyProcessed);
```

**Money:** integer minor units everywhere. `Invoice::$subtotal_minor`. Cast via a `MoneyCast`. No `float`, no `decimal` arithmetic in PHP.

---

# 8. Non-Functional Requirements

| Area | Requirement |
|---|---|
| **Auth** | Argon2id password hashing; session cookies (`HttpOnly`, `Secure`, `SameSite=Lax`) |
| **Authorization** | Every controller action guarded by a Policy. Never trust route params for ownership. |
| **PCI** | SAQ-A. No card data ever touches the server. Tokenize at gateway. |
| **Transport** | HTTPS only, HSTS, TLS 1.2+ |
| **Web security** | CSRF on all state-changing forms; strict CSP; `X-Frame-Options: DENY` |
| **Rate limiting** | Login 5/min · Registration 5/hr/IP · Payment init 10/hr/user · Webhooks unlimited (signature-gated) |
| **Uploads** | Validate MIME + magic bytes; max 5MB; store off-webroot in S3; scan for malware |
| **Performance** | Dashboard < 2s @ 10k students · Catalog < 1s · No N+1 (Larastan + `Model::preventLazyLoading()` in dev) |
| **Availability** | 99.5%. Registration-week peak = 10× baseline; queue workers autoscale. |
| **Audit** | Every grade change, payment, refund, role change, status transition logged with actor, IP, before/after |
| **Backups** | Nightly full DB dump, 30-day retention, quarterly restore drill |
| **Data retention** | Student academic records: permanent. Payment payloads: 7 years. Session logs: 90 days. |
| **Accessibility** | WCAG 2.1 AA on the student portal |
| **Responsive** | Student portal: mobile-first. Admin console: desktop-first, tablet-usable. |
| **Localization** | Laravel `lang/` files; English + Bangla at launch. |
| **Timezone** | Store UTC, display in `Asia/Dhaka`. |

---

# 9. Open Decisions

| # | Question | Options | Recommendation |
|---|---|---|---|
| 1 | Currency scope | BDT only / BDT + USD | **BDT only** for v1. Fix currency per program. Adding a second currency triples payment testing. |
| 2 | Installments | Admin-fixed plans / student-chosen | **Admin-fixed.** Student-chosen creates a collections nightmare. |
| 3 | Grade visibility | Instant on entry / held until published | **Held until published.** Teachers need to enter drafts without student panic. |
| 4 | Drop refunds | Full inside window, then 0 / graduated scale | **Graduated:** 100% during registration, 50% before week 8, 0% after. Bind to `Term` config, not code. |
| 5 | Attendance granularity | Per session / per week | **Per session.** Weekly aggregation is a view, not a storage decision. |
| 6 | Waitlists | Yes / no in v1 | **No.** Phase 2. Adds queueing + notification complexity. |
| 7 | Filament for admin | Adopt / hand-build | **Adopt.** Saves ~35% of admin build. Escape hatch exists for custom pages. |
| 8 | Postgres vs MySQL | | **Postgres.** Partial unique index on `terms.is_active` and JSONB schedule queries are meaningfully cleaner. |

---

# 10. Phasing

## Phase 1 — MVP *(target: 10–12 weeks)*

**Scope:** get a student from landing page to enrolled and paid.

- Auth, roles, permissions
- Landing page + programs
- Signup → application → acceptance flow
- Student dashboard
- Course catalog + enrollment (all 8 rules)
- Invoicing + **Stripe** + **bKash**
- Admin: students, teachers, subjects, classes, terms (Filament CRUD)
- Admin dashboard counters
- Audit logging
- Webhooks + reconciliation job

**Exit criteria:** 50 test students enroll in 5 classes and pay via both gateways with zero manual reconciliation.

## Phase 2 — Academic operations *(6–8 weeks)*

- Attendance (teacher marking, student view, at-risk report)
- Assessment components + grade entry + publish
- GPA calculation and transcripts
- **PayPal** gateway
- Announcements
- Reporting suite + exports
- Waitlists
- Teacher workload dashboard

## Phase 3 — Hardening & extension *(ongoing)*

- 2FA
- Refund workflow with approval chain
- Transcript generation (official, signed PDF)
- Advanced analytics
- Bangla localization pass
- Public API for third-party integrations
- Native mobile app (React Native / Flutter, consuming a new `/api/v1`)

---

# 11. Risks

| Risk | Impact | Mitigation |
|---|---|---|
| Registration-week traffic spike overwhelms seat allocation | High — double-booked seats | Row-level locking + load test at 10× before go-live |
| bKash API instability / poor docs | High — 70% of local payments | Build gateway abstraction first; Stripe as fallback; nightly reconciliation catches drift |
| Webhook missed or delayed | Medium — student paid, invoice unpaid | Status-poll job every 15min for `pending` payments > 10min old |
| Prerequisite data entry errors block enrollment | Medium | Dry-run prerequisite validator; registrar override with logged reason |
| Grade entry mistakes after publish | Medium | Immutable-by-default; override requires reason + registrar approval; full audit trail |
| Scope creep into library/hostel modules | High — timeline | Explicit non-goals in §3. Change requests go to Phase 3 backlog. |

---

# 12. Appendix — Glossary

- **Term** — an academic semester with a defined start, end, and registration window.
- **Subject** — a course in the catalog (`CSE-101 Introduction to Programming`). Abstract.
- **ClassSection** — a concrete offering of a Subject in a Term, taught by a Teacher at a time and place.
- **Enrollment** — a Student's membership in a ClassSection. The central join record.
- **Credit** — a unit of academic weight; determines fees and GPA contribution.
- **Minor units** — the smallest indivisible currency unit (1 BDT = 100 poisha). All money is stored this way.