# **New Architecture Plan: Academic Result Module Revamp**

**Version:** 1.0
**Date:** November 15, 2025
**Purpose:** Redesign the Academic Result Module to be scalable, maintainable, and simple
**Status:** Proposal

---

## Table of Contents

1. [Executive Summary](#1-executive-summary)
2. [Current System Pain Points](#2-current-system-pain-points)
3. [Proposed Architecture Overview](#3-proposed-architecture-overview)
4. [Database Schema Design](#4-database-schema-design)
5. [MongoDB Schema Unification](#5-mongodb-schema-unification)
6. [Calculation Engine](#6-calculation-engine)
7. [Moderation System Redesign](#7-moderation-system-redesign)
8. [API & Controller Architecture](#8-api--controller-architecture)
9. [Migration Strategy](#9-migration-strategy)
10. [Implementation Phases](#10-implementation-phases)

---

## 1. Executive Summary

### 1.1. Vision

Transform the Academic Result Module from a complex, hardcoded system into a **configuration-driven, scalable platform** that can handle any board, class, or result type through simple database configuration rather than code changes.

### 1.2. Core Principles

1. **Single Source of Truth**: One unified schema for all data storage
2. **Configuration over Code**: All business logic driven by database configuration
3. **Extensibility**: Easy to add new boards, subjects, or calculation rules
4. **Maintainability**: Reduce code duplication and complexity
5. **Backward Compatibility**: Seamless migration from existing data

### 1.3. Key Benefits

- ✅ **80% reduction** in controller/helper code complexity
- ✅ **Zero code changes** needed for new subjects or calculation rules
- ✅ **Unified schema** eliminates normalized vs denormalized confusion
- ✅ **Dynamic calculation engine** replaces hardcoded formulas
- ✅ **Scalable moderation** system works for any configuration

---

## 2. Current System Pain Points

### 2.1. Schema Inconsistency

**Problem:**
- Two different MongoDB schemas (normalized vs denormalized) based on class/result type
- Complex logic to determine which schema to use
- Field naming varies (`marks_1` vs `eng_marks1`)
- Difficult to query across different result types

**Impact:** High maintenance cost, prone to bugs, steep learning curve

### 2.2. Hardcoded Calculation Logic

**Problem:**
- 6 different calculation methods (`calculateCals1`, `calculateCals2`, `calculateCalsFor9th10thCBSE`, etc.)
- Board/class/subject specific logic scattered across methods
- Adding new boards requires code changes
- Calculation formulas embedded in PHP code

**Impact:** Cannot add new boards without developer involvement, difficult to audit calculations

### 2.3. Rigid Moderation System

**Problem:**
- Subject mappings hardcoded in MarksModerationHelper
- Different mappings for different class groups
- Moderation rules not configurable
- Percentage thresholds fixed in code

**Impact:** Cannot customize moderation rules per school/board

### 2.4. Dual-Write Complexity

**Problem:**
- Complex logic to sync periodic test marks to Half Yearly/Final
- `perodic_test_field_name` configuration hard to understand
- Special cases for different boards (CE board percentage calculation)
- No clear audit trail of what got copied where

**Impact:** Data sync issues, difficult to debug

### 2.5. Code Duplication

**Problem:**
- Multiple controller files per session (2324, 2425, 2526)
- Similar save methods replicated across sessions
- View files duplicated for different scenarios

**Impact:** Bug fixes need to be applied to multiple files

---

## 3. Proposed Architecture Overview

### 3.1. Architectural Layers

```
┌─────────────────────────────────────────────────────┐
│              Presentation Layer                      │
│  (Unified Views - Single set for all scenarios)     │
└─────────────────────────────────────────────────────┘
                        ↓
┌─────────────────────────────────────────────────────┐
│              API/Controller Layer                    │
│  (Thin controllers - route to services)             │
└─────────────────────────────────────────────────────┘
                        ↓
┌─────────────────────────────────────────────────────┐
│              Service Layer                           │
│  • ResultService                                     │
│  • CalculationEngine                                 │
│  • ModerationService                                 │
│  • PeriodicTestSyncService                          │
└─────────────────────────────────────────────────────┘
                        ↓
┌─────────────────────────────────────────────────────┐
│              Configuration Layer                     │
│  (MySQL - defines structure, rules, mappings)       │
└─────────────────────────────────────────────────────┘
                        ↓
┌─────────────────────────────────────────────────────┐
│              Data Layer                              │
│  (MongoDB - stores actual marks using unified schema)│
└─────────────────────────────────────────────────────┘
```

### 3.2. Key Design Patterns

1. **Strategy Pattern**: Different calculation strategies based on configuration
2. **Repository Pattern**: Abstract data access layer
3. **Service Pattern**: Business logic encapsulation
4. **Factory Pattern**: Dynamic creation of calculation/moderation rules
5. **Observer Pattern**: Event-driven workflow (submission, approval)

---

## 4. Database Schema Design

### 4.1. New Master Tables (MySQL)

#### 4.1.1. `master_subjects`

Defines all possible subjects across all boards.

```sql
CREATE TABLE master_subjects (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    slug VARCHAR(100) NOT NULL UNIQUE,
    subject_type ENUM('scholastic', 'co_scholastic') DEFAULT 'scholastic',
    display_order INT DEFAULT 0,
    is_active BOOLEAN DEFAULT TRUE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

    INDEX idx_slug (slug),
    INDEX idx_type (subject_type),
    INDEX idx_active (is_active)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- Sample Data
INSERT INTO master_subjects (name, slug, subject_type, display_order) VALUES
-- Scholastic Subjects
('English', 'english', 'scholastic', 1),
('Punjabi', 'punjabi', 'scholastic', 2),
('Hindi', 'hindi', 'scholastic', 3),
('Mathematics', 'mathematics', 'scholastic', 4),
('Science', 'science', 'scholastic', 5),
('Social Science', 'social_science', 'scholastic', 6),
('EVS', 'evs', 'scholastic', 7),
('Computer Science', 'computer_science', 'scholastic', 8),
('Artificial Intelligence', 'ai', 'scholastic', 9),
('Divinity', 'divinity', 'scholastic', 10),
('UOI', 'uoi', 'scholastic', 16),
('ICT', 'ict', 'scholastic', 17),

-- Co-Scholastic Subjects
('Physical Education', 'physical_education', 'co_scholastic', 11),
('Art Education', 'art_education', 'co_scholastic', 12),
('Work Education', 'work_education', 'co_scholastic', 13),
('Music', 'music', 'co_scholastic', 14),
('Visual Arts', 'visual_arts', 'co_scholastic', 15),
('PSPE', 'pspe', 'co_scholastic', 18);
```

#### 4.1.2. `master_sub_titles`

Defines all possible components (subtitles) of marks entry.

```sql
CREATE TABLE master_sub_titles (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    slug VARCHAR(100) NOT NULL UNIQUE,
    description TEXT,
    display_order INT DEFAULT 0,
    is_active BOOLEAN DEFAULT TRUE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

    INDEX idx_slug (slug),
    INDEX idx_active (is_active)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- Sample Data
INSERT INTO master_sub_titles (name, slug, display_order) VALUES
('Theory Exam', 'theory', 1),
('Practical Exam', 'practical', 2),
('Internal Assessment', 'internal_assessment', 3),
('Multiple Assessment', 'multiple_assessment', 4),
('Subject Enrichment', 'subject_enrichment', 5),
('Portfolio', 'portfolio', 6),
('Notebook', 'notebook', 7),
('Periodic Test', 'periodic_test', 8),
('Periodic Test Average', 'periodic_test_average', 9),
('Term 1 Exam', 'term1_exam', 10),
('Term 2 Exam', 'term2_exam', 11),
('Best Two PT Average', 'best_two_pt_average', 12),
('Total Marks', 'total_marks', 13),
('Grand Total', 'grand_total', 14),
('Percentage', 'percentage', 15);
```

#### 4.1.3. `master_result_types`

Centralizes all result type definitions.

```sql
CREATE TABLE master_result_types (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    slug VARCHAR(100) NOT NULL,
    display_name VARCHAR(100) NOT NULL,
    class_from INT NOT NULL,
    class_to INT NOT NULL,
    board VARCHAR(50) NOT NULL,
    session VARCHAR(20) NOT NULL,
    display_order INT DEFAULT 0,
    is_periodic_test BOOLEAN DEFAULT FALSE,
    target_result_type_id INT NULL, -- For PT → Half Yearly/Final linking
    is_active BOOLEAN DEFAULT TRUE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

    INDEX idx_slug (slug),
    INDEX idx_board_class (board, class_from, class_to),
    INDEX idx_session (session),
    INDEX idx_active (is_active),
    FOREIGN KEY (target_result_type_id) REFERENCES master_result_types(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- Sample Data
INSERT INTO master_result_types (name, slug, display_name, class_from, class_to, board, session, is_periodic_test, target_result_type_id) VALUES
('Pre Mid Term', 'pre_mid_term', 'Pre Mid Term', 1, 10, 'CBSE', '2025-26', TRUE, NULL),
('Half Yearly', 'half_yearly', 'Half Yearly', 1, 10, 'CBSE', '2025-26', FALSE, NULL),
('Post Mid Term', 'post_mid_term', 'Post Mid Term', 1, 10, 'CBSE', '2025-26', TRUE, NULL),
('Final', 'final', 'Final', 1, 10, 'CBSE', '2025-26', FALSE, NULL),
('PT1', 'pt1', 'PT1', 9, 10, 'CBSE', '2025-26', TRUE, NULL),
('PT2', 'pt2', 'PT2', 9, 10, 'CBSE', '2025-26', TRUE, NULL),
('PT3', 'pt3', 'PT3', 9, 10, 'CBSE', '2025-26', TRUE, NULL);

-- Link PT → Half Yearly/Final
UPDATE master_result_types
SET target_result_type_id = (SELECT id FROM master_result_types WHERE slug = 'half_yearly' LIMIT 1)
WHERE slug IN ('pre_mid_term', 'pt1', 'pt2');

UPDATE master_result_types
SET target_result_type_id = (SELECT id FROM master_result_types WHERE slug = 'final' LIMIT 1)
WHERE slug IN ('post_mid_term', 'pt3');
```

### 4.2. Enhanced Configuration Tables

#### 4.2.1. `school_result_subjects` (Enhanced)

Links master subjects to specific class/board/result type combinations.

```sql
-- First add session column if it doesn't exist
ALTER TABLE school_result_subjects
ADD COLUMN session VARCHAR(20) NULL AFTER result_type;

-- Now add other columns
ALTER TABLE school_result_subjects
ADD COLUMN master_subject_id INT NULL AFTER id,
ADD COLUMN requires_moderation BOOLEAN DEFAULT TRUE AFTER session,
ADD COLUMN display_order INT DEFAULT 0 AFTER requires_moderation,
ADD COLUMN is_active BOOLEAN DEFAULT TRUE AFTER display_order,
ADD INDEX idx_session (session),
ADD INDEX idx_board_class (board, class_from, class_to),
ADD FOREIGN KEY (master_subject_id) REFERENCES master_subjects(id);

-- Migrate existing data
UPDATE school_result_subjects srs
JOIN master_subjects ms ON LOWER(srs.subject) = ms.slug
SET srs.master_subject_id = ms.id;

-- Set requires_moderation based on subject type
UPDATE school_result_subjects srs
JOIN master_subjects ms ON srs.master_subject_id = ms.id
SET srs.requires_moderation = (ms.subject_type = 'scholastic');
```

**Note**: The `is_co_scholastic` information is now derived from `master_subjects.subject_type` via the `master_subject_id` foreign key relationship.

#### 4.2.2. `school_result_sub_title` (Enhanced)

Now the most powerful configuration table.

```sql
-- Remove session column as it's now in school_result_subjects
ALTER TABLE school_result_sub_title
DROP COLUMN session;

-- Add new columns
ALTER TABLE school_result_sub_title
ADD COLUMN master_sub_title_id INT NULL AFTER id,
ADD COLUMN calculation_event ENUM('before_save', 'after_save', 'on_display', 'none') DEFAULT 'none' AFTER max_marks,
ADD COLUMN calculation_function VARCHAR(100) NULL AFTER calculation_event,
ADD COLUMN calculation_parameters JSON NULL AFTER calculation_function,
ADD COLUMN is_hidden BOOLEAN DEFAULT FALSE AFTER calculation_parameters,
ADD COLUMN is_readonly BOOLEAN DEFAULT FALSE AFTER is_hidden,
ADD COLUMN sync_to_result_type_id INT NULL AFTER is_readonly,
ADD COLUMN sync_to_subtitle_id INT NULL AFTER sync_to_result_type_id,
ADD COLUMN input_type ENUM('number', 'grade', 'text', 'percentage') DEFAULT 'number' AFTER sync_to_subtitle_id,
ADD COLUMN validation_rules JSON NULL AFTER input_type,
ADD COLUMN is_active BOOLEAN DEFAULT TRUE AFTER validation_rules,
ADD INDEX idx_subject (subject_id),
ADD INDEX idx_calculation (calculation_function),
ADD FOREIGN KEY (master_sub_title_id) REFERENCES master_sub_titles(id),
ADD FOREIGN KEY (sync_to_result_type_id) REFERENCES master_result_types(id),
ADD FOREIGN KEY (sync_to_subtitle_id) REFERENCES school_result_sub_title(id);

-- Example validation rules JSON
-- {"min": 0, "max": 100, "decimal_places": 2, "required": true}

-- Example calculation_parameters JSON
-- {"method": "sum", "subtitle_slugs": ["term1_exam", "notebook", "portfolio"]}
```

**Key Changes:**
- **Removed `session`**: Session is now managed at the subject level (in `school_result_subjects`)
- **Added `calculation_parameters`**: Stores function-specific parameters as JSON
- **Enhanced indexing**: Better query performance

**Field Explanations:**

- `calculation_event`: When to calculate this field
  - `before_save`: Calculate before saving (e.g., periodic test average shown to teacher)
  - `after_save`: Calculate after all marks saved (e.g., grand total)
  - `on_display`: Calculate only when viewing (doesn't store)
  - `none`: No calculation, manual entry

- `calculation_function`: Name of the calculation function to use (see section 6)

- `is_hidden`: Don't show this field in UI (used for intermediate calculations)

- `is_readonly`: Show but don't allow editing (calculated fields)

- `sync_to_result_type_id`: Target result type for dual-write (replaces `perodic_test_field_name` logic)

- `sync_to_subtitle_id`: Target subtitle within that result type

- `validation_rules`: JSON for client/server side validation

### 4.3. New Tables

#### 4.3.1. `calculation_functions`

Registry of all available calculation functions.

```sql
CREATE TABLE calculation_functions (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100) NOT NULL UNIQUE,
    slug VARCHAR(100) NOT NULL UNIQUE,
    description TEXT,
    function_class VARCHAR(255) NOT NULL, -- PHP class name
    function_method VARCHAR(100) NOT NULL, -- PHP method name
    parameters JSON, -- Expected parameters
    return_type VARCHAR(50), -- What it returns
    examples JSON, -- Usage examples
    is_active BOOLEAN DEFAULT TRUE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

    INDEX idx_slug (slug),
    INDEX idx_active (is_active)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- Sample Data
INSERT INTO calculation_functions (name, slug, function_class, function_method, description, parameters) VALUES
('Sum of Fields', 'sum_fields', 'App\\Services\\Calculations\\BasicCalculations', 'sum',
 'Sum multiple subtitle marks',
 '{"fields": ["marks1", "marks2", "marks3"]}'),

('Weighted Average', 'weighted_average', 'App\\Services\\Calculations\\BasicCalculations', 'weightedAverage',
 'Calculate weighted average',
 '{"marks_field": "marks", "max_marks_field": "max_marks", "weight": 10}'),

('Best Two Average', 'best_two_average', 'App\\Services\\Calculations\\PeriodicTestCalculations', 'bestTwoAverage',
 'Calculate best 2 out of 3 periodic test averages',
 '{"pt1_field": "pt1_percentage", "pt2_field": "pt2_percentage", "pt3_field": "pt3_percentage"}'),

('Percentage Calculation', 'percentage', 'App\\Services\\Calculations\\BasicCalculations', 'percentage',
 'Calculate (marks/max_marks)*100',
 '{"marks_field": "marks", "max_marks_field": "max_marks"}'),

('Grade Assignment', 'grade_from_percentage', 'App\\Services\\Calculations\\GradeCalculations', 'assignGrade',
 'Assign grade based on percentage',
 '{"percentage_field": "percentage", "grading_system": "cbse"}');
```

#### 4.3.2. `moderation_rules`

Configuration-driven moderation rules.

```sql
CREATE TABLE moderation_rules (
    id INT AUTO_INCREMENT PRIMARY KEY,
    board VARCHAR(50) NOT NULL,
    class_from INT NOT NULL,
    class_to INT NOT NULL,
    result_type_id INT NOT NULL,
    master_subject_id INT NOT NULL,
    subtitle_id INT NOT NULL, -- Which subtitle to check for moderation
    max_marks_for_calculation INT NOT NULL DEFAULT 80,

    -- Thresholds
    fail_percentage DECIMAL(5,2) DEFAULT 30.00,
    reassessment_percentage DECIMAL(5,2) DEFAULT 35.00,
    moderation_percentage DECIMAL(5,2) DEFAULT 40.00,

    -- Actions
    apply_grace_marks BOOLEAN DEFAULT TRUE,
    grace_marks_ceiling_percentage DECIMAL(5,2) DEFAULT 40.00,

    priority INT DEFAULT 0, -- For multiple rules, higher priority wins
    is_active BOOLEAN DEFAULT TRUE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

    INDEX idx_board_class (board, class_from, class_to),
    INDEX idx_result_type (result_type_id),
    INDEX idx_subject (master_subject_id),
    FOREIGN KEY (result_type_id) REFERENCES master_result_types(id),
    FOREIGN KEY (master_subject_id) REFERENCES master_subjects(id),
    FOREIGN KEY (subtitle_id) REFERENCES school_result_sub_title(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- Sample Data (replaces hardcoded mappings)
INSERT INTO moderation_rules (board, class_from, class_to, result_type_id, master_subject_id, subtitle_id)
SELECT
    'CBSE',
    3,
    5,
    (SELECT id FROM master_result_types WHERE slug = 'half_yearly' AND board = 'CBSE' LIMIT 1),
    ms.id,
    srs.id
FROM master_subjects ms
JOIN school_result_sub_title srs ON srs.master_sub_title_id =
    (SELECT id FROM master_sub_titles WHERE slug = 'term1_exam' LIMIT 1)
WHERE ms.slug IN ('english', 'punjabi', 'mathematics');
```

#### 4.3.3. `calculation_dependencies`

Defines dependencies between calculated fields.

```sql
CREATE TABLE calculation_dependencies (
    id INT AUTO_INCREMENT PRIMARY KEY,
    subtitle_id INT NOT NULL, -- The field being calculated
    depends_on_subtitle_id INT NOT NULL, -- The field it depends on
    depends_on_subject_id INT NULL, -- If from different subject
    dependency_type ENUM('same_subject', 'other_subject', 'other_result_type') DEFAULT 'same_subject',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,

    FOREIGN KEY (subtitle_id) REFERENCES school_result_sub_title(id),
    FOREIGN KEY (depends_on_subtitle_id) REFERENCES school_result_sub_title(id),
    UNIQUE KEY unique_dependency (subtitle_id, depends_on_subtitle_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
```

---

## 5. MongoDB Schema Unification

### 5.1. New Unified Schema

**All result types, all classes, all boards** use this same structure.

```javascript
{
    // === Composite Key (Identifier) ===
    "_id": ObjectId("..."),
    "school_id": 130,
    "current_class": 10,
    "session": "2025-26",
    "section": "A",
    "board": "CBSE",
    "result_type": "Half Yearly",
    "result_type_id": 2, // NEW: FK to master_result_types
    "s_adm_no_hid": 12345,

    // === Metadata ===
    "submitted_by": "Teacher", // Teacher/Observer
    "status": "P", // P=Pass, M=Moderated, F=Fail, I7=Reassessment, I15=Improvement

    // === Timestamps ===
    "created_at": "2025-11-15 10:30:00",
    "updated_at": "2025-11-15 10:30:00",
    "created_by": 4,
    "updated_by": 4,

    // === Subject Marks (Nested) ===
    "subject_marks": {
        "1": { // Subject ID (from school_result_subjects)
            "subject_id": 1,
            "master_subject_id": 1, // FK to master_subjects
            "subject_slug": "english",
            "subject_name": "English",

            "subtitles": {
                "100": { // Subtitle ID (from school_result_sub_title)
                    "subtitle_id": 100,
                    "master_subtitle_id": 10,
                    "subtitle_slug": "term1_exam",
                    "subtitle_name": "Term 1 Exam",
                    "max_marks": 80.0,
                    "marks": 75.0,
                    "is_calculated": false,
                    "is_hidden": false
                },
                "101": {
                    "subtitle_id": 101,
                    "master_subtitle_id": 9,
                    "subtitle_slug": "periodic_test_average",
                    "subtitle_name": "Periodic Test Average",
                    "max_marks": 10.0,
                    "marks": 9.0,
                    "is_calculated": true,
                    "calculation_source": "pre_mid_term", // From which result type
                    "is_hidden": false
                },
                "102": {
                    "subtitle_id": 102,
                    "master_subtitle_id": 7,
                    "subtitle_slug": "notebook",
                    "subtitle_name": "Notebook",
                    "max_marks": 5.0,
                    "marks": 4.5,
                    "is_calculated": false,
                    "is_hidden": false
                },
                "103": {
                    "subtitle_id": 103,
                    "master_subtitle_id": 5,
                    "subtitle_slug": "subject_enrichment",
                    "subtitle_name": "Subject Enrichment",
                    "max_marks": 5.0,
                    "marks": 4.0,
                    "is_calculated": false,
                    "is_hidden": false
                },
                "104": {
                    "subtitle_id": 104,
                    "master_subtitle_id": 13,
                    "subtitle_slug": "total_marks",
                    "subtitle_name": "Total Marks",
                    "max_marks": 100.0,
                    "marks": 92.5,
                    "is_calculated": true,
                    "calculation_function": "sum_fields",
                    "is_hidden": false
                }
            }
        },

        "2": { // Mathematics
            "subject_id": 2,
            "master_subject_id": 4,
            "subject_slug": "mathematics",
            "subject_name": "Mathematics",

            "subtitles": {
                "105": {
                    "subtitle_id": 105,
                    "master_subtitle_id": 10,
                    "subtitle_slug": "term1_exam",
                    "subtitle_name": "Term 1 Exam",
                    "max_marks": 80.0,
                    "marks": 60.0,
                    "is_calculated": false,
                    "is_hidden": false
                },
                // ... more subtitles
            }
        }
    },

    // === Audit Trail (for Observer modifications) ===
    "modification_history": [
        {
            "modified_at": "2025-11-15 11:00:00",
            "modified_by": 5, // Observer ID
            "modified_by_role": "Observer",
            "changes": [
                {
                    "subject_id": 1,
                    "subtitle_id": 100,
                    "field": "marks",
                    "old_value": 75.0,
                    "new_value": 78.0,
                    "reason": "Reevaluation"
                }
            ]
        }
    ],

    // === Moderation History ===
    "moderation_history": [
        {
            "moderated_at": "2025-11-15 12:00:00",
            "subject_id": 1,
            "subtitle_id": 100,
            "original_marks": 30.0,
            "moderated_marks": 32.0,
            "grace_marks": 2.0,
            "reason": "Below 40% threshold"
        }
    ]
}
```

### 5.2. Benefits of Unified Schema

1. **Consistency**: Same query structure for all result types
2. **Flexibility**: Easy to add new subjects/subtitles without schema changes
3. **Self-Documenting**: Slugs and names stored with data
4. **Audit Trail**: Complete history of modifications
5. **Extensibility**: Can add custom fields per subject without affecting others
6. **Type Safety**: Clear data types for all fields
7. **Relational Links**: IDs link back to configuration tables

### 5.3. Migration from Old Schema

```javascript
// Migration pseudocode
function migrateOldToNewSchema(oldDocument) {
    const newDocument = {
        // Copy base fields
        school_id: oldDocument.school_id,
        current_class: oldDocument.current_class,
        // ... etc

        subject_marks: {}
    };

    // For denormalized schema (eng_marks1, maths_marks1, etc.)
    if (isDenormalized(oldDocument)) {
        const subjectPrefixes = extractSubjectPrefixes(oldDocument);

        for (const prefix of subjectPrefixes) {
            const subjectId = oldDocument[prefix + 'subject_id'];
            const subjectConfig = getSubjectConfig(subjectId);

            newDocument.subject_marks[subjectId] = {
                subject_id: subjectId,
                master_subject_id: subjectConfig.master_subject_id,
                subject_slug: subjectConfig.slug,
                subject_name: subjectConfig.name,
                subtitles: {}
            };

            // Extract all subtitle marks
            for (let i = 1; i <= 10; i++) {
                const marksField = prefix + 'marks' + i;
                const subField = prefix + 'sub' + i;

                if (oldDocument[subField]) {
                    const subtitleId = oldDocument[subField];
                    const subtitleConfig = getSubtitleConfig(subtitleId);

                    newDocument.subject_marks[subjectId].subtitles[subtitleId] = {
                        subtitle_id: subtitleId,
                        master_subtitle_id: subtitleConfig.master_subtitle_id,
                        subtitle_slug: subtitleConfig.slug,
                        subtitle_name: subtitleConfig.name,
                        max_marks: subtitleConfig.max_marks,
                        marks: oldDocument[marksField],
                        is_calculated: false,
                        is_hidden: false
                    };
                }
            }
        }
    }
    // For normalized schema (marks_1, marks_2, etc.)
    else {
        const indices = extractMarksIndices(oldDocument);

        for (const i of indices) {
            const subjectId = oldDocument['main_subject_id_' + i];
            const subtitleId = oldDocument['subject_id_' + i];
            const marks = oldDocument['marks_' + i];
            const maxMarks = oldDocument['max_marks_' + i];

            // Group by subject
            if (!newDocument.subject_marks[subjectId]) {
                const subjectConfig = getSubjectConfig(subjectId);
                newDocument.subject_marks[subjectId] = {
                    subject_id: subjectId,
                    master_subject_id: subjectConfig.master_subject_id,
                    subject_slug: subjectConfig.slug,
                    subject_name: subjectConfig.name,
                    subtitles: {}
                };
            }

            const subtitleConfig = getSubtitleConfig(subtitleId);
            newDocument.subject_marks[subjectId].subtitles[subtitleId] = {
                subtitle_id: subtitleId,
                master_subtitle_id: subtitleConfig.master_subtitle_id,
                subtitle_slug: subtitleConfig.slug,
                subtitle_name: subtitleConfig.name,
                max_marks: maxMarks,
                marks: marks,
                is_calculated: false,
                is_hidden: false
            };
        }
    }

    return newDocument;
}
```

---

## 6. Calculation Engine

### 6.1. Architecture

```
┌─────────────────────────────────────────────────────┐
│          CalculationEngine (Main Orchestrator)       │
└─────────────────────────────────────────────────────┘
                        ↓
┌─────────────────────────────────────────────────────┐
│     CalculationRegistry (Function Lookup)            │
│     - Loads from calculation_functions table         │
│     - Maps slug → Class/Method                       │
└─────────────────────────────────────────────────────┘
                        ↓
┌─────────────────────────────────────────────────────┐
│     Calculation Classes (Implementations)            │
│     • BasicCalculations                              │
│     • PeriodicTestCalculations                       │
│     • GradeCalculations                              │
│     • BoardSpecificCalculations                      │
└─────────────────────────────────────────────────────┘
                        ↓
┌─────────────────────────────────────────────────────┐
│     CalculationContext (Data Access)                 │
│     - Provides access to marks data                  │
│     - Handles cross-subject/cross-result lookups     │
└─────────────────────────────────────────────────────┘
```

### 6.2. Calculation Function Interface

```php
<?php

namespace App\Services\Calculations;

interface CalculationFunctionInterface
{
    /**
     * Execute the calculation
     *
     * @param CalculationContext $context Access to all marks data
     * @param array $parameters Configuration parameters
     * @return mixed Calculated result
     */
    public function calculate(CalculationContext $context, array $parameters);

    /**
     * Validate parameters before calculation
     *
     * @param array $parameters
     * @return bool
     */
    public function validateParameters(array $parameters): bool;

    /**
     * Get dependencies (which fields this calculation needs)
     *
     * @param array $parameters
     * @return array Array of subtitle IDs this depends on
     */
    public function getDependencies(array $parameters): array;
}
```

### 6.3. Sample Calculation Implementations

#### 6.3.1. BasicCalculations

```php
<?php

namespace App\Services\Calculations;

class BasicCalculations implements CalculationFunctionInterface
{
    /**
     * Sum multiple fields
     */
    public function sum(CalculationContext $context, array $parameters)
    {
        $subtitleSlugs = $parameters['subtitle_slugs'] ?? [];
        $total = 0;

        foreach ($subtitleSlugs as $slug) {
            $marks = $context->getMarksBySlug($slug);
            $total += (float) $marks;
        }

        return $total;
    }

    /**
     * Calculate weighted average
     */
    public function weightedAverage(CalculationContext $context, array $parameters)
    {
        $marksSlug = $parameters['marks_slug'];
        $maxMarksSlug = $parameters['max_marks_slug'];
        $weight = $parameters['weight'] ?? 100;

        $marks = $context->getMarksBySlug($marksSlug);
        $maxMarks = $context->getMaxMarksBySlug($maxMarksSlug);

        if ($maxMarks == 0) {
            return 0;
        }

        return ($marks / $maxMarks) * $weight;
    }

    /**
     * Calculate percentage
     */
    public function percentage(CalculationContext $context, array $parameters)
    {
        $marksSlug = $parameters['marks_slug'];
        $maxMarksSlug = $parameters['max_marks_slug'];

        $marks = $context->getMarksBySlug($marksSlug);
        $maxMarks = $context->getMaxMarksBySlug($maxMarksSlug);

        if ($maxMarks == 0) {
            return 0;
        }

        return round(($marks / $maxMarks) * 100, 2);
    }

    public function calculate(CalculationContext $context, array $parameters)
    {
        $method = $parameters['method'];
        return $this->$method($context, $parameters);
    }

    public function validateParameters(array $parameters): bool
    {
        return isset($parameters['method']);
    }

    public function getDependencies(array $parameters): array
    {
        // Return subtitle slugs this calculation depends on
        return $parameters['subtitle_slugs'] ?? [];
    }
}
```

#### 6.3.2. PeriodicTestCalculations

```php
<?php

namespace App\Services\Calculations;

class PeriodicTestCalculations implements CalculationFunctionInterface
{
    /**
     * Calculate best two out of three periodic test averages (for classes 9-10)
     */
    public function bestTwoAverage(CalculationContext $context, array $parameters)
    {
        $pt1ResultType = $parameters['pt1_result_type'] ?? 'PT1';
        $pt2ResultType = $parameters['pt2_result_type'] ?? 'PT2';
        $pt3ResultType = $parameters['pt3_result_type'] ?? 'PT3';
        $subtitleSlug = $parameters['subtitle_slug'] ?? 'periodic_test';

        $currentSubjectId = $context->getCurrentSubjectId();

        // Fetch marks from other result types
        $pt1Marks = $context->getMarksFromOtherResultType(
            $pt1ResultType,
            $currentSubjectId,
            $subtitleSlug
        );

        $pt2Marks = $context->getMarksFromOtherResultType(
            $pt2ResultType,
            $currentSubjectId,
            $subtitleSlug
        );

        $pt3Marks = $context->getMarksFromOtherResultType(
            $pt3ResultType,
            $currentSubjectId,
            $subtitleSlug
        );

        // Calculate ratios (marks/max_marks)
        $pt1MaxMarks = $context->getMaxMarksFromOtherResultType($pt1ResultType, $currentSubjectId, $subtitleSlug);
        $pt2MaxMarks = $context->getMaxMarksFromOtherResultType($pt2ResultType, $currentSubjectId, $subtitleSlug);
        $pt3MaxMarks = $context->getMaxMarksFromOtherResultType($pt3ResultType, $currentSubjectId, $subtitleSlug);

        $ratios = [];
        if ($pt1MaxMarks > 0) $ratios[] = $pt1Marks / $pt1MaxMarks;
        if ($pt2MaxMarks > 0) $ratios[] = $pt2Marks / $pt2MaxMarks;
        if ($pt3MaxMarks > 0) $ratios[] = $pt3Marks / $pt3MaxMarks;

        // Sort descending and take best 2
        rsort($ratios);
        $bestTwo = array_slice($ratios, 0, 2);

        if (count($bestTwo) == 0) {
            return 0;
        }

        // Average of best two
        $average = array_sum($bestTwo) / count($bestTwo);

        // Scale to target max marks
        $targetMaxMarks = $parameters['target_max_marks'] ?? 10;

        return round($average * $targetMaxMarks, 2);
    }

    public function calculate(CalculationContext $context, array $parameters)
    {
        $method = $parameters['method'];
        return $this->$method($context, $parameters);
    }

    public function validateParameters(array $parameters): bool
    {
        return isset($parameters['method']);
    }

    public function getDependencies(array $parameters): array
    {
        // This calculation depends on data from other result types
        return [
            'result_types' => [
                $parameters['pt1_result_type'] ?? 'PT1',
                $parameters['pt2_result_type'] ?? 'PT2',
                $parameters['pt3_result_type'] ?? 'PT3'
            ]
        ];
    }
}
```

### 6.4. CalculationEngine Main Class

```php
<?php

namespace App\Services;

class CalculationEngine
{
    protected $registry;
    protected $configRepository;

    public function __construct(CalculationRegistry $registry, ConfigRepository $configRepository)
    {
        $this->registry = $registry;
        $this->configRepository = $configRepository;
    }

    /**
     * Calculate all fields for a result document
     *
     * @param array &$resultDocument MongoDB document (passed by reference)
     * @param string $event 'before_save' or 'after_save'
     */
    public function calculateAll(array &$resultDocument, string $event = 'before_save')
    {
        $context = new CalculationContext($resultDocument, $this->configRepository);

        // Get all subtitles that need calculation for this event
        $calculableSubtitles = $this->configRepository->getCalculableSubtitles(
            $resultDocument['result_type_id'],
            $event
        );

        // Build dependency graph
        $dependencyGraph = $this->buildDependencyGraph($calculableSubtitles);

        // Topological sort to determine calculation order
        $calculationOrder = $this->topologicalSort($dependencyGraph);

        // Execute calculations in order
        foreach ($calculationOrder as $subtitleConfig) {
            $this->executeCalculation($context, $resultDocument, $subtitleConfig);
        }
    }

    /**
     * Execute a single calculation
     */
    protected function executeCalculation(CalculationContext $context, array &$resultDocument, array $subtitleConfig)
    {
        $functionSlug = $subtitleConfig['calculation_function'];
        $parameters = $subtitleConfig['calculation_parameters'] ?? [];

        // Get calculation function from registry
        $calculationFunction = $this->registry->get($functionSlug);

        if (!$calculationFunction) {
            throw new \Exception("Calculation function not found: {$functionSlug}");
        }

        // Set current context
        $context->setCurrentSubject($subtitleConfig['subject_id']);
        $context->setCurrentSubtitle($subtitleConfig['id']);

        // Execute calculation
        $result = $calculationFunction->calculate($context, $parameters);

        // Update document
        $subjectId = $subtitleConfig['subject_id'];
        $subtitleId = $subtitleConfig['id'];

        $resultDocument['subject_marks'][$subjectId]['subtitles'][$subtitleId]['marks'] = $result;
        $resultDocument['subject_marks'][$subjectId]['subtitles'][$subtitleId]['is_calculated'] = true;
    }

    /**
     * Build dependency graph for calculations
     */
    protected function buildDependencyGraph(array $subtitles): array
    {
        $graph = [];

        foreach ($subtitles as $subtitle) {
            $dependencies = $this->configRepository->getCalculationDependencies($subtitle['id']);
            $graph[$subtitle['id']] = $dependencies;
        }

        return $graph;
    }

    /**
     * Topological sort to determine calculation order
     */
    protected function topologicalSort(array $graph): array
    {
        // Kahn's algorithm for topological sorting
        $sorted = [];
        $inDegree = [];

        // Calculate in-degrees
        foreach ($graph as $node => $dependencies) {
            if (!isset($inDegree[$node])) {
                $inDegree[$node] = 0;
            }
            foreach ($dependencies as $dep) {
                $inDegree[$dep] = ($inDegree[$dep] ?? 0) + 1;
            }
        }

        // Queue for nodes with no dependencies
        $queue = [];
        foreach ($inDegree as $node => $degree) {
            if ($degree == 0) {
                $queue[] = $node;
            }
        }

        // Process queue
        while (!empty($queue)) {
            $node = array_shift($queue);
            $sorted[] = $node;

            foreach ($graph[$node] ?? [] as $dependent) {
                $inDegree[$dependent]--;
                if ($inDegree[$dependent] == 0) {
                    $queue[] = $dependent;
                }
            }
        }

        // Check for cycles
        if (count($sorted) != count($graph)) {
            throw new \Exception("Circular dependency detected in calculations");
        }

        return $sorted;
    }
}
```

### 6.5. Configuration Example

Instead of hardcoded `calculateCals1()`, `calculateCals2()`, etc., we configure in database:

```sql
-- Step 1: First ensure the subject exists for the session
-- (Session is now managed at school_result_subjects level)
INSERT INTO school_result_subjects (
    master_subject_id,
    subject,
    board,
    result_type,
    class_from,
    class_to,
    session
) VALUES (
    (SELECT id FROM master_subjects WHERE slug = 'english'),
    'English',
    'CBSE',
    'Half Yearly',
    1,
    5,
    '2025-26'
) ON DUPLICATE KEY UPDATE session = '2025-26';

-- Step 2: Configure "Total Marks" subtitle to sum all component marks
-- Note: No session column here anymore, it's inherited from the subject
INSERT INTO school_result_sub_title (
    subject_id,
    master_sub_title_id,
    sub_title,
    max_marks,
    calculation_event,
    calculation_function,
    calculation_parameters,
    is_readonly
) VALUES (
    (SELECT id FROM school_result_subjects
     WHERE master_subject_id = (SELECT id FROM master_subjects WHERE slug = 'english')
     AND board = 'CBSE'
     AND result_type = 'Half Yearly'
     AND session = '2025-26'
     LIMIT 1),
    (SELECT id FROM master_sub_titles WHERE slug = 'total_marks'),
    'Total Marks',
    150,
    'before_save',
    'sum_fields',
    JSON_OBJECT(
        'method', 'sum',
        'subtitle_slugs', JSON_ARRAY('term1_exam', 'periodic_test_average', 'notebook', 'subject_enrichment', 'portfolio')
    ),
    TRUE
);

-- Step 3: Configure which fields to sum (dependencies)
INSERT INTO calculation_dependencies (subtitle_id, depends_on_subtitle_id)
SELECT
    (SELECT id FROM school_result_sub_title
     WHERE sub_title = 'Total Marks'
     AND subject_id = (
         SELECT id FROM school_result_subjects
         WHERE master_subject_id = (SELECT id FROM master_subjects WHERE slug = 'english')
         AND session = '2025-26'
         LIMIT 1
     )),
    id
FROM school_result_sub_title srs
JOIN school_result_subjects sbj ON srs.subject_id = sbj.id
WHERE sbj.master_subject_id = (SELECT id FROM master_subjects WHERE slug = 'english')
AND sbj.session = '2025-26'
AND srs.sub_title IN ('Term 1 Exam', 'Periodic Test Average', 'Notebook', 'Subject Enrichment', 'Portfolio');
```

**Result**: No more hardcoded calculation methods! All driven by configuration.

**Key Improvement**: Session management is now at the subject level, so when subjects change between sessions, we create new subject records rather than updating subtitle configurations.

---

## 7. Moderation System Redesign

### 7.1. ModerationService Architecture

```php
<?php

namespace App\Services;

class ModerationService
{
    protected $configRepository;
    protected $calculationEngine;

    /**
     * Moderate all students in a class/section
     */
    public function moderateClassSection(array $filters): array
    {
        $results = $this->fetchResults($filters);
        $moderatedData = [];

        foreach ($results as $result) {
            $moderatedResult = $this->moderateStudent($result);
            $moderatedData[] = $moderatedResult;
        }

        return $moderatedData;
    }

    /**
     * Moderate a single student's result
     */
    public function moderateStudent(array $resultDocument): array
    {
        // Get moderation rules for this result
        $rules = $this->configRepository->getModerationRules(
            $resultDocument['board'],
            $resultDocument['current_class'],
            $resultDocument['result_type_id']
        );

        if (empty($rules)) {
            // No moderation needed
            return [
                'status' => 'P',
                'changes' => [],
                'subject_status' => []
            ];
        }

        $moderationChanges = [];
        $subjectStatus = [];
        $failingCount = 0;
        $reassessmentCount = 0;

        // Evaluate each subject
        foreach ($rules as $rule) {
            $subjectId = $rule->subject_id;
            $subtitleId = $rule->subtitle_id;

            // Get marks for this subtitle
            $marks = $resultDocument['subject_marks'][$subjectId]['subtitles'][$subtitleId]['marks'] ?? null;

            if ($marks === null) {
                continue;
            }

            // Calculate percentage
            $percentage = ($marks / $rule->max_marks_for_calculation) * 100;

            // Apply moderation logic
            $moderationResult = $this->applyModerationLogic($percentage, $marks, $rule);

            $subjectSlug = $resultDocument['subject_marks'][$subjectId]['subject_slug'];
            $subjectStatus[$subjectSlug] = $moderationResult['status'];

            if ($moderationResult['status'] == 'Fail') {
                $failingCount++;
            } elseif ($moderationResult['status'] == 'Reassessment') {
                $reassessmentCount++;
            } elseif ($moderationResult['status'] == 'Moderated') {
                // Apply grace marks
                $moderationChanges[] = [
                    'subject_id' => $subjectId,
                    'subtitle_id' => $subtitleId,
                    'original_marks' => $marks,
                    'new_marks' => $moderationResult['new_marks'],
                    'grace_marks' => $moderationResult['grace_marks']
                ];
            }
        }

        // Determine final status
        $finalStatus = $this->determineFinalStatus($failingCount, $reassessmentCount, $moderationChanges);

        return [
            'status' => $finalStatus,
            'changes' => $moderationChanges,
            'subject_status' => $subjectStatus,
            'failing_count' => $failingCount,
            'reassessment_count' => $reassessmentCount
        ];
    }

    /**
     * Apply moderation logic based on rule
     */
    protected function applyModerationLogic(float $percentage, float $marks, object $rule): array
    {
        if ($percentage < $rule->fail_percentage) {
            return [
                'status' => 'Fail',
                'new_marks' => $marks
            ];
        } elseif ($percentage >= $rule->fail_percentage && $percentage < $rule->reassessment_percentage) {
            return [
                'status' => 'Reassessment',
                'new_marks' => $marks
            ];
        } elseif ($percentage >= $rule->reassessment_percentage && $percentage < $rule->moderation_percentage) {
            if ($rule->apply_grace_marks) {
                $graceMarks = ($rule->max_marks_for_calculation * $rule->grace_marks_ceiling_percentage / 100) - $marks;
                return [
                    'status' => 'Moderated',
                    'new_marks' => $rule->max_marks_for_calculation * $rule->grace_marks_ceiling_percentage / 100,
                    'grace_marks' => $graceMarks
                ];
            } else {
                return [
                    'status' => 'Pass',
                    'new_marks' => $marks
                ];
            }
        } else {
            return [
                'status' => 'Pass',
                'new_marks' => $marks
            ];
        }
    }

    /**
     * Determine final status based on subject-wise results
     */
    protected function determineFinalStatus(int $failingCount, int $reassessmentCount, array $moderationChanges): string
    {
        if ($failingCount >= 2) {
            return 'F'; // Fail
        } elseif ($failingCount == 1) {
            return 'I15'; // Improvement - 15 days
        } elseif ($reassessmentCount > 0) {
            return 'I7'; // Reassessment - 7 days
        } elseif (!empty($moderationChanges)) {
            return 'M'; // Moderated
        } else {
            return 'P'; // Pass
        }
    }

    /**
     * Apply moderation changes to document
     */
    public function applyModerationChanges(array &$resultDocument, array $moderationResult): void
    {
        foreach ($moderationResult['changes'] as $change) {
            $subjectId = $change['subject_id'];
            $subtitleId = $change['subtitle_id'];

            // Update marks
            $resultDocument['subject_marks'][$subjectId]['subtitles'][$subtitleId]['marks'] = $change['new_marks'];

            // Store moderation history
            if (!isset($resultDocument['moderation_history'])) {
                $resultDocument['moderation_history'] = [];
            }

            $resultDocument['moderation_history'][] = [
                'moderated_at' => date('Y-m-d H:i:s'),
                'subject_id' => $subjectId,
                'subtitle_id' => $subtitleId,
                'original_marks' => $change['original_marks'],
                'moderated_marks' => $change['new_marks'],
                'grace_marks' => $change['grace_marks'],
                'reason' => 'Below ' . ($change['new_marks'] / 80 * 100) . '% threshold'
            ];
        }

        // Recalculate dependent fields
        $this->calculationEngine->calculateAll($resultDocument, 'after_moderation');

        // Set status
        $resultDocument['status'] = $moderationResult['status'];
    }
}
```

### 7.2. Configuration Example

```sql
-- Instead of hardcoded subject mappings in MarksModerationHelper,
-- configure in moderation_rules table

-- For CBSE Classes 3-5
INSERT INTO moderation_rules (
    board, class_from, class_to, result_type_id, master_subject_id, subtitle_id,
    max_marks_for_calculation, fail_percentage, reassessment_percentage, moderation_percentage
)
SELECT
    'CBSE',
    3,
    5,
    (SELECT id FROM master_result_types WHERE slug = 'half_yearly' AND board = 'CBSE' LIMIT 1),
    ms.id,
    srs.id,
    80,
    30.00,
    35.00,
    40.00
FROM master_subjects ms
JOIN school_result_sub_title srs ON srs.subject_id IN (
    SELECT id FROM school_result_subjects WHERE master_subject_id = ms.id AND board = 'CBSE'
)
WHERE ms.slug IN ('english', 'punjabi', 'mathematics')
AND srs.master_sub_title_id = (SELECT id FROM master_sub_titles WHERE slug = 'term1_exam');

-- To change moderation rules (e.g., different thresholds for a school):
UPDATE moderation_rules
SET fail_percentage = 25.00,
    reassessment_percentage = 30.00,
    moderation_percentage = 35.00
WHERE board = 'CBSE' AND class_from = 3 AND class_to = 5;

-- To add moderation for a new subject:
INSERT INTO moderation_rules (...) VALUES (...);
-- No code changes needed!
```

---

## 8. API & Controller Architecture

### 8.1. Unified Controller

Instead of separate controllers per session, one unified controller.

```php
<?php

namespace App\Http\Controllers;

use App\Services\ResultService;
use App\Services\CalculationEngine;
use App\Services\ModerationService;
use Illuminate\Http\Request;

class ResultController extends Controller
{
    protected $resultService;
    protected $calculationEngine;
    protected $moderationService;

    public function __construct(
        ResultService $resultService,
        CalculationEngine $calculationEngine,
        ModerationService $moderationService
    ) {
        $this->resultService = $resultService;
        $this->calculationEngine = $calculationEngine;
        $this->moderationService = $moderationService;
    }

    /**
     * Display result entry form
     */
    public function showForm(Request $request)
    {
        $filters = $request->only(['school_id', 'class', 'section', 'board', 'result_type', 'session']);

        // Get configuration
        $config = $this->resultService->getFormConfiguration($filters);

        // Get students
        $students = $this->resultService->getStudents($filters);

        // Get existing results
        $existingResults = $this->resultService->getResults($filters);

        // Determine view based on result type
        $view = $this->determineView($config);

        return view($view, compact('config', 'students', 'existingResults', 'filters'));
    }

    /**
     * Save marks for a student
     */
    public function saveMarks(Request $request)
    {
        $validated = $request->validate([
            'school_id' => 'required|integer',
            'current_class' => 'required|integer',
            'section' => 'required|string',
            'session' => 'required|string',
            'board' => 'required|string',
            'result_type' => 'required|string',
            's_adm_no_hid' => 'required|integer',
            'subject_id' => 'required|integer',
            'marks' => 'required|array'
        ]);

        // Build result document
        $resultDocument = $this->resultService->buildResultDocument($validated);

        // Execute calculations
        $this->calculationEngine->calculateAll($resultDocument, 'before_save');

        // Handle periodic test sync if applicable
        $this->resultService->handlePeriodicTestSync($resultDocument);

        // Save to MongoDB
        $this->resultService->saveResult($resultDocument);

        return response()->json(['success' => true, 'message' => 'Marks saved successfully']);
    }

    /**
     * Submit result for observer/principal
     */
    public function submitForReview(Request $request)
    {
        $filters = $request->only(['school_id', 'class', 'section', 'board', 'result_type', 'session']);

        // Apply moderation
        $moderationResults = $this->moderationService->moderateClassSection($filters);

        // Update result status
        $this->resultService->updateStatus($filters, $request->user());

        return response()->json([
            'success' => true,
            'message' => 'Result submitted successfully',
            'moderation_summary' => $moderationResults
        ]);
    }

    /**
     * View results
     */
    public function viewResults(Request $request)
    {
        $filters = $request->only(['school_id', 'class', 'section', 'board', 'result_type', 'session']);

        $config = $this->resultService->getFormConfiguration($filters);
        $students = $this->resultService->getStudents($filters);
        $results = $this->resultService->getResults($filters);

        // Merge student info with results
        $data = $this->resultService->mergeStudentsWithResults($students, $results);

        $view = $this->determineViewResultsView($config);

        return view($view, compact('config', 'data', 'filters'));
    }

    /**
     * Determine which view to use based on configuration
     */
    protected function determineView(array $config): string
    {
        // All scenarios use the same unified view
        // The view adapts based on configuration data
        return 'results.unified-form';
    }

    protected function determineViewResultsView(array $config): string
    {
        return 'results.unified-view';
    }
}
```

### 8.2. ResultService

```php
<?php

namespace App\Services;

class ResultService
{
    protected $configRepository;
    protected $studentRepository;
    protected $resultRepository;

    /**
     * Get form configuration based on filters
     */
    public function getFormConfiguration(array $filters): array
    {
        // Get result type config
        $resultType = $this->configRepository->getResultType(
            $filters['result_type'],
            $filters['board'],
            $filters['class']
        );

        // Get subjects for this class/board/result type
        $subjects = $this->configRepository->getSubjects(
            $filters['class'],
            $filters['board'],
            $filters['result_type']
        );

        // For each subject, get subtitles
        foreach ($subjects as &$subject) {
            $subject['subtitles'] = $this->configRepository->getSubtitles(
                $subject['id'],
                $filters['session']
            );
        }

        return [
            'result_type' => $resultType,
            'subjects' => $subjects,
            'workflow_status' => $this->getWorkflowStatus($filters)
        ];
    }

    /**
     * Build a result document from request data
     */
    public function buildResultDocument(array $data): array
    {
        // Find existing or create new
        $existing = $this->resultRepository->find([
            'school_id' => $data['school_id'],
            'current_class' => $data['current_class'],
            'section' => $data['section'],
            'session' => $data['session'],
            'board' => $data['board'],
            'result_type' => $data['result_type'],
            's_adm_no_hid' => $data['s_adm_no_hid']
        ]);

        if ($existing) {
            $document = $existing;
        } else {
            $document = $this->initializeDocument($data);
        }

        // Update subject marks
        $subjectId = $data['subject_id'];

        if (!isset($document['subject_marks'][$subjectId])) {
            $subjectConfig = $this->configRepository->getSubject($subjectId);
            $document['subject_marks'][$subjectId] = [
                'subject_id' => $subjectId,
                'master_subject_id' => $subjectConfig->master_subject_id,
                'subject_slug' => $subjectConfig->slug,
                'subject_name' => $subjectConfig->name,
                'subtitles' => []
            ];
        }

        // Update subtitle marks
        foreach ($data['marks'] as $subtitleId => $marks) {
            $subtitleConfig = $this->configRepository->getSubtitle($subtitleId);

            $document['subject_marks'][$subjectId]['subtitles'][$subtitleId] = [
                'subtitle_id' => $subtitleId,
                'master_subtitle_id' => $subtitleConfig->master_sub_title_id,
                'subtitle_slug' => $subtitleConfig->slug,
                'subtitle_name' => $subtitleConfig->name,
                'max_marks' => $subtitleConfig->max_marks,
                'marks' => (float) $marks,
                'is_calculated' => false,
                'is_hidden' => $subtitleConfig->is_hidden
            ];
        }

        $document['updated_at'] = date('Y-m-d H:i:s');
        $document['updated_by'] = auth()->id();

        return $document;
    }

    /**
     * Handle periodic test sync (replaces dual-write logic)
     */
    public function handlePeriodicTestSync(array $resultDocument): void
    {
        $resultTypeConfig = $this->configRepository->getResultTypeById($resultDocument['result_type_id']);

        if (!$resultTypeConfig->is_periodic_test || !$resultTypeConfig->target_result_type_id) {
            return; // Not a periodic test or no target configured
        }

        // Get target result type
        $targetResultType = $this->configRepository->getResultTypeById($resultTypeConfig->target_result_type_id);

        // For each subject in current document
        foreach ($resultDocument['subject_marks'] as $subjectId => $subjectData) {
            // Find subtitles that need to be synced
            foreach ($subjectData['subtitles'] as $subtitleId => $subtitleData) {
                $subtitleConfig = $this->configRepository->getSubtitle($subtitleId);

                if ($subtitleConfig->sync_to_result_type_id && $subtitleConfig->sync_to_subtitle_id) {
                    // Sync this mark to target
                    $this->syncMarkToTarget(
                        $resultDocument,
                        $targetResultType->slug,
                        $subjectId,
                        $subtitleConfig->sync_to_subtitle_id,
                        $subtitleData['marks']
                    );
                }
            }
        }
    }

    /**
     * Sync a mark to target result type
     */
    protected function syncMarkToTarget(
        array $sourceDocument,
        string $targetResultType,
        int $subjectId,
        int $targetSubtitleId,
        float $marks
    ): void {
        // Find or create target document
        $targetDocument = $this->resultRepository->findOrCreate([
            'school_id' => $sourceDocument['school_id'],
            'current_class' => $sourceDocument['current_class'],
            'section' => $sourceDocument['section'],
            'session' => $sourceDocument['session'],
            'board' => $sourceDocument['board'],
            'result_type' => $targetResultType,
            's_adm_no_hid' => $sourceDocument['s_adm_no_hid']
        ]);

        // Initialize subject if not exists
        if (!isset($targetDocument['subject_marks'][$subjectId])) {
            $subjectConfig = $this->configRepository->getSubject($subjectId);
            $targetDocument['subject_marks'][$subjectId] = [
                'subject_id' => $subjectId,
                'master_subject_id' => $subjectConfig->master_subject_id,
                'subject_slug' => $subjectConfig->slug,
                'subject_name' => $subjectConfig->name,
                'subtitles' => []
            ];
        }

        // Update target subtitle
        $targetSubtitleConfig = $this->configRepository->getSubtitle($targetSubtitleId);

        $targetDocument['subject_marks'][$subjectId]['subtitles'][$targetSubtitleId] = [
            'subtitle_id' => $targetSubtitleId,
            'master_subtitle_id' => $targetSubtitleConfig->master_sub_title_id,
            'subtitle_slug' => $targetSubtitleConfig->slug,
            'subtitle_name' => $targetSubtitleConfig->name,
            'max_marks' => $targetSubtitleConfig->max_marks,
            'marks' => $marks,
            'is_calculated' => true,
            'calculation_source' => $sourceDocument['result_type'],
            'is_hidden' => $targetSubtitleConfig->is_hidden
        ];

        $targetDocument['updated_at'] = date('Y-m-d H:i:s');

        // Save target document
        $this->resultRepository->save($targetDocument);
    }
}
```

### 8.3. Benefits of New Architecture

1. **Single Controller**: No more duplicate controllers per session
2. **Service Layer**: Business logic separated from HTTP layer
3. **Configuration-Driven**: Form structure, calculations, and sync logic all from DB
4. **Testable**: Services can be unit tested independently
5. **Extensible**: Easy to add new features without modifying core code

---

## 9. Migration Strategy

### 9.1. Phase 1: Database Migration

**Step 1: Create New Tables**
```bash
php artisan migrate --path=/database/migrations/2025_11_15_create_master_tables.php
```

**Step 2: Migrate Existing Configuration**
```sql
-- Populate master_subjects from existing unique subjects
INSERT INTO master_subjects (name, slug, subject_type)
SELECT DISTINCT
    subject,
    LOWER(REPLACE(subject, ' ', '_')),
    CASE
        WHEN subject IN ('Physical Education', 'Art Education', 'Work Education', 'Music', 'Visual Arts', 'PSPE')
        THEN 'co_scholastic'
        ELSE 'scholastic'
    END
FROM school_result_subjects;

-- Add session column to school_result_subjects if not exists
ALTER TABLE school_result_subjects
ADD COLUMN IF NOT EXISTS session VARCHAR(20) NULL AFTER result_type;

-- Link existing records
UPDATE school_result_subjects srs
JOIN master_subjects ms ON LOWER(srs.subject) = ms.slug
SET srs.master_subject_id = ms.id;

-- Remove session from sub_title table (moved to subjects)
-- First, ensure all subtitles are linked to subjects with proper sessions
ALTER TABLE school_result_sub_title
DROP COLUMN IF EXISTS session;

-- Similar for other tables...
```

**Step 3: Create Calculation Functions Registry**
```sql
-- Register existing calculation logic as functions
INSERT INTO calculation_functions (name, slug, function_class, function_method) VALUES
('CBSE Class 1-5 Total', 'cbse_1_5_total', 'App\\Services\\Calculations\\CBSECalculations', 'calculate15Total'),
('CBSE Class 6-8 Total', 'cbse_6_8_total', 'App\\Services\\Calculations\\CBSECalculations', 'calculate68Total');
-- etc.
```

**Step 4: Configure Calculation Rules**
```sql
-- Link subtitles to calculation functions
UPDATE school_result_sub_title
SET calculation_function = 'cbse_1_5_total',
    calculation_event = 'before_save'
WHERE sub_title LIKE '%Total%'
AND subject_id IN (SELECT id FROM school_result_subjects WHERE class_from >= 1 AND class_to <= 5);
```

### 9.2. Phase 2: Data Migration

**Migrate MongoDB Documents**

```bash
php artisan migrate:mongo-schema
```

```php
<?php
// Command: app/Console/Commands/MigrateMongoSchema.php

class MigrateMongoSchema extends Command
{
    protected $signature = 'migrate:mongo-schema {--batch=100}';

    public function handle()
    {
        $collections = ['student_result', 'observe_student_result'];

        foreach ($collections as $collection) {
            $this->info("Migrating collection: {$collection}");

            $count = DB::connection('mongodb')->table($collection)->count();
            $this->info("Total documents: {$count}");

            $bar = $this->output->createProgressBar($count);

            DB::connection('mongodb')->table($collection)->chunkById($this->option('batch'), function ($documents) use ($bar) {
                foreach ($documents as $doc) {
                    $newDoc = $this->transformDocument($doc);

                    // Save to new collection
                    DB::connection('mongodb')->table('student_result_v2')->insert($newDoc);

                    $bar->advance();
                }
            });

            $bar->finish();
            $this->info("\nMigration complete for {$collection}");
        }
    }

    protected function transformDocument($oldDoc)
    {
        // Use migration logic from section 5.3
        return migrateOldToNewSchema($oldDoc);
    }
}
```

### 9.3. Phase 3: Gradual Rollout

**Step 1: Run Both Systems in Parallel**
- Keep old system operational
- New system writes to both old and new schemas
- Compare results for validation

**Step 2: Enable for Test School**
- Select one school for pilot
- Use new system for one session
- Collect feedback

**Step 3: Gradual Expansion**
- Enable for more schools
- Monitor performance
- Fix any issues

**Step 4: Full Migration**
- Switch all schools to new system
- Keep old system read-only for reference
- After one full session, deprecate old system

### 9.4. Rollback Plan

If issues arise:
- Feature flag to switch back to old system
- Old data still intact
- No data loss

---

## 10. Implementation Phases

### Phase 1: Foundation (Weeks 1-4)

**Week 1-2: Database Schema**
- [ ] Create master tables
- [ ] Enhance existing tables
- [ ] Create calculation_functions registry
- [ ] Create moderation_rules table
- [ ] Migration scripts

**Week 3-4: Core Services**
- [ ] CalculationEngine service
- [ ] CalculationRegistry
- [ ] Basic calculation implementations
- [ ] CalculationContext
- [ ] Unit tests

### Phase 2: Business Logic (Weeks 5-8)

**Week 5-6: Result Service**
- [ ] ResultService implementation
- [ ] ConfigRepository
- [ ] ResultRepository (MongoDB)
- [ ] Periodic test sync service
- [ ] Unit tests

**Week 7-8: Moderation Service**
- [ ] ModerationService implementation
- [ ] Configuration-driven rules
- [ ] Migrate existing moderation logic
- [ ] Unit tests

### Phase 3: API Layer (Weeks 9-10)

**Week 9-10: Controllers & Routes**
- [ ] Unified ResultController
- [ ] API endpoints
- [ ] Request validation
- [ ] Integration tests

### Phase 4: Frontend (Weeks 11-14)

**Week 11-12: Views**
- [ ] Unified form view (Vue.js/React component)
- [ ] Dynamic field rendering based on config
- [ ] Client-side validation
- [ ] AJAX save handlers

**Week 13-14: Result Display**
- [ ] Unified result view
- [ ] PDF export
- [ ] Excel export
- [ ] Print layouts

### Phase 5: Migration (Weeks 15-16)

**Week 15: Data Migration**
- [ ] Mongo schema migration script
- [ ] Validation scripts
- [ ] Data integrity checks

**Week 16: Parallel Run**
- [ ] Deploy both systems
- [ ] Compare outputs
- [ ] Fix discrepancies

### Phase 6: Testing & Deployment (Weeks 17-20)

**Week 17-18: Testing**
- [ ] End-to-end testing
- [ ] Load testing
- [ ] User acceptance testing
- [ ] Bug fixes

**Week 19: Pilot**
- [ ] Deploy to one school
- [ ] Monitor closely
- [ ] Collect feedback
- [ ] Iterate

**Week 20: Full Deployment**
- [ ] Deploy to all schools
- [ ] Training sessions
- [ ] Documentation
- [ ] Support

---

## 11. Success Metrics

### 11.1. Code Metrics

- **Before**: ~3000 lines across SaveResult2526.php, MarksModerationHelper.php, addAcademicResult2526Controller.php
- **After**: ~1500 lines total (50% reduction)

- **Before**: 6 calculation methods, all hardcoded
- **After**: 1 calculation engine, unlimited configurable functions

- **Before**: Hardcoded moderation mappings for 3 class groups
- **After**: Database-driven rules, unlimited configurations

### 11.2. Operational Metrics

- **Configuration Changes**:
  - Before: Requires developer + code deployment
  - After: Admin can configure via UI, no deployment

- **New Board Addition**:
  - Before: 2-3 weeks of development
  - After: 1-2 days of configuration

- **Bug Fix Deployment**:
  - Before: Fix in multiple files across sessions
  - After: Fix once in service layer

### 11.3. Performance Metrics

- **Query Optimization**: Unified schema allows better indexing
- **Caching**: Configuration data cached, reduces DB queries
- **Calculation Performance**: Dependency graph ensures optimal calculation order

---

## 12. Risk Mitigation

### 12.1. Risks

1. **Data Migration Issues**
   - Mitigation: Extensive testing, parallel run, rollback plan

2. **Performance Degradation**
   - Mitigation: Load testing, optimization, caching strategy

3. **User Resistance**
   - Mitigation: Training, documentation, gradual rollout

4. **Calculation Errors**
   - Mitigation: Unit tests, integration tests, comparison with old system

5. **Schema Complexity**
   - Mitigation: Clear documentation, examples, admin UI for configuration

### 12.2. Contingency Plans

- **Feature flags** to enable/disable new system per school
- **Parallel operation** of both systems during transition
- **Automated comparison** scripts to validate calculations
- **Rollback procedures** if critical issues found
- **Support team** trained on both old and new systems

---

## 13. Conclusion

### 13.1. Summary

This new architecture transforms the Academic Result Module from a rigid, hardcoded system into a flexible, configuration-driven platform. Key improvements:

✅ **Unified Schema**: One consistent data structure
✅ **Configuration-Driven**: Business logic in database, not code
✅ **Scalable**: Easy to add boards, subjects, rules
✅ **Maintainable**: Less code, clear separation of concerns
✅ **Testable**: Services can be unit tested
✅ **Auditable**: Complete history of changes

### 13.2. Next Steps

1. **Approval**: Review and approve this architecture plan
2. **Team Formation**: Assign developers to implementation phases
3. **Kickoff**: Begin Phase 1 - Foundation
4. **Regular Reviews**: Weekly progress reviews
5. **Documentation**: Maintain updated technical documentation

### 13.3. Long-Term Vision

This architecture enables:
- **Multi-tenant**: Support multiple schools with different rules
- **API-First**: Expose APIs for mobile apps, integrations
- **AI/ML Ready**: Structured data enables predictive analytics
- **Extensible**: Plugin architecture for custom features
- **Cloud-Ready**: Microservices architecture for scaling

---

**Document Status:** Draft for Review
**Prepared By:** Architecture Team
**Date:** November 15, 2025
**Version:** 1.0
