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

21 KiB

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:

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:

[
    '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.