26 KiB
Full School Year Management Interface Implementation Plan
1. Objective
Replace the current basic school-year CRUD screen with a complete School Year Management module for CodeIgniter 4.
The final module must support the full lifecycle of a school year:
draft → active → closing → closed → archived
It must also support:
- creating and editing school years
- activating exactly one year
- validating year dates and naming
- previewing school-year closure
- identifying blocking issues
- calculating balances to carry forward
- executing closure safely
- recording closure batches and results
- reopening a closed year only under strict rules
- archiving historical years
- preventing unsafe deletion
- enforcing permissions
- showing read-only behavior for closed years
- auditing every critical action
The interface must not allow arbitrary status changes through a general edit form.
2. Current Screen Assessment
The current view already provides:
- school-year listing
- create form
- edit modal
- status display
- activate action
- delete action
- CSRF protection
- DataTables sorting and pagination
- flash messages
The current view must be treated as the starting point only.
The following current behaviors must be removed or restricted:
- direct status editing from the edit modal
- deleting every non-active year
- activating a year without validation
- relying on frontend checks for business rules
- showing all actions to every user
- treating all non-active statuses the same
- allowing closure state to be changed through CRUD
3. School-Year Lifecycle Rules
Use these statuses:
draft
active
closing
closed
archived
Draft
Allowed:
- edit name and dates
- configure semesters
- initialize classes, fees, and assignments
- activate
- delete only when no dependent records exist
Not allowed:
- carry-forward execution
- closure execution
- archive
Active
Allowed:
- normal school operations
- edit limited metadata where safe
- start closing preview
- begin closing
Not allowed:
- delete
- archive directly
- activate another year without a controlled transition
Closing
Allowed:
- run closing validations
- refresh preview
- resolve blocking issues
- execute carry-forward
- complete closure
- cancel closing only before irreversible actions
Not allowed:
- normal unrestricted edits
- delete
- archive
- reactivate another year without resolving the closing state
Closed
Allowed:
- historical viewing
- closing report viewing
- audit review
- controlled reopen action if policy permits
- archive
Not allowed:
- normal inserts, updates, or deletes
- direct activation
- deletion
Archived
Allowed:
- read-only viewing
- audit and report access
Not allowed:
- edits
- reopening by normal administrators
- deletion
- activation
4. Replace Direct Status Editing
Remove the status dropdown from the general edit modal.
The edit form should only update:
- school-year name
- start date
- end date
- optional description
- optional registration start/end dates
Status changes must use dedicated actions:
Activate
Begin Closing
Cancel Closing
Complete Closing
Reopen
Archive
Each action must call a dedicated controller method and service method.
Do not use a generic update action to change lifecycle state.
5. Backend Architecture
Create or update these components:
app/Controllers/Administrator/SchoolYearController.php
app/Controllers/Administrator/SchoolYearClosingController.php
app/Models/SchoolYearModel.php
app/Models/SchoolYearClosingBatchModel.php
app/Models/SchoolYearClosingItemModel.php
app/Models/SchoolYearTransitionLogModel.php
app/Services/SchoolYearManagementService.php
app/Services/SchoolYearClosingService.php
app/Services/SchoolYearContextService.php
app/Services/SchoolYearWriteGuard.php
app/Services/SchoolYearValidationService.php
app/Repositories/SchoolYearRepository.php
app/Repositories/SchoolYearClosingRepository.php
app/Support/SchoolYear/SchoolYearStatus.php
app/Support/SchoolYear/SchoolYearTableRegistry.php
Responsibilities must be separated:
- controller: request/response handling
- model: table persistence
- repository: complex database queries
- service: business rules and transactions
- write guard: read-only enforcement
- validation service: date, status, and dependency checks
Controllers must remain thin.
6. School-Year Model
The school_years table should include at least:
id
name
starts_on
ends_on
status
activated_at
closing_started_at
closed_at
archived_at
created_by
updated_by
created_at
updated_at
Optional fields:
description
registration_starts_on
registration_ends_on
previous_school_year_id
next_school_year_id
Recommended model validation:
protected $validationRules = [
'name' => 'required|regex_match[/^\d{4}-\d{4}$/]',
'starts_on' => 'required|valid_date[Y-m-d]',
'ends_on' => 'required|valid_date[Y-m-d]',
];
Add service-level validation for rules CodeIgniter validation cannot safely express:
- second year equals first year plus one
- start date is before end date
- duplicate name is rejected
- date overlap is rejected where prohibited
- only one active year exists
- lifecycle transition is allowed
- target year is valid for carry-forward
7. Status Transition Service
Create a status transition map.
private const ALLOWED_TRANSITIONS = [
'draft' => ['active'],
'active' => ['closing'],
'closing' => ['active', 'closed'],
'closed' => ['active', 'archived'],
'archived' => [],
];
Do not rely only on this map. Additional business validation is required.
Examples:
closed → activerequires reopen permission and dependency checksclosing → activeis allowed only before closing batches are finalizedactive → closingrequires a closing target yearclosed → archivedrequires all closing records to be finalized
Every transition must:
- validate current status
- validate target status
- validate permissions
- validate dependencies
- run inside a database transaction
- record the transition
- write an audit log
- return a structured result
8. Activation Workflow
Add an activation modal and dedicated endpoint.
Activation must:
- verify the selected year exists
- require status
draftor approved reopened state - verify start and end dates
- verify no conflicting active year
- identify the current active year
- require confirmation when replacing an active year
- avoid automatically closing the previous active year
- update the selected year to
active - update any previous active year according to policy
- initialize required year-owned records
- save
activated_at - record the user performing activation
- write an audit entry
Recommended policy:
Only one school year may have status active.
Activating a draft year changes the previous active year to closing,
not closed.
This forces the previous year through a real closure process instead of pretending changing a label completed accounting.
9. School-Year Initialization
When a year is activated, optionally initialize selected data from the previous year.
Allow the administrator to choose:
- copy class definitions
- copy section definitions
- copy tuition and fee templates
- copy event charge templates
- copy teacher-class assignment templates
- copy calendar templates
- copy communication templates
- create standard semesters
- carry selected active family/student relationships
Do not copy:
- invoices
- payments
- attendance
- grades
- finalized events
- messages
- audit logs
- historical transactions
Initialization must be idempotent. Re-running it must not create duplicate records.
10. Closing Preview
Add a dedicated Closing Preview page.
Route:
GET administrator/school-years/{id}/closing/preview
The preview must display:
School-year summary
- school-year name
- status
- start and end dates
- number of students
- number of families
- number of classes
- number of teachers
- number of invoices
- total invoiced
- total paid
- total outstanding
Financial validation
- families with positive balances
- families with negative balances or credits
- unpaid invoices
- partially paid invoices
- unapplied payments
- failed payment transactions
- pending reimbursements
- pending refunds
- unresolved discounts
- unposted event charges
- mismatched invoice/payment school years
Academic validation
- students without final class placement
- students without promotion decisions
- missing final scores
- incomplete report cards
- incomplete attendance records
- teachers with incomplete class submissions
- orphaned class or enrollment records
Operational validation
- active events extending beyond the year
- unclosed parent meetings
- pending print requests
- pending acknowledgements
- inconsistent school-year relationships
- records missing
school_year_id
Carry-forward preview
For each family:
family
source balance
credits
adjustments
net carry-forward
target year
proposed target record
The preview must classify findings as:
blocking
warning
informational
Closure cannot proceed while blocking findings exist.
11. Closing Batch Tables
Create:
school_year_closing_batches
Suggested columns:
id
source_school_year_id
target_school_year_id
status
preview_hash
total_families
total_positive_balance
total_credit_balance
started_by
completed_by
started_at
completed_at
failed_at
failure_message
created_at
updated_at
Create:
school_year_closing_items
Suggested columns:
id
closing_batch_id
family_id
source_balance
credit_amount
adjustment_amount
carry_forward_amount
target_invoice_id
target_adjustment_id
status
error_message
created_at
updated_at
Create:
school_year_transition_logs
Suggested columns:
id
school_year_id
from_status
to_status
action
performed_by
metadata_json
created_at
Add unique constraints to prevent duplicate finalized carry-forward for the same source year, target year, and family.
12. Begin Closing Workflow
Add a dedicated action:
POST administrator/school-years/{id}/closing/start
The service must:
- require current status
active - require a valid target year
- verify target year is
draftoractiveaccording to policy - run the latest preview
- reject when blocking issues exist
- create a closing batch
- save preview totals and hash
- change source status to
closing - record
closing_started_at - write transition and audit logs
The preview hash should detect material data changes between preview and execution.
13. Carry-Forward Execution
Add:
POST administrator/school-years/{id}/closing/execute
Execution must run inside one database transaction.
For each family with a carry-forward amount:
- calculate final source-year balance
- compare it with previewed balance
- reject if materially changed
- create a target-year opening balance or carry-forward invoice
- link it to the closing batch item
- avoid duplicate creation
- record credits separately from debt if accounting rules require it
- preserve source-year records unchanged
Recommended target records:
opening_balance_transactions
or a dedicated invoice type:
invoice_type = carry_forward
Do not silently merge historical debt into normal tuition invoices.
Execution must be idempotent:
- rerunning a completed batch does nothing
- rerunning a partially failed batch must safely resume or fully roll back
- duplicate carry-forward records must be prevented by database constraints
14. Complete Closing
Add:
POST administrator/school-years/{id}/closing/complete
Completion must verify:
- source status is
closing - closing batch exists
- all batch items succeeded
- no new blocking issues appeared
- carry-forward totals match
- all required reports are generated
- target-year links are valid
Then:
- set source status to
closed - set
closed_at - finalize closing batch
- make source year read-only
- create final closing report
- write transition and audit logs
15. Cancel Closing
Add:
POST administrator/school-years/{id}/closing/cancel
Allow cancellation only when:
- status is
closing - no finalized carry-forward records exist
- no irreversible batch step has completed
Cancellation must:
- mark the batch cancelled
- restore status to
active - preserve audit history
- not delete logs
If carry-forward records already exist, require rollback through a dedicated recovery operation instead of pretending a status change reverses accounting.
16. Reopen Workflow
Add:
POST administrator/school-years/{id}/reopen
Reopening must require a high-level permission.
Validate:
- year status is
closed - target/current year financial activity will not be corrupted
- no dependent archived reports prohibit changes
- no carry-forward records would become inconsistent
- administrator provides a reason
Recommended reopening behavior:
- set status to
activeorclosingaccording to policy - invalidate affected closing reports
- preserve the original closing batch
- create a reopen transition record
- require a new closing batch before closing again
Archived years should not be reopened through the normal interface.
17. Archive Workflow
Add:
POST administrator/school-years/{id}/archive
Allow only when:
- status is
closed - closing batch is finalized
- no pending reopen operation exists
- administrator has archive permission
Archiving must:
- set status
archived - set
archived_at - remove it from default operational selectors
- keep it available in historical search
- enforce strict read-only access
18. Delete Rules
Replace the current generic delete action with deleteDraft.
Route:
POST administrator/school-years/{id}/delete-draft
Allow deletion only when:
- status is
draft - the year is not active
- no year-owned records exist
- no semester records exist
- no closing batches exist
- no transition history other than creation exists
- user has delete permission
Otherwise show:
This school year cannot be deleted because related data exists.
Archive historical years instead.
Never delete active, closing, closed, or archived years.
19. Permissions
Add permissions such as:
school_year.view
school_year.create
school_year.edit
school_year.activate
school_year.close.preview
school_year.close.start
school_year.close.execute
school_year.close.complete
school_year.close.cancel
school_year.reopen
school_year.archive
school_year.delete_draft
school_year.view_financial_closing
Enforce permissions:
- in routes or filters
- in controllers
- in services for critical actions
- in the view for button visibility
Hiding a button is user experience. It is not authorization.
20. Interface Redesign
Replace the current single-table CRUD experience with the following sections.
Header
Display:
- page title
- active year badge
- selected year
- add school year button
- historical years filter
Summary cards
Show:
- active year
- next draft year
- year currently closing
- total archived years
Main table columns
School Year
Dates
Status
Students
Families
Financial Balance
Last Transition
Updated
Actions
Status badges
Use distinct styles:
$statusClasses = [
'draft' => 'bg-info text-dark',
'active' => 'bg-success',
'closing' => 'bg-warning text-dark',
'closed' => 'bg-secondary',
'archived' => 'bg-dark',
];
Action menu by status
Draft
- Edit
- Configure semesters
- Initialize
- Activate
- Delete draft
Active
- View dashboard
- Edit limited metadata
- Preview closing
- Begin closing
Closing
- View closing preview
- Resolve issues
- Execute carry-forward
- Complete closing
- Cancel closing when allowed
Closed
- View closing report
- View historical data
- Reopen when authorized
- Archive
Archived
- View historical data
- View audit trail
Use a dropdown action menu instead of displaying every action inline.
21. Create School Year Modal
The create form should include:
- school-year name
- starts on
- ends on
- optional description
- optional registration period
- initialize semesters checkbox
- copy configuration from previous year checkbox
- source year selector when copying
Do not allow the user to create a year directly as active, closing, closed, or archived.
All new years start as:
draft
Activation is a separate operation.
22. Edit School Year Modal
Allow editing only:
- name
- dates
- description
- registration dates
Restrictions:
- draft: full metadata editing
- active: limited editing with warnings
- closing: mostly locked
- closed: read-only
- archived: read-only
Remove status from the modal.
23. Activation Confirmation Modal
Display:
- year being activated
- current active year
- resulting status of current active year
- initialization options
- explicit confirmation checkbox
- warning that activation does not complete closure
Require a typed confirmation only if activation affects a currently active year.
24. Closing Preview Page
Create a dedicated full-page interface rather than a small modal.
Sections:
- overview
- blocking issues
- warnings
- finance summary
- carry-forward families
- academic completion
- operational completion
- target-year validation
- audit and preview timestamp
Actions:
- refresh preview
- export preview
- return to management
- begin closing
- execute carry-forward
- complete closing
Disable actions based on current status and validation results.
25. Closing Progress Interface
During closing, show a deterministic progress state based on persisted batch data:
Preview generated
Validation passed
Closing batch created
Carry-forward prepared
Carry-forward completed
Final validation passed
Year closed
Do not fake progress with a frontend timer.
The page should reload or poll the batch status only when the backend supports resumable execution.
26. Closing Report
Generate a final report containing:
- source school year
- target school year
- completion date
- administrator
- total families processed
- total balances carried
- total credits carried
- failed or skipped items
- financial reconciliation totals
- academic completion summary
- warning acknowledgements
- transition history
Allow export to CSV or PDF later, but the database report must exist first.
27. Semester Management
Add a school-year detail tab for semesters.
Suggested table:
school_year_semesters
Columns:
id
school_year_id
name
code
starts_on
ends_on
status
sort_order
created_at
updated_at
Rules:
- semester dates must fall within the school year
- semesters must not overlap
- semester order must be explicit
- closed semesters reject grade and attendance edits where required
- not every year-owned table needs a semester column
Do not use a global configuration.semester value as the sole source of truth.
28. Read-Only Enforcement
Use SchoolYearWriteGuard in every year-owned write path.
The interface should disable or hide mutation controls for closed and archived years, but backend enforcement remains mandatory.
API and controller errors should return clear messages such as:
The selected school year is closed and cannot be modified.
29. Routes
Recommended routes:
$routes->group(
'administrator/school-years',
['filter' => 'auth'],
static function ($routes) {
$routes->get('/', 'Administrator\SchoolYearController::index');
$routes->post('store', 'Administrator\SchoolYearController::store');
$routes->post(
'(:num)/update',
'Administrator\SchoolYearController::update/$1'
);
$routes->post(
'(:num)/activate',
'Administrator\SchoolYearController::activate/$1'
);
$routes->post(
'(:num)/delete-draft',
'Administrator\SchoolYearController::deleteDraft/$1'
);
$routes->post(
'(:num)/archive',
'Administrator\SchoolYearController::archive/$1'
);
$routes->post(
'(:num)/reopen',
'Administrator\SchoolYearController::reopen/$1'
);
$routes->get(
'(:num)/closing/preview',
'Administrator\SchoolYearClosingController::preview/$1'
);
$routes->post(
'(:num)/closing/start',
'Administrator\SchoolYearClosingController::start/$1'
);
$routes->post(
'(:num)/closing/execute',
'Administrator\SchoolYearClosingController::execute/$1'
);
$routes->post(
'(:num)/closing/complete',
'Administrator\SchoolYearClosingController::complete/$1'
);
$routes->post(
'(:num)/closing/cancel',
'Administrator\SchoolYearClosingController::cancel/$1'
);
}
);
Use exact route names consistently. The existing route ending in /active should be replaced with /activate.
30. Transactions and Locking
Use database transactions for:
- activation
- starting closing
- carry-forward execution
- completing closing
- reopening
- archiving
Where supported, lock the school-year row during critical transitions.
Before each operation, re-read the current status inside the transaction.
Do not trust the status that was displayed when the page loaded. Another administrator may have changed it since then, because concurrency exists even when the interface would prefer otherwise.
31. Audit Logging
Record:
- creation
- metadata edits
- activation
- initialization
- closing preview generation
- closing start
- carry-forward execution
- closing completion
- cancellation
- reopening
- archive
- failed transition attempts
Audit metadata should include:
user_id
school_year_id
action
from_status
to_status
reason
request_id
ip_address
summary
created_at
Do not store sensitive payment data in audit metadata.
32. Validation and Error Handling
Return clear messages for:
- duplicate year
- invalid name sequence
- date overlap
- invalid status transition
- second active year attempt
- unresolved closing blockers
- changed data since preview
- missing target year
- target year invalid
- duplicate carry-forward
- unauthorized operation
- deletion blocked by dependencies
Use flash messages for normal web redirects and structured errors for API endpoints.
33. Testing Plan
Unit tests
Test:
- valid and invalid status transitions
- name validation
- date validation
- overlap detection
- activation policy
- write guard
- carry-forward calculation
- permission checks
- reopening rules
Database tests
Test:
- only one active year
- draft deletion with no dependencies
- deletion blocked with dependencies
- closing batch creation
- unique carry-forward records
- transaction rollback
- status transition logs
Feature tests
Test every route:
- create
- edit
- activate
- preview
- start closing
- execute
- complete
- cancel
- reopen
- archive
- delete draft
Verify CSRF and permission enforcement.
Finance regression tests
Test:
- old-year debt does not leak into current-year totals
- carry-forward matches source closing balance
- credits are handled correctly
- preview and execution totals match
- duplicate execution is blocked
- rollback leaves no partial target-year records
UI tests
Test:
- buttons appear by status and permission
- closed years are read-only
- correct status badges display
- preview blockers disable execution
- action confirmation modals show correct year
- table sorting works
- dropdowns are alphabetically sorted
- mobile layout remains usable
34. Implementation Phases
Phase 1: Foundation
- add lifecycle status constants
- update database schema
- update model validation
- add management and validation services
- add transition logging
- remove direct status editing
Phase 2: CRUD Hardening
- force new years to draft
- validate dates and names
- restrict editing by status
- replace delete with delete-draft
- add permissions
Phase 3: Activation
- implement activation service
- enforce one active year
- add initialization options
- add activation modal
- add tests
Phase 4: Closing Preview
- build preview queries
- classify blockers and warnings
- add closing preview page
- add target-year validation
- add export-ready result structure
Phase 5: Closing Execution
- create batch tables
- implement start closing
- implement carry-forward
- add idempotency and constraints
- implement completion and cancellation
- add reconciliation tests
Phase 6: Historical Management
- implement reopen
- implement archive
- add closing reports
- add historical filters
- enforce read-only behavior
Phase 7: Semester Management
- create year-semester table
- add semester configuration tab
- validate semester dates
- integrate semester write guards
Phase 8: Final Hardening
- add audit coverage
- add concurrency checks
- add static checks
- run full regression tests
- review production migration and rollback steps
35. Acceptance Criteria
The School Year Management module is complete only when:
- every new school year starts as draft
- status cannot be changed from the edit form
- only one school year can be active
- activation is transactional and audited
- the previous year is not silently considered closed
- closing preview shows financial, academic, and operational blockers
- closure cannot proceed with blocking issues
- carry-forward is transactional and idempotent
- source-year financial records remain unchanged
- closed and archived years are read-only
- deletion is limited to unused draft years
- reopen and archive actions are permission-controlled
- all critical transitions are logged
- conflicting concurrent transitions are rejected
- semester configuration is tied to the correct school year
- tests cover the full lifecycle
- the interface exposes only valid actions for each status
36. Non-Negotiable Rules
- Do not allow arbitrary status editing.
- Do not close a school year by changing one database field.
- Do not carry balances forward without a persisted batch.
- Do not trust frontend school-year values.
- Do not delete historical school years.
- Do not allow closed-year writes without an explicit reopen workflow.
- Do not mix source-year and target-year financial records.
- Do not execute closure without a fresh validated preview.
- Do not use UI button visibility as authorization.
- Do not mark work complete until lifecycle, accounting, permissions, audit, and regression tests all pass.