Files
alrahma_sunday_school/docs/enrollment_eligibility_plan.md
root dbfd72c2f9
Deploy to Shared Hosting / Shared hosting deploy (push) Failing after 48s
Tests / PHPUnit (push) Successful in 1m29s
add open position system
2026-09-02 00:53:55 -04:00

388 lines
21 KiB
Markdown

# Enrollment Eligibility and Admin Exception Plan
## 1. Goal
Tighten re-enrollment eligibility for existing Al Rahma Sunday School students without changing the separate new-student registration flow. Parents should only be able to submit re-enrollment for students already linked to their account and already transitioned or eligible for transition into the selected school year.
This plan matches the current project enrollment architecture:
- CodeIgniter 4 controllers and views under `app/Controllers/View` and `app/Views`.
- School years are referenced by `school_years.name` strings such as `2026-2027`, not by target-year IDs.
- Parent re-enrollment currently uses `/parent/enroll_classes_handler` and `app/Views/parent/enroll_classes.php`.
- New-student registration remains `/parent/register_student` and is out of scope except that re-enrollment must not create new students.
- Eligibility and placement already live mainly in `App\Services\EnrollmentTransitionService`, `App\Support\Enrollment\DeliberationDecision`, and `App\Support\Enrollment\EnrollmentEligibility`.
- Existing operational tables include `enrollments`, `student_class`, `student_decisions`, `school_years`, `enrollment_flags`, `enrollment_age_rules`, `enrollment_transition_audits`, and `invoices`.
## 2. Existing Rules To Preserve
Do not replace the current transition model with a parallel eligibility service. Extend the existing service and return shape.
Current enrollment decisions are based on `student_decisions.decision` or `student_decisions.deliberation_decision_standard`, normalized by `DeliberationDecision`:
| Decision | Current behavior |
|---|---|
| `PASSED` | Parent re-enrollment is allowed when the student can be placed in the next grade. If no next grade exists, the student is blocked with an exit/completion flag. |
| `REPEAT_CLASS` | Parent re-enrollment is allowed into the same class when available; otherwise administration must assign the class. |
| `MAKE_UP_EXAM` | Parent re-enrollment is allowed, but placement is temporary in the same grade until the make-up exam result is resolved. |
| `EXPELLED` | Parent re-enrollment is blocked and administrative review is flagged. |
| `WITHDRAWN` | Parent re-enrollment is blocked and withdrawal review is flagged. Decision values containing the correct spelling `withdraw` are normalized to `WITHDRAWN`. |
| `DEFERRED_DECISION` or pending/missing decision | Parent re-enrollment is blocked until administration records or reviews the decision. |
Also preserve these existing controls:
- Registration window checks from `school_years.registration_opens_at` / `registration_starts_on` and `registration_deadline_at` / `registration_ends_on`.
- Late-registration behavior from `school_years.late_registration_blocked`.
- Adult-student blocking for parent-submitted registration when the student is 18 or older on September 1 of the target school year.
- Age-rule checks from `enrollment_age_rules`.
- Placement statuses such as `automatic_distribution_pending`, `same_class_assigned`, `temporary_same_grade`, `manual_class_required`, `temporary_manual_class_required`, and `exit_required`.
- Follow-up flags in `enrollment_flags`.
- Audit history in `enrollment_transition_audits`.
## 3. New Eligibility Requirements
Add these policy checks to the existing transition evaluation and parent submission path.
### 3.1 Non-overridable checks
These must pass for every parent-submitted enrollment, including an administrative exception:
1. Parent session is authenticated and belongs to an active primary parent account.
2. Student row exists in `students`.
3. Student is linked to the parent through the current project link model. Today the direct link is `students.parent_id`; if `family_students` / `family_guardians` are adopted for this flow, centralize that lookup and use it everywhere.
4. Target school year name exists in `school_years.name`.
5. Source school year can be resolved by the existing previous-year helpers.
6. Student belongs to the source year through `student_class`, `enrollments`, or `student_decisions`.
7. Student does not already have an active target-year enrollment that should block duplicate submission.
8. Parent request values are reloaded from the database. Browser-provided decision, eligibility, class, or exception values are ignored.
9. Parent re-enrollment never creates, attaches, or relinks a student. Keep that work in the existing parent/admin student-management flow.
### 3.2 Standard eligibility checks
After the non-overridable checks pass, standard parent eligibility is:
```text
academic_ok =
deliberation_decision in (PASSED, REPEAT_CLASS, MAKE_UP_EXAM)
AND placement_status is not exit_required
status_ok =
deliberation_decision not in (EXPELLED, WITHDRAWN, DEFERRED_DECISION)
AND latest relevant enrollment status is not denied or withdrawn
window_ok =
registration window is open for parent submissions
age_ok =
student is not an adult student for parent-submitted enrollment
AND configured blocking age rules pass
family_name_ok =
all active linked siblings for the parent have the same normalized last name
financial_ok =
previous-year balance policy does not block submission
standard_parent_enrollment_allowed =
academic_ok AND status_ok AND window_ok AND age_ok AND family_name_ok AND financial_ok
```
Missing or unrecognized academic data should remain blocking/review-required as the current service does today. Make-up exam is not a blocker; it is eligible with a warning and a `PENDING_MAKE_UP_EXAM_PROMOTION` follow-up.
## 4. Sibling Last-Name Rule
Implement sibling last-name matching as a family-level rule in the existing enrollment evaluation.
Required behavior:
- Evaluate all active students linked to the parent for the target re-enrollment flow.
- A parent with one linked student passes this rule.
- If every linked student has the same normalized last name, set the rule result to pass for every student.
- If any linked student has a different, missing, or unusable last name, block every linked student for that parent with the same machine-readable reason.
- Normalize by trimming, collapsing spaces, comparing case-insensitively, and ignoring common punctuation variants. Preserve original names for display and audit.
- Do not hide this rule in SQL-only filtering. Return it as a visible reason code such as `SIBLING_LAST_NAME_MISMATCH`.
This rule should be overrideable by administration, but only after the linked-student relationship is already valid.
## 5. Financial Rule
Use the existing invoice source, not a new finance ledger abstraction unless finance is refactored separately.
Current project behavior:
- `ParentController::familyFinancialSummary()` computes carry-over balance with `invoices.balance` for the previous school year.
- Blocking behavior is configured by `school_years.carry_over_balance_behavior`.
- Existing supported policy values include `information_only`, `payment_plan_required`, `submission_allowed_confirmation_blocked`, `submission_blocked_until_payment`, and `admin_approval_required`.
Required behavior:
- Recalculate the parent balance from `invoices` when the enrollment page is shown and again inside the submit handler.
- Treat a previous-year balance as blocking only when `carry_over_balance_behavior` is `submission_blocked_until_payment` or `admin_approval_required`.
- Keep `payment_plan_required` and `submission_allowed_confirmation_blocked` non-blocking for submission unless school policy changes those meanings.
- Record the balance, source school year, target school year, and policy behavior in the eligibility/audit snapshot.
## 6. Decision Output
Extend the current `EnrollmentTransitionService::evaluate()` response instead of introducing a separate response contract.
Add stable fields like:
```php
[
'decision' => 'ELIGIBLE',
'parent_enrollment_allowed' => true,
'rule_codes' => [],
'blocking_rule_codes' => [],
'warning_rule_codes' => [],
'review_rule_codes' => [],
'admin_exception' => null,
'financial_summary' => [],
]
```
Recommended decision values:
| Decision | Meaning |
|---|---|
| `ELIGIBLE` | Parent may submit re-enrollment. |
| `ELIGIBLE_WITH_WARNING` | Parent may submit, but the student needs a follow-up such as make-up exam promotion or manual placement. |
| `INELIGIBLE` | Confirmed blocker exists. |
| `REVIEW_REQUIRED` | Staff must correct or verify data. |
| `EXCEPTION_ELIGIBLE` | Standard rules fail, but a valid admin exception permits submission. |
| `ALREADY_ENROLLED` | Existing target-year enrollment blocks duplicate submission. |
Suggested rule codes:
- `PASSED`, `REPEAT_CLASS`, `MAKE_UP_EXAM`
- `NO_FINAL_DECISION`, `UNRECOGNIZED_DECISION`
- `EXPELLED`, `WITHDRAWN`, `DEFERRED_DECISION`
- `EXIT_REQUIRED`
- `REGISTRATION_NOT_OPEN`, `REGISTRATION_CLOSED`
- `ADULT_STUDENT_PARENT_BLOCKED`
- `AGE_RULE_BLOCKED`
- `SIBLING_LAST_NAME_MISMATCH`
- `OUTSTANDING_BALANCE_BLOCKED`, `FINANCE_APPROVAL_REQUIRED`
- `STUDENT_NOT_LINKED`, `SOURCE_YEAR_NOT_FOUND`, `TARGET_YEAR_NOT_FOUND`
- `ALREADY_ENROLLED`
Keep human-readable parent messages in `EnrollmentEligibility` or a nearby presenter helper so controllers and views do not hard-code policy text.
## 7. Admin Exception Design
Use the project's existing terminology: an exception is an administrative allowance for a specific enrollment case. Avoid a broad `parent.can_enroll = true` flag.
### 7.1 Table
Add an `enrollment_exceptions` table or explicitly rename the concept if the team prefers `enrollment_overrides`. The table should fit current school-year string usage:
| Field | Purpose |
|---|---|
| `id` | Primary key |
| `parent_id` | Parent receiving the exception |
| `student_id` | Existing linked student covered by the exception |
| `school_year` | Target year name |
| `source_school_year` | Source year name |
| `status` | `active`, `used`, `revoked`, or `expired` |
| `reason_code` | Structured exception category |
| `reason_note` | Required admin explanation |
| `bypassed_rule_codes_json` | Snapshot of standard rules bypassed |
| `created_by` | Admin user ID |
| `approved_by` | Optional second approver |
| `starts_at` / `expires_at` | Bounded validity period |
| `used_at` / `enrollment_id` | Consumption tracking |
| `revoked_at` / `revoked_by` / `revocation_reason` | Revocation tracking |
| `created_at` / `updated_at` | Record history |
Add an index or unique constraint that prevents more than one active exception for the same `parent_id`, `student_id`, and `school_year`.
### 7.2 What an exception may bypass
An exception may bypass only standard eligibility rules:
- `NO_FINAL_DECISION` or `UNRECOGNIZED_DECISION`
- `EXPELLED`, `WITHDRAWN`, or `DEFERRED_DECISION`, if school policy permits the selected reason code
- `AGE_RULE_BLOCKED`, when the configured age rule permits exceptions
- `SIBLING_LAST_NAME_MISMATCH`
- `OUTSTANDING_BALANCE_BLOCKED` or `FINANCE_APPROVAL_REQUIRED`
- Late registration closure, only if `school_years.administrative_exceptions_permitted` allows it
- Manual placement or capacity exception follow-ups
An exception must not bypass identity, parent-student linkage, target-year existence, source-year existence, duplicate enrollment protection, CSRF/server validation, or the rule that parent re-enrollment cannot create new students.
### 7.3 Permissions and UI
Use a narrow permission, for example `enrollment.exception.manage`, and add it through the existing roles/permissions system. Do not give it to every admin role by default.
Admin UI should be added to the existing Enrollment Administration dashboard or parent/family card flow:
1. Choose school year.
2. Choose parent.
3. View linked students and live evaluation results.
4. Select one or more students.
5. Review failed rule codes and messages.
6. Enter reason code, note, and expiry.
7. Require second approval for high-risk cases if policy requires it.
8. Save, revoke, and view history.
Do not add exception controls to the parent UI. Parents should only see that enrollment is allowed, blocked, under review, or requires contacting administration.
## 8. Backend Integration Points
Implement later work in these places:
- `App\Services\EnrollmentTransitionService`
- Add family-level linked-student evaluation.
- Add sibling last-name rule.
- Add financial blocking rule or accept a finance collaborator if the team wants to keep the service narrower.
- Add exception lookup/application.
- Return stable rule codes.
- `App\Support\Enrollment\EnrollmentEligibility`
- Keep parent-safe message generation here or split into a small presenter class.
- `App\Controllers\View\ParentController`
- Keep `/parent/register_student` as new-student registration.
- In the re-enrollment page and `/parent/enroll_classes_handler`, re-evaluate from database state before accepting submitted student IDs.
- Ignore disabled checkbox assumptions from the browser.
- `App\Controllers\View\EnrollmentAdminController`
- Add exception preview/create/revoke/history beside existing flags, follow-ups, audits, launch approval, and email preview.
- `App\Controllers\View\AdministratorController`
- Keep Enrollment/Withdrawal status management, but do not let status changes serve as an unaudited substitute for exceptions.
- Migrations/models
- Add an exception table/model.
- Add any missing unique/index protection for target-year duplicate enrollments where feasible with the current schema.
- Keep audit writes compatible with `enrollment_transition_audits`.
## 9. Submission Safety
Inside the parent re-enrollment submit handler:
1. Start a database transaction.
2. Resolve parent ID from session.
3. Resolve selected target school year and source school year.
4. Load submitted student IDs, then reload each student from the database.
5. Verify parent-student linkage.
6. Re-run `EnrollmentTransitionService::evaluate()` for each submitted student.
7. Recalculate financial blockers for the parent.
8. Validate any active exception by `parent_id`, `student_id`, and `school_year`.
9. Reject students whose final decision is not eligible or exception-eligible.
10. Create or update the target-year `enrollments` row with the current eligibility snapshot.
11. Mark one-use exceptions as used.
12. Write `enrollment_transition_audits` entries for accepted, rejected, and exception-applied outcomes.
13. Commit.
Add database protection against duplicate target-year enrollments. If historical duplicates prevent a strict unique index immediately, first add a cleanup/reporting step and then add the constraint.
## 10. Parent Experience
In `app/Views/parent/enroll_classes.php`, show existing linked students only.
Recommended states:
| State | Meaning |
|---|---|
| Enroll | Eligible or exception-eligible. |
| Eligible with follow-up | Eligible, but requires make-up exam, manual placement, capacity review, or similar flag. |
| Already submitted | Existing target-year enrollment is in review, payment pending, enrolled, waitlist, or another non-duplicate state. |
| Action needed | Financial or data correction is required. |
| Under review | Staff must resolve academic/status/placement data. |
| Contact administration | Parent cannot resolve the blocker in self-service. |
Do not expose internal exception notes or sensitive finance details. Do not show internal labels such as expelled unless school policy explicitly approves the wording; use parent-safe messages already present in `EnrollmentEligibility`.
## 11. Audit and Reporting
Continue using `enrollment_transition_audits` for enrollment decisions. Extend the payloads rather than creating a disconnected audit stream.
Audit records should include:
- actor user ID
- parent ID when available
- student ID
- source and target school-year names
- decision and rule-code snapshot
- financial snapshot used for the decision
- exception ID when applied
- original and new enrollment values
- request timestamp
Reports needed:
- active exceptions by school year
- expiring unused exceptions
- enrollments submitted through exception
- blockers by rule code
- repeated parent submission failures
- financial blockers pending finance/admin approval
## 12. Test Matrix
Add focused tests around the current classes and controller submission behavior:
| Case | Expected result |
|---|---|
| `PASSED` with clean status and placement | Eligible |
| `REPEAT_CLASS` with same target section | Eligible, same class assigned |
| `REPEAT_CLASS` without target section | Eligible with manual-class follow-up |
| `MAKE_UP_EXAM` | Eligible with warning and make-up follow-up |
| `EXPELLED` | Blocked/review-required |
| `WITHDRAWN`, including historical misspellings | Blocked/review-required |
| `DEFERRED_DECISION` | Blocked/review-required |
| Pending or missing final decision | Blocked/review-required |
| Passed highest grade with no next class | Blocked with exit/completion follow-up |
| Registration not open | Blocked for parent |
| Registration closed and late blocking enabled | Blocked for parent |
| Adult student on September 1 | Parent submission blocked |
| Blocking age rule | Blocked unless valid exception applies |
| One linked sibling last name differs | Every linked sibling blocked |
| Linked sibling has missing last name | Every linked sibling blocked |
| One linked student only | Last-name rule passes |
| Previous-year balance with information-only policy | Not blocked |
| Previous-year balance with submission-blocked policy | Blocked |
| Previous-year balance with admin-approval policy | Blocked unless valid exception applies |
| Submitted student does not belong to parent | Denied even with exception |
| Nonexistent student ID | Denied |
| Expired/revoked/wrong-year/wrong-student exception | Denied |
| Valid scoped exception | Exception eligible and audited |
| Browser tampers with disabled checkbox or decision fields | Server ignores browser state |
| Concurrent duplicate submissions | Only one target-year enrollment is accepted |
Keep existing unit tests for `DeliberationDecision` and `EnrollmentEligibility`, and add service-level tests for `EnrollmentTransitionService` with mocked builders where practical. Add integration tests for the submit path if the test database can reliably seed `students`, `student_class`, `student_decisions`, `school_years`, `enrollments`, `invoices`, and exception rows.
## 13. Rollout Plan
1. Confirm policy decisions: sibling rule, financial behavior, exception authority, parent-facing messages, and high-risk approvals.
2. Inventory data quality: duplicate target-year enrollments, missing decisions, misspelled withdrawal values, missing last names, missing parent links, and open invoice balances.
3. Add rule-code output to `EnrollmentTransitionService` without enforcing new blockers yet.
4. Add read-only reporting for who would be blocked by sibling/finance rules.
5. Add exception schema, model, permissions, audit payloads, and admin UI.
6. Enforce server-side parent submission checks behind a feature flag or school-year setting.
7. Monitor blockers, exceptions, failed submissions, duplicate attempts, and finance approval volume during the first enrollment window.
8. Remove any old duplicated eligibility checks only after the central evaluation is stable.
## 14. Decisions Required Before Coding
These are policy decisions and should not be guessed in code:
1. Does the sibling last-name rule apply to every `students.parent_id` match, or only active/current linked students after `family_students` is fully adopted?
2. What exact statuses count as active linked students for the sibling rule?
3. Which parent-facing message is approved for sibling mismatch?
4. Which financial behavior values should block submission versus only block confirmation?
5. What balance tolerance, if any, is allowed?
6. Can finance/admin approve a balance exception, and does it need second approval above a threshold?
7. Can expulsion, withdrawal, deferred decision, or adult-student rules be overridden, and by whom?
8. How long can an exception remain active, and is it single-use?
9. Should duplicate enrollment protection block every existing target-year row, or only statuses such as `admission under review`, `review & decision`, `payment pending`, `enrolled`, and `waitlist`?
## 15. Definition of Done
- Parent re-enrollment uses one authoritative server-side evaluation path.
- Parent submission can enroll only existing linked students.
- New-student registration remains separate and unaffected.
- Academic, placement, registration-window, adult-student, age-rule, sibling-name, financial, and exception checks are all represented as stable rule codes.
- Make-up exam remains eligible with a temporary placement follow-up.
- Sibling last-name mismatch blocks every linked student unless a valid scoped exception applies.
- Financial blocking follows `school_years.carry_over_balance_behavior` and current invoice balances.
- Administrative exceptions are scoped by parent, student, and school-year name.
- Exceptions cannot bypass identity, linkage, school-year validity, source-year validity, or duplicate protection.
- Every accepted, blocked, and exception-applied submission is auditable.
- Duplicate target-year submissions are prevented.
- The test matrix above passes.