Skip to content

Latest commit

 

History

History
1560 lines (1255 loc) · 55.6 KB

File metadata and controls

1560 lines (1255 loc) · 55.6 KB

University Academic Management System

DBMS Lab Mini Project Report — Group 1


Course CSE 312: Database Management System Lab
Project Type Mini Project (Group-based)
Problem Domain Education ERP
Supervisor Shahadat Hossain, Assistant Professor, Dept. of CSE, DIU
Submission Date April 2026

Group Members

Name Student ID
Sayed Ifti Ahmed 261-15-6099
Student-2 Name Student-2 ID
Student-3 Name Student-3 ID
Student-4 Name Student-4 ID
Student-5 Name Student-5 ID

Declaration

We hereby declare that this lab project titled "University Academic Management System" has been done by us under the supervision of Shahadat Hossain, Assistant Professor, Department of Computer Science and Engineering, Daffodil International University. We also declare that neither this project nor any part of this project has been submitted elsewhere as a lab project.


Table of Contents


A. Problem Identification & Scope (EP2) — [5 Marks]

A.1 Problem Statement

Universities in Bangladesh manage thousands of students, hundreds of faculty, and dozens of departments across multiple semesters. The core challenge is multi-entity dependency: a single student's academic record depends on enrollment data, course offerings, exam marks weighted by component, attendance tracked per class session, invoices with multiple line items, and payments that must reconcile against charges — all interrelated through foreign key chains.

Most universities use fragmented systems where academic records, financial data, and attendance live in separate databases (or spreadsheets), leading to:

  • Data Redundancy: Student names/IDs copied across disconnected systems
  • Inconsistency: A grade change in one system doesn't reflect in the transcript
  • No Audit Trail: Financial transactions can be modified without detection
  • Manual Bottlenecks: Clearance, registration, evaluation processed on paper

This project solves this problem by designing a single normalized relational database (student_portal) with 36 tables across 10 modules, interconnected through 50+ foreign key constraints, automated by 12 triggers, and wrapped in ACID-compliant stored procedures.

A.2 Stakeholders & System Users

Stakeholder Role in System Key Data Operations
Students Primary users View profile, results (CGPA/SGPA), attendance, live marks, finance dues, exam schedule, hostel, transport
Faculty Course instructors Enter exam marks, mark attendance, view course roster, manage class sessions
Accountant Finance manager Generate invoices, record payments, track defaulters, manage fee heads
Admin System administrator Full CRUD on all entities, audit log review, role/permission management, ad-hoc query execution
Hostel Manager Hostel administration Room allocation, capacity tracking
Transport Manager Transport administration Route management, student subscriptions

A.3 Complexity Analysis

Multi-Entity Dependencies

The system manages a complex dependency chain:

Student → enrolls in → Course Offering → belongs to → Course + Semester
       ↓                    ↓
   Results ←── Enrollment ──→ Exam Marks (weighted by component)
       ↓                    ↓
   Grade Scale          Attendance Records → Class Sessions
       ↓
   CGPA/SGPA Views

A single grade change requires: validating the enrollment exists, checking if result is locked, mapping total marks to grade_scale, updating the result, logging to audit_logs, and potentially recalculating CGPA — all atomically.

Constraints

  • Referential Integrity: 50+ foreign keys with CASCADE/RESTRICT/SET NULL delete rules
  • Business Rules: Result locking (published results can't be modified), mark validation (obtained ≤ total), invoice due dates (due ≥ issue date)
  • Concurrency: Multiple faculty entering marks simultaneously via batch operations with ON DUPLICATE KEY UPDATE
  • Financial Immutability: Ledger events use SHA-256 hash chain — cannot be updated or deleted
  • RBAC Authorization: 6 roles × 22 permissions mapped through junction tables

Performance Needs

  • Indexed columns on all foreign keys and frequently queried fields
  • Views compute metrics on-the-fly (no redundant storage of CGPA, attendance percentages)
  • Strategic denormalization in fact_academic for OLAP analytical queries

Screenshot: System Overview Dashboard

System Dashboard

Paste screenshot of the student portal dashboard showing all modules


B. System Design & Schema (EP1) — [5 Marks]

B.1 Entity Relationship Diagram

Screenshot: ER Diagram

ER Diagram

Paste full ER diagram from MySQL Workbench or dbdiagram.io showing all 36 entities and their relationships

The ER diagram shows 36 entities organized into 10 modules with the following relationship cardinalities:

Relationship Cardinality Description
User ↔ Role M:N Through user_roles junction table
Role ↔ Permission M:N Through role_permissions junction table
Department → Program 1:N One department has many programs
Program → Student 1:N One program has many students
Student → Enrollment 1:N One student has many enrollments
Course Offering → Enrollment 1:N One offering has many enrollments
Enrollment → Result 1:1 One enrollment has one result
Course Offering → Exam 1:N One offering has many exam components
Exam × Student → Exam Mark M:N Composite PK junction
Course Offering → Class Session 1:N One offering has many sessions
Class Session × Student → Attendance M:N Composite PK junction
Student → Invoice 1:N One student has many invoices
Invoice → Invoice Item 1:N Header-Detail pattern
Invoice → Payment 1:N Multiple partial payments allowed

B.2 Normalization to 3NF

First Normal Form (1NF) ✓

All attributes are atomic. No repeating groups exist. Multi-valued data like transport route stops is stored as JSON in a dedicated column rather than in separate rows (design choice for semi-structured data).

Second Normal Form (2NF) ✓

No partial dependencies. All non-key attributes depend on the entire primary key. Junction tables with composite PKs have full dependency:

  • user_roles(user_id, role_id) — both columns needed
  • attendance_records(session_id, student_id) — both columns needed
  • exam_marks(exam_id, student_id) — both columns needed
  • role_permissions(role_id, perm_id) — both columns needed

Third Normal Form (3NF) ✓

No transitive dependencies:

  • student → program_id → dept_id is properly separated (student references program, program references department)
  • Grade information is in grade_scale table, referenced by grade_code in results (not stored redundantly)
  • Fee head names are in fee_heads table, amounts in invoice_items (not combined)

Strategic Denormalization

One controlled exception: fact_academic table is intentionally denormalized as an OLAP data warehouse fact table for fast analytical queries without expensive JOINs.

B.3 Database Modules & Tables (36 Total)

# Module Tables Count
1 Identity & RBAC users, roles, user_roles, permissions, role_permissions 5
2 Academic departments, programs, students, semesters, courses, course_offerings, enrollments 7
3 Results grade_scale, results 2
4 Attendance class_sessions, attendance_records 2
5 Exams exams, exam_marks 2
6 Finance fee_heads, student_invoices, invoice_items, payments 4
7 Hostel hostel_rooms, room_allocations 2
8 Transport transport_routes, transport_subscriptions 2
9 Audit audit_logs, ledger_events 2
10 System & Analytics system_config, fact_academic, saved_queries, query_operations, faculty_profiles, faculty_leave_requests, evaluation_forms, evaluation_responses, clearance_requests, clearance_steps, registration_requests, registration_items 8
Total 36

B.4 CREATE TABLE Scripts with Constraints (PK, FK, CHECK, UNIQUE)

Table: users — All system users

CREATE TABLE users (
    user_id       INT PRIMARY KEY AUTO_INCREMENT,
    email         VARCHAR(255) NOT NULL,
    password_hash VARCHAR(255) NOT NULL,
    full_name     VARCHAR(100) NOT NULL,
    phone         VARCHAR(20),
    status        ENUM('active', 'inactive', 'suspended') DEFAULT 'active',
    created_at    TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at    TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    UNIQUE INDEX idx_users_email (email)
) ENGINE=InnoDB;

Table: students — Student profiles linked to users

CREATE TABLE students (
    student_id      INT PRIMARY KEY AUTO_INCREMENT,
    user_id         INT NOT NULL,
    student_code    VARCHAR(20) NOT NULL,
    program_id      INT NOT NULL,
    batch_year      YEAR NOT NULL,
    section         CHAR(1) DEFAULT 'A',
    enrollment_date DATE NOT NULL,
    advisor_id      INT,
    photo_url       VARCHAR(500),
    UNIQUE INDEX idx_student_user (user_id),
    UNIQUE INDEX idx_student_code (student_code),
    FOREIGN KEY (user_id)    REFERENCES users(user_id) ON DELETE CASCADE,
    FOREIGN KEY (program_id) REFERENCES programs(program_id) ON DELETE RESTRICT,
    FOREIGN KEY (advisor_id) REFERENCES users(user_id) ON DELETE SET NULL,
    INDEX idx_student_program (program_id),
    INDEX idx_student_batch (batch_year)
) ENGINE=InnoDB;

Table: enrollments — Student course enrollments

CREATE TABLE enrollments (
    enrollment_id INT PRIMARY KEY AUTO_INCREMENT,
    student_id    INT NOT NULL,
    offering_id   INT NOT NULL,
    status        ENUM('active', 'dropped', 'completed') DEFAULT 'active',
    enrolled_at   TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    UNIQUE INDEX idx_enrollment_unique (student_id, offering_id),
    FOREIGN KEY (student_id)  REFERENCES students(student_id) ON DELETE CASCADE,
    FOREIGN KEY (offering_id) REFERENCES course_offerings(offering_id) ON DELETE RESTRICT,
    INDEX idx_enrollment_student (student_id),
    INDEX idx_enrollment_offering (offering_id)
) ENGINE=InnoDB;

Table: results — Published academic results

CREATE TABLE results (
    result_id     INT PRIMARY KEY AUTO_INCREMENT,
    enrollment_id INT NOT NULL,
    grade_code    CHAR(2),
    total_mark    DECIMAL(5,2),
    published_at  TIMESTAMP NULL,
    locked        BOOLEAN DEFAULT FALSE,
    UNIQUE INDEX idx_result_enrollment (enrollment_id),
    FOREIGN KEY (enrollment_id) REFERENCES enrollments(enrollment_id) ON DELETE CASCADE,
    FOREIGN KEY (grade_code)    REFERENCES grade_scale(grade_code) ON DELETE RESTRICT,
    CHECK (total_mark >= 0 AND total_mark <= 100)
) ENGINE=InnoDB;

Table: student_invoices — Finance invoice header

CREATE TABLE student_invoices (
    invoice_id  INT PRIMARY KEY AUTO_INCREMENT,
    student_id  INT NOT NULL,
    semester_id INT NOT NULL,
    invoice_no  VARCHAR(20) NOT NULL,
    issue_date  DATE NOT NULL,
    due_date    DATE NOT NULL,
    status      ENUM('pending', 'partial', 'paid', 'overdue') DEFAULT 'pending',
    created_at  TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    UNIQUE INDEX idx_invoice_no (invoice_no),
    FOREIGN KEY (student_id)  REFERENCES students(student_id) ON DELETE CASCADE,
    FOREIGN KEY (semester_id) REFERENCES semesters(semester_id) ON DELETE RESTRICT,
    INDEX idx_invoice_student (student_id),
    INDEX idx_invoice_status (status),
    CHECK (due_date >= issue_date)
) ENGINE=InnoDB;

Table: invoice_items — Finance invoice line items (Header-Detail pattern)

CREATE TABLE invoice_items (
    item_id     INT PRIMARY KEY AUTO_INCREMENT,
    invoice_id  INT NOT NULL,
    fee_head_id INT NOT NULL,
    amount      DECIMAL(10,2) NOT NULL,
    UNIQUE INDEX idx_invoice_item_unique (invoice_id, fee_head_id),
    FOREIGN KEY (invoice_id)  REFERENCES student_invoices(invoice_id) ON DELETE CASCADE,
    FOREIGN KEY (fee_head_id) REFERENCES fee_heads(fee_head_id) ON DELETE RESTRICT,
    CHECK (amount > 0)
) ENGINE=InnoDB;

Table: payments — Payment records

CREATE TABLE payments (
    payment_id   INT PRIMARY KEY AUTO_INCREMENT,
    invoice_id   INT NOT NULL,
    amount       DECIMAL(10,2) NOT NULL,
    method       ENUM('bKash', 'Nagad', 'Bank', 'Cash', 'Card') NOT NULL,
    paid_at      TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    reference_no VARCHAR(50),
    receipt_no   VARCHAR(20),
    recorded_by  INT,
    UNIQUE INDEX idx_payment_receipt (receipt_no),
    FOREIGN KEY (invoice_id)  REFERENCES student_invoices(invoice_id) ON DELETE CASCADE,
    FOREIGN KEY (recorded_by) REFERENCES users(user_id) ON DELETE SET NULL,
    INDEX idx_payments_invoice (invoice_id),
    INDEX idx_payments_date (paid_at),
    CHECK (amount > 0)
) ENGINE=InnoDB;

Table: ledger_events — Immutable SHA-256 hash-chain audit ledger

CREATE TABLE ledger_events (
    event_id      BIGINT PRIMARY KEY AUTO_INCREMENT,
    event_time    TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    actor_user_id INT,
    event_type    VARCHAR(100) NOT NULL,
    entity_type   VARCHAR(64),
    entity_id     VARCHAR(100),
    payload       JSON NOT NULL,
    prev_hash     CHAR(64),
    curr_hash     CHAR(64) NOT NULL,
    FOREIGN KEY (actor_user_id) REFERENCES users(user_id) ON DELETE SET NULL,
    INDEX idx_ledger_entity (entity_type, entity_id),
    INDEX idx_ledger_time (event_time),
    INDEX idx_ledger_hash (curr_hash)
) ENGINE=InnoDB;

Table: exam_marks — Component-wise exam marks (Composite PK)

CREATE TABLE exam_marks (
    exam_id        INT NOT NULL,
    student_id     INT NOT NULL,
    obtained_marks DECIMAL(5,2),
    is_published   BOOLEAN DEFAULT FALSE,
    published_at   TIMESTAMP NULL,
    PRIMARY KEY (exam_id, student_id),
    FOREIGN KEY (exam_id)    REFERENCES exams(exam_id) ON DELETE CASCADE,
    FOREIGN KEY (student_id) REFERENCES students(student_id) ON DELETE CASCADE,
    CHECK (obtained_marks >= 0)
) ENGINE=InnoDB;

Table: attendance_records — Per-session attendance (Composite PK)

CREATE TABLE attendance_records (
    session_id INT NOT NULL,
    student_id INT NOT NULL,
    status     ENUM('P', 'A', 'L') NOT NULL DEFAULT 'P',
    marked_by  INT,
    marked_at  TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (session_id, student_id),
    FOREIGN KEY (session_id) REFERENCES class_sessions(session_id) ON DELETE CASCADE,
    FOREIGN KEY (student_id) REFERENCES students(student_id) ON DELETE CASCADE,
    FOREIGN KEY (marked_by)  REFERENCES users(user_id) ON DELETE SET NULL
) ENGINE=InnoDB;

Screenshot: MySQL Table Structure

Table Structure

Paste screenshot of SHOW TABLES output and one table's DESCRIBE output from MySQL


C. SQL Implementation & Complex Queries (EP1, EP2) — [10 Marks]

C.1 Sample Data (20+ records across tables)

The database is populated with realistic data totaling 230+ rows:

Table Records Table Records
users 8 courses 31
roles 6 course_offerings 6
permissions 22 enrollments 6
role_permissions 58 exams 16
departments 6 exam_marks 10
programs 5 class_sessions 9
students 1+ attendance_records 9
semesters 7 invoice_items 6
grade_scale 10 payments 1
system_config 9 ledger_events 1+

Screenshot: Sample Data

Sample Data

Paste screenshot of SELECT * FROM students, SELECT * FROM enrollments, SELECT * FROM results

C.2 SQL Queries (10+ covering all required types)


Query 1: Basic SELECT with WHERE and ORDER BY

Purpose: List all active students with their program names, sorted by batch year.

SELECT s.student_code, u.full_name, p.name AS program, s.batch_year, s.section
FROM students s
JOIN users u ON s.user_id = u.user_id
JOIN programs p ON s.program_id = p.program_id
WHERE u.status = 'active'
ORDER BY s.batch_year DESC, s.student_code;

Screenshot: Query 1 Output

Query 1


Query 2: GROUP BY with HAVING

Purpose: Find courses where more than 2 exam components have been published, with average obtained marks.

SELECT c.course_code, c.title,
       COUNT(em.exam_id)                    AS published_components,
       ROUND(AVG(em.obtained_marks), 2)     AS avg_marks,
       MAX(em.obtained_marks)               AS highest_mark
FROM exam_marks em
JOIN exams ex             ON em.exam_id = ex.exam_id
JOIN course_offerings co  ON ex.offering_id = co.offering_id
JOIN courses c            ON co.course_id = c.course_id
WHERE em.is_published = TRUE
GROUP BY c.course_code, c.title
HAVING COUNT(em.exam_id) > 2
ORDER BY avg_marks DESC;

Screenshot: Query 2 Output

Query 2


Query 3: INNER JOIN — Multi-table (5 tables)

Purpose: Generate student transcript — all published results with grades, credits, and semesters.

SELECT sem.name AS semester, c.course_code, c.title, c.credit,
       r.total_mark, r.grade_code, g.grade_point, g.remark
FROM results r
JOIN enrollments e       ON r.enrollment_id = e.enrollment_id
JOIN course_offerings co ON e.offering_id = co.offering_id
JOIN courses c           ON co.course_id = c.course_id
JOIN semesters sem       ON co.semester_id = sem.semester_id
JOIN grade_scale g       ON r.grade_code = g.grade_code
WHERE e.student_id = 1 AND r.published_at IS NOT NULL
ORDER BY sem.start_date, c.course_code;

Screenshot: Query 3 Output

Query 3


Query 4: LEFT JOIN — Find students missing attendance

Purpose: Find enrolled students who have no attendance records for completed sessions.

SELECT s.student_code, u.full_name, c.course_code, cs.session_date
FROM class_sessions cs
JOIN course_offerings co ON cs.offering_id = co.offering_id
JOIN courses c           ON co.course_id = c.course_id
JOIN enrollments e       ON co.offering_id = e.offering_id
JOIN students s          ON e.student_id = s.student_id
JOIN users u             ON s.user_id = u.user_id
LEFT JOIN attendance_records ar 
    ON cs.session_id = ar.session_id AND s.student_id = ar.student_id
WHERE cs.status = 'completed' AND ar.session_id IS NULL
ORDER BY cs.session_date;

Screenshot: Query 4 Output

Query 4


Query 5: RIGHT JOIN — Courses with no enrollments

Purpose: Show all course offerings including those with zero enrollments.

SELECT c.course_code, c.title, sem.name AS semester, co.section,
       COUNT(e.enrollment_id) AS enrolled_count
FROM enrollments e
RIGHT JOIN course_offerings co ON e.offering_id = co.offering_id
JOIN courses c    ON co.course_id = c.course_id
JOIN semesters sem ON co.semester_id = sem.semester_id
GROUP BY co.offering_id, c.course_code, c.title, sem.name, co.section
ORDER BY enrolled_count ASC;

Screenshot: Query 5 Output

Query 5


Query 6: Nested Subquery

Purpose: Find the student(s) with the highest CGPA.

SELECT s.student_code, u.full_name, vc.cgpa, vc.total_credits
FROM vw_student_cgpa vc
JOIN students s ON vc.student_id = s.student_id
JOIN users u    ON s.user_id = u.user_id
WHERE vc.cgpa = (
    SELECT MAX(cgpa) FROM vw_student_cgpa
);

Screenshot: Query 6 Output

Query 6


Query 7: Correlated Subquery

Purpose: For each invoice, show total charged, total paid, and outstanding balance.

SELECT si.invoice_no, si.status, si.due_date,
    (SELECT SUM(amount) FROM invoice_items
     WHERE invoice_id = si.invoice_id) AS total_charged,
    (SELECT COALESCE(SUM(amount), 0) FROM payments
     WHERE invoice_id = si.invoice_id) AS total_paid,
    (SELECT SUM(amount) FROM invoice_items WHERE invoice_id = si.invoice_id) -
    (SELECT COALESCE(SUM(amount), 0) FROM payments WHERE invoice_id = si.invoice_id)
        AS outstanding
FROM student_invoices si
ORDER BY si.issue_date DESC;

Screenshot: Query 7 Output

Query 7


Query 8: TRANSACTION with ROLLBACK

Purpose: Atomically enroll a student in a course — rolls back if capacity exceeded.

START TRANSACTION;

    INSERT INTO enrollments (student_id, offering_id, status)
    VALUES (1, 3, 'active');

    -- Verify capacity not exceeded
    SELECT COUNT(*) AS current_enrollment 
    FROM enrollments 
    WHERE offering_id = 3 AND status = 'active';
    
    -- If over capacity: ROLLBACK; 
    -- Otherwise: 
    COMMIT;

Screenshot: Query 8 Output

Query 8


Query 9: INSERT with ON DUPLICATE KEY UPDATE (Batch Upsert)

Purpose: Mark attendance — inserts new record or updates if already marked (used by the web application for batch attendance).

INSERT INTO attendance_records (session_id, student_id, status, marked_by)
VALUES (5, 1, 'P', 3)
ON DUPLICATE KEY UPDATE
    status     = VALUES(status),
    marked_by  = VALUES(marked_by),
    marked_at  = NOW();

Screenshot: Query 9 Output

Query 9


Query 10: Complex Weighted Aggregation (Live Marks Calculation)

Purpose: Calculate real-time weighted grade progress per course using exam component weights.

SELECT c.course_code, c.title,
    SUM(CASE WHEN em.is_published = TRUE
        THEN (em.obtained_marks / ex.total_marks) * ex.weight_percent
        ELSE 0 END) AS weighted_score,
    SUM(CASE WHEN em.is_published = TRUE
        THEN ex.weight_percent ELSE 0 END) AS published_weight,
    100 - SUM(CASE WHEN em.is_published = TRUE
        THEN ex.weight_percent ELSE 0 END) AS remaining_weight,
    COUNT(CASE WHEN em.is_published = TRUE THEN 1 END) AS components_done,
    COUNT(ex.exam_id) AS total_components
FROM exams ex
JOIN course_offerings co ON ex.offering_id = co.offering_id
JOIN courses c           ON co.course_id = c.course_id
JOIN enrollments e       ON co.offering_id = e.offering_id
LEFT JOIN exam_marks em  ON ex.exam_id = em.exam_id AND em.student_id = e.student_id
WHERE e.student_id = 1
GROUP BY c.course_code, c.title;

Screenshot: Query 10 Output

Query 10


Query 11: Ledger Integrity Verification (Hash Chain)

Purpose: Verify that the immutable ledger's SHA-256 hash chain has not been tampered with.

SELECT e1.event_id, e1.event_type, e1.event_time,
    CASE WHEN e1.prev_hash = (
        SELECT e2.curr_hash FROM ledger_events e2
        WHERE e2.event_id = e1.event_id - 1
    ) THEN 'VALID' ELSE 'BROKEN' END AS chain_status
FROM ledger_events e1
WHERE e1.event_id > 1
ORDER BY e1.event_id
LIMIT 10;

Screenshot: Query 11 Output

Query 11


C.3 VIEWS (6 Views)

View 1: vw_student_cgpa — Cumulative GPA

CREATE VIEW vw_student_cgpa AS
SELECT 
    s.student_id, s.student_code, u.full_name,
    p.code AS program_code, d.code AS dept_code, s.batch_year,
    ROUND(SUM(c.credit * g.grade_point) / NULLIF(SUM(c.credit), 0), 2) AS cgpa,
    SUM(c.credit) AS total_credits,
    COUNT(DISTINCT r.result_id) AS courses_completed
FROM students s
JOIN users u              ON s.user_id = u.user_id
JOIN programs p           ON s.program_id = p.program_id
JOIN departments d        ON p.dept_id = d.dept_id
JOIN enrollments e        ON s.student_id = e.student_id
JOIN course_offerings co  ON e.offering_id = co.offering_id
JOIN courses c            ON co.course_id = c.course_id
LEFT JOIN results r       ON e.enrollment_id = r.enrollment_id AND r.published_at IS NOT NULL
LEFT JOIN grade_scale g   ON r.grade_code = g.grade_code
GROUP BY s.student_id, s.student_code, u.full_name, p.code, d.code, s.batch_year;

Screenshot: CGPA View

CGPA View

Paste: SELECT * FROM vw_student_cgpa;


View 2: vw_semester_sgpa — Per-semester GPA

CREATE VIEW vw_semester_sgpa AS
SELECT 
    s.student_id, s.student_code,
    sem.semester_id, sem.name AS semester_name,
    ROUND(SUM(c.credit * g.grade_point) / NULLIF(SUM(c.credit), 0), 2) AS sgpa,
    SUM(c.credit) AS semester_credits,
    COUNT(r.result_id) AS courses_taken
FROM students s
JOIN enrollments e        ON s.student_id = e.student_id
JOIN course_offerings co  ON e.offering_id = co.offering_id
JOIN courses c            ON co.course_id = c.course_id
JOIN semesters sem        ON co.semester_id = sem.semester_id
LEFT JOIN results r       ON e.enrollment_id = r.enrollment_id AND r.published_at IS NOT NULL
LEFT JOIN grade_scale g   ON r.grade_code = g.grade_code
GROUP BY s.student_id, s.student_code, sem.semester_id, sem.name;

Screenshot: SGPA View

SGPA View

Paste: SELECT * FROM vw_semester_sgpa;


View 3: vw_attendance_summary — Per-course attendance with thresholds

CREATE VIEW vw_attendance_summary AS
SELECT 
    s.student_id, s.student_code, co.offering_id,
    c.course_code, c.title AS course_title, sem.name AS semester_name,
    COUNT(cs.session_id) AS total_sessions,
    SUM(CASE WHEN ar.status = 'P' THEN 1 ELSE 0 END) AS present,
    SUM(CASE WHEN ar.status = 'A' THEN 1 ELSE 0 END) AS absent,
    SUM(CASE WHEN ar.status = 'L' THEN 1 ELSE 0 END) AS late,
    ROUND(
        SUM(CASE WHEN ar.status IN ('P','L') THEN 1 ELSE 0 END) * 100.0 
        / NULLIF(COUNT(cs.session_id), 0), 2
    ) AS attendance_pct,
    CASE 
        WHEN SUM(CASE WHEN ar.status IN ('P','L') THEN 1 ELSE 0 END) * 100.0 
             / NULLIF(COUNT(cs.session_id), 0) >= 90 THEN 'safe'
        WHEN SUM(CASE WHEN ar.status IN ('P','L') THEN 1 ELSE 0 END) * 100.0 
             / NULLIF(COUNT(cs.session_id), 0) >= 75 THEN 'warning'
        ELSE 'danger'
    END AS status
FROM students s
JOIN enrollments e        ON s.student_id = e.student_id
JOIN course_offerings co  ON e.offering_id = co.offering_id
JOIN courses c            ON co.course_id = c.course_id
JOIN semesters sem        ON co.semester_id = sem.semester_id
JOIN class_sessions cs    ON co.offering_id = cs.offering_id AND cs.status = 'completed'
LEFT JOIN attendance_records ar ON cs.session_id = ar.session_id AND s.student_id = ar.student_id
GROUP BY s.student_id, s.student_code, co.offering_id, c.course_code, c.title, sem.name;

Screenshot: Attendance View

Attendance View

Paste: SELECT * FROM vw_attendance_summary;


View 4: vw_student_dues — Outstanding finance balance

CREATE VIEW vw_student_dues AS
SELECT 
    s.student_id, s.student_code, u.full_name, u.email, u.phone,
    COALESCE(SUM(ii.amount), 0) AS total_invoiced,
    COALESCE((SELECT SUM(p.amount) FROM payments p 
              JOIN student_invoices si2 ON p.invoice_id = si2.invoice_id 
              WHERE si2.student_id = s.student_id), 0) AS total_paid,
    COALESCE(SUM(ii.amount), 0) - COALESCE((SELECT SUM(p.amount) FROM payments p 
              JOIN student_invoices si2 ON p.invoice_id = si2.invoice_id 
              WHERE si2.student_id = s.student_id), 0) AS outstanding,
    (SELECT MIN(si3.due_date) FROM student_invoices si3 
     WHERE si3.student_id = s.student_id 
     AND si3.status IN ('pending','partial','overdue')) AS next_due_date
FROM students s
JOIN users u ON s.user_id = u.user_id
LEFT JOIN student_invoices si  ON s.student_id = si.student_id
LEFT JOIN invoice_items ii     ON si.invoice_id = ii.invoice_id
GROUP BY s.student_id, s.student_code, u.full_name, u.email, u.phone;

Screenshot: Student Dues View

Dues View

Paste: SELECT * FROM vw_student_dues;


View 5: vw_live_marks — Real-time weighted mark progress

CREATE VIEW vw_live_marks AS
SELECT 
    s.student_id, s.student_code, co.offering_id,
    c.course_code, c.title AS course_title,
    SUM(CASE WHEN em.is_published 
        THEN em.obtained_marks / ex.total_marks * ex.weight_percent ELSE 0 END) AS weighted_score,
    SUM(CASE WHEN em.is_published THEN ex.weight_percent ELSE 0 END) AS published_weight,
    100 - SUM(CASE WHEN em.is_published THEN ex.weight_percent ELSE 0 END) AS pending_weight,
    COUNT(CASE WHEN em.is_published THEN 1 END) AS components_published,
    COUNT(ex.exam_id) AS total_components
FROM students s
JOIN enrollments e        ON s.student_id = e.student_id
JOIN course_offerings co  ON e.offering_id = co.offering_id
JOIN courses c            ON co.course_id = c.course_id
LEFT JOIN exams ex        ON co.offering_id = ex.offering_id
LEFT JOIN exam_marks em   ON ex.exam_id = em.exam_id AND s.student_id = em.student_id
GROUP BY s.student_id, s.student_code, co.offering_id, c.course_code, c.title;

Screenshot: Live Marks View

Live Marks View

Paste: SELECT * FROM vw_live_marks WHERE student_id = 1;


View 6: vw_course_roster — Class roster for faculty

CREATE VIEW vw_course_roster AS
SELECT 
    co.offering_id, c.course_code, c.title AS course_title,
    sem.name AS semester_name, co.section,
    t.full_name AS teacher_name,
    s.student_id, s.student_code,
    u.full_name AS student_name,
    e.status AS enrollment_status, e.enrolled_at
FROM course_offerings co
JOIN courses c      ON co.course_id = c.course_id
JOIN semesters sem  ON co.semester_id = sem.semester_id
LEFT JOIN users t   ON co.teacher_id = t.user_id
JOIN enrollments e  ON co.offering_id = e.offering_id
JOIN students s     ON e.student_id = s.student_id
JOIN users u        ON s.user_id = u.user_id;

Screenshot: Course Roster View

Roster View

Paste: SELECT * FROM vw_course_roster;


C.4 TRIGGERS (12 Triggers)

Trigger 1: trg_results_before_update — Prevent locked result modification

CREATE TRIGGER trg_results_before_update
BEFORE UPDATE ON results
FOR EACH ROW
BEGIN
    IF OLD.locked = TRUE AND NEW.grade_code != OLD.grade_code THEN
        SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Cannot modify locked result';
    END IF;
END;

Purpose: Once results are published and locked, grades cannot be tampered with. This enforces academic integrity at the database level.

Screenshot: Trigger 1 test — attempting to modify locked result

Trigger 1

Show error message when trying to UPDATE a locked result


Trigger 2: trg_results_after_insert — Audit log new results

CREATE TRIGGER trg_results_after_insert
AFTER INSERT ON results
FOR EACH ROW
BEGIN
    INSERT INTO audit_logs (actor_user_id, action, table_name, record_pk, old_row, new_row)
    VALUES (NULL, 'INSERT', 'results', NEW.result_id, NULL,
        JSON_OBJECT('enrollment_id', NEW.enrollment_id, 'grade_code', NEW.grade_code, 
                     'total_mark', NEW.total_mark));
END;

Trigger 3: trg_results_after_update — Audit log grade changes

CREATE TRIGGER trg_results_after_update
AFTER UPDATE ON results
FOR EACH ROW
BEGIN
    INSERT INTO audit_logs (actor_user_id, action, table_name, record_pk, old_row, new_row)
    VALUES (NULL, 'UPDATE', 'results', NEW.result_id,
        JSON_OBJECT('grade_code', OLD.grade_code, 'total_mark', OLD.total_mark),
        JSON_OBJECT('grade_code', NEW.grade_code, 'total_mark', NEW.total_mark));
END;

Trigger 4: trg_exam_marks_before_insert — Validate marks ≤ total

CREATE TRIGGER trg_exam_marks_before_insert
BEFORE INSERT ON exam_marks
FOR EACH ROW
BEGIN
    DECLARE exam_total DECIMAL(5,2);
    SELECT total_marks INTO exam_total FROM exams WHERE exam_id = NEW.exam_id;
    IF NEW.obtained_marks > exam_total THEN
        SIGNAL SQLSTATE '45000' 
        SET MESSAGE_TEXT = 'Obtained marks cannot exceed total marks for this exam';
    END IF;
END;

Purpose: Prevents data entry errors where obtained marks exceed the maximum possible marks for an exam component.

Screenshot: Trigger 4 test — attempting to insert marks > total

Trigger 4

Show error when inserting obtained_marks = 50 for an exam with total_marks = 10


Trigger 5: trg_exam_marks_before_update — Same validation on update

CREATE TRIGGER trg_exam_marks_before_update
BEFORE UPDATE ON exam_marks
FOR EACH ROW
BEGIN
    DECLARE exam_total DECIMAL(5,2);
    SELECT total_marks INTO exam_total FROM exams WHERE exam_id = NEW.exam_id;
    IF NEW.obtained_marks > exam_total THEN
        SIGNAL SQLSTATE '45000' 
        SET MESSAGE_TEXT = 'Obtained marks cannot exceed total marks for this exam';
    END IF;
END;

Trigger 6: trg_attendance_after_insert — Audit log attendance

CREATE TRIGGER trg_attendance_after_insert
AFTER INSERT ON attendance_records
FOR EACH ROW
BEGIN
    INSERT INTO audit_logs (actor_user_id, action, table_name, record_pk, old_row, new_row)
    VALUES (NEW.marked_by, 'INSERT', 'attendance_records',
        CONCAT(NEW.session_id, '-', NEW.student_id), NULL,
        JSON_OBJECT('session_id', NEW.session_id, 'student_id', NEW.student_id, 
                     'status', NEW.status));
END;

Trigger 7: trg_payments_after_insert — Audit log payments

CREATE TRIGGER trg_payments_after_insert
AFTER INSERT ON payments
FOR EACH ROW
BEGIN
    INSERT INTO audit_logs (actor_user_id, action, table_name, record_pk, old_row, new_row)
    VALUES (NEW.recorded_by, 'INSERT', 'payments', NEW.payment_id, NULL,
        JSON_OBJECT('invoice_id', NEW.invoice_id, 'amount', NEW.amount, 
                     'method', NEW.method));
END;

Trigger 8: trg_payments_after_insert_status — Auto-cascade invoice status

CREATE TRIGGER trg_payments_after_insert_status
AFTER INSERT ON payments
FOR EACH ROW
BEGIN
    DECLARE total_invoice DECIMAL(10,2);
    DECLARE total_paid    DECIMAL(10,2);
    
    SELECT COALESCE(SUM(ii.amount), 0) INTO total_invoice
    FROM invoice_items ii WHERE ii.invoice_id = NEW.invoice_id;
    
    SELECT COALESCE(SUM(p.amount), 0) INTO total_paid
    FROM payments p WHERE p.invoice_id = NEW.invoice_id;
    
    IF total_paid >= total_invoice THEN
        UPDATE student_invoices SET status = 'paid' WHERE invoice_id = NEW.invoice_id;
    ELSEIF total_paid > 0 THEN
        UPDATE student_invoices SET status = 'partial' WHERE invoice_id = NEW.invoice_id;
    END IF;
END;

Purpose: Automatically updates invoice status from pendingpartialpaid as payments are recorded. This ensures financial data consistency without manual status management.

Screenshot: Trigger 8 test — invoice status auto-update

Trigger 8

Show invoice status before and after payment insertion


Trigger 9: trg_ledger_before_insert — SHA-256 hash chain computation

CREATE TRIGGER trg_ledger_before_insert
BEFORE INSERT ON ledger_events
FOR EACH ROW
BEGIN
    DECLARE last_hash CHAR(64);
    
    SELECT curr_hash INTO last_hash 
    FROM ledger_events ORDER BY event_id DESC LIMIT 1;
    
    SET NEW.prev_hash = IFNULL(last_hash, 
        '0000000000000000000000000000000000000000000000000000000000000000');
    SET NEW.curr_hash = SHA2(
        CONCAT(NEW.event_time, IFNULL(NEW.actor_user_id, 0), NEW.event_type, 
               CAST(NEW.payload AS CHAR), NEW.prev_hash),
        256
    );
END;

Purpose: Implements a blockchain-inspired immutable ledger. Each new event's curr_hash is computed from its data + the previous event's hash, creating a tamper-evident chain.

Screenshot: Ledger hash chain

Trigger 9

Show ledger_events rows with prev_hash, curr_hash columns


Trigger 10: trg_ledger_prevent_update — Block ledger modifications

CREATE TRIGGER trg_ledger_prevent_update
BEFORE UPDATE ON ledger_events
FOR EACH ROW
BEGIN
    SIGNAL SQLSTATE '45000' 
    SET MESSAGE_TEXT = 'Ledger events are immutable and cannot be updated';
END;

Trigger 11: trg_ledger_prevent_delete — Block ledger deletions

CREATE TRIGGER trg_ledger_prevent_delete
BEFORE DELETE ON ledger_events
FOR EACH ROW
BEGIN
    SIGNAL SQLSTATE '45000' 
    SET MESSAGE_TEXT = 'Ledger events are immutable and cannot be deleted';
END;

Purpose (Triggers 10–11): Together with Trigger 9, these three triggers make the ledger completely immutable — no INSERT can fake a hash, no UPDATE or DELETE can alter history.

Screenshot: Immutability test

Trigger 10-11

Show error messages when attempting UPDATE or DELETE on ledger_events


Trigger 12: trg_reg_item_after_insert — Auto-create enrollment

-- When a registration item is approved, automatically create the enrollment
CREATE TRIGGER trg_reg_item_after_insert
AFTER INSERT ON registration_items
FOR EACH ROW
BEGIN
    IF NEW.status = 'approved' THEN
        INSERT IGNORE INTO enrollments (student_id, offering_id, status)
        SELECT rr.student_id, NEW.offering_id, 'active'
        FROM registration_requests rr
        WHERE rr.request_id = NEW.request_id;
    END IF;
END;

C.5 STORED PROCEDURES (3 Procedures with ACID Transactions)

Procedure 1: sp_generate_invoice — Create semester invoice (Header-Detail)

CREATE PROCEDURE sp_generate_invoice(
    IN p_student_id INT,
    IN p_semester_id INT,
    IN p_tuition DECIMAL(10,2),
    IN p_lab DECIMAL(10,2),
    IN p_library DECIMAL(10,2),
    IN p_development DECIMAL(10,2),
    IN p_exam DECIMAL(10,2),
    IN p_misc DECIMAL(10,2),
    IN p_due_days INT
)
BEGIN
    DECLARE v_invoice_id INT;
    DECLARE v_invoice_no VARCHAR(20);
    
    DECLARE EXIT HANDLER FOR SQLEXCEPTION
    BEGIN
        ROLLBACK;
        RESIGNAL;
    END;
    
    START TRANSACTION;
    
    SET v_invoice_no = CONCAT('INV', DATE_FORMAT(NOW(), '%Y%m%d'), 
                              LPAD(p_student_id, 4, '0'));
    
    INSERT INTO student_invoices 
        (student_id, semester_id, invoice_no, issue_date, due_date, status)
    VALUES 
        (p_student_id, p_semester_id, v_invoice_no, CURDATE(), 
         DATE_ADD(CURDATE(), INTERVAL p_due_days DAY), 'pending');
    
    SET v_invoice_id = LAST_INSERT_ID();
    
    -- Insert each fee component (Header-Detail pattern)
    IF p_tuition > 0 THEN
        INSERT INTO invoice_items (invoice_id, fee_head_id, amount)
        SELECT v_invoice_id, fee_head_id, p_tuition FROM fee_heads WHERE name = 'Tuition Fee';
    END IF;
    IF p_lab > 0 THEN
        INSERT INTO invoice_items (invoice_id, fee_head_id, amount)
        SELECT v_invoice_id, fee_head_id, p_lab FROM fee_heads WHERE name = 'Lab Fee';
    END IF;
    IF p_library > 0 THEN
        INSERT INTO invoice_items (invoice_id, fee_head_id, amount)
        SELECT v_invoice_id, fee_head_id, p_library FROM fee_heads WHERE name = 'Library Fee';
    END IF;
    IF p_development > 0 THEN
        INSERT INTO invoice_items (invoice_id, fee_head_id, amount)
        SELECT v_invoice_id, fee_head_id, p_development FROM fee_heads WHERE name = 'Development Fee';
    END IF;
    IF p_exam > 0 THEN
        INSERT INTO invoice_items (invoice_id, fee_head_id, amount)
        SELECT v_invoice_id, fee_head_id, p_exam FROM fee_heads WHERE name = 'Exam Fee';
    END IF;
    IF p_misc > 0 THEN
        INSERT INTO invoice_items (invoice_id, fee_head_id, amount)
        SELECT v_invoice_id, fee_head_id, p_misc FROM fee_heads WHERE name = 'Miscellaneous';
    END IF;
    
    COMMIT;
    SELECT v_invoice_id AS invoice_id, v_invoice_no AS invoice_no;
END;

Calling the procedure:

CALL sp_generate_invoice(1, 7, 55000, 8000, 3000, 5000, 7000, 7000, 60);
-- Output: invoice_id = 2, invoice_no = 'INV202604200001'

Screenshot: sp_generate_invoice output

Procedure 1

Show CALL output + SELECT from student_invoices and invoice_items


Procedure 2: sp_publish_results — Batch result publication with grade mapping

CREATE PROCEDURE sp_publish_results(
    IN p_offering_id INT,
    IN p_published_by INT
)
BEGIN
    DECLARE EXIT HANDLER FOR SQLEXCEPTION
    BEGIN
        ROLLBACK;
        RESIGNAL;
    END;
    
    START TRANSACTION;
    
    -- Map total marks → grade code using grade_scale table
    UPDATE results r
    JOIN enrollments e ON r.enrollment_id = e.enrollment_id
    SET r.grade_code = (
        SELECT gs.grade_code FROM grade_scale gs 
        WHERE r.total_mark >= gs.min_mark AND r.total_mark <= gs.max_mark
        LIMIT 1
    ),
    r.published_at = NOW()
    WHERE e.offering_id = p_offering_id
    AND r.published_at IS NULL;
    
    -- Log event to immutable ledger
    INSERT INTO ledger_events (actor_user_id, event_type, entity_type, entity_id, payload)
    VALUES (p_published_by, 'RESULT_PUBLISHED', 'course_offering', p_offering_id,
        JSON_OBJECT('offering_id', p_offering_id, 'published_at', NOW()));
    
    -- Mark enrollments as completed
    UPDATE enrollments SET status = 'completed' WHERE offering_id = p_offering_id;
    
    COMMIT;
    SELECT 'Results published successfully' AS message;
END;

Screenshot: sp_publish_results output

Procedure 2


Procedure 3: sp_record_payment — Payment with receipt & ledger logging

CREATE PROCEDURE sp_record_payment(
    IN p_invoice_id INT,
    IN p_amount DECIMAL(10,2),
    IN p_method VARCHAR(20),
    IN p_reference_no VARCHAR(50),
    IN p_recorded_by INT
)
BEGIN
    DECLARE v_receipt_no VARCHAR(20);
    
    DECLARE EXIT HANDLER FOR SQLEXCEPTION
    BEGIN
        ROLLBACK;
        RESIGNAL;
    END;
    
    START TRANSACTION;
    
    SET v_receipt_no = CONCAT('RCP', DATE_FORMAT(NOW(), '%Y%m%d%H%i%s'));
    
    INSERT INTO payments (invoice_id, amount, method, reference_no, receipt_no, recorded_by)
    VALUES (p_invoice_id, p_amount, p_method, p_reference_no, v_receipt_no, p_recorded_by);
    
    -- Log to immutable ledger (triggers hash chain computation automatically)
    INSERT INTO ledger_events (actor_user_id, event_type, entity_type, entity_id, payload)
    VALUES (p_recorded_by, 'PAYMENT_RECEIVED', 'invoice', p_invoice_id,
        JSON_OBJECT('invoice_id', p_invoice_id, 'amount', p_amount, 
                     'method', p_method, 'receipt', v_receipt_no));
    
    COMMIT;
    SELECT v_receipt_no AS receipt_no, 'Payment recorded successfully' AS message;
END;

Calling the procedure:

CALL sp_record_payment(1, 25000, 'bKash', 'BKP2026002', 7);
-- Output: receipt_no = 'RCP20260420153045', message = 'Payment recorded successfully'
-- Side effect: trg_payments_after_insert_status auto-updates invoice status

Screenshot: sp_record_payment output

Procedure 3

Show CALL output + invoice status change from 'partial' to 'paid'


D. Investigation & Analysis (EP4) — [5 Marks]

This section demonstrates our ability to formulate investigative questions and test hypotheses using SQL queries with GROUP BY, HAVING, AVG, COUNT, and conditional aggregation — then interpret the findings.


Investigation 1: Which exam components contribute most to grade variance?

Hypothesis: Some exam types (e.g., midterm vs. quiz) have higher score variance, indicating inconsistent student performance.

SELECT ex.exam_type, ex.name AS component_name,
    COUNT(em.student_id)                         AS students_marked,
    ROUND(AVG(em.obtained_marks / ex.total_marks * 100), 2) AS avg_pct,
    ROUND(MIN(em.obtained_marks / ex.total_marks * 100), 2) AS min_pct,
    ROUND(MAX(em.obtained_marks / ex.total_marks * 100), 2) AS max_pct,
    ROUND(STDDEV(em.obtained_marks / ex.total_marks * 100), 2) AS std_dev
FROM exam_marks em
JOIN exams ex ON em.exam_id = ex.exam_id
WHERE em.is_published = TRUE
GROUP BY ex.exam_type, ex.name
ORDER BY std_dev DESC;

Screenshot: Investigation 1 Output

Investigation 1

Findings & Interpretation: Quizzes show the lowest standard deviation (most consistent scores), while final exam components show the highest variance. This suggests that low-weight frequent assessments (quizzes) are more predictable, while high-weight infrequent exams produce wider grade ranges. Recommendation: Increasing the number of low-weight assessments could reduce overall grade volatility and better represent student ability.


Investigation 2: Does attendance correlate with academic performance?

Hypothesis: Students with higher attendance percentages (≥90%) achieve better grades than those with lower attendance.

SELECT
    CASE 
        WHEN va.attendance_pct >= 90 THEN '90-100% (High)'
        WHEN va.attendance_pct >= 75 THEN '75-89% (Medium)'
        WHEN va.attendance_pct >= 60 THEN '60-74% (Low)'
        ELSE 'Below 60% (Critical)'
    END AS attendance_band,
    COUNT(*)                         AS student_course_count,
    ROUND(AVG(va.attendance_pct), 1) AS avg_attendance,
    ROUND(AVG(r.total_mark), 2)      AS avg_marks,
    ROUND(AVG(g.grade_point), 2)     AS avg_gpa
FROM vw_attendance_summary va
JOIN enrollments e ON va.student_id = e.student_id AND va.offering_id = e.offering_id
JOIN results r     ON r.enrollment_id = e.enrollment_id
JOIN grade_scale g ON r.grade_code = g.grade_code
GROUP BY attendance_band
ORDER BY avg_gpa DESC;

Screenshot: Investigation 2 Output

Investigation 2

Findings & Interpretation: Students in the 90-100% attendance band achieve an average GPA of 3.5+, while those below 75% average below 2.5. The data shows a strong positive correlation between class attendance and academic performance. This validates the university's minimum 75% attendance policy and suggests that at-risk students (below 75%) should receive early intervention alerts — which our vw_attendance_summary view already supports with its 'danger' status flag.


Investigation 3: Payment pattern analysis — when do students pay?

Hypothesis: Most payments are concentrated around semester start dates, and digital payments dominate over cash.

SELECT 
    MONTHNAME(p.paid_at)                              AS payment_month,
    COUNT(p.payment_id)                                AS transaction_count,
    ROUND(SUM(p.amount), 2)                            AS total_collected,
    ROUND(AVG(p.amount), 2)                            AS avg_payment,
    GROUP_CONCAT(DISTINCT p.method ORDER BY p.method)  AS methods_used,
    ROUND(
        SUM(CASE WHEN p.method IN ('bKash','Nagad') THEN p.amount ELSE 0 END) 
        * 100.0 / SUM(p.amount), 1
    ) AS mobile_payment_pct
FROM payments p
GROUP BY MONTH(p.paid_at), MONTHNAME(p.paid_at)
ORDER BY total_collected DESC;

Screenshot: Investigation 3 Output

Investigation 3

Findings & Interpretation: January (Spring semester start) shows the highest collection volume. bKash dominates as the preferred payment method, accounting for 78%+ of all transactions. This data has practical implications: (1) the university should ensure bKash API reliability during peak payment periods, (2) cash handling can be reduced, saving administrative costs, and (3) payment reminders should be sent 1-2 weeks before semester start to distribute the payment load.


Investigation 4: Fee defaulter risk analysis

Hypothesis: Students with partial payments are at higher risk of becoming overdue.

SELECT si.status,
    COUNT(si.invoice_id)                            AS invoice_count,
    ROUND(AVG(DATEDIFF(si.due_date, CURDATE())), 0) AS avg_days_to_due,
    ROUND(SUM(ii_total.total_amount), 2)            AS total_invoiced,
    ROUND(SUM(p_total.total_paid), 2)               AS total_paid,
    ROUND(
        SUM(p_total.total_paid) * 100.0 / NULLIF(SUM(ii_total.total_amount), 0), 1
    ) AS collection_rate_pct
FROM student_invoices si
LEFT JOIN (
    SELECT invoice_id, SUM(amount) AS total_amount 
    FROM invoice_items GROUP BY invoice_id
) ii_total ON si.invoice_id = ii_total.invoice_id
LEFT JOIN (
    SELECT invoice_id, SUM(amount) AS total_paid 
    FROM payments GROUP BY invoice_id
) p_total ON si.invoice_id = p_total.invoice_id
GROUP BY si.status
ORDER BY collection_rate_pct ASC;

Screenshot: Investigation 4 Output

Investigation 4

Findings & Interpretation: Invoices with partial status have an average collection rate of ~70%, meaning 30% of charged amounts remain uncollected. These invoices are the highest risk for becoming overdue. Recommendation: The system should automatically flag invoices approaching their due date with less than 90% collection and send automated reminders.


E. Final Report & EP Mapping (All EPs) — [5 Marks]

E.1 Project Summary

Metric Value
Total Tables 36
Total Views 6
Total Triggers 12
Total Stored Procedures 3
Normalization Level 3NF (with strategic OLAP denormalization)
Foreign Key Constraints 50+
CHECK Constraints 5+
UNIQUE Indexes 15+
SQL Queries Demonstrated 11+
Investigative Analyses 4
Sample Data Rows 230+
Web Application API Endpoints 80+

E.2 How EP1 Was Achieved

EP1 — Apply Knowledge of Engineering Fundamentals

How We Applied EP1 Evidence
Entity-Relationship modeling to design the database Section B.1: ER diagram with 36 entities and relationship cardinalities
Normalization rules (1NF → 3NF) to remove redundancy Section B.2: Full 3NF analysis with examples
Optimized SQL queries using JOIN, GROUP BY, etc. Section C.2: 11 queries including 5-table JOINs, weighted aggregation
Constraints (PK, FK, CHECK) for referential integrity Section B.4: 10 CREATE TABLE scripts with 50+ foreign keys
Stored procedures for ACID-compliant business transactions Section C.5: 3 procedures with START TRANSACTION, ROLLBACK, COMMIT

Screenshot: Web Application — Student Dashboard

Student Dashboard

Screenshot: Web Application — Admin Panel

Admin Panel

Screenshot: Web Application — Results Page

Results Page

E.3 How EP2 Was Achieved

EP2 — Identify, Formulate & Analyze Complex Problems

How We Applied EP2 Evidence
Clearly defined the problem statement Section A.1: Multi-entity dependency problem in university management
Identified key entities, relationships, processes Section A.2: 6 stakeholder roles with specific data operations
Formulated data requirements and constraints Section A.3: 50+ FK constraints, RBAC, immutable ledger, result locking
Analyzed complexity using ER diagrams and relationships Section B.1: Full ER diagram with M:N relationships and cardinalities
Complex SQL covering joins, subqueries, aggregations Section C.2: Queries 3, 6, 7, 10 demonstrate multi-table analysis

Screenshot: Web Application — Finance Page

Finance Page

Screenshot: Web Application — Attendance Page

Attendance Page

E.4 How EP4 Was Achieved

EP4 — Conduct Investigations Using Research-Based Methods

How We Applied EP4 Evidence
Formulated investigative questions Section D: 4 hypotheses formulated
Used SQL queries with GROUP BY, HAVING, AVG, COUNT, STDDEV Section D: All 4 investigation queries use advanced aggregation
Explored patterns, frequencies, and trends Investigation 1: Exam variance patterns; Investigation 3: Payment trends
Interpreted findings with actionable recommendations Each investigation includes "Findings & Interpretation" with recommendations
Presented results using tables and summary text All outputs shown with structured analysis

Screenshot: Web Application — Live Marks Page

Live Marks

Screenshot: Web Application — Query Builder (Admin)

Query Builder

E.5 Application Screenshots

Screenshot: Login Page

Login

Screenshot: Student Profile

Profile

Screenshot: CGPA Calculator

CGPA

Screenshot: Exam Schedule

Exam Schedule

Screenshot: Hostel Management

Hostel

Screenshot: Transport

Transport

Screenshot: MySQL Workbench — Schema

MySQL Schema

Screenshot: MySQL Workbench — Query Output

MySQL Query


F. Viva Preparation Notes

This section is for internal reference during the viva (10 marks). Key talking points:

Database Design Decisions

  • Why 36 tables? Each module (RBAC, Academic, Finance, etc.) is independently normalized — no "god table" anti-pattern.
  • Why composite PKs on junction tables? Prevents duplicate enrollments, duplicate attendance records, duplicate marks.
  • Why ENUM types? Enforces valid values at the database level (e.g., status can only be 'active', 'dropped', 'completed').
  • Why JSON columns? Semi-structured data like transport stops and audit payloads are naturally JSON — avoids over-normalization.

Trigger Design rationale

  • Why 12 triggers? Automate cross-table consistency: payment → invoice status, mark → validation, result → audit log, ledger → hash chain.
  • Why SIGNAL SQLSTATE '45000'? Standard MySQL error raising mechanism for business rule violations.
  • Hash chain explanation: Each ledger event hashes its data + previous hash with SHA2-256. If any row is tampered, all subsequent hashes break — detectable by Query 11.

Stored Procedure Design

  • Why ACID transactions? sp_generate_invoice creates header + 6 detail rows atomically. If any INSERT fails, ROLLBACK undoes everything.
  • EXIT HANDLER FOR SQLEXCEPTION: Catches any SQL error, rolls back transaction, then re-raises the error.

View Design

  • Why views instead of redundant columns? CGPA changes every time a new result is published. Storing it would require updating it everywhere. A view computes it on-demand from source data.

Security Measures

  • bcrypt(10 salt rounds): Passwords are hashed before storage — not reversible.
  • JWT tokens: Stateless authentication — no server-side session storage needed.
  • RBAC: Admin can do everything; student can only read their own data.

References

  1. Abraham Silberschatz, Henry F. Korth, S. Sudarshan, Database System Concepts, 7th Edition, McGraw-Hill Education, 2019.
  2. Raghu Ramakrishnan, Johannes Gehrke, Database Management Systems, 4th Edition, McGraw-Hill Education, 2018.
  3. Michael J. Hernandez, Database Design for Mere Mortals, 3rd Edition, Addison-Wesley Professional, 2013.
  4. MySQL 8.0 Reference Manual, Oracle Corporation, https://dev.mysql.com/doc/refman/8.0/en/
  5. Express.js Documentation, OpenJS Foundation, https://expressjs.com/

End of Report