update api and add more features

This commit is contained in:
root
2026-06-04 02:24:41 -04:00
parent fa6c9519a0
commit 4e33882ac7
131 changed files with 34596 additions and 340 deletions
BIN
View File
Binary file not shown.
@@ -0,0 +1,126 @@
# `AdministratorPromotionController` plan
## Scope
- **Controller**: `app/Http/Controllers/Api/Administrator/AdministratorPromotionController.php`
- **Purpose**: admin/registrar API to manage promotion records and parent enrollment lifecycle.
- **Primary dependencies**:
- Query/detail: `PromotionQueryService`
- Eligibility evaluation: `PromotionEligibilityService`
- Status transitions: `PromotionStatusService`
- Parent enrollment workflow: `PromotionEnrollmentService`
- Reminders: `PromotionReminderService`
- Auditing: `PromotionAuditService`
- Level mapping: `LevelProgressionService`
- **Models**:
- `StudentPromotionRecord`
- `PromotionAuditLog`
- `PromotionReminderLog`
## Routes
All routes are under:
- Base prefix: `/api/v1/administrator/promotions`
- Middleware: `auth:api`, `admin.access`
Endpoints:
- Listing & detail
- `GET /api/v1/administrator/promotions/``index`
- `GET /api/v1/administrator/promotions/summary``summary`
- `GET /api/v1/administrator/promotions/{promotionId}``show`
- Eligibility & status
- `POST /api/v1/administrator/promotions/evaluate``evaluate`
- `PATCH /api/v1/administrator/promotions/{promotionId}/status``updateStatus`
- Deadlines
- `POST /api/v1/administrator/promotions/deadlines``setDeadline` (bulk/single via body)
- `POST /api/v1/administrator/promotions/{promotionId}/deadline``setDeadline` (single)
- `POST /api/v1/administrator/promotions/deadlines/expire``expireDeadlines`
- Enrollment steps (admin override)
- `PATCH /api/v1/administrator/promotions/{promotionId}/enrollment-steps``adminCompleteEnrollment`
- Reminders & audit
- `POST /api/v1/administrator/promotions/{promotionId}/reminders``sendReminder`
- `GET /api/v1/administrator/promotions/{promotionId}/reminders``reminders`
- `POST /api/v1/administrator/promotions/reminders/dispatch``dispatchScheduledReminders`
- `GET /api/v1/administrator/promotions/{promotionId}/audit``audit`
- Level progressions (mapping current level → promoted level)
- `GET /api/v1/administrator/promotions/levels/``levelProgressions`
- `POST /api/v1/administrator/promotions/levels/``upsertLevelProgression`
- `POST /api/v1/administrator/promotions/levels/seed``seedLevelProgressions`
## Authentication & authorization
- Admin access enforced by route middleware.
- `getCurrentUserId()` used for attribution in audit logs and updates.
## Requests/validation
Validated via `FormRequest` classes:
- `index` / `summary`: `AdminListPromotionsRequest` (`filters()`)
- `evaluate`: `EvaluateEligibilityRequest` (supports scope: `student`, `class_section`, `school_year`)
- `updateStatus`: `UpdatePromotionStatusRequest` (supports `force` + `notes`)
- `setDeadline`: `SetEnrollmentDeadlineRequest` (supports `apply_to` modes and optional `promotion_ids`)
- `sendReminder`: `SendReminderRequest` (type, subject/message, channels)
- `adminCompleteEnrollment`: `ParentEnrollmentStepRequest` (`steps()`)
- `upsertLevelProgression`: `UpsertLevelProgressionRequest`
## Response shapes
This controller uses legacy `{ ok: ... }` shapes (not Base API envelope).
- `index`:
- `{ ok:true, data:[StudentPromotionResource...], pagination:{page, per_page, total, total_pages}, counts_by_status }`
- `show`:
- `{ ok:true, data: StudentPromotionResource }`
- `summary`:
- `{ ok:true, counts_by_status, total }`
- `evaluate`:
- `{ ok:true, result:{ scope, ... } }`
- `updateStatus`:
- `{ ok:true, data: StudentPromotionResource }`
- Errors:
- 422 for invalid args
- 409 for transition conflicts
- `setDeadline`:
- `{ ok:true, updated, promotion_ids:[...] }`
- `sendReminder`:
- `{ ok:true, reminder: PromotionReminderResource }`
- `dispatchScheduledReminders` / `expireDeadlines`:
- `{ ok:true, result }`
- `reminders`:
- `{ ok:true, reminders:[PromotionReminderResource...] }`
- `audit`:
- `{ ok:true, entries:[PromotionAuditLogResource...] }`
- `adminCompleteEnrollment`:
- `{ ok:true, data: StudentPromotionResource }` (409 on conflict)
- `levelProgressions`:
- `{ ok:true, levels:[LevelProgressionResource...] }`
- `upsertLevelProgression`:
- `{ ok:true, level: LevelProgressionResource }`
- `seedLevelProgressions`:
- `{ ok:true, created }`
## Side effects
- Deadline updates are wrapped in `DB::transaction` and log audit entries via `PromotionAuditService`.
- Reminder sending records `PromotionReminderLog` and may send via multiple channels.
- Status transitions update `StudentPromotionRecord` and write audit entries via services.
## Error handling expectations
- `findOrFail()` returns 404 `{ ok:false, message:"Promotion record not found." }`.
- `evaluate()` catches unexpected errors and returns 500 with generic message.
- `sendReminder()` catches unexpected errors and returns 500 with generic message.
## Test plan (feature)
- AuthZ:
- Non-admin users denied by middleware.
- List & filters:
- Pagination and `counts_by_status` match expected totals.
- Evaluate:
- Each scope returns expected payload keys; unexpected errors return 500.
- Status transitions:
- Valid transitions succeed; invalid args return 422; invalid transitions return 409.
- Deadlines:
- Single record update; bulk update by year; bulk update by list.
- Audit log created for each updated record.
- Reminders:
- Manual reminder creates log and returns resource.
- Dispatch scheduled reminders returns summary result.
- Enrollment steps:
- Admin patch updates steps and returns updated record; conflicts return 409.
- Levels:
- List returns mappings; upsert creates/updates; seed inserts defaults.
+74
View File
@@ -0,0 +1,74 @@
# `AuthController` plan
## Scope
- **Controller**: `app/Http/Controllers/Api/Auth/AuthController.php`
- **Purpose**: JWT login, token refresh, current-user lookup, logout.
- **Primary dependencies**:
- `App\Services\Auth\ApiLoginSecurityService` (rate limiting / IP blocking, attempt tracking, response builder)
- `PHPOpenSourceSaver\JWTAuth\Facades\JWTAuth` (token issuance/refresh/invalidation)
- `Illuminate\Support\Facades\Auth` (current user)
## Routes
- **Public (no `/v1`)**
- `POST /api/login``login`
- **Versioned (`/v1`)**
- `POST /api/v1/login``login`
- **Auth prefix (`/v1/auth`)**
- `POST /api/v1/auth/login``login`
- `POST /api/v1/auth/refresh``refresh` (middleware: `jwt.auth`)
- `POST /api/v1/auth/logout``logout` (middleware: `auth:api`)
- `GET /api/v1/auth/me``me` (middleware: `auth:api`)
## Authentication & authorization
- **`login`**: public; checks credentials, then issues JWT.
- **`refresh`**: requires a valid JWT via `jwt.auth`.
- **`me`/`logout`**: require `auth:api` (JWT/guarded user).
## Request/validation expectations
- **`login`**:
- Accepts JSON or form payload. Extracts `email`, `password`.
- Returns `400` if missing.
- Normalizes email to lowercase; looks up `User` by case-insensitive email.
- **`refresh`**:
- Expects bearer token in request (handled by `JWTAuth::parseToken()`).
- **`logout`**:
- Attempts JWT invalidation when bearer token looks like a JWT (3 dot-separated segments).
- Also deletes `currentAccessToken()` when supported (supports Sanctum-style tokens if present).
## Response shapes
- **`login` success**: `ApiLoginSecurityService::buildLoginResponse($user, $jwtToken)` (treat as canonical shape).
- **`login` failures**:
- `400`: `{ status:false, message:"Email and password are required." }`
- `401`: `{ status:false, message:"Invalid email or password." }`
- `403`: `{ status:false, message:"Account suspended. Please reset your password." }`
- `429`: `{ status:false, message:"Too many failed attempts..." }`
- **`refresh` success** (Base API shape):
- `{ status:true, message:"Token refreshed.", data:{ access_token, token_type:"bearer", expires_in } }`
- **`me` success** (Base API shape):
- `{ status:true, message:"OK", data:{ id, firstname, lastname, email, class_section_id, class_section_name } }`
- **`logout` success** (Base API shape):
- `{ status:true, message:"Logged out.", data:null }`
## Side effects & security notes
- Logs IP attempt / failed attempt / successful login via `ApiLoginSecurityService`.
- Suspended users are rejected **after** verifying credentials (avoids email enumeration by response shape).
## Error handling expectations
- `refresh`: any exception becomes `401` via `respondError('Token refresh failed.', 401)`.
- `logout`: JWT invalidation errors are intentionally ignored.
## Test plan (feature)
- **Login**
- Missing email/password → `400`.
- Unknown email → `401` and IP attempt logged.
- Wrong password → `401` and failed attempt handling invoked.
- Suspended user with correct password → `403`.
- Successful login returns expected response shape and includes token.
- IP blocked → `429`.
- **Refresh**
- Valid token refresh returns new token + expires_in.
- Invalid/missing token returns `401`.
- **Me/Logout**
- Requires auth; returns `401` unauthenticated.
- Logout returns success even if token invalidation fails.
@@ -0,0 +1,70 @@
# `AuthorizedUserInviteController` plan
## Scope
- **Controller**: `app/Http/Controllers/Api/Parents/AuthorizedUserInviteController.php`
- **Purpose**: public invite confirmation + password setup for “authorized users” invited by a parent.
- **Primary dependency**: `App\Services\Parents\AuthorizedUsersManagementService`
## Routes
- `GET /api/confirm_authorized_user?token=...``confirm`
- `GET /api/set_authorized_user_password/{authorizedUserId}?token=...``setPasswordForm`
- `POST /api/set_authorized_user_password/{authorizedUserId}``savePassword`
- Path constraint: `{authorizedUserId}` is numeric
## Authentication & authorization
- These endpoints are **public** and rely on **invite tokens** for authorization.
- Redirects are protected via an allowlist check (`isTrustedRedirectUrl()`).
## Endpoint behavior
### `confirm`
- Input:
- Query param `token` (string)
- Calls `AuthorizedUsersManagementService::confirmEmailClick($token)` and expects `redirect_url`.
- Output:
- If request expects JSON: `{ message:"Confirmed.", redirect_url }`
- Otherwise redirects the browser to `redirect_url` via `redirect()->away(...)`
- Errors:
- Invalid token → `400` (JSON or minimal HTML page)
- Untrusted redirect URL → `400` JSON `{ message:"Invalid redirect URL." }`
### `setPasswordForm`
- Input:
- Route param `authorizedUserId`
- Query param `token`
- Calls `AuthorizedUsersManagementService::findActiveSetupRow($authorizedUserId, $token)`.
- Output:
- JSON mode: `{ message:"OK", user_id, token }`
- HTML mode: instructional HTML (no actual password form UI)
- Errors:
- Invalid/expired link → `400` (JSON or plain text)
### `savePassword`
- Input (JSON or form):
- `password` (required; min 8; must include upper/lower/number/special)
- `password_confirmation` or `password_confirm` (one required; must match `password`)
- `user_id` (required int) — expected to match the authorized users owning parent id
- `token` (required string)
- Calls `AuthorizedUsersManagementService::setAuthorizedUserPassword(...)`.
- Output:
- Success: `{ message:"Password has been successfully set." }`
- Errors:
- Validation → `422` with `{ message, errors }`
- Invalid token / mismatch / expired setup row → `400` `{ message }`
## Security notes
- **Redirect allowlist**: only hosts matching `app.url`, `app.frontend_url`, or `services.frontend.url`.
- **Password policy** is enforced at the API level via regex rules.
## Test plan (feature)
- `confirm`
- Invalid token → 400 JSON/HTML depending on `Accept`.
- Valid token but redirect host not allowlisted → 400.
- Valid token → JSON includes redirect_url; non-JSON issues redirect.
- `setPasswordForm`
- Invalid token/id → 400.
- Valid token/id → returns JSON payload or HTML string.
- `savePassword`
- Weak password / missing confirm → 422.
- Token mismatch/expired → 400.
- Success → 200 and password is set (can authenticate as that authorized user).
@@ -0,0 +1,76 @@
# `AuthorizedUsersController` plan
## Scope
- **Controller**: `app/Http/Controllers/Api/Parents/AuthorizedUsersController.php`
- **Purpose**: parent CRUD for secondary “authorized users” attached to the parent account.
- **Primary dependency**: `App\Services\Parents\AuthorizedUsersManagementService`
- **Model**: `App\Models\AuthorizedUser`
## Routes
All routes are under `v1` + `parents` group:
- Base prefix: `GET/POST/PATCH/DELETE /api/v1/parents/...`
- Group middleware: `parent.access`
Endpoints:
- `GET /api/v1/parents/authorized-users``index`
- `GET /api/v1/parents/authorized-users/{id}``show`
- `POST /api/v1/parents/authorized-users``store`
- `PATCH /api/v1/parents/authorized-users/{id}``update`
- `DELETE /api/v1/parents/authorized-users/{id}``destroy`
## Authentication & authorization
- Requires authenticated parent context (effectively enforced by `parent.access` + `auth()` usage).
- Ownership check:
- All record reads/writes are constrained to `AuthorizedUser.user_id = parentId`.
## Requests/validation
- `store`: `StoreAuthorizedUserRequest`
- Reads validated `email` and lowercases/trims before invite.
- `update`: `UpdateAuthorizedUserRequest`
- Supports email change; re-invites when email changed.
## Endpoint behavior
### `index`
- Returns all authorized users for the parent ordered by id.
- Response shape uses Base API envelope:
- `{ status:true, message:"Success", data:[...] }`
### `show`
- Loads an authorized user row for this parent or returns 404.
- Response shape: Base API envelope with the model row.
### `store`
- Invites by email via `AuthorizedUsersManagementService::inviteByEmail($parentId, $email)`.
- If invite already existed / not newly created:
- Returns `200` success with message explaining email sent conditionally.
- If created:
- Returns `201` with created id.
### `update`
- If email provided:
- Calls `changeEmailAndReinvite($row, $email)`
- On invalid argument: returns `422` with error message
- If email not provided:
- Returns success message (no-op update)
### `destroy`
- Deletes the owned record and returns success message.
## Error handling
- Unauthenticated → `401` Base API error `"Authentication required."`
- Not found → `404` Base API error `"Authorized user not found."`
- Invalid email change → `422` Base API error with exception message
## Test plan (feature)
- Auth required:
- Unauthenticated requests → 401 for all endpoints.
- Ownership:
- Parent cannot read/update/delete someone elses authorized user (404).
- Store:
- New email creates record → 201 + id.
- Existing/duplicate invite path → 200 with “If the email belongs…” message.
- Update:
- Email change triggers reinvite; invalid email triggers 422.
- Delete:
- Owned record deleted; subsequent fetch returns 404.
+66
View File
@@ -0,0 +1,66 @@
# `BaseApiController` plan
## Scope
- **Controller**: `app/Http/Controllers/Api/Core/BaseApiController.php`
- **Purpose**: shared API conveniences:
- Standard success/error JSON envelope helpers
- Request payload normalization (JSON body vs query/form)
- Validation helper compatible with legacy CodeIgniter-like rules
- Lightweight pagination helper for arrays/query builders
- Current-user resolution helpers (id + role list)
## Key responsibilities
### Response helpers
- `success($data, $message, $code)``{ status:true, message, data }`
- `error($message, $code, $errors?)``{ status:false, message, errors? }`
- Convenience wrappers:
- `respondSuccess`, `respondCreated`, `respondDeleted`, `respondError`, `respondValidationError`
### Request normalization
- Stores the underlying Laravel request as `laravelRequest`.
- Wraps it in `CiRequestAdapter` as `$this->request` for compatibility patterns.
- `payloadData()`:
- Uses JSON-decoded body when present and valid
- Falls back to merged query + form data
### Validation
- `validateRequest($data, $rules)`:
- Normalizes CodeIgniter-like rule strings into Laravel rules via `normalizeRules()`.
- Saves validator to `$this->validator`.
- Returns `[]` on success, or `errors()->toArray()` on failure.
- `validate($rules)` validates against `payloadData()` and returns boolean.
### Pagination
- `paginate($source, $page, $perPage)` supports:
- Eloquent builder / query builder
- Plain arrays
- Returns `{ data: [...], pagination:{ current_page, per_page, total, total_pages } }`
### Current user helpers
- `getCurrentUserId()` resolves authenticated id from multiple sources.
- `getCurrentUser()`:
- Loads `User` row
- Computes display name
- Loads roles via `user_roles`/`roles` join (best-effort; logs error and returns `roles: []` if it fails)
## Usage guidance (project conventions)
- **Controllers extending `BaseApiController`** should prefer:
- `respondSuccess`/`respondError` for consistent envelopes where possible.
- Returning explicit `{ ok: true }` shapes only when required for legacy client compatibility.
- **Validation**:
- Prefer dedicated `FormRequest` classes for new endpoints.
- Use `validateRequest/normalizeRules` only for legacy/bridge use cases.
## Test plan (unit)
- `normalizeRules()` conversions:
- `max_length[n]``max:n`, `min_length[n]``min:n`, `valid_email``email`, `permit_empty``nullable`, `is_unique[t.c]``unique:t,c`, etc.
- `payloadData()`:
- JSON body takes precedence when valid
- Falls back to query+form when no JSON
- `paginate()`:
- Eloquent builder: returns expected total + slices
- Array source: returns expected slice
- `getCurrentUser()`:
- No auth → `null`
- Role query failure → returns user with empty roles and logs error
+75
View File
@@ -0,0 +1,75 @@
# `ChargeController` plan
## Scope
- **Controller**: `app/Http/Controllers/Api/Finance/ChargeController.php`
- **Purpose**: list charges for a parent and manage extra/event charges (create/cancel/apply/mark paid).
- **Primary dependency**: `App\Services\Billing\ChargeService`
- **Resources**:
- `ChargeListResource` (for list responses)
- `ChargeActionResource` (for mutation responses)
## Routes
All routes are under:
- Base prefix: `/api/v1/finance/fees/charges`
Endpoints:
- `GET /api/v1/finance/fees/charges/``index`
- `POST /api/v1/finance/fees/charges/extra``storeExtra`
- `POST /api/v1/finance/fees/charges/event``storeEvent`
- `POST /api/v1/finance/fees/charges/extra/{id}/cancel``cancelExtra`
- `POST /api/v1/finance/fees/charges/extra/{id}/apply``applyExtra`
- `POST /api/v1/finance/fees/charges/event/{id}/cancel``cancelEvent`
- `POST /api/v1/finance/fees/charges/event/{id}/mark-paid``markEventPaid`
## Authentication & authorization
- `storeExtra` / `storeEvent` require an authenticated user id (checked by `authenticatedUserIdOrUnauthorized()`).
- Other endpoints currently do not call the helper; authorization is expected to be enforced by surrounding route middleware and/or service-layer checks.
## Requests/validation
- `index`: `ChargeListRequest` (`parent_id` required; optional `school_year`)
- `storeExtra`: `StoreExtraChargeRequest` (+ adds `created_by` from authenticated user)
- `storeEvent`: `StoreEventChargeRequest` (+ adds `created_by`)
- `applyExtra`: `ApplyExtraChargeRequest` (`invoice_id`)
- `markEventPaid`: `MarkEventChargePaidRequest` (`paid`, optional `payment_id`)
- `cancelEvent`: uses raw `Request` with query boolean `issue_credit` (default true)
## Response shapes
Legacy `{ ok: ... }` shape:
- `index`:
- `{ ok:true, charges: ChargeListResource }`
- Mutations:
- `{ ok:(bool), charge: ChargeActionResource }`
- HTTP status:
- Create success: `201`
- Duplicate charge success-case: `409` (when `error === "duplicate_charge"`)
- Validation/business failure: `422`
- Cancel/apply/mark-paid: `200` when ok, else `422`
## Side effects
- `storeExtra` / `storeEvent` attach `created_by` for auditing.
- `cancelEvent` optionally issues credits (controlled via `issue_credit` query flag).
- `markEventPaid` can link to a `payment_id` when supplied.
## Error handling expectations
- `storeExtra` / `storeEvent` catch unexpected exceptions:
- return `500` `{ ok:false, message:"Unable to create ... charge." }`
- Unauthorized (store endpoints) returns:
- `401` `{ ok:false, message:"Unauthorized." }`
## Test plan (feature)
- Listing:
- Returns `ChargeListResource` payload for a parent with charges.
- Create extra/event:
- Unauthenticated → 401.
- Valid payload → 201 and ok true.
- Duplicate charge → 409.
- Service exception → 500.
- Apply extra:
- Valid invoice id applies charge → 200.
- Invalid invoice/charge → 422.
- Cancel:
- Extra cancel → 200/422 depending on service result.
- Event cancel with `issue_credit=false` behaves accordingly.
- Mark paid:
- Toggle paid true/false; supports payment_id link.
@@ -0,0 +1,66 @@
# `FeeCalculationController` plan
## Scope
- **Controller**: `app/Http/Controllers/Api/Finance/FeeCalculationController.php`
- **Purpose**: finance calculation endpoints (refunds, tuition totals/breakdowns, family balances).
- **Primary dependencies**:
- `FeeRefundCalculatorService` (refund calculations)
- `FeeStudentFeeService` (tuition total)
- `TuitionCalculationService` (tuition breakdown)
- `BalanceCalculationService` (family account/balance aggregation)
- **Resources**:
- `FeeRefundResource`
- `FeeTuitionTotalResource`
- `FeeTuitionBreakdownResource`
- `FeeFamilyBalanceResource`
## Routes
All routes are under:
- Base prefix: `/api/v1/finance`
- (Route file shows this under a `finance` group; actual auth middleware depends on the surrounding `v1` group setup.)
Endpoints:
- `POST /api/v1/finance/fees/refund``refund`
- `POST /api/v1/finance/fees/tuition-total``tuitionTotal`
- `POST /api/v1/finance/fees/tuition-breakdown``tuitionBreakdown`
- `POST /api/v1/finance/fees/family-balance``familyBalance`
## Requests/validation
- `refund`: `FeeRefundRequest`
- expects `parent_id` and `students` array
- `tuitionTotal`: `FeeTuitionTotalRequest`
- expects `students` array
- `tuitionBreakdown`: `FeeTuitionBreakdownRequest`
- expects `students` array
- `familyBalance`: `FeeFamilyBalanceRequest`
- expects `parent_id`, `students` array, optional `school_year`
## Response shapes
Legacy `{ ok: ... }` shape:
- `refund`:
- `{ ok:true, refund: FeeRefundResource }`
- `tuitionTotal`:
- `{ ok:true, tuition: FeeTuitionTotalResource({ total_tuition }) }`
- `tuitionBreakdown`:
- `{ ok:true, tuition: FeeTuitionBreakdownResource }`
- `familyBalance`:
- `{ ok:true, account: FeeFamilyBalanceResource }`
## Error handling expectations
- Each endpoint wraps calculation in `try/catch`:
- On exception: logs error and returns `500` with `{ ok:false, message:"Unable to ..." }`
- Validation errors are handled by the FormRequest layer (422).
## Test plan (feature/unit mix)
- Validation:
- Missing required fields → 422.
- Refund:
- Valid input returns `ok:true` and resource fields.
- Service exception returns 500 and message.
- Tuition total/breakdown:
- Calculates expected totals for a known dataset.
- Service exception returns 500.
- Family balance:
- Produces stable account totals for known payments/charges.
- Service exception returns 500.
+96
View File
@@ -0,0 +1,96 @@
# `ParentController` plan
## Scope
- **Controller**: `app/Http/Controllers/Api/Parents/ParentController.php`
- **Purpose**: parent portal endpoints for attendance, invoices, enrollments, registration, emergency contacts, profile, and event participation.
- **Primary dependencies**:
- `App\Services\Parents\ParentAttendanceService`
- `App\Services\Parents\ParentInvoiceService`
- `App\Services\Parents\ParentEnrollmentService`
- `App\Services\Parents\ParentRegistrationService`
- `App\Services\Parents\ParentEmergencyContactService`
- `App\Services\Parents\ParentProfileService`
- `App\Services\Parents\ParentEventParticipationService`
## Routes
All routes are under `v1` + `parents` group:
- Base prefix: `GET/POST/PATCH/DELETE /api/v1/parents/...`
- Group middleware: `parent.access`
Endpoints:
- `GET /api/v1/parents/attendance``attendance`
- `GET /api/v1/parents/invoices``invoices`
- `GET /api/v1/parents/enrollments``enrollments`
- `POST /api/v1/parents/enrollments``updateEnrollments`
- `GET /api/v1/parents/registration``registration`
- `POST /api/v1/parents/registration``storeRegistration`
- `PATCH /api/v1/parents/students/{studentId}``updateStudent`
- `DELETE /api/v1/parents/students/{studentId}``deleteStudent`
- `GET /api/v1/parents/emergency-contacts``emergencyContacts`
- `POST /api/v1/parents/emergency-contacts``storeEmergencyContact`
- `PATCH /api/v1/parents/emergency-contacts/{contactId}``updateEmergencyContact`
- `GET /api/v1/parents/profile``profile`
- `PATCH /api/v1/parents/profile``updateProfile`
- `GET /api/v1/parents/events``events`
- `POST /api/v1/parents/events/participation``updateParticipation`
## Authentication & authorization
- Uses `parent.access` middleware which resolves “secondary”/“authorized” users to a primary parent id and stores it in the request attribute `primary_parent_id`.
- Controller helper `parentIdOrUnauthorized()`:
- Prefer `request()->attributes->get('primary_parent_id')`
- Fallback to `auth()->id()`
- On failure returns JSON `{ ok:false, message:"Unauthorized." }` with `401`
## Requests/validation
Validated via `FormRequest` classes per endpoint:
- `attendance`: `ParentAttendanceRequest` (optional `school_year`)
- `invoices`: `ParentInvoiceRequest` (optional `school_year`)
- `enrollments`: `ParentEnrollmentRequest` (optional `school_year`)
- `updateEnrollments`: `ParentEnrollmentActionRequest` (`enroll[]`, `withdraw[]`)
- `storeRegistration`: `ParentRegistrationRequest` (`students[]`, `emergency_contacts[]`)
- `updateStudent`: `ParentStudentUpdateRequest`
- `store/update emergency contact`: `ParentEmergencyContactRequest`
- `updateProfile`: `ParentProfileUpdateRequest`
- `updateParticipation`: `ParentEventParticipationRequest` (`participation[]`)
## Response shapes (legacy `{ ok: ... }`)
This controller largely returns a legacy parent-portal shape rather than the Base API envelope.
- `attendance`:
- `{ ok:true, attendance:[...], schoolYears:[...], selectedYear }`
- `attendance` uses `ParentAttendanceResource::collection(...)`
- `invoices`:
- `{ ok:true, invoices:[...] }` (`InvoiceResource::collection(...)`)
- `enrollments`:
- `{ ok:true, students:[...], schoolYears:[...], selectedYear, withdrawalDeadline, lastDayOfRegistration, schoolStartDate }`
- `updateEnrollments`:
- `{ ok:true, enrolled:[...], withdrawn:[...] }`
- `registration`:
- `{ ok:true, parent, existingKids:[...], emergencies:[...], maxChilds, maxEmergency, lastDayOfRegistration, registrationAgeDeadline }`
- `storeRegistration`:
- `{ ok:true, created_student_ids:[...], skipped:[...] }` with status `201` if created_count>0 else `200`
- `updateStudent` / `deleteStudent` / `updateParticipation`:
- `{ ok:true }`
- `emergencyContacts`:
- `{ ok:true, contacts:[...] }`
- `store/update emergency contact`:
- `{ ok:true, contact:{...} }` (store returns `201`)
- `profile` / `updateProfile`:
- `{ ok:true, profile:{...} }`
- `events`:
- `{ ok:true, activeEvents, charges, yourStudents }`
## Error handling expectations
- Unauthorized: `{ ok:false, message:"Unauthorized." }` with `401` (from `parentIdOrUnauthorized()`).
- Service-layer exceptions are generally allowed to bubble unless handled in the service; controller does minimal try/catch.
## Test plan (feature)
- Auth/parent.access:
- Without auth → 401 `{ ok:false }`.
- With secondary/authorized user → resolved to primary parent id and returns correct data.
- Each endpoint:
- Valid request returns `ok:true` and expected keys.
- Validation errors are returned by FormRequest (422).
- Registration:
- Returns 201 when at least one student is created, 200 when all entries skipped.
@@ -0,0 +1,69 @@
# `ParentPromotionController` plan
## Scope
- **Controller**: `app/Http/Controllers/Api/Parents/ParentPromotionController.php`
- **Purpose**: parent portal endpoints for promotion + enrollment workflow.
- **Primary dependencies**:
- `App\Services\Promotions\PromotionEnrollmentService`
- `App\Services\Promotions\PromotionQueryService`
- **Models**:
- `App\Models\StudentPromotionRecord`
- `App\Models\Student` (ownership verification fallback)
## Routes
All routes are under `v1` + `parents` group:
- Base prefix: `... /api/v1/parents/promotions`
- Group middleware: `parent.access`
Endpoints:
- `GET /api/v1/parents/promotions/``overview`
- `GET /api/v1/parents/promotions/{promotionId}``show`
- `POST /api/v1/parents/promotions/{promotionId}/start``start`
- `PATCH /api/v1/parents/promotions/{promotionId}/steps``updateSteps`
- `POST /api/v1/parents/promotions/{promotionId}/submit``submit`
## Authentication & authorization
- Parent context resolved via `parentIdOrUnauthorized()`:
- prefers request attribute `primary_parent_id` (set by `parent.access`)
- falls back to `auth()->id()`
- Ownership is enforced in `loadOwnedRecord()`:
- Primary check: `StudentPromotionRecord.parent_id == parentId`
- Fallback: loads `Student.parent_id` for the records `student_id`
- If not owned: returns `403` `{ ok:false, message:"You are not authorised..." }`
## Requests/validation
- `overview`: `ParentPromotionOverviewRequest` (optional `next_school_year`)
- `updateSteps`: `ParentEnrollmentStepRequest` (`steps()` accessor provides structured step payload)
## Response shapes
- `overview`:
- `{ ok:true, records, next_school_year, message }`
- Note: `records` are returned as provided by `PromotionEnrollmentService::overviewForParent(...)` (already shaped for the client)
- `show` / `start` / `updateSteps` / `submit`:
- `{ ok:true, data: StudentPromotionResource(...) }`
## State transitions (high-level)
- `start``PromotionEnrollmentService::startEnrollment(...)`
- `updateSteps``PromotionEnrollmentService::updateSteps(...)`
- `submit``PromotionEnrollmentService::submitEnrollment(...)`
- Conflicts (invalid transition / deadline / status) surface as `409` with `{ ok:false, message }`.
## Error handling expectations
- Unauthorized → `401` `{ ok:false, message:"Unauthorized." }`
- Record not found → `404` `{ ok:false, message:"Promotion record not found." }`
- Not owned → `403` `{ ok:false, message:"You are not authorised..." }`
- Invalid transition → `409` `{ ok:false, message }`
## Test plan (feature)
- Ownership:
- Parent cannot access another parents record (403).
- Record with mismatched `parent_id` but student belongs to parent passes ownership check.
- Happy path:
- `overview` returns list for parent.
- `start` moves record to started state and returns updated resource.
- `updateSteps` persists steps and returns updated resource.
- `submit` transitions to submitted state.
- Conflict cases:
- Submitting without required steps → 409.
- Past deadline → 409 (as enforced by service).
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,45 @@
# Islamic Sunday School Modular Plan Update Index
This bundle updates the global modular plan and Phases 1-6 so the extension domain is explicitly **Islamic Sunday School**.
Core rule: `SchoolCore` remains neutral. Islamic-specific behavior belongs in `App\Domain\IslamicSundaySchool` through policies, resolvers, services, templates, and extension profile tables.
## Updated Files
- `app_project_plan_global_modular_islamic.md`
- `phase_1_school_context_execution_plan_islamic.md`
- `phase_2_core_contracts_execution_plan_islamic.md`
- `phase_3_modular_finance_execution_plan_islamic.md`
- `phase_4_modular_attendance_execution_plan_islamic.md`
- `phase_5_modular_student_lifecycle_execution_plan_islamic.md`
- `phase_6_modular_communication_execution_plan_islamic.md`
## Key Renames
```text
App\Domain\SundaySchool -> App\Domain\IslamicSundaySchool
sunday_school -> islamic_sunday_school
Sunday School profile -> Islamic Sunday School profile
church/ministry/pastoral terms -> masjid/community, volunteer/program, imam/admin or religious-sensitive terms
Bible curriculum -> Qur'an/Arabic/Islamic studies curriculum
```
## Non-Negotiable Boundary
`SchoolCore` must not import `IslamicSundaySchool`.
Allowed:
```text
IslamicSundaySchool -> SchoolCore contracts
SchoolCore -> Shared neutral infrastructure
Controllers -> contracts
```
Forbidden:
```text
SchoolCore -> IslamicSundaySchool
SchoolCore contracts containing Islamic-specific vocabulary
Controllers hardcoding Islamic Sunday School rules
```
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,11 @@
# Phase 9 module ownership. Replace usernames/team slugs with real owners before enforcing.
/app/Domain/SchoolCore/Context/ @platform-owner
/app/Domain/SchoolCore/Finance/ @finance-owner @platform-owner
/app/Domain/SchoolCore/Attendance/ @attendance-owner @platform-owner
/app/Domain/SchoolCore/Students/ @students-owner @platform-owner
/app/Domain/SchoolCore/Communication/ @communication-owner @platform-owner
/app/Domain/SchoolCore/Reporting/ @reporting-owner @data-security-owner
/app/Domain/IslamicSundaySchool/ @islamic-sunday-school-owner @platform-owner
/app/Http/Controllers/ @api-owner
/routes/ @api-owner @platform-owner
/docs/ @platform-owner
@@ -0,0 +1,19 @@
## Modular Architecture Checklist
- [ ] SchoolContext is used where required.
- [ ] No SchoolCore dependency on IslamicSundaySchool.
- [ ] Controllers remain thin and delegate to contracts.
- [ ] FormRequest validates mutable endpoint input.
- [ ] Resource or ApiResponse controls output.
- [ ] Authorization is tested.
- [ ] Wrong-school access is tested for scoped data.
- [ ] Sensitive fields are protected or masked.
- [ ] New route appears in route inventory.
- [ ] New report/export is audited.
- [ ] Bulk communication requires preview.
- [ ] Finance changes avoid float math and direct controller mutation.
- [ ] Module owner reviewed.
## Risk Notes
List any boundary exceptions, deprecated paths, new routes, new reports/exports, or sensitive data exposure. If this section is empty for a high-risk change, assume the PR is lying by omission.
@@ -0,0 +1,29 @@
name: Architecture Governance
on:
pull_request:
push:
branches: [ main ]
jobs:
architecture:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
- name: Validate composer
run: composer validate --no-check-publish || true
- name: Install dependencies
run: composer install --no-interaction --prefer-dist || true
- name: Run architecture tests
run: php artisan test tests/Architecture || true
- name: Run architecture scan in warning mode
run: php artisan app:architecture-scan --fail-on=none || true
- name: Generate route inventory
run: php artisan api:route-inventory --markdown || true
- name: Check route inventory
run: php artisan app:route-inventory-check || true
- name: Generate dependency map
run: php artisan app:dependency-map --markdown || true
@@ -0,0 +1,23 @@
name: Release Gate
on:
workflow_dispatch:
push:
tags:
- 'v*'
jobs:
release-gate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
- run: composer install --no-interaction --prefer-dist || true
- run: php artisan test tests/Architecture
- run: php artisan app:architecture-scan --fail-on=error
- run: php artisan api:route-inventory --markdown
- run: php artisan app:route-inventory-check
- run: php artisan app:docs-coverage
- run: php artisan app:dependency-map --markdown
@@ -0,0 +1,27 @@
name: Release Readiness
on:
workflow_dispatch:
pull_request:
paths:
- 'app/**'
- 'config/**'
- 'routes/**'
- 'docs/release/**'
- '.github/workflows/release-readiness.yml'
jobs:
release-readiness:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
- run: composer install --no-interaction --prefer-dist
- run: php artisan app:release-feature-flags
- run: php artisan app:release-migration-validate
- run: php artisan app:release-shadow-compare
- run: php artisan app:legacy-unsafe-route-audit
- run: php artisan app:release-readiness-gate --warning-mode
- run: php artisan test tests/Feature/Release tests/Architecture/Phase10ReleaseReadinessArchitectureTest.php
@@ -0,0 +1,764 @@
# Phase 1 Execution Plan: SchoolContext Foundation
# Islamic Sunday School Alignment Update
Phase 1 must support **Islamic Sunday School** as a domain profile, not generic Sunday School or church-oriented Sunday School.
Add the domain profile value:
```text
islamic_sunday_school
```
`SchoolContext` may expose this profile neutrally as `domain_profile`, but `SchoolCore` services must not branch on Islamic-specific concepts directly. Any Islamic Sunday School behavior must be resolved through extension bindings, policy contracts, or context-aware providers.
Examples of extension-only concepts:
```text
masjid_id
program_session_id
halaqa_id
quran_level_id
arabic_level_id
islamic_studies_track_id
volunteer_team_id
imam_admin_note_permission
```
The core still uses neutral terms: `school`, `term`, `session`, `program`, `group`, `family`, `guardian`, `student`, and `staff`.
---
## 1. Objective
Create a mandatory, immutable `SchoolContext` foundation so every reusable service can operate safely across different school systems.
This phase is the base layer for the modular platform. Without it, `SchoolCore` cannot be reliably reused by Islamic Sunday School, traditional schools, training centers, after-school programs, or any other future domain. A modular platform without tenant/context isolation is just a data leak wearing a nicer folder structure.
## 2. Target Outcome
At the end of Phase 1:
- Every new core service receives an explicit `SchoolContext`.
- Controllers no longer rely only on scattered `auth()`, request values, global configuration, or session-like assumptions to determine school scope.
- Cross-school access fails by default.
- Islamic Sunday School can resolve its own domain profile through context, without forcing Islamic Sunday School concepts into global services.
- Finance, attendance, students, files, reporting, and communication can begin migration into `SchoolCore` safely.
## 3. Current Problem to Fix
The current app has school/year/semester context scattered across:
- authenticated user fields such as `school_id`, `school_year`, and `semester`;
- global configuration access such as `Configuration::getConfig('school_year')` and `Configuration::getConfig('semester')`;
- request query/body values;
- controller-specific assumptions;
- service-level fallback behavior;
- model methods that directly read global configuration;
- finance, attendance, reporting, and student queries that depend on string school-year/semester filters.
This makes the app hard to reuse for multiple school types because context is implicit instead of explicit.
## 4. Design Principle
`SchoolContext` is not a convenience object. It is a safety boundary.
Core rule:
```php
SchoolCore services must not guess school, year, term, actor, timezone, currency, or domain profile.
```
They must receive those values from an explicit context object created at the API boundary.
## 5. Proposed Namespace Structure
Create these namespaces:
```txt
app/
Domain/
SchoolCore/
Context/
SchoolContext.php
SchoolContextResolver.php
SchoolContextFactory.php
SchoolContextValidator.php
SchoolContextStore.php
SchoolContextException.php
Contracts/
RequiresSchoolContext.php
Support/
SchoolContextAware.php
Http/
Middleware/
ResolveSchoolContext.php
```
Optional later namespaces:
```txt
app/
Domain/
IslamicSundaySchool/
Context/
IslamicSundaySchoolContextExtender.php
```
Do not create the Islamic Sunday School extender in Phase 1 unless there is a concrete field or rule that cannot live in the neutral core context. Premature extension objects are how clean architecture becomes a museum of unused abstractions.
## 6. `SchoolContext` Value Object
### 6.1 Required Fields
Initial version:
```php
final readonly class SchoolContext
{
public function __construct(
public int $schoolId,
public int $actorUserId,
public array $actorRoleIds,
public ?string $schoolYear,
public ?string $term,
public string $timezone,
public string $locale,
public string $currency,
public string $domainProfile,
) {}
}
```
### 6.2 Field Rules
| Field | Required? | Purpose |
|---|---:|---|
| `schoolId` | Yes | Primary isolation boundary. Every scoped query must use it where schema supports it. |
| `actorUserId` | Yes | Audit, authorization, ownership checks. |
| `actorRoleIds` | Yes | Policy/gate checks without repeated role queries. |
| `schoolYear` | Nullable during migration | Current academic year or legacy string value. |
| `term` | Nullable during migration | Current term/semester. Keep name neutral in core. |
| `timezone` | Yes | Date boundaries for attendance, deadlines, reports, finance timestamps. |
| `locale` | Yes | Formatting, translation, report language. |
| `currency` | Yes | Finance defaults. |
| `domainProfile` | Yes | Example: `standard_school`, `islamic_sunday_school`, `training_center`. |
### 6.3 Naming Decision
Use `term` in the core context, not `semester`.
Reason: `semester` is one possible academic model. A global school platform may use quarters, trimesters, sessions, Sunday program cycles, training cohorts, or rolling terms. Use `semester` only inside legacy adapters until migration is complete.
## 7. Context Resolution Strategy
### 7.1 Resolution Priority
`SchoolContextResolver` should resolve context in this order:
1. Authenticated user from JWT/Auth.
2. Explicit school selector from request header or route, if allowed.
3. User default school, currently likely `users.school_id`.
4. Current school year/term from request, if allowed and validated.
5. Config fallback for legacy behavior, such as current `Configuration::getConfig('school_year')` and `Configuration::getConfig('semester')`.
6. Safe defaults for timezone/locale/currency.
### 7.2 Recommended Request Headers
Support these headers during migration:
```txt
X-School-Id
X-School-Year
X-School-Term
X-Domain-Profile
```
Rules:
- `X-School-Id` may only switch context if the actor belongs to that school or has platform-level permission.
- `X-School-Year` and `X-School-Term` must be validated against available academic periods once those models exist.
- `X-Domain-Profile` must not override server-side school configuration unless explicitly allowed for test/dev tooling.
## 8. Middleware Plan
### 8.1 Create `ResolveSchoolContext`
Middleware behavior:
```php
public function handle(Request $request, Closure $next): Response
{
$context = $this->resolver->resolve($request);
$this->validator->assertValid($context, $request->user());
$this->store->set($context);
$request->attributes->set(SchoolContext::class, $context);
return $next($request);
}
```
### 8.2 Middleware Order
Run after authentication and before authorization-sensitive controllers:
```txt
ApiJwtAuth
ResolveSchoolContext
RequirePermission / policies / controller
```
Do not run it before authentication unless supporting public routes. Public routes should either not use `SchoolContext`, or should use an explicit public context resolver with sharply limited behavior.
### 8.3 Route Group Rollout
Start with high-risk route groups only:
```txt
/api/finance/*
/api/payments/*
/api/invoices/*
/api/students/*
/api/attendance/*
/api/scanner/*
/api/files/*
/api/reports/*
/api/messages/*
/api/roles/*
```
Then expand to all authenticated API routes after compatibility is verified.
## 9. Service Container Bindings
Create container bindings:
```php
$this->app->singleton(SchoolContextStore::class);
$this->app->bind(SchoolContextResolver::class);
$this->app->bind(SchoolContextValidator::class);
$this->app->bind(SchoolContextFactory::class);
```
Add a helper only if necessary:
```php
school_context(): SchoolContext
```
Do not let the helper become the main service API. Core services should receive context explicitly in command DTOs or method arguments. A global helper is useful for legacy adapters, not for clean new code. Humans love turning helpers into hidden dependencies, and then act surprised when tests are brittle.
## 10. Service Usage Pattern
### 10.1 Preferred Command DTO Pattern
```php
final readonly class RecordPaymentCommand
{
public function __construct(
public SchoolContext $context,
public int $invoiceId,
public int $studentId,
public string $method,
public int $amountCents,
public ?UploadedFile $file,
public ?string $idempotencyKey,
) {}
}
```
### 10.2 Allowed Transitional Pattern
```php
public function recordPayment(SchoolContext $context, array $payload): PaymentResult
```
### 10.3 Forbidden New Pattern
```php
public function recordPayment(array $payload): PaymentResult
{
$user = auth()->user();
$schoolYear = Configuration::getConfig('school_year');
}
```
That pattern is legacy-only and must be marked as such.
## 11. Repository and Query Scoping
### 11.1 Scope Helper
Create a reusable query helper or trait:
```php
trait AppliesSchoolContext
{
protected function applySchoolScope(Builder $query, SchoolContext $context, string $column = 'school_id'): Builder
{
return $query->where($column, $context->schoolId);
}
}
```
### 11.2 Legacy Schema Handling
Some tables may not have `school_id`. For Phase 1, classify tables into:
| Type | Handling |
|---|---|
| Has `school_id` | Apply strict `school_id` filter. |
| Has academic year/term only | Apply `schoolYear` and `term` filters as legacy scope. |
| Global lookup table | Explicitly mark as global. |
| Missing required scope | Add to schema remediation backlog. |
Do not silently use `Schema::hasColumn()` inside business logic forever. Temporary schema detection is acceptable only inside migration adapters with comments and removal tickets.
## 12. Migration Strategy
### Step 1: Add classes without changing behavior
Create:
- `SchoolContext`
- `SchoolContextFactory`
- `SchoolContextResolver`
- `SchoolContextValidator`
- `SchoolContextStore`
- `ResolveSchoolContext` middleware
Add tests for object creation and resolution.
### Step 2: Resolve context for authenticated routes
Enable middleware in a small route group first. Recommended first group:
```txt
finance/payment read-only endpoint or student read-only endpoint
```
Avoid starting with payment mutation until context resolution has tests. Bravery is not a rollout strategy.
### Step 3: Add context to audit/logging
Every high-risk action should have actor and school context available, even before full service refactor.
Add to logs where possible:
```txt
school_id
actor_user_id
school_year
term
domain_profile
request_id
```
### Step 4: Convert first vertical slice
Choose one low-risk read path first:
```txt
Student list/read
```
Then one high-risk mutation path:
```txt
Manual payment record/edit after tests exist
```
### Step 5: Enforce context in new services
Any new `SchoolCore` service must accept context. No exceptions except explicitly global lookup services.
## 13. First Vertical Slice: Student Read Context
Use student read/list endpoints as the first validation slice because they are easier to test than finance mutations.
### Tasks
- Add `ResolveSchoolContext` to student read route group.
- Inject/request `SchoolContext` in controller.
- Pass context to student query service.
- Apply `school_id` scope where available.
- If student table lacks correct school scoping, document schema gap.
- Add wrong-school read test.
### Acceptance Criteria
- User from School A can list School A students.
- User from School A cannot list School B students.
- Missing/invalid context returns `403` or `422`, not a silent fallback.
- Existing valid Islamic Sunday School requests still work.
## 14. Second Vertical Slice: Payment File Access Context
This should connect to the P0 fix already identified.
### Tasks
- Add context to payment file download route.
- Change route from filename-only to payment/file-id-based access.
- Load payment under `school_id` context.
- Authorize actor against payment/invoice/family/finance role.
- Resolve physical file only from stored payment record.
- Log access attempt with context.
### Acceptance Criteria
- Finance/admin from same school can download payment file.
- Parent/guardian can download only if policy allows and relationship matches.
- User from another school cannot download even if they know the filename.
- Unknown payment returns `404`.
- Known payment with unauthorized actor returns `403`.
## 15. Validation Rules
`SchoolContextValidator` must check:
- actor user exists;
- actor is active/not suspended;
- `schoolId` is positive;
- actor belongs to selected school;
- selected school is active;
- selected year/term is allowed for selected school when academic-period tables are available;
- timezone is valid IANA timezone;
- locale is supported;
- currency is valid ISO currency code;
- domain profile is supported.
During migration, when academic period tables are not yet reliable, log warnings instead of blocking all requests. Do not pretend this is perfect. Transitional systems lie unless you make the lies visible.
## 16. Authorization Integration
Policies and gates should receive or resolve context.
Preferred:
```php
$this->authorize('view', [$student, $context]);
```
Alternative transitional behavior:
```php
$context = app(SchoolContextStore::class)->current();
$this->authorize('view', $student);
```
Policy checks must compare resource scope against context:
```php
return $student->school_id === $context->schoolId;
```
If a model does not have `school_id`, the policy must use a documented relationship path.
## 17. Jobs, Events, and Notifications
Any job created from a scoped action must carry context values explicitly.
### Required Job Fields
```txt
school_id
actor_user_id
school_year
term
domain_profile
locale
timezone
request_id or correlation_id
```
Do not let queued jobs recalculate context from the current logged-in user. There is no current logged-in user in a worker. This is apparently easy to forget until production sends the wrong family a message.
## 18. Files and Storage
File services must receive context.
Recommended storage path pattern:
```txt
schools/{school_id}/{module}/{resource_type}/{resource_id}/{filename}
```
Examples:
```txt
schools/12/payments/checks/8821/check_8821.pdf
schools/12/students/documents/440/immunization.pdf
```
Rules:
- never serve files by arbitrary filename alone;
- never infer authorization from path;
- always authorize resource first;
- path resolution happens after authorization;
- logs include context and resource ID.
## 19. Reporting
Reports must include context in every query.
Minimum rules:
- every report query must accept `SchoolContext`;
- report export filenames should include school/year/term where appropriate;
- cross-school report access must fail;
- raw SQL reports must bind context parameters explicitly.
## 20. Test Plan
### 20.1 Unit Tests
Create tests for:
- `SchoolContext` construction;
- invalid school ID rejection;
- invalid timezone rejection;
- invalid currency rejection;
- resolver reads authenticated user school;
- resolver rejects unauthorized `X-School-Id` switch;
- resolver applies legacy school year/term fallback;
- context store returns current context;
- context store throws when missing context.
### 20.2 Feature Tests
Create tests for:
- authenticated route receives context;
- unauthenticated route cannot resolve protected context;
- School A user cannot access School B student;
- School A finance user cannot access School B payment file;
- `X-School-Id` override works only for authorized multi-school/platform actor;
- timezone affects attendance date boundary;
- context is logged for high-risk mutation.
### 20.3 Architecture Tests
Add static tests/rules:
```txt
SchoolCore services must not call auth()
SchoolCore services must not read Request directly
SchoolCore services must not call Configuration::getConfig directly
SchoolCore services must accept SchoolContext for scoped operations
SchoolCore must not import IslamicSundaySchool namespace
```
## 21. Implementation Tickets
### SCX-001: Create `SchoolContext` value object
**Goal:** Define immutable context object used by core services.
**Tasks:**
- Create `App\Domain\SchoolCore\Context\SchoolContext`.
- Add constructor fields listed in this plan.
- Add helper methods:
- `isIslamicSundaySchool(): bool`
- `requiresTerm(): bool`
- `auditPayload(): array`
- `withTerm(?string $term): self`
- Add unit tests.
**Done when:** object is immutable, tested, and contains no request/auth dependencies.
### SCX-002: Create context resolver and factory
**Goal:** Build context from authenticated request safely.
**Tasks:**
- Create `SchoolContextFactory`.
- Create `SchoolContextResolver`.
- Read user, headers, legacy config, and defaults.
- Validate switch permissions for `X-School-Id`.
- Return deterministic context.
**Done when:** resolver works for normal user, admin user, invalid school, and missing school cases.
### SCX-003: Create context validator
**Goal:** Prevent invalid or unauthorized context.
**Tasks:**
- Create `SchoolContextValidator`.
- Validate actor belongs to selected school.
- Validate school active state where model exists.
- Validate timezone/locale/currency/domain profile.
- Add migration warnings for missing academic period tables.
**Done when:** invalid contexts fail before controller logic executes.
### SCX-004: Add middleware
**Goal:** Attach context to authenticated API requests.
**Tasks:**
- Create `ResolveSchoolContext` middleware.
- Register middleware alias.
- Add to selected route groups after auth.
- Store context in request attributes and `SchoolContextStore`.
**Done when:** protected routes can retrieve context and tests prove middleware order.
### SCX-005: Add query scoping helpers
**Goal:** Make school scoping repeatable.
**Tasks:**
- Create `AppliesSchoolContext` trait or query helper.
- Add strict school scope behavior.
- Add legacy year/term scope behavior.
- Add table classification list for global vs scoped tables.
**Done when:** first read service uses helper and wrong-school reads fail.
### SCX-006: Convert first student read/list slice
**Goal:** Prove context works on a low-risk endpoint.
**Tasks:**
- Add context middleware to selected student route.
- Pass context to student query service.
- Scope query by school.
- Add cross-school tests.
**Done when:** same-school student reads pass and cross-school reads fail.
### SCX-007: Convert payment file access slice
**Goal:** Use context on a high-risk P0 issue.
**Tasks:**
- Replace filename-only access with payment-id-based access.
- Load payment using context.
- Authorize resource before file path resolution.
- Add logs and tests.
**Done when:** guessed filenames cannot access files and cross-school access fails.
### SCX-008: Add job/event context payload convention
**Goal:** Prevent async work from losing school/actor context.
**Tasks:**
- Define `ContextPayload` array format.
- Add helper to create payload from `SchoolContext`.
- Update one notification or payment event as pilot.
- Add tests.
**Done when:** queued pilot job does not depend on live auth/session/request state.
### SCX-009: Add architecture enforcement
**Goal:** Stop new code from bypassing context.
**Tasks:**
- Add static/architecture tests.
- Ban `auth()`, `request()`, and direct `Configuration::getConfig()` inside new `SchoolCore` services.
- Ban `SchoolCore -> IslamicSundaySchool` imports.
**Done when:** CI fails on forbidden dependency direction or hidden context access.
## 22. Rollout Timeline by Workstream
### Workstream A: Foundation
1. `SchoolContext`
2. Factory/resolver/validator
3. Middleware/store
4. Tests
### Workstream B: First Usage
1. Student read/list
2. Payment file access
3. Context-aware logging
### Workstream C: Enforcement
1. Static rules
2. Architecture tests
3. Migration checklist for all future services
## 23. Acceptance Criteria for Phase 1
Phase 1 is complete only when all of this is true:
- `SchoolContext` exists and is immutable.
- Context is resolved for selected authenticated API routes.
- Context includes school, actor, year/term, timezone, locale, currency, and domain profile.
- Cross-school student read test passes.
- Cross-school payment file access test passes.
- At least one read service and one high-risk access path use explicit context.
- New `SchoolCore` services are forbidden from reading request/auth/config directly.
- `SchoolCore` cannot depend on `IslamicSundaySchool` namespace.
- Islamic Sunday School still works through context profile and legacy adapters.
- Known schema gaps are documented instead of silently hidden.
## 24. Risks
| Risk | Impact | Mitigation |
|---|---:|---|
| Tables lack `school_id` | Cross-school isolation may be incomplete | Classify tables and add schema remediation tickets. |
| Legacy code depends on global config | Context behavior may differ from old behavior | Use resolver fallback during migration and log warnings. |
| Too much migration at once | High regression risk | Start with student read/list, then payment file access. |
| Developers bypass context | Architecture decay | Add static rules and CI checks. |
| Islamic Sunday School assumptions leak into core | Platform becomes non-reusable | Enforce dependency direction and neutral vocabulary. |
## 25. Non-Goals
Phase 1 does not include:
- moving every service into `SchoolCore`;
- fully refactoring finance;
- fully refactoring attendance;
- replacing all `semester` fields;
- adding a full multi-tenant school administration UI;
- converting every controller;
- renaming every legacy table.
Trying to do all of that in Phase 1 would be how a refactor becomes a bonfire.
## 26. Immediate Next Implementation Order
Use this order:
```txt
1. Create SchoolContext value object
2. Create resolver/factory/validator/store
3. Add middleware after ApiJwtAuth
4. Add unit tests for resolver and validator
5. Add student read/list context slice
6. Add cross-school student tests
7. Add payment file context slice
8. Add cross-school payment file tests
9. Add architecture rules
10. Document remaining schema gaps
```
## 27. Definition of Success
A developer should be able to write this in a future core service:
```php
public function listStudents(SchoolContext $context, StudentListQuery $query): StudentListResult
```
And the service should not need to know whether the school is a Islamic Sunday School, a private academy, a tutoring center, or a training program.
That is the point. The global platform owns the universal school workflow. Islamic Sunday School supplies policies, labels, calendars, and domain-specific extensions. The core stays boring, reusable, and annoyingly difficult to misuse. Good architecture is mostly removing opportunities for future nonsense.
@@ -0,0 +1,942 @@
# Phase 2 Execution Plan: Core Contracts and Module Boundaries
# Islamic Sunday School Alignment Update
Phase 2 must define contracts that support **Islamic Sunday School** through extension implementations, not by adding Islamic-specific language to the core interfaces.
Preferred extension namespace:
```text
App\Domain\IslamicSundaySchool
```
The core contracts remain school-neutral. Islamic Sunday School implementations may bind policies for:
```text
Qur'an progression
Arabic level placement
Islamic studies tracks
halaqa/class grouping
masjid family/community profile
program-session attendance
volunteer team communication
tuition, sadaqah, scholarship, and fee classification
```
Do not put these terms in `SchoolCore` contracts. The contracts define capabilities. The extension defines Islamic Sunday School behavior.
---
## Purpose
Phase 2 establishes the contract layer for the modular school platform. The goal is to define stable interfaces for shared school operations before moving implementation logic into `SchoolCore`.
This phase prevents the platform from becoming Sunday-School-shaped by accident. Islamic Sunday School may extend the global platform, but the global platform must not know Islamic Sunday School exists.
## Phase 2 Outcome
At the end of this phase:
- `SchoolCore` exposes stable contracts for core school operations.
- Islamic Sunday School behavior plugs into the platform through policies, providers, and extension services.
- Controllers and application services begin depending on contracts instead of concrete legacy services.
- Dependency direction is enforced by automated tests.
- No `SchoolCore` class imports from `IslamicSundaySchool`.
- New implementation work has a clear place to go instead of continuing the existing service sprawl.
## Dependency Rule
The dependency direction must be enforced as architecture law:
```text
Http / Controllers / Requests / Resources
Application Services / Use Cases
SchoolCore Contracts
SchoolCore Implementations
Domain Extensions, such as IslamicSundaySchool, may provide policies/providers
```
Allowed:
```text
IslamicSundaySchool → SchoolCore
Controllers → SchoolCore Contracts
Application Services → SchoolCore Contracts
SchoolCore Implementations → SchoolCore Models / Repositories
```
Forbidden:
```text
SchoolCore → IslamicSundaySchool
SchoolCore → Http Request
SchoolCore → auth()
SchoolCore → controller classes
SchoolCore → route-specific assumptions
SchoolCore → Islamic Sunday School program calendar assumptions
SchoolCore → masjid/community/ministry vocabulary unless stored as extension metadata
```
## Required Namespace Structure
Create this structure first. Do not migrate every class immediately. Empty boundaries are acceptable at the start. Fake modularity is not.
```text
app/
Domain/
SchoolCore/
Contracts/
Context/
Identity/
Students/
Guardians/
Enrollment/
Attendance/
Academics/
Finance/
Communication/
Files/
Reporting/
Authorization/
Support/
IslamicSundaySchool/
Contracts/
Providers/
Policies/
Attendance/
Curriculum/
Ministry/
Families/
Reports/
Shared/
Contracts/
ValueObjects/
Exceptions/
Support/
Providers/
SchoolCoreServiceProvider.php
IslamicSundaySchoolServiceProvider.php
```
## Contract Design Rules
Every contract must follow these rules:
1. Accept `SchoolContext` explicitly or be resolved from a context-aware application service.
2. Use DTOs/value objects for input where payloads are complex.
3. Return DTOs/read models where behavior crosses module boundaries.
4. Never accept raw `Request` objects.
5. Never call `auth()` inside domain services.
6. Never use Islamic Sunday School terminology in `SchoolCore` contracts.
7. Never expose Eloquent models as the required public contract unless the service is strictly internal.
8. Define authorization expectations explicitly.
9. Define transaction ownership explicitly for mutation methods.
10. Include failure behavior in the contract documentation.
## Core Contracts to Create
### Context
```php
namespace App\Domain\SchoolCore\Contracts;
use App\Domain\SchoolCore\Context\SchoolContext;
interface RequiresSchoolContext
{
public function setSchoolContext(SchoolContext $context): void;
}
```
### Students
```php
namespace App\Domain\SchoolCore\Contracts;
use App\Domain\SchoolCore\Context\SchoolContext;
use App\Domain\SchoolCore\Students\Data\CreateStudentData;
use App\Domain\SchoolCore\Students\Data\UpdateStudentData;
use App\Domain\SchoolCore\Students\Data\StudentReadModel;
interface StudentServiceContract
{
public function create(SchoolContext $context, CreateStudentData $data): StudentReadModel;
public function update(SchoolContext $context, int $studentId, UpdateStudentData $data): StudentReadModel;
public function find(SchoolContext $context, int $studentId): ?StudentReadModel;
public function archive(SchoolContext $context, int $studentId, string $reason): void;
}
```
### Student Identifier Generation
```php
namespace App\Domain\SchoolCore\Contracts;
use App\Domain\SchoolCore\Context\SchoolContext;
interface StudentIdentifierGeneratorContract
{
public function generate(SchoolContext $context, int $studentId): string;
}
```
### Guardians
```php
namespace App\Domain\SchoolCore\Contracts;
use App\Domain\SchoolCore\Context\SchoolContext;
use App\Domain\SchoolCore\Guardians\Data\GuardianRelationshipData;
interface GuardianServiceContract
{
public function attachGuardian(SchoolContext $context, int $studentId, GuardianRelationshipData $data): void;
public function detachGuardian(SchoolContext $context, int $studentId, int $guardianId): void;
public function listForStudent(SchoolContext $context, int $studentId): array;
}
```
### Enrollment
```php
namespace App\Domain\SchoolCore\Contracts;
use App\Domain\SchoolCore\Context\SchoolContext;
use App\Domain\SchoolCore\Enrollment\Data\EnrollmentData;
interface EnrollmentServiceContract
{
public function enroll(SchoolContext $context, int $studentId, EnrollmentData $data): void;
public function transfer(SchoolContext $context, int $studentId, EnrollmentData $data): void;
public function withdraw(SchoolContext $context, int $studentId, string $reason): void;
}
```
### Attendance
```php
namespace App\Domain\SchoolCore\Contracts;
use App\Domain\SchoolCore\Context\SchoolContext;
use App\Domain\SchoolCore\Attendance\Data\AttendanceMarkData;
use App\Domain\SchoolCore\Attendance\Data\AttendanceScanData;
use App\Domain\SchoolCore\Attendance\Data\AttendanceResult;
interface AttendanceServiceContract
{
public function markStudent(SchoolContext $context, AttendanceMarkData $data): AttendanceResult;
public function processScan(SchoolContext $context, AttendanceScanData $data): AttendanceResult;
public function summarizeDay(SchoolContext $context, string $date): array;
}
```
### Attendance Policy
```php
namespace App\Domain\SchoolCore\Contracts;
use App\Domain\SchoolCore\Context\SchoolContext;
use DateTimeInterface;
interface AttendancePolicyContract
{
public function isSchoolDay(SchoolContext $context, DateTimeInterface $date): bool;
public function determineStatus(SchoolContext $context, DateTimeInterface $scanTime, array $session): string;
public function duplicateScanWindowSeconds(SchoolContext $context): int;
}
```
### Academic Calendar
```php
namespace App\Domain\SchoolCore\Contracts;
use App\Domain\SchoolCore\Context\SchoolContext;
use DateTimeInterface;
interface AcademicCalendarProviderContract
{
public function currentAcademicYear(SchoolContext $context): ?int;
public function currentTerm(SchoolContext $context): ?int;
public function sessionsForDate(SchoolContext $context, DateTimeInterface $date): array;
}
```
### Academics
```php
namespace App\Domain\SchoolCore\Contracts;
use App\Domain\SchoolCore\Context\SchoolContext;
interface GradeScalePolicyContract
{
public function gradeForScore(SchoolContext $context, float $score): string;
public function passingScore(SchoolContext $context): float;
}
```
```php
namespace App\Domain\SchoolCore\Contracts;
use App\Domain\SchoolCore\Context\SchoolContext;
interface PromotionPolicyContract
{
public function canPromote(SchoolContext $context, int $studentId): bool;
public function nextPlacement(SchoolContext $context, int $studentId): array;
}
```
### Finance
```php
namespace App\Domain\SchoolCore\Contracts;
use App\Domain\SchoolCore\Context\SchoolContext;
use App\Domain\SchoolCore\Finance\Data\CreateInvoiceData;
use App\Domain\SchoolCore\Finance\Data\InvoiceReadModel;
interface InvoiceServiceContract
{
public function create(SchoolContext $context, CreateInvoiceData $data): InvoiceReadModel;
public function recalculate(SchoolContext $context, int $invoiceId): InvoiceReadModel;
public function find(SchoolContext $context, int $invoiceId): ?InvoiceReadModel;
}
```
```php
namespace App\Domain\SchoolCore\Contracts;
use App\Domain\SchoolCore\Context\SchoolContext;
use App\Domain\SchoolCore\Finance\Data\RecordPaymentData;
use App\Domain\SchoolCore\Finance\Data\EditPaymentData;
use App\Domain\SchoolCore\Finance\Data\PaymentReadModel;
interface PaymentServiceContract
{
public function record(SchoolContext $context, RecordPaymentData $data): PaymentReadModel;
public function edit(SchoolContext $context, int $paymentId, EditPaymentData $data): PaymentReadModel;
public function void(SchoolContext $context, int $paymentId, string $reason): void;
}
```
```php
namespace App\Domain\SchoolCore\Contracts;
use App\Domain\SchoolCore\Context\SchoolContext;
interface PaymentAllocationPolicyContract
{
public function allocate(SchoolContext $context, int $invoiceId, float $amount): array;
}
```
### Files
```php
namespace App\Domain\SchoolCore\Contracts;
use App\Domain\SchoolCore\Context\SchoolContext;
interface FileAccessPolicyContract
{
public function canAccess(SchoolContext $context, string $filePurpose, int $ownerId, array $metadata = []): bool;
}
```
```php
namespace App\Domain\SchoolCore\Contracts;
use App\Domain\SchoolCore\Context\SchoolContext;
interface FileStorageServiceContract
{
public function store(SchoolContext $context, string $purpose, mixed $file, array $metadata = []): string;
public function resolvePath(SchoolContext $context, string $fileReference): string;
public function delete(SchoolContext $context, string $fileReference): void;
}
```
### Communication
```php
namespace App\Domain\SchoolCore\Contracts;
use App\Domain\SchoolCore\Context\SchoolContext;
use App\Domain\SchoolCore\Communication\Data\MessageData;
use App\Domain\SchoolCore\Communication\Data\RecipientQuery;
interface CommunicationServiceContract
{
public function previewRecipients(SchoolContext $context, RecipientQuery $query): array;
public function send(SchoolContext $context, MessageData $message): array;
}
```
```php
namespace App\Domain\SchoolCore\Contracts;
use App\Domain\SchoolCore\Context\SchoolContext;
use App\Domain\SchoolCore\Communication\Data\RecipientQuery;
interface RecipientResolverContract
{
public function resolve(SchoolContext $context, RecipientQuery $query): array;
}
```
```php
namespace App\Domain\SchoolCore\Contracts;
use App\Domain\SchoolCore\Context\SchoolContext;
interface CommunicationPreferencePolicyContract
{
public function mayContact(SchoolContext $context, int $recipientId, string $channel, string $purpose): bool;
}
```
### Reporting
```php
namespace App\Domain\SchoolCore\Contracts;
use App\Domain\SchoolCore\Context\SchoolContext;
interface ReportServiceContract
{
public function generate(SchoolContext $context, string $reportKey, array $filters = []): array;
}
```
## DTO and Read Model Rules
Create DTOs only where contracts need them. Do not invent DTOs for every two-field method just to feel enterprise.
Recommended naming:
```text
CreateStudentData
UpdateStudentData
StudentReadModel
AttendanceMarkData
AttendanceScanData
AttendanceResult
CreateInvoiceData
RecordPaymentData
EditPaymentData
PaymentReadModel
MessageData
RecipientQuery
```
Rules:
- DTOs must be immutable where practical.
- DTOs must be created from validated request data, not raw request data.
- DTOs must not call the database.
- Read models must not expose sensitive fields by default.
- Read models must include `school_id` only when needed for authorization/debugging, not casually leaked in every response.
## Service Provider Bindings
Create `SchoolCoreServiceProvider`.
```php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use App\Domain\SchoolCore\Contracts\StudentServiceContract;
use App\Domain\SchoolCore\Students\Services\StudentService;
class SchoolCoreServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->bind(StudentServiceContract::class, StudentService::class);
// Bind additional core contracts as implementations are introduced.
}
}
```
Create `IslamicSundaySchoolServiceProvider` for extension policies.
```php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use App\Domain\SchoolCore\Contracts\AttendancePolicyContract;
use App\Domain\IslamicIslamicSundaySchool\Attendance\IslamicSundaySchoolAttendancePolicy;
class IslamicSundaySchoolServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->bind(AttendancePolicyContract::class, IslamicSundaySchoolAttendancePolicy::class);
}
}
```
Important: core contract bindings may point to core defaults. Islamic Sunday School may override only extension contracts/policies, not replace the entire platform unless a module-specific reason is documented.
## Binding Strategy
Use this three-level binding model:
### 1. Core service contracts
These are stable platform services.
Examples:
```text
StudentServiceContract → SchoolCore\Students\Services\StudentService
PaymentServiceContract → SchoolCore\Finance\Services\PaymentService
AttendanceServiceContract → SchoolCore\Attendance\Services\AttendanceService
```
### 2. Policy/provider contracts
These are extension points.
Examples:
```text
AttendancePolicyContract → IslamicSundaySchool\Attendance\IslamicSundaySchoolAttendancePolicy
AcademicCalendarProviderContract → IslamicSundaySchool\Providers\IslamicSundaySchoolCalendarProvider
PaymentAllocationPolicyContract → IslamicSundaySchool\Policies\IslamicSundaySchoolPaymentAllocationPolicy
```
### 3. Infrastructure contracts
These provide storage, messaging, and integration adapters.
Examples:
```text
FileStorageServiceContract → LaravelLocalFileStorageService
CommunicationServiceContract → LaravelCommunicationService
```
## First Migration Slice: Student Identifier Generation
This is the safest first slice because it fixes a real design bug without moving the whole student module.
### Current Problem
Student creation currently risks race conditions when identifiers are generated using `max(id) + 1` before the student row is safely persisted.
### Target Design
```text
StudentController
→ CreateStudentRequest
→ CreateStudentData
→ StudentServiceContract
→ StudentService
→ StudentIdentifierGeneratorContract
```
### Tasks
1. Create `StudentIdentifierGeneratorContract`.
2. Create default implementation: `SequentialStudentIdentifierGenerator`.
3. Generate identifier after student insert using the persisted student ID.
4. Add unique database constraint for `(school_id, school_id_number)` or equivalent identifier column.
5. Add retry behavior for collisions.
6. Add concurrent creation test.
7. Keep the existing API response compatible.
### Acceptance Criteria
- Two concurrent student creations cannot produce the same identifier.
- Student ID format can be changed by binding a different generator.
- Islamic Sunday School-specific identifier formatting does not live inside core student creation logic.
- Existing student creation endpoint still works.
## Second Migration Slice: Payment File Access Policy
This slice validates that contracts can enforce security boundaries.
### Current Problem
Filename-based file serving risks unauthorized access if a user can guess a payment file name.
### Target Design
```text
PaymentManualController
→ PaymentFileDownloadRequest
→ PaymentServiceContract or PaymentFileService
→ FileAccessPolicyContract
→ FileStorageServiceContract
```
### Tasks
1. Create `FileAccessPolicyContract`.
2. Create `PaymentFileAccessPolicy` implementation.
3. Replace filename-only lookup with payment-ID-based lookup.
4. Resolve file path from the payment record.
5. Authorize against school context and actor permissions.
6. Add audit log entry for file access.
7. Return `403` for unauthorized access.
8. Return `404` for missing file or missing payment.
9. Add tests for guessed filename access.
### Acceptance Criteria
- Users cannot download payment files by guessing filenames.
- Finance/admin users can download authorized payment files.
- Parents can only download files tied to their own financial records, if product rules allow parent access.
- Cross-school access fails.
- Payment file access uses `SchoolContext`.
## Third Migration Slice: Attendance Policy Stub
This slice prepares Phase 4 without fully extracting scanner logic yet.
### Target Design
```text
ScannerController
→ AttendanceServiceContract
→ AttendancePolicyContract
→ AcademicCalendarProviderContract
```
### Tasks
1. Create `AttendancePolicyContract`.
2. Create default `StandardSchoolAttendancePolicy`.
3. Create `IslamicSundaySchoolAttendancePolicy`.
4. Create `AcademicCalendarProviderContract`.
5. Create default provider.
6. Bind Islamic Sunday School provider in `IslamicSundaySchoolServiceProvider`.
7. Update scanner service extraction plan to call policies instead of hardcoded date/session assumptions.
### Acceptance Criteria
- Islamic Sunday School program attendance logic lives outside core.
- Attendance duplicate window is policy-driven.
- Scanner code can call a policy without knowing the school type.
## Controller Dependency Rule
New or touched controllers should depend on contracts, not concrete domain services.
Good:
```php
public function __construct(private StudentServiceContract $students) {}
```
Bad:
```php
public function __construct(private IslamicSundaySchoolStudentService $students) {}
```
Also bad:
```php
public function store(Request $request)
{
$schoolId = auth()->user()->school_id;
// 120 lines of student creation logic
}
```
Because apparently controllers become landfill if left unsupervised.
## Architecture Tests
Add architecture tests using one of these approaches:
- Pest architecture tests if Pest is available.
- PHPUnit custom tests scanning namespaces/imports.
- Static analysis rule in PHPStan/Larastan if already installed.
### Required Architecture Tests
#### `SchoolCore` must not import `IslamicSundaySchool`
```php
public function test_school_core_does_not_depend_on_islamic_sunday_school(): void
{
$files = glob(app_path('Domain/SchoolCore/**/*.php'));
foreach ($files as $file) {
$contents = file_get_contents($file);
$this->assertStringNotContainsString('App\\Domain\\IslamicSundaySchool', $contents, $file);
}
}
```
#### Domain services must not use raw request
```php
public function test_domain_services_do_not_depend_on_http_request(): void
{
$files = glob(app_path('Domain/**/*.php'));
foreach ($files as $file) {
$contents = file_get_contents($file);
$this->assertStringNotContainsString('Illuminate\\Http\\Request', $contents, $file);
}
}
```
#### Domain services must not call `auth()`
```php
public function test_domain_services_do_not_call_auth_helper(): void
{
$files = glob(app_path('Domain/**/*.php'));
foreach ($files as $file) {
$contents = file_get_contents($file);
$this->assertStringNotContainsString('auth()', $contents, $file);
}
}
```
#### Core contracts must be neutral
Search contract names and method names for Islamic Sunday School vocabulary:
```text
masjid/community
ministry
sunday
bible
servant
service_day
```
Those words may appear in `IslamicSundaySchool`, not `SchoolCore`.
## Contract Review Checklist
Before approving a contract, answer:
- Is the name globally meaningful for any school?
- Does it require `SchoolContext`?
- Does it avoid `Request`?
- Does it avoid `auth()`?
- Does it avoid Eloquent leakage across module boundaries?
- Does it describe transaction ownership?
- Does it describe authorization expectations?
- Can Islamic Sunday School customize behavior without editing the contract?
- Can another school type use this contract without pretending to be a Islamic Sunday School?
## Implementation Tickets
### CC-001: Create Domain Namespace Skeleton
**Objective:** Create the folder and namespace structure for `SchoolCore`, `IslamicSundaySchool`, and `Shared`.
**Tasks:**
- Add directory structure.
- Add placeholder README files for each major module.
- Document dependency direction in `app/Domain/README.md`.
**Acceptance Criteria:**
- Namespace structure exists.
- Dependency rule is documented.
- No production behavior changes.
### CC-002: Create Core Contract Interfaces
**Objective:** Add first-pass contracts for students, attendance, finance, files, communication, academics, enrollment, and reporting.
**Tasks:**
- Add contracts under `App\Domain\SchoolCore\Contracts`.
- Add minimal DTO namespaces.
- Add docblocks for transaction and authorization expectations.
**Acceptance Criteria:**
- Contracts compile.
- Contracts use `SchoolContext`.
- Contracts do not mention Islamic Sunday School concepts.
### CC-003: Add Service Providers and Bindings
**Objective:** Bind contracts to initial implementations.
**Tasks:**
- Create `SchoolCoreServiceProvider`.
- Create `IslamicSundaySchoolServiceProvider`.
- Register providers.
- Bind first implementations.
**Acceptance Criteria:**
- Container resolves all initial contracts.
- Islamic Sunday School extension policies can override policy/provider contracts.
- Core service bindings remain neutral.
### CC-004: Add Architecture Boundary Tests
**Objective:** Prevent forbidden dependencies.
**Tasks:**
- Add tests for `SchoolCore -> IslamicSundaySchool` imports.
- Add tests preventing `Request` in domain services.
- Add tests preventing `auth()` in domain services.
- Add tests for forbidden vocabulary in `SchoolCore` contracts.
**Acceptance Criteria:**
- Tests fail when forbidden dependencies are introduced.
- Tests run in CI.
### CC-005: Migrate Student Identifier Generation Behind Contract
**Objective:** Fix student identifier race condition and introduce the first real core contract usage.
**Tasks:**
- Create `StudentIdentifierGeneratorContract`.
- Add default implementation.
- Update student creation flow.
- Add unique constraint.
- Add concurrency test.
**Acceptance Criteria:**
- No duplicate student identifiers under concurrent creation.
- Identifier format can be replaced by binding.
- Existing API response is preserved.
### CC-006: Migrate Payment File Access Behind Policy Contract
**Objective:** Use contract architecture to fix payment file authorization.
**Tasks:**
- Create `FileAccessPolicyContract`.
- Create payment-specific policy implementation.
- Replace filename-only access with payment-ID-based access.
- Add audit log.
- Add authorization tests.
**Acceptance Criteria:**
- Guessed filenames cannot access files.
- Cross-school file access fails.
- Authorized finance/admin access succeeds.
### CC-007: Add Attendance Policy Stub
**Objective:** Prepare attendance extraction by separating attendance rules from scanner implementation.
**Tasks:**
- Create `AttendancePolicyContract`.
- Create `AcademicCalendarProviderContract`.
- Add default and Islamic Sunday School implementations.
- Bind policy/provider implementations.
**Acceptance Criteria:**
- Islamic Sunday School attendance rules are outside `SchoolCore`.
- Scanner extraction can use contracts in Phase 4.
## Phase 2 Rollout Order
Use this order:
1. Create namespace skeleton.
2. Add core contracts.
3. Add DTO/read model skeletons.
4. Add providers and empty bindings.
5. Add architecture tests.
6. Migrate student identifier generation.
7. Migrate payment file access.
8. Add attendance policy stub.
9. Update modular platform plan with completed contracts.
## Phase 2 Done Definition
Phase 2 is complete when:
- Core contracts exist and compile.
- Initial service providers are registered.
- `SchoolCore` cannot depend on `IslamicSundaySchool` without failing tests.
- Domain services cannot use `Request` or `auth()` without failing tests.
- Student identifier generation is contract-based and race-safe.
- Payment file access uses policy-based authorization.
- Attendance has policy/provider contracts ready for extraction.
- Islamic Sunday School behavior is expressed through extension contracts, not hardcoded core logic.
## Risks
### Risk: Too many contracts too early
Mitigation: only create contracts that are needed by migration slices. Keep method count small.
### Risk: Interfaces mirror bad legacy services
Mitigation: design contracts around platform capabilities, not current class names.
### Risk: Islamic Sunday School rules leak into core
Mitigation: forbidden vocabulary tests and provider-based extension points.
### Risk: Controllers keep calling old services
Mitigation: each touched controller must move toward contract injection.
### Risk: DTO explosion
Mitigation: create DTOs for cross-module payloads only. Do not manufacture object bureaucracy for two strings and a prayer.
## Non-Goals
Phase 2 does not fully refactor finance, attendance, students, communication, or reporting.
Phase 2 does not remove every legacy service.
Phase 2 does not redesign every database table.
Phase 2 creates the contract layer and proves the boundary works through a few real migration slices.
## Final Notes
This phase must be boring on purpose. Contracts are not where creativity belongs. They are where the platform decides what it promises and what it refuses to know.
The most important rule: `SchoolCore` must remain generic. Islamic Sunday School extends it. It does not secretly define it.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,579 @@
# School Tuition Payment Management Plan
## 1. Purpose
The school needs a clear tuition management system that calculates tuition, tracks payments, manages refunds, records remaining balances, and handles extra charges and event charges.
The existing fee calculation logic should be preserved where valid, but it should be expanded and corrected so the system can handle complete student billing, not only basic tuition and refunds.
## 2. Current Tuition Logic
The current tuition service calculates tuition using three main fee categories:
- First regular student fee
- Additional regular student fee
- Youth student fee
Students are sorted by grade before tuition is calculated.
### Grade Rules
- Kindergarten is treated as grade level 0.
- Grades 1 through 9 are treated as regular students.
- Grades above 9 are treated as youth students.
- Grade “Y” is treated as youth.
- Unknown or malformed grades are treated as invalid or fallback grade level.
### Tuition Fee Rules
For students in grades K through 9:
- The first regular student is charged the first student fee.
- Each additional regular student is charged the second student fee.
For students above grade 9 or youth category:
- Each youth student is charged the youth fee.
### Tuition Formula
```text
Total Tuition = Regular Student Fees + Youth Student Fees
```
Example:
```text
First regular student fee: $350
Second regular student fee: $200
Youth fee: $180
```
Family has:
- Student 1 in Grade 2
- Student 2 in Grade 5
- Student 3 in Youth
```text
Total Tuition = $350 + $200 + $180 = $730
```
## 3. Required Tuition Calculation Improvements
The tuition calculation should return a detailed breakdown, not only a single total.
Each student billing record should include:
- Student ID
- Student name
- Grade
- Grade level
- Enrollment status
- Admission status
- Fee category
- Tuition fee assigned
- Discount applied
- Extra charges
- Event charges
- Payments applied
- Refund amount, if any
- Remaining balance
The system should generate both:
1. Family-level total balance
2. Student-level billing breakdown
This prevents confusion when a parent has multiple children enrolled.
## 4. Tuition Calculation Process
The system should calculate tuition using the following steps:
1. Retrieve school year configuration.
2. Retrieve fee configuration:
- First student fee
- Second student fee
- Youth fee
- Refund deadline
- Weeks of study
- Last school day
3. Load all students connected to the parent or family account.
4. Fetch each students grade or class section name.
5. Normalize grade names.
6. Sort students by grade level.
7. Assign tuition fee to each eligible student.
8. Apply discounts or scholarships.
9. Add required fees.
10. Add extra charges.
11. Add event charges.
12. Subtract payments and credits.
13. Calculate remaining balance.
## 5. Student Eligibility for Tuition
Tuition should only be calculated for students who meet the billing criteria.
### Billable Students
A student should be billable if:
- Enrollment status is enrolled or payment pending
- Admission status is accepted
### Withdrawn Students
A withdrawn student should not continue to receive new tuition charges unless the school policy requires partial tuition.
Withdrawn statuses may include:
- Withdrawn
- Refund pending
- Withdraw under review
Withdrawn students should be included in refund calculations when applicable.
## 6. Remaining Balance Calculation
The system should calculate the remaining balance at the family level and student level.
### Formula
```text
Remaining Balance = Total Charges - Total Payments - Credits - Refunds Applied to Account
```
Where:
```text
Total Charges = Tuition + Required Fees + Extra Charges + Event Charges + Late Fees
```
Credits may include:
- Scholarships
- Discounts
- Financial aid
- Manual credits
- Canceled event credits
- Overpayment credits
Example:
```text
Tuition: $730
Extra Charges: $100
Event Charges: $50
Total Charges: $880
Payments Made: $500
Credits: $80
Remaining Balance = $880 - $500 - $80 = $300
```
## 7. Refund Calculation
The existing refund calculation should be corrected and formalized.
### Refund Eligibility
A student may be eligible for a refund if:
- The student has a withdrawn-related enrollment status
- A valid withdrawal date exists
- The withdrawal date is on or before the refund deadline
- The parent or guardian has made payments
- The refund does not exceed total amount paid
### Refund Formula
```text
Refund Amount = Student Tuition Fee ÷ Weeks of Study × Weeks Remaining
```
The system should calculate weeks remaining from the withdrawal date to the last school day.
### Refund Cap
Total refund cannot exceed total amount paid by the parent or guardian for the school year.
### Refund Example
```text
Student tuition fee: $350
Weeks of study: 8
Weeks remaining: 4
Refund Amount = $350 ÷ 8 × 4 = $175
```
## 8. Refund Logic Correction Needed
The current refund service contains a logic issue.
The service calculates each students fee, but it does not store that fee on the student record before refund calculation.
The corrected logic should assign the calculated fee to each student:
```php
$student['tuition_fee'] = $studentFee;
```
Then the refund loop should use:
```php
$studentFee = $student['tuition_fee'];
```
Without this correction, refund calculations may be unreliable.
## 9. Extra Charges
Extra charges should be stored separately from tuition.
Examples of extra charges:
- Books
- Uniforms
- Transportation
- Meals
- Technology fee
- Replacement ID card
- Lost book fee
- Damaged equipment fee
- Late pickup fee
- After-school care
- Exam fee
- Late payment fee
Each extra charge should include:
- Student ID
- Parent ID
- School year
- Charge name
- Charge description
- Charge amount
- Charge date
- Due date
- Status
- Created by
- Approval status
Extra charges should be included in the remaining balance but should not automatically affect tuition calculation.
## 10. Event Charges
Event charges should be tracked separately from regular extra charges.
Examples of event charges:
- Graduation
- Field trip
- School camp
- Sports event
- Competition
- Cultural event
- Workshop
- School ceremony
Each event charge should include:
- Event ID
- Event name
- Student ID
- Parent ID
- Event date
- Participation status
- Charge amount
- Payment status
- Refund policy
- Cancellation status
If an event is canceled, the system should either issue a refund or apply the charge as account credit.
## 11. Payment Tracking
Each payment should be recorded against the parent or family account and optionally allocated to specific charges.
Each payment record should include:
- Payment ID
- Parent ID
- Student ID, if applicable
- School year
- Amount paid
- Payment method
- Payment date
- Receipt number
- Payment status
- Notes
- Created by
Payment status may include:
- Pending
- Paid
- Failed
- Refunded
- Partially refunded
- Canceled
## 12. Invoice Structure
The invoice should combine all billing items into one clear document.
Invoice sections should include:
1. Tuition charges
2. Extra charges
3. Event charges
4. Discounts and credits
5. Payments received
6. Refunds
7. Remaining balance
Each invoice should show:
- Invoice number
- Parent or guardian name
- Student names
- School year
- Issue date
- Due date
- Total charges
- Total paid
- Remaining balance
- Payment instructions
## 13. Recommended Service Structure
The billing logic should be split into separate services instead of placing everything inside one fee calculation service.
### TuitionCalculationService
Responsible for:
- Sorting students by grade
- Assigning tuition fee
- Calculating family tuition total
- Returning student-level tuition breakdown
### RefundCalculationService
Responsible for:
- Checking refund eligibility
- Calculating proportional refund
- Applying refund caps
- Returning refund details
### BalanceCalculationService
Responsible for:
- Adding tuition, event charges, and extra charges
- Subtracting payments and credits
- Returning remaining balance
### ChargeService
Responsible for:
- Creating extra charges
- Creating event charges
- Canceling charges
- Marking charges paid or unpaid
### InvoiceService
Responsible for:
- Creating invoices
- Updating invoices
- Generating account statements
- Showing billing history
## 14. Required Reports
The system should generate the following reports:
- Total tuition billed
- Total tuition collected
- Remaining balances by parent
- Remaining balances by student
- Refunds pending
- Refunds issued
- Extra charges collected
- Event charges collected
- Overdue balances
- Payment plan status
- Family account summary
## 15. Controls and Validation
The system should prevent incorrect billing through validation rules.
Required validations:
- Do not calculate refund without withdrawal date.
- Do not refund after refund deadline.
- Do not refund more than amount paid.
- Do not bill students who are not accepted.
- Do not duplicate event charges.
- Do not apply payment to the wrong school year.
- Do not allow negative remaining balance unless recorded as credit.
- Do not allow manual balance edits without audit log.
- Do not delete payments; reverse them with adjustment records.
## 16. Recommended Database Tables
The system should include or update the following tables.
### tuition_configurations
Stores school-year billing settings.
Fields:
- school_year
- first_student_fee
- second_student_fee
- youth_fee
- refund_deadline
- weeks_study
- last_school_day
### student_tuition_records
Stores calculated tuition per student.
Fields:
- student_id
- parent_id
- school_year
- grade
- grade_level
- fee_category
- tuition_fee
- discount_amount
- final_tuition_amount
### charges
Stores extra and event charges.
Fields:
- charge_id
- student_id
- parent_id
- school_year
- charge_type
- charge_name
- amount
- due_date
- status
### payments
Stores parent payments.
Fields:
- payment_id
- parent_id
- school_year
- amount
- method
- payment_date
- status
- receipt_number
### refunds
Stores refund records.
Fields:
- refund_id
- parent_id
- student_id
- school_year
- refund_amount
- reason
- withdrawal_date
- approval_status
- refund_status
### account_ledger
Stores every financial transaction.
Fields:
- ledger_id
- parent_id
- student_id
- school_year
- transaction_type
- description
- debit_amount
- credit_amount
- balance_after_transaction
- created_by
- created_at
## 17. Implementation Priority
### Phase 1: Fix Current Tuition and Refund Logic
- Store assigned tuition fee per student.
- Fix refund calculation bug.
- Return detailed student fee breakdown.
- Add logging for each calculation.
- Add tests for multiple-student families.
### Phase 2: Add Balance Calculation
- Combine tuition, extra charges, event charges, and payments.
- Calculate remaining balance.
- Add family account summary.
### Phase 3: Add Charges
- Create extra charge system.
- Create event charge system.
- Add charge statuses.
- Prevent duplicate charges.
### Phase 4: Add Invoice and Statement System
- Generate invoices.
- Generate monthly statements.
- Show payment history.
- Show remaining balance.
### Phase 5: Add Reports and Audit Logs
- Add finance reports.
- Add refund reports.
- Add audit trail.
- Add manual adjustment tracking.
## 18. Final Recommendation
The existing service should not be thrown away, but it should not remain responsible for the entire billing system.
The school should keep the grade-based tuition rules, correct the refund bug, and expand the system into a proper billing module with separate tuition, refund, charge, payment, invoice, and balance services.
The final system should always answer five questions clearly:
1. What was the student charged?
2. Why was the student charged?
3. What has the parent paid?
4. What has been refunded or credited?
5. What balance remains?
+509
View File
@@ -0,0 +1,509 @@
# Student Promotion and Enrollment Management Plan
## 1. Core Rule
A student who passes the current level should not be automatically placed into the next level.
The student should first be marked as:
**Eligible for Promotion**
After that, the parent or guardian must complete enrollment for the promoted level.
Once the parent completes enrollment, the student becomes:
**Promoted and Enrolled**
No separate school approval is required.
## 2. Promotion Principle
Promotion and enrollment are separate steps.
### Promotion
Promotion confirms that the student passed the current level and qualifies for the next level.
### Enrollment
Enrollment confirms that the parent or guardian wants the student to continue at the school in the promoted level for the next school year.
Once enrollment is completed by the parent, the student should be officially assigned to the new promoted level.
## 3. Promotion Statuses
Recommended statuses:
- Not Reviewed
- Eligible for Promotion
- Awaiting Parent Enrollment
- Enrollment Started
- Promoted and Enrolled
- Conditional Promotion
- Repeated Level
- On Hold
- Withdrawn
- Graduated
- Not Enrolled for Next Year
## 4. Status Definitions
### Not Reviewed
The student has not yet been evaluated for promotion.
### Eligible for Promotion
The student passed the current level and qualifies for the next level, but the parent has not started enrollment yet.
### Awaiting Parent Enrollment
The student is eligible for the next level, and the system is waiting for the parent to complete enrollment.
### Enrollment Started
The parent has started the enrollment process but has not completed all required steps.
### Promoted and Enrolled
The parent completed enrollment for the promoted level. The student is officially placed into the next level for the new school year.
### Conditional Promotion
The student has pending academic or administrative requirements before promotion eligibility can be finalized.
Examples:
- Makeup exam required
- Missing final grade
- Attendance review
- Academic condition pending
### Repeated Level
The student did not meet the passing requirements and must repeat the same level.
### On Hold
The student cannot continue through the promotion process because of an unresolved issue.
Examples:
- Missing academic result
- Enrollment form incomplete
- Financial hold, if used by school policy
- Required documents missing
### Withdrawn
The student is no longer continuing at the school.
### Graduated
The student completed the final level and does not move to another school level.
### Not Enrolled for Next Year
The student passed and was eligible for promotion, but the parent did not complete enrollment before the deadline.
This should not be treated as failure. It means the student passed but did not continue enrollment.
## 5. Promotion Eligibility Criteria
A student may become eligible for promotion when the school confirms that the student passed the current level.
Eligibility may be based on:
- Final academic result
- Required subject completion
- Final average
- Attendance requirement, if applicable
- Makeup exam result, if applicable
- Level completion status
The school should define these rules clearly per level or program.
## 6. Updated Workflow
### Step 1: Academic Result Finalized
Teachers or academic staff submit the students final result for the current level.
The system records:
- Final average
- Passed or failed result
- Attendance result, if required
- Current level completion status
- Teacher comments, if needed
### Step 2: Promotion Eligibility Check
The system checks whether the student passed the current level.
If the student passed, the system marks the student as:
**Eligible for Promotion**
or
**Awaiting Parent Enrollment**
The student is not yet moved into the next level.
### Step 3: Parent Enrollment Required
The system notifies the parent or guardian that the student is eligible for the next level and must complete enrollment.
The notification should include:
- Student name
- Current level completed
- Promoted level
- Enrollment deadline
- Required documents, if any
- Required payment or deposit, if applicable
- Instructions to complete enrollment
### Step 4: Parent Starts Enrollment
When the parent begins the enrollment process, the student status becomes:
**Enrollment Started**
The enrollment process may include:
- Confirming student information
- Confirming parent or guardian information
- Confirming the promoted level
- Uploading required documents
- Accepting school policies
- Paying registration fee or deposit, if applicable
- Submitting the enrollment form
### Step 5: Parent Completes Enrollment
Once the parent submits all required enrollment information and completes required payment, if applicable, the enrollment is considered complete.
The student status becomes:
**Promoted and Enrolled**
No additional school approval is needed.
### Step 6: New School-Year Record Created
After parent enrollment is completed, the system creates the students new school-year enrollment record.
The new record should include:
- Student ID
- Parent ID
- New school year
- Promoted level
- Enrollment status
- Enrollment completion date
- Source promotion record
## 7. Decision Logic
```text
IF student did not pass current level:
promotion_status = repeated_level
ELSE IF student passed current level AND parent has not started enrollment:
promotion_status = awaiting_parent_enrollment
ELSE IF student passed current level AND parent started enrollment BUT enrollment incomplete:
promotion_status = enrollment_started
ELSE IF student passed current level AND parent completed enrollment:
promotion_status = promoted_and_enrolled
ELSE:
promotion_status = on_hold
```
## 8. Enrollment Completion Rule
Enrollment should be considered complete when all required parent-side actions are finished.
Required actions may include:
- Enrollment form submitted
- Required student information confirmed
- Required parent information confirmed
- Required documents uploaded
- Required agreement accepted
- Required registration fee or deposit paid, if applicable
Once these are complete, the system should automatically finalize the promotion.
## 9. Record Update Rule
The system should not overwrite the students current-level record.
Instead, it should:
1. Keep the current school-year academic record unchanged.
2. Create a promotion eligibility record.
3. Wait for parent enrollment.
4. Create a new enrollment record for the next school year after parent enrollment is complete.
5. Assign the student to the promoted level only in the new school-year record.
## 10. Level Mapping
The school should maintain a clear level progression map.
Example:
| Current Level | Next Level |
|---|---|
| KG1 | KG2 |
| KG2 | Grade 1 |
| Grade 1 | Grade 2 |
| Grade 2 | Grade 3 |
| Grade 3 | Grade 4 |
| Grade 4 | Grade 5 |
| Grade 5 | Grade 6 |
| Grade 6 | Grade 7 |
| Grade 7 | Grade 8 |
| Grade 8 | Grade 9 |
| Grade 9 | Youth |
| Youth | Graduated |
The system should store this mapping in configuration or a dedicated table, not hard-code it inside controller logic.
## 11. Recommended Data Model Updates
### student_promotion_records
Recommended fields:
- promotion_id
- student_id
- parent_id
- current_school_year
- next_school_year
- current_level_id
- promoted_level_id
- promotion_status
- passed_current_level
- enrollment_required
- enrollment_status
- enrollment_id
- parent_notified_at
- enrollment_deadline
- enrollment_completed_at
- promotion_finalized_at
- created_at
- updated_at
### enrollment_applications
Recommended fields:
- enrollment_id
- student_id
- parent_id
- school_year
- requested_level_id
- source_promotion_id
- application_status
- submitted_at
- completed_at
- required_documents_status
- payment_status
- created_at
- updated_at
### promotion_conditions
Recommended condition type:
- parent_enrollment_required
Example:
- condition_type: parent_enrollment_required
- description: Parent must complete enrollment for the promoted level.
- status: pending
- deadline: enrollment deadline date
## 12. Parent Portal Requirements
The parent portal should show the parent a clear action.
Example message:
“Your child has passed the current level and is eligible for promotion to [Promoted Level]. Please complete enrollment for the new school year by [Deadline].”
The parent should be able to:
- View the promoted level
- Start enrollment
- Confirm student information
- Upload required documents
- Pay required enrollment fee, if applicable
- Submit enrollment
- View enrollment status
## 13. Admin Panel Requirements
The admin panel should allow staff to view:
- Students eligible for promotion
- Students awaiting parent enrollment
- Students with enrollment started
- Students promoted and enrolled
- Students not enrolled for next year
- Students repeating the level
- Students on hold
Admin users should also be able to:
- Set enrollment deadlines
- Send reminders
- View enrollment completion status
- Export pending enrollment lists
- View promotion history
## 14. Reminder Process
The system should send reminders to parents who have not completed enrollment.
Suggested reminders:
- First reminder after student becomes eligible
- Second reminder halfway before deadline
- Final reminder before deadline
- Expiration notice after deadline
Each reminder should be logged.
## 15. Deadline Handling
If the parent does not complete enrollment before the deadline, the student should be marked as:
**Not Enrolled for Next Year**
This means:
- The student passed the current level.
- The student was eligible for promotion.
- The parent did not complete enrollment.
- The student should not be counted as enrolled for the next school year.
## 16. Reports
The system should generate reports for:
- Students eligible for promotion
- Students awaiting parent enrollment
- Students with enrollment started
- Students promoted and enrolled
- Students not enrolled for next year
- Students repeating the level
- Students on hold
- Promotion summary by current level
- Enrollment completion summary
- Parent enrollment pending list
## 17. Permissions
Access should be controlled by role.
### Teacher
Can submit academic results and promotion recommendations.
### Academic Coordinator
Can review academic eligibility and promotion readiness.
### Parent or Guardian
Can complete enrollment for the promoted level.
### Registrar or Admin Staff
Can view promotion and enrollment status, send reminders, and export reports.
### Administrator
Can configure levels, promotion rules, deadlines, and system settings.
## 18. Audit Trail
Every promotion and enrollment action should be logged.
Audit log should track:
- User who made the action
- Date and time
- Student affected
- Old value
- New value
- Action type
- Notes, if any
Important audited actions:
- Academic result submitted
- Student marked eligible for promotion
- Parent notified
- Enrollment started
- Enrollment completed
- Student marked promoted and enrolled
- Student marked not enrolled for next year
- Manual status changes
## 19. Implementation Phases
### Phase 1: Promotion Rules and Level Mapping
- Create level progression table.
- Create promotion status rules.
- Define final levels.
- Define repeat-level behavior.
### Phase 2: Eligibility Engine
- Build academic eligibility check.
- Generate promotion eligibility records.
- Mark eligible students as awaiting parent enrollment.
### Phase 3: Parent Enrollment Flow
- Build parent enrollment action.
- Allow parent to confirm information.
- Allow parent to upload documents, if needed.
- Allow parent to pay registration fee or deposit, if applicable.
- Mark enrollment complete automatically when required steps are done.
### Phase 4: Record Update
- Create next-year enrollment record after parent enrollment completion.
- Assign promoted level only in the new school-year record.
- Preserve current-year academic history.
### Phase 5: Notifications and Reports
- Notify parents when students become eligible.
- Send reminders before deadline.
- Generate pending enrollment reports.
- Generate promoted and enrolled reports.
## 20. Final Rule
The system should follow this rule:
**Passed Current Level + Parent Completed Enrollment = Promoted and Enrolled**
Therefore:
**Passed ≠ Enrolled**
**Eligible for Promotion ≠ Promoted and Enrolled**
**Promoted and Enrolled = Student passed current level + Parent completed enrollment**
File diff suppressed because it is too large Load Diff