388 lines
21 KiB
Markdown
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.
|