# School Year Closure and New Year Rollover Implementation Plan

## 1. Objective

Build a controlled school-year closure feature that allows an authorized administrator to close the current school year, make it read-only, create a new active school year, promote students, and transfer unpaid balances to the new school year based on `parent_id`.

The old school year must remain viewable through a school-year selector, but it must not allow editing, deleting, importing, payment posting, attendance changes, grade changes, enrollment changes, or invoice modifications.

Unpaid balances must be transferred at the parent level, not the student level, because one parent may be responsible for multiple students.

---

## 2. Core Business Rules

1. Only one school year can be active at a time.
2. Closed school years are read-only.
3. Every academic and financial record must be linked to a `school_year_id`.
4. Historical records must not be overwritten during year closure.
5. Student promotion must create new enrollments for the new year.
6. Old enrollments must remain unchanged.
7. Unpaid balances must be calculated by `parent_id`.
8. Transferred balances must be traceable to the original year and original invoices.
9. The closure process must run inside a database transaction.
10. If one closure step fails, the entire operation must roll back.
11. Only authorized users can close, reopen, or archive school years.
12. Duplicate balance transfers must be prevented.
13. Payments after closure should be posted in the new active year against the transferred balance invoice.

---

## 3. Required Data Model

### 3.1 `school_years`

```sql
CREATE TABLE school_years (
  id BIGINT PRIMARY KEY,
  name VARCHAR(50) NOT NULL,
  start_date DATE NOT NULL,
  end_date DATE NOT NULL,
  status VARCHAR(20) NOT NULL,
  is_current BOOLEAN DEFAULT FALSE,
  closed_at TIMESTAMP NULL,
  closed_by BIGINT NULL,
  created_at TIMESTAMP,
  updated_at TIMESTAMP
);
```

Recommended statuses:

```text
draft     = created but not active yet
active    = current editable school year
closed    = read-only historical school year
archived  = old read-only year, hidden from default views if needed
```

There must be a database or application-level rule preventing more than one active/current school year.

---

### 3.2 `student_enrollments`

Students should not store their current grade/class as the only source of truth. Enrollment must be year-based.

```sql
CREATE TABLE student_enrollments (
  id BIGINT PRIMARY KEY,
  student_id BIGINT NOT NULL,
  school_year_id BIGINT NOT NULL,
  grade_id BIGINT NOT NULL,
  class_id BIGINT NULL,
  status VARCHAR(30) NOT NULL,
  created_at TIMESTAMP,
  updated_at TIMESTAMP
);
```

Recommended enrollment statuses:

```text
enrolled
promoted
repeated
graduated
transferred
withdrawn
```

This allows the same student to have a clean academic history across multiple school years.

---

### 3.3 `parent_accounts`

Each parent should have one account per school year.

```sql
CREATE TABLE parent_accounts (
  id BIGINT PRIMARY KEY,
  parent_id BIGINT NOT NULL,
  school_year_id BIGINT NOT NULL,
  opening_balance DECIMAL(12,2) DEFAULT 0,
  current_balance DECIMAL(12,2) DEFAULT 0,
  created_at TIMESTAMP,
  updated_at TIMESTAMP
);
```

Recommended unique constraint:

```sql
ALTER TABLE parent_accounts
ADD CONSTRAINT unique_parent_account_per_year
UNIQUE (parent_id, school_year_id);
```

---

### 3.4 `invoices`

Invoices must belong to a school year and a parent. `student_id` should be nullable for parent-level balances.

```sql
CREATE TABLE invoices (
  id BIGINT PRIMARY KEY,
  parent_id BIGINT NOT NULL,
  student_id BIGINT NULL,
  school_year_id BIGINT NOT NULL,
  amount DECIMAL(12,2) NOT NULL,
  paid_amount DECIMAL(12,2) DEFAULT 0,
  balance_due DECIMAL(12,2) NOT NULL,
  status VARCHAR(30) NOT NULL,
  invoice_type VARCHAR(50),
  description TEXT NULL,
  balance_transfer_id BIGINT NULL,
  created_at TIMESTAMP,
  updated_at TIMESTAMP
);
```

Recommended invoice statuses:

```text
unpaid
partially_paid
paid
cancelled
void
```

Recommended invoice types:

```text
tuition
transport
books
registration
old_balance
adjustment
```

---

### 3.5 `parent_balance_transfers`

This table records unpaid balances transferred from one school year to another.

```sql
CREATE TABLE parent_balance_transfers (
  id BIGINT PRIMARY KEY,
  parent_id BIGINT NOT NULL,
  from_school_year_id BIGINT NOT NULL,
  to_school_year_id BIGINT NOT NULL,
  amount DECIMAL(12,2) NOT NULL,
  status VARCHAR(30) NOT NULL,
  source_summary JSON NULL,
  created_by BIGINT NOT NULL,
  created_at TIMESTAMP
);
```

Recommended statuses:

```text
pending
transferred
reversed
```

Prevent duplicate transfers:

```sql
ALTER TABLE parent_balance_transfers
ADD CONSTRAINT unique_parent_year_balance_transfer
UNIQUE (parent_id, from_school_year_id, to_school_year_id);
```

Example `source_summary`:

```json
{
  "unpaid_invoice_ids": [101, 102, 140],
  "total_due": 1200.00,
  "total_paid": 800.00,
  "remaining_balance": 400.00
}
```

---

### 3.6 `audit_logs`

```sql
CREATE TABLE audit_logs (
  id BIGINT PRIMARY KEY,
  user_id BIGINT NOT NULL,
  action VARCHAR(100) NOT NULL,
  table_name VARCHAR(100) NULL,
  record_id BIGINT NULL,
  old_value JSON NULL,
  new_value JSON NULL,
  metadata JSON NULL,
  created_at TIMESTAMP
);
```

Audit logs must record year closure, balance transfers, reopening, student promotion, and old-balance invoice creation.

---

## 4. Close-Year Workflow

The administrator should close a school year through a guided wizard.

### Step 1: Select Current Year

The system identifies the active school year.

Example:

```text
Current school year: 2024-2025
Status: Active
```

Only an active school year can be closed.

---

### Step 2: Enter New School Year Details

Admin enters:

```text
New school year name
Start date
End date
```

Example:

```text
Name: 2025-2026
Start date: 2025-09-01
End date: 2026-06-30
```

Validation rules:

1. Name must be unique.
2. Start date must be before end date.
3. New year must not overlap an existing year.
4. There must not already be another active year.

---

### Step 3: Student Promotion Preview

The system previews what will happen to students.

Possible actions:

```text
Promote to next grade
Repeat same grade
Graduate
Transfer out
Withdraw
Hold for manual review
```

The preview should show:

```text
Student name
Current grade
Current class
Promotion action
Target grade
Target class
Warnings
```

The system must block closure if students are missing required promotion decisions.

---

### Step 4: Financial Preview

The system calculates unpaid balances grouped by `parent_id`.

Preview columns:

```text
Parent ID
Parent name
Students under parent
Unpaid invoice count
Old unpaid balance
Credit balance, if any
Amount to transfer
Transfer status
```

Example:

```text
Parent ID: P-102
Parent: Mohamed Ali
Students: Sara Ali, Adam Ali
Old balance: 450.00
Transfer to new year: Yes
```

This preview is critical. Closing a school year without a financial preview is how systems create accounting disasters with a user interface.

---

### Step 5: Final Confirmation

Require strong confirmation text.

Example:

```text
Type CLOSE 2024-2025 to confirm.
```

The confirmation screen should clearly state:

```text
The old school year will become read-only.
A new school year will become active.
Students will be promoted based on the preview.
Unpaid parent balances will be transferred to the new year.
This action will be logged.
```

---

## 5. Pre-Closure Validation

Before closure, validate the following:

```text
No unposted payments
No unresolved payment allocations
No invalid or duplicate parent accounts
No active year conflict
No overlapping new school year dates
No students missing promotion decisions
No draft invoices that should be posted
No financial inconsistencies between invoice amount, paid amount, and balance due
Closing user has the required permission
```

Example validation response:

```json
{
  "can_close": false,
  "errors": [
    "12 payments are not allocated.",
    "3 students have no promotion decision.",
    "New school year overlaps with 2024-2025."
  ]
}
```

Closure must be blocked if critical errors exist.

---

## 6. Read-Only Enforcement

Closed years must be protected in both the frontend and backend.

### 6.1 Backend Guard

Every create, update, or delete endpoint touching year-based records must check the school-year status.

```js
async function assertSchoolYearEditable(schoolYearId) {
  const schoolYear = await SchoolYear.findById(schoolYearId);

  if (!schoolYear) {
    throw new Error("School year not found.");
  }

  if (schoolYear.status !== "active") {
    throw new Error("This school year is closed and read-only.");
  }
}
```

Apply the guard to:

```text
attendance
grades
exams
invoices
payments
payment allocations
student enrollments
class assignments
teacher assignments
fee structures
discipline records
imports
bulk updates
```

Backend enforcement is mandatory. Frontend-only protection is not security.

---

### 6.2 Frontend Read-Only Mode

When the selected school year is not active:

```js
const isReadOnly = selectedSchoolYear.status !== "active";
```

Disable:

```text
Add
Edit
Delete
Import
Submit
Generate invoices
Record payments
Change class
Promote student
Bulk update
```

Show a clear banner:

```text
Viewing school year 2024-2025. Read-only mode.
```

---

## 7. School-Year Selector

Add a global school-year selector in the application header.

Example:

```text
School Year: [2025-2026 Current ▼]
```

Dropdown options:

```text
2025-2026 Current
2024-2025 Closed
2023-2024 Archived
```

Every page must filter by the selected `school_year_id`.

Example:

```sql
SELECT * FROM student_enrollments
WHERE school_year_id = :selectedSchoolYearId;
```

Never mix data across years unless the report specifically requires it.

---

## 8. Parent Balance Transfer Logic

### 8.1 Rule

At closure, unpaid balances must be calculated by `parent_id`.

Formula:

```text
Parent unpaid balance = SUM(invoice.balance_due)
WHERE invoice.parent_id = parent_id
AND invoice.school_year_id = old_school_year_id
AND invoice.status IN ('unpaid', 'partially_paid')
AND invoice.balance_due > 0
```

SQL:

```sql
SELECT
  parent_id,
  SUM(balance_due) AS unpaid_balance
FROM invoices
WHERE school_year_id = :oldSchoolYearId
  AND status IN ('unpaid', 'partially_paid')
  AND balance_due > 0
GROUP BY parent_id;
```

---

### 8.2 Transfer Process

For each parent with unpaid balance:

1. Calculate total unpaid balance by `parent_id`.
2. Collect source unpaid invoice IDs.
3. Create or update the new-year parent account.
4. Set the transferred amount as the new-year opening balance.
5. Create a new-year invoice with `invoice_type = 'old_balance'`.
6. Create a `parent_balance_transfers` record.
7. Link the new old-balance invoice to the transfer record.
8. Keep old invoices unchanged and locked in the closed year.

---

### 8.3 Recommended Handling

Use both:

```text
parent_accounts.opening_balance
```

and:

```text
invoices.invoice_type = old_balance
```

But do not double-count them.

Recommended rule:

```text
opening_balance = informational and audit value
old_balance invoice = collectible amount
current_balance = sum(new-year invoices) - sum(new-year payments)
```

The old-balance invoice is what parents pay against in the new year.

---

### 8.4 Old-Balance Invoice Example

```sql
INSERT INTO invoices (
  parent_id,
  student_id,
  school_year_id,
  amount,
  paid_amount,
  balance_due,
  status,
  invoice_type,
  description,
  balance_transfer_id,
  created_at,
  updated_at
)
VALUES (
  :parentId,
  NULL,
  :newSchoolYearId,
  :unpaidBalance,
  0,
  :unpaidBalance,
  'unpaid',
  'old_balance',
  'Previous year unpaid balance transferred to new school year',
  :balanceTransferId,
  NOW(),
  NOW()
);
```

`student_id` should be `NULL` because this is a parent-level debt.

---

## 9. Handling Credit Balances

If a parent overpaid in the old year, the system should transfer the credit to the new year.

Example:

```text
Old year balance: -100.00
New year opening credit: -100.00
```

The credit can reduce future invoices.

Credit transfer logic should be handled separately from unpaid balance transfer.

Recommended behavior:

```text
Positive balance = amount parent owes
Negative balance = credit parent owns
Zero balance = no transfer
```

---

## 10. Closure Transaction

The entire close-year process must be atomic.

Pseudo-code:

```js
async function closeSchoolYear({
  oldSchoolYearId,
  newSchoolYearData,
  promotionRules,
  adminUserId
}) {
  return await db.transaction(async (trx) => {
    const oldYear = await SchoolYear.findById(oldSchoolYearId, trx);

    if (!oldYear || oldYear.status !== "active") {
      throw new Error("Only the active school year can be closed.");
    }

    await validateYearCanBeClosed(oldSchoolYearId, trx);

    const newYear = await SchoolYear.create({
      name: newSchoolYearData.name,
      start_date: newSchoolYearData.startDate,
      end_date: newSchoolYearData.endDate,
      status: "active",
      is_current: true
    }, trx);

    await SchoolYear.update(oldSchoolYearId, {
      status: "closed",
      is_current: false,
      closed_at: new Date(),
      closed_by: adminUserId
    }, trx);

    await promoteStudents({
      fromSchoolYearId: oldSchoolYearId,
      toSchoolYearId: newYear.id,
      promotionRules,
      trx
    });

    await transferParentBalances({
      fromSchoolYearId: oldSchoolYearId,
      toSchoolYearId: newYear.id,
      adminUserId,
      trx
    });

    await copyReusableAcademicSettings({
      fromSchoolYearId: oldSchoolYearId,
      toSchoolYearId: newYear.id,
      trx
    });

    await AuditLog.create({
      user_id: adminUserId,
      action: "CLOSE_SCHOOL_YEAR",
      record_id: oldSchoolYearId,
      metadata: {
        from_school_year_id: oldSchoolYearId,
        to_school_year_id: newYear.id
      }
    }, trx);

    return newYear;
  });
}
```

If anything fails, rollback everything.

---

## 11. Parent Balance Transfer Function

Example using a Knex-like query builder:

```js
async function transferParentBalances({
  fromSchoolYearId,
  toSchoolYearId,
  adminUserId,
  trx
}) {
  const balances = await trx("invoices")
    .select("parent_id")
    .sum("balance_due as unpaid_balance")
    .where("school_year_id", fromSchoolYearId)
    .whereIn("status", ["unpaid", "partially_paid"])
    .where("balance_due", ">", 0)
    .groupBy("parent_id");

  for (const row of balances) {
    const parentId = row.parent_id;
    const unpaidBalance = Number(row.unpaid_balance);

    if (unpaidBalance <= 0) continue;

    const existingTransfer = await trx("parent_balance_transfers")
      .where({
        parent_id: parentId,
        from_school_year_id: fromSchoolYearId,
        to_school_year_id: toSchoolYearId
      })
      .first();

    if (existingTransfer) {
      throw new Error(`Balance already transferred for parent ${parentId}.`);
    }

    const sourceInvoices = await trx("invoices")
      .select("id", "amount", "paid_amount", "balance_due")
      .where("school_year_id", fromSchoolYearId)
      .where("parent_id", parentId)
      .whereIn("status", ["unpaid", "partially_paid"])
      .where("balance_due", ">", 0);

    let parentAccount = await trx("parent_accounts")
      .where({
        parent_id: parentId,
        school_year_id: toSchoolYearId
      })
      .first();

    if (!parentAccount) {
      const insertedAccounts = await trx("parent_accounts")
        .insert({
          parent_id: parentId,
          school_year_id: toSchoolYearId,
          opening_balance: unpaidBalance,
          current_balance: unpaidBalance,
          created_at: new Date(),
          updated_at: new Date()
        })
        .returning("*");

      parentAccount = insertedAccounts[0];
    } else {
      await trx("parent_accounts")
        .where({ id: parentAccount.id })
        .update({
          opening_balance: trx.raw("opening_balance + ?", [unpaidBalance]),
          current_balance: trx.raw("current_balance + ?", [unpaidBalance]),
          updated_at: new Date()
        });
    }

    const insertedTransfers = await trx("parent_balance_transfers")
      .insert({
        parent_id: parentId,
        from_school_year_id: fromSchoolYearId,
        to_school_year_id: toSchoolYearId,
        amount: unpaidBalance,
        status: "transferred",
        source_summary: JSON.stringify({
          unpaid_invoice_ids: sourceInvoices.map(invoice => invoice.id),
          invoices: sourceInvoices,
          remaining_balance: unpaidBalance
        }),
        created_by: adminUserId,
        created_at: new Date()
      })
      .returning("*");

    const transfer = insertedTransfers[0];

    await trx("invoices").insert({
      parent_id: parentId,
      student_id: null,
      school_year_id: toSchoolYearId,
      amount: unpaidBalance,
      paid_amount: 0,
      balance_due: unpaidBalance,
      status: "unpaid",
      invoice_type: "old_balance",
      description: `Previous year unpaid balance transferred from school year ${fromSchoolYearId}`,
      balance_transfer_id: transfer.id,
      created_at: new Date(),
      updated_at: new Date()
    });
  }
}
```

---

## 12. Payments After Closure

Recommended rule:

```text
Payments for closed years cannot be posted directly to the closed year.
Payments must be posted in the active year against the transferred old-balance invoice.
```

Example:

```text
Old year unpaid balance: 400.00
New year old-balance invoice: 400.00
Parent pays: 100.00
Remaining old-balance invoice: 300.00
```

The old year remains historically accurate and read-only.

---

## 13. API Endpoints

Recommended endpoints:

```text
GET    /api/school-years
GET    /api/school-years/:id/summary
POST   /api/school-years/:id/validate-close
POST   /api/school-years/:id/preview-close
POST   /api/school-years/:id/close
POST   /api/school-years/:id/reopen
GET    /api/school-years/:id/parent-balances
GET    /api/school-years/:id/promotion-preview
```

Example close request:

```json
{
  "new_school_year": {
    "name": "2025-2026",
    "start_date": "2025-09-01",
    "end_date": "2026-06-30"
  },
  "promotion_rules": {
    "default_action": "promote",
    "exceptions": [
      {
        "student_id": 104,
        "action": "repeat",
        "target_grade_id": 3
      },
      {
        "student_id": 220,
        "action": "graduate"
      }
    ]
  },
  "transfer_unpaid_balances": true
}
```

---

## 14. Permissions

Recommended permissions:

```text
school_year.view
school_year.close
school_year.reopen
school_year.archive
school_year.view_closed
finance.transfer_balances
finance.view_parent_balances
```

Recommended roles allowed to close years:

```text
Super Admin
Director
Principal
Finance Admin
```

Teachers and normal staff should be allowed to view closed years only if their role permits it.

---

## 15. Reports

### 15.1 Closing Report

Should include:

```text
Closed school year
New school year
Closed by
Closed at
Total students promoted
Total students repeated
Total students graduated
Total students withdrawn/transferred
Total unpaid balance transferred
Total credit transferred
Number of parents with transferred balances
```

---

### 15.2 Parent Balance Transfer Report

Should include:

```text
Parent ID
Parent name
Old school year
New school year
Old unpaid amount
New old-balance invoice ID
Transfer date
Transfer status
Source invoice IDs
```

---

### 15.3 Parent Statement

When viewing a parent in the new year, show:

```text
Opening balance from previous school year
New year invoices
Payments
Credits
Current balance
```

Example:

```text
Opening balance from 2024-2025: 400.00
New year tuition: 1,000.00
Payments: 250.00
Current balance: 1,150.00
```

---

## 16. Edge Cases

### Parent Has Multiple Students

Group all unpaid balances by parent.

Correct:

```text
Student A unpaid: 200.00
Student B unpaid: 300.00
Parent transferred balance: 500.00
```

---

### Parent Has Credit Balance

Transfer credit to the new year.

```text
Old year balance: -100.00
New year opening credit: -100.00
```

---

### Parent Has Zero Balance

Do not create a transfer.

---

### Student Is Not Continuing

The parent balance still transfers if the parent owes money.

The debt belongs to the parent account, not to whether the child continues enrollment.

---

### Payment Happens During Closure

The system should temporarily block payment posting during the closure transaction.

Recommended protected operations during closure:

```text
payment posting
invoice generation
enrollment changes
student promotion changes
fee changes
bulk imports
```

---

### Closure Fails Halfway

Rollback everything.

There must be no partial new year, half-promoted students, or duplicated transferred invoices.

---

## 17. Implementation Phases

### Phase 1: Data Model Preparation

Deliverables:

```text
school_years table
student_enrollments table
parent_accounts table
parent_balance_transfers table
balance_transfer_id on invoices
school_year_id added to all academic and financial records
```

Acceptance criteria:

```text
All year-based records can be filtered by school_year_id
Only one active year exists
Parent balances can be calculated by parent_id and school_year_id
```

---

### Phase 2: Read-Only Enforcement

Deliverables:

```text
Backend school-year edit guard
Frontend read-only mode
Global school-year selector
Closed-year banner
Permission checks
```

Acceptance criteria:

```text
Users can view closed years
Users cannot modify closed-year records
API rejects closed-year write requests
UI disables editing for closed years
```

---

### Phase 3: Close-Year Preview

Deliverables:

```text
Close-year validation endpoint
Promotion preview
Parent unpaid balance preview
Warnings and blockers
```

Acceptance criteria:

```text
Admin sees students to promote, repeat, graduate, or withdraw
Admin sees unpaid balances grouped by parent_id
System blocks closure if critical validation fails
```

---

### Phase 4: Closure Execution

Deliverables:

```text
Transactional close-year service
New-year creation
Student promotion
Reusable settings copy
Parent balance transfer
Old-balance invoice creation
Audit logs
```

Acceptance criteria:

```text
Old year becomes closed/read-only
New year becomes active
Student enrollments are created for the new year
Parent unpaid balances appear in the new year
Old-balance invoices are traceable to transfer records
Duplicate transfers are blocked
```

---

### Phase 5: Reports and Reconciliation

Deliverables:

```text
Closing report
Parent balance transfer report
Parent statement update
Finance dashboard update
```

Acceptance criteria:

```text
Finance can reconcile old unpaid balances against transferred balances
Parent accounts show previous-year balances clearly
No double-counting occurs
No duplicate transfer records exist
```

---

## 18. Testing Plan

### 18.1 Unit Tests

Test:

```text
Only one active year is allowed
Closed year rejects edits
Unpaid invoices are grouped by parent_id
Old-balance invoice is created correctly
Duplicate transfers are blocked
Credit balances are handled correctly
Zero balances are ignored
Student promotion creates new enrollments
Old enrollments remain unchanged
```

---

### 18.2 Integration Tests

Test full closure:

```text
Create active school year
Create students and parents
Create invoices
Add partial payments
Close school year
Verify old year is read-only
Verify new year is active
Verify students are promoted
Verify parent balances are transferred
Verify old-balance invoices are created
Verify audit logs are created
```

---

### 18.3 Financial Test Example

Before closure:

```text
Parent 1:
Invoice A: 500.00, paid 300.00, balance 200.00
Invoice B: 400.00, paid 0.00, balance 400.00

Parent 2:
Invoice C: 600.00, paid 600.00, balance 0.00

Parent 3:
Invoice D: 300.00, paid 350.00, balance -50.00
```

Expected after closure:

```text
Parent 1 new year old_balance invoice: 600.00
Parent 2 no transfer
Parent 3 new year credit: -50.00
```

---

## 19. Final Expected Behavior

When the school year is closed:

```text
Old school year:
- status becomes closed
- all records become read-only
- unpaid invoices remain historically visible
- financial records are locked

New school year:
- status becomes active
- promoted students receive new enrollments
- parent accounts are created or updated
- unpaid balances from the old year are transferred by parent_id
- old-balance invoices are created for collection
- transferred balances are traceable to the original invoices
```

The key architecture rule is simple:

```text
Academic history belongs to school_year_id.
Financial responsibility belongs to parent_id.
Debt transfer must be parent-level, traceable, and protected against duplication.
```

This gives the school a clean, auditable, and safe year-closure process instead of the usual manual spreadsheet ritual, which is basically accounting with a blindfold.
