fix financial and certificates

This commit is contained in:
root
2026-06-05 01:51:08 -04:00
parent d28d11e2e5
commit ad968eaff7
94 changed files with 9654 additions and 214 deletions
@@ -0,0 +1,925 @@
# Strong Enrollment, Registration, Promotion, and Section Placement Plan
## 1. Purpose
This plan defines a stronger, safer lifecycle for student registration, enrollment, promotion, and next-class section placement.
The goal is to separate concepts that are currently too easy to mix together:
- Student identity
- New-student registration
- Returning-student enrollment
- Academic pass/fail decision
- Promotion eligibility
- Class/section placement
- Historical display and auditability
A strong system must not treat all of these as one status field wearing too many hats. That is how school software becomes a haunted spreadsheet.
---
## 2. Core Principle
The system must distinguish between:
| Concept | Meaning | Source of Truth |
|---|---|---|
| Student identity | The person/student record | `students` |
| New-student registration | A new family applies for admission | registration/application tables |
| New-student approval | Admin approves the new student | admin registration/enrollment flow |
| Returning-student enrollment | Parent confirms child is continuing next year | returning enrollment record |
| Academic decision | Student passed, failed, retained, or needs review | `student_decisions` |
| Promotion eligibility | Student may move to next class | derived from `student_decisions` |
| Section placement | Student is assigned to a class section | section placement batch |
| Historical display | Old records remain visible as originally stored | legacy records/tables |
Promotion must not directly mean enrollment. Enrollment must not directly mean class placement. Class placement must not decide pass/fail. Each process gets its own job, because apparently databases behave better when we stop asking one column to run a school.
---
## 3. Backward Compatibility Requirement
The new system must be additive, not destructive.
Existing registration, enrollment, assignment, and promotion data must remain displayable. Old rows must not be silently reinterpreted or recalculated under the new policy.
### Required rules
```text
Old records remain visible.
Old student/class assignments remain readable.
Historical enrollment state is not silently changed.
Historical promotion results are not silently recalculated.
New lifecycle rules apply only to future/new workflow records or explicitly migrated records.
```
### Recommended legacy metadata
Where needed, add metadata such as:
```text
lifecycle_mode = legacy | strong
lifecycle_policy_version = legacy_v1 | strong_v1
```
Existing rows should default to:
```text
lifecycle_mode = legacy
lifecycle_policy_version = legacy_v1
```
This allows the UI to show old data clearly without pretending old rows followed the new workflow.
---
## 4. Student Lifecycle Types
The system must support two main student flows.
## 4.1 New Student Flow
A new student is not already part of the schools prior-year enrollment.
New-student flow:
```text
Parent submits new-student registration
→ Admin reviews application
→ Admin approves or rejects
→ Approved student enters placement pool
→ Student is assigned to class section
→ Enrollment is completed
```
Admin approval is required for new students.
### New-student states
```text
application_draft
application_submitted
admin_review_pending
approved
rejected
waitlisted
placement_pending
placed_in_class
enrollment_completed
```
---
## 4.2 Returning Student Flow
A returning student already exists and was enrolled in the previous school year/class.
Returning-student flow:
```text
Old class/year finalized
→ `student_decisions` records pass/fail/retain/review decision
→ If passed/promoted, student becomes eligible to continue
→ Parent completes returning-student enrollment
→ System validates eligibility
→ Student enters placement pool
→ Student is assigned to class section
→ Enrollment is completed
```
Admin approval is not required for normal returning students.
Admin review is required only for exceptions.
### Returning-student states
```text
academic_not_reviewed
eligible_to_continue
retained
parent_enrollment_pending
parent_enrollment_completed
placement_pending
placed_in_class
enrollment_completed
exception_review_required
```
---
## 5. Pass/Fail Source of Truth
Promotion eligibility must come from `student_decisions`.
The placement system must not recalculate pass/fail. Its job is placement, not academic judgment. Otherwise it becomes judge, registrar, scheduler, and disaster generator.
### Required rule
```text
Promotion placement must read academic eligibility from `student_decisions`.
A student is eligible for next-class placement only when `student_decisions` marks the student as passed/promoted for the completed school year/class.
Failed, retained, pending, or missing-decision students must not enter the automatic next-class placement pool.
```
### Eligibility from `student_decisions`
| Decision | Placement behavior |
|---|---|
| passed/promoted | Eligible for next-class placement after parent enrollment |
| failed/retained | Excluded from automatic next-class placement |
| pending | Exception report |
| missing | Exception report |
| manual review | Exception review |
| withdrawn/inactive | Excluded unless admin override |
### Required exception buckets
The system should report:
```text
Passed but parent enrollment missing
Passed and parent enrollment completed
Failed/retained
Missing student decision
Pending student decision
Decision conflicts with enrollment request
Withdrawn/inactive student
Duplicate target-year enrollment
```
---
## 6. Parent Enrollment Requirement for Returning Students
A returning student must not be placed into the next year/class until the parent completes returning-student enrollment.
### Required rule
```text
For returning students, promotion eligibility plus parent enrollment completion is sufficient to continue enrollment into the next school year, subject to system validation and class placement.
Admin approval is only required for exceptions.
```
### Returning enrollment should capture
```text
student_id
parent_id
target_school_year
requested_grade_level_id
returning_student = true
required forms completed
emergency contact confirmation
medical/contact update confirmation
agreement/consent confirmations
submission timestamp
status
```
### Returning enrollment statuses
```text
not_started
in_progress
submitted
completed
expired
exception_review_required
```
Normal returning-student continuation should use:
```text
parent_enrollment_completed
```
not:
```text
admin_approved
```
Admin approval is for new students and exceptions, not normal returning students.
---
## 7. When Admin Review Is Required for Returning Students
Returning students should flow automatically only when clean.
Admin review is required when:
```text
student failed but parent tries to enroll in next grade
student is retained and needs repeat placement
student has no linked parent/guardian
required forms are incomplete after deadline
requested class/grade conflicts with eligibility
capacity override is needed
manual promotion override is requested
duplicate enrollment exists
student was withdrawn or inactive
student_decisions is missing or pending
```
This keeps normal cases fast and sends only abnormal cases to humans, who are expensive, inconsistent, and sometimes looking for the wrong spreadsheet.
---
## 8. Placement Pool Construction
The final placement pool is built from two sources:
```text
eligible returning students
+ approved new students
```
### 8.1 Returning student inclusion rule
A returning student enters the placement pool only if:
```text
`student_decisions` marks student as passed/promoted
parent completed returning-student enrollment
student is not already placed in the target year
target grade/class matches promotion path
student is active/not withdrawn
```
### 8.2 New student inclusion rule
A new student enters the placement pool only if:
```text
new-student registration/enrollment is admin-approved
student is not already placed in the target year
target grade/class is valid
```
---
## 9. Configurable Section Capacity
The section capacity must not be hardcoded.
The maximum number of students per generated section must be read from the `configuration` table as a key/value setting.
### Required configuration key
Recommended key:
```text
promotion.section_capacity = 20
```
### Configuration table example
| key | value | type | description |
|---|---:|---|---|
| `promotion.section_capacity` | `20` | integer | Maximum number of students per generated promoted section |
### Validation
```text
promotion.section_capacity must be an integer
promotion.section_capacity must be greater than 0
recommended minimum: 5
recommended maximum: 40 unless admin override
```
### Fallback rule
If the key is missing:
```text
section_capacity = 20
```
The fallback protects the system, but missing config should still be logged as a warning. Silent defaults are convenient right up until nobody knows why sections were created that way.
### Snapshot rule
The system must store the capacity used at generation time:
```text
section_capacity_used
```
Optional but recommended:
```text
configuration_snapshot_json
```
Example:
```json
{
"promotion.section_capacity": 20,
"placement.algorithm": "score_band_balanced_v1"
}
```
This prevents old placement batches from changing meaning if the capacity is later changed to 22, 25, or whatever fresh number policy invents.
---
## 10. Section Count Rule
Let:
```text
section_capacity = configuration['promotion.section_capacity'] ?? 20
total_students = count(placement_pool)
```
Then:
```text
If total_students <= section_capacity:
create one section
If total_students > section_capacity:
section_count = ceil(total_students / section_capacity)
```
Examples when `promotion.section_capacity = 20`:
| Total students | Section count |
|---:|---:|
| 1-20 | 1 |
| 21-40 | 2 |
| 41-60 | 3 |
| 61-80 | 4 |
---
## 11. Score Bands for Placement
Students must be grouped into score bands for balanced section placement.
### Bands
| Band | Score range |
|---|---:|
| A | 90-100 |
| B | 80-89 |
| C | 70-79 |
| D | 60-69 |
Students below 60 are not eligible for automatic next-class promotion unless an authorized override exists.
### Score source by student type
| Student type | Eligibility source | Band source |
|---|---|---|
| Returning student | `student_decisions` pass/promote | final promotion score |
| Failed/retained student | `student_decisions` fail/retain | excluded from next-class automatic placement |
| New student with placement assessment | approved new-student enrollment | assessment score band |
| New student without placement assessment | approved new-student enrollment | D band by default |
---
## 12. New Student Default Band Rule
New students do not always have prior school scores. If no approved placement/assessment score exists, they must be assigned to the D band for balancing purposes.
### Required rule
```text
New students approved for enrollment are included in the target class placement pool.
If a new student does not have an approved placement or assessment score, the system assigns the student to the D score band, 60-69, for section-balancing purposes only.
This default band assignment must not be stored as an academic grade.
It must be stored as a placement-band classification with the reason `new_student_default_band`.
```
Do not invent a fake numeric score like 65. That would contaminate reports later, because somebody will export it and believe it is real. Humans keep doing that. We design around it.
### Recommended fields
```text
placement_band = D
placement_score = null
placement_band_reason = new_student_default_band
```
If a new student has an approved placement score:
```text
placement_band = derived from placement score
placement_score = approved assessment score
placement_band_reason = new_student_assessment
```
---
## 13. Balanced Section Placement Rule
When there are multiple sections, the system must balance:
1. Total number of students per section.
2. Score-band mix across sections.
The system should not simply divide each score band and accidentally create sections with different total sizes.
### Required rule
```text
When distributing score bands across multiple sections, the system must prioritize equal total section size while keeping each score band as evenly distributed as possible.
Remainders from score bands should be assigned using a balancing algorithm that gives the next student to the section with the lowest current total, not simply to the next section in fixed order.
```
### Target section sizes
Given:
```text
total_students
section_count
```
Calculate:
```text
base_size = floor(total_students / section_count)
remainder = total_students % section_count
```
Then:
```text
first `remainder` sections may have base_size + 1 students
remaining sections have base_size students
```
Example:
```text
32 students
2 sections
target size = 16 and 16
```
Example:
```text
43 students
3 sections
target size = 15, 14, 14
```
---
## 14. Balanced Placement Algorithm
Use a deterministic algorithm.
### Algorithm: score_band_balanced_v1
```text
1. Load section_capacity from configuration.
2. Validate section_capacity.
3. Build placement pool:
- returning students passed/promoted in `student_decisions`
- returning parent enrollment completed
- approved new students
4. Exclude:
- failed/retained students
- missing/pending decision students
- parent enrollment missing for returning students
- duplicate target-year placements
5. Count placement pool.
6. If total_students <= section_capacity:
create one section and assign all students.
7. If total_students > section_capacity:
section_count = ceil(total_students / section_capacity).
8. Calculate target section sizes.
9. Assign each student a placement band:
returning students: final promotion score band
new students with assessment: assessment score band
new students without assessment: D band
10. Group students by band:
A: 90-100
B: 80-89
C: 70-79
D: 60-69
11. Sort bands from highest to lowest: A, B, C, D.
12. Within each band, sort students deterministically:
final/placement score descending when available
student name ascending or student_id ascending as tie-breaker
13. Assign students one by one to the eligible section with:
lowest current total count
then lowest count for that student's score band
then deterministic section order
14. Do not exceed target section size unless all target sizes are full or admin override exists.
15. Validate every student is assigned exactly once.
16. Save placement snapshot.
17. Show admin preview.
18. Finalize only after preview confirmation.
```
### Tie-break order
The section assignment tie-break order must be:
```text
lowest total section count
then lowest count in the student's score band
then deterministic section order
```
This order matters.
If the system prioritizes band equality before total section size, totals can drift. If it prioritizes total size first and band count second, it produces more balanced sections.
---
## 15. Example Balanced Distribution
For 32 eligible students and capacity 20:
```text
section_count = ceil(32 / 20) = 2
target size = 16 and 16
```
A valid balanced result:
| Band | Section A | Section B |
|---|---:|---:|
| 90-100 | 4 | 4 |
| 80-89 | 5 | 5 |
| 70-79 | 4 | 5 |
| 60-69 | 3 | 2 |
| **Total** | **16** | **16** |
This is the desired behavior: both sections have equal total size, and the band mix is as balanced as possible.
---
## 16. Placement Preview Before Finalization
The system must not immediately write final class assignments.
It should first create a draft placement batch.
### Preview should show
```text
section count
capacity used
target section sizes
student count per section
score-band distribution per section
new students defaulted to D band
students excluded from placement
students needing exception review
capacity warnings
duplicate placement warnings
```
Admin should then finalize the preview.
Only after finalization should the system write final class/section assignments.
This avoids the classic software maneuver where a draft becomes reality because somebody clicked the wrong button and the database had no spine.
---
## 17. Placement Snapshot and Audit Tables
Create a placement batch snapshot so the school can explain how students were assigned.
### Recommended table: `section_placement_batches`
```text
id
from_school_year
to_school_year
from_grade_level_id
to_grade_level_id
section_capacity_used
total_students
section_count
algorithm
configuration_snapshot_json
created_by
created_at
finalized_by
finalized_at
status
```
Recommended statuses:
```text
draft
finalized
cancelled
superseded
```
### Recommended table: `section_placement_batch_students`
```text
id
batch_id
student_id
student_type
source_decision_id nullable
source_enrollment_id nullable
final_score nullable
placement_score nullable
score_band
placement_band_reason
assigned_section_id
assignment_order
was_override
override_reason
created_at
```
### Placement band reasons
```text
returning_student_final_score
new_student_assessment
new_student_default_band
admin_override
```
---
## 18. Validation Before Placement Generation
Before generating placement, validate:
```text
target school year exists
target school year is open for enrollment/placement
target grade/class exists
section_capacity config exists or fallback is applied
source class/year is finalized where applicable
`student_decisions` exists for returning students
returning students have passed/promoted decisions
returning students completed parent enrollment
new students are admin-approved
students are not already placed in target year
target grade/class matches the promotion path
```
---
## 19. Validation Before Placement Finalization
Before finalizing placement, validate:
```text
all eligible students are assigned exactly once
no ineligible student is assigned
no student is already assigned to another target-year section
all sections belong to the target year and grade/class
no section exceeds target size unless override is recorded
score-band distribution report was generated
section_capacity_used is stored
placement snapshot is saved
admin finalized the preview
```
---
## 20. Edge Cases
| Case | Rule |
|---|---|
| Score exactly 90 | Band A |
| Score exactly 80 | Band B |
| Score exactly 70 | Band C |
| Score exactly 60 | Band D |
| Score below 60 | Not automatic promotion unless override |
| Missing final score for returning student | Exception report |
| Missing `student_decisions` | Exception report |
| Pending `student_decisions` | Exception report |
| Parent did not complete returning enrollment | Do not place |
| New student without assessment | D band for balancing only |
| New student with assessment score | Band from assessment |
| Duplicate target-year placement | Block and report |
| Capacity exceeded | Admin override required |
| Uneven score bands | Balance total section size first, band mix second |
| Config changes after placement | Old placement uses stored `section_capacity_used` |
---
## 21. Required Reports
The admin should have reports for:
```text
Passed but parent enrollment missing
Ready for placement
New students approved and ready for placement
Missing/pending student decisions
Failed/retained students
Exception review required
Draft placement batches
Finalized placement batches
Capacity and section distribution
New students defaulted to D band
Manual overrides
```
---
## 22. Service Design
Recommended services:
```text
StudentDecisionEligibilityService
ReturningEnrollmentStatusService
NewStudentAdmissionStatusService
PlacementPoolBuilder
PromotionSectionCapacityService
ScoreBandClassifier
BalancedSectionPlacementService
SectionPlacementPreviewService
SectionPlacementFinalizationService
SectionPlacementAuditService
```
### Responsibility split
| Service | Responsibility |
|---|---|
| `StudentDecisionEligibilityService` | Reads `student_decisions`; determines pass/fail eligibility |
| `ReturningEnrollmentStatusService` | Checks parent returning enrollment completion |
| `NewStudentAdmissionStatusService` | Checks admin-approved new students |
| `PlacementPoolBuilder` | Builds final placement pool |
| `PromotionSectionCapacityService` | Reads and validates `promotion.section_capacity` |
| `ScoreBandClassifier` | Assigns A/B/C/D bands |
| `BalancedSectionPlacementService` | Generates balanced section assignments |
| `SectionPlacementPreviewService` | Creates draft placement preview |
| `SectionPlacementFinalizationService` | Writes final class assignments |
| `SectionPlacementAuditService` | Stores snapshots and reports |
Controllers should orchestrate requests, not contain placement policy. Controllers are traffic cops, not the Ministry of Education.
---
## 23. Implementation Priority
### Phase 1: Audit and preserve current data
```text
Document current tables and flows.
Add lifecycle/display metadata where needed.
Do not silently migrate old rows.
Keep old records displayable.
```
### Phase 2: Strengthen pass/fail eligibility
```text
Use `student_decisions` as source of truth.
Add reports for missing/pending decisions.
Block automatic placement without passed/promoted decision.
```
### Phase 3: Strengthen returning enrollment
```text
Require parent completed returning enrollment.
Separate returning enrollment from new-student registration.
Admin approval only for new students and exceptions.
```
### Phase 4: Add configurable section capacity
```text
Add/use `promotion.section_capacity` in configuration table.
Validate it.
Default to 20 only if missing.
Store capacity used in placement snapshot.
```
### Phase 5: Build placement pool
```text
Merge eligible returning students and approved new students.
Default new students without assessment to D band.
Exclude ineligible and incomplete records.
Generate exception report.
```
### Phase 6: Generate balanced placement preview
```text
Create sections based on capacity.
Group students by score band.
Balance section totals first, band mix second.
Generate draft preview.
```
### Phase 7: Finalize placement
```text
Validate draft.
Admin finalizes.
Write class section assignments.
Store placement snapshot.
```
### Phase 8: Add audit and reports
```text
Placement batch history.
Distribution reports.
Override logs.
New-student D-band report.
Parent enrollment missing report.
```
---
## 24. Final Policy Summary
The strong system must follow these rules:
```text
1. New students require admin approval.
2. Returning students do not require admin approval unless there is an exception.
3. Returning students must pass according to `student_decisions`.
4. Returning students must have parent enrollment completed.
5. Promotion placement must not recalculate pass/fail.
6. Section capacity comes from the `configuration` table using `promotion.section_capacity`.
7. If total placement pool size is less than or equal to capacity, create one section.
8. If total placement pool size is greater than capacity, create ceil(total / capacity) sections.
9. Returning students are grouped by final promotion score bands.
10. New students without assessment are placed in D band for balancing only.
11. Multiple sections must be balanced by total section size first and score-band mix second.
12. Placement must be previewed before finalization.
13. Final placement must store a snapshot of the algorithm, capacity, distribution, and assigned students.
14. Old data remains displayable and must not be silently changed.
```
---
## 25. Bottom Line
The strong model is:
```text
student_decisions = academic eligibility
parent returning enrollment = family intent to continue
admin approval = new-student admission or exception handling
configuration = section capacity
placement algorithm = balanced section assignment
snapshot = audit trail
```
That separation is the whole point.
Without it, the system will keep confusing pass/fail, enrollment, and placement. Then someone will ask why a failed student is in the next class, why a parent never enrolled but the child appears in attendance, or why one section has all the high scorers. And then everyone will stare at the database like it committed a moral crime on its own.
It did not. The design did.
@@ -0,0 +1,315 @@
# Laravel-Compatible Financial System Plan With Stakeholder Reporting
## 1. Purpose
This document converts the uploaded financial-system remediation plan into a Laravel-compatible implementation plan for this project.
The previous plan was written mostly in CodeIgniter terms: `app/Config/Routes.php`, Spark commands, `app/Libraries`, and CI-style controllers. This project is Laravel, so implementation must use Laravel routes, controllers, services, form requests, resources, middleware, policies, migrations, console commands, storage, and Eloquent/DB transactions.
The goal is not to win a framework cosplay contest. The goal is to fix finance without breaking finance, which is a surprisingly radical position in software.
## 2. Non-Negotiable Compatibility Rule
The existing financial system must keep working while improvements are added.
That means:
```text
Do not delete existing finance routes in the first pass.
Do not remove existing PayPal records or controllers until a separate decommission is approved.
Do not rewrite payment, invoice, refund, discount, expense, or reimbursement flows destructively.
Do not mutate historical invoice math during reporting.
Do not silently reinterpret old statuses.
Add new services and reporting endpoints beside the old flow first.
```
The original plan says to remove PayPal completely. That may still be a valid future phase, but it is not compatible with the current instruction to keep the old financial system working. Therefore, in this Laravel-compatible version, PayPal removal becomes a later controlled decommission phase, not a first-pass code deletion.
## 3. Laravel Mapping
| Old plan concept | Laravel-compatible location |
|---|---|
| `app/Config/Routes.php` | `routes/api.php`, `routes/web.php` |
| `app/Libraries/*` | `app/Services/*` or `app/Support/*` |
| CI controller validation | `app/Http/Requests/*` FormRequest classes |
| CI controllers | `app/Http/Controllers/Api/*` |
| Spark command | Laravel Artisan command in `app/Console/Commands` |
| CI filters | Laravel middleware in `app/Http/Middleware` |
| CI models | Eloquent models in `app/Models` |
| CI views | API resources, frontend pages, or Blade/Inertia only if used |
| `WRITEPATH/uploads` | Laravel `storage/app` or `storage/app/private` |
| Direct route filters | `auth:api`, custom middleware, policies, and gates |
## 4. First-Pass Implementation Strategy
Use an additive approach.
Phase 1 should add:
```text
StakeholderFinancialAnalysisService
StakeholderFinancialAnalysisRequest
GET /api/v1/finance/stakeholder-analysis
GET /api/v1/finance/stakeholder-analysis/csv
administrator aliases for the same stakeholder analysis endpoints
Laravel-compatible documentation
```
Phase 1 must not remove:
```text
existing payment routes
existing invoice routes
existing PayPal routes
existing PayPal transaction exports
existing payment sync code
existing invoice generation code
existing financial summary/report endpoints
```
This lets leadership inspect finance without detonating operations. Boring, safe, good. A rare trilogy.
## 5. Stakeholder Financial Analysis And Reporting
Stakeholders need management-level financial visibility, not raw operational dumps.
The report should be read-only and expose:
```text
gross revenue
net revenue
cash collected
refunds
discounts
expenses
reimbursements
net cash
open receivables
collection rate
expense ratio
operating margin
invoice count
paying family count
payment method mix
monthly charge/collection/expense trend
receivables aging
largest open invoices
risk flags
school-year comparison
CSV export
```
The stakeholder report must not mutate invoices or trigger recalculation. Reports that change accounting are not reports. They are traps with charts.
## 6. Laravel Endpoint Design
Add routes under the existing authenticated finance group:
```php
Route::prefix('finance')->group(function () {
Route::get('stakeholder-analysis', [FinancialController::class, 'stakeholderAnalysis']);
Route::get('stakeholder-analysis/csv', [FinancialController::class, 'stakeholderAnalysisCsv']);
});
```
Also add administrator aliases if the admin dashboard uses `/administrator/...` finance paths:
```php
Route::prefix('reports')->group(function () {
Route::get('stakeholder-analysis', [FinancialController::class, 'stakeholderAnalysis']);
Route::get('stakeholder-analysis/csv', [FinancialController::class, 'stakeholderAnalysisCsv']);
});
```
The routes should remain inside existing `auth:api` and `admin.access` protected groups where applicable.
## 7. Request Validation
Create:
```text
app/Http/Requests/Finance/StakeholderFinancialAnalysisRequest.php
```
Allowed inputs:
```text
date_from: nullable YYYY-MM-DD
date_to: nullable YYYY-MM-DD, after or equal to date_from
school_year: nullable string
compare_school_year: nullable string
include_monthly_trend: nullable boolean
include_parent_risk: nullable boolean
```
The request should extend the existing `FinancialReportRequest` to preserve behavior and avoid yet another tiny validation kingdom.
## 8. Service Design
Create:
```text
app/Services/Finance/StakeholderFinancialAnalysisService.php
```
Responsibilities:
```text
read existing invoices, payments, refunds, discounts, expenses, reimbursements, extra charges, and event charges
exclude voided/failed/canceled records from management totals
calculate management KPIs
build monthly trends
build receivables aging
build payment method mix
build expense category mix
compare selected school year against another school year
produce CSV rows
return JSON-ready arrays
```
The service must use `Schema::hasTable()` and `Schema::hasColumn()` guards where the legacy schema is inconsistent. This project has legacy compatibility layers, so brittle assumptions are how bugs sneak in wearing a school hoodie.
## 9. Keep Old Financial System Working
In the first pass, do not replace the existing `FinancialSummaryService`, `FinancialReportService`, `PaymentController`, `InvoiceController`, `RefundController`, or PayPal controllers.
Instead:
```text
Reuse existing financial tables.
Reuse existing auth groups.
Add read-only reporting beside the current operations.
Leave existing payment/invoice mutation paths untouched.
Do not force status normalization yet.
Do not force invoice recalculation yet.
```
Once stakeholder reporting is stable, a later phase can introduce a centralized Laravel ledger service behind feature flags.
## 10. Later Laravel Ledger Phase
The old CodeIgniter-style `InvoiceLedgerService` recommendation should become:
```text
app/Services/Finance/InvoiceLedgerService.php
```
It should be introduced behind a config flag:
```text
finance.ledger_mode = legacy|centralized
```
When `legacy`, existing controllers continue using current behavior.
When `centralized`, payment/refund/discount/additional-charge writes call the ledger service after database writes.
Every write should eventually use:
```php
DB::transaction(function () use ($invoiceId) {
Invoice::query()->whereKey($invoiceId)->lockForUpdate()->firstOrFail();
// create/update financial record
$this->invoiceLedgerService->recalculate($invoiceId);
});
```
But do not force that transition before tests exist. Accounting rewrites without tests are just gambling with extra syntax.
## 11. Tuition Forecast Compatibility
The tuition forecast idea from the uploaded plan is valid, but it should be Laravelized:
```text
app/Services/Finance/Tuition/OldTuitionCalculatorService.php
app/Services/Finance/Tuition/NewTuitionCalculatorService.php
app/Services/Finance/Tuition/TuitionForecastService.php
app/Http/Requests/Finance/TuitionForecastRequest.php
app/Http/Controllers/Api/Finance/TuitionForecastController.php
```
The old calculator must remain the default until leadership approves switching invoice generation.
Use:
```text
tuition_calculator_version = old|new
```
stored in the existing `configuration` table through `Configuration::getConfigValueByKey()` / `setConfigValueByKey()`.
## 12. PayPal Compatibility Position
Because the current instruction is to keep the old financial system working, PayPal is not removed in this pass.
Instead:
```text
Keep existing PayPal routes/controllers/models active.
Keep historical PayPal reports available.
Do not drop or rename PayPal tables.
Document PayPal as legacy.
Plan separate decommission only after staff confirms no operational dependency remains.
```
Future PayPal decommission should be its own release with:
```text
route inventory
frontend dependency scan
data archive migration
rollback plan
parent communication
post-release monitoring
```
## 13. Reporting Security
Stakeholder financial reporting should be restricted to authenticated admin/finance users.
Minimum:
```text
Route inside auth:api group.
Route inside administrator/admin finance area when exposed to admin UI.
Add permission middleware later if the permission catalog has a finance-report permission.
```
Preferred future middleware:
```php
->middleware(['auth:api', 'permission:view_financial_reports'])
```
Do not expose stakeholder reports to ordinary parent users. This should not need saying, but software history suggests otherwise.
## 14. Acceptance Criteria
This Laravel-compatible phase is complete when:
```text
Existing finance endpoints still work.
Existing PayPal endpoints still exist.
New stakeholder analysis JSON endpoint works.
New stakeholder analysis CSV endpoint works.
No financial write flow is changed.
No invoice is recalculated by the stakeholder report.
The report handles missing optional legacy columns safely.
The code passes PHP lint.
The plan is stored in docs/ for future implementation work.
```
## 15. Next Hardening Steps
After the additive reporting phase:
```text
Add tests for stakeholder report calculations.
Add permission middleware for finance reports.
Add centralized InvoiceLedgerService behind a feature flag.
Add a dry-run invoice recalculation Artisan command.
Add tuition forecast services.
Normalize financial statuses through migrations only after database backup.
Design a separate PayPal decommission release.
```
The brutal truth: the uploaded plan had the right instincts, but it assumed the wrong framework and moved too aggressively on removal. In this project, compatibility wins first. Then hardening. Then cleanup. In that order, unless the goal is accounting chaos with a nicer namespace.
File diff suppressed because it is too large Load Diff
+764
View File
@@ -0,0 +1,764 @@
# Strong Grading System Plan
## Purpose
This document defines a stronger grading-system plan for the Alrahma Sunday School API. It is intentionally a planning document, not an implementation patch.
The goal is to move the grading system from a functional calculator into a fair, explainable, auditable, backward-compatible, and policy-driven gradebook.
The current system already has a useful foundation: a centralized semester score service, separated score-entry flows, grading locks, and missing-score override tracking. However, the current scoring model still allows too much ambiguity around blank scores, missing work, validation, and finalization.
The strongest first move is not to redesign every formula. The strongest first move is to kill ambiguity while keeping old data readable and old calculations explainable.
---
## Core Principles
A strong grading system must be:
1. **Fair**
Missing work must not accidentally improve a student's grade.
2. **Explainable**
Every final score must be traceable to category scores, weights, attendance, exam score, and policy rules.
3. **Auditable**
Locked or finalized scores must preserve the exact inputs and formula used at the time.
4. **Policy-driven**
Grading policy should live in services, configuration, and grading profiles, not scattered controller logic.
5. **Safe**
Invalid scores must be rejected before they reach averages or semester calculations.
6. **Backward-compatible**
Old data must remain readable, displayable, and explainable under the legacy policy that created it.
7. **Additive, not destructive**
The new grading system must be introduced beside the old one first. Existing historical scores must not be silently recalculated, reclassified, hidden, or overwritten.
---
## Non-Negotiable Backward Compatibility Requirement
The system must preserve old data display.
Historical grades must remain visible exactly as they were calculated under the legacy system. The new strong grading system must not reinterpret old blanks, old averages, or old semester scores as if they were created under the new policy.
This is the hard rule:
```text
Old data must remain readable, displayable, and explainable exactly as it was calculated under the legacy system.
```
The new system must support two modes:
| Mode | Purpose | Formula Source | Data Behavior |
|---|---|---|---|
| Legacy display mode | Show old/historical grades | Existing stored semester values and legacy formula | Do not reinterpret blanks or statuses |
| Strong scoring mode | Calculate future grades under stronger rules | New scoring service/profile | Uses statuses, max points, snapshots, and finalization validation |
Legacy records must show stored values from the existing `semester_scores` table, such as:
```text
homework_avg
quiz_avg
project_avg
participation_score
attendance_score
ptap_score
midterm_exam_score
final_exam_score
semester_score
```
Legacy displays should include a visible label:
```text
Legacy Calculation
```
And a short explanation:
```text
This score was calculated under the legacy grading policy. Blank scores were ignored in category averages, PTAP used legacy dynamic weighting, attendance included one-absence grace, and the semester exam used the legacy 60% weighting.
```
This is not optional. Without this, the system risks showing old data through new rules and creating disputes that nobody can cleanly answer. Software should not gaslight the gradebook. Humans already do enough of that.
---
## Historical Data Protection Rules
Historical grades must not be automatically recalculated under the new scoring policy.
Rules:
1. Existing `semester_scores` rows stay in place.
2. Existing historical rows are marked as legacy.
3. Legacy rows are displayed from stored aggregate values.
4. Legacy rows are not forced into the new status model.
5. Legacy blanks are not automatically converted to `missing`.
6. Legacy scores are not recalculated unless an authorized admin explicitly starts a recalculation job.
7. If recalculation is ever allowed, the original legacy score must be preserved beside the new-policy result.
Recommended display if a historical record is later recalculated for comparison:
```text
legacy_semester_score: 91.4
new_policy_semester_score: 87.2
displayed_score: 91.4 unless admin explicitly chooses otherwise
```
The default behavior must protect the original displayed score.
---
## Recommended Compatibility Metadata
Do not replace the existing `semester_scores` table. Keep it.
Add nullable metadata columns:
```text
calculation_mode
calculation_policy_version
snapshot_id nullable
```
Suggested values:
```text
calculation_mode = legacy | strong
calculation_policy_version = legacy_v1 | strong_v1
```
Backfill existing rows as:
```text
calculation_mode = legacy
calculation_policy_version = legacy_v1
snapshot_id = null
```
New strong-system finalized rows should be saved with:
```text
calculation_mode = strong
calculation_policy_version = strong_v1
snapshot_id = related semester_score_snapshots row
```
This keeps old grade display stable while allowing new scores to become audit-proof.
---
## Display Resolver Requirement
Create a display resolver instead of forcing every grade display through the new scoring engine.
Recommended service:
```php
App\Services\Grading\GradeCalculationDisplayResolver
```
Responsibilities:
```text
If calculation_mode is legacy:
Load and display existing semester_scores fields.
Show Legacy Calculation badge.
Do not apply new status or profile rules.
If calculation_mode is strong:
Load snapshot and/or strong calculation breakdown.
Show Strong Calculation badge.
Display status-aware category breakdown.
```
The display layer must not recalculate legacy scores just to show them. It should show the stored values. Recalculating old scores on display is a terrible idea, and yet somehow tempting to developers who enjoy turning page loads into historical revisionism.
---
## Important Policy Clarification: Attendance Grace
The attendance grace rule is school policy.
The current formula effectively gives students one absence before attendance points are reduced:
```text
attendance_score = ((total_days + 1 - absences) / total_days) * 100
```
This is policy-compliant, but the formula hides the intent. It should be rewritten more explicitly:
```text
grace_absences = 1
effective_absences = max(0, absences - grace_absences)
attendance_score = ((total_days - effective_absences) / total_days) * 100
```
This keeps the same policy while making the rule clear. Future developers should not have to decode a `+1` and wonder whether it is a bug wearing a tiny policy hat.
---
## Phase 0: Lock the Grading Policy Before Writing Code
Before implementation, these policy decisions must be explicit.
| Question | Recommended Decision |
|---|---|
| Are old/historical grades still displayed? | Yes. Always. |
| Are old/historical grades recalculated automatically? | No. Never silently. |
| Are old blanks converted to missing? | No. Old blanks remain legacy behavior unless explicitly migrated by admin workflow. |
| Are scores always out of 100? | No. Add `max_points`, then normalize to percentages for strong scoring. |
| What does a blank score mean in the new system? | Nothing final. It means pending or unresolved. |
| Does missing work count as zero in the new system? | Yes, unless explicitly excused. |
| Does excused work affect the average? | No. Excused work is excluded from the denominator. |
| Can a class be locked with pending scores in the new system? | No. |
| Can a class be locked with missing work in the new system? | Yes, but missing work counts as zero unless excused. |
| Does attendance have one absence grace? | Yes. This is school policy. |
| Should final scores keep an audit trail? | Yes. Store a calculation snapshot when locking/finalizing new strong scores. |
These decisions are non-negotiable if the system needs to be defensible.
---
## Phase 1: Stabilize the Current Legacy System
Do not start with grading profiles. That is architecture candy. It looks mature, but it does not fix the most dangerous problem first.
The first goal is to stop bad data, protect current behavior with tests, and keep legacy grade display working.
### 1. Add Tests for Existing Legacy Formulas
Before changing calculation behavior, freeze the current formulas in tests.
Test coverage should include:
```text
homework average ignores blanks
quiz average ignores blanks
project average ignores blanks
participation missing returns null
PTAP with no quiz/project
PTAP with quiz only
PTAP with project only
PTAP with all categories
Fall semester formula
Spring semester formula
attendance with 0 absences = 100
attendance with 1 absence = 100
attendance with 2 absences = reduced score
legacy semester_scores display from stored values
legacy rows are not recalculated on display
```
This creates a safety net. Without it, every refactor becomes a guessing game with grades, which is exactly as bad as it sounds.
### 2. Add Central Score Validation
Create one central validator used by every score write path.
Validation rules:
```text
score may be null
score must be numeric when present
score must be >= 0
score must be <= max_points
max_points must be > 0
```
For the first pass, use `max_points = 100` by default.
Example service shape:
```php
final class ScoreValueValidator
{
public function normalizeOrFail(mixed $value, float $maxPoints = 100.0): ?float
{
if ($value === null || (is_string($value) && trim($value) === '')) {
return null;
}
if (!is_numeric($value)) {
throw new InvalidArgumentException('Score must be numeric.');
}
$score = (float) $value;
if ($score < 0 || $score > $maxPoints) {
throw new InvalidArgumentException("Score must be between 0 and {$maxPoints}.");
}
return $score;
}
}
```
Use this validator in all score-entry paths:
```text
HomeworkScoreService
QuizScoreService
ProjectScoreService
ParticipationScoreService
ExamScoreService
GradingScoreService
```
This should reject impossible new values without changing how existing historical rows are displayed.
### 3. Make Attendance Grace Explicit
Refactor the attendance calculation without changing its result.
Replace hidden arithmetic:
```text
(total_days + 1 - absences) / total_days
```
With named policy logic:
```text
grace_absences = 1
effective_absences = max(0, absences - grace_absences)
attendance_score = ((total_days - effective_absences) / total_days) * 100
```
Add tests:
| Total Days | Absences | Expected Attendance Score |
|---:|---:|---:|
| 10 | 0 | 100 |
| 10 | 1 | 100 |
| 10 | 2 | 90 |
| 10 | 10 | 10 |
| 10 | 11+ | 0 |
### 4. Add Legacy Display Badge
Before introducing strong scoring, make the UI able to label old records.
For legacy rows, show:
```text
Policy: Legacy Calculation
```
Example display:
```text
Semester Score: 91.4
Policy: Legacy Calculation
Attendance: 100.0
PTAP: 88.5
Midterm: 92.0
Note: This score was calculated under the legacy policy. Blank scores were ignored in averages.
```
This prevents users from assuming legacy and strong scores use the same rules.
---
## Phase 2: Make Missing Scores Explicit for New/Strong Scoring
This phase applies to the new strong scoring path. It must not automatically rewrite old historical records.
Right now, a blank score can mean too many things:
```text
not graded yet
student missing work
student excused
teacher forgot
assignment not applicable
```
A grading system cannot be fair if the data refuses to say what it means.
### 1. Add Score Statuses
Every new strong score-bearing record should have both a numeric score and a status.
Minimum statuses:
| Status | Meaning | Counts in Average? | Blocks Finalization? |
|---|---|---:|---:|
| `pending` | Teacher has not resolved the item | No | Yes |
| `scored` | Valid numeric score exists | Yes | No |
| `missing` | Student did not submit | Yes, as 0 | No |
| `excused` | Approved exclusion | No | No |
| `not_assigned` | Does not apply | No | No |
Recommended columns for homework, quiz, project, participation, midterm, and final tables:
```text
status
max_points
excused_reason
locked_at
locked_by
```
### 2. Migrate Existing Data Carefully
Do not blindly turn old blanks into missing work. That would punish students for old system ambiguity.
Initial migration mapping for active, not-yet-finalized records:
| Existing Data | Initial Status |
|---|---|
| Numeric score exists | `scored` |
| Blank/null score | `pending` |
| Missing override exists and is allowed | `excused` or `not_assigned`, depending policy meaning |
For historical finalized legacy records, do not require full status migration just to display them. Keep them as legacy display records.
After migration, teachers/admins must resolve pending items before final lock for strong-scoring sections.
---
## Phase 3: Change Calculation Semantics for Strong Scoring
Once statuses exist, strong category calculations should stop relying on blank/null behavior.
New score handling:
| Status | Calculation Behavior |
|---|---|
| `scored` | Include normalized score |
| `missing` | Include 0 |
| `excused` | Exclude from denominator |
| `not_assigned` | Exclude from denominator |
| `pending` | Block finalization |
Normalize every scored item:
```text
normalized_score = (raw_score / max_points) * 100
```
Then calculate the category average:
```text
category_average = average(all included normalized scores)
```
This prevents missing work from silently improving a student's grade.
Legacy records must still display legacy stored values. Do not apply this status-aware calculation to historical rows unless an admin explicitly opts into a recalculation workflow.
---
## Phase 4: Add Finalization Validation
Locking a strong-scoring class should not simply flip a flag. It should prove the gradebook is complete enough to finalize.
Create a validator service:
```php
App\Services\Grading\GradebookFinalizationValidator
```
It should validate:
```text
No pending required scores
All numeric scores are within range
Required categories are present
Required exam score exists
Required participation score exists
Attendance can be calculated
Missing items are either counted as zero or excused
Every student has a calculable final score
```
If validation fails, return a structured report:
```text
student
category
item
problem
required_action
```
Example:
```text
Student: Ahmed Ali
Category: Homework
Item: Homework #4
Problem: Pending score
Required action: Mark as scored, missing, excused, or not assigned before locking.
```
The lock button should become a gate, not a decorative suggestion.
Legacy historical records do not need to pass the new finalization validator just to be displayed.
---
## Phase 5: Add Calculation Snapshots for Strong Scores
When strong grades are finalized or locked, store exactly how the final score was produced.
Create table:
```text
semester_score_snapshots
```
Recommended fields:
```text
id
student_id
class_section_id
semester
school_year
grading_policy_version
input_json
calculation_json
semester_score
calculated_by
calculated_at
```
The snapshot should include:
```text
raw scores used
score statuses used
max points used
normalized scores
category averages
category weights
attendance grace policy
exam score
final formula
final score
```
This protects the school when someone disputes a grade later.
The current `semester_scores` table can show the latest aggregate. The snapshot proves what was true at finalization.
Legacy historical grades may have no snapshot. That is acceptable as long as they are clearly labeled `legacy_v1` and displayed from stored values.
---
## Phase 6: Replace Hardcoded PTAP With Grading Profiles
Only after validation, statuses, legacy display protection, and snapshots are in place should hardcoded weighting be replaced.
Current PTAP behavior changes based on data availability:
```text
No tests and no projects
Tests but no projects
Projects but no tests
All categories exist
```
That is flexible, but dangerous. It lets missing or unentered data affect the formula.
### Recommended Tables
```text
grading_profiles
```
Fields:
```text
id
name
school_year
semester
class_section_id nullable
is_default
version
created_by
created_at
updated_at
```
```text
grading_profile_categories
```
Fields:
```text
id
grading_profile_id
category_key
weight
required
redistribute_if_missing
```
### Example Profile
| Category | Weight | Required | Redistribute If Unused |
|---|---:|---:|---:|
| homework | 15 | Yes | No |
| quiz | 15 | Configurable | Yes |
| project | 15 | Configurable | Yes |
| participation | 15 | Yes | No |
| attendance | 10 | Yes | No |
| exam | 30 | Yes | No |
Important distinction:
```text
Category not used by profile -> redistribute its weight
Category assigned but scores pending -> block lock
Category assigned but student missing -> count as zero
Category excused -> exclude according to policy
```
Do not redistribute weight just because a teacher forgot to enter grades. That would be grading by accident, which is somehow worse than grading by spreadsheet.
Legacy rows continue using legacy PTAP display and must not be forced into grading profiles.
---
## Phase 7: Opt-In Activation Strategy
Strong scoring should be activated by class section, semester, and school year. It should not globally replace legacy behavior in one switch.
Recommended activation metadata:
```text
class_section_id
school_year
semester
calculation_mode = legacy | strong
grading_profile_id nullable
activated_by
activated_at
```
Activation rules:
1. Existing historical records remain `legacy`.
2. Current active sections may remain `legacy` until admins choose migration.
3. New future sections can default to `strong` once the system is ready.
4. A class section using `legacy` must continue calculating and displaying using the legacy service.
5. A class section using `strong` must use statuses, max points, finalization validation, and snapshots.
6. Switching a section from legacy to strong should require an admin confirmation and a preflight report.
Preflight report should show:
```text
number of numeric scores that will become scored
number of blanks that will become pending
number of overrides requiring review
students blocked from finalization until pending items are resolved
```
No quiet migrations. Quiet migrations are where data integrity goes to disappear.
---
## Phase 8: Refactor Controllers Last
Controllers should not contain grading policy.
Final desired flow:
```text
Controller receives request
Request validates input shape
Application service saves score/status
Scoring mode resolver selects legacy or strong path
Scoring service recalculates draft score
Finalization validator blocks or allows lock for strong mode
Snapshot service records locked strong calculation
Display resolver shows legacy or strong breakdown
```
Controllers should not know formulas. They should not decide whether missing counts. They should not know category redistribution rules.
Controllers are traffic cops, not education philosophers.
---
## Recommended Implementation Order
| Step | Work | Risk Reduced |
|---:|---|---|
| 1 | Add tests for current legacy formulas and legacy display | Prevent accidental formula/display drift |
| 2 | Add compatibility metadata to `semester_scores` | Preserve old records clearly |
| 3 | Add legacy/strong display resolver | Keep old data visible |
| 4 | Add central score validation | Stop impossible new scores |
| 5 | Make attendance grace explicit | Preserve policy and improve clarity |
| 6 | Add `max_points` columns | Enable normalization |
| 7 | Add `status` columns | Remove blank-score ambiguity for strong scoring |
| 8 | Build category calculator using statuses | Fix missing-score fairness |
| 9 | Add finalization validator | Stop invalid strong locks |
| 10 | Add calculation snapshots | Audit-proof locked strong grades |
| 11 | Add grading profiles | Remove hardcoded weights for strong mode |
| 12 | Add opt-in activation by class/semester | Prevent disruptive rollout |
| 13 | Refactor controllers | Clean architecture after behavior is safe |
---
## MVP Strong Upgrade
The smallest serious upgrade is:
```text
1. Keep current legacy formulas.
2. Keep old data display from stored semester_scores values.
3. Add calculation_mode and calculation_policy_version metadata.
4. Add legacy display badge.
5. Add central score validation for new writes.
6. Make attendance grace explicit without changing results.
7. Add score statuses for active/future strong-scoring sections.
8. Treat missing as zero, excused as excluded, and pending as blocking lock in strong mode only.
9. Add a lock validation report for strong mode.
10. Add calculation snapshots on strong-mode lock.
```
This improves fairness and auditability without breaking old records or immediately changing all weights.
After that, replace PTAP branching with grading profiles.
---
## Hard Safety Rules
1. Do not change formulas and database semantics in the same step without tests.
2. Do not silently recalculate historical scores.
3. Do not convert old blanks to missing for historical records.
4. Do not hide legacy data because it lacks new statuses.
5. Do not activate strong scoring globally without class/semester-level opt-in.
6. Do not display a strong-policy explanation for a legacy-policy score.
7. Do not let a lock action become the first time the system discovers incomplete data.
That is how a school ends up unable to explain whether grades changed because of policy, migration, code, or a null-handling goblin hiding in the service layer.
---
## Final Recommendation
Do not begin by building grading profiles.
Begin by making legacy display safe and every new score state explicit.
The strongest first milestone is:
```text
Old grades remain visible under Legacy Calculation, and future strong-mode grades cannot be locked until every score is classified as scored, missing, excused, pending, or not assigned.
```
Once that is true, the grading system becomes defensible. Until that is true, every formula is built on fog.