diff --git a/app.zip b/app.zip new file mode 100644 index 00000000..1e7f5ec5 Binary files /dev/null and b/app.zip differ diff --git a/app/.DS_Store b/app/.DS_Store new file mode 100644 index 00000000..ce11d4c9 Binary files /dev/null and b/app/.DS_Store differ diff --git a/app/Http/.DS_Store b/app/Http/.DS_Store new file mode 100644 index 00000000..e20214d3 Binary files /dev/null and b/app/Http/.DS_Store differ diff --git a/app/Http/Controllers/.DS_Store b/app/Http/Controllers/.DS_Store new file mode 100644 index 00000000..67deef52 Binary files /dev/null and b/app/Http/Controllers/.DS_Store differ diff --git a/app/Http/Controllers/Api.zip b/app/Http/Controllers/Api.zip new file mode 100644 index 00000000..34a143ba Binary files /dev/null and b/app/Http/Controllers/Api.zip differ diff --git a/app/Http/Controllers/Api/Administrator/AdministratorPromotionController.php b/app/Http/Controllers/Api/Administrator/AdministratorPromotionController.php new file mode 100644 index 00000000..fd6f230a --- /dev/null +++ b/app/Http/Controllers/Api/Administrator/AdministratorPromotionController.php @@ -0,0 +1,375 @@ +query->list($request->filters()); + return response()->json([ + 'ok' => true, + 'data' => StudentPromotionResource::collection($result['data']), + 'pagination' => [ + 'page' => $result['page'], + 'per_page' => $result['per_page'], + 'total' => $result['total'], + 'total_pages' => $result['total_pages'], + ], + 'counts_by_status' => $result['counts_by_status'], + ]); + } + + public function show(int $promotionId): JsonResponse + { + $record = $this->findOrFail($promotionId); + if ($record instanceof JsonResponse) { + return $record; + } + return response()->json([ + 'ok' => true, + 'data' => new StudentPromotionResource($this->query->detail($record)), + ]); + } + + public function summary(AdminListPromotionsRequest $request): JsonResponse + { + $counts = $this->query->countsByStatus($request->filters()); + return response()->json([ + 'ok' => true, + 'counts_by_status' => $counts, + 'total' => array_sum($counts), + ]); + } + + public function evaluate(EvaluateEligibilityRequest $request): JsonResponse + { + $payload = $request->validated(); + $userId = $this->getCurrentUserId(); + $scope = $payload['scope'] ?? 'school_year'; + + try { + $result = match ($scope) { + 'student' => [ + 'scope' => 'student', + 'record' => optional( + $this->eligibility->evaluateStudent( + (int) $payload['student_id'], + $payload['current_school_year'] ?? null, + $userId + ) + )?->toArray(), + ], + 'class_section' => array_merge( + ['scope' => 'class_section'], + $this->eligibility->evaluateClassSection( + (int) $payload['class_section_id'], + $payload['current_school_year'] ?? null, + $userId + ) + ), + default => array_merge( + ['scope' => 'school_year'], + $this->eligibility->evaluateSchoolYear( + $payload['current_school_year'] ?? null, + $userId + ) + ), + }; + } catch (\Throwable $e) { + Log::error('Promotion eligibility evaluation failed', ['exception' => $e]); + return response()->json([ + 'ok' => false, + 'message' => 'Failed to evaluate promotion eligibility.', + ], Response::HTTP_INTERNAL_SERVER_ERROR); + } + + return response()->json([ + 'ok' => true, + 'result' => $result, + ]); + } + + public function updateStatus(UpdatePromotionStatusRequest $request, int $promotionId): JsonResponse + { + $record = $this->findOrFail($promotionId); + if ($record instanceof JsonResponse) { + return $record; + } + + $payload = $request->validated(); + $userId = $this->getCurrentUserId(); + + try { + $updated = !empty($payload['force']) + ? $this->statusService->forceStatus($record, $payload['status'], $userId, $payload['notes'] ?? null) + : $this->statusService->transition($record, $payload['status'], $userId, $payload['notes'] ?? null); + } catch (\InvalidArgumentException $e) { + return response()->json(['ok' => false, 'message' => $e->getMessage()], Response::HTTP_UNPROCESSABLE_ENTITY); + } catch (\RuntimeException $e) { + return response()->json(['ok' => false, 'message' => $e->getMessage()], Response::HTTP_CONFLICT); + } + + return response()->json([ + 'ok' => true, + 'data' => new StudentPromotionResource($this->query->detail($updated->refresh())), + ]); + } + + public function setDeadline(SetEnrollmentDeadlineRequest $request, ?int $promotionId = null): JsonResponse + { + $payload = $request->validated(); + $userId = $this->getCurrentUserId(); + $newDeadline = $payload['enrollment_deadline']; + $applyTo = $payload['apply_to'] ?? ($promotionId ? 'single' : 'school_year'); + + $records = collect(); + if ($promotionId !== null || $applyTo === 'single') { + $id = $promotionId ?? null; + if ($id === null) { + return response()->json(['ok' => false, 'message' => 'promotion_id is required for single deadline updates.'], Response::HTTP_UNPROCESSABLE_ENTITY); + } + $record = $this->findOrFail($id); + if ($record instanceof JsonResponse) { + return $record; + } + $records = collect([$record]); + } elseif ($applyTo === 'filter' && !empty($payload['promotion_ids'])) { + $records = StudentPromotionRecord::query() + ->whereIn('promotion_id', $payload['promotion_ids']) + ->get(); + } else { + $year = $payload['next_school_year'] ?? null; + if ($year === null || $year === '') { + return response()->json([ + 'ok' => false, + 'message' => 'next_school_year is required when applying a bulk deadline.', + ], Response::HTTP_UNPROCESSABLE_ENTITY); + } + $records = StudentPromotionRecord::query() + ->forNextYear($year) + ->whereIn('promotion_status', StudentPromotionRecord::openStatuses()) + ->get(); + } + + $updatedIds = []; + foreach ($records as $record) { + $oldDeadline = $record->enrollment_deadline?->toDateString(); + DB::transaction(function () use ($record, $newDeadline, $oldDeadline, $userId, &$updatedIds) { + $record->enrollment_deadline = $newDeadline; + $record->updated_by = $userId; + $record->save(); + $this->audit->logDeadlineSet($record, $oldDeadline, $newDeadline, $userId); + $updatedIds[] = (int) $record->getKey(); + }); + } + + return response()->json([ + 'ok' => true, + 'updated' => count($updatedIds), + 'promotion_ids' => $updatedIds, + ]); + } + + public function sendReminder(SendReminderRequest $request, int $promotionId): JsonResponse + { + $record = $this->findOrFail($promotionId); + if ($record instanceof JsonResponse) { + return $record; + } + + $payload = $request->validated(); + $userId = $this->getCurrentUserId(); + + try { + $log = $this->reminders->send( + $record, + $payload['reminder_type'] ?? PromotionReminderLog::TYPE_MANUAL, + $userId, + $payload['subject'] ?? null, + $payload['message'] ?? null, + $payload['channels'] ?? ['in_app', 'email'] + ); + } catch (\Throwable $e) { + Log::error('Promotion reminder send failed', [ + 'promotion_id' => $promotionId, + 'exception' => $e, + ]); + return response()->json([ + 'ok' => false, + 'message' => 'Failed to send reminder.', + ], Response::HTTP_INTERNAL_SERVER_ERROR); + } + + return response()->json([ + 'ok' => true, + 'reminder' => new PromotionReminderResource($log), + ]); + } + + public function dispatchScheduledReminders(): JsonResponse + { + $userId = $this->getCurrentUserId(); + $result = $this->reminders->dispatchScheduledReminders(null, $userId); + return response()->json([ + 'ok' => true, + 'result' => $result, + ]); + } + + public function reminders(int $promotionId): JsonResponse + { + $record = $this->findOrFail($promotionId); + if ($record instanceof JsonResponse) { + return $record; + } + $rows = PromotionReminderLog::query() + ->forPromotion((int) $record->getKey()) + ->orderByDesc('id') + ->get(); + return response()->json([ + 'ok' => true, + 'reminders' => PromotionReminderResource::collection($rows), + ]); + } + + public function audit(int $promotionId): JsonResponse + { + $record = $this->findOrFail($promotionId); + if ($record instanceof JsonResponse) { + return $record; + } + $rows = PromotionAuditLog::query() + ->forPromotion((int) $record->getKey()) + ->orderByDesc('id') + ->get(); + return response()->json([ + 'ok' => true, + 'entries' => PromotionAuditLogResource::collection($rows), + ]); + } + + public function expireDeadlines(): JsonResponse + { + $result = $this->enrollmentService->markExpiredDeadlines(null, $this->getCurrentUserId()); + return response()->json([ + 'ok' => true, + 'result' => $result, + ]); + } + + public function adminCompleteEnrollment(ParentEnrollmentStepRequest $request, int $promotionId): JsonResponse + { + $record = $this->findOrFail($promotionId); + if ($record instanceof JsonResponse) { + return $record; + } + $userId = $this->getCurrentUserId(); + $parentId = (int) $record->parent_id ?: $userId ?: 0; + + if ($parentId <= 0) { + return response()->json([ + 'ok' => false, + 'message' => 'Promotion record has no associated parent.', + ], Response::HTTP_UNPROCESSABLE_ENTITY); + } + + try { + $updated = $this->enrollmentService->updateSteps( + $record, + $parentId, + $request->steps(), + $userId + ); + } catch (\RuntimeException $e) { + return response()->json(['ok' => false, 'message' => $e->getMessage()], Response::HTTP_CONFLICT); + } + + return response()->json([ + 'ok' => true, + 'data' => new StudentPromotionResource($this->query->detail($updated->refresh())), + ]); + } + + public function levelProgressions(): JsonResponse + { + $rows = $this->progression->list(false); + return response()->json([ + 'ok' => true, + 'levels' => LevelProgressionResource::collection($rows), + ]); + } + + public function upsertLevelProgression(UpsertLevelProgressionRequest $request): JsonResponse + { + $row = $this->progression->upsertMapping($request->validated()); + return response()->json([ + 'ok' => true, + 'level' => new LevelProgressionResource($row), + ]); + } + + public function seedLevelProgressions(): JsonResponse + { + $created = $this->progression->seedDefaults(); + return response()->json([ + 'ok' => true, + 'created' => $created, + ]); + } + + private function findOrFail(int $promotionId): StudentPromotionRecord|JsonResponse + { + $record = StudentPromotionRecord::query()->find($promotionId); + if (!$record) { + return response()->json([ + 'ok' => false, + 'message' => 'Promotion record not found.', + ], Response::HTTP_NOT_FOUND); + } + return $record; + } +} diff --git a/app/Http/Controllers/Api/Auth/AuthController.php b/app/Http/Controllers/Api/Auth/AuthController.php index 44bf7935..6766a6d2 100644 --- a/app/Http/Controllers/Api/Auth/AuthController.php +++ b/app/Http/Controllers/Api/Auth/AuthController.php @@ -20,8 +20,7 @@ class AuthController extends BaseApiController $payload = is_array($decoded) ? $decoded : []; } - // Match CodeIgniter apiLogin: raw email/password (no trim); empty check is identical to CI `!$email || !$password`. - $email = (string) ($payload['email'] ?? ''); + $email = strtolower(trim((string) ($payload['email'] ?? ''))); $password = (string) ($payload['password'] ?? ''); if ($email === '' || $password === '') { @@ -39,7 +38,7 @@ class AuthController extends BaseApiController ], 429); } - $user = User::query()->where('email', $email)->first(); + $user = User::query()->whereRaw('LOWER(email) = ?', [$email])->first(); if (!$user) { $security->logIpAttempt((string) $ip); @@ -49,13 +48,6 @@ class AuthController extends BaseApiController ], 401); } - if ($user->is_suspended) { - return response()->json([ - 'status' => false, - 'message' => 'Account suspended. Please reset your password.', - ], 403); - } - if (! ci_password_verify($password, (string) $user->password)) { $security->handleFailedLogin($user, $email, (string) $ip); @@ -65,6 +57,15 @@ class AuthController extends BaseApiController ], 401); } + // Suspension is checked AFTER credentials are verified so that the + // response shape cannot be used to enumerate which emails exist. + if ($user->is_suspended) { + return response()->json([ + 'status' => false, + 'message' => 'Account suspended. Please reset your password.', + ], 403); + } + $security->resetFailedAttempts($user); $security->logSuccessfulLogin($user->fresh(), $request); diff --git a/app/Http/Controllers/Api/Core/BaseApiController.php b/app/Http/Controllers/Api/Core/BaseApiController.php index 9f48d3de..ebd54c7e 100644 --- a/app/Http/Controllers/Api/Core/BaseApiController.php +++ b/app/Http/Controllers/Api/Core/BaseApiController.php @@ -245,28 +245,31 @@ class BaseApiController extends Controller return null; } - $user = app(User::class); - $row = $user->find($userId); + /** @var User|null $row */ + $row = User::query()->find($userId); if (!$row) { return null; } - $name = trim(($row['firstname'] ?? '') . ' ' . ($row['lastname'] ?? '')); + $firstname = (string) ($row->firstname ?? ''); + $lastname = (string) ($row->lastname ?? ''); + $email = $row->email; + + $name = trim($firstname . ' ' . $lastname); if ($name === '') { - $name = $row['email'] ?? 'User #' . $userId; + $name = $email ?? 'User #' . $userId; } try { - $roles = DB::table('user_roles ur') - ->select('LOWER(r.name) AS name') - ->join('roles r', 'r.id', '=', 'ur.role_id') + $roles = DB::table('user_roles as ur') + ->join('roles as r', 'r.id', '=', 'ur.role_id') ->where('ur.user_id', $userId) ->whereNull('ur.deleted_at') - ->pluck('name') - ->map(fn($name) => strtolower((string) $name)) + ->pluck('r.name') + ->map(fn ($name) => strtolower((string) $name)) ->unique() ->values() - ->toArray(); + ->all(); } catch (\Throwable $e) { Log::error('Unable to load user roles: ' . $e->getMessage()); $roles = []; @@ -275,7 +278,7 @@ class BaseApiController extends Controller return (object) [ 'id' => (int) $userId, 'name' => $name, - 'email' => $row['email'] ?? null, + 'email' => $email, 'roles' => $roles, ]; } diff --git a/app/Http/Controllers/Api/Finance/ChargeController.php b/app/Http/Controllers/Api/Finance/ChargeController.php new file mode 100644 index 00000000..ccb535ca --- /dev/null +++ b/app/Http/Controllers/Api/Finance/ChargeController.php @@ -0,0 +1,158 @@ +validated(); + $result = $this->charges->listForParent( + (int) $payload['parent_id'], + $payload['school_year'] ?? null + ); + + return response()->json([ + 'ok' => true, + 'charges' => new ChargeListResource($result), + ]); + } + + public function storeExtra(StoreExtraChargeRequest $request): JsonResponse + { + $guard = $this->authenticatedUserIdOrUnauthorized(); + if ($guard instanceof JsonResponse) { + return $guard; + } + + $payload = $request->validated(); + $payload['created_by'] = $guard; + + try { + $result = $this->charges->createExtraCharge($payload); + } catch (\Throwable $e) { + Log::error('Store extra charge failed', ['exception' => $e]); + + return response()->json(['ok' => false, 'message' => 'Unable to create extra charge.'], 500); + } + + $status = ($result['ok'] ?? false) + ? (isset($result['error']) && $result['error'] === 'duplicate_charge' ? 409 : 201) + : 422; + + return response()->json([ + 'ok' => (bool) ($result['ok'] ?? false), + 'charge' => new ChargeActionResource($result), + ], $status); + } + + public function storeEvent(StoreEventChargeRequest $request): JsonResponse + { + $guard = $this->authenticatedUserIdOrUnauthorized(); + if ($guard instanceof JsonResponse) { + return $guard; + } + + $payload = $request->validated(); + $payload['created_by'] = $guard; + + try { + $result = $this->charges->createEventCharge($payload); + } catch (\Throwable $e) { + Log::error('Store event charge failed', ['exception' => $e]); + + return response()->json(['ok' => false, 'message' => 'Unable to create event charge.'], 500); + } + + $status = ($result['ok'] ?? false) + ? (isset($result['error']) && $result['error'] === 'duplicate_charge' ? 409 : 201) + : 422; + + return response()->json([ + 'ok' => (bool) ($result['ok'] ?? false), + 'charge' => new ChargeActionResource($result), + ], $status); + } + + public function cancelExtra(int $id): JsonResponse + { + $result = $this->charges->cancelExtraCharge($id); + $status = ($result['ok'] ?? false) ? 200 : 422; + + return response()->json([ + 'ok' => (bool) ($result['ok'] ?? false), + 'charge' => new ChargeActionResource($result), + ], $status); + } + + public function applyExtra(ApplyExtraChargeRequest $request, int $id): JsonResponse + { + $payload = $request->validated(); + $result = $this->charges->applyExtraCharge($id, (int) $payload['invoice_id']); + $status = ($result['ok'] ?? false) ? 200 : 422; + + return response()->json([ + 'ok' => (bool) ($result['ok'] ?? false), + 'charge' => new ChargeActionResource($result), + ], $status); + } + + public function cancelEvent(Request $request, int $id): JsonResponse + { + $issueCredit = $request->boolean('issue_credit', true); + $result = $this->charges->cancelEventCharge($id, $issueCredit); + $status = ($result['ok'] ?? false) ? 200 : 422; + + return response()->json([ + 'ok' => (bool) ($result['ok'] ?? false), + 'charge' => new ChargeActionResource($result), + ], $status); + } + + public function markEventPaid(MarkEventChargePaidRequest $request, int $id): JsonResponse + { + $payload = $request->validated(); + $result = $this->charges->markEventChargePaid( + $id, + (bool) $payload['paid'], + isset($payload['payment_id']) ? (int) $payload['payment_id'] : null + ); + $status = ($result['ok'] ?? false) ? 200 : 422; + + return response()->json([ + 'ok' => (bool) ($result['ok'] ?? false), + 'charge' => new ChargeActionResource($result), + ], $status); + } + + /** + * @return int|JsonResponse + */ + private function authenticatedUserIdOrUnauthorized(): int|JsonResponse + { + $userId = (int) (auth()->id() ?? 0); + if ($userId <= 0) { + return response()->json(['ok' => false, 'message' => 'Unauthorized.'], 401); + } + + return $userId; + } +} diff --git a/app/Http/Controllers/Api/Finance/FeeCalculationController.php b/app/Http/Controllers/Api/Finance/FeeCalculationController.php index aa2d24cc..a53e9bae 100644 --- a/app/Http/Controllers/Api/Finance/FeeCalculationController.php +++ b/app/Http/Controllers/Api/Finance/FeeCalculationController.php @@ -3,12 +3,18 @@ namespace App\Http\Controllers\Api\Finance; use App\Http\Controllers\Api\Core\BaseApiController; +use App\Http\Requests\Finance\FeeFamilyBalanceRequest; use App\Http\Requests\Finance\FeeRefundRequest; +use App\Http\Requests\Finance\FeeTuitionBreakdownRequest; use App\Http\Requests\Finance\FeeTuitionTotalRequest; +use App\Http\Resources\Fees\FeeFamilyBalanceResource; use App\Http\Resources\Fees\FeeRefundResource; +use App\Http\Resources\Fees\FeeTuitionBreakdownResource; use App\Http\Resources\Fees\FeeTuitionTotalResource; +use App\Services\Billing\BalanceCalculationService; use App\Services\Fees\FeeRefundCalculatorService; use App\Services\Fees\FeeStudentFeeService; +use App\Services\Fees\TuitionCalculationService; use Illuminate\Http\JsonResponse; use Illuminate\Support\Facades\Log; @@ -16,7 +22,9 @@ class FeeCalculationController extends BaseApiController { public function __construct( private FeeRefundCalculatorService $refunds, - private FeeStudentFeeService $tuition + private FeeStudentFeeService $tuition, + private TuitionCalculationService $tuitionCalculator, + private BalanceCalculationService $balance ) { parent::__construct(); } @@ -69,4 +77,55 @@ class FeeCalculationController extends BaseApiController ]), ]); } + + public function tuitionBreakdown(FeeTuitionBreakdownRequest $request): JsonResponse + { + $payload = $request->validated(); + + try { + $result = $this->tuitionCalculator->calculate($payload['students']); + } catch (\Throwable $e) { + Log::error('Tuition breakdown calculation failed', [ + 'exception' => $e, + ]); + + return response()->json([ + 'ok' => false, + 'message' => 'Unable to calculate tuition breakdown.', + ], 500); + } + + return response()->json([ + 'ok' => true, + 'tuition' => new FeeTuitionBreakdownResource($result), + ]); + } + + public function familyBalance(FeeFamilyBalanceRequest $request): JsonResponse + { + $payload = $request->validated(); + + try { + $result = $this->balance->calculateFamilyAccount( + $payload['students'], + (int) $payload['parent_id'], + $payload['school_year'] ?? null + ); + } catch (\Throwable $e) { + Log::error('Family balance calculation failed', [ + 'parent_id' => $payload['parent_id'] ?? null, + 'exception' => $e, + ]); + + return response()->json([ + 'ok' => false, + 'message' => 'Unable to calculate family balance.', + ], 500); + } + + return response()->json([ + 'ok' => true, + 'account' => new FeeFamilyBalanceResource($result), + ]); + } } diff --git a/app/Http/Controllers/Api/Parents/AuthorizedUserInviteController.php b/app/Http/Controllers/Api/Parents/AuthorizedUserInviteController.php index b8b550aa..ec0d7e3a 100644 --- a/app/Http/Controllers/Api/Parents/AuthorizedUserInviteController.php +++ b/app/Http/Controllers/Api/Parents/AuthorizedUserInviteController.php @@ -37,14 +37,19 @@ class AuthorizedUserInviteController extends Controller ); } + $redirectUrl = (string) $result['redirect_url']; + if (! $this->isTrustedRedirectUrl($redirectUrl)) { + return response()->json(['message' => 'Invalid redirect URL.'], 400); + } + if ($request->expectsJson()) { return response()->json([ 'message' => 'Confirmed.', - 'redirect_url' => $result['redirect_url'], + 'redirect_url' => $redirectUrl, ]); } - return redirect()->away($result['redirect_url']); + return redirect()->away($redirectUrl); } public function setPasswordForm(Request $request, int $authorizedUserId): JsonResponse|Response @@ -79,7 +84,8 @@ class AuthorizedUserInviteController extends Controller { $validator = Validator::make($request->all(), [ 'password' => ['required', 'string', 'min:8', 'regex:/[A-Z]/', 'regex:/[a-z]/', 'regex:/[0-9]/', 'regex:/[\W_]/'], - 'password_confirm' => ['required', 'same:password'], + 'password_confirmation' => ['required_without:password_confirm', 'same:password'], + 'password_confirm' => ['required_without:password_confirmation', 'same:password'], 'user_id' => ['required', 'integer'], 'token' => ['required', 'string'], ], [ @@ -108,4 +114,23 @@ class AuthorizedUserInviteController extends Controller return response()->json(['message' => 'Password has been successfully set.']); } + + private function isTrustedRedirectUrl(string $url): bool + { + $host = parse_url($url, PHP_URL_HOST); + if (! is_string($host) || $host === '') { + return false; + } + + $trusted = [ + parse_url((string) config('app.url'), PHP_URL_HOST), + parse_url((string) config('app.frontend_url'), PHP_URL_HOST), + parse_url((string) config('services.frontend.url'), PHP_URL_HOST), + ]; + + return in_array(strtolower($host), array_filter(array_map( + static fn ($value) => is_string($value) ? strtolower($value) : null, + $trusted + )), true); + } } diff --git a/app/Http/Controllers/Api/Parents/AuthorizedUsersController.php b/app/Http/Controllers/Api/Parents/AuthorizedUsersController.php index b0fca933..ffd6cb0e 100644 --- a/app/Http/Controllers/Api/Parents/AuthorizedUsersController.php +++ b/app/Http/Controllers/Api/Parents/AuthorizedUsersController.php @@ -58,14 +58,17 @@ class AuthorizedUsersController extends BaseApiController return $guard; } - $email = strtolower($request->validated('email')); + $email = strtolower(trim((string) $request->validated('email'))); $result = $this->svc->inviteByEmail($guard, $email); - $message = 'Authorized user added. A confirmation email has been sent.'; if (! $result['created']) { - return $this->success(['message' => $message], $message, Response::HTTP_CREATED); + $message = 'If the email belongs to a registered user, a confirmation email has been sent.'; + + return $this->success(['message' => $message], $message, Response::HTTP_OK); } + $message = 'Authorized user added. A confirmation email has been sent.'; + return $this->success( ['message' => $message, 'id' => $result['record']->id], $message, @@ -89,11 +92,18 @@ class AuthorizedUsersController extends BaseApiController $row = $resolution; $email = $request->validated('email') ?? null; - if (is_string($email) && $email !== '') { - $row->email = strtolower($email); - } + if (is_string($email) && trim($email) !== '') { + try { + $this->svc->changeEmailAndReinvite($row, $email); + } catch (\InvalidArgumentException $e) { + return $this->error($e->getMessage(), Response::HTTP_UNPROCESSABLE_ENTITY); + } - $row->save(); + return $this->success( + ['message' => 'Authorized user email updated. A new confirmation email has been sent.'], + 'Authorized user email updated. A new confirmation email has been sent.' + ); + } return $this->success(['message' => 'Authorized user information updated.'], 'Authorized user information updated.'); } @@ -127,15 +137,15 @@ class AuthorizedUsersController extends BaseApiController private function authorizedUserForParent(int $id, int $parentId): AuthorizedUser|JsonResponse { - $row = AuthorizedUser::query()->find($id); + $row = AuthorizedUser::query() + ->where('id', $id) + ->where('user_id', $parentId) + ->first(); + if (! $row) { return $this->error('Authorized user not found.', Response::HTTP_NOT_FOUND); } - if ((int) $row->user_id !== $parentId) { - return $this->error('You do not have access to this resource.', Response::HTTP_FORBIDDEN); - } - return $row; } } diff --git a/app/Http/Controllers/Api/Parents/ParentController.php b/app/Http/Controllers/Api/Parents/ParentController.php index e651b045..1dd0f3b8 100644 --- a/app/Http/Controllers/Api/Parents/ParentController.php +++ b/app/Http/Controllers/Api/Parents/ParentController.php @@ -148,10 +148,14 @@ class ParentController extends BaseApiController $payload['emergency_contacts'] ?? [] ); + $createdCount = count($result['created_student_ids']); + $skipped = $result['skipped'] ?? []; + return response()->json([ 'ok' => true, 'created_student_ids' => $result['created_student_ids'], - ], 201); + 'skipped' => $skipped, + ], $createdCount > 0 ? 201 : 200); } public function updateStudent(ParentStudentUpdateRequest $request, int $studentId): JsonResponse @@ -283,10 +287,21 @@ class ParentController extends BaseApiController } /** - * @return int|JsonResponse Authenticated parent user id, or 401 JSON for this controller's legacy `{ ok }` shape. + * @return int|JsonResponse Effective primary-parent user id, or 401 JSON for this controller's legacy `{ ok }` shape. + * + * `parent.access` middleware resolves second parents (user_type=secondary) + * and authorized users (user_type=tertiary) to their primary parent and + * stashes the result as `primary_parent_id` on the request. If we are + * called outside that middleware we fall back to the authenticated id. */ private function parentIdOrUnauthorized(): int|JsonResponse { + $request = request(); + $primary = (int) ($request?->attributes?->get('primary_parent_id') ?? 0); + if ($primary > 0) { + return $primary; + } + $parentId = (int) (auth()->id() ?? 0); if ($parentId <= 0) { return response()->json(['ok' => false, 'message' => 'Unauthorized.'], 401); diff --git a/app/Http/Controllers/Api/Parents/ParentPromotionController.php b/app/Http/Controllers/Api/Parents/ParentPromotionController.php new file mode 100644 index 00000000..6b861b4a --- /dev/null +++ b/app/Http/Controllers/Api/Parents/ParentPromotionController.php @@ -0,0 +1,173 @@ +parentIdOrUnauthorized(); + if ($guard instanceof JsonResponse) { + return $guard; + } + + $data = $this->service->overviewForParent( + $guard, + $request->validated()['next_school_year'] ?? null + ); + + return response()->json([ + 'ok' => true, + 'records' => $data['records'], + 'next_school_year' => $data['next_school_year'], + 'message' => $data['message'], + ]); + } + + public function show(int $promotionId): JsonResponse + { + $guard = $this->parentIdOrUnauthorized(); + if ($guard instanceof JsonResponse) { + return $guard; + } + + $record = $this->loadOwnedRecord($promotionId, $guard); + if ($record instanceof JsonResponse) { + return $record; + } + + return response()->json([ + 'ok' => true, + 'data' => new StudentPromotionResource($this->query->detail($record)), + ]); + } + + public function start(int $promotionId): JsonResponse + { + $guard = $this->parentIdOrUnauthorized(); + if ($guard instanceof JsonResponse) { + return $guard; + } + + $record = $this->loadOwnedRecord($promotionId, $guard); + if ($record instanceof JsonResponse) { + return $record; + } + + try { + $updated = $this->service->startEnrollment($record, $guard, $guard); + } catch (\RuntimeException $e) { + return response()->json(['ok' => false, 'message' => $e->getMessage()], Response::HTTP_CONFLICT); + } + + return response()->json([ + 'ok' => true, + 'data' => new StudentPromotionResource($this->query->detail($updated->refresh())), + ]); + } + + public function updateSteps(ParentEnrollmentStepRequest $request, int $promotionId): JsonResponse + { + $guard = $this->parentIdOrUnauthorized(); + if ($guard instanceof JsonResponse) { + return $guard; + } + + $record = $this->loadOwnedRecord($promotionId, $guard); + if ($record instanceof JsonResponse) { + return $record; + } + + try { + $updated = $this->service->updateSteps($record, $guard, $request->steps(), $guard); + } catch (\RuntimeException $e) { + return response()->json(['ok' => false, 'message' => $e->getMessage()], Response::HTTP_CONFLICT); + } + + return response()->json([ + 'ok' => true, + 'data' => new StudentPromotionResource($this->query->detail($updated->refresh())), + ]); + } + + public function submit(int $promotionId): JsonResponse + { + $guard = $this->parentIdOrUnauthorized(); + if ($guard instanceof JsonResponse) { + return $guard; + } + + $record = $this->loadOwnedRecord($promotionId, $guard); + if ($record instanceof JsonResponse) { + return $record; + } + + try { + $updated = $this->service->submitEnrollment($record, $guard, $guard); + } catch (\RuntimeException $e) { + return response()->json(['ok' => false, 'message' => $e->getMessage()], Response::HTTP_CONFLICT); + } + + return response()->json([ + 'ok' => true, + 'data' => new StudentPromotionResource($this->query->detail($updated->refresh())), + ]); + } + + private function loadOwnedRecord(int $promotionId, int $parentId): StudentPromotionRecord|JsonResponse + { + $record = StudentPromotionRecord::query()->find($promotionId); + if (!$record) { + return response()->json(['ok' => false, 'message' => 'Promotion record not found.'], Response::HTTP_NOT_FOUND); + } + + $owns = (int) $record->parent_id === $parentId; + if (!$owns) { + $studentParentId = (int) (Student::query() + ->where('id', $record->student_id) + ->value('parent_id') ?? 0); + $owns = $studentParentId === $parentId; + } + if (!$owns) { + return response()->json(['ok' => false, 'message' => 'You are not authorised to view this promotion record.'], Response::HTTP_FORBIDDEN); + } + return $record; + } + + private function parentIdOrUnauthorized(): int|JsonResponse + { + $request = request(); + $primary = (int) ($request?->attributes?->get('primary_parent_id') ?? 0); + if ($primary > 0) { + return $primary; + } + $parentId = (int) (auth()->id() ?? 0); + if ($parentId <= 0) { + return response()->json(['ok' => false, 'message' => 'Unauthorized.'], Response::HTTP_UNAUTHORIZED); + } + return $parentId; + } +} diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php deleted file mode 100644 index 00f76766..00000000 --- a/app/Http/Kernel.php +++ /dev/null @@ -1,68 +0,0 @@ - [ - \App\Http\Middleware\EncryptCookies::class, - \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, - \Illuminate\Routing\Middleware\SubstituteBindings::class, - ], - - 'api' => [ - // Throttling (optional) - \Illuminate\Routing\Middleware\ThrottleRequests::class.':api', - - // JWT Authentication - \PHPOpenSourceSaver\JWTAuth\Http\Middleware\Authenticate::class, - - \Illuminate\Routing\Middleware\SubstituteBindings::class, - ], - ]; - - /** - * The application's route middleware. - * - * These may be assigned to routes individually. - */ - protected $routeMiddleware = [ - 'auth' => \App\Http\Middleware\Authenticate::class, - - // Laravel default middleware - 'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class, - 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, - - // JWT middleware - 'jwt.auth' => \PHPOpenSourceSaver\JWTAuth\Http\Middleware\Authenticate::class, - 'jwt.refresh' => \PHPOpenSourceSaver\JWTAuth\Http\Middleware\RefreshToken::class, - ]; -} diff --git a/app/Http/Middleware/EnsureParentProgressAccess.php b/app/Http/Middleware/EnsureParentProgressAccess.php index 205a8950..6525acb1 100644 --- a/app/Http/Middleware/EnsureParentProgressAccess.php +++ b/app/Http/Middleware/EnsureParentProgressAccess.php @@ -3,12 +3,21 @@ namespace App\Http\Middleware; use App\Models\User; +use App\Services\Parents\PrimaryParentUserResolver; use Closure; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Symfony\Component\HttpFoundation\Response; -/** CodeIgniter route filter `auth:parent`. */ +/** + * CodeIgniter route filter `auth:parent`. + * + * Allows access for any caller that ultimately maps to a primary parent + * record: the primary parent themselves (parent role / user_type=primary), + * a registered second parent (user_type=secondary) or an authorized user + * (user_type=tertiary). The resolved primary parent id is stashed on the + * request as `primary_parent_id` for controllers to consume. + */ class EnsureParentProgressAccess { public function handle(Request $request, Closure $next): Response @@ -25,9 +34,23 @@ class EnsureParentProgressAccess ->toArray(); if (in_array('parent', $roles, true)) { + $request->attributes->set('primary_parent_id', (int) $user->id); + return $next($request); } + $userType = strtolower(trim((string) ($user->user_type ?? ''))); + if (in_array($userType, ['secondary', 'tertiary'], true)) { + /** @var PrimaryParentUserResolver $resolver */ + $resolver = app(PrimaryParentUserResolver::class); + $primaryId = $resolver->resolveFromCredentials((int) $user->id, $userType); + if ($primaryId !== null && $primaryId > 0) { + $request->attributes->set('primary_parent_id', $primaryId); + + return $next($request); + } + } + return response()->json(['message' => 'Forbidden.'], 403); } } diff --git a/app/Http/Requests/Finance/ApplyExtraChargeRequest.php b/app/Http/Requests/Finance/ApplyExtraChargeRequest.php new file mode 100644 index 00000000..25bb1867 --- /dev/null +++ b/app/Http/Requests/Finance/ApplyExtraChargeRequest.php @@ -0,0 +1,20 @@ +user() !== null; + } + + public function rules(): array + { + return [ + 'invoice_id' => ['required', 'integer', 'min:1'], + ]; + } +} diff --git a/app/Http/Requests/Finance/ChargeListRequest.php b/app/Http/Requests/Finance/ChargeListRequest.php new file mode 100644 index 00000000..e69b3839 --- /dev/null +++ b/app/Http/Requests/Finance/ChargeListRequest.php @@ -0,0 +1,21 @@ +user() !== null; + } + + public function rules(): array + { + return [ + 'parent_id' => ['required', 'integer', 'min:1'], + 'school_year' => ['nullable', 'string', 'max:20'], + ]; + } +} diff --git a/app/Http/Requests/Finance/FeeFamilyBalanceRequest.php b/app/Http/Requests/Finance/FeeFamilyBalanceRequest.php new file mode 100644 index 00000000..2282ba61 --- /dev/null +++ b/app/Http/Requests/Finance/FeeFamilyBalanceRequest.php @@ -0,0 +1,30 @@ +user() !== null; + } + + public function rules(): array + { + return [ + 'parent_id' => ['required', 'integer', 'min:1'], + 'school_year' => ['nullable', 'string', 'max:20'], + 'students' => ['required', 'array', 'min:1'], + 'students.*.student_id' => ['nullable', 'integer', 'min:1'], + 'students.*.firstname' => ['nullable', 'string', 'max:100'], + 'students.*.lastname' => ['nullable', 'string', 'max:100'], + 'students.*.class_section_id' => ['nullable', 'integer', 'min:1'], + 'students.*.grade' => ['nullable', 'string', 'max:20'], + 'students.*.enrollment_status' => ['nullable', 'string', 'max:50'], + 'students.*.admission_status' => ['nullable', 'string', 'max:50'], + 'students.*.discount_amount' => ['nullable', 'numeric', 'min:0'], + ]; + } +} diff --git a/app/Http/Requests/Finance/FeeTuitionBreakdownRequest.php b/app/Http/Requests/Finance/FeeTuitionBreakdownRequest.php new file mode 100644 index 00000000..1a74d389 --- /dev/null +++ b/app/Http/Requests/Finance/FeeTuitionBreakdownRequest.php @@ -0,0 +1,28 @@ +user() !== null; + } + + public function rules(): array + { + return [ + 'students' => ['required', 'array', 'min:1'], + 'students.*.student_id' => ['nullable', 'integer', 'min:1'], + 'students.*.firstname' => ['nullable', 'string', 'max:100'], + 'students.*.lastname' => ['nullable', 'string', 'max:100'], + 'students.*.class_section_id' => ['nullable', 'integer', 'min:1'], + 'students.*.grade' => ['nullable', 'string', 'max:20'], + 'students.*.enrollment_status' => ['nullable', 'string', 'max:50'], + 'students.*.admission_status' => ['nullable', 'string', 'max:50'], + 'students.*.discount_amount' => ['nullable', 'numeric', 'min:0'], + ]; + } +} diff --git a/app/Http/Requests/Finance/MarkEventChargePaidRequest.php b/app/Http/Requests/Finance/MarkEventChargePaidRequest.php new file mode 100644 index 00000000..fa48e0cd --- /dev/null +++ b/app/Http/Requests/Finance/MarkEventChargePaidRequest.php @@ -0,0 +1,21 @@ +user() !== null; + } + + public function rules(): array + { + return [ + 'paid' => ['required', 'boolean'], + 'payment_id' => ['nullable', 'integer', 'min:1'], + ]; + } +} diff --git a/app/Http/Requests/Finance/StoreEventChargeRequest.php b/app/Http/Requests/Finance/StoreEventChargeRequest.php new file mode 100644 index 00000000..aab2d4fd --- /dev/null +++ b/app/Http/Requests/Finance/StoreEventChargeRequest.php @@ -0,0 +1,27 @@ +user() !== null; + } + + public function rules(): array + { + return [ + 'parent_id' => ['required', 'integer', 'min:1'], + 'student_id' => ['required', 'integer', 'min:1'], + 'event_id' => ['nullable', 'integer', 'min:1'], + 'event_name' => ['required_without:event_id', 'nullable', 'string', 'max:255'], + 'amount' => ['nullable', 'numeric', 'min:0'], + 'description' => ['nullable', 'string', 'max:2000'], + 'school_year' => ['nullable', 'string', 'max:20'], + 'semester' => ['nullable', 'string', 'max:20'], + ]; + } +} diff --git a/app/Http/Requests/Finance/StoreExtraChargeRequest.php b/app/Http/Requests/Finance/StoreExtraChargeRequest.php new file mode 100644 index 00000000..7d7a5c95 --- /dev/null +++ b/app/Http/Requests/Finance/StoreExtraChargeRequest.php @@ -0,0 +1,28 @@ +user() !== null; + } + + public function rules(): array + { + return [ + 'parent_id' => ['required', 'integer', 'min:1'], + 'title' => ['required', 'string', 'min:2', 'max:255'], + 'amount' => ['required', 'numeric', 'min:0.01'], + 'charge_type' => ['required', 'in:add,deduct'], + 'description' => ['nullable', 'string', 'max:2000'], + 'due_date' => ['nullable', 'date_format:Y-m-d'], + 'invoice_id' => ['nullable', 'integer', 'min:1'], + 'school_year' => ['nullable', 'string', 'max:20'], + 'semester' => ['nullable', 'string', 'max:20'], + ]; + } +} diff --git a/app/Http/Requests/Promotions/AdminListPromotionsRequest.php b/app/Http/Requests/Promotions/AdminListPromotionsRequest.php new file mode 100644 index 00000000..f6b54c47 --- /dev/null +++ b/app/Http/Requests/Promotions/AdminListPromotionsRequest.php @@ -0,0 +1,51 @@ + ['nullable'], + 'status.*' => ['string', Rule::in(StudentPromotionRecord::ALL_STATUSES)], + 'next_school_year' => ['nullable', 'string', 'max:9'], + 'current_school_year' => ['nullable', 'string', 'max:9'], + 'parent_id' => ['nullable', 'integer', 'min:1'], + 'student_id' => ['nullable', 'integer', 'min:1'], + 'search' => ['nullable', 'string', 'max:191'], + 'parent_action_required' => ['nullable', 'boolean'], + 'page' => ['nullable', 'integer', 'min:1'], + 'per_page' => ['nullable', 'integer', 'min:1', 'max:200'], + ]; + } + + public function filters(): array + { + $data = $this->validated(); + $status = $data['status'] ?? null; + if (is_string($status) && $status !== '') { + $status = [$status]; + } elseif (!is_array($status)) { + $status = null; + } + + return [ + 'status' => $status, + 'next_school_year' => $data['next_school_year'] ?? null, + 'current_school_year' => $data['current_school_year'] ?? null, + 'parent_id' => isset($data['parent_id']) ? (int) $data['parent_id'] : null, + 'student_id' => isset($data['student_id']) ? (int) $data['student_id'] : null, + 'search' => $data['search'] ?? null, + 'parent_action_required' => isset($data['parent_action_required']) + ? (bool) $data['parent_action_required'] + : false, + 'page' => isset($data['page']) ? (int) $data['page'] : 1, + 'per_page' => isset($data['per_page']) ? (int) $data['per_page'] : 50, + ]; + } +} diff --git a/app/Http/Requests/Promotions/EvaluateEligibilityRequest.php b/app/Http/Requests/Promotions/EvaluateEligibilityRequest.php new file mode 100644 index 00000000..a41273e0 --- /dev/null +++ b/app/Http/Requests/Promotions/EvaluateEligibilityRequest.php @@ -0,0 +1,18 @@ + ['nullable', 'string', 'in:student,class_section,school_year'], + 'student_id' => ['nullable', 'integer', 'min:1', 'required_if:scope,student'], + 'class_section_id' => ['nullable', 'integer', 'min:1', 'required_if:scope,class_section'], + 'current_school_year' => ['nullable', 'string', 'max:9'], + ]; + } +} diff --git a/app/Http/Requests/Promotions/ParentEnrollmentStepRequest.php b/app/Http/Requests/Promotions/ParentEnrollmentStepRequest.php new file mode 100644 index 00000000..8689d033 --- /dev/null +++ b/app/Http/Requests/Promotions/ParentEnrollmentStepRequest.php @@ -0,0 +1,30 @@ + ['nullable', 'boolean'], + 'documents_uploaded' => ['nullable', 'boolean'], + 'agreement_accepted' => ['nullable', 'boolean'], + 'payment_completed' => ['nullable', 'boolean'], + ]; + } + + public function steps(): array + { + $data = $this->validated(); + $steps = []; + foreach (['info_confirmed', 'documents_uploaded', 'agreement_accepted', 'payment_completed'] as $field) { + if (array_key_exists($field, $data) && $data[$field] !== null) { + $steps[$field] = (bool) $data[$field]; + } + } + return $steps; + } +} diff --git a/app/Http/Requests/Promotions/ParentPromotionOverviewRequest.php b/app/Http/Requests/Promotions/ParentPromotionOverviewRequest.php new file mode 100644 index 00000000..d657553b --- /dev/null +++ b/app/Http/Requests/Promotions/ParentPromotionOverviewRequest.php @@ -0,0 +1,15 @@ + ['nullable', 'string', 'max:9'], + ]; + } +} diff --git a/app/Http/Requests/Promotions/SendReminderRequest.php b/app/Http/Requests/Promotions/SendReminderRequest.php new file mode 100644 index 00000000..f347623c --- /dev/null +++ b/app/Http/Requests/Promotions/SendReminderRequest.php @@ -0,0 +1,21 @@ + ['nullable', 'string', Rule::in(PromotionReminderLog::ALLOWED_TYPES)], + 'subject' => ['nullable', 'string', 'max:191'], + 'message' => ['nullable', 'string', 'max:5000'], + 'channels' => ['nullable', 'array'], + 'channels.*' => ['string', Rule::in(['in_app', 'email', 'sms'])], + ]; + } +} diff --git a/app/Http/Requests/Promotions/SetEnrollmentDeadlineRequest.php b/app/Http/Requests/Promotions/SetEnrollmentDeadlineRequest.php new file mode 100644 index 00000000..21af7646 --- /dev/null +++ b/app/Http/Requests/Promotions/SetEnrollmentDeadlineRequest.php @@ -0,0 +1,19 @@ + ['required', 'date_format:Y-m-d'], + 'apply_to' => ['nullable', 'string', 'in:single,school_year,filter'], + 'next_school_year' => ['nullable', 'string', 'max:9'], + 'promotion_ids' => ['nullable', 'array'], + 'promotion_ids.*' => ['integer', 'min:1'], + ]; + } +} diff --git a/app/Http/Requests/Promotions/UpdatePromotionStatusRequest.php b/app/Http/Requests/Promotions/UpdatePromotionStatusRequest.php new file mode 100644 index 00000000..b3cb1331 --- /dev/null +++ b/app/Http/Requests/Promotions/UpdatePromotionStatusRequest.php @@ -0,0 +1,19 @@ + ['required', 'string', Rule::in(StudentPromotionRecord::ALL_STATUSES)], + 'force' => ['nullable', 'boolean'], + 'notes' => ['nullable', 'string', 'max:500'], + ]; + } +} diff --git a/app/Http/Requests/Promotions/UpsertLevelProgressionRequest.php b/app/Http/Requests/Promotions/UpsertLevelProgressionRequest.php new file mode 100644 index 00000000..d596ba97 --- /dev/null +++ b/app/Http/Requests/Promotions/UpsertLevelProgressionRequest.php @@ -0,0 +1,22 @@ + ['nullable', 'integer', 'min:1'], + 'current_level_name' => ['required', 'string', 'max:100'], + 'next_level_id' => ['nullable', 'integer', 'min:1'], + 'next_level_name' => ['nullable', 'string', 'max:100'], + 'order_index' => ['nullable', 'integer', 'min:0', 'max:65535'], + 'is_terminal' => ['nullable', 'boolean'], + 'is_active' => ['nullable', 'boolean'], + 'notes' => ['nullable', 'string', 'max:500'], + ]; + } +} diff --git a/app/Http/Resources/Fees/ChargeActionResource.php b/app/Http/Resources/Fees/ChargeActionResource.php new file mode 100644 index 00000000..abe4b09c --- /dev/null +++ b/app/Http/Resources/Fees/ChargeActionResource.php @@ -0,0 +1,33 @@ + (bool) ($this->resource['ok'] ?? false), + 'charge_type' => $this->resource['charge_type'] ?? null, + 'id' => isset($this->resource['id']) ? (int) $this->resource['id'] : null, + 'parent_id' => isset($this->resource['parent_id']) ? (int) $this->resource['parent_id'] : null, + 'student_id' => isset($this->resource['student_id']) ? (int) $this->resource['student_id'] : null, + 'invoice_id' => isset($this->resource['invoice_id']) ? (int) $this->resource['invoice_id'] : null, + 'event_id' => isset($this->resource['event_id']) ? (int) $this->resource['event_id'] : null, + 'event_name' => $this->resource['event_name'] ?? null, + 'amount' => isset($this->resource['amount']) ? (float) $this->resource['amount'] : null, + 'status' => $this->resource['status'] ?? null, + 'payment_status' => $this->resource['payment_status'] ?? null, + 'participation_status' => $this->resource['participation_status'] ?? null, + 'cancellation_status' => $this->resource['cancellation_status'] ?? null, + 'account_credit_charge_id' => isset($this->resource['account_credit_charge_id']) + ? (int) $this->resource['account_credit_charge_id'] + : null, + 'message' => $this->resource['message'] ?? null, + 'error' => $this->resource['error'] ?? null, + ], fn ($value) => $value !== null); + } +} diff --git a/app/Http/Resources/Fees/ChargeListResource.php b/app/Http/Resources/Fees/ChargeListResource.php new file mode 100644 index 00000000..1ae9ba9f --- /dev/null +++ b/app/Http/Resources/Fees/ChargeListResource.php @@ -0,0 +1,19 @@ + (int) ($this->resource['parent_id'] ?? 0), + 'school_year' => $this->resource['school_year'] ?? null, + 'extra_charges' => $this->resource['extra_charges'] ?? [], + 'event_charges' => $this->resource['event_charges'] ?? [], + ]; + } +} diff --git a/app/Http/Resources/Fees/FeeFamilyBalanceResource.php b/app/Http/Resources/Fees/FeeFamilyBalanceResource.php new file mode 100644 index 00000000..eeb7bec1 --- /dev/null +++ b/app/Http/Resources/Fees/FeeFamilyBalanceResource.php @@ -0,0 +1,53 @@ +resource['charges'] ?? []; + $credits = $this->resource['credits'] ?? []; + $students = $this->resource['students'] ?? []; + + return [ + 'parent_id' => (int) ($this->resource['parent_id'] ?? 0), + 'school_year' => $this->resource['school_year'] ?? null, + 'charges' => [ + 'tuition' => (float) ($charges['tuition'] ?? 0), + 'extra_charges' => (float) ($charges['extra_charges'] ?? 0), + 'event_charges' => (float) ($charges['event_charges'] ?? 0), + 'late_fees' => (float) ($charges['late_fees'] ?? 0), + 'total' => (float) ($charges['total'] ?? 0), + ], + 'credits' => [ + 'discounts' => (float) ($credits['discounts'] ?? 0), + 'total' => (float) ($credits['total'] ?? 0), + ], + 'payments' => (float) ($this->resource['payments'] ?? 0), + 'refunds_applied' => (float) ($this->resource['refunds_applied'] ?? 0), + 'remaining_balance' => (float) ($this->resource['remaining_balance'] ?? 0), + 'account_credit' => (float) ($this->resource['account_credit'] ?? 0), + 'students' => array_map(function (array $row): array { + return [ + 'student_id' => $row['student_id'] ?? null, + 'student_name' => $row['student_name'] ?? null, + 'grade' => $row['grade'] ?? null, + 'enrollment_status' => $row['enrollment_status'] ?? null, + 'tuition' => (float) ($row['tuition'] ?? 0), + 'extra_charges' => (float) ($row['extra_charges'] ?? 0), + 'event_charges' => (float) ($row['event_charges'] ?? 0), + 'total_charges' => (float) ($row['total_charges'] ?? 0), + 'payments_applied' => (float) ($row['payments_applied'] ?? 0), + 'credits_applied' => (float) ($row['credits_applied'] ?? 0), + 'refunds_applied' => (float) ($row['refunds_applied'] ?? 0), + 'remaining_balance' => (float) ($row['remaining_balance'] ?? 0), + 'account_credit' => (float) ($row['account_credit'] ?? 0), + ]; + }, $students), + ]; + } +} diff --git a/app/Http/Resources/Fees/FeeRefundResource.php b/app/Http/Resources/Fees/FeeRefundResource.php index a8befcb4..6d31a2fe 100644 --- a/app/Http/Resources/Fees/FeeRefundResource.php +++ b/app/Http/Resources/Fees/FeeRefundResource.php @@ -9,6 +9,8 @@ class FeeRefundResource extends JsonResource { public function toArray(Request $request): array { + $students = $this->resource['students'] ?? []; + return [ 'refund_amount' => (float) ($this->resource['refund_amount'] ?? 0), 'total_paid' => (float) ($this->resource['total_paid'] ?? 0), @@ -16,6 +18,20 @@ class FeeRefundResource extends JsonResource 'school_end_date' => $this->resource['school_end_date'] ?? null, 'weeks_of_study' => (float) ($this->resource['weeks_of_study'] ?? 0), 'withdrawn_students' => (int) ($this->resource['withdrawn_students'] ?? 0), + 'students' => array_map(function (array $row): array { + return [ + 'student_id' => $row['student_id'] ?? null, + 'grade' => $row['grade'] ?? null, + 'grade_level' => $row['grade_level'] ?? null, + 'fee_category' => $row['fee_category'] ?? null, + 'tuition_fee' => (float) ($row['tuition_fee'] ?? 0), + 'withdrawal_date' => $row['withdrawal_date'] ?? null, + 'weeks_remaining' => (int) ($row['weeks_remaining'] ?? 0), + 'refund_amount' => (float) ($row['refund_amount'] ?? 0), + 'eligible' => (bool) ($row['eligible'] ?? false), + 'reason' => $row['reason'] ?? null, + ]; + }, $students), ]; } } diff --git a/app/Http/Resources/Fees/FeeTuitionBreakdownResource.php b/app/Http/Resources/Fees/FeeTuitionBreakdownResource.php new file mode 100644 index 00000000..11bc1985 --- /dev/null +++ b/app/Http/Resources/Fees/FeeTuitionBreakdownResource.php @@ -0,0 +1,41 @@ +resource['students'] ?? []; + $fees = $this->resource['fees'] ?? []; + + return [ + 'school_year' => $this->resource['school_year'] ?? null, + 'family_total' => (float) ($this->resource['family_total'] ?? 0), + 'billable_count' => (int) ($this->resource['billable_count'] ?? 0), + 'non_billable_count' => (int) ($this->resource['non_billable_count'] ?? 0), + 'fees' => [ + 'first_student_fee' => (float) ($fees['first_student_fee'] ?? 0), + 'second_student_fee' => (float) ($fees['second_student_fee'] ?? 0), + 'youth_fee' => (float) ($fees['youth_fee'] ?? 0), + ], + 'students' => array_map(function (array $row): array { + return [ + 'student_id' => $row['student_id'] ?? null, + 'student_name' => $row['student_name'] ?? null, + 'grade' => $row['grade'] ?? null, + 'grade_level' => $row['grade_level'] ?? null, + 'enrollment_status' => $row['enrollment_status'] ?? null, + 'admission_status' => $row['admission_status'] ?? null, + 'fee_category' => $row['fee_category'] ?? null, + 'tuition_fee' => (float) ($row['tuition_fee'] ?? 0), + 'discount_amount' => (float) ($row['discount_amount'] ?? 0), + 'final_tuition_amount' => (float) ($row['final_tuition_amount'] ?? 0), + ]; + }, $students), + ]; + } +} diff --git a/app/Http/Resources/Promotions/LevelProgressionResource.php b/app/Http/Resources/Promotions/LevelProgressionResource.php new file mode 100644 index 00000000..82d576d8 --- /dev/null +++ b/app/Http/Resources/Promotions/LevelProgressionResource.php @@ -0,0 +1,23 @@ + (int) ($this->id ?? 0), + 'current_level_id' => $this->current_level_id !== null ? (int) $this->current_level_id : null, + 'current_level_name' => (string) ($this->current_level_name ?? ''), + 'next_level_id' => $this->next_level_id !== null ? (int) $this->next_level_id : null, + 'next_level_name' => $this->next_level_name, + 'order_index' => (int) ($this->order_index ?? 0), + 'is_terminal' => (bool) $this->is_terminal, + 'is_active' => (bool) $this->is_active, + 'notes' => $this->notes, + ]; + } +} diff --git a/app/Http/Resources/Promotions/PromotionAuditLogResource.php b/app/Http/Resources/Promotions/PromotionAuditLogResource.php new file mode 100644 index 00000000..3a9a86b6 --- /dev/null +++ b/app/Http/Resources/Promotions/PromotionAuditLogResource.php @@ -0,0 +1,25 @@ + (int) ($this->id ?? 0), + 'promotion_id' => $this->promotion_id ? (int) $this->promotion_id : null, + 'student_id' => $this->student_id ? (int) $this->student_id : null, + 'user_id' => $this->user_id ? (int) $this->user_id : null, + 'action_type' => $this->action_type, + 'field' => $this->field, + 'old_value' => $this->old_value, + 'new_value' => $this->new_value, + 'notes' => $this->notes, + 'performed_at' => optional($this->performed_at)->toDateTimeString(), + 'created_at' => optional($this->created_at)->toDateTimeString(), + ]; + } +} diff --git a/app/Http/Resources/Promotions/PromotionReminderResource.php b/app/Http/Resources/Promotions/PromotionReminderResource.php new file mode 100644 index 00000000..0bfb5824 --- /dev/null +++ b/app/Http/Resources/Promotions/PromotionReminderResource.php @@ -0,0 +1,24 @@ + (int) ($this->id ?? 0), + 'promotion_id' => (int) ($this->promotion_id ?? 0), + 'student_id' => $this->student_id ? (int) $this->student_id : null, + 'parent_id' => $this->parent_id ? (int) $this->parent_id : null, + 'reminder_type' => $this->reminder_type, + 'channel' => $this->channel, + 'subject' => $this->subject, + 'message' => $this->message, + 'sent_at' => optional($this->sent_at)->toDateTimeString(), + 'sent_by' => $this->sent_by ? (int) $this->sent_by : null, + ]; + } +} diff --git a/app/Http/Resources/Promotions/StudentPromotionResource.php b/app/Http/Resources/Promotions/StudentPromotionResource.php new file mode 100644 index 00000000..ec6455c0 --- /dev/null +++ b/app/Http/Resources/Promotions/StudentPromotionResource.php @@ -0,0 +1,53 @@ +resource; + $get = static function (string $key, $default = null) use ($r) { + if (is_array($r)) { + return $r[$key] ?? $default; + } + return $r->{$key} ?? $default; + }; + + return [ + 'promotion_id' => (int) ($get('promotion_id') ?? 0), + 'student_id' => (int) ($get('student_id') ?? 0), + 'student_name' => $get('student_name'), + 'school_id' => $get('school_id'), + 'parent_id' => $get('parent_id') ? (int) $get('parent_id') : null, + 'parent_name' => $get('parent_name'), + 'parent_email' => $get('parent_email'), + 'current_school_year' => $get('current_school_year'), + 'next_school_year' => $get('next_school_year'), + 'current_level' => $get('current_level') ?? $get('current_level_name'), + 'promoted_level' => $get('promoted_level') ?? $get('promoted_level_name'), + 'promotion_status' => $get('promotion_status'), + 'enrollment_status' => $get('enrollment_status'), + 'enrollment_deadline' => $get('enrollment_deadline'), + 'parent_notified_at' => $get('parent_notified_at'), + 'enrollment_started_at' => $get('enrollment_started_at'), + 'enrollment_completed_at' => $get('enrollment_completed_at'), + 'promotion_finalized_at' => $get('promotion_finalized_at'), + 'final_average' => $get('final_average') !== null ? (float) $get('final_average') : null, + 'passed_current_level' => $get('passed_current_level'), + 'checklist' => $get('checklist'), + 'eligibility_notes' => $get('eligibility_notes'), + 'enrollment_id' => $get('enrollment_id') ? (int) $get('enrollment_id') : null, + 'updated_by' => $get('updated_by') ? (int) $get('updated_by') : null, + 'updated_at' => $get('updated_at'), + ]; + } +} diff --git a/app/Models/AuthorizedUser.php b/app/Models/AuthorizedUser.php index 86781447..d51ddb81 100644 --- a/app/Models/AuthorizedUser.php +++ b/app/Models/AuthorizedUser.php @@ -17,6 +17,7 @@ class AuthorizedUser extends BaseModel 'firstname', 'lastname', 'phone_number', + 'gender', 'email', 'relation_to_user', 'token', diff --git a/app/Models/EventCharges.php b/app/Models/EventCharges.php index 654b6d6e..43355863 100644 --- a/app/Models/EventCharges.php +++ b/app/Models/EventCharges.php @@ -14,6 +14,9 @@ class EventCharges extends BaseModel protected $fillable = [ 'event_id', + 'event_name', + 'description', + 'amount', 'parent_id', 'student_id', 'participation', diff --git a/app/Models/LevelProgression.php b/app/Models/LevelProgression.php new file mode 100644 index 00000000..c1e1e8c4 --- /dev/null +++ b/app/Models/LevelProgression.php @@ -0,0 +1,62 @@ + 'integer', + 'next_level_id' => 'integer', + 'order_index' => 'integer', + 'is_terminal' => 'boolean', + 'is_active' => 'boolean', + ]; + + public function scopeActive(Builder $q): Builder + { + return $q->where('is_active', 1); + } + + public function scopeOrdered(Builder $q): Builder + { + return $q->orderBy('order_index')->orderBy('current_level_name'); + } + + public static function findByCurrentLevelId(int $levelId): ?self + { + return static::query() + ->where('current_level_id', $levelId) + ->where('is_active', 1) + ->first(); + } + + public static function findByCurrentLevelName(string $name): ?self + { + return static::query() + ->whereRaw('LOWER(current_level_name) = ?', [strtolower(trim($name))]) + ->where('is_active', 1) + ->first(); + } +} diff --git a/app/Models/PromotionAuditLog.php b/app/Models/PromotionAuditLog.php new file mode 100644 index 00000000..7d6b1423 --- /dev/null +++ b/app/Models/PromotionAuditLog.php @@ -0,0 +1,64 @@ + 'integer', + 'student_id' => 'integer', + 'user_id' => 'integer', + 'performed_at' => 'datetime', + ]; + + public function promotion(): BelongsTo + { + return $this->belongsTo(StudentPromotionRecord::class, 'promotion_id', 'promotion_id'); + } + + public function actor(): BelongsTo + { + return $this->belongsTo(User::class, 'user_id'); + } + + public function scopeForPromotion(Builder $q, int $promotionId): Builder + { + return $q->where('promotion_id', $promotionId); + } +} diff --git a/app/Models/PromotionReminderLog.php b/app/Models/PromotionReminderLog.php new file mode 100644 index 00000000..88973cf1 --- /dev/null +++ b/app/Models/PromotionReminderLog.php @@ -0,0 +1,61 @@ + 'integer', + 'student_id' => 'integer', + 'parent_id' => 'integer', + 'sent_by' => 'integer', + 'sent_at' => 'datetime', + ]; + + public function promotion(): BelongsTo + { + return $this->belongsTo(StudentPromotionRecord::class, 'promotion_id', 'promotion_id'); + } + + public function scopeForPromotion(Builder $q, int $promotionId): Builder + { + return $q->where('promotion_id', $promotionId); + } +} diff --git a/app/Models/StudentPromotionRecord.php b/app/Models/StudentPromotionRecord.php new file mode 100644 index 00000000..1611f7ae --- /dev/null +++ b/app/Models/StudentPromotionRecord.php @@ -0,0 +1,194 @@ + 'integer', + 'parent_id' => 'integer', + 'current_level_id' => 'integer', + 'promoted_level_id' => 'integer', + 'passed_current_level' => 'boolean', + 'final_average' => 'float', + 'attendance_ok' => 'boolean', + 'enrollment_required' => 'boolean', + 'enrollment_id' => 'integer', + 'parent_notified_at' => 'datetime', + 'enrollment_deadline' => 'date', + 'enrollment_started_at' => 'datetime', + 'enrollment_completed_at' => 'datetime', + 'promotion_finalized_at' => 'datetime', + 'info_confirmed' => 'boolean', + 'documents_uploaded' => 'boolean', + 'agreement_accepted' => 'boolean', + 'payment_completed' => 'boolean', + 'updated_by' => 'integer', + ]; + + public function student(): BelongsTo + { + return $this->belongsTo(Student::class, 'student_id'); + } + + public function parent(): BelongsTo + { + return $this->belongsTo(User::class, 'parent_id'); + } + + public function enrollment(): BelongsTo + { + return $this->belongsTo(Enrollment::class, 'enrollment_id'); + } + + public function scopeForNextYear(Builder $q, string $year): Builder + { + return $q->where('next_school_year', $year); + } + + public function scopeStatus(Builder $q, string $status): Builder + { + return $q->where('promotion_status', $status); + } + + public function scopeForStudent(Builder $q, int $studentId): Builder + { + return $q->where('student_id', $studentId); + } + + public function scopeForParent(Builder $q, int $parentId): Builder + { + return $q->where('parent_id', $parentId); + } + + /** + * Statuses the parent portal should treat as "actionable" (parent + * can still complete enrollment). + */ + public static function parentActionableStatuses(): array + { + return [ + self::STATUS_ELIGIBLE, + self::STATUS_AWAITING_PARENT, + self::STATUS_ENROLLMENT_STARTED, + ]; + } + + /** + * Statuses considered "open" for the next school year (admin views + * may want to filter on these). + */ + public static function openStatuses(): array + { + return [ + self::STATUS_NOT_REVIEWED, + self::STATUS_ELIGIBLE, + self::STATUS_AWAITING_PARENT, + self::STATUS_ENROLLMENT_STARTED, + self::STATUS_CONDITIONAL, + self::STATUS_ON_HOLD, + ]; + } + + public function isFinalized(): bool + { + return in_array( + $this->promotion_status, + [ + self::STATUS_PROMOTED_AND_ENROLLED, + self::STATUS_REPEATED, + self::STATUS_WITHDRAWN, + self::STATUS_GRADUATED, + self::STATUS_NOT_ENROLLED, + ], + true + ); + } + + public function enrollmentChecklistComplete(): bool + { + // Mirrors plan section 8 – parent-side completion criteria. + // Payment is only required when payment_completed has been + // explicitly set; treat null/false as still required by default. + return (bool) $this->info_confirmed + && (bool) $this->documents_uploaded + && (bool) $this->agreement_accepted + && (bool) $this->payment_completed; + } +} diff --git a/app/Services/Auth/ApiLoginSecurityService.php b/app/Services/Auth/ApiLoginSecurityService.php index 23067cd7..9504fde7 100644 --- a/app/Services/Auth/ApiLoginSecurityService.php +++ b/app/Services/Auth/ApiLoginSecurityService.php @@ -6,6 +6,7 @@ use App\Models\IpAttempt; use App\Models\LoginActivity; use App\Models\Configuration; use App\Models\User; +use Carbon\CarbonImmutable; use Illuminate\Http\Request; use Illuminate\Support\Facades\Log; @@ -15,6 +16,10 @@ use Illuminate\Support\Facades\Log; */ class ApiLoginSecurityService { + private const MAX_ATTEMPTS = 10; + + private const BLOCK_HOURS = 24; + public function isIpBlocked(string $ip): bool { $row = IpAttempt::query()->where('ip_address', $ip)->first(); @@ -27,19 +32,28 @@ class ApiLoginSecurityService public function logIpAttempt(string $ip): void { - $now = utc_now(); + $now = CarbonImmutable::now('UTC'); + $nowStr = $now->toDateTimeString(); $attempt = IpAttempt::query()->where('ip_address', $ip)->first(); if ($attempt) { - $attempts = ((int) $attempt->attempts) + 1; - $blockedUntil = null; - if ($attempts >= 10) { - $blockedUntil = date('Y-m-d H:i:s', strtotime('+24 hours')); - } + $previouslyBlocked = $attempt->blocked_until + ? CarbonImmutable::parse($attempt->blocked_until, 'UTC')->isFuture() + : false; + + // If the previous block has already expired, start a fresh window + // so we don't perma-ban an IP after its first 10 lifetime failures. + $attempts = $previouslyBlocked + ? ((int) $attempt->attempts) + 1 + : 1; + + $blockedUntil = $attempts >= self::MAX_ATTEMPTS + ? $now->addHours(self::BLOCK_HOURS)->toDateTimeString() + : null; $attempt->update([ 'attempts' => $attempts, - 'last_attempt_at' => $now, + 'last_attempt_at' => $nowStr, 'blocked_until' => $blockedUntil, ]); @@ -49,7 +63,7 @@ class ApiLoginSecurityService IpAttempt::query()->create([ 'ip_address' => $ip, 'attempts' => 1, - 'last_attempt_at' => $now, + 'last_attempt_at' => $nowStr, ]); } diff --git a/app/Services/Billing/BalanceCalculationService.php b/app/Services/Billing/BalanceCalculationService.php new file mode 100644 index 00000000..46ca0a0b --- /dev/null +++ b/app/Services/Billing/BalanceCalculationService.php @@ -0,0 +1,172 @@ +> $students + * + * @return array + */ + public function calculateFamilyAccount(array $students, int $parentId, ?string $schoolYear = null): array + { + $schoolYear = $schoolYear ?? $this->config->getSchoolYear(); + + $tuitionBreakdown = $this->tuition->calculate($students); + $tuitionTotal = (float) ($tuitionBreakdown['family_total'] ?? 0); + $extraCharges = $this->billingTotals->additionalSubtotalByParent($parentId, $schoolYear); + $eventByStudent = $this->billingTotals->eventSubtotalsByStudent($parentId, $schoolYear); + $eventCharges = round(array_sum($eventByStudent), 2); + $lateFees = 0.0; + + $totalCharges = round($tuitionTotal + $extraCharges + $eventCharges + $lateFees, 2); + + $totalPayments = round(Payment::getTotalPaidByParentId($parentId, $schoolYear), 2); + $totalCredits = round( + DiscountUsage::getTotalDiscountByParentIdAndSchoolYear($parentId, $schoolYear), + 2 + ); + $refundsApplied = round( + Refund::totalApprovedPaidOutForParentAndYear($parentId, $schoolYear), + 2 + ); + + $netting = round($totalPayments + $totalCredits + $refundsApplied, 2); + $rawRemaining = round($totalCharges - $netting, 2); + $remainingBalance = max(0.0, $rawRemaining); + $accountCredit = $rawRemaining < 0 ? round(abs($rawRemaining), 2) : 0.0; + + $studentRows = $this->buildStudentBalances( + $tuitionBreakdown['students'] ?? [], + $eventByStudent, + $totalCharges, + $totalPayments, + $totalCredits, + $refundsApplied + ); + + Log::info('Family account balance calculated', [ + 'parent_id' => $parentId, + 'school_year' => $schoolYear, + 'total_charges' => $totalCharges, + 'remaining_balance' => $remainingBalance, + 'account_credit' => $accountCredit, + ]); + + return [ + 'parent_id' => $parentId, + 'school_year' => $schoolYear, + 'charges' => [ + 'tuition' => $tuitionTotal, + 'extra_charges' => $extraCharges, + 'event_charges' => $eventCharges, + 'late_fees' => $lateFees, + 'total' => $totalCharges, + ], + 'credits' => [ + 'discounts' => $totalCredits, + 'total' => $totalCredits, + ], + 'payments' => $totalPayments, + 'refunds_applied' => $refundsApplied, + 'remaining_balance' => $remainingBalance, + 'account_credit' => $accountCredit, + 'tuition' => $tuitionBreakdown, + 'students' => $studentRows, + ]; + } + + /** + * @param array> $tuitionStudents + * @param array $eventByStudent + * + * @return array> + */ + private function buildStudentBalances( + array $tuitionStudents, + array $eventByStudent, + float $familyTotalCharges, + float $totalPayments, + float $totalCredits, + float $refundsApplied + ): array { + $familyNetting = $totalPayments + $totalCredits + $refundsApplied; + $rows = []; + + foreach ($tuitionStudents as $student) { + $studentId = (int) ($student['student_id'] ?? 0); + $tuitionAmount = (float) ($student['final_tuition_amount'] ?? $student['tuition_fee'] ?? 0); + $eventAmount = (float) ($eventByStudent[$studentId] ?? 0); + $extraAmount = 0.0; + $studentTotalCharges = round($tuitionAmount + $eventAmount + $extraAmount, 2); + + $share = $familyTotalCharges > 0 + ? ($studentTotalCharges / $familyTotalCharges) + : 0.0; + + $paymentsApplied = round($totalPayments * $share, 2); + $creditsApplied = round($totalCredits * $share, 2); + $refundsAppliedShare = round($refundsApplied * $share, 2); + $allocatedNetting = round($familyNetting * $share, 2); + $rawRemaining = round($studentTotalCharges - $allocatedNetting, 2); + + $rows[] = [ + 'student_id' => $studentId > 0 ? $studentId : ($student['student_id'] ?? null), + 'student_name' => $student['student_name'] ?? null, + 'grade' => $student['grade'] ?? null, + 'enrollment_status' => $student['enrollment_status'] ?? null, + 'tuition' => round($tuitionAmount, 2), + 'extra_charges' => $extraAmount, + 'event_charges' => $eventAmount, + 'total_charges' => $studentTotalCharges, + 'payments_applied' => $paymentsApplied, + 'credits_applied' => $creditsApplied, + 'refunds_applied' => $refundsAppliedShare, + 'remaining_balance' => max(0.0, $rawRemaining), + 'account_credit' => $rawRemaining < 0 ? round(abs($rawRemaining), 2) : 0.0, + ]; + } + + $unassignedEvent = (float) ($eventByStudent[0] ?? 0); + if ($unassignedEvent > 0) { + $rows[] = [ + 'student_id' => null, + 'student_name' => null, + 'grade' => null, + 'enrollment_status' => null, + 'tuition' => 0.0, + 'extra_charges' => 0.0, + 'event_charges' => $unassignedEvent, + 'total_charges' => $unassignedEvent, + 'payments_applied' => 0.0, + 'credits_applied' => 0.0, + 'refunds_applied' => 0.0, + 'remaining_balance' => $unassignedEvent, + 'account_credit' => 0.0, + ]; + } + + return $rows; + } +} diff --git a/app/Services/Billing/BillingTotalsService.php b/app/Services/Billing/BillingTotalsService.php index a80e82e4..92ce9a6c 100644 --- a/app/Services/Billing/BillingTotalsService.php +++ b/app/Services/Billing/BillingTotalsService.php @@ -10,41 +10,109 @@ class BillingTotalsService { public function eventSubtotal(int $parentId, string $schoolYear): float { - $eventSubtotal = 0.0; + return round(array_sum($this->eventSubtotalsByStudent($parentId, $schoolYear)), 2); + } + + /** + * @return array student_id => amount + */ + public function eventSubtotalsByStudent(int $parentId, string $schoolYear): array + { + $byStudent = []; + try { $events = EventCharges::getChargesWithEventInfo($parentId, $schoolYear) ?? []; foreach ($events as $event) { - $eventSubtotal += (float) ($event['charged'] ?? 0.0); + $amount = (float) ($event['charged'] ?? 0); + if ($amount <= 0) { + continue; + } + + $studentId = (int) ($event['student_id'] ?? 0); + if ($studentId > 0) { + $byStudent[$studentId] = ($byStudent[$studentId] ?? 0.0) + $amount; + } else { + $byStudent[0] = ($byStudent[0] ?? 0.0) + $amount; + } } } catch (\Throwable $e) { Log::warning('Failed to sum event charges.', ['error' => $e->getMessage()]); } - return round($eventSubtotal, 2); + foreach ($byStudent as $studentId => $amount) { + $byStudent[$studentId] = round($amount, 2); + } + + return $byStudent; } public function additionalSubtotal(int $invoiceId, ?string $schoolYear = null): float { - $additionalSubtotal = 0.0; + return $this->sumAdditionalChargeRows( + $this->fetchAdditionalChargeRows(invoiceId: $invoiceId, schoolYear: $schoolYear) + ); + } + + public function additionalSubtotalByParent(int $parentId, string $schoolYear): float + { + return $this->sumAdditionalChargeRows( + $this->fetchAdditionalChargeRows(parentId: $parentId, schoolYear: $schoolYear) + ); + } + + /** + * @return array + */ + private function fetchAdditionalChargeRows( + ?int $parentId = null, + ?int $invoiceId = null, + ?string $schoolYear = null + ): array { + $rows = []; + try { $builder = AdditionalCharge::query() ->select('charge_type', 'amount') - ->where('invoice_id', $invoiceId) ->where('status', 'applied'); + if ($parentId !== null) { + $builder->where('parent_id', $parentId); + } + + if ($invoiceId !== null) { + $builder->where('invoice_id', $invoiceId); + } + if ($schoolYear !== null && $schoolYear !== '') { $builder->where('school_year', $schoolYear); } $rows = $builder->get()->map(fn ($row) => (array) $row)->all(); - foreach ($rows as $row) { - $amount = (float) ($row['amount'] ?? 0); - $type = strtolower((string) ($row['charge_type'] ?? 'add')); - $amount = $type === 'deduct' ? -abs($amount) : abs($amount); - $additionalSubtotal += $amount; - } } catch (\Throwable $e) { - Log::warning('Failed to sum additional charges.', ['error' => $e->getMessage()]); + Log::warning('Failed to load additional charges.', ['error' => $e->getMessage()]); + } + + return $rows; + } + + /** + * @param array $rows + */ + private function sumAdditionalChargeRows(array $rows): float + { + $additionalSubtotal = 0.0; + + foreach ($rows as $row) { + $amount = (float) ($row['amount'] ?? 0); + $type = strtolower((string) ($row['charge_type'] ?? 'add')); + + if (in_array($type, ['deduct', 'previous'], true)) { + $amount = -abs($amount); + } else { + $amount = abs($amount); + } + + $additionalSubtotal += $amount; } return round($additionalSubtotal, 2); diff --git a/app/Services/Billing/ChargeService.php b/app/Services/Billing/ChargeService.php new file mode 100644 index 00000000..6c2125ef --- /dev/null +++ b/app/Services/Billing/ChargeService.php @@ -0,0 +1,456 @@ +context->getSchoolYear()); + $title = trim((string) ($payload['title'] ?? '')); + $chargeType = (string) ($payload['charge_type'] ?? 'add'); + $amountAbs = round(abs((float) ($payload['amount'] ?? 0)), 2); + + if ($parentId <= 0 || $title === '' || $amountAbs <= 0) { + return ['ok' => false, 'message' => 'Invalid extra charge payload.']; + } + + if ($this->findDuplicateExtraCharge($parentId, $schoolYear, $title, $amountAbs, $chargeType)) { + Log::warning('Duplicate extra charge blocked', [ + 'parent_id' => $parentId, + 'title' => $title, + 'school_year' => $schoolYear, + ]); + + return [ + 'ok' => false, + 'message' => 'A matching extra charge already exists for this parent.', + 'error' => 'duplicate_charge', + ]; + } + + $result = $this->extraCharges->createCharge(array_merge($payload, [ + 'school_year' => $schoolYear, + 'semester' => $payload['semester'] ?? $this->context->getSemester(), + ])); + + if (!($result['ok'] ?? false)) { + return $result; + } + + return [ + 'ok' => true, + 'charge_type' => 'extra', + 'id' => (int) ($result['id'] ?? 0), + 'parent_id' => $parentId, + 'invoice_id' => $result['invoice_id'] ?? null, + 'status' => !empty($payload['invoice_id']) ? 'applied' : 'pending', + ]; + } + + public function cancelExtraCharge(int $chargeId): array + { + $charge = AdditionalCharge::query()->find($chargeId); + if (!$charge) { + return ['ok' => false, 'message' => 'Extra charge not found.']; + } + + if ((string) $charge->status === 'void') { + return ['ok' => false, 'message' => 'Charge is already void.', 'error' => 'already_canceled']; + } + + $ok = $this->extraCharges->voidCharge($charge); + + return [ + 'ok' => $ok, + 'charge_type' => 'extra', + 'id' => $chargeId, + 'status' => $ok ? 'void' : (string) $charge->status, + 'message' => $ok ? null : 'Unable to void extra charge.', + ]; + } + + public function applyExtraCharge(int $chargeId, int $invoiceId): array + { + $charge = AdditionalCharge::query()->find($chargeId); + if (!$charge) { + return ['ok' => false, 'message' => 'Extra charge not found.']; + } + + if ((string) $charge->status === 'void') { + return ['ok' => false, 'message' => 'Cannot apply a void charge.']; + } + + if ((string) $charge->status === 'applied' && (int) $charge->invoice_id === $invoiceId) { + return [ + 'ok' => true, + 'charge_type' => 'extra', + 'id' => $chargeId, + 'status' => 'applied', + 'invoice_id' => $invoiceId, + ]; + } + + if ((string) $charge->status === 'applied') { + return ['ok' => false, 'message' => 'Charge is already applied to another invoice.']; + } + + $chargeType = (string) ($charge->charge_type ?? 'add'); + $amountAbs = round(abs((float) $charge->amount), 2); + + DB::beginTransaction(); + + try { + $this->invoices->applyToInvoice($invoiceId, $chargeType, $amountAbs); + $charge->invoice_id = $invoiceId; + $charge->status = 'applied'; + $charge->save(); + DB::commit(); + } catch (\Throwable $e) { + DB::rollBack(); + Log::error('Apply extra charge failed', ['charge_id' => $chargeId, 'error' => $e->getMessage()]); + + return ['ok' => false, 'message' => 'Unable to apply charge to invoice.']; + } + + return [ + 'ok' => true, + 'charge_type' => 'extra', + 'id' => $chargeId, + 'status' => 'applied', + 'invoice_id' => $invoiceId, + ]; + } + + public function createEventCharge(array $payload): array + { + $parentId = (int) ($payload['parent_id'] ?? 0); + $studentId = (int) ($payload['student_id'] ?? 0); + $schoolYear = (string) ($payload['school_year'] ?? $this->context->getSchoolYear()); + $semester = (string) ($payload['semester'] ?? $this->context->getSemester()); + $eventId = isset($payload['event_id']) ? (int) $payload['event_id'] : null; + $eventName = trim((string) ($payload['event_name'] ?? '')); + $actorId = (int) ($payload['created_by'] ?? $payload['updated_by'] ?? 0); + + if ($parentId <= 0 || $studentId <= 0) { + return ['ok' => false, 'message' => 'Parent and student are required.']; + } + + $amount = isset($payload['amount']) ? (float) $payload['amount'] : null; + if ($eventId > 0) { + $event = Event::getEvent($eventId, $schoolYear); + if (!$event) { + return ['ok' => false, 'message' => 'Event not found.']; + } + $eventName = (string) ($event->event_name ?? $eventName); + $amount = $amount ?? (float) ($event->amount ?? 0); + } + + if ($eventName === '' || $amount === null || $amount < 0) { + return ['ok' => false, 'message' => 'Event name and amount are required.']; + } + + if ($this->findDuplicateEventCharge($parentId, $studentId, $schoolYear, $semester, $eventId, $eventName)) { + Log::warning('Duplicate event charge blocked', [ + 'parent_id' => $parentId, + 'student_id' => $studentId, + 'event_id' => $eventId, + 'event_name' => $eventName, + ]); + + return [ + 'ok' => false, + 'message' => 'This student already has an active charge for this event.', + 'error' => 'duplicate_charge', + ]; + } + + $charge = EventCharges::query()->create([ + 'event_id' => $eventId > 0 ? $eventId : null, + 'event_name' => $eventId > 0 ? null : $eventName, + 'parent_id' => $parentId, + 'student_id' => $studentId, + 'participation' => 'yes', + 'charged' => round($amount, 2), + 'amount' => round($amount, 2), + 'school_year' => $schoolYear, + 'semester' => $semester, + 'created_by' => $actorId ?: null, + 'updated_by' => $actorId ?: null, + ]); + + $this->invoiceGeneration->generateInvoice($parentId, $schoolYear); + + return [ + 'ok' => true, + 'charge_type' => 'event', + 'id' => (int) $charge->id, + 'parent_id' => $parentId, + 'student_id' => $studentId, + 'event_id' => $eventId > 0 ? $eventId : null, + 'event_name' => $eventName, + 'amount' => round($amount, 2), + 'participation_status' => 'yes', + 'payment_status' => 'unpaid', + 'cancellation_status' => 'active', + ]; + } + + public function cancelEventCharge(int $chargeId, bool $issueCredit = true): array + { + $charge = EventCharges::query()->find($chargeId); + if (!$charge) { + return ['ok' => false, 'message' => 'Event charge not found.']; + } + + if ($this->isEventChargeCanceled($charge)) { + return ['ok' => false, 'message' => 'Event charge is already canceled.', 'error' => 'already_canceled']; + } + + $amount = round(abs((float) ($charge->charged ?? $charge->amount ?? 0)), 2); + $eventName = $this->resolveEventName($charge); + $parentId = (int) $charge->parent_id; + $schoolYear = (string) $charge->school_year; + + DB::beginTransaction(); + + try { + $charge->participation = 'no'; + $charge->charged = 0; + if (Schema::hasColumn('event_charges', 'amount')) { + $charge->amount = 0; + } + $charge->save(); + + $creditId = null; + if ($issueCredit && $amount > 0 && $parentId > 0) { + $credit = $this->createExtraCharge([ + 'parent_id' => $parentId, + 'title' => 'Event credit: ' . $eventName, + 'description' => 'Account credit for canceled event charge #' . $chargeId, + 'amount' => $amount, + 'charge_type' => 'deduct', + 'school_year' => $schoolYear, + 'semester' => (string) ($charge->semester ?? $this->context->getSemester()), + 'created_by' => (int) ($charge->updated_by ?? 0), + ]); + if ($credit['ok'] ?? false) { + $creditId = (int) ($credit['id'] ?? 0); + } + } + + $this->invoiceGeneration->generateInvoice($parentId, $schoolYear); + DB::commit(); + } catch (\Throwable $e) { + DB::rollBack(); + Log::error('Cancel event charge failed', ['charge_id' => $chargeId, 'error' => $e->getMessage()]); + + return ['ok' => false, 'message' => 'Unable to cancel event charge.']; + } + + return [ + 'ok' => true, + 'charge_type' => 'event', + 'id' => $chargeId, + 'cancellation_status' => 'canceled', + 'participation_status' => 'no', + 'account_credit_charge_id' => $creditId, + ]; + } + + public function markEventChargePaid(int $chargeId, bool $paid, ?int $paymentId = null): array + { + $charge = EventCharges::query()->find($chargeId); + if (!$charge) { + return ['ok' => false, 'message' => 'Event charge not found.']; + } + + if ($this->isEventChargeCanceled($charge)) { + return ['ok' => false, 'message' => 'Cannot update payment on a canceled charge.']; + } + + if (Schema::hasColumn('event_charges', 'event_paid')) { + $charge->event_paid = $paid ? 1 : 0; + } + if (Schema::hasColumn('event_charges', 'event_payment_id') && $paymentId !== null) { + $charge->event_payment_id = $paymentId; + } + $charge->save(); + + return [ + 'ok' => true, + 'charge_type' => 'event', + 'id' => $chargeId, + 'payment_status' => $paid ? 'paid' : 'unpaid', + ]; + } + + /** + * @return array{extra_charges: array>, event_charges: array>} + */ + public function listForParent(int $parentId, ?string $schoolYear = null): array + { + $schoolYear = $schoolYear ?? $this->context->getSchoolYear(); + + $extraRows = AdditionalCharge::query() + ->where('parent_id', $parentId) + ->where('school_year', $schoolYear) + ->orderByDesc('id') + ->get(); + + $eventRows = EventCharges::getChargesWithEventInfo($parentId, $schoolYear); + + return [ + 'parent_id' => $parentId, + 'school_year' => $schoolYear, + 'extra_charges' => $extraRows->map(fn (AdditionalCharge $row) => $this->formatExtraCharge($row))->all(), + 'event_charges' => array_map(fn (array $row) => $this->formatEventCharge($row), $eventRows), + ]; + } + + private function findDuplicateExtraCharge( + int $parentId, + string $schoolYear, + string $title, + float $amountAbs, + string $chargeType + ): bool { + $normalizedTitle = strtolower($title); + $signedAmount = $chargeType === 'deduct' ? -$amountAbs : $amountAbs; + + return AdditionalCharge::query() + ->where('parent_id', $parentId) + ->where('school_year', $schoolYear) + ->whereIn('status', ['pending', 'applied']) + ->whereRaw('LOWER(title) = ?', [$normalizedTitle]) + ->where('amount', $signedAmount) + ->exists(); + } + + private function findDuplicateEventCharge( + int $parentId, + int $studentId, + string $schoolYear, + string $semester, + ?int $eventId, + string $eventName + ): bool { + $query = EventCharges::query() + ->where('parent_id', $parentId) + ->where('student_id', $studentId) + ->where('school_year', $schoolYear) + ->where('semester', $semester) + ->where('participation', 'yes') + ->where('charged', '>', 0); + + if ($eventId > 0) { + $query->where('event_id', $eventId); + } else { + $query->where('event_name', $eventName); + } + + return $query->exists(); + } + + private function isEventChargeCanceled(EventCharges $charge): bool + { + if (strtolower((string) $charge->participation) === 'no') { + return true; + } + + return (float) ($charge->charged ?? 0) <= 0; + } + + private function resolveEventName(EventCharges $charge): string + { + if (!empty($charge->event_name)) { + return (string) $charge->event_name; + } + + if ($charge->event_id) { + $event = Event::query()->find($charge->event_id); + if ($event) { + return (string) $event->event_name; + } + } + + return 'Event'; + } + + private function formatExtraCharge(AdditionalCharge $row): array + { + return [ + 'id' => (int) $row->id, + 'charge_type' => 'extra', + 'parent_id' => (int) $row->parent_id, + 'invoice_id' => $row->invoice_id ? (int) $row->invoice_id : null, + 'school_year' => $row->school_year, + 'semester' => $row->semester, + 'charge_name' => $row->title, + 'charge_description' => $row->description, + 'amount' => round(abs((float) $row->amount), 2), + 'signed_amount' => round((float) $row->amount, 2), + 'fee_type' => $row->charge_type, + 'due_date' => $row->due_date, + 'status' => $row->status, + 'approval_status' => $row->status, + 'created_by' => $row->created_by, + 'created_at' => $row->created_at, + ]; + } + + /** + * @param array $row + */ + private function formatEventCharge(array $row): array + { + $charged = (float) ($row['charged'] ?? 0); + $canceled = strtolower((string) ($row['participation'] ?? '')) === 'no' || $charged <= 0; + $paid = false; + if (isset($row['event_paid'])) { + $paid = (bool) $row['event_paid']; + } + + return [ + 'id' => (int) ($row['id'] ?? 0), + 'charge_type' => 'event', + 'event_id' => isset($row['event_id']) ? (int) $row['event_id'] : null, + 'event_name' => $row['event_name'] ?? null, + 'parent_id' => isset($row['parent_id']) ? (int) $row['parent_id'] : null, + 'student_id' => isset($row['student_id']) ? (int) $row['student_id'] : null, + 'school_year' => $row['school_year'] ?? null, + 'semester' => $row['semester'] ?? null, + 'participation_status' => $row['participation'] ?? null, + 'charge_amount' => $charged, + 'payment_status' => $paid ? 'paid' : ($canceled ? 'canceled' : 'unpaid'), + 'cancellation_status' => $canceled ? 'canceled' : 'active', + 'event_date' => $row['updated_at'] ?? $row['created_at'] ?? null, + ]; + } +} diff --git a/app/Services/Fees/FeeRefundCalculatorService.php b/app/Services/Fees/FeeRefundCalculatorService.php index f314bd1d..65cc299e 100644 --- a/app/Services/Fees/FeeRefundCalculatorService.php +++ b/app/Services/Fees/FeeRefundCalculatorService.php @@ -22,82 +22,198 @@ class FeeRefundCalculatorService $totalPaid = Payment::getTotalPaidByParentId($parentId, $schoolYear); if ($totalPaid <= 0) { - return $this->resultPayload(0.0, $totalPaid, $refundDeadline, $schoolEndDate, $weeksOfStudy, 0); + Log::info('Refund skipped: parent has no payments', [ + 'parent_id' => $parentId, + 'school_year' => $schoolYear, + ]); + return $this->resultPayload(0.0, $totalPaid, $refundDeadline, $schoolEndDate, $weeksOfStudy, 0, []); } - $registered = []; - $withdrawn = []; - - foreach ($students as $student) { - $status = strtolower((string) ($student['enrollment_status'] ?? '')); - $admission = strtolower((string) ($student['admission_status'] ?? '')); - - if (in_array($status, ['withdrawn', 'refund pending', 'withdraw under review'], true)) { - $withdrawn[] = $student; - continue; - } - - if (in_array($status, ['enrolled', 'payment pending'], true) && $admission === 'accepted') { - $registered[] = $student; - } - } + [$registered, $withdrawn] = $this->partitionStudents($students); if (empty($withdrawn)) { - return $this->resultPayload(0.0, $totalPaid, $refundDeadline, $schoolEndDate, $weeksOfStudy, 0); + Log::info('Refund skipped: no withdrawn students found', [ + 'parent_id' => $parentId, + 'registered_count' => count($registered), + ]); + return $this->resultPayload(0.0, $totalPaid, $refundDeadline, $schoolEndDate, $weeksOfStudy, 0, []); } - $allStudents = array_merge($registered, $withdrawn); - $allStudents = $this->studentFees->assignFees($allStudents); - - $feeByStudentId = []; - foreach ($allStudents as $student) { - $key = $student['student_id'] ?? null; - if ($key === null) { - continue; - } - $feeByStudentId[(string) $key] = (float) ($student['tuition_fee'] ?? 0); + // Per the school billing plan (section 8), each student must carry the + // tuition fee that was assigned to them during fee calculation BEFORE + // the refund loop runs. We mark withdrawn vs registered before merging + // so the fee tier (first/second regular) is computed against the full + // family and we can still iterate only the withdrawn list afterwards. + foreach ($registered as &$student) { + $student['_is_withdrawn'] = false; } + unset($student); + foreach ($withdrawn as &$student) { + $student['_is_withdrawn'] = true; + } + unset($student); + + $allStudents = $this->studentFees->assignFees(array_merge($registered, $withdrawn)); + + $withdrawnWithFees = array_values(array_filter( + $allStudents, + fn (array $student): bool => !empty($student['_is_withdrawn']) + )); $refundAmount = 0.0; $withdrawCount = 0; + $studentBreakdown = []; - foreach ($withdrawn as $student) { + foreach ($withdrawnWithFees as $student) { + $studentId = $student['student_id'] ?? null; + $studentFee = (float) ($student['tuition_fee'] ?? 0); $withdrawalDate = $student['withdrawal_date'] ?? null; + + $entry = [ + 'student_id' => $studentId, + 'grade' => $student['grade'] ?? null, + 'grade_level' => $student['grade_level'] ?? null, + 'fee_category' => $student['fee_category'] ?? null, + 'tuition_fee' => round($studentFee, 2), + 'withdrawal_date' => $withdrawalDate, + 'weeks_remaining' => 0, + 'refund_amount' => 0.0, + 'eligible' => false, + 'reason' => null, + ]; + if (!$withdrawalDate) { - Log::warning('Missing withdraw date for student', ['student_id' => $student['student_id'] ?? null]); + $entry['reason'] = 'missing_withdrawal_date'; + Log::warning('Refund skipped for student: missing withdrawal date', [ + 'parent_id' => $parentId, + 'student_id' => $studentId, + ]); + $studentBreakdown[] = $entry; continue; } if (!$refundDeadline || strtotime($withdrawalDate) > strtotime($refundDeadline)) { + $entry['reason'] = 'after_refund_deadline'; + $studentBreakdown[] = $entry; continue; } if (!$schoolEndDate) { + $entry['reason'] = 'missing_school_end_date'; + Log::warning('Refund skipped for student: missing school end date', [ + 'parent_id' => $parentId, + 'student_id' => $studentId, + ]); + $studentBreakdown[] = $entry; continue; } - $withdrawDateObj = new \DateTime(date('Y-m-d', strtotime($withdrawalDate))); - $schoolEndDateObj = new \DateTime($schoolEndDate); - $daysRemaining = $withdrawDateObj->diff($schoolEndDateObj)->days; - $weeksRemaining = min($weeksOfStudy, max(0, (int) ceil($daysRemaining / 7))); - - $studentId = (string) ($student['student_id'] ?? ''); - $studentFee = $feeByStudentId[$studentId] ?? 0.0; - if ($studentFee <= 0 || $weeksOfStudy <= 0) { + $entry['reason'] = 'zero_fee_or_weeks'; + Log::warning('Refund skipped for student: zero fee or weeks_of_study', [ + 'parent_id' => $parentId, + 'student_id' => $studentId, + 'student_fee' => $studentFee, + 'weeks_of_study' => $weeksOfStudy, + ]); + $studentBreakdown[] = $entry; continue; } + $weeksRemaining = $this->weeksRemaining($withdrawalDate, $schoolEndDate, $weeksOfStudy); + + // Plan section 7: Refund Amount = Student Tuition Fee / Weeks of Study * Weeks Remaining $proportionalRefund = ($studentFee / $weeksOfStudy) * $weeksRemaining; - $refundAmount += $proportionalRefund; - $withdrawCount++; + $proportionalRefund = round(max(0.0, $proportionalRefund), 2); + + $entry['weeks_remaining'] = $weeksRemaining; + $entry['refund_amount'] = $proportionalRefund; + $entry['eligible'] = $proportionalRefund > 0; + if ($entry['eligible']) { + $withdrawCount++; + $refundAmount += $proportionalRefund; + } else { + $entry['reason'] = 'no_weeks_remaining'; + } + + $studentBreakdown[] = $entry; } + $uncappedRefund = $refundAmount; if ($refundAmount > $totalPaid) { + Log::info('Refund capped at total paid', [ + 'parent_id' => $parentId, + 'uncapped' => round($uncappedRefund, 2), + 'capped' => round($totalPaid, 2), + ]); $refundAmount = $totalPaid; + + // Scale per-student refunds proportionally so the breakdown + // adds up to the capped family total. + if ($uncappedRefund > 0) { + $ratio = $totalPaid / $uncappedRefund; + foreach ($studentBreakdown as &$row) { + if (!empty($row['eligible'])) { + $row['refund_amount'] = round($row['refund_amount'] * $ratio, 2); + } + } + unset($row); + } } - return $this->resultPayload($refundAmount, $totalPaid, $refundDeadline, $schoolEndDate, $weeksOfStudy, $withdrawCount); + Log::info('Refund calculation complete', [ + 'parent_id' => $parentId, + 'school_year' => $schoolYear, + 'withdrawn_total' => count($withdrawnWithFees), + 'withdrawn_eligible' => $withdrawCount, + 'refund_amount' => round($refundAmount, 2), + 'total_paid' => round($totalPaid, 2), + ]); + + return $this->resultPayload( + $refundAmount, + $totalPaid, + $refundDeadline, + $schoolEndDate, + $weeksOfStudy, + $withdrawCount, + $studentBreakdown + ); + } + + /** + * @return array{0: array>, 1: array>} + */ + private function partitionStudents(array $students): array + { + $registered = []; + $withdrawn = []; + + foreach ($students as $student) { + if ($this->studentFees->isWithdrawn($student)) { + $withdrawn[] = $student; + continue; + } + + if ($this->studentFees->isBillable($student)) { + $registered[] = $student; + } + } + + return [$registered, $withdrawn]; + } + + private function weeksRemaining(string $withdrawalDate, string $schoolEndDate, float $weeksOfStudy): int + { + $withdrawDateObj = new \DateTime(date('Y-m-d', strtotime($withdrawalDate))); + $schoolEndDateObj = new \DateTime($schoolEndDate); + + if ($withdrawDateObj > $schoolEndDateObj) { + return 0; + } + + $daysRemaining = $withdrawDateObj->diff($schoolEndDateObj)->days; + return (int) min($weeksOfStudy, max(0, (int) ceil($daysRemaining / 7))); } private function resultPayload( @@ -106,7 +222,8 @@ class FeeRefundCalculatorService ?string $refundDeadline, ?string $schoolEndDate, float $weeksOfStudy, - int $withdrawCount + int $withdrawCount, + array $students ): array { return [ 'refund_amount' => round($refund, 2), @@ -115,6 +232,7 @@ class FeeRefundCalculatorService 'school_end_date' => $schoolEndDate, 'weeks_of_study' => $weeksOfStudy, 'withdrawn_students' => $withdrawCount, + 'students' => $students, ]; } } diff --git a/app/Services/Fees/FeeStudentFeeService.php b/app/Services/Fees/FeeStudentFeeService.php index 81560e29..546d92e1 100644 --- a/app/Services/Fees/FeeStudentFeeService.php +++ b/app/Services/Fees/FeeStudentFeeService.php @@ -3,29 +3,46 @@ namespace App\Services\Fees; use App\Models\ClassSection; +use Illuminate\Support\Facades\Log; class FeeStudentFeeService { + public const CATEGORY_FIRST_REGULAR = 'first_regular'; + public const CATEGORY_ADDITIONAL_REGULAR = 'additional_regular'; + public const CATEGORY_YOUTH = 'youth'; + public const CATEGORY_NOT_BILLABLE = 'not_billable'; + + private const BILLABLE_ENROLLMENT_STATUSES = ['enrolled', 'payment pending']; + private const WITHDRAWN_ENROLLMENT_STATUSES = ['withdrawn', 'refund pending', 'withdraw under review']; + private const ACCEPTED_ADMISSION_STATUS = 'accepted'; + public function __construct( private FeeGradeService $grades, private FeeConfigService $config ) { } + /** + * Normalize grades, sort by grade level, and assign a tuition fee to each + * student so that downstream callers (refund, balance, invoice) can read + * the stored fee directly from the student record. + * + * Each student gets the following keys mutated/added: + * - grade (normalized) + * - grade_level + * - fee_category (first_regular | additional_regular | youth) + * - tuition_fee + */ public function assignFees(array $students): array { $fees = $this->config->getFees(); - $firstFee = $fees['first_student_fee']; - $secondFee = $fees['second_student_fee']; - $youthFee = $fees['youth_fee']; + $firstFee = (float) $fees['first_student_fee']; + $secondFee = (float) $fees['second_student_fee']; + $youthFee = (float) $fees['youth_fee']; foreach ($students as &$student) { - $grade = $student['grade'] ?? null; - if ($grade === null || trim((string) $grade) === '') { - $sectionId = $student['class_section_id'] ?? null; - $grade = $sectionId ? ClassSection::getClassSectionNameBySectionId($sectionId) : null; - } - $student['grade'] = $this->grades->normalizeGrade($grade); + $student['grade'] = $this->resolveGrade($student); + $student['grade_level'] = $this->grades->getGradeLevel($student['grade']); } unset($student); @@ -35,11 +52,18 @@ class FeeStudentFeeService $regularCount = 0; foreach ($students as &$student) { - $gradeLevel = $this->grades->getGradeLevel($student['grade'] ?? ''); + $gradeLevel = (int) ($student['grade_level'] ?? 999); if ($gradeLevel > 9) { + $student['fee_category'] = self::CATEGORY_YOUTH; $student['tuition_fee'] = $youthFee; } else { - $student['tuition_fee'] = ($regularCount === 0) ? $firstFee : $secondFee; + if ($regularCount === 0) { + $student['fee_category'] = self::CATEGORY_FIRST_REGULAR; + $student['tuition_fee'] = $firstFee; + } else { + $student['fee_category'] = self::CATEGORY_ADDITIONAL_REGULAR; + $student['tuition_fee'] = $secondFee; + } $regularCount++; } } @@ -59,4 +83,126 @@ class FeeStudentFeeService return $total; } + + /** + * Return a detailed student-level + family-level tuition breakdown. + * + * Only students that are accepted and either enrolled or payment-pending + * are billed. Withdrawn students appear in the breakdown with a zero + * tuition fee and the not_billable category so callers (refund service, + * invoice generation) can still see them in one consistent payload. + * + * @return array{ + * family_total: float, + * billable_count: int, + * non_billable_count: int, + * students: array>, + * fees: array + * } + */ + public function calculateBreakdown(array $students): array + { + $fees = $this->config->getFees(); + + $billable = []; + $nonBillable = []; + + foreach ($students as $index => $student) { + $student['_original_index'] = $index; + $student['grade'] = $this->resolveGrade($student); + $student['grade_level'] = $this->grades->getGradeLevel($student['grade']); + $student['enrollment_status'] = isset($student['enrollment_status']) + ? strtolower((string) $student['enrollment_status']) + : null; + $student['admission_status'] = isset($student['admission_status']) + ? strtolower((string) $student['admission_status']) + : null; + + if ($this->isBillable($student)) { + $billable[] = $student; + } else { + $nonBillable[] = $student; + } + } + + $billable = $this->assignFees($billable); + + $nonBillable = array_map(function (array $student): array { + $student['fee_category'] = self::CATEGORY_NOT_BILLABLE; + $student['tuition_fee'] = 0.0; + return $student; + }, $nonBillable); + + $combined = array_merge($billable, $nonBillable); + usort($combined, fn ($a, $b) => ($a['_original_index'] ?? 0) <=> ($b['_original_index'] ?? 0)); + + $familyTotal = 0.0; + $studentRows = []; + foreach ($combined as $student) { + unset($student['_original_index']); + + $tuitionFee = (float) ($student['tuition_fee'] ?? 0); + $discount = (float) ($student['discount_amount'] ?? 0); + $finalTuition = max(0.0, $tuitionFee - $discount); + $familyTotal += $finalTuition; + + $studentRows[] = [ + 'student_id' => $student['student_id'] ?? null, + 'student_name' => trim(($student['firstname'] ?? '') . ' ' . ($student['lastname'] ?? '')), + 'grade' => $student['grade'] ?? null, + 'grade_level' => $student['grade_level'] ?? null, + 'enrollment_status' => $student['enrollment_status'] ?? null, + 'admission_status' => $student['admission_status'] ?? null, + 'fee_category' => $student['fee_category'] ?? null, + 'tuition_fee' => round($tuitionFee, 2), + 'discount_amount' => round($discount, 2), + 'final_tuition_amount' => round($finalTuition, 2), + ]; + } + + Log::info('Tuition breakdown calculated', [ + 'total_students' => count($studentRows), + 'billable' => count($billable), + 'non_billable' => count($nonBillable), + 'family_total' => round($familyTotal, 2), + ]); + + return [ + 'family_total' => round($familyTotal, 2), + 'billable_count' => count($billable), + 'non_billable_count' => count($nonBillable), + 'students' => $studentRows, + 'fees' => [ + 'first_student_fee' => (float) $fees['first_student_fee'], + 'second_student_fee' => (float) $fees['second_student_fee'], + 'youth_fee' => (float) $fees['youth_fee'], + ], + ]; + } + + public function isBillable(array $student): bool + { + $status = strtolower((string) ($student['enrollment_status'] ?? '')); + $admission = strtolower((string) ($student['admission_status'] ?? '')); + + return in_array($status, self::BILLABLE_ENROLLMENT_STATUSES, true) + && $admission === self::ACCEPTED_ADMISSION_STATUS; + } + + public function isWithdrawn(array $student): bool + { + $status = strtolower((string) ($student['enrollment_status'] ?? '')); + return in_array($status, self::WITHDRAWN_ENROLLMENT_STATUSES, true); + } + + private function resolveGrade(array $student): string + { + $grade = $student['grade'] ?? null; + if ($grade === null || trim((string) $grade) === '') { + $sectionId = $student['class_section_id'] ?? null; + $grade = $sectionId ? ClassSection::getClassSectionNameBySectionId($sectionId) : null; + } + + return $this->grades->normalizeGrade($grade); + } } diff --git a/app/Services/Fees/TuitionCalculationService.php b/app/Services/Fees/TuitionCalculationService.php new file mode 100644 index 00000000..cfeceb33 --- /dev/null +++ b/app/Services/Fees/TuitionCalculationService.php @@ -0,0 +1,73 @@ +, + * family_total: float, + * billable_count: int, + * non_billable_count: int, + * students: array> + * } + */ + public function calculate(array $students): array + { + $schoolYear = $this->config->getSchoolYear(); + $breakdown = $this->studentFees->calculateBreakdown($students); + + Log::info('Tuition calculation requested', [ + 'school_year' => $schoolYear, + 'student_count' => count($students), + 'family_total' => $breakdown['family_total'], + ]); + + return [ + 'school_year' => $schoolYear, + 'fees' => $breakdown['fees'], + 'family_total' => $breakdown['family_total'], + 'billable_count' => $breakdown['billable_count'], + 'non_billable_count' => $breakdown['non_billable_count'], + 'students' => $breakdown['students'], + ]; + } + + /** + * Convenience helper: the family-level tuition total only. + */ + public function familyTotal(array $students): float + { + return $this->calculate($students)['family_total']; + } +} diff --git a/app/Services/Parents/AuthorizedUsersManagementService.php b/app/Services/Parents/AuthorizedUsersManagementService.php index d764635a..f2f4f484 100644 --- a/app/Services/Parents/AuthorizedUsersManagementService.php +++ b/app/Services/Parents/AuthorizedUsersManagementService.php @@ -27,6 +27,9 @@ class AuthorizedUsersManagementService /** * CI create() first step — lookup by token after invite email (pending row, created within TTL). + * + * Tokens are stored as SHA-256 hashes; we only ever look up by hash so that + * a leaked database row cannot be replayed as a plaintext token. */ public function findPendingByIncomingToken(string $plainToken): ?AuthorizedUser { @@ -34,12 +37,8 @@ class AuthorizedUsersManagementService return null; } - $hash = $this->hashToken($plainToken); - return AuthorizedUser::query() - ->where(function ($q) use ($hash, $plainToken) { - $q->where('token', $hash)->orWhere('token', $plainToken); - }) + ->where('token', $this->hashToken($plainToken)) ->where('created_at', '>=', now()->subHours(self::TOKEN_TTL_HOURS)) ->first(); } @@ -49,16 +48,12 @@ class AuthorizedUsersManagementService */ public function findActiveSetupRow(int $authorizedUserId, string $plainToken): ?AuthorizedUser { - if ($plainToken === '') { + if ($plainToken === '' || $authorizedUserId <= 0) { return null; } - $hash = $this->hashToken($plainToken); - return AuthorizedUser::query() - ->where(function ($q) use ($hash, $plainToken) { - $q->where('token', $hash)->orWhere('token', $plainToken); - }) + ->where('token', $this->hashToken($plainToken)) ->where('authorized_user_id', $authorizedUserId) ->where('status', 'Active') ->where('updated_at', '>=', now()->subHours(self::TOKEN_TTL_HOURS)) @@ -90,6 +85,9 @@ class AuthorizedUsersManagementService /** * CI `create` — when email is not a registered user, respond success without leaking (privacy). * + * Refuses self-invites and silently re-uses an existing invite row to avoid + * letting a parent spam another user with confirmation emails. + * * @return array{created: bool, record: AuthorizedUser|null} */ public function inviteByEmail(int $primaryUserId, string $email): array @@ -104,22 +102,49 @@ class AuthorizedUsersManagementService return ['created' => false, 'record' => null]; } + // A parent cannot invite themselves as their own authorized user. + if ((int) $user->id === $primaryUserId) { + return ['created' => false, 'record' => null]; + } + + // If the same parent already invited this user, rotate the token instead + // of creating a duplicate row (prevents spam + race-driven duplicates). + $existing = AuthorizedUser::query() + ->where('user_id', $primaryUserId) + ->where('authorized_user_id', (int) $user->id) + ->first(); + $plain = bin2hex(random_bytes(48)); $tokenHash = $this->hashToken($plain); $phone = trim((string) ($user->cellphone ?? '')); - $record = AuthorizedUser::query()->create([ - 'user_id' => $primaryUserId, - 'authorized_user_id' => (int) $user->id, - 'firstname' => (string) ($user->firstname ?? ''), - 'lastname' => (string) ($user->lastname ?? ''), - 'phone_number' => $phone !== '' ? $phone : '-', - 'gender' => trim((string) ($user->gender ?? '')) !== '' ? (string) $user->gender : '-', - 'email' => $email, - 'relation_to_user' => null, - 'token' => $tokenHash, - 'status' => 'Pending', - ]); + $gender = trim((string) ($user->gender ?? '')); + + if ($existing) { + $existing->update([ + 'firstname' => (string) ($user->firstname ?? ''), + 'lastname' => (string) ($user->lastname ?? ''), + 'phone_number' => $phone !== '' ? $phone : '-', + 'gender' => $gender !== '' ? $gender : '-', + 'email' => $email, + 'token' => $tokenHash, + 'status' => 'Pending', + ]); + $record = $existing; + } else { + $record = AuthorizedUser::query()->create([ + 'user_id' => $primaryUserId, + 'authorized_user_id' => (int) $user->id, + 'firstname' => (string) ($user->firstname ?? ''), + 'lastname' => (string) ($user->lastname ?? ''), + 'phone_number' => $phone !== '' ? $phone : '-', + 'gender' => $gender !== '' ? $gender : '-', + 'email' => $email, + 'relation_to_user' => null, + 'token' => $tokenHash, + 'status' => 'Pending', + ]); + } $this->sendConfirmationEmail($email, $plain); diff --git a/app/Services/Parents/ParentEnrollmentService.php b/app/Services/Parents/ParentEnrollmentService.php index 13dcf6e2..7b5021ac 100644 --- a/app/Services/Parents/ParentEnrollmentService.php +++ b/app/Services/Parents/ParentEnrollmentService.php @@ -107,10 +107,34 @@ class ParentEnrollmentService $schoolYear = $context['school_year']; $semester = $context['semester']; + // Authorize every submitted student id against this parent up-front to + // prevent IDOR — a parent must not be able to enroll or withdraw + // another family's students by passing arbitrary ids. + $requestedIds = array_values(array_unique(array_map( + 'intval', + array_merge($enrollIds, $withdrawIds) + ))); + $requestedIds = array_values(array_filter($requestedIds, static fn ($id) => $id > 0)); + + $ownedIds = $requestedIds === [] + ? [] + : Student::query() + ->where('parent_id', $parentId) + ->whereIn('id', $requestedIds) + ->pluck('id') + ->map(fn ($id) => (int) $id) + ->all(); + $ownedSet = array_flip($ownedIds); + $enrolled = []; $withdrawn = []; foreach ($enrollIds as $studentId) { + $studentId = (int) $studentId; + if (! isset($ownedSet[$studentId])) { + continue; + } + $existing = Enrollment::query() ->where('student_id', $studentId) ->where('school_year', $schoolYear) @@ -140,12 +164,18 @@ class ParentEnrollmentService ]); } - $enrolled[] = (int) $studentId; + $enrolled[] = $studentId; } foreach ($withdrawIds as $studentId) { + $studentId = (int) $studentId; + if (! isset($ownedSet[$studentId])) { + continue; + } + $enrollment = Enrollment::query() ->where('student_id', $studentId) + ->where('parent_id', $parentId) ->where('school_year', $schoolYear) ->where('semester', $semester) ->where('is_withdrawn', 0) @@ -195,7 +225,7 @@ class ParentEnrollmentService } } - $withdrawn[] = (int) $studentId; + $withdrawn[] = $studentId; } return [ diff --git a/app/Services/Parents/ParentEventParticipationService.php b/app/Services/Parents/ParentEventParticipationService.php index 4e90a896..e1bb7b57 100644 --- a/app/Services/Parents/ParentEventParticipationService.php +++ b/app/Services/Parents/ParentEventParticipationService.php @@ -5,6 +5,7 @@ namespace App\Services\Parents; use App\Models\Event; use App\Models\EventCharges; use App\Models\Enrollment; +use App\Models\Student; use App\Services\Invoices\InvoiceGenerationService; class ParentEventParticipationService @@ -48,15 +49,61 @@ class ParentEventParticipationService $schoolYear = $context['school_year']; $semester = $context['semester']; + // First pass: parse + validate every `student_id:event_id` key and + // collect the student ids that this parent actually owns. This stops + // a parent from creating or deleting charges on another family's + // students by manipulating the participation keys. + $parsed = []; + $candidateStudentIds = []; foreach ($participations as $key => $value) { - [$studentId, $eventId] = array_map('intval', explode(':', (string) $key)); + $parts = explode(':', (string) $key, 2); + if (count($parts) !== 2 || ! ctype_digit($parts[0]) || ! ctype_digit($parts[1])) { + continue; + } + + $studentId = (int) $parts[0]; + $eventId = (int) $parts[1]; + if ($studentId <= 0 || $eventId <= 0) { + continue; + } + + $normalized = strtolower(trim((string) $value)); + if (! in_array($normalized, ['yes', 'no'], true)) { + continue; + } + + $parsed[] = [ + 'student_id' => $studentId, + 'event_id' => $eventId, + 'value' => $normalized, + ]; + $candidateStudentIds[$studentId] = true; + } + + if ($parsed === []) { + return; + } + + $ownedIds = Student::query() + ->where('parent_id', $parentId) + ->whereIn('id', array_keys($candidateStudentIds)) + ->pluck('id') + ->map(fn ($id) => (int) $id) + ->all(); + $ownedSet = array_flip($ownedIds); + + foreach ($parsed as $row) { + if (! isset($ownedSet[$row['student_id']])) { + continue; + } + $existing = EventCharges::query() ->where('parent_id', $parentId) - ->where('student_id', $studentId) - ->where('event_id', $eventId) + ->where('student_id', $row['student_id']) + ->where('event_id', $row['event_id']) ->first(); - if ($value === 'no') { + if ($row['value'] === 'no') { if ($existing) { $existing->delete(); } @@ -64,14 +111,14 @@ class ParentEventParticipationService } if ($existing) { - $existing->update(['participation' => $value]); + $existing->update(['participation' => $row['value']]); } else { - $event = Event::getEvent($eventId, $schoolYear); + $event = Event::getEvent($row['event_id'], $schoolYear); EventCharges::query()->create([ 'parent_id' => $parentId, - 'student_id' => $studentId, - 'event_id' => $eventId, - 'participation' => $value, + 'student_id' => $row['student_id'], + 'event_id' => $row['event_id'], + 'participation' => $row['value'], 'charged' => $event['amount'] ?? 0, 'school_year' => $schoolYear, 'semester' => $semester, diff --git a/app/Services/Parents/ParentRegistrationService.php b/app/Services/Parents/ParentRegistrationService.php index 9b63a564..fc4c4e31 100644 --- a/app/Services/Parents/ParentRegistrationService.php +++ b/app/Services/Parents/ParentRegistrationService.php @@ -74,6 +74,7 @@ class ParentRegistrationService } $createdStudentIds = []; + $skippedStudents = []; DB::transaction(function () use ( $parentId, @@ -81,17 +82,28 @@ class ParentRegistrationService $emergencyContacts, $schoolYear, $semester, - &$createdStudentIds + &$createdStudentIds, + &$skippedStudents ) { foreach ($students as $student) { + // Duplicate check is scoped to THIS parent: one family's + // legitimately registered child must not block another family + // from registering a child with the same name and DOB. $exists = Student::query() + ->where('parent_id', $parentId) ->where('school_year', $schoolYear) ->where('dob', $student['dob']) - ->where('firstname', $student['firstname']) - ->where('lastname', $student['lastname']) + ->whereRaw('LOWER(TRIM(firstname)) = ?', [strtolower(trim((string) $student['firstname']))]) + ->whereRaw('LOWER(TRIM(lastname)) = ?', [strtolower(trim((string) $student['lastname']))]) ->first(); if ($exists) { + $skippedStudents[] = [ + 'firstname' => $student['firstname'], + 'lastname' => $student['lastname'], + 'dob' => $student['dob'], + 'reason' => 'already_registered', + ]; continue; } @@ -153,6 +165,7 @@ class ParentRegistrationService return [ 'created_student_ids' => $createdStudentIds, + 'skipped' => $skippedStudents, ]; } diff --git a/app/Services/Payments/PaymentEventChargesService.php b/app/Services/Payments/PaymentEventChargesService.php index 5c0ccecf..838da34a 100644 --- a/app/Services/Payments/PaymentEventChargesService.php +++ b/app/Services/Payments/PaymentEventChargesService.php @@ -6,10 +6,15 @@ use App\Models\Configuration; use App\Models\Enrollment; use App\Models\StudentClass; use App\Models\User; +use App\Services\Billing\ChargeService; use Illuminate\Support\Facades\DB; class PaymentEventChargesService { + public function __construct(private ChargeService $chargeService) + { + } + public function listCharges(?string $schoolYear, ?string $semester): array { $schoolYear = $schoolYear ?: Configuration::getConfig('school_year'); @@ -44,34 +49,45 @@ class PaymentEventChargesService { $studentIds = $payload['student_ids'] ?? []; $parentId = (int) $payload['parent_id']; - - $commonData = [ - 'parent_id' => $parentId, - 'event_name' => $payload['event_name'], - 'description' => $payload['description'] ?? null, - 'amount' => (float) $payload['amount'], - 'semester' => $payload['semester'], - 'school_year' => $payload['school_year'], - 'charged' => 0, - 'updated_by' => $userId, - 'created_at' => now(), - 'updated_at' => now(), - ]; + $schoolYear = (string) $payload['school_year']; + $semester = (string) $payload['semester']; + $eventName = (string) $payload['event_name']; + $amount = (float) $payload['amount']; $count = 0; if (!empty($studentIds)) { foreach ($studentIds as $studentId) { - DB::table('event_charges')->insert(array_merge($commonData, [ + $result = $this->chargeService->createEventCharge([ + 'parent_id' => $parentId, 'student_id' => (int) $studentId, - ])); - $count++; + 'event_name' => $eventName, + 'amount' => $amount, + 'description' => $payload['description'] ?? null, + 'school_year' => $schoolYear, + 'semester' => $semester, + 'created_by' => $userId, + ]); + + if ($result['ok'] ?? false) { + $count++; + } } } elseif (!empty($payload['student_id'])) { - DB::table('event_charges')->insert(array_merge($commonData, [ + $result = $this->chargeService->createEventCharge([ + 'parent_id' => $parentId, 'student_id' => (int) $payload['student_id'], - ])); - $count++; + 'event_name' => $eventName, + 'amount' => $amount, + 'description' => $payload['description'] ?? null, + 'school_year' => $schoolYear, + 'semester' => $semester, + 'created_by' => $userId, + ]); + + if ($result['ok'] ?? false) { + $count++; + } } return $count; diff --git a/app/Services/Promotions/LevelProgressionService.php b/app/Services/Promotions/LevelProgressionService.php new file mode 100644 index 00000000..60b42182 --- /dev/null +++ b/app/Services/Promotions/LevelProgressionService.php @@ -0,0 +1,202 @@ + 'KG1', 'next' => 'KG2', 'order' => 10, 'terminal' => false], + ['current' => 'KG2', 'next' => 'Grade 1', 'order' => 20, 'terminal' => false], + ['current' => 'Grade 1','next' => 'Grade 2', 'order' => 30, 'terminal' => false], + ['current' => 'Grade 2','next' => 'Grade 3', 'order' => 40, 'terminal' => false], + ['current' => 'Grade 3','next' => 'Grade 4', 'order' => 50, 'terminal' => false], + ['current' => 'Grade 4','next' => 'Grade 5', 'order' => 60, 'terminal' => false], + ['current' => 'Grade 5','next' => 'Grade 6', 'order' => 70, 'terminal' => false], + ['current' => 'Grade 6','next' => 'Grade 7', 'order' => 80, 'terminal' => false], + ['current' => 'Grade 7','next' => 'Grade 8', 'order' => 90, 'terminal' => false], + ['current' => 'Grade 8','next' => 'Grade 9', 'order' => 100, 'terminal' => false], + ['current' => 'Grade 9','next' => 'Youth', 'order' => 110, 'terminal' => false], + ['current' => 'Youth', 'next' => null, 'order' => 120, 'terminal' => true], + ]; + + /** + * Returns all configured level progressions, seeding defaults if the + * table is empty. + * + * @return Collection + */ + public function list(bool $activeOnly = true): Collection + { + $query = LevelProgression::query()->ordered(); + if ($activeOnly) { + $query->active(); + } + $rows = $query->get(); + + if ($rows->isEmpty() && $activeOnly) { + $this->seedDefaults(); + $rows = LevelProgression::query()->ordered()->active()->get(); + } + + return $rows; + } + + /** + * Resolve next-level information for a student based on their current + * `class_id` (i.e. classes.id). Returns null if no progression rule + * matches and no fallback can be derived. + * + * @return array{ + * current_level_id:?int, + * current_level_name:?string, + * next_level_id:?int, + * next_level_name:?string, + * is_terminal:bool + * }|null + */ + public function resolveByCurrentClassId(?int $currentClassId): ?array + { + if ($currentClassId === null || $currentClassId <= 0) { + return null; + } + + $current = SchoolClass::query()->find($currentClassId); + if (!$current) { + return null; + } + $currentName = (string) ($current->class_name ?? ''); + + $progression = LevelProgression::findByCurrentLevelId($currentClassId) + ?: ($currentName !== '' ? LevelProgression::findByCurrentLevelName($currentName) : null); + + if ($progression) { + return [ + 'current_level_id' => $currentClassId, + 'current_level_name' => $currentName !== '' ? $currentName : $progression->current_level_name, + 'next_level_id' => $progression->next_level_id !== null ? (int) $progression->next_level_id : null, + 'next_level_name' => $progression->next_level_name, + 'is_terminal' => (bool) $progression->is_terminal, + ]; + } + + // Legacy fallback: numeric next class id (matches AccountEventService::preparePromotionQueue). + $nextId = $currentClassId + 1; + $next = SchoolClass::query()->find($nextId); + + return [ + 'current_level_id' => $currentClassId, + 'current_level_name' => $currentName !== '' ? $currentName : null, + 'next_level_id' => $next ? (int) $next->id : null, + 'next_level_name' => $next ? (string) $next->class_name : null, + 'is_terminal' => false, + ]; + } + + /** + * Resolve next-level info from a `classSection` row id (the + * "from_class_section_id" used elsewhere). + */ + public function resolveByCurrentClassSectionId(?int $classSectionId): ?array + { + if ($classSectionId === null || $classSectionId <= 0) { + return null; + } + $classId = ClassSection::getClassId($classSectionId); + if (!$classId) { + return null; + } + return $this->resolveByCurrentClassId((int) $classId); + } + + /** + * Seed the table with the default mapping from {@see DEFAULT_MAP}. + * Idempotent – relies on the unique key on `current_level_name`. + */ + public function seedDefaults(): int + { + $created = 0; + foreach (self::DEFAULT_MAP as $row) { + $existing = LevelProgression::query() + ->whereRaw('LOWER(current_level_name) = ?', [strtolower($row['current'])]) + ->first(); + if ($existing) { + continue; + } + + $currentClassId = $this->resolveClassIdByName($row['current']); + $nextClassId = isset($row['next']) && $row['next'] !== null + ? $this->resolveClassIdByName($row['next']) + : null; + + LevelProgression::query()->create([ + 'current_level_id' => $currentClassId, + 'current_level_name' => $row['current'], + 'next_level_id' => $nextClassId, + 'next_level_name' => $row['next'] ?? null, + 'order_index' => (int) ($row['order'] ?? 0), + 'is_terminal' => (bool) ($row['terminal'] ?? false), + 'is_active' => true, + ]); + $created++; + } + + return $created; + } + + public function upsertMapping(array $payload): LevelProgression + { + $currentName = (string) ($payload['current_level_name'] ?? ''); + if ($currentName === '') { + throw new \InvalidArgumentException('current_level_name is required'); + } + + $existing = LevelProgression::findByCurrentLevelName($currentName); + $data = [ + 'current_level_id' => isset($payload['current_level_id']) ? (int) $payload['current_level_id'] : ($existing->current_level_id ?? null), + 'current_level_name' => $currentName, + 'next_level_id' => isset($payload['next_level_id']) ? (int) $payload['next_level_id'] : null, + 'next_level_name' => $payload['next_level_name'] ?? null, + 'order_index' => isset($payload['order_index']) ? (int) $payload['order_index'] : ($existing->order_index ?? 0), + 'is_terminal' => (bool) ($payload['is_terminal'] ?? ($existing->is_terminal ?? false)), + 'is_active' => array_key_exists('is_active', $payload) ? (bool) $payload['is_active'] : ($existing->is_active ?? true), + 'notes' => $payload['notes'] ?? ($existing->notes ?? null), + ]; + + if ($existing) { + $existing->fill($data); + $existing->save(); + return $existing; + } + + return LevelProgression::query()->create($data); + } + + private function resolveClassIdByName(?string $name): ?int + { + if ($name === null || $name === '') { + return null; + } + $row = SchoolClass::query() + ->whereRaw('LOWER(class_name) = ?', [strtolower(trim($name))]) + ->orderByDesc('id') + ->first(); + + return $row ? (int) $row->id : null; + } +} diff --git a/app/Services/Promotions/PromotionAuditService.php b/app/Services/Promotions/PromotionAuditService.php new file mode 100644 index 00000000..a7082183 --- /dev/null +++ b/app/Services/Promotions/PromotionAuditService.php @@ -0,0 +1,153 @@ +log($record, PromotionAuditLog::ACTION_RECORD_CREATED, [ + 'user_id' => $userId, + 'new_value' => $record->promotion_status, + 'notes' => $notes, + ]); + } + + public function logStatusChange( + StudentPromotionRecord $record, + ?string $oldStatus, + string $newStatus, + ?int $userId, + ?string $notes = null + ): PromotionAuditLog { + return $this->log($record, PromotionAuditLog::ACTION_STATUS_CHANGED, [ + 'user_id' => $userId, + 'field' => 'promotion_status', + 'old_value' => $oldStatus, + 'new_value' => $newStatus, + 'notes' => $notes, + ]); + } + + public function logEligibilityEvaluation( + StudentPromotionRecord $record, + bool $passed, + ?int $userId, + ?string $notes = null + ): PromotionAuditLog { + return $this->log($record, PromotionAuditLog::ACTION_ELIGIBILITY_EVALUATED, [ + 'user_id' => $userId, + 'field' => 'passed_current_level', + 'new_value' => $passed ? '1' : '0', + 'notes' => $notes, + ]); + } + + public function logParentNotified(StudentPromotionRecord $record, ?int $userId, ?string $notes = null): PromotionAuditLog + { + return $this->log($record, PromotionAuditLog::ACTION_PARENT_NOTIFIED, [ + 'user_id' => $userId, + 'notes' => $notes, + ]); + } + + public function logEnrollmentStepCompleted( + StudentPromotionRecord $record, + string $field, + ?int $userId, + ?string $notes = null + ): PromotionAuditLog { + return $this->log($record, PromotionAuditLog::ACTION_ENROLLMENT_STEP_COMPLETED, [ + 'user_id' => $userId, + 'field' => $field, + 'new_value' => '1', + 'notes' => $notes, + ]); + } + + public function logEnrollmentStarted(StudentPromotionRecord $record, ?int $userId): PromotionAuditLog + { + return $this->log($record, PromotionAuditLog::ACTION_ENROLLMENT_STARTED, [ + 'user_id' => $userId, + ]); + } + + public function logEnrollmentCompleted(StudentPromotionRecord $record, ?int $userId): PromotionAuditLog + { + return $this->log($record, PromotionAuditLog::ACTION_ENROLLMENT_COMPLETED, [ + 'user_id' => $userId, + ]); + } + + public function logPromotionFinalized(StudentPromotionRecord $record, ?int $userId): PromotionAuditLog + { + return $this->log($record, PromotionAuditLog::ACTION_PROMOTION_FINALIZED, [ + 'user_id' => $userId, + 'new_value' => $record->promotion_status, + ]); + } + + public function logDeadlineSet( + StudentPromotionRecord $record, + ?string $oldDeadline, + ?string $newDeadline, + ?int $userId + ): PromotionAuditLog { + return $this->log($record, PromotionAuditLog::ACTION_DEADLINE_SET, [ + 'user_id' => $userId, + 'field' => 'enrollment_deadline', + 'old_value' => $oldDeadline, + 'new_value' => $newDeadline, + ]); + } + + public function logReminderSent( + StudentPromotionRecord $record, + string $reminderType, + ?int $userId + ): PromotionAuditLog { + return $this->log($record, PromotionAuditLog::ACTION_REMINDER_SENT, [ + 'user_id' => $userId, + 'field' => 'reminder_type', + 'new_value' => $reminderType, + ]); + } + + public function logDeadlineExpired(StudentPromotionRecord $record): PromotionAuditLog + { + return $this->log($record, PromotionAuditLog::ACTION_DEADLINE_EXPIRED, []); + } + + public function logManualOverride( + StudentPromotionRecord $record, + string $field, + ?string $oldValue, + ?string $newValue, + ?int $userId, + ?string $notes = null + ): PromotionAuditLog { + return $this->log($record, PromotionAuditLog::ACTION_MANUAL_OVERRIDE, [ + 'user_id' => $userId, + 'field' => $field, + 'old_value' => $oldValue, + 'new_value' => $newValue, + 'notes' => $notes, + ]); + } + + private function log(StudentPromotionRecord $record, string $action, array $extra): PromotionAuditLog + { + return PromotionAuditLog::query()->create(array_merge([ + 'promotion_id' => (int) $record->getKey(), + 'student_id' => (int) $record->student_id, + 'action_type' => $action, + 'performed_at' => now(), + ], $extra)); + } +} diff --git a/app/Services/Promotions/PromotionEligibilityService.php b/app/Services/Promotions/PromotionEligibilityService.php new file mode 100644 index 00000000..1c777cda --- /dev/null +++ b/app/Services/Promotions/PromotionEligibilityService.php @@ -0,0 +1,393 @@ +find($studentId); + if (!$student) { + return null; + } + + $current = $currentSchoolYear ?: $this->resolveCurrentSchoolYear(); + if ($current === null) { + return null; + } + $next = $this->nextSchoolYear($current); + if ($next === null) { + return null; + } + + return $this->evaluateOne($student, $current, $next, $userId); + } + + /** + * Evaluate eligibility for a class section worth of students at once. + * + * @return array{ evaluated:int, eligible:int, repeated:int, on_hold:int, conditional:int, graduated:int, errors:array } + */ + public function evaluateClassSection( + int $classSectionId, + ?string $currentSchoolYear = null, + ?int $userId = null + ): array { + $current = $currentSchoolYear ?: $this->resolveCurrentSchoolYear(); + if ($current === null) { + return $this->emptySummary(); + } + $next = $this->nextSchoolYear($current); + if ($next === null) { + return $this->emptySummary(); + } + + $studentIds = DB::table('student_class') + ->where('class_section_id', $classSectionId) + ->where('school_year', $current) + ->pluck('student_id') + ->map(fn ($id) => (int) $id) + ->all(); + + $summary = $this->emptySummary(); + foreach ($studentIds as $studentId) { + $student = Student::query()->find($studentId); + if (!$student) { + continue; + } + try { + $record = $this->evaluateOne($student, $current, $next, $userId); + if ($record) { + $summary['evaluated']++; + $key = $this->statusBucket($record->promotion_status); + if ($key !== null) { + $summary[$key]++; + } + } + } catch (\Throwable $e) { + Log::error('Promotion eligibility evaluation failed', [ + 'student_id' => $studentId, + 'exception' => $e, + ]); + $summary['errors'][] = sprintf('Student %d: %s', $studentId, $e->getMessage()); + } + } + + return $summary; + } + + /** + * Evaluate eligibility for an entire school year (all students with + * class assignments). Convenience wrapper around `evaluateOne`. + */ + public function evaluateSchoolYear(?string $currentSchoolYear = null, ?int $userId = null): array + { + $current = $currentSchoolYear ?: $this->resolveCurrentSchoolYear(); + if ($current === null) { + return $this->emptySummary(); + } + $next = $this->nextSchoolYear($current); + if ($next === null) { + return $this->emptySummary(); + } + + $studentIds = DB::table('student_class') + ->where('school_year', $current) + ->distinct() + ->pluck('student_id') + ->map(fn ($id) => (int) $id) + ->all(); + + $summary = $this->emptySummary(); + foreach ($studentIds as $studentId) { + $student = Student::query()->find($studentId); + if (!$student) { + continue; + } + try { + $record = $this->evaluateOne($student, $current, $next, $userId); + if ($record) { + $summary['evaluated']++; + $key = $this->statusBucket($record->promotion_status); + if ($key !== null) { + $summary[$key]++; + } + } + } catch (\Throwable $e) { + Log::error('Promotion eligibility evaluation failed', [ + 'student_id' => $studentId, + 'exception' => $e, + ]); + $summary['errors'][] = sprintf('Student %d: %s', $studentId, $e->getMessage()); + } + } + + return $summary; + } + + /** + * Core per-student logic implementing section 7's decision tree. + */ + public function evaluateOne( + Student $student, + string $currentSchoolYear, + string $nextSchoolYear, + ?int $userId = null + ): StudentPromotionRecord { + $studentId = (int) $student->getKey(); + + $sectionId = (int) DB::table('student_class') + ->where('student_id', $studentId) + ->where('school_year', $currentSchoolYear) + ->orderByDesc('updated_at') + ->orderByDesc('id') + ->value('class_section_id'); + + $progressionInfo = $this->progression->resolveByCurrentClassSectionId($sectionId); + if ($progressionInfo === null && $sectionId > 0) { + // Even without a mapping we still want to record the eligibility decision. + $progressionInfo = [ + 'current_level_id' => null, + 'current_level_name' => ClassSection::getClassSectionNameBySectionId($sectionId), + 'next_level_id' => null, + 'next_level_name' => null, + 'is_terminal' => false, + ]; + } + $progressionInfo ??= [ + 'current_level_id' => null, + 'current_level_name' => $student->registration_grade, + 'next_level_id' => null, + 'next_level_name' => null, + 'is_terminal' => false, + ]; + + $scoreInfo = $this->loadAcademicResult($studentId, $currentSchoolYear); + $passed = $scoreInfo['passed']; + $finalAvg = $scoreInfo['final_average']; + + return DB::transaction(function () use ( + $student, + $studentId, + $currentSchoolYear, + $nextSchoolYear, + $progressionInfo, + $passed, + $finalAvg, + $userId, + $scoreInfo + ) { + $existing = StudentPromotionRecord::query() + ->where('student_id', $studentId) + ->where('next_school_year', $nextSchoolYear) + ->first(); + $isNew = $existing === null; + + $record = $existing ?: new StudentPromotionRecord(); + $record->fill([ + 'student_id' => $studentId, + 'parent_id' => (int) ($student->parent_id ?? 0) ?: null, + 'current_school_year' => $currentSchoolYear, + 'next_school_year' => $nextSchoolYear, + 'current_level_id' => $progressionInfo['current_level_id'], + 'current_level_name' => $progressionInfo['current_level_name'], + 'promoted_level_id' => $progressionInfo['next_level_id'], + 'promoted_level_name' => $progressionInfo['next_level_name'], + 'final_average' => $finalAvg, + 'eligibility_notes' => $scoreInfo['notes'], + 'updated_by' => $userId, + ]); + if ($isNew) { + $record->promotion_status = StudentPromotionRecord::STATUS_NOT_REVIEWED; + $record->enrollment_required = true; + $record->enrollment_status = StudentPromotionRecord::ENROLLMENT_NOT_STARTED; + } + + $previousStatus = (string) ($record->promotion_status ?? StudentPromotionRecord::STATUS_NOT_REVIEWED); + $record->passed_current_level = $passed; + $newStatus = $this->resolveStatusFromEvaluation( + $passed, + $progressionInfo['is_terminal'] ?? false, + $scoreInfo['has_data'] + ); + + // Preserve admin overrides for terminal/holds. + if (in_array($previousStatus, [ + StudentPromotionRecord::STATUS_WITHDRAWN, + StudentPromotionRecord::STATUS_GRADUATED, + StudentPromotionRecord::STATUS_PROMOTED_AND_ENROLLED, + StudentPromotionRecord::STATUS_NOT_ENROLLED, + ], true)) { + $newStatus = $previousStatus; + } + + $record->promotion_status = $newStatus; + $record->save(); + + if ($isNew) { + $this->audit->logRecordCreated($record, $userId); + } + $this->audit->logEligibilityEvaluation( + $record, + (bool) $passed, + $userId, + $scoreInfo['notes'] + ); + if ($previousStatus !== $newStatus) { + $this->audit->logStatusChange($record, $previousStatus, $newStatus, $userId, 'Auto eligibility evaluation'); + } + + return $record; + }); + } + + /** + * Best effort lookup of the student's final academic result for the + * given school year. Treats missing fall + spring scores as "no + * data" → on hold (plan section 4: On Hold for missing result). + * + * @return array{ passed:?bool, final_average:?float, notes:?string, has_data:bool } + */ + private function loadAcademicResult(int $studentId, string $schoolYear): array + { + $rows = DB::table('semester_scores') + ->select('semester', 'semester_score') + ->where('student_id', $studentId) + ->where('school_year', $schoolYear) + ->get(); + + $fall = null; + $spring = null; + foreach ($rows as $row) { + $semester = strtolower((string) ($row->semester ?? '')); + $score = isset($row->semester_score) ? (float) $row->semester_score : null; + if ($semester === 'fall') { + $fall = $score; + } elseif ($semester === 'spring') { + $spring = $score; + } + } + + $threshold = $this->passThreshold(); + + if ($fall === null && $spring === null) { + return [ + 'passed' => null, + 'final_average' => null, + 'notes' => 'Missing fall and spring scores', + 'has_data' => false, + ]; + } + if ($fall === null || $spring === null) { + $existing = $fall ?? $spring; + return [ + 'passed' => null, + 'final_average' => $existing, + 'notes' => 'Awaiting both fall and spring scores', + 'has_data' => false, + ]; + } + + $avg = round(($fall + $spring) / 2.0, 2); + return [ + 'passed' => $avg >= $threshold, + 'final_average' => $avg, + 'notes' => sprintf('Final average %.2f (threshold %.2f)', $avg, $threshold), + 'has_data' => true, + ]; + } + + private function resolveStatusFromEvaluation(?bool $passed, bool $terminal, bool $hasData): string + { + if (!$hasData) { + return StudentPromotionRecord::STATUS_ON_HOLD; + } + if ($passed === false) { + return StudentPromotionRecord::STATUS_REPEATED; + } + if ($terminal) { + return StudentPromotionRecord::STATUS_GRADUATED; + } + return StudentPromotionRecord::STATUS_AWAITING_PARENT; + } + + private function statusBucket(string $status): ?string + { + return match ($status) { + StudentPromotionRecord::STATUS_AWAITING_PARENT, + StudentPromotionRecord::STATUS_ELIGIBLE => 'eligible', + StudentPromotionRecord::STATUS_REPEATED => 'repeated', + StudentPromotionRecord::STATUS_ON_HOLD => 'on_hold', + StudentPromotionRecord::STATUS_CONDITIONAL => 'conditional', + StudentPromotionRecord::STATUS_GRADUATED => 'graduated', + default => null, + }; + } + + private function emptySummary(): array + { + return [ + 'evaluated' => 0, + 'eligible' => 0, + 'repeated' => 0, + 'on_hold' => 0, + 'conditional' => 0, + 'graduated' => 0, + 'errors' => [], + ]; + } + + private function resolveCurrentSchoolYear(): ?string + { + $year = (string) (Configuration::getConfigValueByKey('school_year') ?? ''); + return $year !== '' ? $year : null; + } + + private function nextSchoolYear(string $current): ?string + { + if (!preg_match('/^(\d{4})-(\d{4})$/', $current, $m)) { + return null; + } + return ((int) $m[1] + 1) . '-' . ((int) $m[2] + 1); + } + + private function passThreshold(): float + { + $configured = Configuration::getConfigValueByKey('promotion_pass_threshold'); + if ($configured !== null && is_numeric($configured)) { + return (float) $configured; + } + return self::DEFAULT_PASS_THRESHOLD; + } +} diff --git a/app/Services/Promotions/PromotionEnrollmentService.php b/app/Services/Promotions/PromotionEnrollmentService.php new file mode 100644 index 00000000..b6a3c54c --- /dev/null +++ b/app/Services/Promotions/PromotionEnrollmentService.php @@ -0,0 +1,391 @@ +guessNextSchoolYear(); + + $query = StudentPromotionRecord::query()->forParent($parentId); + if ($year !== null) { + $query->forNextYear($year); + } + $records = $query + ->orderByRaw('CASE WHEN promotion_status IN (?, ?, ?) THEN 0 ELSE 1 END', [ + StudentPromotionRecord::STATUS_AWAITING_PARENT, + StudentPromotionRecord::STATUS_ELIGIBLE, + StudentPromotionRecord::STATUS_ENROLLMENT_STARTED, + ]) + ->orderBy('promotion_id', 'desc') + ->get(); + + $studentIds = $records->pluck('student_id')->unique()->all(); + /** @var EloquentCollection $studentsCollection */ + $studentsCollection = !empty($studentIds) + ? Student::query()->whereIn('id', $studentIds)->get() + : new EloquentCollection(); + $studentsById = $studentsCollection->keyBy('id'); + + $payload = $records->map(function (StudentPromotionRecord $record) use ($studentsById) { + return $this->presentRecord($record, $studentsById->get($record->student_id)); + })->all(); + + return [ + 'records' => $payload, + 'next_school_year' => $year, + 'message' => $this->parentBannerMessage($records), + ]; + } + + /** + * Mark the start of the enrollment process for a single promotion + * record. + */ + public function startEnrollment(StudentPromotionRecord $record, int $parentId, ?int $userId = null): StudentPromotionRecord + { + $this->assertParentOwns($record, $parentId); + $this->assertActionable($record); + + return DB::transaction(function () use ($record, $parentId, $userId) { + if (!$record->enrollment_started_at) { + $record->enrollment_started_at = now(); + } + $record->enrollment_status = StudentPromotionRecord::ENROLLMENT_IN_PROGRESS; + $record->parent_id = $record->parent_id ?: $parentId; + $record->updated_by = $userId; + $record->save(); + + if ($record->promotion_status !== StudentPromotionRecord::STATUS_ENROLLMENT_STARTED) { + $this->statusService->transition( + $record, + StudentPromotionRecord::STATUS_ENROLLMENT_STARTED, + $userId, + 'Parent started enrollment' + ); + } + $this->audit->logEnrollmentStarted($record, $userId); + + return $record->refresh(); + }); + } + + /** + * Update one or more checklist steps. Returns the (possibly + * finalised) record. + */ + public function updateSteps( + StudentPromotionRecord $record, + int $parentId, + array $steps, + ?int $userId = null + ): StudentPromotionRecord { + $this->assertParentOwns($record, $parentId); + $this->assertActionable($record); + + $sanitized = []; + foreach ($steps as $field => $value) { + if (!in_array($field, self::ALLOWED_STEPS, true)) { + continue; + } + $sanitized[$field] = (bool) $value; + } + if (empty($sanitized)) { + return $record; + } + + return DB::transaction(function () use ($record, $parentId, $sanitized, $userId) { + $changed = []; + foreach ($sanitized as $field => $value) { + if ((bool) $record->{$field} !== $value) { + $record->{$field} = $value; + $changed[] = $field; + } + } + + if (empty($changed)) { + return $record; + } + + if (!$record->enrollment_started_at) { + $record->enrollment_started_at = now(); + } + $record->enrollment_status = StudentPromotionRecord::ENROLLMENT_IN_PROGRESS; + $record->parent_id = $record->parent_id ?: $parentId; + $record->updated_by = $userId; + $record->save(); + + foreach ($changed as $field) { + $this->audit->logEnrollmentStepCompleted($record, $field, $userId); + } + + if ($record->promotion_status !== StudentPromotionRecord::STATUS_ENROLLMENT_STARTED) { + if (in_array($record->promotion_status, [ + StudentPromotionRecord::STATUS_ELIGIBLE, + StudentPromotionRecord::STATUS_AWAITING_PARENT, + ], true)) { + $this->statusService->transition( + $record, + StudentPromotionRecord::STATUS_ENROLLMENT_STARTED, + $userId, + 'Enrollment progress recorded' + ); + } + } + + // Auto-finalise once all checklist items are complete. + if ($record->enrollmentChecklistComplete()) { + $record = $this->finalize($record, $userId); + } + + return $record; + }); + } + + /** + * Submit the enrollment, asserting the checklist is complete. + */ + public function submitEnrollment( + StudentPromotionRecord $record, + int $parentId, + ?int $userId = null + ): StudentPromotionRecord { + $this->assertParentOwns($record, $parentId); + $this->assertActionable($record); + + if (!$record->enrollmentChecklistComplete()) { + throw new RuntimeException('Enrollment checklist is incomplete.'); + } + + return $this->finalize($record, $userId); + } + + /** + * Mark expired records (past deadline, still incomplete) as + * "not_enrolled_for_next_year" (plan section 15). + * + * @return array{ expired:int, ids:array } + */ + public function markExpiredDeadlines(?\DateTimeInterface $now = null, ?int $userId = null): array + { + $now = $now ?: now(); + $records = StudentPromotionRecord::query() + ->whereIn('promotion_status', [ + StudentPromotionRecord::STATUS_ELIGIBLE, + StudentPromotionRecord::STATUS_AWAITING_PARENT, + StudentPromotionRecord::STATUS_ENROLLMENT_STARTED, + ]) + ->whereNotNull('enrollment_deadline') + ->whereDate('enrollment_deadline', '<', $now) + ->get(); + + $ids = []; + foreach ($records as $record) { + DB::transaction(function () use ($record, $userId, &$ids) { + $record->enrollment_status = StudentPromotionRecord::ENROLLMENT_EXPIRED; + $record->save(); + $this->audit->logDeadlineExpired($record); + $this->statusService->forceStatus( + $record, + StudentPromotionRecord::STATUS_NOT_ENROLLED, + $userId, + 'Deadline expired without enrollment' + ); + $ids[] = (int) $record->getKey(); + }); + } + + return [ + 'expired' => count($ids), + 'ids' => $ids, + ]; + } + + /** + * Finalise promotion + create the next-year enrollment record + * (plan sections 5, 6 step 6 and section 9). + */ + private function finalize(StudentPromotionRecord $record, ?int $userId): StudentPromotionRecord + { + if ($record->promotion_status === StudentPromotionRecord::STATUS_PROMOTED_AND_ENROLLED) { + return $record; + } + + return DB::transaction(function () use ($record, $userId) { + $record->enrollment_status = StudentPromotionRecord::ENROLLMENT_COMPLETED; + $record->enrollment_completed_at = $record->enrollment_completed_at ?: now(); + $record->promotion_finalized_at = now(); + $record->updated_by = $userId; + + $enrollmentId = $this->ensureNextYearEnrollment($record); + if ($enrollmentId !== null) { + $record->enrollment_id = $enrollmentId; + } + $record->save(); + + $this->audit->logEnrollmentCompleted($record, $userId); + + $this->statusService->transition( + $record, + StudentPromotionRecord::STATUS_PROMOTED_AND_ENROLLED, + $userId, + 'Enrollment checklist complete' + ); + $this->audit->logPromotionFinalized($record->refresh(), $userId); + + return $record->refresh(); + }); + } + + /** + * Create or update an `enrollments` row for the next school year so + * the student is officially placed in the new year (plan section 9 + * record-update rule). + */ + private function ensureNextYearEnrollment(StudentPromotionRecord $record): ?int + { + $existing = Enrollment::query() + ->where('student_id', $record->student_id) + ->where('school_year', $record->next_school_year) + ->first(); + + $payload = [ + 'student_id' => $record->student_id, + 'parent_id' => $record->parent_id, + 'school_year' => $record->next_school_year, + 'enrollment_status' => 'enrolled', + 'admission_status' => 'accepted', + 'is_withdrawn' => false, + 'enrollment_date' => now()->toDateString(), + ]; + + if ($existing) { + $existing->fill($payload); + $existing->save(); + return (int) $existing->getKey(); + } + + $created = Enrollment::query()->create($payload); + return $created ? (int) $created->getKey() : null; + } + + private function presentRecord(StudentPromotionRecord $record, ?Student $student): array + { + return [ + 'promotion_id' => (int) $record->getKey(), + 'student_id' => (int) $record->student_id, + 'student_name' => $student + ? trim((string) $student->firstname . ' ' . (string) $student->lastname) + : null, + 'current_level' => $record->current_level_name, + 'promoted_level' => $record->promoted_level_name, + 'current_school_year' => $record->current_school_year, + 'next_school_year' => $record->next_school_year, + 'promotion_status' => $record->promotion_status, + 'enrollment_status' => $record->enrollment_status, + 'enrollment_deadline' => $record->enrollment_deadline?->toDateString(), + 'enrollment_started_at' => $record->enrollment_started_at?->toDateTimeString(), + 'enrollment_completed_at' => $record->enrollment_completed_at?->toDateTimeString(), + 'parent_action_required' => in_array( + $record->promotion_status, + StudentPromotionRecord::parentActionableStatuses(), + true + ), + 'checklist' => [ + 'info_confirmed' => (bool) $record->info_confirmed, + 'documents_uploaded' => (bool) $record->documents_uploaded, + 'agreement_accepted' => (bool) $record->agreement_accepted, + 'payment_completed' => (bool) $record->payment_completed, + ], + 'final_average' => $record->final_average !== null ? (float) $record->final_average : null, + 'passed_current_level' => $record->passed_current_level, + ]; + } + + private function parentBannerMessage(EloquentCollection $records): ?string + { + $actionable = $records->first(function (StudentPromotionRecord $r) { + return in_array( + $r->promotion_status, + StudentPromotionRecord::parentActionableStatuses(), + true + ); + }); + if (!$actionable) { + return null; + } + $level = $actionable->promoted_level_name ?: 'the next level'; + $deadline = $actionable->enrollment_deadline?->toDateString(); + $deadlineText = $deadline ? 'by ' . $deadline : 'as soon as possible'; + return sprintf( + 'Your child is eligible for promotion to %s. Please complete enrollment for the new school year %s.', + $level, + $deadlineText + ); + } + + private function assertParentOwns(StudentPromotionRecord $record, int $parentId): void + { + if ($parentId <= 0) { + throw new RuntimeException('Authenticated parent is required.'); + } + if ((int) $record->parent_id !== $parentId) { + $studentParent = (int) (Student::query()->where('id', $record->student_id)->value('parent_id') ?? 0); + if ($studentParent !== $parentId) { + throw new RuntimeException('You are not authorised to act on this promotion record.'); + } + } + } + + private function assertActionable(StudentPromotionRecord $record): void + { + $actionable = StudentPromotionRecord::parentActionableStatuses(); + if (!in_array($record->promotion_status, $actionable, true)) { + throw new RuntimeException(sprintf( + 'Promotion record is not actionable in status "%s".', + $record->promotion_status + )); + } + } + + private function guessNextSchoolYear(): ?string + { + $current = (string) (Configuration::getConfigValueByKey('school_year') ?? ''); + if (!preg_match('/^(\d{4})-(\d{4})$/', $current, $m)) { + return null; + } + return ((int) $m[1] + 1) . '-' . ((int) $m[2] + 1); + } +} diff --git a/app/Services/Promotions/PromotionQueryService.php b/app/Services/Promotions/PromotionQueryService.php new file mode 100644 index 00000000..8cd3eb31 --- /dev/null +++ b/app/Services/Promotions/PromotionQueryService.php @@ -0,0 +1,223 @@ +> when paginate=false, + * otherwise a Laravel paginator instance. + * + * @param array{ + * status?: string|array, + * next_school_year?: string|null, + * current_school_year?: string|null, + * parent_id?: int|null, + * student_id?: int|null, + * search?: string|null, + * parent_action_required?: bool, + * per_page?: int|null, + * } $filters + * + * @return array{ + * data: array>, + * total: int, + * page: int, + * per_page: int, + * total_pages: int, + * counts_by_status: array + * } + */ + public function list(array $filters): array + { + $query = StudentPromotionRecord::query(); + $this->applyFilters($query, $filters); + + $perPage = isset($filters['per_page']) ? max(1, min(200, (int) $filters['per_page'])) : 50; + $page = isset($filters['page']) ? max(1, (int) $filters['page']) : 1; + + /** @var LengthAwarePaginator $paginator */ + $paginator = $query->orderByDesc('promotion_id')->paginate($perPage, ['*'], 'page', $page); + + /** @var EloquentCollection $records */ + $records = $paginator->getCollection(); + $studentIds = $records->pluck('student_id')->unique()->values()->all(); + $parentIds = $records->pluck('parent_id')->filter()->unique()->values()->all(); + + $studentsById = !empty($studentIds) + ? Student::query()->whereIn('id', $studentIds)->get()->keyBy('id') + : new EloquentCollection(); + $parentsById = !empty($parentIds) + ? User::query()->whereIn('id', $parentIds)->get()->keyBy('id') + : new EloquentCollection(); + + $rows = $records->map(function (StudentPromotionRecord $record) use ($studentsById, $parentsById) { + return $this->presentAdminRow( + $record, + $studentsById->get($record->student_id), + $record->parent_id ? $parentsById->get($record->parent_id) : null + ); + })->all(); + + return [ + 'data' => array_values($rows), + 'total' => (int) $paginator->total(), + 'page' => (int) $paginator->currentPage(), + 'per_page' => (int) $paginator->perPage(), + 'total_pages' => (int) $paginator->lastPage(), + 'counts_by_status' => $this->countsByStatus($filters), + ]; + } + + /** + * Returns the per-status count of promotion records that match the + * given filter set (ignoring `status` and pagination filters). Plan + * section 16's reports rely on these counts. + * + * @return array + */ + public function countsByStatus(array $filters): array + { + $query = StudentPromotionRecord::query(); + // Apply non-status filters only. + $filtersSansStatus = $filters; + unset($filtersSansStatus['status'], $filtersSansStatus['parent_action_required']); + $this->applyFilters($query, $filtersSansStatus); + + $rows = $query + ->select('promotion_status', DB::raw('count(*) as total')) + ->groupBy('promotion_status') + ->get(); + + $counts = array_fill_keys(StudentPromotionRecord::ALL_STATUSES, 0); + foreach ($rows as $row) { + $counts[(string) $row->promotion_status] = (int) $row->total; + } + return $counts; + } + + /** + * Hydrates a single record with student + parent context for the + * admin detail view. + */ + public function detail(StudentPromotionRecord $record): array + { + $student = Student::query()->find($record->student_id); + $parent = $record->parent_id ? User::query()->find($record->parent_id) : null; + + return $this->presentAdminRow($record, $student, $parent, true); + } + + /** + * @param Builder $query + */ + private function applyFilters(Builder $query, array $filters): void + { + if (!empty($filters['status'])) { + $statuses = is_array($filters['status']) ? $filters['status'] : [$filters['status']]; + $statuses = array_values(array_filter($statuses, static fn ($s) => is_string($s) && $s !== '')); + if (!empty($statuses)) { + $query->whereIn('promotion_status', $statuses); + } + } + if (!empty($filters['parent_action_required'])) { + $query->whereIn('promotion_status', StudentPromotionRecord::parentActionableStatuses()); + } + if (!empty($filters['next_school_year'])) { + $query->where('next_school_year', $filters['next_school_year']); + } + if (!empty($filters['current_school_year'])) { + $query->where('current_school_year', $filters['current_school_year']); + } + if (!empty($filters['parent_id'])) { + $query->where('parent_id', (int) $filters['parent_id']); + } + if (!empty($filters['student_id'])) { + $query->where('student_id', (int) $filters['student_id']); + } + if (!empty($filters['search'])) { + $search = '%' . strtolower((string) $filters['search']) . '%'; + $query->whereIn('student_id', function ($sub) use ($search) { + $sub->select('id') + ->from('students') + ->where(function ($w) use ($search) { + $w->whereRaw('LOWER(firstname) LIKE ?', [$search]) + ->orWhereRaw('LOWER(lastname) LIKE ?', [$search]); + }); + }); + } + } + + private function presentAdminRow( + StudentPromotionRecord $record, + ?Student $student, + ?User $parent, + bool $detail = false + ): array { + $row = [ + 'promotion_id' => (int) $record->getKey(), + 'student_id' => (int) $record->student_id, + 'student_name' => $student + ? trim((string) $student->firstname . ' ' . (string) $student->lastname) + : null, + 'school_id' => $student->school_id ?? null, + 'parent_id' => $record->parent_id ? (int) $record->parent_id : null, + 'parent_name' => $parent + ? trim((string) $parent->firstname . ' ' . (string) $parent->lastname) + : null, + 'parent_email' => $parent->email ?? null, + 'current_school_year' => $record->current_school_year, + 'next_school_year' => $record->next_school_year, + 'current_level' => $record->current_level_name, + 'promoted_level' => $record->promoted_level_name, + 'promotion_status' => $record->promotion_status, + 'enrollment_status' => $record->enrollment_status, + 'enrollment_deadline' => $record->enrollment_deadline?->toDateString(), + 'parent_notified_at' => $record->parent_notified_at?->toDateTimeString(), + 'enrollment_started_at' => $record->enrollment_started_at?->toDateTimeString(), + 'enrollment_completed_at' => $record->enrollment_completed_at?->toDateTimeString(), + 'promotion_finalized_at' => $record->promotion_finalized_at?->toDateTimeString(), + 'final_average' => $record->final_average !== null ? (float) $record->final_average : null, + 'passed_current_level' => $record->passed_current_level, + 'updated_by' => $record->updated_by ? (int) $record->updated_by : null, + 'updated_at' => $record->updated_at?->toDateTimeString(), + ]; + + if ($detail) { + $row['checklist'] = [ + 'info_confirmed' => (bool) $record->info_confirmed, + 'documents_uploaded' => (bool) $record->documents_uploaded, + 'agreement_accepted' => (bool) $record->agreement_accepted, + 'payment_completed' => (bool) $record->payment_completed, + ]; + $row['eligibility_notes'] = $record->eligibility_notes; + $row['enrollment_id'] = $record->enrollment_id ? (int) $record->enrollment_id : null; + } + + return $row; + } +} diff --git a/app/Services/Promotions/PromotionReminderService.php b/app/Services/Promotions/PromotionReminderService.php new file mode 100644 index 00000000..5227946b --- /dev/null +++ b/app/Services/Promotions/PromotionReminderService.php @@ -0,0 +1,207 @@ +dispatcher = $dispatcher; + } + + /** + * Records a single reminder dispatch. + */ + public function send( + StudentPromotionRecord $record, + string $reminderType, + ?int $userId = null, + ?string $subject = null, + ?string $message = null, + array $channels = ['in_app', 'email'] + ): PromotionReminderLog { + if (!in_array($reminderType, PromotionReminderLog::ALLOWED_TYPES, true)) { + $reminderType = PromotionReminderLog::TYPE_MANUAL; + } + + $studentName = $this->studentName($record); + $deadline = $record->enrollment_deadline?->toDateString(); + $level = $record->promoted_level_name ?: 'the next level'; + + $defaultSubject = 'Reminder: complete enrollment for the next school year'; + $defaultMessage = sprintf( + '%s is eligible for promotion to %s.%s Please complete enrollment as soon as possible.', + $studentName ?: 'Your child', + $level, + $deadline ? ' Deadline: ' . $deadline . '.' : '' + ); + + $finalSubject = $subject ?: $defaultSubject; + $finalMessage = $message ?: $defaultMessage; + + return DB::transaction(function () use ( + $record, + $reminderType, + $finalSubject, + $finalMessage, + $channels, + $userId + ) { + $log = PromotionReminderLog::query()->create([ + 'promotion_id' => (int) $record->getKey(), + 'student_id' => (int) $record->student_id, + 'parent_id' => (int) $record->parent_id ?: null, + 'reminder_type' => $reminderType, + 'channel' => implode(',', array_values(array_unique(array_filter($channels)))), + 'subject' => $finalSubject, + 'message' => $finalMessage, + 'sent_at' => now(), + 'sent_by' => $userId, + ]); + + if ($record->parent_id) { + $this->dispatch((int) $record->parent_id, $finalSubject, $finalMessage, $channels); + } + + if (!$record->parent_notified_at) { + $record->parent_notified_at = now(); + $record->save(); + $this->audit->logParentNotified($record, $userId, $reminderType); + } + $this->audit->logReminderSent($record, $reminderType, $userId); + + return $log; + }); + } + + /** + * Plan section 14 – schedule-driven reminders. Sends the next due + * reminder type for every actionable record whose deadline is set. + * + * @return array{ processed:int, sent:int, skipped:int } + */ + public function dispatchScheduledReminders(?\DateTimeInterface $now = null, ?int $userId = null): array + { + $now = CarbonImmutable::instance($now ?: now()); + + $records = StudentPromotionRecord::query() + ->whereIn('promotion_status', StudentPromotionRecord::parentActionableStatuses()) + ->whereNotNull('enrollment_deadline') + ->get(); + + $processed = 0; + $sent = 0; + $skipped = 0; + + foreach ($records as $record) { + $processed++; + $type = $this->resolveReminderType($record, $now); + if ($type === null) { + $skipped++; + continue; + } + $alreadySent = PromotionReminderLog::query() + ->where('promotion_id', $record->getKey()) + ->where('reminder_type', $type) + ->exists(); + if ($alreadySent) { + $skipped++; + continue; + } + try { + $this->send($record, $type, $userId); + $sent++; + } catch (\Throwable $e) { + Log::error('Promotion reminder dispatch failed', [ + 'promotion_id' => $record->getKey(), + 'exception' => $e, + ]); + $skipped++; + } + } + + return [ + 'processed' => $processed, + 'sent' => $sent, + 'skipped' => $skipped, + ]; + } + + /** + * Returns the reminder type that should be sent next based on the + * deadline / current time. + */ + private function resolveReminderType(StudentPromotionRecord $record, CarbonImmutable $now): ?string + { + $deadline = $record->enrollment_deadline ? CarbonImmutable::instance($record->enrollment_deadline) : null; + if ($deadline === null) { + return null; + } + $createdAt = $record->created_at ? CarbonImmutable::instance($record->created_at) : $now; + + if ($now->greaterThan($deadline)) { + return PromotionReminderLog::TYPE_EXPIRATION; + } + + $totalSeconds = max(1, $deadline->getTimestamp() - $createdAt->getTimestamp()); + $elapsedSeconds = max(0, $now->getTimestamp() - $createdAt->getTimestamp()); + $remainingSeconds = max(0, $deadline->getTimestamp() - $now->getTimestamp()); + $remainingDays = (int) floor($remainingSeconds / 86400); + + if ($elapsedSeconds === 0) { + return PromotionReminderLog::TYPE_FIRST; + } + if ($remainingDays <= 3) { + return PromotionReminderLog::TYPE_FINAL; + } + if ($elapsedSeconds * 2 >= $totalSeconds) { + return PromotionReminderLog::TYPE_HALFWAY; + } + return PromotionReminderLog::TYPE_FIRST; + } + + private function studentName(StudentPromotionRecord $record): ?string + { + $student = Student::query()->find($record->student_id); + if (!$student) { + return null; + } + $name = trim((string) ($student->firstname ?? '') . ' ' . (string) ($student->lastname ?? '')); + return $name !== '' ? $name : null; + } + + private function dispatch(int $userId, string $subject, string $message, array $channels): void + { + if (!$this->dispatcher) { + return; + } + try { + ($this->dispatcher)($userId, $subject, $message, $channels); + } catch (\Throwable $e) { + Log::error('Promotion reminder dispatcher threw', [ + 'user_id' => $userId, + 'exception' => $e, + ]); + } + } +} diff --git a/app/Services/Promotions/PromotionStatusService.php b/app/Services/Promotions/PromotionStatusService.php new file mode 100644 index 00000000..6399a451 --- /dev/null +++ b/app/Services/Promotions/PromotionStatusService.php @@ -0,0 +1,164 @@ + [ + StudentPromotionRecord::STATUS_ELIGIBLE, + StudentPromotionRecord::STATUS_AWAITING_PARENT, + StudentPromotionRecord::STATUS_CONDITIONAL, + StudentPromotionRecord::STATUS_REPEATED, + StudentPromotionRecord::STATUS_ON_HOLD, + StudentPromotionRecord::STATUS_GRADUATED, + StudentPromotionRecord::STATUS_WITHDRAWN, + ], + StudentPromotionRecord::STATUS_ELIGIBLE => [ + StudentPromotionRecord::STATUS_AWAITING_PARENT, + StudentPromotionRecord::STATUS_ENROLLMENT_STARTED, + StudentPromotionRecord::STATUS_CONDITIONAL, + StudentPromotionRecord::STATUS_ON_HOLD, + StudentPromotionRecord::STATUS_NOT_ENROLLED, + StudentPromotionRecord::STATUS_WITHDRAWN, + ], + StudentPromotionRecord::STATUS_AWAITING_PARENT => [ + StudentPromotionRecord::STATUS_ENROLLMENT_STARTED, + StudentPromotionRecord::STATUS_PROMOTED_AND_ENROLLED, + StudentPromotionRecord::STATUS_NOT_ENROLLED, + StudentPromotionRecord::STATUS_ON_HOLD, + StudentPromotionRecord::STATUS_WITHDRAWN, + ], + StudentPromotionRecord::STATUS_ENROLLMENT_STARTED => [ + StudentPromotionRecord::STATUS_PROMOTED_AND_ENROLLED, + StudentPromotionRecord::STATUS_AWAITING_PARENT, + StudentPromotionRecord::STATUS_NOT_ENROLLED, + StudentPromotionRecord::STATUS_ON_HOLD, + StudentPromotionRecord::STATUS_WITHDRAWN, + ], + StudentPromotionRecord::STATUS_CONDITIONAL => [ + StudentPromotionRecord::STATUS_ELIGIBLE, + StudentPromotionRecord::STATUS_AWAITING_PARENT, + StudentPromotionRecord::STATUS_REPEATED, + StudentPromotionRecord::STATUS_ON_HOLD, + StudentPromotionRecord::STATUS_NOT_REVIEWED, + ], + StudentPromotionRecord::STATUS_ON_HOLD => [ + StudentPromotionRecord::STATUS_NOT_REVIEWED, + StudentPromotionRecord::STATUS_ELIGIBLE, + StudentPromotionRecord::STATUS_AWAITING_PARENT, + StudentPromotionRecord::STATUS_REPEATED, + StudentPromotionRecord::STATUS_CONDITIONAL, + StudentPromotionRecord::STATUS_WITHDRAWN, + StudentPromotionRecord::STATUS_NOT_ENROLLED, + ], + // Terminal statuses are intentionally not in the map (no auto + // transitions); admins can still override via `forceStatus`. + ]; + + public function canTransition(string $from, string $to): bool + { + if ($from === $to) { + return true; + } + if (!in_array($to, StudentPromotionRecord::ALL_STATUSES, true)) { + return false; + } + $allowed = self::ALLOWED_TRANSITIONS[$from] ?? []; + return in_array($to, $allowed, true); + } + + /** + * Apply a status transition with audit logging. Returns the saved + * record. Throws if the new status is invalid or the transition is + * not allowed. + */ + public function transition( + StudentPromotionRecord $record, + string $newStatus, + ?int $userId = null, + ?string $notes = null + ): StudentPromotionRecord { + if (!in_array($newStatus, StudentPromotionRecord::ALL_STATUSES, true)) { + throw new InvalidArgumentException(sprintf('Unknown promotion status "%s".', $newStatus)); + } + + $oldStatus = (string) $record->promotion_status; + if ($oldStatus === $newStatus) { + return $record; + } + if (!$this->canTransition($oldStatus, $newStatus)) { + throw new RuntimeException(sprintf( + 'Transition from "%s" to "%s" is not allowed.', + $oldStatus, + $newStatus + )); + } + + return DB::transaction(function () use ($record, $oldStatus, $newStatus, $userId, $notes) { + $record->promotion_status = $newStatus; + $record->updated_by = $userId; + $record->save(); + + $this->audit->logStatusChange($record, $oldStatus, $newStatus, $userId, $notes); + + return $record; + }); + } + + /** + * Force a status change ignoring the transition map (e.g. admin + * override). The audit trail records this as a manual override. + */ + public function forceStatus( + StudentPromotionRecord $record, + string $newStatus, + ?int $userId = null, + ?string $notes = null + ): StudentPromotionRecord { + if (!in_array($newStatus, StudentPromotionRecord::ALL_STATUSES, true)) { + throw new InvalidArgumentException(sprintf('Unknown promotion status "%s".', $newStatus)); + } + + $oldStatus = (string) $record->promotion_status; + if ($oldStatus === $newStatus) { + return $record; + } + + return DB::transaction(function () use ($record, $oldStatus, $newStatus, $userId, $notes) { + $record->promotion_status = $newStatus; + $record->updated_by = $userId; + $record->save(); + + $this->audit->logManualOverride( + $record, + 'promotion_status', + $oldStatus, + $newStatus, + $userId, + $notes ?? 'Manual status override' + ); + + return $record; + }); + } +} diff --git a/artisan b/artisan old mode 100755 new mode 100644 diff --git a/bootstrap/app.php b/bootstrap/app.php index dd11725f..3ac15e08 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -16,20 +16,35 @@ return Application::configure(basePath: dirname(__DIR__)) ) ->withMiddleware(function (Middleware $middleware): void { $middleware->alias([ - 'auth.multi' => \App\Http\Middleware\MultiAuth::class, + // JWT auth 'jwt.auth' => \PHPOpenSourceSaver\JWTAuth\Http\Middleware\Authenticate::class, 'jwt.refresh' => \PHPOpenSourceSaver\JWTAuth\Http\Middleware\RefreshToken::class, 'api.jwt' => \App\Http\Middleware\ApiJwtAuth::class, + + // Multi-guard auth (both the new and legacy alias point at the same class) + 'auth.multi' => \App\Http\Middleware\MultiAuth::class, + 'multi.auth' => \App\Http\Middleware\MultiAuth::class, + + // Permission / role gates 'perm' => \App\Http\Middleware\RequirePermission::class, - 'timezone' => \App\Http\Middleware\PrimeTimezone::class, - 'cleanup.scheduler' => \App\Http\Middleware\CleanupScheduler::class, - 'auth.docs' => \App\Http\Middleware\ApiDocsAuth::class, - 'teacher.portal.auth' => \App\Http\Middleware\TeacherPortalAuthenticate::class, - 'teacher.progress' => \App\Http\Middleware\EnsureClassProgressTeacherPortalAccess::class, + 'require.permission' => \App\Http\Middleware\RequirePermission::class, + 'admin.access' => \App\Http\Middleware\EnsurePrintRequestsAdminAccess::class, + 'parent.access' => \App\Http\Middleware\EnsureParentProgressAccess::class, 'parent.progress' => \App\Http\Middleware\EnsureParentProgressAccess::class, + 'teacher.progress' => \App\Http\Middleware\EnsureClassProgressTeacherPortalAccess::class, + 'class_progress.teacher' => \App\Http\Middleware\EnsureClassProgressTeacherPortalAccess::class, 'print_requests.teacher' => \App\Http\Middleware\EnsurePrintRequestsTeacherAccess::class, 'print_requests.admin' => \App\Http\Middleware\EnsurePrintRequestsAdminAccess::class, 'badge_scan.logs' => \App\Http\Middleware\EnsureBadgeScanLogsAccess::class, + + // Docs + teacher portal + 'auth.docs' => \App\Http\Middleware\ApiDocsAuth::class, + 'teacher.portal.auth' => \App\Http\Middleware\TeacherPortalAuthenticate::class, + + // Utility + 'timezone' => \App\Http\Middleware\PrimeTimezone::class, + 'prime.timezone' => \App\Http\Middleware\PrimeTimezone::class, + 'cleanup.scheduler' => \App\Http\Middleware\CleanupScheduler::class, ]); $middleware->redirectGuestsTo(function (Request $request) { diff --git a/database/migrations/2026_05_29_120000_create_level_progressions_table.php b/database/migrations/2026_05_29_120000_create_level_progressions_table.php new file mode 100644 index 00000000..45e13ce6 --- /dev/null +++ b/database/migrations/2026_05_29_120000_create_level_progressions_table.php @@ -0,0 +1,35 @@ +id(); + $table->unsignedInteger('current_level_id')->nullable()->index(); + $table->string('current_level_name', 100); + $table->unsignedInteger('next_level_id')->nullable()->index(); + $table->string('next_level_name', 100)->nullable(); + $table->unsignedSmallInteger('order_index')->default(0); + $table->boolean('is_terminal')->default(false); + $table->boolean('is_active')->default(true); + $table->text('notes')->nullable(); + $table->timestamps(); + + $table->unique('current_level_name', 'uq_level_progressions_current_level_name'); + }); + } + + public function down(): void + { + Schema::dropIfExists('level_progressions'); + } +}; diff --git a/database/migrations/2026_05_29_120010_create_student_promotion_records_table.php b/database/migrations/2026_05_29_120010_create_student_promotion_records_table.php new file mode 100644 index 00000000..83281b44 --- /dev/null +++ b/database/migrations/2026_05_29_120010_create_student_promotion_records_table.php @@ -0,0 +1,64 @@ +id('promotion_id'); + $table->unsignedInteger('student_id')->index(); + $table->unsignedInteger('parent_id')->nullable()->index(); + + $table->string('current_school_year', 9); + $table->string('next_school_year', 9); + + $table->unsignedInteger('current_level_id')->nullable(); + $table->unsignedInteger('promoted_level_id')->nullable(); + $table->string('current_level_name', 100)->nullable(); + $table->string('promoted_level_name', 100)->nullable(); + + // Promotion lifecycle status (matches plan section 3) + $table->string('promotion_status', 40)->default('not_reviewed'); + + $table->boolean('passed_current_level')->nullable(); + $table->decimal('final_average', 6, 2)->nullable(); + $table->boolean('attendance_ok')->nullable(); + $table->text('eligibility_notes')->nullable(); + + $table->boolean('enrollment_required')->default(true); + $table->string('enrollment_status', 40)->default('not_started'); + $table->unsignedBigInteger('enrollment_id')->nullable()->index(); + + $table->dateTime('parent_notified_at')->nullable(); + $table->date('enrollment_deadline')->nullable(); + $table->dateTime('enrollment_started_at')->nullable(); + $table->dateTime('enrollment_completed_at')->nullable(); + $table->dateTime('promotion_finalized_at')->nullable(); + + $table->boolean('info_confirmed')->default(false); + $table->boolean('documents_uploaded')->default(false); + $table->boolean('agreement_accepted')->default(false); + $table->boolean('payment_completed')->default(false); + + $table->unsignedInteger('updated_by')->nullable(); + $table->timestamps(); + + $table->unique(['student_id', 'next_school_year'], 'uq_promotion_student_year'); + $table->index('promotion_status', 'idx_promotion_status'); + $table->index(['next_school_year', 'promotion_status'], 'idx_promotion_year_status'); + }); + } + + public function down(): void + { + Schema::dropIfExists('student_promotion_records'); + } +}; diff --git a/database/migrations/2026_05_29_120020_create_promotion_audit_log_table.php b/database/migrations/2026_05_29_120020_create_promotion_audit_log_table.php new file mode 100644 index 00000000..94ed3ea7 --- /dev/null +++ b/database/migrations/2026_05_29_120020_create_promotion_audit_log_table.php @@ -0,0 +1,34 @@ +id(); + $table->unsignedBigInteger('promotion_id')->nullable()->index(); + $table->unsignedInteger('student_id')->nullable()->index(); + $table->unsignedInteger('user_id')->nullable()->comment('Actor that performed the action'); + $table->string('action_type', 60); + $table->string('field', 60)->nullable(); + $table->string('old_value', 191)->nullable(); + $table->string('new_value', 191)->nullable(); + $table->text('notes')->nullable(); + $table->timestamp('performed_at')->useCurrent(); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('promotion_audit_log'); + } +}; diff --git a/database/migrations/2026_05_29_120030_create_promotion_reminders_log_table.php b/database/migrations/2026_05_29_120030_create_promotion_reminders_log_table.php new file mode 100644 index 00000000..fdf44484 --- /dev/null +++ b/database/migrations/2026_05_29_120030_create_promotion_reminders_log_table.php @@ -0,0 +1,35 @@ +id(); + $table->unsignedBigInteger('promotion_id')->index(); + $table->unsignedInteger('student_id')->nullable()->index(); + $table->unsignedInteger('parent_id')->nullable(); + $table->string('reminder_type', 40)->default('manual') + ->comment('first|halfway|final|expiration|manual'); + $table->string('channel', 20)->default('in_app'); + $table->string('subject', 191)->nullable(); + $table->text('message')->nullable(); + $table->dateTime('sent_at')->nullable(); + $table->unsignedInteger('sent_by')->nullable(); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('promotion_reminders_log'); + } +}; diff --git a/docs/.DS_Store b/docs/.DS_Store new file mode 100644 index 00000000..f33d2d62 Binary files /dev/null and b/docs/.DS_Store differ diff --git a/docs/api_administrator_promotion_controller_plan.md b/docs/api_administrator_promotion_controller_plan.md new file mode 100644 index 00000000..92fc8419 --- /dev/null +++ b/docs/api_administrator_promotion_controller_plan.md @@ -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. + diff --git a/docs/api_auth_controller_plan.md b/docs/api_auth_controller_plan.md new file mode 100644 index 00000000..4dcef1ad --- /dev/null +++ b/docs/api_auth_controller_plan.md @@ -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. + diff --git a/docs/api_authorized_user_invite_controller_plan.md b/docs/api_authorized_user_invite_controller_plan.md new file mode 100644 index 00000000..c38dc45f --- /dev/null +++ b/docs/api_authorized_user_invite_controller_plan.md @@ -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 user’s 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). + diff --git a/docs/api_authorized_users_controller_plan.md b/docs/api_authorized_users_controller_plan.md new file mode 100644 index 00000000..39ff2ce1 --- /dev/null +++ b/docs/api_authorized_users_controller_plan.md @@ -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 else’s 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. + diff --git a/docs/api_base_api_controller_plan.md b/docs/api_base_api_controller_plan.md new file mode 100644 index 00000000..dc45a1c5 --- /dev/null +++ b/docs/api_base_api_controller_plan.md @@ -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 + diff --git a/docs/api_charge_controller_plan.md b/docs/api_charge_controller_plan.md new file mode 100644 index 00000000..1e256433 --- /dev/null +++ b/docs/api_charge_controller_plan.md @@ -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. + diff --git a/docs/api_fee_calculation_controller_plan.md b/docs/api_fee_calculation_controller_plan.md new file mode 100644 index 00000000..92ff7aba --- /dev/null +++ b/docs/api_fee_calculation_controller_plan.md @@ -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. + diff --git a/docs/api_parent_controller_plan.md b/docs/api_parent_controller_plan.md new file mode 100644 index 00000000..611c2729 --- /dev/null +++ b/docs/api_parent_controller_plan.md @@ -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. + diff --git a/docs/api_parent_promotion_controller_plan.md b/docs/api_parent_promotion_controller_plan.md new file mode 100644 index 00000000..49ac4369 --- /dev/null +++ b/docs/api_parent_promotion_controller_plan.md @@ -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 record’s `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 parent’s 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). + diff --git a/docs/app_project_plan.md b/docs/app_project_plan.md new file mode 100644 index 00000000..a3526681 --- /dev/null +++ b/docs/app_project_plan.md @@ -0,0 +1,1640 @@ +# App Project Implementation Plan + +Generated: 2026-05-29 + +## 1. Scope and Readout + +This plan is based on the uploaded `app/` project slice. The zip contains the Laravel-style application layer, not the full repository. That means routes, migrations, config outside `app/`, composer metadata, tests, CI, frontend assets, and deployment files were not available unless embedded in this directory. So the plan avoids pretending the missing pieces magically exist, a level of honesty software planning could use more often. + +### Inventory + +- Real project files inspected: **1184** +- PHP files inspected: **1183** +- Approximate PHP LOC: **103,161** +- Controllers: **129** +- Models: **133** +- Services: **386** +- Form requests: **298** +- API resources: **167** +- Middleware: **12** +- Console commands: **14** +- Policies: **10** + + +## 2. Project Thesis + +The app is already organized around a service-heavy backend: controllers orchestrate, request classes validate, resources shape responses, and services carry domain logic. That is the right direction. The weak point is not that the code lacks structure. The weak point is that the project is broad enough for rules to drift across domains unless the boundaries are enforced deliberately. + +The plan should therefore optimize for correctness and regression safety, not cosmetic refactors. Refactoring without tests here would be performance art with a stack trace. + + + +## 3. Main Architecture Plan + +### Target shape + +- Controllers stay thin: authorize, validate, call one service action, return one resource/response. +- Services own business rules and transactions. +- Form requests own validation and request normalization. +- Resources own response shape and avoid leaking internal model structure. +- Policies/Gates own permission checks. +- Events/listeners own async or side-effect workflows, especially communications. +- Console commands remain operational entry points only, not hidden domain logic. + +### Non-negotiable rules + +- No money-changing action without an explicit transaction, audit trail, idempotency check, and test. +- No attendance, score, promotion, or enrollment mutation without actor tracking and date boundary tests. +- No file endpoint without MIME, size, storage path, authorization, and download-name tests. +- No communication endpoint without recipient preview, opt-out logic, deduplication, and send-log tests. +- No direct request parsing in controllers where a FormRequest already exists or should exist. +- No raw SQL without parameter binding review and a test proving the query behavior. + + +## 4. Evidence-Based Risk Notes + +These are not accusations; they are where bugs usually crawl out wearing a tiny hat. + +- **Raw SQL / raw query usage:** 59 files, 166 matches. +- **Direct request input access:** 50 files, 194 matches. +- **File operation endpoints:** 18 files, 34 matches. +- **Email or messaging behavior:** 62 files, 192 matches. +- **Auth/Gate/authorization usage:** 325 files, 482 matches. +- **Explicit DB transactions:** 48 files, 72 matches. +- **CodeIgniter compatibility surface:** 41 files, 62 matches. +- **Queued or dispatched work:** 7 files, 8 matches. +- **Cache-dependent behavior:** 5 files, 12 matches. + + +Review priority should follow blast radius: auth, finance, payments, attendance, grading, promotions, messaging, files, then lower-risk CRUD. + + + +## 5. Delivery Phases + +### Phase 0: Baseline and Guardrails + +- Add a project map document describing module ownership, service boundaries, and route ownership. +- Add static analysis to CI: PHPStan or Larastan, Pint/PHP-CS-Fixer, and a dead-code detector if practical. +- Add a minimal smoke test suite covering auth, health checks, and one representative CRUD module. +- Create fixtures/factories for User, Student, Family, Enrollment, Invoice, Payment, AttendanceRecord, Score, and Staff. +- Freeze API response contracts for critical endpoints with JSON snapshot tests. + +### Phase 1: Security and Access Control + +- Inventory every controller method and map required role/permission/policy. +- Confirm every mutation endpoint uses authorization before business logic. +- Normalize role switching/session timeout behavior and log security-relevant events. +- Review IP ban, login attempts, password reset cleanup, registration captcha, and auth session flows. +- Add negative tests proving unauthorized roles cannot read or mutate cross-family, cross-student, or cross-staff data. + +### Phase 2: Financial Correctness + +- Define canonical money rules: rounding, currency precision, refund state machine, invoice adjustment rules, and payment reconciliation. +- Wrap invoice/payment/refund/discount mutations in transactions and audit logs. +- Add idempotency rules to PayPal sync, payment notifications, manual payments, refunds, and invoice generation. +- Build reconciliation tests: invoice total = tuition + fees + event charges + extras - discounts - refunds - payments. +- Add report consistency tests so PDFs, dashboard totals, and API totals cannot disagree like three humans in a meeting. + +### Phase 3: Student Lifecycle Correctness + +- Model the lifecycle from registration/enrollment through attendance, grading, report cards, and promotion. +- Add date boundary tests for semesters, attendance windows, class assignment windows, grading locks, and promotion deadlines. +- Centralize student/family visibility checks. +- Make promotion eligibility deterministic and explainable through audit logs. +- Add regression tests for edge cases: missing scores, below-60 notifications, class transfers, inactive students, deleted/unverified users. + +### Phase 4: Communications Reliability + +- Standardize recipient resolution for Email, Messaging, Notifications, WhatsApp, and BroadcastEmail. +- Require preview/dry-run paths for bulk sends. +- Add send logs, dedupe keys, failure states, and retry strategy. +- Separate composition from delivery. +- Test opt-outs, guardian/family recipient selection, teacher/staff targeting, and notification cleanup. + +### Phase 5: Operations and Reporting + +- Review PDF generation paths for badges, certificates, finance reports, report cards, slips, stickers, and print requests. +- Validate all file downloads and upload paths. +- Add operational health endpoints for database, config, cleanup scheduler, and time-sensitive jobs. +- Add runbooks for cleanup commands, payment sync, attendance publishing, and notification dispatch. + +### Phase 6: Documentation and API Contracts + +- Generate OpenAPI output from route definitions, once routes are included. +- Document each module: owner, purpose, inputs, outputs, permissions, side effects, and failure modes. +- Document legacy compatibility areas explicitly, especially CodeIgniter adapters and helper shims. +- Add API examples for primary user flows: login, role switch, parent dashboard, attendance submission, invoice payment, grading submission. + + + +## 6. Module-by-Module Plan + +Each module gets the same discipline: define ownership, confirm validation, confirm authorization, isolate service logic, test happy paths and failures, document side effects. + + + +### Admin + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 0 controllers, 2 services, 3 requests, 5 resources. + +**Services:** CompetitionWinnersDomain, CompetitionWinnersAdminService +**Requests:** StoreCompetitionWinnerRequest, UpdateCompetitionWinnerRequest, SaveCompetitionScoresRequest +**Resources:** PrintRecipientsResource, TeacherSubmissionReportResource, AdminNotificationAlertsResource, ProgressGroupResource, ProgressReportDetailResource +**Plan:** +- Confirm all inputs are validated through FormRequest classes. +- Confirm all reads and writes are authorized. +- Move business rules out of controllers if any remain. +- Add feature tests for list/show/create/update/delete and key failure paths. + +### Administrator + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 8 controllers, 18 services, 5 requests, 0 resources. + +**Controllers:** AdministratorAbsenceController, AdministratorDashboardController, AdministratorEnrollmentController, AdministratorNotificationController, AdministratorPromotionController, AdministratorTeacherSubmissionController, EmergencyContactController, TeacherClassAssignmentController +**Services:** AdministratorUserSearchService, AdministratorAbsenceService, AdministratorDashboardService, AdministratorMetricsService, AdministratorEnrollmentStatusService, AdministratorEnrollmentEventService, AdministratorNotificationService, AdminNotificationSubjectService, TeacherSubmissionSupportService, AdministratorSharedService, TeacherSubmissionNotificationService, TeacherSubmissionReportService ... +**Requests:** SavePrintRecipientsRequest, SaveAdminNotificationSubjectsRequest, SubmitAdministratorAbsenceRequest, UpdateEnrollmentStatusesRequest, SendTeacherSubmissionNotificationsRequest +**Plan:** +- Centralize identity lookup and cross-entity authorization. +- Test directory/search pagination and visibility filtering. +- Ensure updates cannot orphan related records or break family/student/staff links. +- Add lifecycle tests for active, inactive, newly enrolled, transferred, and deleted/unverified users. + +### Api + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 0 controllers, 0 services, 4 requests, 0 resources. + +**Requests:** SaveAdminNotificationSubjectsRequest, IndexAdminProgressRequest, SendTeacherSubmissionNotificationsRequest, SavePrintNotificationRecipientsRequest +**Plan:** +- Confirm all inputs are validated through FormRequest classes. +- Confirm all reads and writes are authorized. +- Move business rules out of controllers if any remain. +- Add feature tests for list/show/create/update/delete and key failure paths. + +### ApiRoot + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 7 controllers, 0 services, 0 requests, 0 resources. + +**Controllers:** BaseApiController, CiRequestAdapter, RolePermissionController, RoleSwitcherController, ScannerController, StatsController, UserController +**Plan:** +- Confirm all inputs are validated through FormRequest classes. +- Confirm all reads and writes are authorized. +- Move business rules out of controllers if any remain. +- Add feature tests for list/show/create/update/delete and key failure paths. + +### Assignment + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 1 controllers, 5 services, 2 requests, 2 resources. + +**Controllers:** AssignmentApiController +**Services:** AssignmentSectionService, AssignmentMetaService, AssignmentService, AssignmentContextService, AssignmentDataLoaderService +**Requests:** StoreAssignmentRequest, StoreStudentAssignmentRequest +**Resources:** AssignmentOverviewResource, AssignmentSectionResource +**Plan:** +- Define the academic workflow state machine and valid transitions. +- Test teacher/admin/parent role boundaries. +- Protect uploads and generated academic files. +- Add audit logs for status changes, submissions, and deadline changes. + +### Attendance + +**Priority:** P1 Student lifecycle +**Detected surface:** 6 controllers, 16 services, 7 requests, 7 resources. + +**Controllers:** AdminAttendanceApiController, AttendanceCommentTemplateController, EarlyDismissalsController, LateSlipLogsController, StaffAttendanceApiController, TeacherAttendanceApiController +**Services:** SemesterRangeService, TeacherAttendanceSubmissionService, AttendanceAutoPublishJobService, AttendanceAutoPublishService, LateSlipLogQueryService, AttendanceConsequenceService, AttendanceRecordSyncService, AttendanceDailySummaryService, AttendanceService, StudentAttendanceWriterService, LateSlipLogCommandService, AttendanceSummaryRebuildService ... +**Requests:** SaveStaffAttendanceCellRequest, UpdateAttendanceManagementRequest, LateSlipLogIndexRequest, TeacherSubmitAttendanceRequest, SaveAdminAttendanceRequest, TeacherUpdateGridRequest, AdminAddAttendanceEntryRequest +**Resources:** DailyAttendanceResource, AdminAttendanceResource, LateSlipLogCollection, LateSlipLogResource, TeacherGridResource, StaffMonthResource, TeacherAttendanceFormResource +**Plan:** +- Lock down date and semester boundary behavior before changing implementation. +- Test teacher/admin/parent visibility separately. +- Ensure auto-publish, notification, late slip, early dismissal, and violation rules are deterministic. +- Record who changed attendance, when, and why. + +### AttendanceCommentTemplate + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 0 controllers, 0 services, 2 requests, 0 resources. + +**Requests:** StoreAttendanceCommentTemplateRequest, UpdateAttendanceCommentTemplateRequest +**Plan:** +- Confirm all inputs are validated through FormRequest classes. +- Confirm all reads and writes are authorized. +- Move business rules out of controllers if any remain. +- Add feature tests for list/show/create/update/delete and key failure paths. + +### AttendanceTracking + +**Priority:** P1 Student lifecycle +**Detected surface:** 1 controllers, 12 services, 5 requests, 0 resources. + +**Controllers:** AttendanceTrackingController +**Services:** AttendanceNotificationLogService, AttendanceViolationStudentResolverService, AttendanceNotificationWorkflowService, AttendanceTrackingService, ViolationRuleEngineService, AttendanceMailerService, AttendanceParentLookupService, AttendanceCommunicationSupportService, AttendanceEmailComposerService, AttendancePendingViolationService, AttendanceCaseQueryService, DefaultAttendanceMailerService +**Requests:** RecordAttendanceTrackingRequest, ComposeAttendanceEmailRequest, SendAttendanceManualEmailRequest, ParentsInfoRequest, SaveAttendanceNotificationNoteRequest +**Plan:** +- Lock down date and semester boundary behavior before changing implementation. +- Test teacher/admin/parent visibility separately. +- Ensure auto-publish, notification, late slip, early dismissal, and violation rules are deterministic. +- Record who changed attendance, when, and why. + +### Auth + +**Priority:** P0 Security/Foundation +**Detected surface:** 7 controllers, 9 services, 3 requests, 1 resources. + +**Controllers:** AuthController, AuthSessionController, IpBanController, RegisterController, RolePermissionController, RoleSwitcherController, SessionTimeoutController +**Services:** RegistrationCaptchaService, PermissionCheckService, CodeIgniterApiRegistrationService, RegistrationService, ApiLoginSecurityService, AuthSessionService, UserRoleService, PasswordResetCleanupService, RegistrationFormatterService +**Requests:** RegisterRequest, SetRoleSessionRequest, LoginSessionRequest +**Resources:** RegisterResource +**Plan:** +- Map every endpoint to required auth state, role, and permission. +- Test login throttling, IP bans, password reset cleanup, session timeout, role switching, and registration captcha. +- Confirm security logs include actor, IP, user agent, outcome, and reason. +- Reject role changes that create privilege escalation or stale-session leaks. + +### BadgeScan + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 1 controllers, 1 services, 0 requests, 0 resources. + +**Controllers:** BadgeScanController +**Services:** BadgeScanService +**Plan:** +- Confirm all inputs are validated through FormRequest classes. +- Confirm all reads and writes are authorized. +- Move business rules out of controllers if any remain. +- Add feature tests for list/show/create/update/delete and key failure paths. + +### Badges + +**Priority:** P2 Operations +**Detected surface:** 1 controllers, 7 services, 3 requests, 0 resources. + +**Controllers:** BadgeController +**Services:** BadgeTextFormatter, BadgeStudentLookupService, BadgeFormDataService, BadgePdfService, BadgeUserLookupService, BadgeService, BadgePrintLogService +**Requests:** BadgePrintStatusRequest, GenerateBadgePdfRequest, LogBadgePrintRequest +**Plan:** +- Review upload/download/PDF generation permissions. +- Validate file type, file size, generated names, and storage paths. +- Add tests for inventory movement consistency and purchase-order state transitions where applicable. +- Make printable outputs reproducible from stable data snapshots. + +### Billing + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 0 controllers, 3 services, 0 requests, 0 resources. + +**Services:** BalanceCalculationService, BillingTotalsService, ChargeService +**Plan:** +- Confirm all inputs are validated through FormRequest classes. +- Confirm all reads and writes are authorized. +- Move business rules out of controllers if any remain. +- Add feature tests for list/show/create/update/delete and key failure paths. + +### BroadcastEmail + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 0 controllers, 5 services, 0 requests, 0 resources. + +**Services:** BroadcastEmailComposerService, BroadcastEmailRecipientService, BroadcastEmailDispatchService, BroadcastEmailImageService, BroadcastEmailSenderOptionsService +**Plan:** +- Separate recipient selection, message composition, dispatch, and logging. +- Add preview/dry-run tests before bulk sending. +- Deduplicate sends and persist failure states. +- Verify opt-outs, guardian selection, role targeting, and retry behavior. + +### Certificates + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 1 controllers, 1 services, 0 requests, 0 resources. + +**Controllers:** CertificateController +**Services:** CertificatePdfService +**Plan:** +- Review upload/download/PDF generation permissions. +- Validate file type, file size, generated names, and storage paths. +- Add tests for inventory movement consistency and purchase-order state transitions where applicable. +- Make printable outputs reproducible from stable data snapshots. + +### ClassPrep + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 0 controllers, 2 services, 0 requests, 0 resources. + +**Services:** ClassRosterService, StickerCountService +**Plan:** +- Confirm all inputs are validated through FormRequest classes. +- Confirm all reads and writes are authorized. +- Move business rules out of controllers if any remain. +- Add feature tests for list/show/create/update/delete and key failure paths. + +### ClassPreparation + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 1 controllers, 9 services, 4 requests, 2 resources. + +**Controllers:** ClassPreparationController +**Services:** ClassPreparationAdjustmentWriterService, ClassPreparationCalculatorService, ClassPreparationLogService, ClassPreparationAdjustmentService, ClassPreparationContextService, ClassPreparationService, ClassPreparationPrintService, ClassPreparationRosterService, ClassPreparationInventoryService +**Requests:** ClassPreparationMarkPrintedRequest, ClassPreparationPrintRequest, ClassPreparationIndexRequest, ClassPreparationAdjustmentRequest +**Resources:** ClassPreparationPrintResource, ClassPreparationResultResource +**Plan:** +- Define the academic workflow state machine and valid transitions. +- Test teacher/admin/parent role boundaries. +- Protect uploads and generated academic files. +- Add audit logs for status changes, submissions, and deadline changes. + +### ClassProgress + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 1 controllers, 5 services, 5 requests, 4 resources. + +**Controllers:** ClassProgressController +**Services:** ClassProgressRuleService, ClassProgressAttachmentService, ClassProgressQueryService, ClassProgressMetaService, ClassProgressMutationService +**Requests:** ClassProgressStoreRequest, ClassProgressMetaRequest, ClassProgressIndexRequest, ClassProgressFormRequest, ClassProgressUpdateRequest +**Resources:** ClassProgressGroupCollection, ClassProgressAttachmentResource, ClassProgressShowResource, ClassProgressReportResource +**Plan:** +- Define the academic workflow state machine and valid transitions. +- Test teacher/admin/parent role boundaries. +- Protect uploads and generated academic files. +- Add audit logs for status changes, submissions, and deadline changes. + +### ClassSections + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 0 controllers, 4 services, 0 requests, 0 resources. + +**Services:** ClassSectionCommandService, ClassAttendanceService, ClassSectionSeedService, ClassSectionQueryService +**Plan:** +- Define the academic workflow state machine and valid transitions. +- Test teacher/admin/parent role boundaries. +- Protect uploads and generated academic files. +- Add audit logs for status changes, submissions, and deadline changes. + +### Classes + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 1 controllers, 0 services, 3 requests, 4 resources. + +**Controllers:** ClassController +**Requests:** ClassSectionUpdateRequest, ClassSectionStoreRequest, ClassSectionIndexRequest +**Resources:** ClassSectionCollection, ClassSectionResource, ClassAttendanceResource, ClassAttendanceStudentResource +**Plan:** +- Define the academic workflow state machine and valid transitions. +- Test teacher/admin/parent role boundaries. +- Protect uploads and generated academic files. +- Add audit logs for status changes, submissions, and deadline changes. + +### Communication + +**Priority:** P1 Communications +**Detected surface:** 1 controllers, 6 services, 0 requests, 0 resources. + +**Controllers:** CommunicationController +**Services:** CommunicationFamilyService, CommunicationSendService, CommunicationTemplateService, CommunicationPreviewService, CommunicationStudentService, CommunicationLogService +**Plan:** +- Separate recipient selection, message composition, dispatch, and logging. +- Add preview/dry-run tests before bulk sending. +- Deduplicate sends and persist failure states. +- Verify opt-outs, guardian selection, role targeting, and retry behavior. + +### CompetitionScores + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 1 controllers, 4 services, 0 requests, 0 resources. + +**Controllers:** CompetitionScoresController +**Services:** CompetitionScoresRosterService, CompetitionScoresSaveService, CompetitionScoresContextService, CompetitionScoresQueryService +**Plan:** +- Confirm all inputs are validated through FormRequest classes. +- Confirm all reads and writes are authorized. +- Move business rules out of controllers if any remain. +- Add feature tests for list/show/create/update/delete and key failure paths. + +### Core + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 2 controllers, 0 services, 0 requests, 0 resources. + +**Controllers:** BaseApiController, CiRequestAdapter +**Plan:** +- Confirm config reads/writes are permissioned and validated. +- Add health/config smoke tests. +- Document public versus authenticated surfaces. +- Keep UI/style/static-page services separated from domain mutations. + +### Dashboard + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 0 controllers, 1 services, 0 requests, 0 resources. + +**Services:** DashboardRouteService +**Plan:** +- Confirm config reads/writes are permissioned and validated. +- Add health/config smoke tests. +- Document public versus authenticated surfaces. +- Keep UI/style/static-page services separated from domain mutations. + +### Discounts + +**Priority:** P0 Financial correctness +**Detected surface:** 1 controllers, 5 services, 3 requests, 2 resources. + +**Controllers:** DiscountController +**Services:** DiscountParentService, DiscountApplyService, DiscountInvoiceService, DiscountVoucherService, DiscountContextService +**Requests:** UpdateDiscountVoucherRequest, ApplyDiscountVoucherRequest, StoreDiscountVoucherRequest +**Resources:** DiscountVoucherResource, DiscountParentResource +**Plan:** +- Define one source of truth for monetary calculations and rounding. +- Require transactions around every mutation that changes balance, invoice state, refund state, payment state, or reimbursement status. +- Add audit logs and idempotency keys for external payment/payment-notification flows. +- Build reconciliation tests across API, reports, dashboards, and PDFs. + +### Docs + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 0 controllers, 3 services, 0 requests, 0 resources. + +**Services:** DocsCatalogService, ApiDocsService, OpenApiRouteExporter +**Plan:** +- Confirm config reads/writes are permissioned and validated. +- Add health/config smoke tests. +- Document public versus authenticated surfaces. +- Keep UI/style/static-page services separated from domain mutations. + +### Documentation + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 2 controllers, 0 services, 0 requests, 0 resources. + +**Controllers:** ApiDocsAdminController, DocsCatalogController +**Plan:** +- Confirm config reads/writes are permissioned and validated. +- Add health/config smoke tests. +- Document public versus authenticated surfaces. +- Keep UI/style/static-page services separated from domain mutations. + +### Email + +**Priority:** P1 Communications +**Detected surface:** 3 controllers, 5 services, 2 requests, 3 resources. + +**Controllers:** BroadcastEmailController, EmailController, EmailExtractorController +**Services:** EmailAttachmentService, EmailExtractorService, EmailProfileService, EmailDispatchService, EmailSenderOptionsService +**Requests:** EmailExtractorCompareRequest, EmailSendRequest +**Resources:** EmailSenderResource, EmailExtractorEmailsResource, EmailExtractorCompareResource +**Plan:** +- Separate recipient selection, message composition, dispatch, and logging. +- Add preview/dry-run tests before bulk sending. +- Deduplicate sends and persist failure states. +- Verify opt-outs, guardian selection, role targeting, and retry behavior. + +### EmergencyContacts + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 0 controllers, 2 services, 2 requests, 3 resources. + +**Services:** EmergencyContactDirectoryService, EmergencyContactCrudService +**Requests:** EmergencyContactParentRequest, EmergencyContactUpdateRequest +**Resources:** EmergencyContactGroupResource, EmergencyContactStudentResource, EmergencyContactResource +**Plan:** +- Confirm all inputs are validated through FormRequest classes. +- Confirm all reads and writes are authorized. +- Move business rules out of controllers if any remain. +- Add feature tests for list/show/create/update/delete and key failure paths. + +### Events + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 0 controllers, 6 services, 6 requests, 3 resources. + +**Services:** EventChargeService, EventStudentChargeService, EventCategoryService, EventListService, EventChargeQueryService, EventManagementService +**Requests:** EventIndexRequest, EventUpdateRequest, EventStoreRequest, EventStudentsWithChargesRequest, EventChargeUpdateRequest, EventChargeIndexRequest +**Resources:** EventChargeResource, EventResource, EventStudentChargeResource +**Plan:** +- Confirm all inputs are validated through FormRequest classes. +- Confirm all reads and writes are authorized. +- Move business rules out of controllers if any remain. +- Add feature tests for list/show/create/update/delete and key failure paths. + +### Exams + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 1 controllers, 4 services, 3 requests, 1 resources. + +**Controllers:** ExamDraftController +**Services:** ExamDraftTeacherService, ExamDraftAdminService, ExamDraftConfigService, ExamDraftFileService +**Requests:** ExamDraftTeacherStoreRequest, ExamDraftAdminLegacyRequest, ExamDraftAdminReviewRequest +**Resources:** ExamDraftResource +**Plan:** +- Define the academic workflow state machine and valid transitions. +- Test teacher/admin/parent role boundaries. +- Protect uploads and generated academic files. +- Add audit logs for status changes, submissions, and deadline changes. + +### Expenses + +**Priority:** P0 Financial correctness +**Detected surface:** 1 controllers, 7 services, 3 requests, 2 resources. + +**Controllers:** ExpenseController +**Services:** ExpenseQueryService, ExpenseStaffService, ExpenseRetailorService, ExpenseReceiptService, ExpenseWriteService, ExpenseContextService, ExpenseStatusService +**Requests:** UpdateExpenseRequest, StoreExpenseRequest, UpdateExpenseStatusRequest +**Resources:** ExpenseResource, ExpenseStaffResource +**Plan:** +- Define one source of truth for monetary calculations and rounding. +- Require transactions around every mutation that changes balance, invoice state, refund state, payment state, or reimbursement status. +- Add audit logs and idempotency keys for external payment/payment-notification flows. +- Build reconciliation tests across API, reports, dashboards, and PDFs. + +### ExtraCharges + +**Priority:** P0 Financial correctness +**Detected surface:** 1 controllers, 6 services, 2 requests, 3 resources. + +**Controllers:** ExtraChargesController +**Services:** ExtraChargesMetaService, ExtraChargesInvoiceService, ExtraChargesParentService, ExtraChargesChargeService, ExtraChargesContextService, ExtraChargesListService +**Requests:** UpdateExtraChargeRequest, StoreExtraChargeRequest +**Resources:** ExtraChargeInvoiceOptionResource, ExtraChargeResource, ExtraChargeParentOptionResource +**Plan:** +- Define one source of truth for monetary calculations and rounding. +- Require transactions around every mutation that changes balance, invoice state, refund state, payment state, or reimbursement status. +- Add audit logs and idempotency keys for external payment/payment-notification flows. +- Build reconciliation tests across API, reports, dashboards, and PDFs. + +### Families + +**Priority:** P1 Student lifecycle +**Detected surface:** 0 controllers, 4 services, 14 requests, 7 resources. + +**Services:** FamilyNotificationService, FamilyFinanceService, FamilyMutationService, FamilyQueryService +**Requests:** FamilyImportLegacyRequest, FamilyUnlinkStudentRequest, FamilySetPrimaryHomeRequest, FamilyAdminCardRequest, FamiliesByStudentRequest, FamilyBootstrapRequest, FamilyAttachSecondByEmailRequest, FamilyAdminSearchRequest ... +**Resources:** FamilyPaymentResource, FamilyInvoiceResource, FamilySearchItemResource, FamilyResource, FamilyGuardianResource, FamilyStudentResource, FamilyEmergencyContactResource +**Plan:** +- Centralize identity lookup and cross-entity authorization. +- Test directory/search pagination and visibility filtering. +- Ensure updates cannot orphan related records or break family/student/staff links. +- Add lifecycle tests for active, inactive, newly enrolled, transferred, and deleted/unverified users. + +### Family + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 2 controllers, 0 services, 0 requests, 0 resources. + +**Controllers:** FamilyAdminController, FamilyController +**Plan:** +- Confirm all inputs are validated through FormRequest classes. +- Confirm all reads and writes are authorized. +- Move business rules out of controllers if any remain. +- Add feature tests for list/show/create/update/delete and key failure paths. + +### Fees + +**Priority:** P0 Financial correctness +**Detected surface:** 0 controllers, 6 services, 2 requests, 6 resources. + +**Services:** FeeConfigService, TuitionCalculationService, FeeRefundCalculatorService, FeeGradeService, FeeRefundDetailService, FeeStudentFeeService +**Requests:** FeeTuitionTotalRequest, FeeRefundRequest +**Resources:** FeeRefundResource, FeeFamilyBalanceResource, ChargeActionResource, ChargeListResource, FeeTuitionBreakdownResource, FeeTuitionTotalResource +**Plan:** +- Define one source of truth for monetary calculations and rounding. +- Require transactions around every mutation that changes balance, invoice state, refund state, payment state, or reimbursement status. +- Add audit logs and idempotency keys for external payment/payment-notification flows. +- Build reconciliation tests across API, reports, dashboards, and PDFs. + +### Files + +**Priority:** P2 Operations +**Detected surface:** 0 controllers, 2 services, 1 requests, 1 resources. + +**Services:** FileServeService, ExamDraftDownloadNameService +**Requests:** FileNameRequest +**Resources:** FileMetaResource +**Plan:** +- Review upload/download/PDF generation permissions. +- Validate file type, file size, generated names, and storage paths. +- Add tests for inventory movement consistency and purchase-order state transitions where applicable. +- Make printable outputs reproducible from stable data snapshots. + +### Finance + +**Priority:** P0 Financial correctness +**Detected surface:** 14 controllers, 8 services, 12 requests, 9 resources. + +**Controllers:** ChargeController, FeeCalculationController, FinancialController, InvoiceController, PaymentController, PaymentEventChargesController, PaymentManualController, PaymentNotificationController, PaymentTransactionController, PaypalPaymentController ... +**Services:** FinancialPdfReportService, FinancialSchoolYearService, FinancialPaymentService, FinancialReportService, FinancialSummaryService, FinancialChartService, FinancialDonationRecipientService, FinancialUnpaidParentsService +**Requests:** FinancialUnpaidParentsRequest, FeeTuitionTotalRequest, ChargeListRequest, FinancialReportRequest, FinancialSummaryRequest, MarkEventChargePaidRequest, FeeTuitionBreakdownRequest, StoreExtraChargeRequest ... +**Resources:** FinancialReportResource, FinancialSummaryResource, FinancialExpenseSummaryResource, FinancialInvoiceResource, FinancialPaymentResource, FinancialRefundResource, FinancialUnpaidParentResource, FinancialDiscountResource ... +**Plan:** +- Define one source of truth for monetary calculations and rounding. +- Require transactions around every mutation that changes balance, invoice state, refund state, payment state, or reimbursement status. +- Add audit logs and idempotency keys for external payment/payment-notification flows. +- Build reconciliation tests across API, reports, dashboards, and PDFs. + +### Frontend + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 4 controllers, 9 services, 2 requests, 6 resources. + +**Controllers:** FrontendController, InfoIconController, LandingPageController, PageController +**Services:** LandingPageTeacherSummaryService, LandingPageRoleService, LandingPageParentDashboardService, LandingPageService, StaticPageService, FrontendPageService, ContactSubmissionService, LandingPageContextService, ProfileIconService +**Requests:** LandingTeacherRequest, ContactSubmitRequest +**Resources:** LandingPageResource, ContactSubmissionResource, FrontendPageResource, FrontendUserResource, ProfileIconResource, PageContentResource +**Plan:** +- Confirm config reads/writes are permissioned and validated. +- Add health/config smoke tests. +- Document public versus authenticated surfaces. +- Keep UI/style/static-page services separated from domain mutations. + +### Grading + +**Priority:** P1 Student lifecycle +**Detected surface:** 2 controllers, 9 services, 18 requests, 3 resources. + +**Controllers:** GradingController, HomeworkTrackingController +**Services:** BelowSixtyEmailService, GradingLockService, GradingOverviewService, GradingPlacementService, HomeworkTrackingService, GradingScoreService, GradingBelowSixtyService, HomeworkTrackingCalendarService, GradingRefreshService +**Requests:** BelowSixtyListRequest, BelowSixtySendRequest, GradingLockAllRequest, BelowSixtyMeetingRequest, PlacementLevelsRequest, GradingRefreshRequest, GradingScoreShowRequest, HomeworkTrackingRequest ... +**Resources:** BelowSixtyStudentResource, GradingScoreResource, HomeworkTrackingTeacherResource +**Plan:** +- Centralize score formulas and missing-score behavior. +- Add golden test cases for semester score, quiz, homework, project, participation, final/midterm, and prediction risk calculations. +- Prove grading locks block writes but not allowed reads. +- Verify below-60 communication and parent visibility rules. + +### Incidents + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 1 controllers, 4 services, 5 requests, 4 resources. + +**Controllers:** IncidentController +**Services:** CurrentIncidentService, IncidentHistoryService, IncidentLookupService, IncidentAnalysisService +**Requests:** IncidentCancelRequest, IncidentCloseRequest, IncidentStoreRequest, IncidentListRequest, IncidentStateRequest +**Resources:** IncidentStudentOptionResource, IncidentAnalysisStudentResource, IncidentResource, IncidentGradeResource +**Plan:** +- Confirm all inputs are validated through FormRequest classes. +- Confirm all reads and writes are authorized. +- Move business rules out of controllers if any remain. +- Add feature tests for list/show/create/update/delete and key failure paths. + +### Inventory + +**Priority:** P2 Operations +**Detected surface:** 5 controllers, 8 services, 19 requests, 5 resources. + +**Controllers:** InventoryCategoryController, InventoryController, InventoryMovementController, SupplierController, SupplyCategoryController +**Services:** InventorySummaryService, InventoryCategoryService, InventoryItemService, InventoryContextService, SupplierService, InventoryMovementService, SupplyCategoryService, InventoryTeacherDistributionService +**Requests:** InventoryItemIndexRequest, SupplierUpdateRequest, InventoryCategoryStoreRequest, InventoryTeacherDistributionRequest, InventoryItemUpdateRequest, InventoryItemStoreRequest, InventoryTeacherDistributionStoreRequest, InventoryCategoryUpdateRequest ... +**Resources:** InventoryItemResource, InventoryCategoryResource, InventoryMovementResource, SupplierResource, SupplyCategoryResource +**Plan:** +- Review upload/download/PDF generation permissions. +- Validate file type, file size, generated names, and storage paths. +- Add tests for inventory movement consistency and purchase-order state transitions where applicable. +- Make printable outputs reproducible from stable data snapshots. + +### Invoices + +**Priority:** P0 Financial correctness +**Detected surface:** 0 controllers, 8 services, 4 requests, 2 resources. + +**Services:** InvoiceGradeService, InvoiceConfigService, InvoicePaymentService, InvoiceTuitionService, InvoiceManagementService, InvoicePdfService, InvoiceService, InvoiceGenerationService +**Requests:** InvoiceStatusRequest, InvoiceManagementRequest, InvoiceParentRequest, InvoiceGenerateRequest +**Resources:** InvoiceManagementParentResource, InvoiceResource +**Plan:** +- Define one source of truth for monetary calculations and rounding. +- Require transactions around every mutation that changes balance, invoice state, refund state, payment state, or reimbursement status. +- Add audit logs and idempotency keys for external payment/payment-notification flows. +- Build reconciliation tests across API, reports, dashboards, and PDFs. + +### Messaging + +**Priority:** P1 Communications +**Detected surface:** 2 controllers, 3 services, 3 requests, 3 resources. + +**Controllers:** MessagesController, WhatsappController +**Services:** MessageRecipientService, MessageCommandService, MessageQueryService +**Requests:** MessageSendRequest, MessageIndexRequest, MessageUpdateRequest +**Resources:** MessageResource, MessageCollection, RecipientResource +**Plan:** +- Separate recipient selection, message composition, dispatch, and logging. +- Add preview/dry-run tests before bulk sending. +- Deduplicate sends and persist failure states. +- Verify opt-outs, guardian selection, role targeting, and retry behavior. + +### Navigation + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 0 controllers, 2 services, 0 requests, 0 resources. + +**Services:** NavBuilderService, NavbarService +**Plan:** +- Confirm config reads/writes are permissioned and validated. +- Add health/config smoke tests. +- Document public versus authenticated surfaces. +- Keep UI/style/static-page services separated from domain mutations. + +### NonApi + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 3 controllers, 0 services, 0 requests, 0 resources. + +**Controllers:** Controller, DocsController, TeacherCalendarPageController +**Plan:** +- Confirm all inputs are validated through FormRequest classes. +- Confirm all reads and writes are authorized. +- Move business rules out of controllers if any remain. +- Add feature tests for list/show/create/update/delete and key failure paths. + +### Notifications + +**Priority:** P1 Communications +**Detected surface:** 1 controllers, 12 services, 5 requests, 2 resources. + +**Controllers:** NotificationController +**Services:** NotificationDispatchService, NotificationRecipientService, NotificationShowService, NotificationManagementService, NotificationReadService, NotificationUserListService, NotificationCleanupService, UserNotificationDispatchService, NotificationTriggerService, NotificationSendService, NotificationActiveService, NotificationDeletedService +**Requests:** NotificationSendRequest, NotificationActiveRequest, NotificationDeletedRequest, NotificationUpdateRequest, NotificationIndexRequest +**Resources:** UserNotificationResource, NotificationResource +**Plan:** +- Separate recipient selection, message composition, dispatch, and logging. +- Add preview/dry-run tests before bulk sending. +- Deduplicate sends and persist failure states. +- Verify opt-outs, guardian selection, role targeting, and retry behavior. + +### Parents + +**Priority:** P1 Student lifecycle +**Detected surface:** 5 controllers, 14 services, 16 requests, 4 resources. + +**Controllers:** AuthorizedUserInviteController, AuthorizedUsersController, ParentAttendanceReportController, ParentController, ParentPromotionController +**Services:** ParentEventParticipationService, ParentRegistrationService, PrimaryParentUserResolver, ParentAttendanceReportCalendarService, ParentAttendanceService, AuthorizedUsersManagementService, ParentEnrollmentService, ParentInvoiceService, ParentProgressQueryService, ParentAttendanceReportService, ParentReportCardService, ParentConfigService ... +**Requests:** ParentAttendanceReportUpdateRequest, ParentAttendanceReportListRequest, ParentAttendanceReportSubmitRequest, ParentEmergencyContactRequest, UpdateAuthorizedUserRequest, ParentAttendanceReportCheckRequest, ParentProfileUpdateRequest, ParentInvoiceRequest ... +**Resources:** ParentEmergencyContactResource, ParentAttendanceReportResource, ParentStudentResource, ParentAttendanceResource +**Plan:** +- Centralize identity lookup and cross-entity authorization. +- Test directory/search pagination and visibility filtering. +- Ensure updates cannot orphan related records or break family/student/staff links. +- Add lifecycle tests for active, inactive, newly enrolled, transferred, and deleted/unverified users. + +### Payments + +**Priority:** P0 Financial correctness +**Detected surface:** 0 controllers, 14 services, 15 requests, 4 resources. + +**Services:** PaymentNotificationDispatchService, PaymentMissedCheckService, PaymentManualService, PaymentLookupService, PaymentTestNotificationService, PaypalPaymentSyncService, PaymentBalanceService, PaypalPaymentService, PaymentTransactionService, PaymentEnrollmentEventService, PaymentNotificationService, PaymentPlanService ... +**Requests:** PaymentManualSearchRequest, PaymentEventChargesListRequest, PaymentNotificationSendRequest, PaymentNotificationListRequest, PaymentCreateRequest, PaypalTransactionsListRequest, PaymentManualEditRequest, PaymentEventChargesStoreRequest ... +**Resources:** PaymentNotificationLogResource, PaymentTransactionResource, PaypalTransactionResource, PaymentResource +**Plan:** +- Define one source of truth for monetary calculations and rounding. +- Require transactions around every mutation that changes balance, invoice state, refund state, payment state, or reimbursement status. +- Add audit logs and idempotency keys for external payment/payment-notification flows. +- Build reconciliation tests across API, reports, dashboards, and PDFs. + +### Phone + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 0 controllers, 1 services, 0 requests, 0 resources. + +**Services:** PhoneFormatterService +**Plan:** +- Confirm all inputs are validated through FormRequest classes. +- Confirm all reads and writes are authorized. +- Move business rules out of controllers if any remain. +- Add feature tests for list/show/create/update/delete and key failure paths. + +### Policy + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 0 controllers, 1 services, 0 requests, 0 resources. + +**Services:** PolicyContentService +**Plan:** +- Confirm config reads/writes are permissioned and validated. +- Add health/config smoke tests. +- Document public versus authenticated surfaces. +- Keep UI/style/static-page services separated from domain mutations. + +### Preferences + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 0 controllers, 3 services, 2 requests, 3 resources. + +**Services:** PreferencesCommandService, PreferencesQueryService, PreferencesOptionsService +**Requests:** PreferencesIndexRequest, PreferencesUpsertRequest +**Resources:** PreferencesListResource, PreferencesCollection, PreferencesResource +**Plan:** +- Confirm config reads/writes are permissioned and validated. +- Add health/config smoke tests. +- Document public versus authenticated surfaces. +- Keep UI/style/static-page services separated from domain mutations. + +### PrintRequests + +**Priority:** P2 Operations +**Detected surface:** 1 controllers, 1 services, 0 requests, 0 resources. + +**Controllers:** PrintRequestsController +**Services:** PrintRequestsPortalService +**Plan:** +- Review upload/download/PDF generation permissions. +- Validate file type, file size, generated names, and storage paths. +- Add tests for inventory movement consistency and purchase-order state transitions where applicable. +- Make printable outputs reproducible from stable data snapshots. + +### Promotions + +**Priority:** P1 Student lifecycle +**Detected surface:** 0 controllers, 7 services, 8 requests, 4 resources. + +**Services:** PromotionStatusService, PromotionEnrollmentService, PromotionReminderService, LevelProgressionService, PromotionAuditService, PromotionQueryService, PromotionEligibilityService +**Requests:** SendReminderRequest, ParentPromotionOverviewRequest, AdminListPromotionsRequest, EvaluateEligibilityRequest, UpdatePromotionStatusRequest, ParentEnrollmentStepRequest, SetEnrollmentDeadlineRequest, UpsertLevelProgressionRequest +**Resources:** LevelProgressionResource, PromotionAuditLogResource, PromotionReminderResource, StudentPromotionResource +**Plan:** +- Define the academic workflow state machine and valid transitions. +- Test teacher/admin/parent role boundaries. +- Protect uploads and generated academic files. +- Add audit logs for status changes, submissions, and deadline changes. + +### Public + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 1 controllers, 0 services, 0 requests, 0 resources. + +**Controllers:** PublicWinnersController +**Plan:** +- Confirm config reads/writes are permissioned and validated. +- Add health/config smoke tests. +- Document public versus authenticated surfaces. +- Keep UI/style/static-page services separated from domain mutations. + +### PublicWinners + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 0 controllers, 1 services, 0 requests, 0 resources. + +**Services:** PublicWinnersPortalService +**Plan:** +- Confirm all inputs are validated through FormRequest classes. +- Confirm all reads and writes are authorized. +- Move business rules out of controllers if any remain. +- Add feature tests for list/show/create/update/delete and key failure paths. + +### PurchaseOrders + +**Priority:** P2 Operations +**Detected surface:** 0 controllers, 4 services, 3 requests, 2 resources. + +**Services:** PurchaseOrderStatusService, PurchaseOrderReceiveService, PurchaseOrderCreateService, PurchaseOrderQueryService +**Requests:** PurchaseOrderIndexRequest, PurchaseOrderStoreRequest, PurchaseOrderReceiveRequest +**Resources:** PurchaseOrderResource, PurchaseOrderItemResource +**Plan:** +- Review upload/download/PDF generation permissions. +- Validate file type, file size, generated names, and storage paths. +- Add tests for inventory movement consistency and purchase-order state transitions where applicable. +- Make printable outputs reproducible from stable data snapshots. + +### Refunds + +**Priority:** P0 Financial correctness +**Detected surface:** 0 controllers, 9 services, 6 requests, 1 resources. + +**Services:** RefundSummaryService, RefundInvoiceAdjustmentService, RefundPolicyService, RefundPayoutService, RefundRequestService, RefundQueryService, RefundOverpaymentService, RefundDecisionService, RefundNotificationService +**Requests:** RefundStoreRequest, RefundPaymentRequest, RefundDecisionRequest, RefundRecalculateRequest, RefundIndexRequest, RefundRejectRequest +**Resources:** RefundResource +**Plan:** +- Define one source of truth for monetary calculations and rounding. +- Require transactions around every mutation that changes balance, invoice state, refund state, payment state, or reimbursement status. +- Add audit logs and idempotency keys for external payment/payment-notification flows. +- Build reconciliation tests across API, reports, dashboards, and PDFs. + +### Reimbursements + +**Priority:** P0 Financial correctness +**Detected surface:** 0 controllers, 11 services, 13 requests, 5 resources. + +**Services:** ReimbursementFileService, ReimbursementBatchAssignmentService, ReimbursementContextService, ReimbursementExportService, ReimbursementLookupService, ReimbursementEmailService, ReimbursementRecipientService, ReimbursementBatchService, ReimbursementQueryService, ReimbursementDonationService, ReimbursementCrudService +**Requests:** ReimbursementBatchAdminFileRequest, ReimbursementUpdateRequest, ReimbursementBatchCreateRequest, ReimbursementBatchEmailRequest, ReimbursementProcessRequest, ReimbursementExportBatchRequest, ReimbursementIndexRequest, ReimbursementExportRequest ... +**Resources:** ReimbursementRecipientResource, ReimbursementExpenseResource, ReimbursementBatchResource, ReimbursementResource, ReimbursementUnderProcessingItemResource +**Plan:** +- Define one source of truth for monetary calculations and rounding. +- Require transactions around every mutation that changes balance, invoice state, refund state, payment state, or reimbursement status. +- Add audit logs and idempotency keys for external payment/payment-notification flows. +- Build reconciliation tests across API, reports, dashboards, and PDFs. + +### Reports + +**Priority:** P2 Operations +**Detected surface:** 4 controllers, 9 services, 11 requests, 8 resources. + +**Controllers:** FilesController, ReportCardsController, SlipPrinterController, StickersController +**Services:** SlipPrinterFormatterService, SlipPrinterPdfService, SlipPrinterService, SlipPrinterConfigService, StickerPrintService, StickerPresetService, StickerQueryService, StickerContextService, ReportCardService +**Requests:** SlipPreviewRequest, SlipLogListRequest, SlipPrintRequest, SlipReprintRequest, StickerStudentsByClassRequest, StickerPrintRequest, StickerFormDataRequest, ReportCardCompletenessRequest ... +**Resources:** LateSlipLogResource, StickerStudentResource, StickerPresetResource, StickerClassSectionResource, ReportCardStudentMetaResource, ReportCardAcknowledgementResource, ReportCardClassSectionResource, ReportCardCompletenessStudentResource +**Plan:** +- Review upload/download/PDF generation permissions. +- Validate file type, file size, generated names, and storage paths. +- Add tests for inventory movement consistency and purchase-order state transitions where applicable. +- Make printable outputs reproducible from stable data snapshots. + +### Roles + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 0 controllers, 8 services, 9 requests, 4 resources. + +**Services:** RolePermissionService, RoleDashboardService, RoleCrudService, RoleAssignmentService, RoleSwitchService, RoleStaffService, PermissionCrudService, RoleQueryService +**Requests:** AssignUserRolesRequest, RolePermissionUpdateRequest, UserRoleListRequest, PermissionUpdateRequest, PermissionStoreRequest, RoleSwitchRequest, RoleStoreRequest, RoleUpdateRequest ... +**Resources:** RolePermissionResource, RoleResource, UserWithRolesResource, PermissionResource +**Plan:** +- Confirm all inputs are validated through FormRequest classes. +- Confirm all reads and writes are authorized. +- Move business rules out of controllers if any remain. +- Add feature tests for list/show/create/update/delete and key failure paths. + +### Root + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 0 controllers, 8 services, 1 requests, 1 resources. + +**Services:** FeeCalculationService, SemesterRangeService, ApplicationUrlService, SchoolIdService, AssignmentService, AttendanceCommentTemplateService, PhoneFormatterService, EmailService +**Requests:** ApiFormRequest +**Resources:** TeacherSubmissionRowResource +**Plan:** +- Confirm all inputs are validated through FormRequest classes. +- Confirm all reads and writes are authorized. +- Move business rules out of controllers if any remain. +- Add feature tests for list/show/create/update/delete and key failure paths. + +### School + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 0 controllers, 4 services, 0 requests, 0 resources. + +**Services:** AccountEventService, SemesterSelectionService, EnrollmentEventService, PaymentEventService +**Plan:** +- Confirm config reads/writes are permissioned and validated. +- Add health/config smoke tests. +- Document public versus authenticated surfaces. +- Keep UI/style/static-page services separated from domain mutations. + +### SchoolIds + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 0 controllers, 2 services, 1 requests, 1 resources. + +**Services:** SchoolIdAssignmentService, SchoolIdGenerationService +**Requests:** SchoolIdAssignRequest +**Resources:** SchoolIdResource +**Plan:** +- Confirm config reads/writes are permissioned and validated. +- Add health/config smoke tests. +- Document public versus authenticated surfaces. +- Keep UI/style/static-page services separated from domain mutations. + +### Scores + +**Priority:** P1 Student lifecycle +**Detected surface:** 9 controllers, 16 services, 10 requests, 3 resources. + +**Controllers:** FinalController, HomeworkController, MidtermController, ParticipationController, ProjectController, QuizController, ScoreCommentController, ScoreController, ScorePredictorController +**Services:** ParticipationScoreService, QuizCalculator, QuizScoreService, ScoreCommentService, ScoreDashboardService, HomeworkCalculator, SemesterScoreService, ScorePredictorRiskService, ProjectScoreService, HomeworkScoreService, ScoreTermService, ProjectCalculator ... +**Requests:** ScoreCommentSaveRequest, ScorePredictorRequest, ScoreLockRequest, ScoreUpdateRequest, ScoreOverviewRequest, ScoreStudentRequest, ScoreCommentListRequest, ScoreClassRequest ... +**Resources:** ScorePredictorStudentResource, ScoreCommentResource, ScoreStudentResource +**Plan:** +- Centralize score formulas and missing-score behavior. +- Add golden test cases for semester score, quiz, homework, project, participation, final/midterm, and prediction risk calculations. +- Prove grading locks block writes but not allowed reads. +- Verify below-60 communication and parent visibility rules. + +### Security + +**Priority:** P0 Security/Foundation +**Detected surface:** 0 controllers, 3 services, 3 requests, 2 resources. + +**Services:** IpBanCommandService, IpBanQueryService, Pbkdf2Hasher +**Requests:** IpBanIndexRequest, IpUnbanRequest, IpBanRequest +**Resources:** IpBanCollection, IpBanResource +**Plan:** +- Map every endpoint to required auth state, role, and permission. +- Test login throttling, IP bans, password reset cleanup, session timeout, role switching, and registration captcha. +- Confirm security logs include actor, IP, user agent, outcome, and reason. +- Reject role changes that create privilege escalation or stale-session leaks. + +### Semesters + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 0 controllers, 1 services, 4 requests, 2 resources. + +**Services:** SemesterConfigService +**Requests:** SchoolYearRangeRequest, SemesterResolveRequest, SemesterNormalizeRequest, SemesterRangeRequest +**Resources:** SemesterResolveResource, SemesterRangeResource +**Plan:** +- Confirm config reads/writes are permissioned and validated. +- Add health/config smoke tests. +- Document public versus authenticated surfaces. +- Keep UI/style/static-page services separated from domain mutations. + +### Settings + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 6 controllers, 8 services, 8 requests, 5 resources. + +**Controllers:** ConfigurationAdminController, EventController, PolicyController, PreferencesController, SchoolCalendarController, SettingsController +**Services:** SettingsService, ConfigurationService, SchoolCalendarMutationService, SchoolCalendarNotificationService, SchoolCalendarContextService, SchoolCalendarQueryService, SchoolCalendarFormatterService, SchoolCalendarMeetingService +**Requests:** SchoolCalendarStoreRequest, ConfigurationStoreRequest, SettingsUpdateRequest, SchoolCalendarIndexRequest, ConfigurationUpdateRequest, SchoolCalendarUpdateRequest, PolicyShowRequest, SchoolCalendarOptionsRequest +**Resources:** PolicyResource, SchoolCalendarEventResource, SettingsResource, ConfigurationResource, SchoolCalendarEventDetailResource +**Plan:** +- Confirm config reads/writes are permissioned and validated. +- Add health/config smoke tests. +- Document public versus authenticated surfaces. +- Keep UI/style/static-page services separated from domain mutations. + +### Staff + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 3 controllers, 3 services, 3 requests, 2 resources. + +**Controllers:** StaffController, TeacherController, TimeOffNotificationController +**Services:** StaffQueryService, StaffTimeOffLinkService, StaffCommandService +**Requests:** StaffIndexRequest, StaffUpdateRequest, StaffStoreRequest +**Resources:** StaffCollection, StaffResource +**Plan:** +- Centralize identity lookup and cross-entity authorization. +- Test directory/search pagination and visibility filtering. +- Ensure updates cannot orphan related records or break family/student/staff links. +- Add lifecycle tests for active, inactive, newly enrolled, transferred, and deleted/unverified users. + +### Students + +**Priority:** P1 Student lifecycle +**Detected surface:** 1 controllers, 7 services, 7 requests, 4 resources. + +**Controllers:** StudentController +**Services:** StudentScoreCardService, StudentStatusService, StudentDirectoryService, StudentAutoDistributionService, StudentConfigService, StudentProfileService, StudentAssignmentService +**Requests:** StudentRemoveClassRequest, StudentUpdateRequest, StudentAssignClassRequest, StudentSetActiveRequest, StudentPromotionTotalsRequest, StudentAssignmentListRequest, StudentAutoDistributeRequest +**Resources:** StudentAssignmentResource, StudentRemovedResource, StudentClassSectionResource, StudentScoreCardRowResource +**Plan:** +- Centralize identity lookup and cross-entity authorization. +- Test directory/search pagination and visibility filtering. +- Ensure updates cannot orphan related records or break family/student/staff links. +- Add lifecycle tests for active, inactive, newly enrolled, transferred, and deleted/unverified users. + +### Subjects + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 1 controllers, 1 services, 2 requests, 2 resources. + +**Controllers:** SubjectCurriculumController +**Services:** SubjectCurriculumService +**Requests:** SubjectCurriculumUpdateRequest, SubjectCurriculumStoreRequest +**Resources:** SubjectCurriculumResource, SubjectClassResource +**Plan:** +- Define the academic workflow state machine and valid transitions. +- Test teacher/admin/parent role boundaries. +- Protect uploads and generated academic files. +- Add audit logs for status changes, submissions, and deadline changes. + +### Support + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 2 controllers, 2 services, 3 requests, 3 resources. + +**Controllers:** ContactController, SupportController +**Services:** SupportRequestService, ContactMessageService +**Requests:** SupportRequestStoreRequest, SupportRequestIndexRequest, ContactSendRequest +**Resources:** ContactMessageResource, SupportRequestResource, SupportRequestCollection +**Plan:** +- Confirm all inputs are validated through FormRequest classes. +- Confirm all reads and writes are authorized. +- Move business rules out of controllers if any remain. +- Add feature tests for list/show/create/update/delete and key failure paths. + +### System + +**Priority:** P0 Security/Foundation +**Detected surface:** 9 controllers, 6 services, 2 requests, 4 resources. + +**Controllers:** AccessDeniedController, DashboardController, DashboardRedirectController, DatabaseHealthController, HealthController, NavBuilderController, SchoolIdController, SemesterRangeController, StatsController +**Services:** HealthCheckService, CleanupSchedulerService, GlobalConfigService, DatabaseHealthService, ConfigUpdateService, TimeService +**Requests:** NavItemStoreRequest, NavItemReorderRequest +**Resources:** DashboardRouteResource, NavItemResource, HealthStatusResource, NavBuilderDataResource +**Plan:** +- Confirm config reads/writes are permissioned and validated. +- Add health/config smoke tests. +- Document public versus authenticated surfaces. +- Keep UI/style/static-page services separated from domain mutations. + +### Teachers + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 0 controllers, 4 services, 6 requests, 4 resources. + +**Services:** TeacherAbsenceService, TeacherDashboardService, TeacherConfigService, TeacherAssignmentService +**Requests:** TeacherClassViewRequest, TeacherAbsenceSubmitRequest, TeacherAssignmentDeleteRequest, TeacherAssignmentStoreRequest, TeacherAssignmentListRequest, TeacherSwitchClassRequest +**Resources:** TeacherAbsenceResource, TeacherStudentResource, TeacherClassResource, TeacherAssignmentResource +**Plan:** +- Centralize identity lookup and cross-entity authorization. +- Test directory/search pagination and visibility filtering. +- Ensure updates cannot orphan related records or break family/student/staff links. +- Add lifecycle tests for active, inactive, newly enrolled, transferred, and deleted/unverified users. + +### Ui + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 1 controllers, 1 services, 1 requests, 1 resources. + +**Controllers:** UiController +**Services:** UiStyleService +**Requests:** UiStyleRequest +**Resources:** UiStyleResource +**Plan:** +- Confirm config reads/writes are permissioned and validated. +- Add health/config smoke tests. +- Document public versus authenticated surfaces. +- Keep UI/style/static-page services separated from domain mutations. + +### Users + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 1 controllers, 6 services, 4 requests, 2 resources. + +**Controllers:** UserController +**Services:** UserListService, LoginActivityService, InactiveUserCleanupService, UserManagementService, UserEventService, DeleteUnverifiedUserService +**Requests:** UserStoreRequest, UserListRequest, UserUpdateRequest, LoginActivityRequest +**Resources:** LoginActivityResource, UserWithRolesResource +**Plan:** +- Confirm all inputs are validated through FormRequest classes. +- Confirm all reads and writes are authorized. +- Move business rules out of controllers if any remain. +- Add feature tests for list/show/create/update/delete and key failure paths. + +### Utilities + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 2 controllers, 0 services, 1 requests, 1 resources. + +**Controllers:** PhoneFormatterController, ProofreadController +**Requests:** PhoneFormatRequest +**Resources:** PhoneFormatResource +**Plan:** +- Confirm config reads/writes are permissioned and validated. +- Add health/config smoke tests. +- Document public versus authenticated surfaces. +- Keep UI/style/static-page services separated from domain mutations. + +### Whatsapp + +**Priority:** P1 Communications +**Detected surface:** 0 controllers, 8 services, 7 requests, 5 resources. + +**Services:** WhatsappContactService, WhatsappInviteService, WhatsappLinkService, WhatsappContextService, WhatsappInviteNotificationService, WhatsappMembershipService, WhatsappInviteBundleService, WhatsappInviteEmailService +**Requests:** WhatsappLinkIndexRequest, WhatsappMembershipUpdateRequest, WhatsappLinkStoreRequest, WhatsappLinkUpdateRequest, WhatsappParentContactsRequest, WhatsappParentContactsByClassRequest, WhatsappInviteSendRequest +**Resources:** WhatsappParentContactResource, WhatsappGroupLinkResource, WhatsappClassSectionParentResource, WhatsappGroupLinkCollection, WhatsappClassSectionContactResource +**Plan:** +- Separate recipient selection, message composition, dispatch, and logging. +- Add preview/dry-run tests before bulk sending. +- Deduplicate sends and persist failure states. +- Verify opt-outs, guardian selection, role targeting, and retry behavior. + + +## 7. Cross-Cutting Test Matrix + +| Area | Minimum tests | Failure tests | Regression traps | +|---|---:|---:|---| +| Auth / Security | Login, refresh, logout, role switch, timeout | Invalid creds, banned IP, stale role, unauthorized role | Privilege escalation, session confusion | +| Finance / Payments | Invoice generation, manual payment, PayPal sync, refunds | Duplicate webhook, failed payment, partial refund | Balance mismatch, rounding drift | +| Attendance | Teacher submit, admin edit, parent report | Out-of-window edit, unauthorized class, duplicate submit | Semester boundary, timezone/date errors | +| Grading / Scores | Score calculations, locks, below-60 notification | Missing score, locked write, invalid class | Formula drift, notification spam | +| Students / Families | Profile, guardians, enrollments, authorized users | Cross-family read/write, inactive student | Orphan relationships | +| Communications | Preview, send, log, retry | Invalid recipient, opt-out, duplicate send | Bulk-send mistakes | +| Files / Reports | Upload, download, PDF generation | Bad MIME, oversize file, unauthorized file | Path traversal, stale report data | +| Inventory / Ops | Item movement, PO receive, print requests | Negative stock, duplicate receive | Count mismatch | + + +## 8. Refactor Rules + +- Do not start by renaming everything. That is not architecture; that is sweeping broken glass into a different corner. +- Start with tests around the highest-risk behavior, then refactor behind those tests. +- One module per pull request unless a shared contract demands otherwise. +- For each PR, include: risk summary, affected endpoints, permission impact, migration impact, rollback path, and test evidence. +- Any service over roughly 300 lines should be reviewed for split points, but line count alone is not a crime. Unclear responsibility is the crime. +- Any controller doing calculations, direct DB orchestration, or multi-step workflow should delegate to a service. +- Any duplicated request validation should be consolidated into shared rules or dedicated FormRequests. + +## 9. Immediate Next Work Queue + +1. Add route inventory from `routes/api.php` and `routes/web.php` once the full repo is available. +2. Produce an endpoint-permission matrix from routes + controllers + policies. +3. Add baseline feature tests for Auth, Finance/Payments, Attendance, Grading/Scores, Students/Families, and Communications. +4. Create factories and seed data for the core school domain. +5. Add OpenAPI generation to CI using the existing Docs/OpenAPI service surface. +6. Review raw SQL and direct request access matches before any large refactor. +7. Add idempotency and audit requirements to payment, invoice, refund, promotion, and messaging mutations. +8. Write runbooks for console commands and operational cleanup jobs. + +## 10. Definition of Done + +A module is not considered done until all of this is true: + +- All public endpoints have route docs and permission mapping. +- All inputs use FormRequests or clearly justified validation. +- All responses use Resources or documented response builders. +- All mutations are transactional where consistency matters. +- All side effects are logged and testable. +- All critical behavior has feature tests and at least one negative authorization test. +- All money/date/file/message behavior has edge-case tests. +- No endpoint relies on undocumented legacy behavior unless that behavior is explicitly preserved and tested. + +## Appendix A: Controller Groups + +- **Administrator:** 8 controllers. AdministratorAbsenceController, AdministratorDashboardController, AdministratorEnrollmentController, AdministratorNotificationController, AdministratorPromotionController, AdministratorTeacherSubmissionController, EmergencyContactController, TeacherClassAssignmentController +- **ApiRoot:** 7 controllers. BaseApiController, CiRequestAdapter, RolePermissionController, RoleSwitcherController, ScannerController, StatsController, UserController +- **Assignment:** 1 controllers. AssignmentApiController +- **Attendance:** 6 controllers. AdminAttendanceApiController, AttendanceCommentTemplateController, EarlyDismissalsController, LateSlipLogsController, StaffAttendanceApiController, TeacherAttendanceApiController +- **AttendanceTracking:** 1 controllers. AttendanceTrackingController +- **Auth:** 7 controllers. AuthController, AuthSessionController, IpBanController, RegisterController, RolePermissionController, RoleSwitcherController, SessionTimeoutController +- **BadgeScan:** 1 controllers. BadgeScanController +- **Badges:** 1 controllers. BadgeController +- **Certificates:** 1 controllers. CertificateController +- **ClassPreparation:** 1 controllers. ClassPreparationController +- **ClassProgress:** 1 controllers. ClassProgressController +- **Classes:** 1 controllers. ClassController +- **Communication:** 1 controllers. CommunicationController +- **CompetitionScores:** 1 controllers. CompetitionScoresController +- **Core:** 2 controllers. BaseApiController, CiRequestAdapter +- **Discounts:** 1 controllers. DiscountController +- **Documentation:** 2 controllers. ApiDocsAdminController, DocsCatalogController +- **Email:** 3 controllers. BroadcastEmailController, EmailController, EmailExtractorController +- **Exams:** 1 controllers. ExamDraftController +- **Expenses:** 1 controllers. ExpenseController +- **ExtraCharges:** 1 controllers. ExtraChargesController +- **Family:** 2 controllers. FamilyAdminController, FamilyController +- **Finance:** 14 controllers. ChargeController, FeeCalculationController, FinancialController, InvoiceController, PaymentController, PaymentEventChargesController, PaymentManualController, PaymentNotificationController, PaymentTransactionController, PaypalPaymentController, PaypalTransactionsController, PurchaseOrderController, RefundController, ReimbursementController +- **Frontend:** 4 controllers. FrontendController, InfoIconController, LandingPageController, PageController +- **Grading:** 2 controllers. GradingController, HomeworkTrackingController +- **Incidents:** 1 controllers. IncidentController +- **Inventory:** 5 controllers. InventoryCategoryController, InventoryController, InventoryMovementController, SupplierController, SupplyCategoryController +- **Messaging:** 2 controllers. MessagesController, WhatsappController +- **NonApi:** 3 controllers. Controller, DocsController, TeacherCalendarPageController +- **Notifications:** 1 controllers. NotificationController +- **Parents:** 5 controllers. AuthorizedUserInviteController, AuthorizedUsersController, ParentAttendanceReportController, ParentController, ParentPromotionController +- **PrintRequests:** 1 controllers. PrintRequestsController +- **Public:** 1 controllers. PublicWinnersController +- **Reports:** 4 controllers. FilesController, ReportCardsController, SlipPrinterController, StickersController +- **Scores:** 9 controllers. FinalController, HomeworkController, MidtermController, ParticipationController, ProjectController, QuizController, ScoreCommentController, ScoreController, ScorePredictorController +- **Settings:** 6 controllers. ConfigurationAdminController, EventController, PolicyController, PreferencesController, SchoolCalendarController, SettingsController +- **Staff:** 3 controllers. StaffController, TeacherController, TimeOffNotificationController +- **Students:** 1 controllers. StudentController +- **Subjects:** 1 controllers. SubjectCurriculumController +- **Support:** 2 controllers. ContactController, SupportController +- **System:** 9 controllers. AccessDeniedController, DashboardController, DashboardRedirectController, DatabaseHealthController, HealthController, NavBuilderController, SchoolIdController, SemesterRangeController, StatsController +- **Ui:** 1 controllers. UiController +- **Users:** 1 controllers. UserController +- **Utilities:** 2 controllers. PhoneFormatterController, ProofreadController + +## Appendix B: Service Groups + +- **Admin:** 2 services, 1,089 LOC. CompetitionWinnersDomain, CompetitionWinnersAdminService +- **Administrator:** 18 services, 2,196 LOC. AdministratorUserSearchService, AdministratorAbsenceService, AdministratorDashboardService, AdministratorMetricsService, AdministratorEnrollmentStatusService, AdministratorEnrollmentEventService, AdministratorNotificationService, AdminNotificationSubjectService, TeacherSubmissionSupportService, AdministratorSharedService, TeacherSubmissionNotificationService, TeacherSubmissionReportService, AdminNotificationUserService, AdministratorEnrollmentRefundService, AdminPrintRecipientService, AdministratorTeacherSubmissionService, AdministratorEnrollmentService, AdministratorEnrollmentQueryService +- **Assignment:** 5 services, 421 LOC. AssignmentSectionService, AssignmentMetaService, AssignmentService, AssignmentContextService, AssignmentDataLoaderService +- **Attendance:** 16 services, 3,241 LOC. SemesterRangeService, TeacherAttendanceSubmissionService, AttendanceAutoPublishJobService, AttendanceAutoPublishService, LateSlipLogQueryService, AttendanceConsequenceService, AttendanceRecordSyncService, AttendanceDailySummaryService, AttendanceService, StudentAttendanceWriterService, LateSlipLogCommandService, AttendanceSummaryRebuildService, StaffAttendanceService, AttendanceQueryService, AttendancePolicyService, AttendanceCommentService +- **AttendanceTracking:** 12 services, 2,979 LOC. AttendanceNotificationLogService, AttendanceViolationStudentResolverService, AttendanceNotificationWorkflowService, AttendanceTrackingService, ViolationRuleEngineService, AttendanceMailerService, AttendanceParentLookupService, AttendanceCommunicationSupportService, AttendanceEmailComposerService, AttendancePendingViolationService, AttendanceCaseQueryService, DefaultAttendanceMailerService +- **Auth:** 9 services, 870 LOC. RegistrationCaptchaService, PermissionCheckService, CodeIgniterApiRegistrationService, RegistrationService, ApiLoginSecurityService, AuthSessionService, UserRoleService, PasswordResetCleanupService, RegistrationFormatterService +- **BadgeScan:** 1 services, 108 LOC. BadgeScanService +- **Badges:** 7 services, 2,815 LOC. BadgeTextFormatter, BadgeStudentLookupService, BadgeFormDataService, BadgePdfService, BadgeUserLookupService, BadgeService, BadgePrintLogService +- **Billing:** 3 services, 751 LOC. BalanceCalculationService, BillingTotalsService, ChargeService +- **BroadcastEmail:** 5 services, 241 LOC. BroadcastEmailComposerService, BroadcastEmailRecipientService, BroadcastEmailDispatchService, BroadcastEmailImageService, BroadcastEmailSenderOptionsService +- **Certificates:** 1 services, 144 LOC. CertificatePdfService +- **ClassPrep:** 2 services, 205 LOC. ClassRosterService, StickerCountService +- **ClassPreparation:** 9 services, 653 LOC. ClassPreparationAdjustmentWriterService, ClassPreparationCalculatorService, ClassPreparationLogService, ClassPreparationAdjustmentService, ClassPreparationContextService, ClassPreparationService, ClassPreparationPrintService, ClassPreparationRosterService, ClassPreparationInventoryService +- **ClassProgress:** 5 services, 941 LOC. ClassProgressRuleService, ClassProgressAttachmentService, ClassProgressQueryService, ClassProgressMetaService, ClassProgressMutationService +- **ClassSections:** 4 services, 190 LOC. ClassSectionCommandService, ClassAttendanceService, ClassSectionSeedService, ClassSectionQueryService +- **Communication:** 6 services, 374 LOC. CommunicationFamilyService, CommunicationSendService, CommunicationTemplateService, CommunicationPreviewService, CommunicationStudentService, CommunicationLogService +- **CompetitionScores:** 4 services, 325 LOC. CompetitionScoresRosterService, CompetitionScoresSaveService, CompetitionScoresContextService, CompetitionScoresQueryService +- **Dashboard:** 1 services, 70 LOC. DashboardRouteService +- **Discounts:** 5 services, 792 LOC. DiscountParentService, DiscountApplyService, DiscountInvoiceService, DiscountVoucherService, DiscountContextService +- **Docs:** 3 services, 244 LOC. DocsCatalogService, ApiDocsService, OpenApiRouteExporter +- **Email:** 5 services, 395 LOC. EmailAttachmentService, EmailExtractorService, EmailProfileService, EmailDispatchService, EmailSenderOptionsService +- **EmergencyContacts:** 2 services, 142 LOC. EmergencyContactDirectoryService, EmergencyContactCrudService +- **Events:** 6 services, 496 LOC. EventChargeService, EventStudentChargeService, EventCategoryService, EventListService, EventChargeQueryService, EventManagementService +- **Exams:** 4 services, 532 LOC. ExamDraftTeacherService, ExamDraftAdminService, ExamDraftConfigService, ExamDraftFileService +- **Expenses:** 7 services, 231 LOC. ExpenseQueryService, ExpenseStaffService, ExpenseRetailorService, ExpenseReceiptService, ExpenseWriteService, ExpenseContextService, ExpenseStatusService +- **ExtraCharges:** 6 services, 475 LOC. ExtraChargesMetaService, ExtraChargesInvoiceService, ExtraChargesParentService, ExtraChargesChargeService, ExtraChargesContextService, ExtraChargesListService +- **Families:** 4 services, 827 LOC. FamilyNotificationService, FamilyFinanceService, FamilyMutationService, FamilyQueryService +- **Fees:** 6 services, 952 LOC. FeeConfigService, TuitionCalculationService, FeeRefundCalculatorService, FeeGradeService, FeeRefundDetailService, FeeStudentFeeService +- **Files:** 2 services, 159 LOC. FileServeService, ExamDraftDownloadNameService +- **Finance:** 8 services, 1,150 LOC. FinancialPdfReportService, FinancialSchoolYearService, FinancialPaymentService, FinancialReportService, FinancialSummaryService, FinancialChartService, FinancialDonationRecipientService, FinancialUnpaidParentsService +- **Frontend:** 9 services, 1,011 LOC. LandingPageTeacherSummaryService, LandingPageRoleService, LandingPageParentDashboardService, LandingPageService, StaticPageService, FrontendPageService, ContactSubmissionService, LandingPageContextService, ProfileIconService +- **Grading:** 9 services, 1,969 LOC. BelowSixtyEmailService, GradingLockService, GradingOverviewService, GradingPlacementService, HomeworkTrackingService, GradingScoreService, GradingBelowSixtyService, HomeworkTrackingCalendarService, GradingRefreshService +- **Incidents:** 4 services, 499 LOC. CurrentIncidentService, IncidentHistoryService, IncidentLookupService, IncidentAnalysisService +- **Inventory:** 8 services, 1,449 LOC. InventorySummaryService, InventoryCategoryService, InventoryItemService, InventoryContextService, SupplierService, InventoryMovementService, SupplyCategoryService, InventoryTeacherDistributionService +- **Invoices:** 8 services, 1,248 LOC. InvoiceGradeService, InvoiceConfigService, InvoicePaymentService, InvoiceTuitionService, InvoiceManagementService, InvoicePdfService, InvoiceService, InvoiceGenerationService +- **Messaging:** 3 services, 334 LOC. MessageRecipientService, MessageCommandService, MessageQueryService +- **Navigation:** 2 services, 273 LOC. NavBuilderService, NavbarService +- **Notifications:** 12 services, 451 LOC. NotificationDispatchService, NotificationRecipientService, NotificationShowService, NotificationManagementService, NotificationReadService, NotificationUserListService, NotificationCleanupService, UserNotificationDispatchService, NotificationTriggerService, NotificationSendService, NotificationActiveService, NotificationDeletedService +- **Parents:** 14 services, 2,306 LOC. ParentEventParticipationService, ParentRegistrationService, PrimaryParentUserResolver, ParentAttendanceReportCalendarService, ParentAttendanceService, AuthorizedUsersManagementService, ParentEnrollmentService, ParentInvoiceService, ParentProgressQueryService, ParentAttendanceReportService, ParentReportCardService, ParentConfigService, ParentProfileService, ParentEmergencyContactService +- **Payments:** 14 services, 2,096 LOC. PaymentNotificationDispatchService, PaymentMissedCheckService, PaymentManualService, PaymentLookupService, PaymentTestNotificationService, PaypalPaymentSyncService, PaymentBalanceService, PaypalPaymentService, PaymentTransactionService, PaymentEnrollmentEventService, PaymentNotificationService, PaymentPlanService, PaymentEventChargesService, PaypalTransactionsService +- **Phone:** 1 services, 23 LOC. PhoneFormatterService +- **Policy:** 1 services, 51 LOC. PolicyContentService +- **Preferences:** 3 services, 190 LOC. PreferencesCommandService, PreferencesQueryService, PreferencesOptionsService +- **PrintRequests:** 1 services, 558 LOC. PrintRequestsPortalService +- **Promotions:** 7 services, 1,740 LOC. PromotionStatusService, PromotionEnrollmentService, PromotionReminderService, LevelProgressionService, PromotionAuditService, PromotionQueryService, PromotionEligibilityService +- **PublicWinners:** 1 services, 115 LOC. PublicWinnersPortalService +- **PurchaseOrders:** 4 services, 252 LOC. PurchaseOrderStatusService, PurchaseOrderReceiveService, PurchaseOrderCreateService, PurchaseOrderQueryService +- **Refunds:** 9 services, 1,210 LOC. RefundSummaryService, RefundInvoiceAdjustmentService, RefundPolicyService, RefundPayoutService, RefundRequestService, RefundQueryService, RefundOverpaymentService, RefundDecisionService, RefundNotificationService +- **Reimbursements:** 11 services, 1,776 LOC. ReimbursementFileService, ReimbursementBatchAssignmentService, ReimbursementContextService, ReimbursementExportService, ReimbursementLookupService, ReimbursementEmailService, ReimbursementRecipientService, ReimbursementBatchService, ReimbursementQueryService, ReimbursementDonationService, ReimbursementCrudService +- **Reports:** 9 services, 2,445 LOC. SlipPrinterFormatterService, SlipPrinterPdfService, SlipPrinterService, SlipPrinterConfigService, StickerPrintService, StickerPresetService, StickerQueryService, StickerContextService, ReportCardService +- **Roles:** 8 services, 545 LOC. RolePermissionService, RoleDashboardService, RoleCrudService, RoleAssignmentService, RoleSwitchService, RoleStaffService, PermissionCrudService, RoleQueryService +- **Root:** 8 services, 550 LOC. FeeCalculationService, SemesterRangeService, ApplicationUrlService, SchoolIdService, AssignmentService, AttendanceCommentTemplateService, PhoneFormatterService, EmailService +- **School:** 4 services, 990 LOC. AccountEventService, SemesterSelectionService, EnrollmentEventService, PaymentEventService +- **SchoolIds:** 2 services, 41 LOC. SchoolIdAssignmentService, SchoolIdGenerationService +- **Scores:** 16 services, 2,846 LOC. ParticipationScoreService, QuizCalculator, QuizScoreService, ScoreCommentService, ScoreDashboardService, HomeworkCalculator, SemesterScoreService, ScorePredictorRiskService, ProjectScoreService, HomeworkScoreService, ScoreTermService, ProjectCalculator, ScorePredictorDataService, ScorePredictorService, ExamScoreService, AttendanceCalculator +- **Security:** 3 services, 171 LOC. IpBanCommandService, IpBanQueryService, Pbkdf2Hasher +- **Semesters:** 1 services, 42 LOC. SemesterConfigService +- **Settings:** 8 services, 656 LOC. SettingsService, ConfigurationService, SchoolCalendarMutationService, SchoolCalendarNotificationService, SchoolCalendarContextService, SchoolCalendarQueryService, SchoolCalendarFormatterService, SchoolCalendarMeetingService +- **Staff:** 3 services, 325 LOC. StaffQueryService, StaffTimeOffLinkService, StaffCommandService +- **Students:** 7 services, 1,254 LOC. StudentScoreCardService, StudentStatusService, StudentDirectoryService, StudentAutoDistributionService, StudentConfigService, StudentProfileService, StudentAssignmentService +- **Subjects:** 1 services, 80 LOC. SubjectCurriculumService +- **Support:** 2 services, 123 LOC. SupportRequestService, ContactMessageService +- **System:** 6 services, 353 LOC. HealthCheckService, CleanupSchedulerService, GlobalConfigService, DatabaseHealthService, ConfigUpdateService, TimeService +- **Teachers:** 4 services, 641 LOC. TeacherAbsenceService, TeacherDashboardService, TeacherConfigService, TeacherAssignmentService +- **Ui:** 1 services, 95 LOC. UiStyleService +- **Users:** 6 services, 647 LOC. UserListService, LoginActivityService, InactiveUserCleanupService, UserManagementService, UserEventService, DeleteUnverifiedUserService +- **Whatsapp:** 8 services, 1,407 LOC. WhatsappContactService, WhatsappInviteService, WhatsappLinkService, WhatsappContextService, WhatsappInviteNotificationService, WhatsappMembershipService, WhatsappInviteBundleService, WhatsappInviteEmailService + +## Appendix C: Request Groups + +- **Admin:** 3 requests. StoreCompetitionWinnerRequest, UpdateCompetitionWinnerRequest, SaveCompetitionScoresRequest +- **Administrator:** 5 requests. SavePrintRecipientsRequest, SaveAdminNotificationSubjectsRequest, SubmitAdministratorAbsenceRequest, UpdateEnrollmentStatusesRequest, SendTeacherSubmissionNotificationsRequest +- **Api:** 4 requests. SaveAdminNotificationSubjectsRequest, IndexAdminProgressRequest, SendTeacherSubmissionNotificationsRequest, SavePrintNotificationRecipientsRequest +- **Assignment:** 2 requests. StoreAssignmentRequest, StoreStudentAssignmentRequest +- **Attendance:** 7 requests. SaveStaffAttendanceCellRequest, UpdateAttendanceManagementRequest, LateSlipLogIndexRequest, TeacherSubmitAttendanceRequest, SaveAdminAttendanceRequest, TeacherUpdateGridRequest, AdminAddAttendanceEntryRequest +- **AttendanceCommentTemplate:** 2 requests. StoreAttendanceCommentTemplateRequest, UpdateAttendanceCommentTemplateRequest +- **AttendanceTracking:** 5 requests. RecordAttendanceTrackingRequest, ComposeAttendanceEmailRequest, SendAttendanceManualEmailRequest, ParentsInfoRequest, SaveAttendanceNotificationNoteRequest +- **Auth:** 3 requests. RegisterRequest, SetRoleSessionRequest, LoginSessionRequest +- **Badges:** 3 requests. BadgePrintStatusRequest, GenerateBadgePdfRequest, LogBadgePrintRequest +- **ClassPreparation:** 4 requests. ClassPreparationMarkPrintedRequest, ClassPreparationPrintRequest, ClassPreparationIndexRequest, ClassPreparationAdjustmentRequest +- **ClassProgress:** 5 requests. ClassProgressStoreRequest, ClassProgressMetaRequest, ClassProgressIndexRequest, ClassProgressFormRequest, ClassProgressUpdateRequest +- **Classes:** 3 requests. ClassSectionUpdateRequest, ClassSectionStoreRequest, ClassSectionIndexRequest +- **Discounts:** 3 requests. UpdateDiscountVoucherRequest, ApplyDiscountVoucherRequest, StoreDiscountVoucherRequest +- **Email:** 2 requests. EmailExtractorCompareRequest, EmailSendRequest +- **EmergencyContacts:** 2 requests. EmergencyContactParentRequest, EmergencyContactUpdateRequest +- **Events:** 6 requests. EventIndexRequest, EventUpdateRequest, EventStoreRequest, EventStudentsWithChargesRequest, EventChargeUpdateRequest, EventChargeIndexRequest +- **Exams:** 3 requests. ExamDraftTeacherStoreRequest, ExamDraftAdminLegacyRequest, ExamDraftAdminReviewRequest +- **Expenses:** 3 requests. UpdateExpenseRequest, StoreExpenseRequest, UpdateExpenseStatusRequest +- **ExtraCharges:** 2 requests. UpdateExtraChargeRequest, StoreExtraChargeRequest +- **Families:** 14 requests. FamilyImportLegacyRequest, FamilyUnlinkStudentRequest, FamilySetPrimaryHomeRequest, FamilyAdminCardRequest, FamiliesByStudentRequest, FamilyBootstrapRequest, FamilyAttachSecondByEmailRequest, FamilyAdminSearchRequest, FamilySetGuardianFlagsRequest, FamilyAdminIndexRequest, FamilyComposeEmailRequest, FamilyUnlinkGuardianRequest, GuardiansByFamilyRequest, FamilyAttachSecondByUserRequest +- **Fees:** 2 requests. FeeTuitionTotalRequest, FeeRefundRequest +- **Files:** 1 requests. FileNameRequest +- **Finance:** 12 requests. FinancialUnpaidParentsRequest, FeeTuitionTotalRequest, ChargeListRequest, FinancialReportRequest, FinancialSummaryRequest, MarkEventChargePaidRequest, FeeTuitionBreakdownRequest, StoreExtraChargeRequest, StoreEventChargeRequest, FeeFamilyBalanceRequest, ApplyExtraChargeRequest, FeeRefundRequest +- **Frontend:** 2 requests. LandingTeacherRequest, ContactSubmitRequest +- **Grading:** 18 requests. BelowSixtyListRequest, BelowSixtySendRequest, GradingLockAllRequest, BelowSixtyMeetingRequest, PlacementLevelsRequest, GradingRefreshRequest, GradingScoreShowRequest, HomeworkTrackingRequest, GradingScoreUpdateRequest, PlacementRequest, PlacementBatchUpdateRequest, BelowSixtyEmailRequest, BelowSixtyStatusRequest, PlacementBatchRequest, GradingLockRequest, PlacementLevelRequest, BelowSixtyMeetingSaveRequest, GradingOverviewRequest +- **Incidents:** 5 requests. IncidentCancelRequest, IncidentCloseRequest, IncidentStoreRequest, IncidentListRequest, IncidentStateRequest +- **Inventory:** 19 requests. InventoryItemIndexRequest, SupplierUpdateRequest, InventoryCategoryStoreRequest, InventoryTeacherDistributionRequest, InventoryItemUpdateRequest, InventoryItemStoreRequest, InventoryTeacherDistributionStoreRequest, InventoryCategoryUpdateRequest, SupplyCategoryStoreRequest, InventoryCategoryIndexRequest, InventoryAdjustRequest, InventoryAuditRequest, SupplierIndexRequest, InventoryMovementIndexRequest, InventoryMovementBulkDeleteRequest, SupplierStoreRequest, InventoryMovementUpdateRequest, InventoryMovementStoreRequest ... +- **Invoices:** 4 requests. InvoiceStatusRequest, InvoiceManagementRequest, InvoiceParentRequest, InvoiceGenerateRequest +- **Messaging:** 3 requests. MessageSendRequest, MessageIndexRequest, MessageUpdateRequest +- **Notifications:** 5 requests. NotificationSendRequest, NotificationActiveRequest, NotificationDeletedRequest, NotificationUpdateRequest, NotificationIndexRequest +- **Parents:** 16 requests. ParentAttendanceReportUpdateRequest, ParentAttendanceReportListRequest, ParentAttendanceReportSubmitRequest, ParentEmergencyContactRequest, UpdateAuthorizedUserRequest, ParentAttendanceReportCheckRequest, ParentProfileUpdateRequest, ParentInvoiceRequest, ParentEnrollmentRequest, SignParentReportCardRequest, ParentAttendanceRequest, StoreAuthorizedUserRequest, ParentStudentUpdateRequest, ParentEventParticipationRequest, ParentEnrollmentActionRequest, ParentRegistrationRequest +- **Payments:** 15 requests. PaymentManualSearchRequest, PaymentEventChargesListRequest, PaymentNotificationSendRequest, PaymentNotificationListRequest, PaymentCreateRequest, PaypalTransactionsListRequest, PaymentManualEditRequest, PaymentEventChargesStoreRequest, PaymentUpdateBalanceRequest, PaymentManualUpdateRequest, PaymentManualSuggestRequest, PaypalExecuteRequest, PaymentByParentRequest, PaymentTransactionStatusRequest, PaymentTransactionCreateRequest +- **Preferences:** 2 requests. PreferencesIndexRequest, PreferencesUpsertRequest +- **Promotions:** 8 requests. SendReminderRequest, ParentPromotionOverviewRequest, AdminListPromotionsRequest, EvaluateEligibilityRequest, UpdatePromotionStatusRequest, ParentEnrollmentStepRequest, SetEnrollmentDeadlineRequest, UpsertLevelProgressionRequest +- **PurchaseOrders:** 3 requests. PurchaseOrderIndexRequest, PurchaseOrderStoreRequest, PurchaseOrderReceiveRequest +- **Refunds:** 6 requests. RefundStoreRequest, RefundPaymentRequest, RefundDecisionRequest, RefundRecalculateRequest, RefundIndexRequest, RefundRejectRequest +- **Reimbursements:** 13 requests. ReimbursementBatchAdminFileRequest, ReimbursementUpdateRequest, ReimbursementBatchCreateRequest, ReimbursementBatchEmailRequest, ReimbursementProcessRequest, ReimbursementExportBatchRequest, ReimbursementIndexRequest, ReimbursementExportRequest, ReimbursementMarkDonationRequest, ReimbursementBatchAssignmentRequest, ReimbursementStoreRequest, ReimbursementBatchLockRequest, ReimbursementUnderProcessingRequest +- **Reports:** 11 requests. SlipPreviewRequest, SlipLogListRequest, SlipPrintRequest, SlipReprintRequest, StickerStudentsByClassRequest, StickerPrintRequest, StickerFormDataRequest, ReportCardCompletenessRequest, ReportCardPdfRequest, ReportCardAcknowledgementRequest, ReportCardMetaRequest +- **Roles:** 9 requests. AssignUserRolesRequest, RolePermissionUpdateRequest, UserRoleListRequest, PermissionUpdateRequest, PermissionStoreRequest, RoleSwitchRequest, RoleStoreRequest, RoleUpdateRequest, RoleListRequest +- **Root:** 1 requests. ApiFormRequest +- **SchoolIds:** 1 requests. SchoolIdAssignRequest +- **Scores:** 10 requests. ScoreCommentSaveRequest, ScorePredictorRequest, ScoreLockRequest, ScoreUpdateRequest, ScoreOverviewRequest, ScoreStudentRequest, ScoreCommentListRequest, ScoreClassRequest, ScoreCommentUpdateRequest, ScoreAddColumnRequest +- **Security:** 3 requests. IpBanIndexRequest, IpUnbanRequest, IpBanRequest +- **Semesters:** 4 requests. SchoolYearRangeRequest, SemesterResolveRequest, SemesterNormalizeRequest, SemesterRangeRequest +- **Settings:** 8 requests. SchoolCalendarStoreRequest, ConfigurationStoreRequest, SettingsUpdateRequest, SchoolCalendarIndexRequest, ConfigurationUpdateRequest, SchoolCalendarUpdateRequest, PolicyShowRequest, SchoolCalendarOptionsRequest +- **Staff:** 3 requests. StaffIndexRequest, StaffUpdateRequest, StaffStoreRequest +- **Students:** 7 requests. StudentRemoveClassRequest, StudentUpdateRequest, StudentAssignClassRequest, StudentSetActiveRequest, StudentPromotionTotalsRequest, StudentAssignmentListRequest, StudentAutoDistributeRequest +- **Subjects:** 2 requests. SubjectCurriculumUpdateRequest, SubjectCurriculumStoreRequest +- **Support:** 3 requests. SupportRequestStoreRequest, SupportRequestIndexRequest, ContactSendRequest +- **System:** 2 requests. NavItemStoreRequest, NavItemReorderRequest +- **Teachers:** 6 requests. TeacherClassViewRequest, TeacherAbsenceSubmitRequest, TeacherAssignmentDeleteRequest, TeacherAssignmentStoreRequest, TeacherAssignmentListRequest, TeacherSwitchClassRequest +- **Ui:** 1 requests. UiStyleRequest +- **Users:** 4 requests. UserStoreRequest, UserListRequest, UserUpdateRequest, LoginActivityRequest +- **Utilities:** 1 requests. PhoneFormatRequest +- **Whatsapp:** 7 requests. WhatsappLinkIndexRequest, WhatsappMembershipUpdateRequest, WhatsappLinkStoreRequest, WhatsappLinkUpdateRequest, WhatsappParentContactsRequest, WhatsappParentContactsByClassRequest, WhatsappInviteSendRequest + +## Appendix D: Resource Groups + +- **Admin:** 5 resources. PrintRecipientsResource, TeacherSubmissionReportResource, AdminNotificationAlertsResource, ProgressGroupResource, ProgressReportDetailResource +- **Assignment:** 2 resources. AssignmentOverviewResource, AssignmentSectionResource +- **Attendance:** 7 resources. DailyAttendanceResource, AdminAttendanceResource, LateSlipLogCollection, LateSlipLogResource, TeacherGridResource, StaffMonthResource, TeacherAttendanceFormResource +- **Auth:** 1 resources. RegisterResource +- **ClassPreparation:** 2 resources. ClassPreparationPrintResource, ClassPreparationResultResource +- **ClassProgress:** 4 resources. ClassProgressGroupCollection, ClassProgressAttachmentResource, ClassProgressShowResource, ClassProgressReportResource +- **Classes:** 4 resources. ClassSectionCollection, ClassSectionResource, ClassAttendanceResource, ClassAttendanceStudentResource +- **Discounts:** 2 resources. DiscountVoucherResource, DiscountParentResource +- **Email:** 3 resources. EmailSenderResource, EmailExtractorEmailsResource, EmailExtractorCompareResource +- **EmergencyContacts:** 3 resources. EmergencyContactGroupResource, EmergencyContactStudentResource, EmergencyContactResource +- **Events:** 3 resources. EventChargeResource, EventResource, EventStudentChargeResource +- **Exams:** 1 resources. ExamDraftResource +- **Expenses:** 2 resources. ExpenseResource, ExpenseStaffResource +- **ExtraCharges:** 3 resources. ExtraChargeInvoiceOptionResource, ExtraChargeResource, ExtraChargeParentOptionResource +- **Families:** 7 resources. FamilyPaymentResource, FamilyInvoiceResource, FamilySearchItemResource, FamilyResource, FamilyGuardianResource, FamilyStudentResource, FamilyEmergencyContactResource +- **Fees:** 6 resources. FeeRefundResource, FeeFamilyBalanceResource, ChargeActionResource, ChargeListResource, FeeTuitionBreakdownResource, FeeTuitionTotalResource +- **Files:** 1 resources. FileMetaResource +- **Finance:** 9 resources. FinancialReportResource, FinancialSummaryResource, FinancialExpenseSummaryResource, FinancialInvoiceResource, FinancialPaymentResource, FinancialRefundResource, FinancialUnpaidParentResource, FinancialDiscountResource, FinancialReimbursementSummaryResource +- **Frontend:** 6 resources. LandingPageResource, ContactSubmissionResource, FrontendPageResource, FrontendUserResource, ProfileIconResource, PageContentResource +- **Grading:** 3 resources. BelowSixtyStudentResource, GradingScoreResource, HomeworkTrackingTeacherResource +- **Incidents:** 4 resources. IncidentStudentOptionResource, IncidentAnalysisStudentResource, IncidentResource, IncidentGradeResource +- **Inventory:** 5 resources. InventoryItemResource, InventoryCategoryResource, InventoryMovementResource, SupplierResource, SupplyCategoryResource +- **Invoices:** 2 resources. InvoiceManagementParentResource, InvoiceResource +- **Messaging:** 3 resources. MessageResource, MessageCollection, RecipientResource +- **Notifications:** 2 resources. UserNotificationResource, NotificationResource +- **Parents:** 4 resources. ParentEmergencyContactResource, ParentAttendanceReportResource, ParentStudentResource, ParentAttendanceResource +- **Payments:** 4 resources. PaymentNotificationLogResource, PaymentTransactionResource, PaypalTransactionResource, PaymentResource +- **Preferences:** 3 resources. PreferencesListResource, PreferencesCollection, PreferencesResource +- **Promotions:** 4 resources. LevelProgressionResource, PromotionAuditLogResource, PromotionReminderResource, StudentPromotionResource +- **PurchaseOrders:** 2 resources. PurchaseOrderResource, PurchaseOrderItemResource +- **Refunds:** 1 resources. RefundResource +- **Reimbursements:** 5 resources. ReimbursementRecipientResource, ReimbursementExpenseResource, ReimbursementBatchResource, ReimbursementResource, ReimbursementUnderProcessingItemResource +- **Reports:** 8 resources. LateSlipLogResource, StickerStudentResource, StickerPresetResource, StickerClassSectionResource, ReportCardStudentMetaResource, ReportCardAcknowledgementResource, ReportCardClassSectionResource, ReportCardCompletenessStudentResource +- **Roles:** 4 resources. RolePermissionResource, RoleResource, UserWithRolesResource, PermissionResource +- **Root:** 1 resources. TeacherSubmissionRowResource +- **SchoolIds:** 1 resources. SchoolIdResource +- **Scores:** 3 resources. ScorePredictorStudentResource, ScoreCommentResource, ScoreStudentResource +- **Security:** 2 resources. IpBanCollection, IpBanResource +- **Semesters:** 2 resources. SemesterResolveResource, SemesterRangeResource +- **Settings:** 5 resources. PolicyResource, SchoolCalendarEventResource, SettingsResource, ConfigurationResource, SchoolCalendarEventDetailResource +- **Staff:** 2 resources. StaffCollection, StaffResource +- **Students:** 4 resources. StudentAssignmentResource, StudentRemovedResource, StudentClassSectionResource, StudentScoreCardRowResource +- **Subjects:** 2 resources. SubjectCurriculumResource, SubjectClassResource +- **Support:** 3 resources. ContactMessageResource, SupportRequestResource, SupportRequestCollection +- **System:** 4 resources. DashboardRouteResource, NavItemResource, HealthStatusResource, NavBuilderDataResource +- **Teachers:** 4 resources. TeacherAbsenceResource, TeacherStudentResource, TeacherClassResource, TeacherAssignmentResource +- **Ui:** 1 resources. UiStyleResource +- **Users:** 2 resources. LoginActivityResource, UserWithRolesResource +- **Utilities:** 1 resources. PhoneFormatResource +- **Whatsapp:** 5 resources. WhatsappParentContactResource, WhatsappGroupLinkResource, WhatsappClassSectionParentResource, WhatsappGroupLinkCollection, WhatsappClassSectionContactResource + +## Appendix E: Notable Review Queues + +### Raw SQL review queue + +- `app/Services/Finance/FinancialSummaryService.php`: 28 matches +- `app/Services/Grading/GradingOverviewService.php`: 11 matches +- `app/Services/Finance/FinancialReportService.php`: 10 matches +- `app/Models/User.php`: 7 matches +- `app/Models/AttendanceData.php`: 6 matches +- `app/Services/Refunds/RefundOverpaymentService.php`: 6 matches +- `app/Services/Reports/ReportCards/ReportCardService.php`: 6 matches +- `app/Services/AttendanceTracking/ViolationRuleEngineService.php`: 5 matches +- `app/Services/AttendanceTracking/AttendanceCaseQueryService.php`: 4 matches +- `app/Services/Grading/GradingBelowSixtyService.php`: 4 matches +- `app/Services/Grading/HomeworkTrackingService.php`: 4 matches +- `app/Models/Payment.php`: 3 matches +- `app/Services/Attendance/AttendanceQueryService.php`: 3 matches +- `app/Services/Inventory/InventoryItemService.php`: 3 matches +- `app/Services/Inventory/InventoryMovementService.php`: 3 matches +- `app/Services/Payments/PaymentMissedCheckService.php`: 3 matches +- `app/Services/Scores/ScoreDashboardService.php`: 3 matches +- `app/Services/Scores/ScorePredictorDataService.php`: 3 matches +- `app/Models/Invoice.php`: 2 matches +- `app/Models/Role.php`: 2 matches +- `app/Models/TeacherClass.php`: 2 matches +- `app/Services/Admin/CompetitionWinners/CompetitionWinnersAdminService.php`: 2 matches +- `app/Services/Attendance/AttendanceDailySummaryService.php`: 2 matches +- `app/Services/AttendanceTracking/AttendancePendingViolationService.php`: 2 matches +- `app/Services/Families/FamilyQueryService.php`: 2 matches +- `app/Services/Finance/FinancialPaymentService.php`: 2 matches +- `app/Services/Promotions/PromotionQueryService.php`: 2 matches +- `app/Services/Refunds/RefundPayoutService.php`: 2 matches +- `app/Services/Refunds/RefundQueryService.php`: 2 matches +- `app/Services/Staff/StaffQueryService.php`: 2 matches +- `app/Services/Whatsapp/WhatsappInviteBundleService.php`: 2 matches +- `app/Models/AdditionalCharge.php`: 1 matches +- `app/Models/AttendanceRecord.php`: 1 matches +- `app/Models/AttendanceTracking.php`: 1 matches +- `app/Models/Project.php`: 1 matches +- `app/Models/RoleNavItem.php`: 1 matches +- `app/Models/StaffAttendance.php`: 1 matches +- `app/Models/StudentAllergy.php`: 1 matches +- `app/Models/StudentMedicalCondition.php`: 1 matches +- `app/Models/WhatsappGroupMembership.php`: 1 matches +- ... 19 more files + +### Direct request access review queue + +- `app/Http/Controllers/Api/AttendanceTracking/AttendanceTrackingController.php`: 16 matches +- `app/Http/Controllers/Api/Attendance/AttendanceCommentTemplateController.php`: 15 matches +- `app/Http/Controllers/Api/Communication/CommunicationController.php`: 12 matches +- `app/Http/Controllers/Api/Badges/BadgeController.php`: 9 matches +- `app/Http/Controllers/Api/Email/BroadcastEmailController.php`: 9 matches +- `app/Http/Controllers/Api/ExtraCharges/ExtraChargesController.php`: 9 matches +- `app/Http/Controllers/Api/Students/StudentController.php`: 9 matches +- `app/Http/Controllers/Api/Attendance/StaffAttendanceApiController.php`: 8 matches +- `app/Http/Controllers/Api/Attendance/EarlyDismissalsController.php`: 7 matches +- `app/Http/Controllers/Api/ClassPreparation/ClassPreparationController.php`: 7 matches +- `app/Http/Controllers/Api/Attendance/TeacherAttendanceApiController.php`: 5 matches +- `app/Http/Controllers/Api/Certificates/CertificateController.php`: 5 matches +- `app/Http/Controllers/Api/Reports/ReportCardsController.php`: 5 matches +- `app/Http/Controllers/Api/CompetitionScores/CompetitionScoresController.php`: 4 matches +- `app/Http/Controllers/Api/Finance/ReimbursementController.php`: 4 matches +- `app/Http/Controllers/Api/PrintRequests/PrintRequestsController.php`: 4 matches +- `app/Http/Controllers/Api/Administrator/AdministratorEnrollmentController.php`: 3 matches +- `app/Http/Controllers/Api/Assignment/AssignmentApiController.php`: 3 matches +- `app/Http/Controllers/Api/Finance/InvoiceController.php`: 3 matches +- `app/Http/Controllers/Api/Finance/PaymentManualController.php`: 3 matches +- `app/Http/Controllers/Api/Parents/AuthorizedUserInviteController.php`: 3 matches +- `app/Http/Controllers/Api/Scores/ScoreCommentController.php`: 3 matches +- `app/Http/Controllers/Api/Scores/ScoreController.php`: 3 matches +- `app/Services/Administrator/AdministratorAbsenceService.php`: 3 matches +- `app/Http/Controllers/Api/Administrator/AdministratorNotificationController.php`: 2 matches +- `app/Http/Controllers/Api/Attendance/AdminAttendanceApiController.php`: 2 matches +- `app/Http/Controllers/Api/Auth/AuthSessionController.php`: 2 matches +- `app/Http/Controllers/Api/Auth/RegisterController.php`: 2 matches +- `app/Http/Controllers/Api/Classes/ClassController.php`: 2 matches +- `app/Http/Controllers/Api/Finance/PaymentController.php`: 2 matches +- `app/Http/Controllers/Api/Finance/PaymentTransactionController.php`: 2 matches +- `app/Http/Controllers/Api/Finance/PurchaseOrderController.php`: 2 matches +- `app/Http/Controllers/Api/Incidents/IncidentController.php`: 2 matches +- `app/Http/Controllers/Api/Scores/FinalController.php`: 2 matches +- `app/Http/Controllers/Api/Scores/HomeworkController.php`: 2 matches +- `app/Http/Controllers/Api/Scores/MidtermController.php`: 2 matches +- `app/Http/Controllers/Api/Scores/ParticipationController.php`: 2 matches +- `app/Http/Controllers/Api/Scores/ProjectController.php`: 2 matches +- `app/Http/Controllers/Api/Scores/QuizController.php`: 2 matches +- `app/Services/Administrator/TeacherSubmissionNotificationService.php`: 2 matches +- ... 10 more files + +### File operation review queue + +- `app/Services/Reimbursements/ReimbursementEmailService.php`: 5 matches +- `app/Http/Controllers/Api/Students/StudentController.php`: 3 matches +- `app/Services/Badges/BadgePdfService.php`: 3 matches +- `app/ThirdParty/fpdf/fpdf.php`: 3 matches +- `app/Http/Controllers/Api/ClassProgress/ClassProgressController.php`: 2 matches +- `app/Http/Controllers/Api/Finance/ReimbursementController.php`: 2 matches +- `app/Services/Payments/PaymentManualService.php`: 2 matches +- `app/Services/PrintRequests/PrintRequestsPortalService.php`: 2 matches +- `app/Services/System/ConfigUpdateService.php`: 2 matches +- `app/Services/Whatsapp/WhatsappInviteEmailService.php`: 2 matches +- `app/Http/Controllers/Api/Attendance/EarlyDismissalsController.php`: 1 matches +- `app/Http/Controllers/Api/Finance/FinancialController.php`: 1 matches +- `app/Http/Controllers/Api/Finance/PaypalTransactionsController.php`: 1 matches +- `app/Http/Controllers/Api/PrintRequests/PrintRequestsController.php`: 1 matches +- `app/Services/Attendance/StaffAttendanceService.php`: 1 matches +- `app/Services/Finance/FinancialChartService.php`: 1 matches +- `app/Services/Parents/ParentAttendanceReportService.php`: 1 matches +- `app/Services/Reimbursements/ReimbursementFileService.php`: 1 matches + +### Queue/dispatch review queue + +- `app/Services/Promotions/PromotionReminderService.php`: 2 matches +- `app/Console/Commands/DeleteUnverifiedUsersCommand.php`: 1 matches +- `app/Services/Administrator/AdministratorEnrollmentEventService.php`: 1 matches +- `app/Services/Grading/GradingBelowSixtyService.php`: 1 matches +- `app/Services/Notifications/NotificationTriggerService.php`: 1 matches +- `app/Services/Whatsapp/WhatsappInviteNotificationService.php`: 1 matches +- `app/Services/Whatsapp/WhatsappInviteService.php`: 1 matches + +### Legacy compatibility review queue + +- `app/Services/Parents/ParentAttendanceReportService.php`: 4 matches +- `app/Config/Database.php`: 3 matches +- `app/Helpers/ci_helpers.php`: 3 matches +- `app/Http/Controllers/Api/Attendance/AttendanceCommentTemplateController.php`: 3 matches +- `app/Http/Controllers/Api/Auth/RegisterController.php`: 3 matches +- `app/Http/Controllers/Api/BaseApiController.php`: 3 matches +- `app/Http/Controllers/Api/Core/BaseApiController.php`: 2 matches +- `app/Services/Admin/CompetitionWinners/CompetitionWinnersAdminService.php`: 2 matches +- `app/Services/AttendanceTracking/AttendanceTrackingService.php`: 2 matches +- `app/Services/Auth/ApiLoginSecurityService.php`: 2 matches +- `app/Services/Auth/CodeIgniterApiRegistrationService.php`: 2 matches +- `app/Services/Discounts/DiscountApplyService.php`: 2 matches +- `app/Services/ExtraCharges/ExtraChargesChargeService.php`: 2 matches +- `app/Services/Payments/PaymentManualService.php`: 2 matches +- `app/Http/Controllers/Api/Auth/AuthController.php`: 1 matches +- `app/Http/Controllers/Api/Auth/AuthSessionController.php`: 1 matches +- `app/Http/Controllers/Api/CiRequestAdapter.php`: 1 matches +- `app/Http/Controllers/Api/Core/CiRequestAdapter.php`: 1 matches +- `app/Http/Controllers/Api/System/HealthController.php`: 1 matches +- `app/Http/Controllers/Api/Utilities/ProofreadController.php`: 1 matches +- `app/Http/Middleware/EnsureClassProgressTeacherPortalAccess.php`: 1 matches +- `app/Http/Middleware/EnsureParentProgressAccess.php`: 1 matches +- `app/Http/Middleware/EnsurePrintRequestsAdminAccess.php`: 1 matches +- `app/Http/Middleware/EnsurePrintRequestsTeacherAccess.php`: 1 matches +- `app/Http/Middleware/TeacherPortalAuthenticate.php`: 1 matches +- `app/Http/Requests/Admin/SaveCompetitionScoresRequest.php`: 1 matches +- `app/Http/Requests/Admin/StoreCompetitionWinnerRequest.php`: 1 matches +- `app/Http/Requests/Admin/UpdateCompetitionWinnerRequest.php`: 1 matches +- `app/Models/CiDatabase.php`: 1 matches +- `app/Models/User.php`: 1 matches +- `app/Services/Admin/CompetitionWinners/CompetitionWinnersDomain.php`: 1 matches +- `app/Services/Attendance/StaffAttendanceService.php`: 1 matches +- `app/Services/Auth/AuthSessionService.php`: 1 matches +- `app/Services/BadgeScan/BadgeScanService.php`: 1 matches +- `app/Services/Docs/DocsCatalogService.php`: 1 matches +- `app/Services/Parents/AuthorizedUsersManagementService.php`: 1 matches +- `app/Services/Parents/ParentProgressQueryService.php`: 1 matches +- `app/Services/Parents/ParentReportCardService.php`: 1 matches +- `app/Services/Parents/PrimaryParentUserResolver.php`: 1 matches +- `app/Services/PrintRequests/PrintRequestsPortalService.php`: 1 matches +- ... 1 more files diff --git a/docs/app_project_plan_hardened.md b/docs/app_project_plan_hardened.md new file mode 100644 index 00000000..fe8920e0 --- /dev/null +++ b/docs/app_project_plan_hardened.md @@ -0,0 +1,1903 @@ +# App Project Implementation Plan + +Generated: 2026-05-29 + +## 1. Scope and Readout + +This plan is based on the uploaded `app/` project slice. The zip contains the Laravel-style application layer, not the full repository. That means routes, migrations, config outside `app/`, composer metadata, tests, CI, frontend assets, and deployment files were not available unless embedded in this directory. So the plan avoids pretending the missing pieces magically exist, a level of honesty software planning could use more often. + +### Inventory + +- Real project files inspected: **1184** +- PHP files inspected: **1183** +- Approximate PHP LOC: **103,161** +- Controllers: **129** +- Models: **133** +- Services: **386** +- Form requests: **298** +- API resources: **167** +- Middleware: **12** +- Console commands: **14** +- Policies: **10** + + +## 2. Project Thesis + +The app is already organized around a service-heavy backend: controllers orchestrate, request classes validate, resources shape responses, and services carry domain logic. That is the right direction. The weak point is not that the code lacks structure. The weak point is that the project is broad enough for rules to drift across domains unless the boundaries are enforced deliberately. + +The plan should therefore optimize for correctness and regression safety, not cosmetic refactors. Refactoring without tests here would be performance art with a stack trace. + + + +## 3. Main Architecture Plan + +### Target shape + +- Controllers stay thin: authorize, validate, call one service action, return one resource/response. +- Services own business rules and transactions. +- Form requests own validation and request normalization. +- Resources own response shape and avoid leaking internal model structure. +- Policies/Gates own permission checks. +- Events/listeners own async or side-effect workflows, especially communications. +- Console commands remain operational entry points only, not hidden domain logic. + +### Non-negotiable rules + +- No money-changing action without an explicit transaction, audit trail, idempotency check, and test. +- No attendance, score, promotion, or enrollment mutation without actor tracking and date boundary tests. +- No file endpoint without MIME, size, storage path, authorization, and download-name tests. +- No communication endpoint without recipient preview, opt-out logic, deduplication, and send-log tests. +- No direct request parsing in controllers where a FormRequest already exists or should exist. +- No raw SQL without parameter binding review and a test proving the query behavior. + + +### Critical Implementation Review Queue + +These items override ordinary module cleanup. Do not spend time prettifying controllers while these can still leak data, corrupt money, or create irreversible school records. Charming architectural theater can wait. + +#### P0: Must review before feature work + +1. **Payment file access authorization** + - Replace filename-only download access with payment-id-based access. + - Load the payment/invoice/family context before resolving any file path. + - Authorize the current actor against the payment record, not against a guessed filename. + - Resolve the file path only from the stored payment record. + - Return `403` for unauthorized access and `404` for missing/unknown files. + - Required tests: finance/admin can download, unrelated parent cannot download, teacher cannot download, guessed filename cannot download, traversal-style filename fails. + +2. **Manual payment edit transaction safety** + - Wrap payment edit and invoice recalculation in one database transaction. + - Lock the payment row and linked invoice row with `lockForUpdate()` or equivalent. + - Recompute balance inside the lock. + - Reject edits that overpay, violate payment method rules, or create invalid invoice state. + - Store before/after audit details with actor ID. + - Required tests: concurrent edit, overpayment rejection, audit creation, invoice balance consistency. + +3. **Student school ID race condition** + - Remove pre-insert `max(id) + 1` generation. + - Create the student first inside a transaction. + - Generate `school_id` from the persisted student ID or a dedicated sequence. + - Add a unique database constraint on `students.school_id`. + - Add retry behavior for rare collisions. + - Required tests: two concurrent student creations cannot produce duplicate school IDs. + +4. **Scanner attendance transaction and idempotency** + - Split scanner orchestration out of the controller. + - Wrap all attendance mutations from one scan in a single transaction. + - Add an idempotency key based on badge/student/staff, scan date, station, and scan window. + - Prevent duplicate late slips, duplicate scan logs, and double attendance counts. + - Required tests: duplicate scan, late scan, staff scan, student scan, invalid badge, semester boundary. + +5. **Role and permission mutation authorization** + - Map every role/permission mutation route to a policy or gate. + - Add wrong-role and wrong-owner negative tests. + - Log actor, target, old role/permission state, and new state. + +6. **Bulk communication recipient safety** + - Require preview/dry-run before bulk send. + - Deduplicate recipients by canonical user/contact identity. + - Respect opt-out and communication preferences. + - Store send logs and failure states. + - Required tests: preview count matches send count, duplicate recipients collapse, opted-out recipients excluded. + +7. **Refund/payment/invoice audit completeness** + - Every mutation must record actor, target, amount, old state, new state, source endpoint/job, and idempotency key where applicable. + - Reports must reconcile against the same canonical ledger behavior as API reads. + +### Module Acceptance Standard + +A module is not considered hardened unless: + +- every public endpoint has a permission rule; +- every mutation has an audit actor; +- every money, date, file, or message operation has edge-case tests; +- every response shape is documented or represented by an API Resource; +- every side effect is logged; +- every async/send action has retry and dedupe behavior; +- every legacy fallback is documented, quarantined, or removed; +- every controller-level validator has either been moved to a FormRequest or explicitly marked as temporary legacy behavior. + + +## 4. Evidence-Based Risk Notes + +These are not accusations; they are where bugs usually crawl out wearing a tiny hat. + +- **Raw SQL / raw query usage:** 59 files, 166 matches. +- **Direct request input access:** 50 files, 194 matches. +- **File operation endpoints:** 18 files, 34 matches. +- **Email or messaging behavior:** 62 files, 192 matches. +- **Auth/Gate/authorization usage:** 325 files, 482 matches. +- **Explicit DB transactions:** 48 files, 72 matches. +- **CodeIgniter compatibility surface:** 41 files, 62 matches. +- **Queued or dispatched work:** 7 files, 8 matches. +- **Cache-dependent behavior:** 5 files, 12 matches. + + +Review priority should follow blast radius: auth, finance, payments, attendance, grading, promotions, messaging, files, then lower-risk CRUD. + + + +## 5. Delivery Phases + +### Phase 0: Baseline and Guardrails + +- Add a project map document describing module ownership, service boundaries, and route ownership. +- Add static analysis to CI: PHPStan or Larastan, Pint/PHP-CS-Fixer, and a dead-code detector if practical. +- Add a minimal smoke test suite covering auth, health checks, and one representative CRUD module. +- Create fixtures/factories for User, Student, Family, Enrollment, Invoice, Payment, AttendanceRecord, Score, and Staff. +- Freeze API response contracts for critical endpoints with JSON snapshot tests. + +### Phase 1: Security and Access Control + +- Inventory every controller method and map required role/permission/policy. +- Build an endpoint authorization matrix with actor role, resource type, ownership boundary, action, policy/gate/middleware, and required negative tests. +- Confirm every mutation endpoint uses authorization before business logic. +- Normalize role switching/session timeout behavior and log security-relevant events. +- Review IP ban, login attempts, password reset cleanup, registration captcha, and auth session flows. +- Add negative tests proving unauthorized roles cannot read or mutate cross-family, cross-student, cross-staff, payment, invoice, file, role, or message data. +- Treat “authenticated” as insufficient for any financial, student, attendance, role, file, or communication mutation. Logged in is not allowed; it is merely present, like a bug report with no reproduction steps. + +### Phase 2: Financial Correctness + +- Define canonical money rules: rounding, currency precision, refund state machine, invoice adjustment rules, and payment reconciliation. +- Decide whether money is stored as integer cents or strict fixed-decimal values; do not use floating-point arithmetic for persistent financial state. +- Wrap invoice/payment/refund/discount/reimbursement mutations in transactions and audit logs. +- Lock invoice/payment/refund rows during balance-changing edits. +- Add idempotency rules to PayPal sync, payment notifications, manual payments, refunds, and invoice generation. +- Replace filename-driven payment file access with record-driven, authorized access. +- Build reconciliation tests: invoice total = tuition + fees + event charges + extras - discounts - refunds - payments. +- Add report consistency tests so PDFs, dashboard totals, and API totals cannot disagree like three humans in a meeting. + +### Phase 3: Student Lifecycle Correctness + +- Model the lifecycle from registration/enrollment through attendance, grading, report cards, and promotion. +- Add date boundary tests for semesters, attendance windows, class assignment windows, grading locks, and promotion deadlines. +- Centralize student/family visibility checks. +- Make promotion eligibility deterministic and explainable through audit logs. +- Add regression tests for edge cases: missing scores, below-60 notifications, class transfers, inactive students, deleted/unverified users. + +### Phase 4: Communications Reliability + +- Standardize recipient resolution for Email, Messaging, Notifications, WhatsApp, and BroadcastEmail. +- Require preview/dry-run paths for bulk sends. +- Add send logs, dedupe keys, failure states, and retry strategy. +- Separate composition from delivery. +- Test opt-outs, guardian/family recipient selection, teacher/staff targeting, and notification cleanup. + +### Phase 5: Operations and Reporting + +- Review PDF generation paths for badges, certificates, finance reports, report cards, slips, stickers, and print requests. +- Validate all file downloads and upload paths. +- Add operational health endpoints for database, config, cleanup scheduler, and time-sensitive jobs. +- Add runbooks for cleanup commands, payment sync, attendance publishing, and notification dispatch. + +### Phase 6: Documentation and API Contracts + +- Generate OpenAPI output from route definitions, once routes are included. +- Document each module: owner, purpose, inputs, outputs, permissions, side effects, and failure modes. +- Document legacy compatibility areas explicitly, especially CodeIgniter adapters and helper shims. +- Add API examples for primary user flows: login, role switch, parent dashboard, attendance submission, invoice payment, grading submission. + + + +## 6. Module-by-Module Plan + +Each module gets the same discipline: define ownership, confirm validation, confirm authorization, isolate service logic, test happy paths and failures, document side effects. + + + +### Admin + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 0 controllers, 2 services, 3 requests, 5 resources. + +**Services:** CompetitionWinnersDomain, CompetitionWinnersAdminService +**Requests:** StoreCompetitionWinnerRequest, UpdateCompetitionWinnerRequest, SaveCompetitionScoresRequest +**Resources:** PrintRecipientsResource, TeacherSubmissionReportResource, AdminNotificationAlertsResource, ProgressGroupResource, ProgressReportDetailResource +**Plan:** +- Confirm all inputs are validated through FormRequest classes. +- Confirm all reads and writes are authorized. +- Move business rules out of controllers if any remain. +- Add feature tests for list/show/create/update/delete and key failure paths. + +### Administrator + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 8 controllers, 18 services, 5 requests, 0 resources. + +**Controllers:** AdministratorAbsenceController, AdministratorDashboardController, AdministratorEnrollmentController, AdministratorNotificationController, AdministratorPromotionController, AdministratorTeacherSubmissionController, EmergencyContactController, TeacherClassAssignmentController +**Services:** AdministratorUserSearchService, AdministratorAbsenceService, AdministratorDashboardService, AdministratorMetricsService, AdministratorEnrollmentStatusService, AdministratorEnrollmentEventService, AdministratorNotificationService, AdminNotificationSubjectService, TeacherSubmissionSupportService, AdministratorSharedService, TeacherSubmissionNotificationService, TeacherSubmissionReportService ... +**Requests:** SavePrintRecipientsRequest, SaveAdminNotificationSubjectsRequest, SubmitAdministratorAbsenceRequest, UpdateEnrollmentStatusesRequest, SendTeacherSubmissionNotificationsRequest +**Plan:** +- Centralize identity lookup and cross-entity authorization. +- Test directory/search pagination and visibility filtering. +- Ensure updates cannot orphan related records or break family/student/staff links. +- Add lifecycle tests for active, inactive, newly enrolled, transferred, and deleted/unverified users. + +### Api + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 0 controllers, 0 services, 4 requests, 0 resources. + +**Requests:** SaveAdminNotificationSubjectsRequest, IndexAdminProgressRequest, SendTeacherSubmissionNotificationsRequest, SavePrintNotificationRecipientsRequest +**Plan:** +- Confirm all inputs are validated through FormRequest classes. +- Confirm all reads and writes are authorized. +- Move business rules out of controllers if any remain. +- Add feature tests for list/show/create/update/delete and key failure paths. + +### ApiRoot + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 7 controllers, 0 services, 0 requests, 0 resources. + +**Controllers:** BaseApiController, CiRequestAdapter, RolePermissionController, RoleSwitcherController, ScannerController, StatsController, UserController +**Plan:** +- Quarantine legacy/root controllers and define canonical replacements for duplicate controller names. +- Split `ScannerController` into a FormRequest, scanner orchestration service, badge lookup service, student scan service, staff scan service, late-arrival service, scan logger, and response factory. +- Add transaction boundaries and idempotency around scanner attendance mutations. +- Define whether `BaseApiController`, `CiRequestAdapter`, root role controllers, and namespaced role controllers are canonical or shims. +- Ensure shim classes contain no business logic. +- Add feature tests for scanner flows, role switching, stats visibility, and user visibility. + +#### Scanner refactor acceptance criteria + +- One public scan endpoint delegates to one scanner service action. +- All scan input is validated through a dedicated FormRequest. +- A duplicate scan within the configured duplicate window does not double-count attendance or create duplicate late-slip records. +- Student and staff scan paths have separate service-level tests. +- Scan logs are written once per accepted mutation and include actor/station/source context. + +### Assignment + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 1 controllers, 5 services, 2 requests, 2 resources. + +**Controllers:** AssignmentApiController +**Services:** AssignmentSectionService, AssignmentMetaService, AssignmentService, AssignmentContextService, AssignmentDataLoaderService +**Requests:** StoreAssignmentRequest, StoreStudentAssignmentRequest +**Resources:** AssignmentOverviewResource, AssignmentSectionResource +**Plan:** +- Define the academic workflow state machine and valid transitions. +- Test teacher/admin/parent role boundaries. +- Protect uploads and generated academic files. +- Add audit logs for status changes, submissions, and deadline changes. + +### Attendance + +**Priority:** P1 Student lifecycle +**Detected surface:** 6 controllers, 16 services, 7 requests, 7 resources. + +**Controllers:** AdminAttendanceApiController, AttendanceCommentTemplateController, EarlyDismissalsController, LateSlipLogsController, StaffAttendanceApiController, TeacherAttendanceApiController +**Services:** SemesterRangeService, TeacherAttendanceSubmissionService, AttendanceAutoPublishJobService, AttendanceAutoPublishService, LateSlipLogQueryService, AttendanceConsequenceService, AttendanceRecordSyncService, AttendanceDailySummaryService, AttendanceService, StudentAttendanceWriterService, LateSlipLogCommandService, AttendanceSummaryRebuildService ... +**Requests:** SaveStaffAttendanceCellRequest, UpdateAttendanceManagementRequest, LateSlipLogIndexRequest, TeacherSubmitAttendanceRequest, SaveAdminAttendanceRequest, TeacherUpdateGridRequest, AdminAddAttendanceEntryRequest +**Resources:** DailyAttendanceResource, AdminAttendanceResource, LateSlipLogCollection, LateSlipLogResource, TeacherGridResource, StaffMonthResource, TeacherAttendanceFormResource +**Plan:** +- Lock down date and semester boundary behavior before changing implementation. +- Test teacher/admin/parent visibility separately. +- Ensure auto-publish, notification, late slip, early dismissal, scanner scan, and violation rules are deterministic. +- Record who changed attendance, when, station/source context when relevant, and why. +- Move scan-driven attendance mutations into transactional services shared by scanner and attendance modules where appropriate. +- Add idempotency tests for repeated scanner submissions and repeated auto-publish attempts. + +### AttendanceCommentTemplate + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 0 controllers, 0 services, 2 requests, 0 resources. + +**Requests:** StoreAttendanceCommentTemplateRequest, UpdateAttendanceCommentTemplateRequest +**Plan:** +- Confirm all inputs are validated through FormRequest classes. +- Confirm all reads and writes are authorized. +- Move business rules out of controllers if any remain. +- Add feature tests for list/show/create/update/delete and key failure paths. + +### AttendanceTracking + +**Priority:** P1 Student lifecycle +**Detected surface:** 1 controllers, 12 services, 5 requests, 0 resources. + +**Controllers:** AttendanceTrackingController +**Services:** AttendanceNotificationLogService, AttendanceViolationStudentResolverService, AttendanceNotificationWorkflowService, AttendanceTrackingService, ViolationRuleEngineService, AttendanceMailerService, AttendanceParentLookupService, AttendanceCommunicationSupportService, AttendanceEmailComposerService, AttendancePendingViolationService, AttendanceCaseQueryService, DefaultAttendanceMailerService +**Requests:** RecordAttendanceTrackingRequest, ComposeAttendanceEmailRequest, SendAttendanceManualEmailRequest, ParentsInfoRequest, SaveAttendanceNotificationNoteRequest +**Plan:** +- Lock down date and semester boundary behavior before changing implementation. +- Test teacher/admin/parent visibility separately. +- Ensure auto-publish, notification, late slip, early dismissal, and violation rules are deterministic. +- Record who changed attendance, when, and why. + +### Auth + +**Priority:** P0 Security/Foundation +**Detected surface:** 7 controllers, 9 services, 3 requests, 1 resources. + +**Controllers:** AuthController, AuthSessionController, IpBanController, RegisterController, RolePermissionController, RoleSwitcherController, SessionTimeoutController +**Services:** RegistrationCaptchaService, PermissionCheckService, CodeIgniterApiRegistrationService, RegistrationService, ApiLoginSecurityService, AuthSessionService, UserRoleService, PasswordResetCleanupService, RegistrationFormatterService +**Requests:** RegisterRequest, SetRoleSessionRequest, LoginSessionRequest +**Resources:** RegisterResource +**Plan:** +- Map every endpoint to required auth state, role, and permission. +- Test login throttling, IP bans, password reset cleanup, session timeout, role switching, and registration captcha. +- Confirm security logs include actor, IP, user agent, outcome, and reason. +- Reject role changes that create privilege escalation or stale-session leaks. + +### BadgeScan + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 1 controllers, 1 services, 0 requests, 0 resources. + +**Controllers:** BadgeScanController +**Services:** BadgeScanService +**Plan:** +- Confirm all inputs are validated through FormRequest classes. +- Confirm all reads and writes are authorized. +- Move business rules out of controllers if any remain. +- Add feature tests for list/show/create/update/delete and key failure paths. + +### Badges + +**Priority:** P2 Operations +**Detected surface:** 1 controllers, 7 services, 3 requests, 0 resources. + +**Controllers:** BadgeController +**Services:** BadgeTextFormatter, BadgeStudentLookupService, BadgeFormDataService, BadgePdfService, BadgeUserLookupService, BadgeService, BadgePrintLogService +**Requests:** BadgePrintStatusRequest, GenerateBadgePdfRequest, LogBadgePrintRequest +**Plan:** +- Review upload/download/PDF generation permissions. +- Validate file type, file size, generated names, and storage paths. +- Add tests for inventory movement consistency and purchase-order state transitions where applicable. +- Make printable outputs reproducible from stable data snapshots. + +### Billing + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 0 controllers, 3 services, 0 requests, 0 resources. + +**Services:** BalanceCalculationService, BillingTotalsService, ChargeService +**Plan:** +- Confirm all inputs are validated through FormRequest classes. +- Confirm all reads and writes are authorized. +- Move business rules out of controllers if any remain. +- Add feature tests for list/show/create/update/delete and key failure paths. + +### BroadcastEmail + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 0 controllers, 5 services, 0 requests, 0 resources. + +**Services:** BroadcastEmailComposerService, BroadcastEmailRecipientService, BroadcastEmailDispatchService, BroadcastEmailImageService, BroadcastEmailSenderOptionsService +**Plan:** +- Separate recipient selection, message composition, dispatch, and logging. +- Add preview/dry-run tests before bulk sending. +- Deduplicate sends and persist failure states. +- Verify opt-outs, guardian selection, role targeting, and retry behavior. + +### Certificates + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 1 controllers, 1 services, 0 requests, 0 resources. + +**Controllers:** CertificateController +**Services:** CertificatePdfService +**Plan:** +- Review upload/download/PDF generation permissions. +- Validate file type, file size, generated names, and storage paths. +- Add tests for inventory movement consistency and purchase-order state transitions where applicable. +- Make printable outputs reproducible from stable data snapshots. + +### ClassPrep + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 0 controllers, 2 services, 0 requests, 0 resources. + +**Services:** ClassRosterService, StickerCountService +**Plan:** +- Confirm all inputs are validated through FormRequest classes. +- Confirm all reads and writes are authorized. +- Move business rules out of controllers if any remain. +- Add feature tests for list/show/create/update/delete and key failure paths. + +### ClassPreparation + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 1 controllers, 9 services, 4 requests, 2 resources. + +**Controllers:** ClassPreparationController +**Services:** ClassPreparationAdjustmentWriterService, ClassPreparationCalculatorService, ClassPreparationLogService, ClassPreparationAdjustmentService, ClassPreparationContextService, ClassPreparationService, ClassPreparationPrintService, ClassPreparationRosterService, ClassPreparationInventoryService +**Requests:** ClassPreparationMarkPrintedRequest, ClassPreparationPrintRequest, ClassPreparationIndexRequest, ClassPreparationAdjustmentRequest +**Resources:** ClassPreparationPrintResource, ClassPreparationResultResource +**Plan:** +- Define the academic workflow state machine and valid transitions. +- Test teacher/admin/parent role boundaries. +- Protect uploads and generated academic files. +- Add audit logs for status changes, submissions, and deadline changes. + +### ClassProgress + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 1 controllers, 5 services, 5 requests, 4 resources. + +**Controllers:** ClassProgressController +**Services:** ClassProgressRuleService, ClassProgressAttachmentService, ClassProgressQueryService, ClassProgressMetaService, ClassProgressMutationService +**Requests:** ClassProgressStoreRequest, ClassProgressMetaRequest, ClassProgressIndexRequest, ClassProgressFormRequest, ClassProgressUpdateRequest +**Resources:** ClassProgressGroupCollection, ClassProgressAttachmentResource, ClassProgressShowResource, ClassProgressReportResource +**Plan:** +- Define the academic workflow state machine and valid transitions. +- Test teacher/admin/parent role boundaries. +- Protect uploads and generated academic files. +- Add audit logs for status changes, submissions, and deadline changes. + +### ClassSections + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 0 controllers, 4 services, 0 requests, 0 resources. + +**Services:** ClassSectionCommandService, ClassAttendanceService, ClassSectionSeedService, ClassSectionQueryService +**Plan:** +- Define the academic workflow state machine and valid transitions. +- Test teacher/admin/parent role boundaries. +- Protect uploads and generated academic files. +- Add audit logs for status changes, submissions, and deadline changes. + +### Classes + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 1 controllers, 0 services, 3 requests, 4 resources. + +**Controllers:** ClassController +**Requests:** ClassSectionUpdateRequest, ClassSectionStoreRequest, ClassSectionIndexRequest +**Resources:** ClassSectionCollection, ClassSectionResource, ClassAttendanceResource, ClassAttendanceStudentResource +**Plan:** +- Define the academic workflow state machine and valid transitions. +- Test teacher/admin/parent role boundaries. +- Protect uploads and generated academic files. +- Add audit logs for status changes, submissions, and deadline changes. + +### Communication + +**Priority:** P1 Communications +**Detected surface:** 1 controllers, 6 services, 0 requests, 0 resources. + +**Controllers:** CommunicationController +**Services:** CommunicationFamilyService, CommunicationSendService, CommunicationTemplateService, CommunicationPreviewService, CommunicationStudentService, CommunicationLogService +**Plan:** +- Separate recipient selection, message composition, dispatch, and logging. +- Add preview/dry-run tests before bulk sending. +- Deduplicate sends and persist failure states. +- Verify opt-outs, guardian selection, role targeting, and retry behavior. + +### CompetitionScores + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 1 controllers, 4 services, 0 requests, 0 resources. + +**Controllers:** CompetitionScoresController +**Services:** CompetitionScoresRosterService, CompetitionScoresSaveService, CompetitionScoresContextService, CompetitionScoresQueryService +**Plan:** +- Confirm all inputs are validated through FormRequest classes. +- Confirm all reads and writes are authorized. +- Move business rules out of controllers if any remain. +- Add feature tests for list/show/create/update/delete and key failure paths. + +### Core + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 2 controllers, 0 services, 0 requests, 0 resources. + +**Controllers:** BaseApiController, CiRequestAdapter +**Plan:** +- Confirm config reads/writes are permissioned and validated. +- Add health/config smoke tests. +- Document public versus authenticated surfaces. +- Keep UI/style/static-page services separated from domain mutations. + +### Dashboard + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 0 controllers, 1 services, 0 requests, 0 resources. + +**Services:** DashboardRouteService +**Plan:** +- Confirm config reads/writes are permissioned and validated. +- Add health/config smoke tests. +- Document public versus authenticated surfaces. +- Keep UI/style/static-page services separated from domain mutations. + +### Discounts + +**Priority:** P0 Financial correctness +**Detected surface:** 1 controllers, 5 services, 3 requests, 2 resources. + +**Controllers:** DiscountController +**Services:** DiscountParentService, DiscountApplyService, DiscountInvoiceService, DiscountVoucherService, DiscountContextService +**Requests:** UpdateDiscountVoucherRequest, ApplyDiscountVoucherRequest, StoreDiscountVoucherRequest +**Resources:** DiscountVoucherResource, DiscountParentResource +**Plan:** +- Define one source of truth for monetary calculations and rounding. +- Require transactions around every mutation that changes balance, invoice state, refund state, payment state, or reimbursement status. +- Add audit logs and idempotency keys for external payment/payment-notification flows. +- Build reconciliation tests across API, reports, dashboards, and PDFs. + +### Docs + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 0 controllers, 3 services, 0 requests, 0 resources. + +**Services:** DocsCatalogService, ApiDocsService, OpenApiRouteExporter +**Plan:** +- Confirm config reads/writes are permissioned and validated. +- Add health/config smoke tests. +- Document public versus authenticated surfaces. +- Keep UI/style/static-page services separated from domain mutations. + +### Documentation + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 2 controllers, 0 services, 0 requests, 0 resources. + +**Controllers:** ApiDocsAdminController, DocsCatalogController +**Plan:** +- Confirm config reads/writes are permissioned and validated. +- Add health/config smoke tests. +- Document public versus authenticated surfaces. +- Keep UI/style/static-page services separated from domain mutations. + +### Email + +**Priority:** P1 Communications +**Detected surface:** 3 controllers, 5 services, 2 requests, 3 resources. + +**Controllers:** BroadcastEmailController, EmailController, EmailExtractorController +**Services:** EmailAttachmentService, EmailExtractorService, EmailProfileService, EmailDispatchService, EmailSenderOptionsService +**Requests:** EmailExtractorCompareRequest, EmailSendRequest +**Resources:** EmailSenderResource, EmailExtractorEmailsResource, EmailExtractorCompareResource +**Plan:** +- Separate recipient selection, message composition, dispatch, and logging. +- Add preview/dry-run tests before bulk sending. +- Deduplicate sends and persist failure states. +- Verify opt-outs, guardian selection, role targeting, and retry behavior. + +### EmergencyContacts + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 0 controllers, 2 services, 2 requests, 3 resources. + +**Services:** EmergencyContactDirectoryService, EmergencyContactCrudService +**Requests:** EmergencyContactParentRequest, EmergencyContactUpdateRequest +**Resources:** EmergencyContactGroupResource, EmergencyContactStudentResource, EmergencyContactResource +**Plan:** +- Confirm all inputs are validated through FormRequest classes. +- Confirm all reads and writes are authorized. +- Move business rules out of controllers if any remain. +- Add feature tests for list/show/create/update/delete and key failure paths. + +### Events + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 0 controllers, 6 services, 6 requests, 3 resources. + +**Services:** EventChargeService, EventStudentChargeService, EventCategoryService, EventListService, EventChargeQueryService, EventManagementService +**Requests:** EventIndexRequest, EventUpdateRequest, EventStoreRequest, EventStudentsWithChargesRequest, EventChargeUpdateRequest, EventChargeIndexRequest +**Resources:** EventChargeResource, EventResource, EventStudentChargeResource +**Plan:** +- Confirm all inputs are validated through FormRequest classes. +- Confirm all reads and writes are authorized. +- Move business rules out of controllers if any remain. +- Add feature tests for list/show/create/update/delete and key failure paths. + +### Exams + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 1 controllers, 4 services, 3 requests, 1 resources. + +**Controllers:** ExamDraftController +**Services:** ExamDraftTeacherService, ExamDraftAdminService, ExamDraftConfigService, ExamDraftFileService +**Requests:** ExamDraftTeacherStoreRequest, ExamDraftAdminLegacyRequest, ExamDraftAdminReviewRequest +**Resources:** ExamDraftResource +**Plan:** +- Define the academic workflow state machine and valid transitions. +- Test teacher/admin/parent role boundaries. +- Protect uploads and generated academic files. +- Add audit logs for status changes, submissions, and deadline changes. + +### Expenses + +**Priority:** P0 Financial correctness +**Detected surface:** 1 controllers, 7 services, 3 requests, 2 resources. + +**Controllers:** ExpenseController +**Services:** ExpenseQueryService, ExpenseStaffService, ExpenseRetailorService, ExpenseReceiptService, ExpenseWriteService, ExpenseContextService, ExpenseStatusService +**Requests:** UpdateExpenseRequest, StoreExpenseRequest, UpdateExpenseStatusRequest +**Resources:** ExpenseResource, ExpenseStaffResource +**Plan:** +- Define one source of truth for monetary calculations and rounding. +- Require transactions around every mutation that changes balance, invoice state, refund state, payment state, or reimbursement status. +- Add audit logs and idempotency keys for external payment/payment-notification flows. +- Build reconciliation tests across API, reports, dashboards, and PDFs. + +### ExtraCharges + +**Priority:** P0 Financial correctness +**Detected surface:** 1 controllers, 6 services, 2 requests, 3 resources. + +**Controllers:** ExtraChargesController +**Services:** ExtraChargesMetaService, ExtraChargesInvoiceService, ExtraChargesParentService, ExtraChargesChargeService, ExtraChargesContextService, ExtraChargesListService +**Requests:** UpdateExtraChargeRequest, StoreExtraChargeRequest +**Resources:** ExtraChargeInvoiceOptionResource, ExtraChargeResource, ExtraChargeParentOptionResource +**Plan:** +- Define one source of truth for monetary calculations and rounding. +- Require transactions around every mutation that changes balance, invoice state, refund state, payment state, or reimbursement status. +- Add audit logs and idempotency keys for external payment/payment-notification flows. +- Build reconciliation tests across API, reports, dashboards, and PDFs. + +### Families + +**Priority:** P1 Student lifecycle +**Detected surface:** 0 controllers, 4 services, 14 requests, 7 resources. + +**Services:** FamilyNotificationService, FamilyFinanceService, FamilyMutationService, FamilyQueryService +**Requests:** FamilyImportLegacyRequest, FamilyUnlinkStudentRequest, FamilySetPrimaryHomeRequest, FamilyAdminCardRequest, FamiliesByStudentRequest, FamilyBootstrapRequest, FamilyAttachSecondByEmailRequest, FamilyAdminSearchRequest ... +**Resources:** FamilyPaymentResource, FamilyInvoiceResource, FamilySearchItemResource, FamilyResource, FamilyGuardianResource, FamilyStudentResource, FamilyEmergencyContactResource +**Plan:** +- Centralize identity lookup and cross-entity authorization. +- Test directory/search pagination and visibility filtering. +- Ensure updates cannot orphan related records or break family/student/staff links. +- Add lifecycle tests for active, inactive, newly enrolled, transferred, and deleted/unverified users. + +### Family + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 2 controllers, 0 services, 0 requests, 0 resources. + +**Controllers:** FamilyAdminController, FamilyController +**Plan:** +- Confirm all inputs are validated through FormRequest classes. +- Confirm all reads and writes are authorized. +- Move business rules out of controllers if any remain. +- Add feature tests for list/show/create/update/delete and key failure paths. + +### Fees + +**Priority:** P0 Financial correctness +**Detected surface:** 0 controllers, 6 services, 2 requests, 6 resources. + +**Services:** FeeConfigService, TuitionCalculationService, FeeRefundCalculatorService, FeeGradeService, FeeRefundDetailService, FeeStudentFeeService +**Requests:** FeeTuitionTotalRequest, FeeRefundRequest +**Resources:** FeeRefundResource, FeeFamilyBalanceResource, ChargeActionResource, ChargeListResource, FeeTuitionBreakdownResource, FeeTuitionTotalResource +**Plan:** +- Define one source of truth for monetary calculations and rounding. +- Require transactions around every mutation that changes balance, invoice state, refund state, payment state, or reimbursement status. +- Add audit logs and idempotency keys for external payment/payment-notification flows. +- Build reconciliation tests across API, reports, dashboards, and PDFs. + +### Files + +**Priority:** P2 Operations +**Detected surface:** 0 controllers, 2 services, 1 requests, 1 resources. + +**Services:** FileServeService, ExamDraftDownloadNameService +**Requests:** FileNameRequest +**Resources:** FileMetaResource +**Plan:** +- Review upload/download/PDF generation permissions. +- Validate file type, file size, generated names, and storage paths. +- Add tests for inventory movement consistency and purchase-order state transitions where applicable. +- Make printable outputs reproducible from stable data snapshots. + +### Finance + +**Priority:** P0 Financial correctness +**Detected surface:** 14 controllers, 8 services, 12 requests, 9 resources. + +**Controllers:** ChargeController, FeeCalculationController, FinancialController, InvoiceController, PaymentController, PaymentEventChargesController, PaymentManualController, PaymentNotificationController, PaymentTransactionController, PaypalPaymentController ... +**Services:** FinancialPdfReportService, FinancialSchoolYearService, FinancialPaymentService, FinancialReportService, FinancialSummaryService, FinancialChartService, FinancialDonationRecipientService, FinancialUnpaidParentsService +**Requests:** FinancialUnpaidParentsRequest, FeeTuitionTotalRequest, ChargeListRequest, FinancialReportRequest, FinancialSummaryRequest, MarkEventChargePaidRequest, FeeTuitionBreakdownRequest, StoreExtraChargeRequest ... +**Resources:** FinancialReportResource, FinancialSummaryResource, FinancialExpenseSummaryResource, FinancialInvoiceResource, FinancialPaymentResource, FinancialRefundResource, FinancialUnpaidParentResource, FinancialDiscountResource ... +**Plan:** +- Define one source of truth for monetary calculations and rounding. +- Require transactions around every mutation that changes balance, invoice state, refund state, payment state, or reimbursement status. +- Add audit logs and idempotency keys for external payment/payment-notification flows. +- Harden payment file access before any broader finance refactor: no filename-only authorization, no arbitrary file resolution, no unaudited downloads. +- Lock affected invoice/payment/refund/reimbursement rows during edits and recalculations. +- Normalize all finance responses through resources so dashboards, APIs, reports, and PDFs do not compute separate truths, because apparently one truth was not complicated enough. +- Build reconciliation tests across API, reports, dashboards, and PDFs. + +#### Finance P0 acceptance criteria + +- Manual payment create/edit/delete and invoice recalculation run inside transactions. +- Payment edits create before/after audit records. +- Payment file downloads are authorized by payment/invoice ownership or finance role. +- Wrong-role and wrong-owner tests exist for invoices, payments, refunds, reimbursements, and downloadable files. +- All finance mutation tests assert final ledger/invoice balance, not just HTTP status. + +### Frontend + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 4 controllers, 9 services, 2 requests, 6 resources. + +**Controllers:** FrontendController, InfoIconController, LandingPageController, PageController +**Services:** LandingPageTeacherSummaryService, LandingPageRoleService, LandingPageParentDashboardService, LandingPageService, StaticPageService, FrontendPageService, ContactSubmissionService, LandingPageContextService, ProfileIconService +**Requests:** LandingTeacherRequest, ContactSubmitRequest +**Resources:** LandingPageResource, ContactSubmissionResource, FrontendPageResource, FrontendUserResource, ProfileIconResource, PageContentResource +**Plan:** +- Confirm config reads/writes are permissioned and validated. +- Add health/config smoke tests. +- Document public versus authenticated surfaces. +- Keep UI/style/static-page services separated from domain mutations. + +### Grading + +**Priority:** P1 Student lifecycle +**Detected surface:** 2 controllers, 9 services, 18 requests, 3 resources. + +**Controllers:** GradingController, HomeworkTrackingController +**Services:** BelowSixtyEmailService, GradingLockService, GradingOverviewService, GradingPlacementService, HomeworkTrackingService, GradingScoreService, GradingBelowSixtyService, HomeworkTrackingCalendarService, GradingRefreshService +**Requests:** BelowSixtyListRequest, BelowSixtySendRequest, GradingLockAllRequest, BelowSixtyMeetingRequest, PlacementLevelsRequest, GradingRefreshRequest, GradingScoreShowRequest, HomeworkTrackingRequest ... +**Resources:** BelowSixtyStudentResource, GradingScoreResource, HomeworkTrackingTeacherResource +**Plan:** +- Centralize score formulas and missing-score behavior. +- Add golden test cases for semester score, quiz, homework, project, participation, final/midterm, and prediction risk calculations. +- Prove grading locks block writes but not allowed reads. +- Verify below-60 communication and parent visibility rules. + +### Incidents + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 1 controllers, 4 services, 5 requests, 4 resources. + +**Controllers:** IncidentController +**Services:** CurrentIncidentService, IncidentHistoryService, IncidentLookupService, IncidentAnalysisService +**Requests:** IncidentCancelRequest, IncidentCloseRequest, IncidentStoreRequest, IncidentListRequest, IncidentStateRequest +**Resources:** IncidentStudentOptionResource, IncidentAnalysisStudentResource, IncidentResource, IncidentGradeResource +**Plan:** +- Confirm all inputs are validated through FormRequest classes. +- Confirm all reads and writes are authorized. +- Move business rules out of controllers if any remain. +- Add feature tests for list/show/create/update/delete and key failure paths. + +### Inventory + +**Priority:** P2 Operations +**Detected surface:** 5 controllers, 8 services, 19 requests, 5 resources. + +**Controllers:** InventoryCategoryController, InventoryController, InventoryMovementController, SupplierController, SupplyCategoryController +**Services:** InventorySummaryService, InventoryCategoryService, InventoryItemService, InventoryContextService, SupplierService, InventoryMovementService, SupplyCategoryService, InventoryTeacherDistributionService +**Requests:** InventoryItemIndexRequest, SupplierUpdateRequest, InventoryCategoryStoreRequest, InventoryTeacherDistributionRequest, InventoryItemUpdateRequest, InventoryItemStoreRequest, InventoryTeacherDistributionStoreRequest, InventoryCategoryUpdateRequest ... +**Resources:** InventoryItemResource, InventoryCategoryResource, InventoryMovementResource, SupplierResource, SupplyCategoryResource +**Plan:** +- Review upload/download/PDF generation permissions. +- Validate file type, file size, generated names, and storage paths. +- Add tests for inventory movement consistency and purchase-order state transitions where applicable. +- Make printable outputs reproducible from stable data snapshots. + +### Invoices + +**Priority:** P0 Financial correctness +**Detected surface:** 0 controllers, 8 services, 4 requests, 2 resources. + +**Services:** InvoiceGradeService, InvoiceConfigService, InvoicePaymentService, InvoiceTuitionService, InvoiceManagementService, InvoicePdfService, InvoiceService, InvoiceGenerationService +**Requests:** InvoiceStatusRequest, InvoiceManagementRequest, InvoiceParentRequest, InvoiceGenerateRequest +**Resources:** InvoiceManagementParentResource, InvoiceResource +**Plan:** +- Define one source of truth for monetary calculations and rounding. +- Require transactions around every mutation that changes balance, invoice state, refund state, payment state, or reimbursement status. +- Add audit logs and idempotency keys for external payment/payment-notification flows. +- Build reconciliation tests across API, reports, dashboards, and PDFs. + +### Messaging + +**Priority:** P1 Communications +**Detected surface:** 2 controllers, 3 services, 3 requests, 3 resources. + +**Controllers:** MessagesController, WhatsappController +**Services:** MessageRecipientService, MessageCommandService, MessageQueryService +**Requests:** MessageSendRequest, MessageIndexRequest, MessageUpdateRequest +**Resources:** MessageResource, MessageCollection, RecipientResource +**Plan:** +- Separate recipient selection, message composition, dispatch, and logging. +- Add preview/dry-run tests before bulk sending. +- Deduplicate sends and persist failure states. +- Verify opt-outs, guardian selection, role targeting, and retry behavior. + +### Navigation + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 0 controllers, 2 services, 0 requests, 0 resources. + +**Services:** NavBuilderService, NavbarService +**Plan:** +- Confirm config reads/writes are permissioned and validated. +- Add health/config smoke tests. +- Document public versus authenticated surfaces. +- Keep UI/style/static-page services separated from domain mutations. + +### NonApi + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 3 controllers, 0 services, 0 requests, 0 resources. + +**Controllers:** Controller, DocsController, TeacherCalendarPageController +**Plan:** +- Confirm all inputs are validated through FormRequest classes. +- Confirm all reads and writes are authorized. +- Move business rules out of controllers if any remain. +- Add feature tests for list/show/create/update/delete and key failure paths. + +### Notifications + +**Priority:** P1 Communications +**Detected surface:** 1 controllers, 12 services, 5 requests, 2 resources. + +**Controllers:** NotificationController +**Services:** NotificationDispatchService, NotificationRecipientService, NotificationShowService, NotificationManagementService, NotificationReadService, NotificationUserListService, NotificationCleanupService, UserNotificationDispatchService, NotificationTriggerService, NotificationSendService, NotificationActiveService, NotificationDeletedService +**Requests:** NotificationSendRequest, NotificationActiveRequest, NotificationDeletedRequest, NotificationUpdateRequest, NotificationIndexRequest +**Resources:** UserNotificationResource, NotificationResource +**Plan:** +- Separate recipient selection, message composition, dispatch, and logging. +- Add preview/dry-run tests before bulk sending. +- Deduplicate sends and persist failure states. +- Verify opt-outs, guardian selection, role targeting, and retry behavior. + +### Parents + +**Priority:** P1 Student lifecycle +**Detected surface:** 5 controllers, 14 services, 16 requests, 4 resources. + +**Controllers:** AuthorizedUserInviteController, AuthorizedUsersController, ParentAttendanceReportController, ParentController, ParentPromotionController +**Services:** ParentEventParticipationService, ParentRegistrationService, PrimaryParentUserResolver, ParentAttendanceReportCalendarService, ParentAttendanceService, AuthorizedUsersManagementService, ParentEnrollmentService, ParentInvoiceService, ParentProgressQueryService, ParentAttendanceReportService, ParentReportCardService, ParentConfigService ... +**Requests:** ParentAttendanceReportUpdateRequest, ParentAttendanceReportListRequest, ParentAttendanceReportSubmitRequest, ParentEmergencyContactRequest, UpdateAuthorizedUserRequest, ParentAttendanceReportCheckRequest, ParentProfileUpdateRequest, ParentInvoiceRequest ... +**Resources:** ParentEmergencyContactResource, ParentAttendanceReportResource, ParentStudentResource, ParentAttendanceResource +**Plan:** +- Centralize identity lookup and cross-entity authorization. +- Test directory/search pagination and visibility filtering. +- Ensure updates cannot orphan related records or break family/student/staff links. +- Add lifecycle tests for active, inactive, newly enrolled, transferred, and deleted/unverified users. + +### Payments + +**Priority:** P0 Financial correctness +**Detected surface:** 0 controllers, 14 services, 15 requests, 4 resources. + +**Services:** PaymentNotificationDispatchService, PaymentMissedCheckService, PaymentManualService, PaymentLookupService, PaymentTestNotificationService, PaypalPaymentSyncService, PaymentBalanceService, PaypalPaymentService, PaymentTransactionService, PaymentEnrollmentEventService, PaymentNotificationService, PaymentPlanService ... +**Requests:** PaymentManualSearchRequest, PaymentEventChargesListRequest, PaymentNotificationSendRequest, PaymentNotificationListRequest, PaymentCreateRequest, PaypalTransactionsListRequest, PaymentManualEditRequest, PaymentEventChargesStoreRequest ... +**Resources:** PaymentNotificationLogResource, PaymentTransactionResource, PaypalTransactionResource, PaymentResource +**Plan:** +- Define one source of truth for payment state transitions, balance calculations, file handling, and notification dispatch. +- Wrap manual payment edits and invoice recalculation in one transaction with row locks. +- Replace filename-only check-file downloads with payment-record-based downloads and authorization. +- Add idempotency keys for PayPal sync, notification dispatch, manual payment creation, and retry flows. +- Store audit trails for payment create/edit/delete, refund application, missed-check updates, and external transaction sync. +- Add reconciliation tests across API, reports, dashboards, and PDFs. + +#### PaymentManualService hardening checklist + +- `recordPayment()` and `editPayment()` use the same transaction/audit/recalculation rules. +- `serveCheckFile()` cannot be called with only a user-supplied filename; it must resolve files from a payment record. +- Upload validation covers extension, MIME, size, storage path, and scan-friendly safe names. +- Tests cover unrelated parent access, unrelated staff access, valid finance/admin access, guessed filename, missing file, and duplicate edit attempts. + +### Phone + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 0 controllers, 1 services, 0 requests, 0 resources. + +**Services:** PhoneFormatterService +**Plan:** +- Confirm all inputs are validated through FormRequest classes. +- Confirm all reads and writes are authorized. +- Move business rules out of controllers if any remain. +- Add feature tests for list/show/create/update/delete and key failure paths. + +### Policy + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 0 controllers, 1 services, 0 requests, 0 resources. + +**Services:** PolicyContentService +**Plan:** +- Confirm config reads/writes are permissioned and validated. +- Add health/config smoke tests. +- Document public versus authenticated surfaces. +- Keep UI/style/static-page services separated from domain mutations. + +### Preferences + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 0 controllers, 3 services, 2 requests, 3 resources. + +**Services:** PreferencesCommandService, PreferencesQueryService, PreferencesOptionsService +**Requests:** PreferencesIndexRequest, PreferencesUpsertRequest +**Resources:** PreferencesListResource, PreferencesCollection, PreferencesResource +**Plan:** +- Confirm config reads/writes are permissioned and validated. +- Add health/config smoke tests. +- Document public versus authenticated surfaces. +- Keep UI/style/static-page services separated from domain mutations. + +### PrintRequests + +**Priority:** P2 Operations +**Detected surface:** 1 controllers, 1 services, 0 requests, 0 resources. + +**Controllers:** PrintRequestsController +**Services:** PrintRequestsPortalService +**Plan:** +- Review upload/download/PDF generation permissions. +- Validate file type, file size, generated names, and storage paths. +- Add tests for inventory movement consistency and purchase-order state transitions where applicable. +- Make printable outputs reproducible from stable data snapshots. + +### Promotions + +**Priority:** P1 Student lifecycle +**Detected surface:** 0 controllers, 7 services, 8 requests, 4 resources. + +**Services:** PromotionStatusService, PromotionEnrollmentService, PromotionReminderService, LevelProgressionService, PromotionAuditService, PromotionQueryService, PromotionEligibilityService +**Requests:** SendReminderRequest, ParentPromotionOverviewRequest, AdminListPromotionsRequest, EvaluateEligibilityRequest, UpdatePromotionStatusRequest, ParentEnrollmentStepRequest, SetEnrollmentDeadlineRequest, UpsertLevelProgressionRequest +**Resources:** LevelProgressionResource, PromotionAuditLogResource, PromotionReminderResource, StudentPromotionResource +**Plan:** +- Define the academic workflow state machine and valid transitions. +- Test teacher/admin/parent role boundaries. +- Protect uploads and generated academic files. +- Add audit logs for status changes, submissions, and deadline changes. + +### Public + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 1 controllers, 0 services, 0 requests, 0 resources. + +**Controllers:** PublicWinnersController +**Plan:** +- Confirm config reads/writes are permissioned and validated. +- Add health/config smoke tests. +- Document public versus authenticated surfaces. +- Keep UI/style/static-page services separated from domain mutations. + +### PublicWinners + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 0 controllers, 1 services, 0 requests, 0 resources. + +**Services:** PublicWinnersPortalService +**Plan:** +- Confirm all inputs are validated through FormRequest classes. +- Confirm all reads and writes are authorized. +- Move business rules out of controllers if any remain. +- Add feature tests for list/show/create/update/delete and key failure paths. + +### PurchaseOrders + +**Priority:** P2 Operations +**Detected surface:** 0 controllers, 4 services, 3 requests, 2 resources. + +**Services:** PurchaseOrderStatusService, PurchaseOrderReceiveService, PurchaseOrderCreateService, PurchaseOrderQueryService +**Requests:** PurchaseOrderIndexRequest, PurchaseOrderStoreRequest, PurchaseOrderReceiveRequest +**Resources:** PurchaseOrderResource, PurchaseOrderItemResource +**Plan:** +- Review upload/download/PDF generation permissions. +- Validate file type, file size, generated names, and storage paths. +- Add tests for inventory movement consistency and purchase-order state transitions where applicable. +- Make printable outputs reproducible from stable data snapshots. + +### Refunds + +**Priority:** P0 Financial correctness +**Detected surface:** 0 controllers, 9 services, 6 requests, 1 resources. + +**Services:** RefundSummaryService, RefundInvoiceAdjustmentService, RefundPolicyService, RefundPayoutService, RefundRequestService, RefundQueryService, RefundOverpaymentService, RefundDecisionService, RefundNotificationService +**Requests:** RefundStoreRequest, RefundPaymentRequest, RefundDecisionRequest, RefundRecalculateRequest, RefundIndexRequest, RefundRejectRequest +**Resources:** RefundResource +**Plan:** +- Define one source of truth for monetary calculations and rounding. +- Require transactions around every mutation that changes balance, invoice state, refund state, payment state, or reimbursement status. +- Add audit logs and idempotency keys for external payment/payment-notification flows. +- Build reconciliation tests across API, reports, dashboards, and PDFs. + +### Reimbursements + +**Priority:** P0 Financial correctness +**Detected surface:** 0 controllers, 11 services, 13 requests, 5 resources. + +**Services:** ReimbursementFileService, ReimbursementBatchAssignmentService, ReimbursementContextService, ReimbursementExportService, ReimbursementLookupService, ReimbursementEmailService, ReimbursementRecipientService, ReimbursementBatchService, ReimbursementQueryService, ReimbursementDonationService, ReimbursementCrudService +**Requests:** ReimbursementBatchAdminFileRequest, ReimbursementUpdateRequest, ReimbursementBatchCreateRequest, ReimbursementBatchEmailRequest, ReimbursementProcessRequest, ReimbursementExportBatchRequest, ReimbursementIndexRequest, ReimbursementExportRequest ... +**Resources:** ReimbursementRecipientResource, ReimbursementExpenseResource, ReimbursementBatchResource, ReimbursementResource, ReimbursementUnderProcessingItemResource +**Plan:** +- Define one source of truth for monetary calculations and rounding. +- Require transactions around every mutation that changes balance, invoice state, refund state, payment state, or reimbursement status. +- Add audit logs and idempotency keys for external payment/payment-notification flows. +- Build reconciliation tests across API, reports, dashboards, and PDFs. + +### Reports + +**Priority:** P2 Operations +**Detected surface:** 4 controllers, 9 services, 11 requests, 8 resources. + +**Controllers:** FilesController, ReportCardsController, SlipPrinterController, StickersController +**Services:** SlipPrinterFormatterService, SlipPrinterPdfService, SlipPrinterService, SlipPrinterConfigService, StickerPrintService, StickerPresetService, StickerQueryService, StickerContextService, ReportCardService +**Requests:** SlipPreviewRequest, SlipLogListRequest, SlipPrintRequest, SlipReprintRequest, StickerStudentsByClassRequest, StickerPrintRequest, StickerFormDataRequest, ReportCardCompletenessRequest ... +**Resources:** LateSlipLogResource, StickerStudentResource, StickerPresetResource, StickerClassSectionResource, ReportCardStudentMetaResource, ReportCardAcknowledgementResource, ReportCardClassSectionResource, ReportCardCompletenessStudentResource +**Plan:** +- Review upload/download/PDF generation permissions. +- Validate file type, file size, generated names, and storage paths. +- Add tests for inventory movement consistency and purchase-order state transitions where applicable. +- Make printable outputs reproducible from stable data snapshots. + +### Roles + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 0 controllers, 8 services, 9 requests, 4 resources. + +**Services:** RolePermissionService, RoleDashboardService, RoleCrudService, RoleAssignmentService, RoleSwitchService, RoleStaffService, PermissionCrudService, RoleQueryService +**Requests:** AssignUserRolesRequest, RolePermissionUpdateRequest, UserRoleListRequest, PermissionUpdateRequest, PermissionStoreRequest, RoleSwitchRequest, RoleStoreRequest, RoleUpdateRequest ... +**Resources:** RolePermissionResource, RoleResource, UserWithRolesResource, PermissionResource +**Plan:** +- Confirm all inputs are validated through FormRequest classes. +- Confirm all reads and writes are authorized. +- Move business rules out of controllers if any remain. +- Add feature tests for list/show/create/update/delete and key failure paths. + +### Root + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 0 controllers, 8 services, 1 requests, 1 resources. + +**Services:** FeeCalculationService, SemesterRangeService, ApplicationUrlService, SchoolIdService, AssignmentService, AttendanceCommentTemplateService, PhoneFormatterService, EmailService +**Requests:** ApiFormRequest +**Resources:** TeacherSubmissionRowResource +**Plan:** +- Confirm all inputs are validated through FormRequest classes. +- Confirm all reads and writes are authorized. +- Move business rules out of controllers if any remain. +- Add feature tests for list/show/create/update/delete and key failure paths. + +### School + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 0 controllers, 4 services, 0 requests, 0 resources. + +**Services:** AccountEventService, SemesterSelectionService, EnrollmentEventService, PaymentEventService +**Plan:** +- Confirm config reads/writes are permissioned and validated. +- Add health/config smoke tests. +- Document public versus authenticated surfaces. +- Keep UI/style/static-page services separated from domain mutations. + +### SchoolIds + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 0 controllers, 2 services, 1 requests, 1 resources. + +**Services:** SchoolIdAssignmentService, SchoolIdGenerationService +**Requests:** SchoolIdAssignRequest +**Resources:** SchoolIdResource +**Plan:** +- Confirm config reads/writes are permissioned and validated. +- Add health/config smoke tests. +- Document public versus authenticated surfaces. +- Keep UI/style/static-page services separated from domain mutations. + +### Scores + +**Priority:** P1 Student lifecycle +**Detected surface:** 9 controllers, 16 services, 10 requests, 3 resources. + +**Controllers:** FinalController, HomeworkController, MidtermController, ParticipationController, ProjectController, QuizController, ScoreCommentController, ScoreController, ScorePredictorController +**Services:** ParticipationScoreService, QuizCalculator, QuizScoreService, ScoreCommentService, ScoreDashboardService, HomeworkCalculator, SemesterScoreService, ScorePredictorRiskService, ProjectScoreService, HomeworkScoreService, ScoreTermService, ProjectCalculator ... +**Requests:** ScoreCommentSaveRequest, ScorePredictorRequest, ScoreLockRequest, ScoreUpdateRequest, ScoreOverviewRequest, ScoreStudentRequest, ScoreCommentListRequest, ScoreClassRequest ... +**Resources:** ScorePredictorStudentResource, ScoreCommentResource, ScoreStudentResource +**Plan:** +- Centralize score formulas and missing-score behavior. +- Add golden test cases for semester score, quiz, homework, project, participation, final/midterm, and prediction risk calculations. +- Prove grading locks block writes but not allowed reads. +- Verify below-60 communication and parent visibility rules. + +### Security + +**Priority:** P0 Security/Foundation +**Detected surface:** 0 controllers, 3 services, 3 requests, 2 resources. + +**Services:** IpBanCommandService, IpBanQueryService, Pbkdf2Hasher +**Requests:** IpBanIndexRequest, IpUnbanRequest, IpBanRequest +**Resources:** IpBanCollection, IpBanResource +**Plan:** +- Map every endpoint to required auth state, role, and permission. +- Test login throttling, IP bans, password reset cleanup, session timeout, role switching, and registration captcha. +- Confirm security logs include actor, IP, user agent, outcome, and reason. +- Reject role changes that create privilege escalation or stale-session leaks. + +### Semesters + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 0 controllers, 1 services, 4 requests, 2 resources. + +**Services:** SemesterConfigService +**Requests:** SchoolYearRangeRequest, SemesterResolveRequest, SemesterNormalizeRequest, SemesterRangeRequest +**Resources:** SemesterResolveResource, SemesterRangeResource +**Plan:** +- Confirm config reads/writes are permissioned and validated. +- Add health/config smoke tests. +- Document public versus authenticated surfaces. +- Keep UI/style/static-page services separated from domain mutations. + +### Settings + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 6 controllers, 8 services, 8 requests, 5 resources. + +**Controllers:** ConfigurationAdminController, EventController, PolicyController, PreferencesController, SchoolCalendarController, SettingsController +**Services:** SettingsService, ConfigurationService, SchoolCalendarMutationService, SchoolCalendarNotificationService, SchoolCalendarContextService, SchoolCalendarQueryService, SchoolCalendarFormatterService, SchoolCalendarMeetingService +**Requests:** SchoolCalendarStoreRequest, ConfigurationStoreRequest, SettingsUpdateRequest, SchoolCalendarIndexRequest, ConfigurationUpdateRequest, SchoolCalendarUpdateRequest, PolicyShowRequest, SchoolCalendarOptionsRequest +**Resources:** PolicyResource, SchoolCalendarEventResource, SettingsResource, ConfigurationResource, SchoolCalendarEventDetailResource +**Plan:** +- Confirm config reads/writes are permissioned and validated. +- Add health/config smoke tests. +- Document public versus authenticated surfaces. +- Keep UI/style/static-page services separated from domain mutations. + +### Staff + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 3 controllers, 3 services, 3 requests, 2 resources. + +**Controllers:** StaffController, TeacherController, TimeOffNotificationController +**Services:** StaffQueryService, StaffTimeOffLinkService, StaffCommandService +**Requests:** StaffIndexRequest, StaffUpdateRequest, StaffStoreRequest +**Resources:** StaffCollection, StaffResource +**Plan:** +- Centralize identity lookup and cross-entity authorization. +- Test directory/search pagination and visibility filtering. +- Ensure updates cannot orphan related records or break family/student/staff links. +- Add lifecycle tests for active, inactive, newly enrolled, transferred, and deleted/unverified users. + +### Students + +**Priority:** P1 Student lifecycle +**Detected surface:** 1 controllers, 7 services, 7 requests, 4 resources. + +**Controllers:** StudentController +**Services:** StudentScoreCardService, StudentStatusService, StudentDirectoryService, StudentAutoDistributionService, StudentConfigService, StudentProfileService, StudentAssignmentService +**Requests:** StudentRemoveClassRequest, StudentUpdateRequest, StudentAssignClassRequest, StudentSetActiveRequest, StudentPromotionTotalsRequest, StudentAssignmentListRequest, StudentAutoDistributeRequest +**Resources:** StudentAssignmentResource, StudentRemovedResource, StudentClassSectionResource, StudentScoreCardRowResource +**Plan:** +- Centralize identity lookup and cross-entity authorization. +- Replace any `max(id) + 1` style school ID generation with persisted-ID or database-sequence-based generation. +- Add a unique database constraint on `students.school_id` and retry logic for rare collisions. +- Test directory/search pagination and visibility filtering. +- Ensure updates cannot orphan related records or break family/student/staff links. +- Add lifecycle tests for active, inactive, newly enrolled, transferred, and deleted/unverified users. + +#### Student creation acceptance criteria + +- Student creation runs in a transaction. +- School ID is generated only after the student has a persisted ID or from a dedicated sequence. +- Concurrent student creation cannot create duplicate school IDs. +- Parent/family/student visibility tests prove users cannot create, view, or mutate records outside their boundary. + +### Subjects + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 1 controllers, 1 services, 2 requests, 2 resources. + +**Controllers:** SubjectCurriculumController +**Services:** SubjectCurriculumService +**Requests:** SubjectCurriculumUpdateRequest, SubjectCurriculumStoreRequest +**Resources:** SubjectCurriculumResource, SubjectClassResource +**Plan:** +- Define the academic workflow state machine and valid transitions. +- Test teacher/admin/parent role boundaries. +- Protect uploads and generated academic files. +- Add audit logs for status changes, submissions, and deadline changes. + +### Support + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 2 controllers, 2 services, 3 requests, 3 resources. + +**Controllers:** ContactController, SupportController +**Services:** SupportRequestService, ContactMessageService +**Requests:** SupportRequestStoreRequest, SupportRequestIndexRequest, ContactSendRequest +**Resources:** ContactMessageResource, SupportRequestResource, SupportRequestCollection +**Plan:** +- Confirm all inputs are validated through FormRequest classes. +- Confirm all reads and writes are authorized. +- Move business rules out of controllers if any remain. +- Add feature tests for list/show/create/update/delete and key failure paths. + +### System + +**Priority:** P0 Security/Foundation +**Detected surface:** 9 controllers, 6 services, 2 requests, 4 resources. + +**Controllers:** AccessDeniedController, DashboardController, DashboardRedirectController, DatabaseHealthController, HealthController, NavBuilderController, SchoolIdController, SemesterRangeController, StatsController +**Services:** HealthCheckService, CleanupSchedulerService, GlobalConfigService, DatabaseHealthService, ConfigUpdateService, TimeService +**Requests:** NavItemStoreRequest, NavItemReorderRequest +**Resources:** DashboardRouteResource, NavItemResource, HealthStatusResource, NavBuilderDataResource +**Plan:** +- Confirm config reads/writes are permissioned and validated. +- Add health/config smoke tests. +- Document public versus authenticated surfaces. +- Keep UI/style/static-page services separated from domain mutations. + +### Teachers + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 0 controllers, 4 services, 6 requests, 4 resources. + +**Services:** TeacherAbsenceService, TeacherDashboardService, TeacherConfigService, TeacherAssignmentService +**Requests:** TeacherClassViewRequest, TeacherAbsenceSubmitRequest, TeacherAssignmentDeleteRequest, TeacherAssignmentStoreRequest, TeacherAssignmentListRequest, TeacherSwitchClassRequest +**Resources:** TeacherAbsenceResource, TeacherStudentResource, TeacherClassResource, TeacherAssignmentResource +**Plan:** +- Centralize identity lookup and cross-entity authorization. +- Test directory/search pagination and visibility filtering. +- Ensure updates cannot orphan related records or break family/student/staff links. +- Add lifecycle tests for active, inactive, newly enrolled, transferred, and deleted/unverified users. + +### Ui + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 1 controllers, 1 services, 1 requests, 1 resources. + +**Controllers:** UiController +**Services:** UiStyleService +**Requests:** UiStyleRequest +**Resources:** UiStyleResource +**Plan:** +- Confirm config reads/writes are permissioned and validated. +- Add health/config smoke tests. +- Document public versus authenticated surfaces. +- Keep UI/style/static-page services separated from domain mutations. + +### Users + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 1 controllers, 6 services, 4 requests, 2 resources. + +**Controllers:** UserController +**Services:** UserListService, LoginActivityService, InactiveUserCleanupService, UserManagementService, UserEventService, DeleteUnverifiedUserService +**Requests:** UserStoreRequest, UserListRequest, UserUpdateRequest, LoginActivityRequest +**Resources:** LoginActivityResource, UserWithRolesResource +**Plan:** +- Confirm all inputs are validated through FormRequest classes. +- Confirm all reads and writes are authorized. +- Move business rules out of controllers if any remain. +- Add feature tests for list/show/create/update/delete and key failure paths. + +### Utilities + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 2 controllers, 0 services, 1 requests, 1 resources. + +**Controllers:** PhoneFormatterController, ProofreadController +**Requests:** PhoneFormatRequest +**Resources:** PhoneFormatResource +**Plan:** +- Confirm config reads/writes are permissioned and validated. +- Add health/config smoke tests. +- Document public versus authenticated surfaces. +- Keep UI/style/static-page services separated from domain mutations. + +### Whatsapp + +**Priority:** P1 Communications +**Detected surface:** 0 controllers, 8 services, 7 requests, 5 resources. + +**Services:** WhatsappContactService, WhatsappInviteService, WhatsappLinkService, WhatsappContextService, WhatsappInviteNotificationService, WhatsappMembershipService, WhatsappInviteBundleService, WhatsappInviteEmailService +**Requests:** WhatsappLinkIndexRequest, WhatsappMembershipUpdateRequest, WhatsappLinkStoreRequest, WhatsappLinkUpdateRequest, WhatsappParentContactsRequest, WhatsappParentContactsByClassRequest, WhatsappInviteSendRequest +**Resources:** WhatsappParentContactResource, WhatsappGroupLinkResource, WhatsappClassSectionParentResource, WhatsappGroupLinkCollection, WhatsappClassSectionContactResource +**Plan:** +- Separate recipient selection, message composition, dispatch, and logging. +- Add preview/dry-run tests before bulk sending. +- Deduplicate sends and persist failure states. +- Verify opt-outs, guardian selection, role targeting, and retry behavior. + + +## 7. Cross-Cutting Test Matrix + +| Area | Minimum tests | Failure tests | Regression traps | +|---|---:|---:|---| +| Auth / Security | Login, refresh, logout, role switch, timeout | Invalid creds, banned IP, stale role, unauthorized role | Privilege escalation, session confusion | +| Finance / Payments | Invoice generation, manual payment, PayPal sync, refunds | Duplicate webhook, failed payment, partial refund | Balance mismatch, rounding drift | +| Attendance | Teacher submit, admin edit, parent report | Out-of-window edit, unauthorized class, duplicate submit | Semester boundary, timezone/date errors | +| Grading / Scores | Score calculations, locks, below-60 notification | Missing score, locked write, invalid class | Formula drift, notification spam | +| Students / Families | Profile, guardians, enrollments, authorized users | Cross-family read/write, inactive student | Orphan relationships | +| Communications | Preview, send, log, retry | Invalid recipient, opt-out, duplicate send | Bulk-send mistakes | +| Files / Reports | Upload, download, PDF generation | Bad MIME, oversize file, unauthorized file | Path traversal, stale report data | +| Inventory / Ops | Item movement, PO receive, print requests | Negative stock, duplicate receive | Count mismatch | + + +## 8. Refactor Rules + +- Do not start by renaming everything. That is not architecture; that is sweeping broken glass into a different corner. +- Start with tests around the highest-risk behavior, then refactor behind those tests. +- One module per pull request unless a shared contract demands otherwise. +- For each PR, include: risk summary, affected endpoints, permission impact, migration impact, rollback path, and test evidence. +- Any service over roughly 300 lines should be reviewed for split points, but line count alone is not a crime. Unclear responsibility is the crime. +- Any controller doing calculations, direct DB orchestration, or multi-step workflow should delegate to a service. +- Any duplicated request validation should be consolidated into shared rules or dedicated FormRequests. + +## 9. Immediate Next Work Queue + +Work in this order. Alphabetical cleanup is banned until P0 is closed, because alphabetical order has never once saved a ledger. + +1. Patch the plan into issue tickets for the seven P0 items in the Critical Implementation Review Queue. +2. Add route inventory from `routes/api.php` and `routes/web.php` once the full repo is available. +3. Produce an endpoint-permission matrix from routes + controllers + policies. +4. Write failing/covering tests for payment file access, manual payment edit transaction safety, student school ID concurrency, scanner idempotency, role mutation authorization, bulk communication recipient safety, and refund/payment/invoice auditing. +5. Implement finance/file authorization fixes before controller refactors. +6. Implement transaction/locking fixes for payments, invoices, refunds, and reimbursements. +7. Implement student school ID generation fix and database uniqueness constraint. +8. Extract scanner services and add duplicate-scan/idempotency behavior. +9. Add baseline feature tests for Auth, Finance/Payments, Attendance, Grading/Scores, Students/Families, and Communications. +10. Create factories and seed data for the core school domain. +11. Add OpenAPI generation to CI using the existing Docs/OpenAPI service surface. +12. Review raw SQL and direct request access matches before any large refactor. +13. Add idempotency and audit requirements to payment, invoice, refund, promotion, and messaging mutations. +14. Write runbooks for console commands and operational cleanup jobs. + +## 10. Definition of Done + +A module is not considered done until all of this is true: + +- All public endpoints have route docs and permission mapping. +- All inputs use FormRequests or clearly justified validation. +- All responses use Resources or documented response builders. +- All mutations are transactional where consistency matters. +- All side effects are logged and testable. +- All critical behavior has feature tests and at least one negative authorization test. +- All money/date/file/message behavior has edge-case tests. +- No endpoint relies on undocumented legacy behavior unless that behavior is explicitly preserved and tested. + +## Appendix A: Controller Groups + +- **Administrator:** 8 controllers. AdministratorAbsenceController, AdministratorDashboardController, AdministratorEnrollmentController, AdministratorNotificationController, AdministratorPromotionController, AdministratorTeacherSubmissionController, EmergencyContactController, TeacherClassAssignmentController +- **ApiRoot:** 7 controllers. BaseApiController, CiRequestAdapter, RolePermissionController, RoleSwitcherController, ScannerController, StatsController, UserController +- **Assignment:** 1 controllers. AssignmentApiController +- **Attendance:** 6 controllers. AdminAttendanceApiController, AttendanceCommentTemplateController, EarlyDismissalsController, LateSlipLogsController, StaffAttendanceApiController, TeacherAttendanceApiController +- **AttendanceTracking:** 1 controllers. AttendanceTrackingController +- **Auth:** 7 controllers. AuthController, AuthSessionController, IpBanController, RegisterController, RolePermissionController, RoleSwitcherController, SessionTimeoutController +- **BadgeScan:** 1 controllers. BadgeScanController +- **Badges:** 1 controllers. BadgeController +- **Certificates:** 1 controllers. CertificateController +- **ClassPreparation:** 1 controllers. ClassPreparationController +- **ClassProgress:** 1 controllers. ClassProgressController +- **Classes:** 1 controllers. ClassController +- **Communication:** 1 controllers. CommunicationController +- **CompetitionScores:** 1 controllers. CompetitionScoresController +- **Core:** 2 controllers. BaseApiController, CiRequestAdapter +- **Discounts:** 1 controllers. DiscountController +- **Documentation:** 2 controllers. ApiDocsAdminController, DocsCatalogController +- **Email:** 3 controllers. BroadcastEmailController, EmailController, EmailExtractorController +- **Exams:** 1 controllers. ExamDraftController +- **Expenses:** 1 controllers. ExpenseController +- **ExtraCharges:** 1 controllers. ExtraChargesController +- **Family:** 2 controllers. FamilyAdminController, FamilyController +- **Finance:** 14 controllers. ChargeController, FeeCalculationController, FinancialController, InvoiceController, PaymentController, PaymentEventChargesController, PaymentManualController, PaymentNotificationController, PaymentTransactionController, PaypalPaymentController, PaypalTransactionsController, PurchaseOrderController, RefundController, ReimbursementController +- **Frontend:** 4 controllers. FrontendController, InfoIconController, LandingPageController, PageController +- **Grading:** 2 controllers. GradingController, HomeworkTrackingController +- **Incidents:** 1 controllers. IncidentController +- **Inventory:** 5 controllers. InventoryCategoryController, InventoryController, InventoryMovementController, SupplierController, SupplyCategoryController +- **Messaging:** 2 controllers. MessagesController, WhatsappController +- **NonApi:** 3 controllers. Controller, DocsController, TeacherCalendarPageController +- **Notifications:** 1 controllers. NotificationController +- **Parents:** 5 controllers. AuthorizedUserInviteController, AuthorizedUsersController, ParentAttendanceReportController, ParentController, ParentPromotionController +- **PrintRequests:** 1 controllers. PrintRequestsController +- **Public:** 1 controllers. PublicWinnersController +- **Reports:** 4 controllers. FilesController, ReportCardsController, SlipPrinterController, StickersController +- **Scores:** 9 controllers. FinalController, HomeworkController, MidtermController, ParticipationController, ProjectController, QuizController, ScoreCommentController, ScoreController, ScorePredictorController +- **Settings:** 6 controllers. ConfigurationAdminController, EventController, PolicyController, PreferencesController, SchoolCalendarController, SettingsController +- **Staff:** 3 controllers. StaffController, TeacherController, TimeOffNotificationController +- **Students:** 1 controllers. StudentController +- **Subjects:** 1 controllers. SubjectCurriculumController +- **Support:** 2 controllers. ContactController, SupportController +- **System:** 9 controllers. AccessDeniedController, DashboardController, DashboardRedirectController, DatabaseHealthController, HealthController, NavBuilderController, SchoolIdController, SemesterRangeController, StatsController +- **Ui:** 1 controllers. UiController +- **Users:** 1 controllers. UserController +- **Utilities:** 2 controllers. PhoneFormatterController, ProofreadController + +## Appendix B: Service Groups + +- **Admin:** 2 services, 1,089 LOC. CompetitionWinnersDomain, CompetitionWinnersAdminService +- **Administrator:** 18 services, 2,196 LOC. AdministratorUserSearchService, AdministratorAbsenceService, AdministratorDashboardService, AdministratorMetricsService, AdministratorEnrollmentStatusService, AdministratorEnrollmentEventService, AdministratorNotificationService, AdminNotificationSubjectService, TeacherSubmissionSupportService, AdministratorSharedService, TeacherSubmissionNotificationService, TeacherSubmissionReportService, AdminNotificationUserService, AdministratorEnrollmentRefundService, AdminPrintRecipientService, AdministratorTeacherSubmissionService, AdministratorEnrollmentService, AdministratorEnrollmentQueryService +- **Assignment:** 5 services, 421 LOC. AssignmentSectionService, AssignmentMetaService, AssignmentService, AssignmentContextService, AssignmentDataLoaderService +- **Attendance:** 16 services, 3,241 LOC. SemesterRangeService, TeacherAttendanceSubmissionService, AttendanceAutoPublishJobService, AttendanceAutoPublishService, LateSlipLogQueryService, AttendanceConsequenceService, AttendanceRecordSyncService, AttendanceDailySummaryService, AttendanceService, StudentAttendanceWriterService, LateSlipLogCommandService, AttendanceSummaryRebuildService, StaffAttendanceService, AttendanceQueryService, AttendancePolicyService, AttendanceCommentService +- **AttendanceTracking:** 12 services, 2,979 LOC. AttendanceNotificationLogService, AttendanceViolationStudentResolverService, AttendanceNotificationWorkflowService, AttendanceTrackingService, ViolationRuleEngineService, AttendanceMailerService, AttendanceParentLookupService, AttendanceCommunicationSupportService, AttendanceEmailComposerService, AttendancePendingViolationService, AttendanceCaseQueryService, DefaultAttendanceMailerService +- **Auth:** 9 services, 870 LOC. RegistrationCaptchaService, PermissionCheckService, CodeIgniterApiRegistrationService, RegistrationService, ApiLoginSecurityService, AuthSessionService, UserRoleService, PasswordResetCleanupService, RegistrationFormatterService +- **BadgeScan:** 1 services, 108 LOC. BadgeScanService +- **Badges:** 7 services, 2,815 LOC. BadgeTextFormatter, BadgeStudentLookupService, BadgeFormDataService, BadgePdfService, BadgeUserLookupService, BadgeService, BadgePrintLogService +- **Billing:** 3 services, 751 LOC. BalanceCalculationService, BillingTotalsService, ChargeService +- **BroadcastEmail:** 5 services, 241 LOC. BroadcastEmailComposerService, BroadcastEmailRecipientService, BroadcastEmailDispatchService, BroadcastEmailImageService, BroadcastEmailSenderOptionsService +- **Certificates:** 1 services, 144 LOC. CertificatePdfService +- **ClassPrep:** 2 services, 205 LOC. ClassRosterService, StickerCountService +- **ClassPreparation:** 9 services, 653 LOC. ClassPreparationAdjustmentWriterService, ClassPreparationCalculatorService, ClassPreparationLogService, ClassPreparationAdjustmentService, ClassPreparationContextService, ClassPreparationService, ClassPreparationPrintService, ClassPreparationRosterService, ClassPreparationInventoryService +- **ClassProgress:** 5 services, 941 LOC. ClassProgressRuleService, ClassProgressAttachmentService, ClassProgressQueryService, ClassProgressMetaService, ClassProgressMutationService +- **ClassSections:** 4 services, 190 LOC. ClassSectionCommandService, ClassAttendanceService, ClassSectionSeedService, ClassSectionQueryService +- **Communication:** 6 services, 374 LOC. CommunicationFamilyService, CommunicationSendService, CommunicationTemplateService, CommunicationPreviewService, CommunicationStudentService, CommunicationLogService +- **CompetitionScores:** 4 services, 325 LOC. CompetitionScoresRosterService, CompetitionScoresSaveService, CompetitionScoresContextService, CompetitionScoresQueryService +- **Dashboard:** 1 services, 70 LOC. DashboardRouteService +- **Discounts:** 5 services, 792 LOC. DiscountParentService, DiscountApplyService, DiscountInvoiceService, DiscountVoucherService, DiscountContextService +- **Docs:** 3 services, 244 LOC. DocsCatalogService, ApiDocsService, OpenApiRouteExporter +- **Email:** 5 services, 395 LOC. EmailAttachmentService, EmailExtractorService, EmailProfileService, EmailDispatchService, EmailSenderOptionsService +- **EmergencyContacts:** 2 services, 142 LOC. EmergencyContactDirectoryService, EmergencyContactCrudService +- **Events:** 6 services, 496 LOC. EventChargeService, EventStudentChargeService, EventCategoryService, EventListService, EventChargeQueryService, EventManagementService +- **Exams:** 4 services, 532 LOC. ExamDraftTeacherService, ExamDraftAdminService, ExamDraftConfigService, ExamDraftFileService +- **Expenses:** 7 services, 231 LOC. ExpenseQueryService, ExpenseStaffService, ExpenseRetailorService, ExpenseReceiptService, ExpenseWriteService, ExpenseContextService, ExpenseStatusService +- **ExtraCharges:** 6 services, 475 LOC. ExtraChargesMetaService, ExtraChargesInvoiceService, ExtraChargesParentService, ExtraChargesChargeService, ExtraChargesContextService, ExtraChargesListService +- **Families:** 4 services, 827 LOC. FamilyNotificationService, FamilyFinanceService, FamilyMutationService, FamilyQueryService +- **Fees:** 6 services, 952 LOC. FeeConfigService, TuitionCalculationService, FeeRefundCalculatorService, FeeGradeService, FeeRefundDetailService, FeeStudentFeeService +- **Files:** 2 services, 159 LOC. FileServeService, ExamDraftDownloadNameService +- **Finance:** 8 services, 1,150 LOC. FinancialPdfReportService, FinancialSchoolYearService, FinancialPaymentService, FinancialReportService, FinancialSummaryService, FinancialChartService, FinancialDonationRecipientService, FinancialUnpaidParentsService +- **Frontend:** 9 services, 1,011 LOC. LandingPageTeacherSummaryService, LandingPageRoleService, LandingPageParentDashboardService, LandingPageService, StaticPageService, FrontendPageService, ContactSubmissionService, LandingPageContextService, ProfileIconService +- **Grading:** 9 services, 1,969 LOC. BelowSixtyEmailService, GradingLockService, GradingOverviewService, GradingPlacementService, HomeworkTrackingService, GradingScoreService, GradingBelowSixtyService, HomeworkTrackingCalendarService, GradingRefreshService +- **Incidents:** 4 services, 499 LOC. CurrentIncidentService, IncidentHistoryService, IncidentLookupService, IncidentAnalysisService +- **Inventory:** 8 services, 1,449 LOC. InventorySummaryService, InventoryCategoryService, InventoryItemService, InventoryContextService, SupplierService, InventoryMovementService, SupplyCategoryService, InventoryTeacherDistributionService +- **Invoices:** 8 services, 1,248 LOC. InvoiceGradeService, InvoiceConfigService, InvoicePaymentService, InvoiceTuitionService, InvoiceManagementService, InvoicePdfService, InvoiceService, InvoiceGenerationService +- **Messaging:** 3 services, 334 LOC. MessageRecipientService, MessageCommandService, MessageQueryService +- **Navigation:** 2 services, 273 LOC. NavBuilderService, NavbarService +- **Notifications:** 12 services, 451 LOC. NotificationDispatchService, NotificationRecipientService, NotificationShowService, NotificationManagementService, NotificationReadService, NotificationUserListService, NotificationCleanupService, UserNotificationDispatchService, NotificationTriggerService, NotificationSendService, NotificationActiveService, NotificationDeletedService +- **Parents:** 14 services, 2,306 LOC. ParentEventParticipationService, ParentRegistrationService, PrimaryParentUserResolver, ParentAttendanceReportCalendarService, ParentAttendanceService, AuthorizedUsersManagementService, ParentEnrollmentService, ParentInvoiceService, ParentProgressQueryService, ParentAttendanceReportService, ParentReportCardService, ParentConfigService, ParentProfileService, ParentEmergencyContactService +- **Payments:** 14 services, 2,096 LOC. PaymentNotificationDispatchService, PaymentMissedCheckService, PaymentManualService, PaymentLookupService, PaymentTestNotificationService, PaypalPaymentSyncService, PaymentBalanceService, PaypalPaymentService, PaymentTransactionService, PaymentEnrollmentEventService, PaymentNotificationService, PaymentPlanService, PaymentEventChargesService, PaypalTransactionsService +- **Phone:** 1 services, 23 LOC. PhoneFormatterService +- **Policy:** 1 services, 51 LOC. PolicyContentService +- **Preferences:** 3 services, 190 LOC. PreferencesCommandService, PreferencesQueryService, PreferencesOptionsService +- **PrintRequests:** 1 services, 558 LOC. PrintRequestsPortalService +- **Promotions:** 7 services, 1,740 LOC. PromotionStatusService, PromotionEnrollmentService, PromotionReminderService, LevelProgressionService, PromotionAuditService, PromotionQueryService, PromotionEligibilityService +- **PublicWinners:** 1 services, 115 LOC. PublicWinnersPortalService +- **PurchaseOrders:** 4 services, 252 LOC. PurchaseOrderStatusService, PurchaseOrderReceiveService, PurchaseOrderCreateService, PurchaseOrderQueryService +- **Refunds:** 9 services, 1,210 LOC. RefundSummaryService, RefundInvoiceAdjustmentService, RefundPolicyService, RefundPayoutService, RefundRequestService, RefundQueryService, RefundOverpaymentService, RefundDecisionService, RefundNotificationService +- **Reimbursements:** 11 services, 1,776 LOC. ReimbursementFileService, ReimbursementBatchAssignmentService, ReimbursementContextService, ReimbursementExportService, ReimbursementLookupService, ReimbursementEmailService, ReimbursementRecipientService, ReimbursementBatchService, ReimbursementQueryService, ReimbursementDonationService, ReimbursementCrudService +- **Reports:** 9 services, 2,445 LOC. SlipPrinterFormatterService, SlipPrinterPdfService, SlipPrinterService, SlipPrinterConfigService, StickerPrintService, StickerPresetService, StickerQueryService, StickerContextService, ReportCardService +- **Roles:** 8 services, 545 LOC. RolePermissionService, RoleDashboardService, RoleCrudService, RoleAssignmentService, RoleSwitchService, RoleStaffService, PermissionCrudService, RoleQueryService +- **Root:** 8 services, 550 LOC. FeeCalculationService, SemesterRangeService, ApplicationUrlService, SchoolIdService, AssignmentService, AttendanceCommentTemplateService, PhoneFormatterService, EmailService +- **School:** 4 services, 990 LOC. AccountEventService, SemesterSelectionService, EnrollmentEventService, PaymentEventService +- **SchoolIds:** 2 services, 41 LOC. SchoolIdAssignmentService, SchoolIdGenerationService +- **Scores:** 16 services, 2,846 LOC. ParticipationScoreService, QuizCalculator, QuizScoreService, ScoreCommentService, ScoreDashboardService, HomeworkCalculator, SemesterScoreService, ScorePredictorRiskService, ProjectScoreService, HomeworkScoreService, ScoreTermService, ProjectCalculator, ScorePredictorDataService, ScorePredictorService, ExamScoreService, AttendanceCalculator +- **Security:** 3 services, 171 LOC. IpBanCommandService, IpBanQueryService, Pbkdf2Hasher +- **Semesters:** 1 services, 42 LOC. SemesterConfigService +- **Settings:** 8 services, 656 LOC. SettingsService, ConfigurationService, SchoolCalendarMutationService, SchoolCalendarNotificationService, SchoolCalendarContextService, SchoolCalendarQueryService, SchoolCalendarFormatterService, SchoolCalendarMeetingService +- **Staff:** 3 services, 325 LOC. StaffQueryService, StaffTimeOffLinkService, StaffCommandService +- **Students:** 7 services, 1,254 LOC. StudentScoreCardService, StudentStatusService, StudentDirectoryService, StudentAutoDistributionService, StudentConfigService, StudentProfileService, StudentAssignmentService +- **Subjects:** 1 services, 80 LOC. SubjectCurriculumService +- **Support:** 2 services, 123 LOC. SupportRequestService, ContactMessageService +- **System:** 6 services, 353 LOC. HealthCheckService, CleanupSchedulerService, GlobalConfigService, DatabaseHealthService, ConfigUpdateService, TimeService +- **Teachers:** 4 services, 641 LOC. TeacherAbsenceService, TeacherDashboardService, TeacherConfigService, TeacherAssignmentService +- **Ui:** 1 services, 95 LOC. UiStyleService +- **Users:** 6 services, 647 LOC. UserListService, LoginActivityService, InactiveUserCleanupService, UserManagementService, UserEventService, DeleteUnverifiedUserService +- **Whatsapp:** 8 services, 1,407 LOC. WhatsappContactService, WhatsappInviteService, WhatsappLinkService, WhatsappContextService, WhatsappInviteNotificationService, WhatsappMembershipService, WhatsappInviteBundleService, WhatsappInviteEmailService + +## Appendix C: Request Groups + +- **Admin:** 3 requests. StoreCompetitionWinnerRequest, UpdateCompetitionWinnerRequest, SaveCompetitionScoresRequest +- **Administrator:** 5 requests. SavePrintRecipientsRequest, SaveAdminNotificationSubjectsRequest, SubmitAdministratorAbsenceRequest, UpdateEnrollmentStatusesRequest, SendTeacherSubmissionNotificationsRequest +- **Api:** 4 requests. SaveAdminNotificationSubjectsRequest, IndexAdminProgressRequest, SendTeacherSubmissionNotificationsRequest, SavePrintNotificationRecipientsRequest +- **Assignment:** 2 requests. StoreAssignmentRequest, StoreStudentAssignmentRequest +- **Attendance:** 7 requests. SaveStaffAttendanceCellRequest, UpdateAttendanceManagementRequest, LateSlipLogIndexRequest, TeacherSubmitAttendanceRequest, SaveAdminAttendanceRequest, TeacherUpdateGridRequest, AdminAddAttendanceEntryRequest +- **AttendanceCommentTemplate:** 2 requests. StoreAttendanceCommentTemplateRequest, UpdateAttendanceCommentTemplateRequest +- **AttendanceTracking:** 5 requests. RecordAttendanceTrackingRequest, ComposeAttendanceEmailRequest, SendAttendanceManualEmailRequest, ParentsInfoRequest, SaveAttendanceNotificationNoteRequest +- **Auth:** 3 requests. RegisterRequest, SetRoleSessionRequest, LoginSessionRequest +- **Badges:** 3 requests. BadgePrintStatusRequest, GenerateBadgePdfRequest, LogBadgePrintRequest +- **ClassPreparation:** 4 requests. ClassPreparationMarkPrintedRequest, ClassPreparationPrintRequest, ClassPreparationIndexRequest, ClassPreparationAdjustmentRequest +- **ClassProgress:** 5 requests. ClassProgressStoreRequest, ClassProgressMetaRequest, ClassProgressIndexRequest, ClassProgressFormRequest, ClassProgressUpdateRequest +- **Classes:** 3 requests. ClassSectionUpdateRequest, ClassSectionStoreRequest, ClassSectionIndexRequest +- **Discounts:** 3 requests. UpdateDiscountVoucherRequest, ApplyDiscountVoucherRequest, StoreDiscountVoucherRequest +- **Email:** 2 requests. EmailExtractorCompareRequest, EmailSendRequest +- **EmergencyContacts:** 2 requests. EmergencyContactParentRequest, EmergencyContactUpdateRequest +- **Events:** 6 requests. EventIndexRequest, EventUpdateRequest, EventStoreRequest, EventStudentsWithChargesRequest, EventChargeUpdateRequest, EventChargeIndexRequest +- **Exams:** 3 requests. ExamDraftTeacherStoreRequest, ExamDraftAdminLegacyRequest, ExamDraftAdminReviewRequest +- **Expenses:** 3 requests. UpdateExpenseRequest, StoreExpenseRequest, UpdateExpenseStatusRequest +- **ExtraCharges:** 2 requests. UpdateExtraChargeRequest, StoreExtraChargeRequest +- **Families:** 14 requests. FamilyImportLegacyRequest, FamilyUnlinkStudentRequest, FamilySetPrimaryHomeRequest, FamilyAdminCardRequest, FamiliesByStudentRequest, FamilyBootstrapRequest, FamilyAttachSecondByEmailRequest, FamilyAdminSearchRequest, FamilySetGuardianFlagsRequest, FamilyAdminIndexRequest, FamilyComposeEmailRequest, FamilyUnlinkGuardianRequest, GuardiansByFamilyRequest, FamilyAttachSecondByUserRequest +- **Fees:** 2 requests. FeeTuitionTotalRequest, FeeRefundRequest +- **Files:** 1 requests. FileNameRequest +- **Finance:** 12 requests. FinancialUnpaidParentsRequest, FeeTuitionTotalRequest, ChargeListRequest, FinancialReportRequest, FinancialSummaryRequest, MarkEventChargePaidRequest, FeeTuitionBreakdownRequest, StoreExtraChargeRequest, StoreEventChargeRequest, FeeFamilyBalanceRequest, ApplyExtraChargeRequest, FeeRefundRequest +- **Frontend:** 2 requests. LandingTeacherRequest, ContactSubmitRequest +- **Grading:** 18 requests. BelowSixtyListRequest, BelowSixtySendRequest, GradingLockAllRequest, BelowSixtyMeetingRequest, PlacementLevelsRequest, GradingRefreshRequest, GradingScoreShowRequest, HomeworkTrackingRequest, GradingScoreUpdateRequest, PlacementRequest, PlacementBatchUpdateRequest, BelowSixtyEmailRequest, BelowSixtyStatusRequest, PlacementBatchRequest, GradingLockRequest, PlacementLevelRequest, BelowSixtyMeetingSaveRequest, GradingOverviewRequest +- **Incidents:** 5 requests. IncidentCancelRequest, IncidentCloseRequest, IncidentStoreRequest, IncidentListRequest, IncidentStateRequest +- **Inventory:** 19 requests. InventoryItemIndexRequest, SupplierUpdateRequest, InventoryCategoryStoreRequest, InventoryTeacherDistributionRequest, InventoryItemUpdateRequest, InventoryItemStoreRequest, InventoryTeacherDistributionStoreRequest, InventoryCategoryUpdateRequest, SupplyCategoryStoreRequest, InventoryCategoryIndexRequest, InventoryAdjustRequest, InventoryAuditRequest, SupplierIndexRequest, InventoryMovementIndexRequest, InventoryMovementBulkDeleteRequest, SupplierStoreRequest, InventoryMovementUpdateRequest, InventoryMovementStoreRequest ... +- **Invoices:** 4 requests. InvoiceStatusRequest, InvoiceManagementRequest, InvoiceParentRequest, InvoiceGenerateRequest +- **Messaging:** 3 requests. MessageSendRequest, MessageIndexRequest, MessageUpdateRequest +- **Notifications:** 5 requests. NotificationSendRequest, NotificationActiveRequest, NotificationDeletedRequest, NotificationUpdateRequest, NotificationIndexRequest +- **Parents:** 16 requests. ParentAttendanceReportUpdateRequest, ParentAttendanceReportListRequest, ParentAttendanceReportSubmitRequest, ParentEmergencyContactRequest, UpdateAuthorizedUserRequest, ParentAttendanceReportCheckRequest, ParentProfileUpdateRequest, ParentInvoiceRequest, ParentEnrollmentRequest, SignParentReportCardRequest, ParentAttendanceRequest, StoreAuthorizedUserRequest, ParentStudentUpdateRequest, ParentEventParticipationRequest, ParentEnrollmentActionRequest, ParentRegistrationRequest +- **Payments:** 15 requests. PaymentManualSearchRequest, PaymentEventChargesListRequest, PaymentNotificationSendRequest, PaymentNotificationListRequest, PaymentCreateRequest, PaypalTransactionsListRequest, PaymentManualEditRequest, PaymentEventChargesStoreRequest, PaymentUpdateBalanceRequest, PaymentManualUpdateRequest, PaymentManualSuggestRequest, PaypalExecuteRequest, PaymentByParentRequest, PaymentTransactionStatusRequest, PaymentTransactionCreateRequest +- **Preferences:** 2 requests. PreferencesIndexRequest, PreferencesUpsertRequest +- **Promotions:** 8 requests. SendReminderRequest, ParentPromotionOverviewRequest, AdminListPromotionsRequest, EvaluateEligibilityRequest, UpdatePromotionStatusRequest, ParentEnrollmentStepRequest, SetEnrollmentDeadlineRequest, UpsertLevelProgressionRequest +- **PurchaseOrders:** 3 requests. PurchaseOrderIndexRequest, PurchaseOrderStoreRequest, PurchaseOrderReceiveRequest +- **Refunds:** 6 requests. RefundStoreRequest, RefundPaymentRequest, RefundDecisionRequest, RefundRecalculateRequest, RefundIndexRequest, RefundRejectRequest +- **Reimbursements:** 13 requests. ReimbursementBatchAdminFileRequest, ReimbursementUpdateRequest, ReimbursementBatchCreateRequest, ReimbursementBatchEmailRequest, ReimbursementProcessRequest, ReimbursementExportBatchRequest, ReimbursementIndexRequest, ReimbursementExportRequest, ReimbursementMarkDonationRequest, ReimbursementBatchAssignmentRequest, ReimbursementStoreRequest, ReimbursementBatchLockRequest, ReimbursementUnderProcessingRequest +- **Reports:** 11 requests. SlipPreviewRequest, SlipLogListRequest, SlipPrintRequest, SlipReprintRequest, StickerStudentsByClassRequest, StickerPrintRequest, StickerFormDataRequest, ReportCardCompletenessRequest, ReportCardPdfRequest, ReportCardAcknowledgementRequest, ReportCardMetaRequest +- **Roles:** 9 requests. AssignUserRolesRequest, RolePermissionUpdateRequest, UserRoleListRequest, PermissionUpdateRequest, PermissionStoreRequest, RoleSwitchRequest, RoleStoreRequest, RoleUpdateRequest, RoleListRequest +- **Root:** 1 requests. ApiFormRequest +- **SchoolIds:** 1 requests. SchoolIdAssignRequest +- **Scores:** 10 requests. ScoreCommentSaveRequest, ScorePredictorRequest, ScoreLockRequest, ScoreUpdateRequest, ScoreOverviewRequest, ScoreStudentRequest, ScoreCommentListRequest, ScoreClassRequest, ScoreCommentUpdateRequest, ScoreAddColumnRequest +- **Security:** 3 requests. IpBanIndexRequest, IpUnbanRequest, IpBanRequest +- **Semesters:** 4 requests. SchoolYearRangeRequest, SemesterResolveRequest, SemesterNormalizeRequest, SemesterRangeRequest +- **Settings:** 8 requests. SchoolCalendarStoreRequest, ConfigurationStoreRequest, SettingsUpdateRequest, SchoolCalendarIndexRequest, ConfigurationUpdateRequest, SchoolCalendarUpdateRequest, PolicyShowRequest, SchoolCalendarOptionsRequest +- **Staff:** 3 requests. StaffIndexRequest, StaffUpdateRequest, StaffStoreRequest +- **Students:** 7 requests. StudentRemoveClassRequest, StudentUpdateRequest, StudentAssignClassRequest, StudentSetActiveRequest, StudentPromotionTotalsRequest, StudentAssignmentListRequest, StudentAutoDistributeRequest +- **Subjects:** 2 requests. SubjectCurriculumUpdateRequest, SubjectCurriculumStoreRequest +- **Support:** 3 requests. SupportRequestStoreRequest, SupportRequestIndexRequest, ContactSendRequest +- **System:** 2 requests. NavItemStoreRequest, NavItemReorderRequest +- **Teachers:** 6 requests. TeacherClassViewRequest, TeacherAbsenceSubmitRequest, TeacherAssignmentDeleteRequest, TeacherAssignmentStoreRequest, TeacherAssignmentListRequest, TeacherSwitchClassRequest +- **Ui:** 1 requests. UiStyleRequest +- **Users:** 4 requests. UserStoreRequest, UserListRequest, UserUpdateRequest, LoginActivityRequest +- **Utilities:** 1 requests. PhoneFormatRequest +- **Whatsapp:** 7 requests. WhatsappLinkIndexRequest, WhatsappMembershipUpdateRequest, WhatsappLinkStoreRequest, WhatsappLinkUpdateRequest, WhatsappParentContactsRequest, WhatsappParentContactsByClassRequest, WhatsappInviteSendRequest + +## Appendix D: Resource Groups + +- **Admin:** 5 resources. PrintRecipientsResource, TeacherSubmissionReportResource, AdminNotificationAlertsResource, ProgressGroupResource, ProgressReportDetailResource +- **Assignment:** 2 resources. AssignmentOverviewResource, AssignmentSectionResource +- **Attendance:** 7 resources. DailyAttendanceResource, AdminAttendanceResource, LateSlipLogCollection, LateSlipLogResource, TeacherGridResource, StaffMonthResource, TeacherAttendanceFormResource +- **Auth:** 1 resources. RegisterResource +- **ClassPreparation:** 2 resources. ClassPreparationPrintResource, ClassPreparationResultResource +- **ClassProgress:** 4 resources. ClassProgressGroupCollection, ClassProgressAttachmentResource, ClassProgressShowResource, ClassProgressReportResource +- **Classes:** 4 resources. ClassSectionCollection, ClassSectionResource, ClassAttendanceResource, ClassAttendanceStudentResource +- **Discounts:** 2 resources. DiscountVoucherResource, DiscountParentResource +- **Email:** 3 resources. EmailSenderResource, EmailExtractorEmailsResource, EmailExtractorCompareResource +- **EmergencyContacts:** 3 resources. EmergencyContactGroupResource, EmergencyContactStudentResource, EmergencyContactResource +- **Events:** 3 resources. EventChargeResource, EventResource, EventStudentChargeResource +- **Exams:** 1 resources. ExamDraftResource +- **Expenses:** 2 resources. ExpenseResource, ExpenseStaffResource +- **ExtraCharges:** 3 resources. ExtraChargeInvoiceOptionResource, ExtraChargeResource, ExtraChargeParentOptionResource +- **Families:** 7 resources. FamilyPaymentResource, FamilyInvoiceResource, FamilySearchItemResource, FamilyResource, FamilyGuardianResource, FamilyStudentResource, FamilyEmergencyContactResource +- **Fees:** 6 resources. FeeRefundResource, FeeFamilyBalanceResource, ChargeActionResource, ChargeListResource, FeeTuitionBreakdownResource, FeeTuitionTotalResource +- **Files:** 1 resources. FileMetaResource +- **Finance:** 9 resources. FinancialReportResource, FinancialSummaryResource, FinancialExpenseSummaryResource, FinancialInvoiceResource, FinancialPaymentResource, FinancialRefundResource, FinancialUnpaidParentResource, FinancialDiscountResource, FinancialReimbursementSummaryResource +- **Frontend:** 6 resources. LandingPageResource, ContactSubmissionResource, FrontendPageResource, FrontendUserResource, ProfileIconResource, PageContentResource +- **Grading:** 3 resources. BelowSixtyStudentResource, GradingScoreResource, HomeworkTrackingTeacherResource +- **Incidents:** 4 resources. IncidentStudentOptionResource, IncidentAnalysisStudentResource, IncidentResource, IncidentGradeResource +- **Inventory:** 5 resources. InventoryItemResource, InventoryCategoryResource, InventoryMovementResource, SupplierResource, SupplyCategoryResource +- **Invoices:** 2 resources. InvoiceManagementParentResource, InvoiceResource +- **Messaging:** 3 resources. MessageResource, MessageCollection, RecipientResource +- **Notifications:** 2 resources. UserNotificationResource, NotificationResource +- **Parents:** 4 resources. ParentEmergencyContactResource, ParentAttendanceReportResource, ParentStudentResource, ParentAttendanceResource +- **Payments:** 4 resources. PaymentNotificationLogResource, PaymentTransactionResource, PaypalTransactionResource, PaymentResource +- **Preferences:** 3 resources. PreferencesListResource, PreferencesCollection, PreferencesResource +- **Promotions:** 4 resources. LevelProgressionResource, PromotionAuditLogResource, PromotionReminderResource, StudentPromotionResource +- **PurchaseOrders:** 2 resources. PurchaseOrderResource, PurchaseOrderItemResource +- **Refunds:** 1 resources. RefundResource +- **Reimbursements:** 5 resources. ReimbursementRecipientResource, ReimbursementExpenseResource, ReimbursementBatchResource, ReimbursementResource, ReimbursementUnderProcessingItemResource +- **Reports:** 8 resources. LateSlipLogResource, StickerStudentResource, StickerPresetResource, StickerClassSectionResource, ReportCardStudentMetaResource, ReportCardAcknowledgementResource, ReportCardClassSectionResource, ReportCardCompletenessStudentResource +- **Roles:** 4 resources. RolePermissionResource, RoleResource, UserWithRolesResource, PermissionResource +- **Root:** 1 resources. TeacherSubmissionRowResource +- **SchoolIds:** 1 resources. SchoolIdResource +- **Scores:** 3 resources. ScorePredictorStudentResource, ScoreCommentResource, ScoreStudentResource +- **Security:** 2 resources. IpBanCollection, IpBanResource +- **Semesters:** 2 resources. SemesterResolveResource, SemesterRangeResource +- **Settings:** 5 resources. PolicyResource, SchoolCalendarEventResource, SettingsResource, ConfigurationResource, SchoolCalendarEventDetailResource +- **Staff:** 2 resources. StaffCollection, StaffResource +- **Students:** 4 resources. StudentAssignmentResource, StudentRemovedResource, StudentClassSectionResource, StudentScoreCardRowResource +- **Subjects:** 2 resources. SubjectCurriculumResource, SubjectClassResource +- **Support:** 3 resources. ContactMessageResource, SupportRequestResource, SupportRequestCollection +- **System:** 4 resources. DashboardRouteResource, NavItemResource, HealthStatusResource, NavBuilderDataResource +- **Teachers:** 4 resources. TeacherAbsenceResource, TeacherStudentResource, TeacherClassResource, TeacherAssignmentResource +- **Ui:** 1 resources. UiStyleResource +- **Users:** 2 resources. LoginActivityResource, UserWithRolesResource +- **Utilities:** 1 resources. PhoneFormatResource +- **Whatsapp:** 5 resources. WhatsappParentContactResource, WhatsappGroupLinkResource, WhatsappClassSectionParentResource, WhatsappGroupLinkCollection, WhatsappClassSectionContactResource + +## Appendix E: Implementation Fix Tickets + +These tickets are the plan’s mandatory implementation layer. Each one should become a tracked issue with tests before code changes begin. + +### Ticket P0-001: Payment file access hardening + +**Problem:** Filename-driven file serving can allow users to request files without proving access to the payment record. + +**Implementation:** + +- Change the endpoint contract from `{filename}` to `{payment}` or `{payment}/file`. +- Load the payment with invoice/family context. +- Authorize using a policy or gate before resolving file path. +- Resolve file path from the stored payment record only. +- Keep basename/path traversal protection as defense-in-depth, not as the primary access control. +- Log download attempts with actor, payment, result, and source IP where available. + +**Tests:** + +- finance/admin success; +- owning parent success if business rules allow it; +- unrelated parent forbidden; +- teacher/staff forbidden unless explicitly allowed; +- guessed filename forbidden; +- traversal filename rejected; +- missing file returns `404` without leaking storage path. + +### Ticket P0-002: Manual payment edit transaction safety + +**Problem:** Payment edit and invoice recalculation must not be split across unprotected operations. + +**Implementation:** + +- Wrap edit, invoice lookup, balance recompute, payment update, and audit insert in one transaction. +- Lock payment and invoice rows before recalculation. +- Recalculate after mutation inside the same transaction. +- Enforce no-overpayment and valid method/status transitions. +- Write before/after audit data. + +**Tests:** + +- successful edit updates invoice balance; +- rejected edit leaves payment and invoice unchanged; +- two concurrent edits do not corrupt balance; +- audit row contains actor and before/after values. + +### Ticket P0-003: Student school ID generation + +**Problem:** `max(id) + 1` style generation is race-prone. + +**Implementation:** + +- Create the student first, then derive school ID from the persisted ID, or use a database-backed sequence. +- Add a unique constraint to `students.school_id`. +- Save school ID inside the same transaction. +- Add retry behavior for collision. + +**Tests:** + +- normal create assigns expected school ID format; +- duplicate school ID violates database constraint; +- concurrent create does not produce duplicates; +- rollback does not leave orphaned related records. + +### Ticket P0-004: Scanner service extraction and idempotency + +**Problem:** Scanner logic is too large for a controller and mixes validation, lookup, mutation, late-slip behavior, logging, and response formatting. + +**Implementation:** + +- Add `ScannerRequest`. +- Create `ScannerService` as the orchestration entry point. +- Extract `BadgeLookupService`, `StudentScanService`, `StaffScanService`, `LateArrivalService`, `AttendanceScanLogger`, and `ScannerResponseFactory`. +- Put attendance mutations inside one transaction. +- Add duplicate-scan/idempotency protection. + +**Tests:** + +- valid student scan; +- valid staff scan; +- invalid badge; +- duplicate scan; +- late arrival; +- semester/date boundary; +- scan log written exactly once per accepted mutation. + +### Ticket P0-005: Role and permission mutation authorization + +**Problem:** Role/permission mutations have high blast radius and must not rely on casual controller checks. + +**Implementation:** + +- Define canonical role/permission controllers. +- Mark duplicate root/auth controllers as canonical, shim, deprecated, or accidental duplicate. +- Route only to canonical controllers. +- Use a policy/gate for every mutation. +- Log actor, target, old state, and new state. + +**Tests:** + +- super-admin/admin allowed according to policy; +- lower-role forbidden; +- user cannot mutate own privilege unless explicitly allowed; +- deprecated/shim routes cannot bypass policy. + +### Ticket P0-006: Bulk communication safety + +**Problem:** Bulk send behavior can accidentally spam, duplicate, or target the wrong recipients. + +**Implementation:** + +- Require preview/dry-run before send. +- Canonicalize and deduplicate recipients. +- Apply opt-outs/preferences. +- Log send attempts, failures, and retries. +- Separate message composition from delivery. + +**Tests:** + +- preview count equals eligible send count; +- duplicate guardians collapse; +- opted-out recipients are excluded; +- failed sends are logged and retryable; +- wrong-role sender cannot bulk send. + +### Ticket P0-007: Refund/payment/invoice audit completeness + +**Problem:** Financial state must be reconstructable after the fact. “Trust me, it updated” is not an audit strategy, it is a confession. + +**Implementation:** + +- Standardize audit fields across payments, invoices, refunds, discounts, reimbursements, and external sync. +- Include actor, target, amount, old state, new state, source, and idempotency key. +- Ensure reports use the same canonical ledger/balance behavior as API reads. + +**Tests:** + +- each financial mutation creates audit data; +- reports reconcile with API totals; +- repeated external sync does not double-apply; +- failed mutation does not write misleading audit success. + +## Appendix E: Notable Review Queues + +### Raw SQL review queue + +- `app/Services/Finance/FinancialSummaryService.php`: 28 matches +- `app/Services/Grading/GradingOverviewService.php`: 11 matches +- `app/Services/Finance/FinancialReportService.php`: 10 matches +- `app/Models/User.php`: 7 matches +- `app/Models/AttendanceData.php`: 6 matches +- `app/Services/Refunds/RefundOverpaymentService.php`: 6 matches +- `app/Services/Reports/ReportCards/ReportCardService.php`: 6 matches +- `app/Services/AttendanceTracking/ViolationRuleEngineService.php`: 5 matches +- `app/Services/AttendanceTracking/AttendanceCaseQueryService.php`: 4 matches +- `app/Services/Grading/GradingBelowSixtyService.php`: 4 matches +- `app/Services/Grading/HomeworkTrackingService.php`: 4 matches +- `app/Models/Payment.php`: 3 matches +- `app/Services/Attendance/AttendanceQueryService.php`: 3 matches +- `app/Services/Inventory/InventoryItemService.php`: 3 matches +- `app/Services/Inventory/InventoryMovementService.php`: 3 matches +- `app/Services/Payments/PaymentMissedCheckService.php`: 3 matches +- `app/Services/Scores/ScoreDashboardService.php`: 3 matches +- `app/Services/Scores/ScorePredictorDataService.php`: 3 matches +- `app/Models/Invoice.php`: 2 matches +- `app/Models/Role.php`: 2 matches +- `app/Models/TeacherClass.php`: 2 matches +- `app/Services/Admin/CompetitionWinners/CompetitionWinnersAdminService.php`: 2 matches +- `app/Services/Attendance/AttendanceDailySummaryService.php`: 2 matches +- `app/Services/AttendanceTracking/AttendancePendingViolationService.php`: 2 matches +- `app/Services/Families/FamilyQueryService.php`: 2 matches +- `app/Services/Finance/FinancialPaymentService.php`: 2 matches +- `app/Services/Promotions/PromotionQueryService.php`: 2 matches +- `app/Services/Refunds/RefundPayoutService.php`: 2 matches +- `app/Services/Refunds/RefundQueryService.php`: 2 matches +- `app/Services/Staff/StaffQueryService.php`: 2 matches +- `app/Services/Whatsapp/WhatsappInviteBundleService.php`: 2 matches +- `app/Models/AdditionalCharge.php`: 1 matches +- `app/Models/AttendanceRecord.php`: 1 matches +- `app/Models/AttendanceTracking.php`: 1 matches +- `app/Models/Project.php`: 1 matches +- `app/Models/RoleNavItem.php`: 1 matches +- `app/Models/StaffAttendance.php`: 1 matches +- `app/Models/StudentAllergy.php`: 1 matches +- `app/Models/StudentMedicalCondition.php`: 1 matches +- `app/Models/WhatsappGroupMembership.php`: 1 matches +- ... 19 more files + +### Direct request access review queue + +- `app/Http/Controllers/Api/AttendanceTracking/AttendanceTrackingController.php`: 16 matches +- `app/Http/Controllers/Api/Attendance/AttendanceCommentTemplateController.php`: 15 matches +- `app/Http/Controllers/Api/Communication/CommunicationController.php`: 12 matches +- `app/Http/Controllers/Api/Badges/BadgeController.php`: 9 matches +- `app/Http/Controllers/Api/Email/BroadcastEmailController.php`: 9 matches +- `app/Http/Controllers/Api/ExtraCharges/ExtraChargesController.php`: 9 matches +- `app/Http/Controllers/Api/Students/StudentController.php`: 9 matches +- `app/Http/Controllers/Api/Attendance/StaffAttendanceApiController.php`: 8 matches +- `app/Http/Controllers/Api/Attendance/EarlyDismissalsController.php`: 7 matches +- `app/Http/Controllers/Api/ClassPreparation/ClassPreparationController.php`: 7 matches +- `app/Http/Controllers/Api/Attendance/TeacherAttendanceApiController.php`: 5 matches +- `app/Http/Controllers/Api/Certificates/CertificateController.php`: 5 matches +- `app/Http/Controllers/Api/Reports/ReportCardsController.php`: 5 matches +- `app/Http/Controllers/Api/CompetitionScores/CompetitionScoresController.php`: 4 matches +- `app/Http/Controllers/Api/Finance/ReimbursementController.php`: 4 matches +- `app/Http/Controllers/Api/PrintRequests/PrintRequestsController.php`: 4 matches +- `app/Http/Controllers/Api/Administrator/AdministratorEnrollmentController.php`: 3 matches +- `app/Http/Controllers/Api/Assignment/AssignmentApiController.php`: 3 matches +- `app/Http/Controllers/Api/Finance/InvoiceController.php`: 3 matches +- `app/Http/Controllers/Api/Finance/PaymentManualController.php`: 3 matches +- `app/Http/Controllers/Api/Parents/AuthorizedUserInviteController.php`: 3 matches +- `app/Http/Controllers/Api/Scores/ScoreCommentController.php`: 3 matches +- `app/Http/Controllers/Api/Scores/ScoreController.php`: 3 matches +- `app/Services/Administrator/AdministratorAbsenceService.php`: 3 matches +- `app/Http/Controllers/Api/Administrator/AdministratorNotificationController.php`: 2 matches +- `app/Http/Controllers/Api/Attendance/AdminAttendanceApiController.php`: 2 matches +- `app/Http/Controllers/Api/Auth/AuthSessionController.php`: 2 matches +- `app/Http/Controllers/Api/Auth/RegisterController.php`: 2 matches +- `app/Http/Controllers/Api/Classes/ClassController.php`: 2 matches +- `app/Http/Controllers/Api/Finance/PaymentController.php`: 2 matches +- `app/Http/Controllers/Api/Finance/PaymentTransactionController.php`: 2 matches +- `app/Http/Controllers/Api/Finance/PurchaseOrderController.php`: 2 matches +- `app/Http/Controllers/Api/Incidents/IncidentController.php`: 2 matches +- `app/Http/Controllers/Api/Scores/FinalController.php`: 2 matches +- `app/Http/Controllers/Api/Scores/HomeworkController.php`: 2 matches +- `app/Http/Controllers/Api/Scores/MidtermController.php`: 2 matches +- `app/Http/Controllers/Api/Scores/ParticipationController.php`: 2 matches +- `app/Http/Controllers/Api/Scores/ProjectController.php`: 2 matches +- `app/Http/Controllers/Api/Scores/QuizController.php`: 2 matches +- `app/Services/Administrator/TeacherSubmissionNotificationService.php`: 2 matches +- ... 10 more files + +### File operation review queue + +- `app/Services/Reimbursements/ReimbursementEmailService.php`: 5 matches +- `app/Http/Controllers/Api/Students/StudentController.php`: 3 matches +- `app/Services/Badges/BadgePdfService.php`: 3 matches +- `app/ThirdParty/fpdf/fpdf.php`: 3 matches +- `app/Http/Controllers/Api/ClassProgress/ClassProgressController.php`: 2 matches +- `app/Http/Controllers/Api/Finance/ReimbursementController.php`: 2 matches +- `app/Services/Payments/PaymentManualService.php`: 2 matches +- `app/Services/PrintRequests/PrintRequestsPortalService.php`: 2 matches +- `app/Services/System/ConfigUpdateService.php`: 2 matches +- `app/Services/Whatsapp/WhatsappInviteEmailService.php`: 2 matches +- `app/Http/Controllers/Api/Attendance/EarlyDismissalsController.php`: 1 matches +- `app/Http/Controllers/Api/Finance/FinancialController.php`: 1 matches +- `app/Http/Controllers/Api/Finance/PaypalTransactionsController.php`: 1 matches +- `app/Http/Controllers/Api/PrintRequests/PrintRequestsController.php`: 1 matches +- `app/Services/Attendance/StaffAttendanceService.php`: 1 matches +- `app/Services/Finance/FinancialChartService.php`: 1 matches +- `app/Services/Parents/ParentAttendanceReportService.php`: 1 matches +- `app/Services/Reimbursements/ReimbursementFileService.php`: 1 matches + +### Queue/dispatch review queue + +- `app/Services/Promotions/PromotionReminderService.php`: 2 matches +- `app/Console/Commands/DeleteUnverifiedUsersCommand.php`: 1 matches +- `app/Services/Administrator/AdministratorEnrollmentEventService.php`: 1 matches +- `app/Services/Grading/GradingBelowSixtyService.php`: 1 matches +- `app/Services/Notifications/NotificationTriggerService.php`: 1 matches +- `app/Services/Whatsapp/WhatsappInviteNotificationService.php`: 1 matches +- `app/Services/Whatsapp/WhatsappInviteService.php`: 1 matches + +### Legacy compatibility review queue + +- `app/Services/Parents/ParentAttendanceReportService.php`: 4 matches +- `app/Config/Database.php`: 3 matches +- `app/Helpers/ci_helpers.php`: 3 matches +- `app/Http/Controllers/Api/Attendance/AttendanceCommentTemplateController.php`: 3 matches +- `app/Http/Controllers/Api/Auth/RegisterController.php`: 3 matches +- `app/Http/Controllers/Api/BaseApiController.php`: 3 matches +- `app/Http/Controllers/Api/Core/BaseApiController.php`: 2 matches +- `app/Services/Admin/CompetitionWinners/CompetitionWinnersAdminService.php`: 2 matches +- `app/Services/AttendanceTracking/AttendanceTrackingService.php`: 2 matches +- `app/Services/Auth/ApiLoginSecurityService.php`: 2 matches +- `app/Services/Auth/CodeIgniterApiRegistrationService.php`: 2 matches +- `app/Services/Discounts/DiscountApplyService.php`: 2 matches +- `app/Services/ExtraCharges/ExtraChargesChargeService.php`: 2 matches +- `app/Services/Payments/PaymentManualService.php`: 2 matches +- `app/Http/Controllers/Api/Auth/AuthController.php`: 1 matches +- `app/Http/Controllers/Api/Auth/AuthSessionController.php`: 1 matches +- `app/Http/Controllers/Api/CiRequestAdapter.php`: 1 matches +- `app/Http/Controllers/Api/Core/CiRequestAdapter.php`: 1 matches +- `app/Http/Controllers/Api/System/HealthController.php`: 1 matches +- `app/Http/Controllers/Api/Utilities/ProofreadController.php`: 1 matches +- `app/Http/Middleware/EnsureClassProgressTeacherPortalAccess.php`: 1 matches +- `app/Http/Middleware/EnsureParentProgressAccess.php`: 1 matches +- `app/Http/Middleware/EnsurePrintRequestsAdminAccess.php`: 1 matches +- `app/Http/Middleware/EnsurePrintRequestsTeacherAccess.php`: 1 matches +- `app/Http/Middleware/TeacherPortalAuthenticate.php`: 1 matches +- `app/Http/Requests/Admin/SaveCompetitionScoresRequest.php`: 1 matches +- `app/Http/Requests/Admin/StoreCompetitionWinnerRequest.php`: 1 matches +- `app/Http/Requests/Admin/UpdateCompetitionWinnerRequest.php`: 1 matches +- `app/Models/CiDatabase.php`: 1 matches +- `app/Models/User.php`: 1 matches +- `app/Services/Admin/CompetitionWinners/CompetitionWinnersDomain.php`: 1 matches +- `app/Services/Attendance/StaffAttendanceService.php`: 1 matches +- `app/Services/Auth/AuthSessionService.php`: 1 matches +- `app/Services/BadgeScan/BadgeScanService.php`: 1 matches +- `app/Services/Docs/DocsCatalogService.php`: 1 matches +- `app/Services/Parents/AuthorizedUsersManagementService.php`: 1 matches +- `app/Services/Parents/ParentProgressQueryService.php`: 1 matches +- `app/Services/Parents/ParentReportCardService.php`: 1 matches +- `app/Services/Parents/PrimaryParentUserResolver.php`: 1 matches +- `app/Services/PrintRequests/PrintRequestsPortalService.php`: 1 matches +- ... 1 more files diff --git a/docs/controller_plan.md b/docs/controller_plan.md new file mode 100644 index 00000000..bc1231fd --- /dev/null +++ b/docs/controller_plan.md @@ -0,0 +1,2093 @@ +# API Controller Plan + +Generated from the uploaded `Api(1).zip` controller tree. This is a controller-by-controller implementation and hardening plan, not a routing map, because no route file was included. Yes, apparently we are deriving architecture from controllers now, because civilization is fragile. + +- Controller files found: **124** +- Scope: `Api/**/*.php` files ending in `Controller.php` +- Default standard for every controller: validate with FormRequest or explicit validator, keep business logic in services, return consistent JSON envelopes, enforce auth/permissions, log failures without PII, and cover endpoints with feature tests. + +## Global cleanup plan + +1. **Normalize namespaces and duplicates.** There are root controllers and module controllers with overlapping names, especially `BaseApiController`, `RolePermissionController`, `RoleSwitcherController`, `StatsController`, and `UserController`. Decide which are canonical and deprecate the rest. +2. **Make controllers thin.** Anything doing calculations, scans, finance state changes, reporting, or multi-table writes belongs in a service/action class. +3. **Standardize response shape.** Use one success/error format, predictable status codes, and resource classes for payloads. +4. **Permission matrix.** For each endpoint, define roles allowed, ownership rules, and school/tenant scoping. Write denied-path tests first, because hope is not access control. +5. **Audit mutating operations.** Store actor, before/after, timestamp, and reason for attendance, finance, grades, family/student/staff changes. +6. **Test by risk.** Finance, auth, attendance, grading, and mass communication get transaction/idempotency/authorization tests before cosmetic endpoints. + +## Table of contents + +- [Administrator](#administrator) (8) +- [Assignment](#assignment) (1) +- [Attendance](#attendance) (6) +- [AttendanceTracking](#attendancetracking) (1) +- [Auth](#auth) (7) +- [BadgeScan](#badgescan) (1) +- [Badges](#badges) (1) +- [Certificates](#certificates) (1) +- [ClassPreparation](#classpreparation) (1) +- [ClassProgress](#classprogress) (1) +- [Classes](#classes) (1) +- [Communication](#communication) (1) +- [CompetitionScores](#competitionscores) (1) +- [Core](#core) (1) +- [Discounts](#discounts) (1) +- [Documentation](#documentation) (2) +- [Email](#email) (3) +- [Exams](#exams) (1) +- [Expenses](#expenses) (1) +- [ExtraCharges](#extracharges) (1) +- [Family](#family) (2) +- [Finance](#finance) (14) +- [Frontend](#frontend) (4) +- [Grading](#grading) (2) +- [Incidents](#incidents) (1) +- [Inventory](#inventory) (5) +- [Messaging](#messaging) (2) +- [Notifications](#notifications) (1) +- [Parents](#parents) (5) +- [PrintRequests](#printrequests) (1) +- [Public](#public) (1) +- [Reports](#reports) (4) +- [Root](#root) (6) +- [Scores](#scores) (9) +- [Settings](#settings) (6) +- [Staff](#staff) (3) +- [Students](#students) (1) +- [Subjects](#subjects) (1) +- [Support](#support) (2) +- [System](#system) (9) +- [Ui](#ui) (1) +- [Users](#users) (1) +- [Utilities](#utilities) (2) + +## Administrator + +### AdministratorAbsenceController +**File:** `Api/Administrator/AdministratorAbsenceController.php` +**Purpose:** manage school identity and operations workflow for administrator absence. +**Public methods:** `index()`, `store(Request $request)` +**Detected dependencies:** Services: `AdministratorAbsenceService` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Add dedicated FormRequest classes where this still uses raw `Request`; inline validation is a trap wearing a tiny fake mustache. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Authorization boundaries are the product, not decoration. Test forbidden paths first. +- Avoid leaking whether accounts, roles, or sessions exist unless intended. +- Attendance changes need actor, timestamp, school term, and edit history. +- Define conflict behavior for duplicate scans or same-day edits. +- Feature tests: list/filter pagination, empty result, invalid filter, tenant scoping; create/submit validation, happy path, duplicate submission. + +### AdministratorDashboardController +**File:** `Api/Administrator/AdministratorDashboardController.php` +**Purpose:** manage school identity and operations workflow for administrator dashboard. +**Public methods:** `metrics()`, `userSearch(Request $request)` +**Detected dependencies:** Services: `AdministratorDashboardService` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Add dedicated FormRequest classes where this still uses raw `Request`; inline validation is a trap wearing a tiny fake mustache. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Feature tests: happy path, unauthorized path, invalid input, service exception. + +### AdministratorEnrollmentController +**File:** `Api/Administrator/AdministratorEnrollmentController.php` +**Purpose:** manage school identity and operations workflow for administrator enrollment. +**Public methods:** `index(Request $request)`, `newStudents()`, `updateStatuses(Request $request)` +**Detected dependencies:** Services: `AdministratorEnrollmentService` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Add dedicated FormRequest classes where this still uses raw `Request`; inline validation is a trap wearing a tiny fake mustache. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Money operations must be idempotent, auditable, and transaction-safe. +- Never trust client-calculated totals; recompute server-side. +- Authorization boundaries are the product, not decoration. Test forbidden paths first. +- Avoid leaking whether accounts, roles, or sessions exist unless intended. +- Feature tests: list/filter pagination, empty result, invalid filter, tenant scoping. + +### AdministratorNotificationController +**File:** `Api/Administrator/AdministratorNotificationController.php` +**Purpose:** manage school identity and operations workflow for administrator notification. +**Public methods:** `alerts()`, `saveAlerts(Request $request)`, `printRecipients()`, `savePrintRecipients(Request $request)` +**Detected dependencies:** Services: `AdministratorNotificationService` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Add dedicated FormRequest classes where this still uses raw `Request`; inline validation is a trap wearing a tiny fake mustache. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Rate-limit sends and record delivery state; retries must not spam families into oblivion. +- Sanitize templates and recipient selection. +- Validate file type, size, storage path, and access ownership. +- Generated documents need reproducible inputs and predictable naming. +- Feature tests: happy path, unauthorized path, invalid input, service exception. + +### AdministratorPromotionController +**File:** `Api/Administrator/AdministratorPromotionController.php` +**Purpose:** manage school identity and operations workflow for administrator promotion. +**Public methods:** `index(AdminListPromotionsRequest $request)`, `show(int $promotionId)`, `summary(AdminListPromotionsRequest $request)`, `evaluate(EvaluateEligibilityRequest $request)`, `updateStatus(UpdatePromotionStatusRequest $request, int $promotionId)`, `setDeadline(SetEnrollmentDeadlineRequest $request, ?int $promotionId = null)`, `sendReminder(SendReminderRequest $request, int $promotionId)`, `dispatchScheduledReminders()`, `reminders(int $promotionId)`, `audit(int $promotionId)`, `expireDeadlines()`, `adminCompleteEnrollment(ParentEnrollmentStepRequest $request, int $promotionId)`, `levelProgressions()`, `upsertLevelProgression(UpsertLevelProgressionRequest $request)`, `seedLevelProgressions()` +**Detected dependencies:** Services: `LevelProgressionService`, `PromotionAuditService`, `PromotionEligibilityService`, `PromotionEnrollmentService`, `PromotionQueryService`, `PromotionReminderService`, `PromotionStatusService` | Requests: `AdminListPromotionsRequest`, `EvaluateEligibilityRequest`, `ParentEnrollmentStepRequest`, `SendReminderRequest`, `SetEnrollmentDeadlineRequest`, `UpdatePromotionStatusRequest`, `UpsertLevelProgressionRequest` | Resources: `LevelProgressionResource`, `PromotionAuditLogResource`, `PromotionReminderResource`, `StudentPromotionResource` | Models: `PromotionAuditLog`, `PromotionReminderLog`, `StudentPromotionRecord` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Mutating endpoints need request validation, service-layer ownership, and database transactions where multiple rows change. +- This controller is large enough to hide bugs. Split orchestration from business logic before it becomes archaeology. +- Feature tests: list/filter pagination, empty result, invalid filter, tenant scoping; show success, missing record, unauthorized record. + +### AdministratorTeacherSubmissionController +**File:** `Api/Administrator/AdministratorTeacherSubmissionController.php` +**Purpose:** manage school identity and operations workflow for administrator teacher submission. +**Public methods:** `index()`, `notify(Request $request)` +**Detected dependencies:** Services: `AdministratorTeacherSubmissionService` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Add dedicated FormRequest classes where this still uses raw `Request`; inline validation is a trap wearing a tiny fake mustache. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Authorization boundaries are the product, not decoration. Test forbidden paths first. +- Avoid leaking whether accounts, roles, or sessions exist unless intended. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Feature tests: list/filter pagination, empty result, invalid filter, tenant scoping. + +### EmergencyContactController +**File:** `Api/Administrator/EmergencyContactController.php` +**Purpose:** manage school identity and operations workflow for emergency contact. +**Public methods:** `index(Request $request)`, `show(EmergencyContactParentRequest $request, int $contactId)`, `update(EmergencyContactUpdateRequest $request, int $contactId)`, `destroy(EmergencyContactParentRequest $request, int $contactId)` +**Detected dependencies:** Services: `EmergencyContactCrudService`, `EmergencyContactDirectoryService` | Requests: `EmergencyContactParentRequest`, `EmergencyContactUpdateRequest` | Resources: `EmergencyContactGroupResource`, `EmergencyContactResource` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Rate-limit sends and record delivery state; retries must not spam families into oblivion. +- Sanitize templates and recipient selection. +- Feature tests: list/filter pagination, empty result, invalid filter, tenant scoping; show success, missing record, unauthorized record; update authorization, partial payload, concurrency/transaction rollback; delete success, already deleted, protected record. + +### TeacherClassAssignmentController +**File:** `Api/Administrator/TeacherClassAssignmentController.php` +**Purpose:** manage school identity and operations workflow for teacher class assignment. +**Public methods:** `index(TeacherAssignmentListRequest $request)`, `store(TeacherAssignmentStoreRequest $request)`, `destroy(TeacherAssignmentDeleteRequest $request)` +**Detected dependencies:** Services: `TeacherAssignmentService` | Requests: `TeacherAssignmentDeleteRequest`, `TeacherAssignmentListRequest`, `TeacherAssignmentStoreRequest` | Resources: `TeacherAssignmentResource`, `TeacherClassResource` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Authorization boundaries are the product, not decoration. Test forbidden paths first. +- Avoid leaking whether accounts, roles, or sessions exist unless intended. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Feature tests: list/filter pagination, empty result, invalid filter, tenant scoping; create/submit validation, happy path, duplicate submission; delete success, already deleted, protected record. + +## Assignment + +### AssignmentApiController +**File:** `Api/Assignment/AssignmentApiController.php` +**Purpose:** serve assignment api API responsibilities. +**Public methods:** `index(Request $request)`, `store(Request $request)`, `classAssignmentData()` +**Detected dependencies:** Services: `AssignmentService` | Resources: `AssignmentOverviewResource`, `AssignmentSectionResource` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Add dedicated FormRequest classes where this still uses raw `Request`; inline validation is a trap wearing a tiny fake mustache. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Authorization boundaries are the product, not decoration. Test forbidden paths first. +- Avoid leaking whether accounts, roles, or sessions exist unless intended. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Feature tests: list/filter pagination, empty result, invalid filter, tenant scoping; create/submit validation, happy path, duplicate submission. + +## Attendance + +### AdminAttendanceApiController +**File:** `Api/Attendance/AdminAttendanceApiController.php` +**Purpose:** manage attendance workflow for admin attendance api. +**Public methods:** `dailyData(Request $request)`, `updateManagement(UpdateAttendanceManagementRequest $request)`, `addEntry(AdminAddAttendanceEntryRequest $request)` +**Detected dependencies:** Services: `AttendanceQueryService`, `AttendanceService` | Requests: `AdminAddAttendanceEntryRequest`, `UpdateAttendanceManagementRequest` | Resources: `DailyAttendanceResource` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Authorization boundaries are the product, not decoration. Test forbidden paths first. +- Avoid leaking whether accounts, roles, or sessions exist unless intended. +- Attendance changes need actor, timestamp, school term, and edit history. +- Define conflict behavior for duplicate scans or same-day edits. +- Feature tests: list/filter pagination, empty result, invalid filter, tenant scoping; update authorization, partial payload, concurrency/transaction rollback. + +### AttendanceCommentTemplateController +**File:** `Api/Attendance/AttendanceCommentTemplateController.php` +**Purpose:** manage attendance workflow for attendance comment template. +**Public methods:** `bootstrapUrls()`, `index(Request $request)`, `listData(Request $request)`, `show(int $id)`, `store(Request $request)`, `update(Request $request, int $id)`, `destroy(int $id)`, `legacySave(Request $request)`, `legacyDelete(Request $request)` +**Detected dependencies:** Services: `ApplicationUrlService`, `AttendanceCommentTemplateService` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Add dedicated FormRequest classes where this still uses raw `Request`; inline validation is a trap wearing a tiny fake mustache. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Attendance changes need actor, timestamp, school term, and edit history. +- Define conflict behavior for duplicate scans or same-day edits. +- Mutating endpoints need request validation, service-layer ownership, and database transactions where multiple rows change. +- Feature tests: list/filter pagination, empty result, invalid filter, tenant scoping; show success, missing record, unauthorized record; create/submit validation, happy path, duplicate submission; update authorization, partial payload, concurrency/transaction rollback; delete success, already deleted, protected record. + +### EarlyDismissalsController +**File:** `Api/Attendance/EarlyDismissalsController.php` +**Purpose:** manage attendance workflow for early dismissals. +**Public methods:** `index(Request $request)`, `studentOptions()`, `store(Request $request)`, `uploadSignature(Request $request)` +**Detected dependencies:** Models: `Configuration`, `EarlyDismissalSignature` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Add dedicated FormRequest classes where this still uses raw `Request`; inline validation is a trap wearing a tiny fake mustache. +- Extract business logic into service/action classes if the controller currently queries models or mutates state directly. +- Authorization boundaries are the product, not decoration. Test forbidden paths first. +- Avoid leaking whether accounts, roles, or sessions exist unless intended. +- Attendance changes need actor, timestamp, school term, and edit history. +- Define conflict behavior for duplicate scans or same-day edits. +- Feature tests: list/filter pagination, empty result, invalid filter, tenant scoping; create/submit validation, happy path, duplicate submission. + +### LateSlipLogsController +**File:** `Api/Attendance/LateSlipLogsController.php` +**Purpose:** manage attendance workflow for late slip logs. +**Public methods:** `index(LateSlipLogIndexRequest $request)`, `show(int $id)`, `destroy(int $id)` +**Detected dependencies:** Services: `LateSlipLogCommandService`, `LateSlipLogQueryService` | Requests: `LateSlipLogIndexRequest` | Resources: `LateSlipLogCollection`, `LateSlipLogResource` | Models: `LateSlipLog` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Authorization boundaries are the product, not decoration. Test forbidden paths first. +- Avoid leaking whether accounts, roles, or sessions exist unless intended. +- Attendance changes need actor, timestamp, school term, and edit history. +- Define conflict behavior for duplicate scans or same-day edits. +- Feature tests: list/filter pagination, empty result, invalid filter, tenant scoping; show success, missing record, unauthorized record; delete success, already deleted, protected record. + +### StaffAttendanceApiController +**File:** `Api/Attendance/StaffAttendanceApiController.php` +**Purpose:** manage attendance workflow for staff attendance api. +**Public methods:** `monthData(Request $request)`, `admins(Request $request)`, `saveAdmins(SaveAdminAttendanceRequest $request)`, `saveCell(SaveStaffAttendanceCellRequest $request)`, `monthCsv(Request $request)` +**Detected dependencies:** Services: `StaffAttendanceService` | Requests: `SaveAdminAttendanceRequest`, `SaveStaffAttendanceCellRequest` | Resources: `AdminAttendanceResource`, `StaffMonthResource` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Authorization boundaries are the product, not decoration. Test forbidden paths first. +- Avoid leaking whether accounts, roles, or sessions exist unless intended. +- Attendance changes need actor, timestamp, school term, and edit history. +- Define conflict behavior for duplicate scans or same-day edits. +- Feature tests: list/filter pagination, empty result, invalid filter, tenant scoping; update authorization, partial payload, concurrency/transaction rollback. + +### TeacherAttendanceApiController +**File:** `Api/Attendance/TeacherAttendanceApiController.php` +**Purpose:** manage attendance workflow for teacher attendance api. +**Public methods:** `grid(Request $request)`, `form(Request $request)`, `submit(TeacherSubmitAttendanceRequest $request)` +**Detected dependencies:** Services: `AttendanceQueryService`, `AttendanceService` | Requests: `TeacherSubmitAttendanceRequest` | Resources: `TeacherAttendanceFormResource`, `TeacherGridResource` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Authorization boundaries are the product, not decoration. Test forbidden paths first. +- Avoid leaking whether accounts, roles, or sessions exist unless intended. +- Attendance changes need actor, timestamp, school term, and edit history. +- Define conflict behavior for duplicate scans or same-day edits. +- Feature tests: list/filter pagination, empty result, invalid filter, tenant scoping; create/submit validation, happy path, duplicate submission. + +## AttendanceTracking + +### AttendanceTrackingController +**File:** `Api/AttendanceTracking/AttendanceTrackingController.php` +**Purpose:** manage attendance workflow for attendance tracking. +**Public methods:** `pendingViolations(Request $request)`, `notifiedViolations(Request $request)`, `studentCase(int $studentId, Request $request)`, `record(Request $request)`, `sendAutoEmails(Request $request)`, `compose(Request $request)`, `sendManualEmail(Request $request)`, `parentsInfo(Request $request)`, `saveNotificationNote(Request $request)` +**Detected dependencies:** Services: `AttendanceTrackingService` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Add dedicated FormRequest classes where this still uses raw `Request`; inline validation is a trap wearing a tiny fake mustache. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Attendance changes need actor, timestamp, school term, and edit history. +- Define conflict behavior for duplicate scans or same-day edits. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Feature tests: happy path, unauthorized path, invalid input, service exception. + +## Auth + +### AuthController +**File:** `Api/Auth/AuthController.php` +**Purpose:** handle authentication/authorization workflow for auth. +**Public methods:** `login(Request $request, ApiLoginSecurityService $security)`, `refresh(Request $request)`, `me()`, `logout(Request $request)` +**Detected dependencies:** Services: `ApiLoginSecurityService` | Models: `User` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Add dedicated FormRequest classes where this still uses raw `Request`; inline validation is a trap wearing a tiny fake mustache. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Authorization boundaries are the product, not decoration. Test forbidden paths first. +- Avoid leaking whether accounts, roles, or sessions exist unless intended. +- Attendance changes need actor, timestamp, school term, and edit history. +- Define conflict behavior for duplicate scans or same-day edits. +- Feature tests: happy path, unauthorized path, invalid input, service exception; role matrix: allowed, denied, expired session. + +### AuthSessionController +**File:** `Api/Auth/AuthSessionController.php` +**Purpose:** handle authentication/authorization workflow for auth session. +**Public methods:** `loginMask(Request $request)`, `login(LoginSessionRequest $request)`, `logout(Request $request)`, `selectRole(Request $request)`, `setRole(SetRoleSessionRequest $request)` +**Detected dependencies:** Services: `ApplicationUrlService`, `ApiLoginSecurityService`, `AuthSessionService` | Requests: `LoginSessionRequest`, `SetRoleSessionRequest` | Models: `User` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Authorization boundaries are the product, not decoration. Test forbidden paths first. +- Avoid leaking whether accounts, roles, or sessions exist unless intended. +- This controller is large enough to hide bugs. Split orchestration from business logic before it becomes archaeology. +- Feature tests: happy path, unauthorized path, invalid input, service exception; role matrix: allowed, denied, expired session. + +### IpBanController +**File:** `Api/Auth/IpBanController.php` +**Purpose:** handle authentication/authorization workflow for ip ban. +**Public methods:** `index(IpBanIndexRequest $request)`, `show(int $id)`, `ban(IpBanRequest $request)`, `unban(IpUnbanRequest $request)` +**Detected dependencies:** Services: `IpBanCommandService`, `IpBanQueryService` | Requests: `IpBanIndexRequest`, `IpBanRequest`, `IpUnbanRequest` | Resources: `IpBanCollection`, `IpBanResource` | Models: `IpAttempt` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Authorization boundaries are the product, not decoration. Test forbidden paths first. +- Avoid leaking whether accounts, roles, or sessions exist unless intended. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Feature tests: list/filter pagination, empty result, invalid filter, tenant scoping; show success, missing record, unauthorized record; role matrix: allowed, denied, expired session. + +### RegisterController +**File:** `Api/Auth/RegisterController.php` +**Purpose:** handle authentication/authorization workflow for register. +**Public methods:** `captcha()`, `store(Request $request)` +**Detected dependencies:** Services: `CodeIgniterApiRegistrationService`, `RegistrationCaptchaService`, `RegistrationService` | Resources: `RegisterResource` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Add dedicated FormRequest classes where this still uses raw `Request`; inline validation is a trap wearing a tiny fake mustache. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Authorization boundaries are the product, not decoration. Test forbidden paths first. +- Avoid leaking whether accounts, roles, or sessions exist unless intended. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Feature tests: create/submit validation, happy path, duplicate submission; role matrix: allowed, denied, expired session. + +### RolePermissionController +**File:** `Api/Auth/RolePermissionController.php` +**Purpose:** handle authentication/authorization workflow for role permission. +**Public methods:** `roles(RoleListRequest $request)`, `showRole(int $roleId)`, `storeRole(RoleStoreRequest $request)`, `updateRole(RoleUpdateRequest $request, int $roleId)`, `deleteRole(int $roleId)`, `users(UserRoleListRequest $request)`, `assignRoles(AssignUserRolesRequest $request, int $userId)`, `permissions()`, `showPermission(int $permissionId)`, `storePermission(PermissionStoreRequest $request)`, `updatePermission(PermissionUpdateRequest $request, int $permissionId)`, `deletePermission(int $permissionId)`, `rolePermissions(int $roleId)`, `updateRolePermissions(RolePermissionUpdateRequest $request, int $roleId)` +**Detected dependencies:** Services: `PermissionCrudService`, `RoleAssignmentService`, `RoleCrudService`, `RolePermissionService`, `RoleQueryService` | Requests: `AssignUserRolesRequest`, `PermissionStoreRequest`, `PermissionUpdateRequest`, `RoleListRequest`, `RolePermissionUpdateRequest`, `RoleStoreRequest`, `RoleUpdateRequest`, `UserRoleListRequest` | Resources: `PermissionResource`, `RolePermissionResource`, `RoleResource`, `UserWithRolesResource` | Models: `Permission`, `Role` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Authorization boundaries are the product, not decoration. Test forbidden paths first. +- Avoid leaking whether accounts, roles, or sessions exist unless intended. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Feature tests: update authorization, partial payload, concurrency/transaction rollback; role matrix: allowed, denied, expired session. + +### RoleSwitcherController +**File:** `Api/Auth/RoleSwitcherController.php` +**Purpose:** handle authentication/authorization workflow for role switcher. +**Public methods:** `index()`, `switch(RoleSwitchRequest $request)` +**Detected dependencies:** Services: `RoleSwitchService` | Requests: `RoleSwitchRequest` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Authorization boundaries are the product, not decoration. Test forbidden paths first. +- Avoid leaking whether accounts, roles, or sessions exist unless intended. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Feature tests: list/filter pagination, empty result, invalid filter, tenant scoping; role matrix: allowed, denied, expired session. + +### SessionTimeoutController +**File:** `Api/Auth/SessionTimeoutController.php` +**Purpose:** handle authentication/authorization workflow for session timeout. +**Public methods:** `getTimeoutConfig()`, `checkTimeout(Request $request)`, `pingActivity(Request $request)` +**Detected dependencies:** Services: `ApplicationUrlService` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Add dedicated FormRequest classes where this still uses raw `Request`; inline validation is a trap wearing a tiny fake mustache. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Authorization boundaries are the product, not decoration. Test forbidden paths first. +- Avoid leaking whether accounts, roles, or sessions exist unless intended. +- Feature tests: happy path, unauthorized path, invalid input, service exception; role matrix: allowed, denied, expired session. + +## BadgeScan + +### BadgeScanController +**File:** `Api/BadgeScan/BadgeScanController.php` +**Purpose:** serve badge scan API responsibilities. +**Public methods:** `scan(Request $request)`, `logs()` +**Detected dependencies:** Services: `BadgeScanService` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Add dedicated FormRequest classes where this still uses raw `Request`; inline validation is a trap wearing a tiny fake mustache. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Authorization boundaries are the product, not decoration. Test forbidden paths first. +- Avoid leaking whether accounts, roles, or sessions exist unless intended. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Feature tests: happy path, unauthorized path, invalid input, service exception. + +## Badges + +### BadgeController +**File:** `Api/Badges/BadgeController.php` +**Purpose:** serve badge API responsibilities. +**Public methods:** `generatePdf(Request $request)`, `printStatus(Request $request)`, `logPrint(Request $request)`, `formData(Request $request)` +**Detected dependencies:** Services: `BadgeFormDataService`, `BadgePdfService`, `BadgePrintLogService` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Add dedicated FormRequest classes where this still uses raw `Request`; inline validation is a trap wearing a tiny fake mustache. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Authorization boundaries are the product, not decoration. Test forbidden paths first. +- Avoid leaking whether accounts, roles, or sessions exist unless intended. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Feature tests: happy path, unauthorized path, invalid input, service exception. + +## Certificates + +### CertificateController +**File:** `Api/Certificates/CertificateController.php` +**Purpose:** serve certificate API responsibilities. +**Public methods:** `formOptions(Request $request)`, `generate(Request $request)` +**Detected dependencies:** Services: `CertificatePdfService` | Models: `Configuration` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Add dedicated FormRequest classes where this still uses raw `Request`; inline validation is a trap wearing a tiny fake mustache. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Rate-limit sends and record delivery state; retries must not spam families into oblivion. +- Sanitize templates and recipient selection. +- Feature tests: happy path, unauthorized path, invalid input, service exception. + +## ClassPreparation + +### ClassPreparationController +**File:** `Api/ClassPreparation/ClassPreparationController.php` +**Purpose:** serve class preparation API responsibilities. +**Public methods:** `index(ClassPreparationIndexRequest $request)`, `markPrinted(ClassPreparationMarkPrintedRequest $request)`, `saveAdjustments(ClassPreparationAdjustmentRequest $request)`, `print(ClassPreparationPrintRequest $request)`, `stickerCounts(Request $request)`, `studentsByClass(Request $request, int $classSectionId)` +**Detected dependencies:** Services: `ClassRosterService`, `StickerCountService`, `ClassPreparationService` | Requests: `ClassPreparationAdjustmentRequest`, `ClassPreparationIndexRequest`, `ClassPreparationMarkPrintedRequest`, `ClassPreparationPrintRequest` | Resources: `ClassPreparationPrintResource`, `ClassPreparationResultResource` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Validate file type, size, storage path, and access ownership. +- Generated documents need reproducible inputs and predictable naming. +- Feature tests: list/filter pagination, empty result, invalid filter, tenant scoping. + +## ClassProgress + +### ClassProgressController +**File:** `Api/ClassProgress/ClassProgressController.php` +**Purpose:** serve class progress API responsibilities. +**Public methods:** `meta(ClassProgressMetaRequest $request)`, `legacySubmitForm(ClassProgressMetaRequest $request)`, `index(ClassProgressIndexRequest $request)`, `store(ClassProgressStoreRequest $request)`, `show(ClassProgressReport $class_progress)`, `update(ClassProgressUpdateRequest $request, ClassProgressReport $class_progress)`, `destroy(ClassProgressReport $class_progress)`, `downloadAttachment(int $attachment)`, `downloadLegacyAttachment(ClassProgressReport $class_progress)` +**Detected dependencies:** Services: `ClassProgressAttachmentService`, `ClassProgressMetaService`, `ClassProgressMutationService`, `ClassProgressQueryService` | Requests: `ClassProgressIndexRequest`, `ClassProgressMetaRequest`, `ClassProgressStoreRequest`, `ClassProgressUpdateRequest` | Resources: `ClassProgressGroupCollection`, `ClassProgressReportResource`, `ClassProgressShowResource` | Models: `ClassProgressAttachment`, `ClassProgressReport`, `Configuration`, `User` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Authorization boundaries are the product, not decoration. Test forbidden paths first. +- Avoid leaking whether accounts, roles, or sessions exist unless intended. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Feature tests: list/filter pagination, empty result, invalid filter, tenant scoping; show success, missing record, unauthorized record; create/submit validation, happy path, duplicate submission; update authorization, partial payload, concurrency/transaction rollback; delete success, already deleted, protected record. + +## Classes + +### ClassController +**File:** `Api/Classes/ClassController.php` +**Purpose:** serve class API responsibilities. +**Public methods:** `index(ClassSectionIndexRequest $request)`, `show(ClassSection $classSection)`, `store(ClassSectionStoreRequest $request)`, `update(ClassSectionUpdateRequest $request, ClassSection $classSection)`, `destroy(ClassSection $classSection)`, `attendance(Request $request, int $classSectionId)`, `seedDefaults()` +**Detected dependencies:** Services: `ClassAttendanceService`, `ClassSectionCommandService`, `ClassSectionQueryService`, `ClassSectionSeedService` | Requests: `ClassSectionIndexRequest`, `ClassSectionStoreRequest`, `ClassSectionUpdateRequest` | Resources: `ClassAttendanceResource`, `ClassSectionCollection`, `ClassSectionResource` | Models: `ClassSection` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Authorization boundaries are the product, not decoration. Test forbidden paths first. +- Avoid leaking whether accounts, roles, or sessions exist unless intended. +- Attendance changes need actor, timestamp, school term, and edit history. +- Define conflict behavior for duplicate scans or same-day edits. +- Feature tests: list/filter pagination, empty result, invalid filter, tenant scoping; show success, missing record, unauthorized record; create/submit validation, happy path, duplicate submission; update authorization, partial payload, concurrency/transaction rollback; delete success, already deleted, protected record. + +## Communication + +### CommunicationController +**File:** `Api/Communication/CommunicationController.php` +**Purpose:** serve communication API responsibilities. +**Public methods:** `options()`, `families(int $studentId)`, `guardians(int $familyId)`, `preview(Request $request)`, `send(Request $request)` +**Detected dependencies:** Services: `CommunicationFamilyService`, `CommunicationPreviewService`, `CommunicationSendService`, `CommunicationStudentService`, `CommunicationTemplateService` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Add dedicated FormRequest classes where this still uses raw `Request`; inline validation is a trap wearing a tiny fake mustache. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Attendance changes need actor, timestamp, school term, and edit history. +- Define conflict behavior for duplicate scans or same-day edits. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Feature tests: happy path, unauthorized path, invalid input, service exception. + +## CompetitionScores + +### CompetitionScoresController +**File:** `Api/CompetitionScores/CompetitionScoresController.php` +**Purpose:** manage academic scoring/grading workflow for competition scores. +**Public methods:** `index(Request $request)`, `edit(Request $request, int $id)`, `save(Request $request, int $id)` +**Detected dependencies:** Services: `CompetitionScoresContextService`, `CompetitionScoresQueryService`, `CompetitionScoresRosterService`, `CompetitionScoresSaveService` | Models: `Competition` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Add dedicated FormRequest classes where this still uses raw `Request`; inline validation is a trap wearing a tiny fake mustache. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Authorization boundaries are the product, not decoration. Test forbidden paths first. +- Avoid leaking whether accounts, roles, or sessions exist unless intended. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Feature tests: list/filter pagination, empty result, invalid filter, tenant scoping; create/submit validation, happy path, duplicate submission. + +## Core + +### BaseApiController +**File:** `Api/Core/BaseApiController.php` +**Purpose:** shared API response, validation, auth, and compatibility foundation. +**Public methods:** _No public endpoint methods detected._ +**Detected dependencies:** Models: `User` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Add dedicated FormRequest classes where this still uses raw `Request`; inline validation is a trap wearing a tiny fake mustache. +- Extract business logic into service/action classes if the controller currently queries models or mutates state directly. +- Rate-limit sends and record delivery state; retries must not spam families into oblivion. +- Sanitize templates and recipient selection. +- This controller is large enough to hide bugs. Split orchestration from business logic before it becomes archaeology. +- Feature tests: happy path, unauthorized path, invalid input, service exception. + +## Discounts + +### DiscountController +**File:** `Api/Discounts/DiscountController.php` +**Purpose:** serve discount API responsibilities. +**Public methods:** `options()`, `apply(ApplyDiscountVoucherRequest $request)`, `listVouchers()`, `storeVoucher(StoreDiscountVoucherRequest $request)`, `updateVoucher(UpdateDiscountVoucherRequest $request, int $id)`, `showVoucher(int $id)`, `deleteVoucher(int $id)` +**Detected dependencies:** Services: `DiscountApplyService`, `DiscountContextService`, `DiscountParentService`, `DiscountVoucherService` | Requests: `ApplyDiscountVoucherRequest`, `StoreDiscountVoucherRequest`, `UpdateDiscountVoucherRequest` | Resources: `DiscountParentResource`, `DiscountVoucherResource` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Authorization boundaries are the product, not decoration. Test forbidden paths first. +- Avoid leaking whether accounts, roles, or sessions exist unless intended. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Feature tests: happy path, unauthorized path, invalid input, service exception. + +## Documentation + +### ApiDocsAdminController +**File:** `Api/Documentation/ApiDocsAdminController.php` +**Purpose:** serve api docs admin API responsibilities. +**Public methods:** `index()`, `public()` +**Detected dependencies:** Services: `ApiDocsService` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Add dedicated FormRequest classes where this still uses raw `Request`; inline validation is a trap wearing a tiny fake mustache. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Authorization boundaries are the product, not decoration. Test forbidden paths first. +- Avoid leaking whether accounts, roles, or sessions exist unless intended. +- Feature tests: list/filter pagination, empty result, invalid filter, tenant scoping. + +### DocsCatalogController +**File:** `Api/Documentation/DocsCatalogController.php` +**Purpose:** serve docs catalog API responsibilities. +**Public methods:** `index(Request $request)`, `ui()`, `swagger()`, `swaggerMergedJson()` +**Detected dependencies:** Services: `DocsCatalogService`, `OpenApiRouteExporter` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Add dedicated FormRequest classes where this still uses raw `Request`; inline validation is a trap wearing a tiny fake mustache. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Validate file type, size, storage path, and access ownership. +- Generated documents need reproducible inputs and predictable naming. +- Feature tests: list/filter pagination, empty result, invalid filter, tenant scoping. + +## Email + +### BroadcastEmailController +**File:** `Api/Email/BroadcastEmailController.php` +**Purpose:** serve broadcast email API responsibilities. +**Public methods:** `options()`, `send(Request $request)`, `uploadImage(Request $request)` +**Detected dependencies:** Services: `BroadcastEmailComposerService`, `BroadcastEmailDispatchService`, `BroadcastEmailImageService`, `BroadcastEmailRecipientService`, `BroadcastEmailSenderOptionsService` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Add dedicated FormRequest classes where this still uses raw `Request`; inline validation is a trap wearing a tiny fake mustache. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Rate-limit sends and record delivery state; retries must not spam families into oblivion. +- Sanitize templates and recipient selection. +- Feature tests: happy path, unauthorized path, invalid input, service exception. + +### EmailController +**File:** `Api/Email/EmailController.php` +**Purpose:** serve email API responsibilities. +**Public methods:** `senders()`, `send(EmailSendRequest $request)` +**Detected dependencies:** Services: `EmailAttachmentService`, `EmailDispatchService`, `EmailSenderOptionsService` | Requests: `EmailSendRequest` | Resources: `EmailSenderResource` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Rate-limit sends and record delivery state; retries must not spam families into oblivion. +- Sanitize templates and recipient selection. +- Feature tests: happy path, unauthorized path, invalid input, service exception. + +### EmailExtractorController +**File:** `Api/Email/EmailExtractorController.php` +**Purpose:** serve email extractor API responsibilities. +**Public methods:** `emails()`, `compare(EmailExtractorCompareRequest $request)` +**Detected dependencies:** Services: `EmailExtractorService` | Requests: `EmailExtractorCompareRequest` | Resources: `EmailExtractorCompareResource`, `EmailExtractorEmailsResource` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Rate-limit sends and record delivery state; retries must not spam families into oblivion. +- Sanitize templates and recipient selection. +- Feature tests: happy path, unauthorized path, invalid input, service exception. + +## Exams + +### ExamDraftController +**File:** `Api/Exams/ExamDraftController.php` +**Purpose:** serve exam draft API responsibilities. +**Public methods:** `teacherIndex()`, `teacherStore(ExamDraftTeacherStoreRequest $request)`, `adminIndex()`, `adminUploadLegacy(ExamDraftAdminLegacyRequest $request)`, `adminReview(ExamDraftAdminReviewRequest $request)` +**Detected dependencies:** Services: `ExamDraftAdminService`, `ExamDraftTeacherService` | Requests: `ExamDraftAdminLegacyRequest`, `ExamDraftAdminReviewRequest`, `ExamDraftTeacherStoreRequest` | Resources: `ExamDraftResource` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Authorization boundaries are the product, not decoration. Test forbidden paths first. +- Avoid leaking whether accounts, roles, or sessions exist unless intended. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Feature tests: happy path, unauthorized path, invalid input, service exception. + +## Expenses + +### ExpenseController +**File:** `Api/Expenses/ExpenseController.php` +**Purpose:** serve expense API responsibilities. +**Public methods:** `options()`, `index()`, `show(int $id)`, `store(StoreExpenseRequest $request)`, `update(UpdateExpenseRequest $request, int $id)`, `updateStatus(UpdateExpenseStatusRequest $request, int $id)` +**Detected dependencies:** Services: `ExpenseContextService`, `ExpenseQueryService`, `ExpenseReceiptService`, `ExpenseRetailorService`, `ExpenseStaffService`, `ExpenseStatusService`, `ExpenseWriteService` | Requests: `StoreExpenseRequest`, `UpdateExpenseRequest`, `UpdateExpenseStatusRequest` | Resources: `ExpenseResource`, `ExpenseStaffResource` | Models: `Expense` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Mutating endpoints need request validation, service-layer ownership, and database transactions where multiple rows change. +- Feature tests: list/filter pagination, empty result, invalid filter, tenant scoping; show success, missing record, unauthorized record; create/submit validation, happy path, duplicate submission; update authorization, partial payload, concurrency/transaction rollback. + +## ExtraCharges + +### ExtraChargesController +**File:** `Api/ExtraCharges/ExtraChargesController.php` +**Purpose:** manage finance workflow for extra charges, with money-state integrity as the main risk. +**Public methods:** `list(Request $request)`, `options(Request $request)`, `parentOptions(Request $request)`, `invoicesForParent(Request $request)`, `store(StoreExtraChargeRequest $request)`, `update(UpdateExtraChargeRequest $request, int $id)`, `void(int $id)`, `reverse(int $id)` +**Detected dependencies:** Services: `ExtraChargesChargeService`, `ExtraChargesContextService`, `ExtraChargesInvoiceService`, `ExtraChargesListService`, `ExtraChargesMetaService`, `ExtraChargesParentService` | Requests: `StoreExtraChargeRequest`, `UpdateExtraChargeRequest` | Resources: `ExtraChargeInvoiceOptionResource`, `ExtraChargeParentOptionResource`, `ExtraChargeResource` | Models: `AdditionalCharge` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Money operations must be idempotent, auditable, and transaction-safe. +- Never trust client-calculated totals; recompute server-side. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Feature tests: list/filter pagination, empty result, invalid filter, tenant scoping; create/submit validation, happy path, duplicate submission; update authorization, partial payload, concurrency/transaction rollback; idempotency and ledger/audit consistency. + +## Family + +### FamilyAdminController +**File:** `Api/Family/FamilyAdminController.php` +**Purpose:** manage school identity and operations workflow for family admin. +**Public methods:** `index(FamilyAdminIndexRequest $request)`, `search(FamilyAdminSearchRequest $request)`, `card(FamilyAdminCardRequest $request)`, `composeEmail(FamilyComposeEmailRequest $request)` +**Detected dependencies:** Services: `FamilyNotificationService`, `FamilyQueryService` | Requests: `FamilyAdminCardRequest`, `FamilyAdminIndexRequest`, `FamilyAdminSearchRequest`, `FamilyComposeEmailRequest` | Resources: `FamilyResource`, `FamilySearchItemResource` | Models: `Configuration` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Rate-limit sends and record delivery state; retries must not spam families into oblivion. +- Sanitize templates and recipient selection. +- Feature tests: list/filter pagination, empty result, invalid filter, tenant scoping. + +### FamilyController +**File:** `Api/Family/FamilyController.php` +**Purpose:** manage school identity and operations workflow for family. +**Public methods:** `familiesByStudent(FamiliesByStudentRequest $request, int $studentId)`, `guardiansByFamily(GuardiansByFamilyRequest $request, int $familyId)`, `bootstrap(FamilyBootstrapRequest $request)`, `attachSecondByUser(FamilyAttachSecondByUserRequest $request)`, `attachSecondByEmail(FamilyAttachSecondByEmailRequest $request)`, `setPrimaryHome(FamilySetPrimaryHomeRequest $request)`, `setGuardianFlags(FamilySetGuardianFlagsRequest $request)`, `unlinkGuardian(FamilyUnlinkGuardianRequest $request)`, `unlinkStudent(FamilyUnlinkStudentRequest $request)`, `importSecondParentsFromLegacy(FamilyImportLegacyRequest $request)` +**Detected dependencies:** Services: `FamilyMutationService`, `FamilyQueryService` | Requests: `FamiliesByStudentRequest`, `FamilyAttachSecondByEmailRequest`, `FamilyAttachSecondByUserRequest`, `FamilyBootstrapRequest`, `FamilyImportLegacyRequest`, `FamilySetGuardianFlagsRequest`, `FamilySetPrimaryHomeRequest`, `FamilyUnlinkGuardianRequest`… | Resources: `FamilyGuardianResource`, `FamilyResource` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Rate-limit sends and record delivery state; retries must not spam families into oblivion. +- Sanitize templates and recipient selection. +- Feature tests: happy path, unauthorized path, invalid input, service exception. + +## Finance + +### ChargeController +**File:** `Api/Finance/ChargeController.php` +**Purpose:** manage finance workflow for charge, with money-state integrity as the main risk. +**Public methods:** `index(ChargeListRequest $request)`, `storeExtra(StoreExtraChargeRequest $request)`, `storeEvent(StoreEventChargeRequest $request)`, `cancelExtra(int $id)`, `applyExtra(ApplyExtraChargeRequest $request, int $id)`, `cancelEvent(Request $request, int $id)`, `markEventPaid(MarkEventChargePaidRequest $request, int $id)` +**Detected dependencies:** Services: `ChargeService` | Requests: `ApplyExtraChargeRequest`, `ChargeListRequest`, `MarkEventChargePaidRequest`, `StoreEventChargeRequest`, `StoreExtraChargeRequest` | Resources: `ChargeActionResource`, `ChargeListResource` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Money operations must be idempotent, auditable, and transaction-safe. +- Never trust client-calculated totals; recompute server-side. +- Authorization boundaries are the product, not decoration. Test forbidden paths first. +- Avoid leaking whether accounts, roles, or sessions exist unless intended. +- Feature tests: list/filter pagination, empty result, invalid filter, tenant scoping; idempotency and ledger/audit consistency. + +### FeeCalculationController +**File:** `Api/Finance/FeeCalculationController.php` +**Purpose:** manage finance workflow for fee calculation, with money-state integrity as the main risk. +**Public methods:** `refund(FeeRefundRequest $request)`, `tuitionTotal(FeeTuitionTotalRequest $request)`, `tuitionBreakdown(FeeTuitionBreakdownRequest $request)`, `familyBalance(FeeFamilyBalanceRequest $request)` +**Detected dependencies:** Services: `BalanceCalculationService`, `FeeRefundCalculatorService`, `FeeStudentFeeService`, `TuitionCalculationService` | Requests: `FeeFamilyBalanceRequest`, `FeeRefundRequest`, `FeeTuitionBreakdownRequest`, `FeeTuitionTotalRequest` | Resources: `FeeFamilyBalanceResource`, `FeeRefundResource`, `FeeTuitionBreakdownResource`, `FeeTuitionTotalResource` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Money operations must be idempotent, auditable, and transaction-safe. +- Never trust client-calculated totals; recompute server-side. +- Attendance changes need actor, timestamp, school term, and edit history. +- Define conflict behavior for duplicate scans or same-day edits. +- Feature tests: happy path, unauthorized path, invalid input, service exception; idempotency and ledger/audit consistency. + +### FinancialController +**File:** `Api/Finance/FinancialController.php` +**Purpose:** manage finance workflow for financial, with money-state integrity as the main risk. +**Public methods:** `report(FinancialReportRequest $request)`, `summary(FinancialSummaryRequest $request)`, `unpaidParents(FinancialUnpaidParentsRequest $request)`, `downloadCsv(FinancialReportRequest $request)`, `downloadPdf(FinancialReportRequest $request)` +**Detected dependencies:** Services: `FinancialPdfReportService`, `FinancialReportService`, `FinancialSchoolYearService`, `FinancialSummaryService`, `FinancialUnpaidParentsService` | Requests: `FinancialReportRequest`, `FinancialSummaryRequest`, `FinancialUnpaidParentsRequest` | Resources: `FinancialReportResource`, `FinancialSummaryResource`, `FinancialUnpaidParentResource` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Validate file type, size, storage path, and access ownership. +- Generated documents need reproducible inputs and predictable naming. +- Feature tests: happy path, unauthorized path, invalid input, service exception; idempotency and ledger/audit consistency. + +### InvoiceController +**File:** `Api/Finance/InvoiceController.php` +**Purpose:** manage finance workflow for invoice, with money-state integrity as the main risk. +**Public methods:** `management(InvoiceManagementRequest $request)`, `generate(Request $request)`, `byParent(InvoiceParentRequest $request, int $parentId)`, `parentPayment(InvoiceParentRequest $request)`, `updateStatus(InvoiceStatusRequest $request, int $invoiceId)`, `unpaid()`, `pdf(int $invoiceId)` +**Detected dependencies:** Services: `InvoiceGenerationService`, `InvoiceManagementService`, `InvoicePaymentService`, `InvoicePdfService` | Requests: `InvoiceManagementRequest`, `InvoiceParentRequest`, `InvoiceStatusRequest` | Resources: `InvoiceManagementParentResource`, `InvoiceResource` | Models: `Invoice` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Money operations must be idempotent, auditable, and transaction-safe. +- Never trust client-calculated totals; recompute server-side. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Feature tests: happy path, unauthorized path, invalid input, service exception; idempotency and ledger/audit consistency. + +### PaymentController +**File:** `Api/Finance/PaymentController.php` +**Purpose:** manage finance workflow for payment, with money-state integrity as the main risk. +**Public methods:** `store(Request $request)`, `byParent(PaymentByParentRequest $request, int $parentId)`, `updateBalance(Request $request, int $paymentId)` +**Detected dependencies:** Services: `PaymentBalanceService`, `PaymentLookupService`, `PaymentPlanService` | Requests: `PaymentByParentRequest` | Resources: `PaymentResource` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Money operations must be idempotent, auditable, and transaction-safe. +- Never trust client-calculated totals; recompute server-side. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Feature tests: create/submit validation, happy path, duplicate submission; idempotency and ledger/audit consistency. + +### PaymentEventChargesController +**File:** `Api/Finance/PaymentEventChargesController.php` +**Purpose:** manage finance workflow for payment event charges, with money-state integrity as the main risk. +**Public methods:** `index(PaymentEventChargesListRequest $request)`, `store(Request $request)`, `enrolledStudents(PaymentEventChargesListRequest $request, int $parentId)` +**Detected dependencies:** Services: `PaymentEventChargesService` | Requests: `PaymentEventChargesListRequest` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Money operations must be idempotent, auditable, and transaction-safe. +- Never trust client-calculated totals; recompute server-side. +- Authorization boundaries are the product, not decoration. Test forbidden paths first. +- Avoid leaking whether accounts, roles, or sessions exist unless intended. +- Feature tests: list/filter pagination, empty result, invalid filter, tenant scoping; create/submit validation, happy path, duplicate submission; idempotency and ledger/audit consistency. + +### PaymentManualController +**File:** `Api/Finance/PaymentManualController.php` +**Purpose:** manage finance workflow for payment manual, with money-state integrity as the main risk. +**Public methods:** `search(Request $request)`, `suggest(Request $request)`, `record(PaymentManualUpdateRequest $request, int $invoiceId)`, `edit(PaymentManualEditRequest $request, int $paymentId)`, `file(Request $request, string $filename)` +**Detected dependencies:** Services: `PaymentManualService` | Requests: `PaymentManualEditRequest`, `PaymentManualUpdateRequest` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Money operations must be idempotent, auditable, and transaction-safe. +- Never trust client-calculated totals; recompute server-side. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Feature tests: happy path, unauthorized path, invalid input, service exception; idempotency and ledger/audit consistency. + +### PaymentNotificationController +**File:** `Api/Finance/PaymentNotificationController.php` +**Purpose:** manage finance workflow for payment notification, with money-state integrity as the main risk. +**Public methods:** `index(PaymentNotificationListRequest $request)`, `send(PaymentNotificationSendRequest $request)` +**Detected dependencies:** Services: `PaymentNotificationService` | Requests: `PaymentNotificationListRequest`, `PaymentNotificationSendRequest` | Resources: `PaymentNotificationLogResource` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Money operations must be idempotent, auditable, and transaction-safe. +- Never trust client-calculated totals; recompute server-side. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Feature tests: list/filter pagination, empty result, invalid filter, tenant scoping; idempotency and ledger/audit consistency. + +### PaymentTransactionController +**File:** `Api/Finance/PaymentTransactionController.php` +**Purpose:** manage finance workflow for payment transaction, with money-state integrity as the main risk. +**Public methods:** `store(Request $request)`, `byPayment(int $paymentId)`, `updateStatus(Request $request, string $transactionId)` +**Detected dependencies:** Services: `PaymentTransactionService` | Resources: `PaymentTransactionResource` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Add dedicated FormRequest classes where this still uses raw `Request`; inline validation is a trap wearing a tiny fake mustache. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Money operations must be idempotent, auditable, and transaction-safe. +- Never trust client-calculated totals; recompute server-side. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Feature tests: create/submit validation, happy path, duplicate submission; idempotency and ledger/audit consistency. + +### PaypalPaymentController +**File:** `Api/Finance/PaypalPaymentController.php` +**Purpose:** manage finance workflow for paypal payment, with money-state integrity as the main risk. +**Public methods:** `redirect()`, `create(int $paymentId)`, `execute(PaypalExecuteRequest $request)`, `cancel()` +**Detected dependencies:** Services: `PaypalPaymentService` | Requests: `PaypalExecuteRequest` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Money operations must be idempotent, auditable, and transaction-safe. +- Never trust client-calculated totals; recompute server-side. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Feature tests: create/submit validation, happy path, duplicate submission; idempotency and ledger/audit consistency. + +### PaypalTransactionsController +**File:** `Api/Finance/PaypalTransactionsController.php` +**Purpose:** manage finance workflow for paypal transactions, with money-state integrity as the main risk. +**Public methods:** `index(PaypalTransactionsListRequest $request)`, `downloadCsv(PaypalTransactionsListRequest $request)` +**Detected dependencies:** Services: `PaypalTransactionsService` | Requests: `PaypalTransactionsListRequest` | Resources: `PaypalTransactionResource` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Money operations must be idempotent, auditable, and transaction-safe. +- Never trust client-calculated totals; recompute server-side. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Feature tests: list/filter pagination, empty result, invalid filter, tenant scoping; idempotency and ledger/audit consistency. + +### PurchaseOrderController +**File:** `Api/Finance/PurchaseOrderController.php` +**Purpose:** manage finance workflow for purchase order, with money-state integrity as the main risk. +**Public methods:** `index(PurchaseOrderIndexRequest $request)`, `options()`, `show(int $id)`, `store(Request $request)`, `receive(Request $request, int $id)`, `cancel(int $id)` +**Detected dependencies:** Services: `PurchaseOrderCreateService`, `PurchaseOrderQueryService`, `PurchaseOrderReceiveService`, `PurchaseOrderStatusService` | Requests: `PurchaseOrderIndexRequest` | Resources: `PurchaseOrderItemResource`, `PurchaseOrderResource` | Models: `PurchaseOrder` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Money operations must be idempotent, auditable, and transaction-safe. +- Never trust client-calculated totals; recompute server-side. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Feature tests: list/filter pagination, empty result, invalid filter, tenant scoping; show success, missing record, unauthorized record; create/submit validation, happy path, duplicate submission; idempotency and ledger/audit consistency. + +### RefundController +**File:** `Api/Finance/RefundController.php` +**Purpose:** manage finance workflow for refund, with money-state integrity as the main risk. +**Public methods:** `index(RefundIndexRequest $request)`, `show(int $refundId)`, `store(RefundStoreRequest $request)`, `update(RefundDecisionRequest $request, int $refundId)`, `destroy(int $refundId)`, `approve(int $refundId)`, `reject(RefundRejectRequest $request, int $refundId)`, `recordPayment(RefundPaymentRequest $request, int $refundId)`, `recalculateOverpayments(RefundRecalculateRequest $request)`, `parentBalances(int $parentId)` +**Detected dependencies:** Services: `RefundDecisionService`, `RefundOverpaymentService`, `RefundPayoutService`, `RefundQueryService`, `RefundRequestService`, `RefundSummaryService` | Requests: `RefundDecisionRequest`, `RefundIndexRequest`, `RefundPaymentRequest`, `RefundRecalculateRequest`, `RefundRejectRequest`, `RefundStoreRequest` | Resources: `RefundResource` | Models: `Configuration` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Money operations must be idempotent, auditable, and transaction-safe. +- Never trust client-calculated totals; recompute server-side. +- Attendance changes need actor, timestamp, school term, and edit history. +- Define conflict behavior for duplicate scans or same-day edits. +- Feature tests: list/filter pagination, empty result, invalid filter, tenant scoping; show success, missing record, unauthorized record; create/submit validation, happy path, duplicate submission; update authorization, partial payload, concurrency/transaction rollback; delete success, already deleted, protected record; idempotency and ledger/audit consistency. + +### ReimbursementController +**File:** `Api/Finance/ReimbursementController.php` +**Purpose:** manage finance workflow for reimbursement, with money-state integrity as the main risk. +**Public methods:** `underProcessing(ReimbursementUnderProcessingRequest $request)`, `markDonation(Request $request)`, `createBatch(Request $request)`, `updateBatchAssignment(ReimbursementBatchAssignmentRequest $request)`, `lockBatch(ReimbursementBatchLockRequest $request)`, `uploadBatchAdminFile(ReimbursementBatchAdminFileRequest $request)`, `serveAdminCheckFile(FileNameRequest $request, string $name, string $mode = 'inline')`, `index(ReimbursementIndexRequest $request)`, `sendBatchEmail(ReimbursementBatchEmailRequest $request)`, `export(ReimbursementExportRequest $request)`, `exportBatch(ReimbursementExportBatchRequest $request)`, `store(Request $request)`, `process(ReimbursementProcessRequest $request)`, `reimbursedExpenses()`, `update(Request $request, int $id)` +**Detected dependencies:** Services: `FileServeService`, `ReimbursementBatchAssignmentService`, `ReimbursementBatchService`, `ReimbursementContextService`, `ReimbursementCrudService`, `ReimbursementDonationService`, `ReimbursementEmailService`, `ReimbursementExportService`… | Requests: `FileNameRequest`, `ReimbursementBatchAdminFileRequest`, `ReimbursementBatchAssignmentRequest`, `ReimbursementBatchEmailRequest`, `ReimbursementBatchLockRequest`, `ReimbursementExportBatchRequest`, `ReimbursementExportRequest`, `ReimbursementIndexRequest`… | Resources: `ReimbursementBatchResource`, `ReimbursementExpenseResource`, `ReimbursementRecipientResource`, `ReimbursementResource`, `ReimbursementUnderProcessingItemResource` | Models: `Reimbursement`, `ReimbursementBatch` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Money operations must be idempotent, auditable, and transaction-safe. +- Never trust client-calculated totals; recompute server-side. +- Rate-limit sends and record delivery state; retries must not spam families into oblivion. +- Sanitize templates and recipient selection. +- Feature tests: list/filter pagination, empty result, invalid filter, tenant scoping; create/submit validation, happy path, duplicate submission; update authorization, partial payload, concurrency/transaction rollback; idempotency and ledger/audit consistency. + +## Frontend + +### FrontendController +**File:** `Api/Frontend/FrontendController.php` +**Purpose:** serve frontend API responsibilities. +**Public methods:** `index()`, `facility()`, `team()`, `callToAction()`, `testimonial()`, `notFound()`, `fetchUser()` +**Detected dependencies:** Services: `FrontendPageService` | Resources: `FrontendPageResource`, `FrontendUserResource` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Add dedicated FormRequest classes where this still uses raw `Request`; inline validation is a trap wearing a tiny fake mustache. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Authorization boundaries are the product, not decoration. Test forbidden paths first. +- Avoid leaking whether accounts, roles, or sessions exist unless intended. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Feature tests: list/filter pagination, empty result, invalid filter, tenant scoping. + +### InfoIconController +**File:** `Api/Frontend/InfoIconController.php` +**Purpose:** serve info icon API responsibilities. +**Public methods:** `profileIcon()` +**Detected dependencies:** Services: `ProfileIconService` | Resources: `ProfileIconResource` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Add dedicated FormRequest classes where this still uses raw `Request`; inline validation is a trap wearing a tiny fake mustache. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Authorization boundaries are the product, not decoration. Test forbidden paths first. +- Avoid leaking whether accounts, roles, or sessions exist unless intended. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Feature tests: happy path, unauthorized path, invalid input, service exception. + +### LandingPageController +**File:** `Api/Frontend/LandingPageController.php` +**Purpose:** serve landing page API responsibilities. +**Public methods:** `index(LandingTeacherRequest $request)`, `teacher(LandingTeacherRequest $request)`, `parent()`, `administrator()`, `admin()`, `student()`, `guest()` +**Detected dependencies:** Services: `LandingPageService` | Requests: `LandingTeacherRequest` | Resources: `LandingPageResource` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Authorization boundaries are the product, not decoration. Test forbidden paths first. +- Avoid leaking whether accounts, roles, or sessions exist unless intended. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Feature tests: list/filter pagination, empty result, invalid filter, tenant scoping. + +### PageController +**File:** `Api/Frontend/PageController.php` +**Purpose:** serve page API responsibilities. +**Public methods:** `privacyPolicy()`, `termsOfService()`, `helpCenter()`, `submitContact(ContactSubmitRequest $request)` +**Detected dependencies:** Services: `ContactSubmissionService`, `StaticPageService` | Requests: `ContactSubmitRequest` | Resources: `ContactSubmissionResource`, `PageContentResource` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Rate-limit sends and record delivery state; retries must not spam families into oblivion. +- Sanitize templates and recipient selection. +- Feature tests: happy path, unauthorized path, invalid input, service exception. + +## Grading + +### GradingController +**File:** `Api/Grading/GradingController.php` +**Purpose:** manage academic scoring/grading workflow for grading. +**Public methods:** `overview(GradingOverviewRequest $request)`, `show(GradingScoreShowRequest $request, string $type, int $classSectionId, int $studentId)`, `update(GradingScoreUpdateRequest $request)`, `toggleLock(GradingLockRequest $request)`, `lockAll(GradingLockAllRequest $request)`, `refresh(GradingRefreshRequest $request)`, `placement(PlacementRequest $request)`, `updatePlacementLevel(PlacementLevelRequest $request)`, `updatePlacementLevels(PlacementLevelsRequest $request)`, `createPlacementBatch(PlacementBatchRequest $request)`, `showPlacementBatch(int $batchId)`, `updatePlacementBatch(PlacementBatchUpdateRequest $request, int $batchId)`, `belowSixty(BelowSixtyListRequest $request)`, `belowSixtyEmail(BelowSixtyEmailRequest $request)`, `sendBelowSixtyEmail(BelowSixtySendRequest $request)`, `updateBelowSixtyStatus(BelowSixtyStatusRequest $request)`, `belowSixtyMeeting(BelowSixtyMeetingRequest $request)`, `saveBelowSixtyMeeting(BelowSixtyMeetingSaveRequest $request)` +**Detected dependencies:** Services: `GradingBelowSixtyService`, `GradingLockService`, `GradingOverviewService`, `GradingPlacementService`, `GradingRefreshService`, `GradingScoreService` | Requests: `BelowSixtyEmailRequest`, `BelowSixtyMeetingRequest`, `BelowSixtyMeetingSaveRequest`, `BelowSixtySendRequest`, `BelowSixtyStatusRequest`, `BelowSixtyListRequest`, `GradingLockAllRequest`, `GradingLockRequest`… | Resources: `BelowSixtyStudentResource`, `GradingScoreResource` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Rate-limit sends and record delivery state; retries must not spam families into oblivion. +- Sanitize templates and recipient selection. +- Feature tests: show success, missing record, unauthorized record; update authorization, partial payload, concurrency/transaction rollback. + +### HomeworkTrackingController +**File:** `Api/Grading/HomeworkTrackingController.php` +**Purpose:** manage academic scoring/grading workflow for homework tracking. +**Public methods:** `index(HomeworkTrackingRequest $request)` +**Detected dependencies:** Services: `HomeworkTrackingService` | Requests: `HomeworkTrackingRequest` | Resources: `HomeworkTrackingTeacherResource` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Validate file type, size, storage path, and access ownership. +- Generated documents need reproducible inputs and predictable naming. +- Feature tests: list/filter pagination, empty result, invalid filter, tenant scoping. + +## Incidents + +### IncidentController +**File:** `Api/Incidents/IncidentController.php` +**Purpose:** serve incident API responsibilities. +**Public methods:** `current()`, `formMeta()`, `history(IncidentListRequest $request)`, `processed(IncidentListRequest $request)`, `analysis(IncidentListRequest $request)`, `studentsByGrade(int $gradeId)`, `store(Request $request)`, `updateState(IncidentStateRequest $request, int $incidentId)`, `close(Request $request, int $incidentId)`, `cancel(IncidentCancelRequest $request, int $incidentId)` +**Detected dependencies:** Services: `CurrentIncidentService`, `IncidentAnalysisService`, `IncidentHistoryService` | Requests: `IncidentCancelRequest`, `IncidentCloseRequest`, `IncidentListRequest`, `IncidentStateRequest` | Resources: `IncidentAnalysisStudentResource`, `IncidentGradeResource`, `IncidentResource`, `IncidentStudentOptionResource` | Models: `Configuration` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Mutating endpoints need request validation, service-layer ownership, and database transactions where multiple rows change. +- Feature tests: create/submit validation, happy path, duplicate submission. + +## Inventory + +### InventoryCategoryController +**File:** `Api/Inventory/InventoryCategoryController.php` +**Purpose:** provide CRUD API endpoints for inventory category. +**Public methods:** `index(InventoryCategoryIndexRequest $request)`, `store(InventoryCategoryStoreRequest $request)`, `update(InventoryCategoryUpdateRequest $request, int $id)`, `destroy(int $id)` +**Detected dependencies:** Services: `InventoryCategoryService` | Requests: `InventoryCategoryIndexRequest`, `InventoryCategoryStoreRequest`, `InventoryCategoryUpdateRequest` | Resources: `InventoryCategoryResource` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Rate-limit sends and record delivery state; retries must not spam families into oblivion. +- Sanitize templates and recipient selection. +- Feature tests: list/filter pagination, empty result, invalid filter, tenant scoping; create/submit validation, happy path, duplicate submission; update authorization, partial payload, concurrency/transaction rollback; delete success, already deleted, protected record. + +### InventoryController +**File:** `Api/Inventory/InventoryController.php` +**Purpose:** serve inventory API responsibilities. +**Public methods:** `index(InventoryItemIndexRequest $request)`, `show(int $id)`, `store(InventoryItemStoreRequest $request)`, `update(InventoryItemUpdateRequest $request, int $id)`, `destroy(int $id)`, `audit(InventoryAuditRequest $request, int $id)`, `adjust(InventoryAdjustRequest $request, int $id)`, `summary(string $type)`, `summaryAll()`, `teacherDistribution(InventoryTeacherDistributionRequest $request)`, `teacherDistributionStore(InventoryTeacherDistributionStoreRequest $request)` +**Detected dependencies:** Services: `InventoryItemService`, `InventorySummaryService`, `InventoryTeacherDistributionService` | Requests: `InventoryAdjustRequest`, `InventoryAuditRequest`, `InventoryItemIndexRequest`, `InventoryItemStoreRequest`, `InventoryItemUpdateRequest`, `InventoryTeacherDistributionRequest`, `InventoryTeacherDistributionStoreRequest` | Resources: `InventoryItemResource` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Mutating endpoints need request validation, service-layer ownership, and database transactions where multiple rows change. +- Feature tests: list/filter pagination, empty result, invalid filter, tenant scoping; show success, missing record, unauthorized record; create/submit validation, happy path, duplicate submission; update authorization, partial payload, concurrency/transaction rollback; delete success, already deleted, protected record. + +### InventoryMovementController +**File:** `Api/Inventory/InventoryMovementController.php` +**Purpose:** serve inventory movement API responsibilities. +**Public methods:** `index(InventoryMovementIndexRequest $request)`, `store(InventoryMovementStoreRequest $request)`, `update(InventoryMovementUpdateRequest $request, int $id)`, `destroy(int $id)`, `bulkDelete(InventoryMovementBulkDeleteRequest $request)` +**Detected dependencies:** Services: `InventoryMovementService` | Requests: `InventoryMovementBulkDeleteRequest`, `InventoryMovementIndexRequest`, `InventoryMovementStoreRequest`, `InventoryMovementUpdateRequest` | Resources: `InventoryMovementResource` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Authorization boundaries are the product, not decoration. Test forbidden paths first. +- Avoid leaking whether accounts, roles, or sessions exist unless intended. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Feature tests: list/filter pagination, empty result, invalid filter, tenant scoping; create/submit validation, happy path, duplicate submission; update authorization, partial payload, concurrency/transaction rollback; delete success, already deleted, protected record. + +### SupplierController +**File:** `Api/Inventory/SupplierController.php` +**Purpose:** provide CRUD API endpoints for supplier. +**Public methods:** `index(SupplierIndexRequest $request)`, `store(SupplierStoreRequest $request)`, `update(SupplierUpdateRequest $request, int $id)`, `destroy(int $id)` +**Detected dependencies:** Services: `SupplierService` | Requests: `SupplierIndexRequest`, `SupplierStoreRequest`, `SupplierUpdateRequest` | Resources: `SupplierResource` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Rate-limit sends and record delivery state; retries must not spam families into oblivion. +- Sanitize templates and recipient selection. +- Feature tests: list/filter pagination, empty result, invalid filter, tenant scoping; create/submit validation, happy path, duplicate submission; update authorization, partial payload, concurrency/transaction rollback; delete success, already deleted, protected record. + +### SupplyCategoryController +**File:** `Api/Inventory/SupplyCategoryController.php` +**Purpose:** provide CRUD API endpoints for supply category. +**Public methods:** `index()`, `store(SupplyCategoryStoreRequest $request)`, `update(SupplyCategoryUpdateRequest $request, int $id)`, `destroy(int $id)` +**Detected dependencies:** Services: `SupplyCategoryService` | Requests: `SupplyCategoryStoreRequest`, `SupplyCategoryUpdateRequest` | Resources: `SupplyCategoryResource` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Rate-limit sends and record delivery state; retries must not spam families into oblivion. +- Sanitize templates and recipient selection. +- Feature tests: list/filter pagination, empty result, invalid filter, tenant scoping; create/submit validation, happy path, duplicate submission; update authorization, partial payload, concurrency/transaction rollback; delete success, already deleted, protected record. + +## Messaging + +### MessagesController +**File:** `Api/Messaging/MessagesController.php` +**Purpose:** serve messages API responsibilities. +**Public methods:** `index(MessageIndexRequest $request)`, `inbox(MessageIndexRequest $request)`, `sent(MessageIndexRequest $request)`, `drafts(MessageIndexRequest $request)`, `trash(MessageIndexRequest $request)`, `show(int $id)`, `store(MessageSendRequest $request)`, `receive()`, `recipients(string $type)`, `update(MessageUpdateRequest $request, int $id)`, `destroy(int $id)` +**Detected dependencies:** Services: `MessageCommandService`, `MessageQueryService`, `MessageRecipientService` | Requests: `MessageIndexRequest`, `MessageSendRequest`, `MessageUpdateRequest` | Resources: `MessageCollection`, `MessageResource`, `RecipientResource` | Models: `Message` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Authorization boundaries are the product, not decoration. Test forbidden paths first. +- Avoid leaking whether accounts, roles, or sessions exist unless intended. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Feature tests: list/filter pagination, empty result, invalid filter, tenant scoping; show success, missing record, unauthorized record; create/submit validation, happy path, duplicate submission; update authorization, partial payload, concurrency/transaction rollback; delete success, already deleted, protected record. + +### WhatsappController +**File:** `Api/Messaging/WhatsappController.php` +**Purpose:** serve whatsapp API responsibilities. +**Public methods:** `index(WhatsappLinkIndexRequest $request)`, `show(int $linkId)`, `store(WhatsappLinkStoreRequest $request)`, `update(WhatsappLinkUpdateRequest $request, int $linkId)`, `destroy(int $linkId)`, `parentContacts(WhatsappParentContactsRequest $request)`, `parentContactsByClass(WhatsappParentContactsByClassRequest $request)`, `sendInvites(WhatsappInviteSendRequest $request)`, `updateMembership(WhatsappMembershipUpdateRequest $request)` +**Detected dependencies:** Services: `WhatsappContactService`, `WhatsappContextService`, `WhatsappInviteService`, `WhatsappLinkService`, `WhatsappMembershipService` | Requests: `WhatsappInviteSendRequest`, `WhatsappLinkIndexRequest`, `WhatsappLinkStoreRequest`, `WhatsappLinkUpdateRequest`, `WhatsappMembershipUpdateRequest`, `WhatsappParentContactsByClassRequest`, `WhatsappParentContactsRequest` | Resources: `WhatsappClassSectionContactResource`, `WhatsappGroupLinkCollection`, `WhatsappGroupLinkResource`, `WhatsappParentContactResource` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Rate-limit sends and record delivery state; retries must not spam families into oblivion. +- Sanitize templates and recipient selection. +- Feature tests: list/filter pagination, empty result, invalid filter, tenant scoping; show success, missing record, unauthorized record; create/submit validation, happy path, duplicate submission; update authorization, partial payload, concurrency/transaction rollback; delete success, already deleted, protected record. + +## Notifications + +### NotificationController +**File:** `Api/Notifications/NotificationController.php` +**Purpose:** serve notification API responsibilities. +**Public methods:** `index(NotificationIndexRequest $request)`, `show(int $notificationId)`, `store(NotificationSendRequest $request)`, `update(NotificationUpdateRequest $request, int $notificationId)`, `destroy(int $notificationId)`, `restore(int $notificationId)`, `markRead(int $notificationId)`, `active(NotificationActiveRequest $request)`, `deleted(NotificationDeletedRequest $request)` +**Detected dependencies:** Services: `NotificationActiveService`, `NotificationDeletedService`, `NotificationManagementService`, `NotificationReadService`, `NotificationSendService`, `NotificationShowService`, `NotificationUserListService` | Requests: `NotificationActiveRequest`, `NotificationDeletedRequest`, `NotificationIndexRequest`, `NotificationSendRequest`, `NotificationUpdateRequest` | Resources: `NotificationResource`, `UserNotificationResource` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Authorization boundaries are the product, not decoration. Test forbidden paths first. +- Avoid leaking whether accounts, roles, or sessions exist unless intended. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Feature tests: list/filter pagination, empty result, invalid filter, tenant scoping; show success, missing record, unauthorized record; create/submit validation, happy path, duplicate submission; update authorization, partial payload, concurrency/transaction rollback; delete success, already deleted, protected record. + +## Parents + +### AuthorizedUserInviteController +**File:** `Api/Parents/AuthorizedUserInviteController.php` +**Purpose:** handle authentication/authorization workflow for authorized user invite. +**Public methods:** `confirm(Request $request)`, `setPasswordForm(Request $request, int $authorizedUserId)`, `savePassword(Request $request, int $authorizedUserId)` +**Detected dependencies:** Services: `AuthorizedUsersManagementService` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Add dedicated FormRequest classes where this still uses raw `Request`; inline validation is a trap wearing a tiny fake mustache. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Authorization boundaries are the product, not decoration. Test forbidden paths first. +- Avoid leaking whether accounts, roles, or sessions exist unless intended. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Feature tests: happy path, unauthorized path, invalid input, service exception; role matrix: allowed, denied, expired session. + +### AuthorizedUsersController +**File:** `Api/Parents/AuthorizedUsersController.php` +**Purpose:** handle authentication/authorization workflow for authorized users. +**Public methods:** `index()`, `show(int $id)`, `store(StoreAuthorizedUserRequest $request)`, `update(UpdateAuthorizedUserRequest $request, int $id)`, `destroy(int $id)` +**Detected dependencies:** Services: `AuthorizedUsersManagementService` | Requests: `StoreAuthorizedUserRequest`, `UpdateAuthorizedUserRequest` | Models: `AuthorizedUser` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Authorization boundaries are the product, not decoration. Test forbidden paths first. +- Avoid leaking whether accounts, roles, or sessions exist unless intended. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Feature tests: list/filter pagination, empty result, invalid filter, tenant scoping; show success, missing record, unauthorized record; create/submit validation, happy path, duplicate submission; update authorization, partial payload, concurrency/transaction rollback; delete success, already deleted, protected record; role matrix: allowed, denied, expired session. + +### ParentAttendanceReportController +**File:** `Api/Parents/ParentAttendanceReportController.php` +**Purpose:** manage attendance workflow for parent attendance report. +**Public methods:** `form()`, `submit(ParentAttendanceReportSubmitRequest $request)`, `list(ParentAttendanceReportListRequest $request)`, `checkExisting(ParentAttendanceReportCheckRequest $request)`, `update(ParentAttendanceReportUpdateRequest $request, int $reportId)` +**Detected dependencies:** Services: `ParentAttendanceReportService` | Requests: `ParentAttendanceReportSubmitRequest`, `ParentAttendanceReportListRequest`, `ParentAttendanceReportCheckRequest`, `ParentAttendanceReportUpdateRequest` | Resources: `ParentAttendanceReportResource` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Authorization boundaries are the product, not decoration. Test forbidden paths first. +- Avoid leaking whether accounts, roles, or sessions exist unless intended. +- Attendance changes need actor, timestamp, school term, and edit history. +- Define conflict behavior for duplicate scans or same-day edits. +- Feature tests: list/filter pagination, empty result, invalid filter, tenant scoping; create/submit validation, happy path, duplicate submission; update authorization, partial payload, concurrency/transaction rollback. + +### ParentController +**File:** `Api/Parents/ParentController.php` +**Purpose:** manage school identity and operations workflow for parent. +**Public methods:** `attendance(ParentAttendanceRequest $request)`, `invoices(ParentInvoiceRequest $request)`, `enrollments(ParentEnrollmentRequest $request)`, `updateEnrollments(ParentEnrollmentActionRequest $request)`, `registration()`, `storeRegistration(ParentRegistrationRequest $request)`, `updateStudent(ParentStudentUpdateRequest $request, int $studentId)`, `deleteStudent(int $studentId)`, `emergencyContacts()`, `storeEmergencyContact(ParentEmergencyContactRequest $request)`, `updateEmergencyContact(ParentEmergencyContactRequest $request, int $contactId)`, `profile()`, `updateProfile(ParentProfileUpdateRequest $request)`, `events()`, `updateParticipation(ParentEventParticipationRequest $request)` +**Detected dependencies:** Services: `ParentAttendanceService`, `ParentEmergencyContactService`, `ParentEnrollmentService`, `ParentEventParticipationService`, `ParentInvoiceService`, `ParentProfileService`, `ParentRegistrationService` | Requests: `ParentAttendanceRequest`, `ParentEnrollmentActionRequest`, `ParentEnrollmentRequest`, `ParentEmergencyContactRequest`, `ParentEventParticipationRequest`, `ParentInvoiceRequest`, `ParentProfileUpdateRequest`, `ParentRegistrationRequest`… | Resources: `ParentAttendanceResource`, `ParentEmergencyContactResource`, `ParentStudentResource`, `InvoiceResource` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Money operations must be idempotent, auditable, and transaction-safe. +- Never trust client-calculated totals; recompute server-side. +- Authorization boundaries are the product, not decoration. Test forbidden paths first. +- Avoid leaking whether accounts, roles, or sessions exist unless intended. +- Feature tests: happy path, unauthorized path, invalid input, service exception. + +### ParentPromotionController +**File:** `Api/Parents/ParentPromotionController.php` +**Purpose:** manage school identity and operations workflow for parent promotion. +**Public methods:** `overview(ParentPromotionOverviewRequest $request)`, `show(int $promotionId)`, `start(int $promotionId)`, `updateSteps(ParentEnrollmentStepRequest $request, int $promotionId)`, `submit(int $promotionId)` +**Detected dependencies:** Services: `PromotionEnrollmentService`, `PromotionQueryService` | Requests: `ParentEnrollmentStepRequest`, `ParentPromotionOverviewRequest` | Resources: `StudentPromotionResource` | Models: `Student`, `StudentPromotionRecord` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Authorization boundaries are the product, not decoration. Test forbidden paths first. +- Avoid leaking whether accounts, roles, or sessions exist unless intended. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Feature tests: show success, missing record, unauthorized record; create/submit validation, happy path, duplicate submission. + +## PrintRequests + +### PrintRequestsController +**File:** `Api/PrintRequests/PrintRequestsController.php` +**Purpose:** serve print requests API responsibilities. +**Public methods:** `teacher()`, `admin()`, `store(Request $request)`, `teacherUpdate(Request $request, int $id)`, `adminStatus(Request $request, int $id)`, `destroy(int $id)`, `copy(int $id)`, `handCopy(Request $request)`, `download(int $id)` +**Detected dependencies:** Services: `PrintRequestsPortalService` | Models: `PrintRequest`, `User` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Add dedicated FormRequest classes where this still uses raw `Request`; inline validation is a trap wearing a tiny fake mustache. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Authorization boundaries are the product, not decoration. Test forbidden paths first. +- Avoid leaking whether accounts, roles, or sessions exist unless intended. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Feature tests: create/submit validation, happy path, duplicate submission; delete success, already deleted, protected record. + +## Public + +### PublicWinnersController +**File:** `Api/Public/PublicWinnersController.php` +**Purpose:** serve public winners API responsibilities. +**Public methods:** `competitionIndex()`, `competitionShow(int $id)` +**Detected dependencies:** Services: `PublicWinnersPortalService` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Add dedicated FormRequest classes where this still uses raw `Request`; inline validation is a trap wearing a tiny fake mustache. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Rate-limit sends and record delivery state; retries must not spam families into oblivion. +- Sanitize templates and recipient selection. +- Feature tests: happy path, unauthorized path, invalid input, service exception. + +## Reports + +### FilesController +**File:** `Api/Reports/FilesController.php` +**Purpose:** serve files API responsibilities. +**Public methods:** `receipt(FileNameRequest $request, string $name)`, `reimb(FileNameRequest $request, string $name)`, `earlyDismissalSignature(FileNameRequest $request, string $name)`, `examDraftTeacher(FileNameRequest $request, string $name)`, `examDraftFinal(FileNameRequest $request, string $name)` +**Detected dependencies:** Services: `ExamDraftDownloadNameService`, `FileServeService` | Requests: `FileNameRequest` | Resources: `FileMetaResource` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Money operations must be idempotent, auditable, and transaction-safe. +- Never trust client-calculated totals; recompute server-side. +- Attendance changes need actor, timestamp, school term, and edit history. +- Define conflict behavior for duplicate scans or same-day edits. +- Feature tests: happy path, unauthorized path, invalid input, service exception. + +### ReportCardsController +**File:** `Api/Reports/ReportCardsController.php` +**Purpose:** serve report cards API responsibilities. +**Public methods:** `meta(ReportCardMetaRequest $request)`, `completeness(ReportCardCompletenessRequest $request)`, `acknowledgement(Request $request)`, `studentReport(ReportCardPdfRequest $request, int $studentId)`, `classReport(ReportCardPdfRequest $request, int $classSectionId)` +**Detected dependencies:** Services: `ReportCardService` | Requests: `ReportCardCompletenessRequest`, `ReportCardMetaRequest`, `ReportCardPdfRequest` | Resources: `ReportCardAcknowledgementResource`, `ReportCardClassSectionResource`, `ReportCardCompletenessStudentResource`, `ReportCardStudentMetaResource` | Models: `Configuration` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Validate file type, size, storage path, and access ownership. +- Generated documents need reproducible inputs and predictable naming. +- Feature tests: happy path, unauthorized path, invalid input, service exception. + +### SlipPrinterController +**File:** `Api/Reports/SlipPrinterController.php` +**Purpose:** serve slip printer API responsibilities. +**Public methods:** `print(SlipPrintRequest $request)`, `preview(SlipPreviewRequest $request)`, `logs(SlipLogListRequest $request)`, `reprint(SlipReprintRequest $request)` +**Detected dependencies:** Services: `SlipPrinterService` | Requests: `SlipLogListRequest`, `SlipPreviewRequest`, `SlipPrintRequest`, `SlipReprintRequest` | Resources: `LateSlipLogResource` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Authorization boundaries are the product, not decoration. Test forbidden paths first. +- Avoid leaking whether accounts, roles, or sessions exist unless intended. +- Attendance changes need actor, timestamp, school term, and edit history. +- Define conflict behavior for duplicate scans or same-day edits. +- Feature tests: happy path, unauthorized path, invalid input, service exception. + +### StickersController +**File:** `Api/Reports/StickersController.php` +**Purpose:** serve stickers API responsibilities. +**Public methods:** `formData(StickerFormDataRequest $request)`, `studentsByClass(StickerStudentsByClassRequest $request)`, `print(StickerPrintRequest $request)` +**Detected dependencies:** Services: `StickerPresetService`, `StickerPrintService`, `StickerQueryService` | Requests: `StickerFormDataRequest`, `StickerPrintRequest`, `StickerStudentsByClassRequest` | Resources: `StickerClassSectionResource`, `StickerPresetResource`, `StickerStudentResource` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Validate file type, size, storage path, and access ownership. +- Generated documents need reproducible inputs and predictable naming. +- Feature tests: happy path, unauthorized path, invalid input, service exception. + +## Root + +### BaseApiController +**File:** `Api/BaseApiController.php` +**Purpose:** shared API response, validation, auth, and compatibility foundation. +**Public methods:** _No public endpoint methods detected._ +**Detected dependencies:** Models: `User` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Add dedicated FormRequest classes where this still uses raw `Request`; inline validation is a trap wearing a tiny fake mustache. +- Extract business logic into service/action classes if the controller currently queries models or mutates state directly. +- Rate-limit sends and record delivery state; retries must not spam families into oblivion. +- Sanitize templates and recipient selection. +- This controller is large enough to hide bugs. Split orchestration from business logic before it becomes archaeology. +- Feature tests: happy path, unauthorized path, invalid input, service exception. + +### RolePermissionController +**File:** `Api/RolePermissionController.php` +**Purpose:** handle authentication/authorization workflow for role permission. +**Public methods:** `roles(RoleListRequest $request)`, `showRole(int $roleId)`, `storeRole(RoleStoreRequest $request)`, `updateRole(RoleUpdateRequest $request, int $roleId)`, `deleteRole(int $roleId)`, `users(UserRoleListRequest $request)`, `assignRoles(AssignUserRolesRequest $request, int $userId)`, `permissions()`, `showPermission(int $permissionId)`, `storePermission(PermissionStoreRequest $request)`, `updatePermission(PermissionUpdateRequest $request, int $permissionId)`, `deletePermission(int $permissionId)`, `rolePermissions(int $roleId)`, `updateRolePermissions(RolePermissionUpdateRequest $request, int $roleId)` +**Detected dependencies:** Services: `PermissionCrudService`, `RoleAssignmentService`, `RoleCrudService`, `RolePermissionService`, `RoleQueryService` | Requests: `AssignUserRolesRequest`, `PermissionStoreRequest`, `PermissionUpdateRequest`, `RoleListRequest`, `RolePermissionUpdateRequest`, `RoleStoreRequest`, `RoleUpdateRequest`, `UserRoleListRequest` | Resources: `PermissionResource`, `RolePermissionResource`, `RoleResource`, `UserWithRolesResource` | Models: `Permission`, `Role` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Authorization boundaries are the product, not decoration. Test forbidden paths first. +- Avoid leaking whether accounts, roles, or sessions exist unless intended. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Feature tests: update authorization, partial payload, concurrency/transaction rollback; role matrix: allowed, denied, expired session. + +### RoleSwitcherController +**File:** `Api/RoleSwitcherController.php` +**Purpose:** handle authentication/authorization workflow for role switcher. +**Public methods:** `index()`, `switch(RoleSwitchRequest $request)` +**Detected dependencies:** Services: `RoleSwitchService` | Requests: `RoleSwitchRequest` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Authorization boundaries are the product, not decoration. Test forbidden paths first. +- Avoid leaking whether accounts, roles, or sessions exist unless intended. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Feature tests: list/filter pagination, empty result, invalid filter, tenant scoping; role matrix: allowed, denied, expired session. + +### ScannerController +**File:** `Api/ScannerController.php` +**Purpose:** serve scanner API responsibilities. +**Public methods:** `process(Request $request)` +**Detected dependencies:** Models: `AttendanceData`, `AttendanceDay`, `AttendanceRecord`, `Configuration`, `CurrentFlag`, `LateSlipLog`, `Payment`, `SemesterScore`… + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Add dedicated FormRequest classes where this still uses raw `Request`; inline validation is a trap wearing a tiny fake mustache. +- Extract business logic into service/action classes if the controller currently queries models or mutates state directly. +- Money operations must be idempotent, auditable, and transaction-safe. +- Never trust client-calculated totals; recompute server-side. +- Authorization boundaries are the product, not decoration. Test forbidden paths first. +- Avoid leaking whether accounts, roles, or sessions exist unless intended. +- Feature tests: create/submit validation, happy path, duplicate submission. + +### StatsController +**File:** `Api/StatsController.php` +**Purpose:** provide CRUD API endpoints for stats. +**Public methods:** `index()` +**Detected dependencies:** Models: `Stats` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Add dedicated FormRequest classes where this still uses raw `Request`; inline validation is a trap wearing a tiny fake mustache. +- Extract business logic into service/action classes if the controller currently queries models or mutates state directly. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Rate-limit sends and record delivery state; retries must not spam families into oblivion. +- Sanitize templates and recipient selection. +- Feature tests: list/filter pagination, empty result, invalid filter, tenant scoping. + +### UserController +**File:** `Api/UserController.php` +**Purpose:** serve user API responsibilities. +**Public methods:** `index(UserListRequest $request)`, `store(UserStoreRequest $request)`, `update(UserUpdateRequest $request, int $userId)`, `destroy(int $userId)`, `loginActivity(LoginActivityRequest $request)` +**Detected dependencies:** Services: `LoginActivityService`, `UserListService`, `UserManagementService` | Requests: `LoginActivityRequest`, `UserListRequest`, `UserStoreRequest`, `UserUpdateRequest` | Resources: `LoginActivityResource`, `UserWithRolesResource` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Authorization boundaries are the product, not decoration. Test forbidden paths first. +- Avoid leaking whether accounts, roles, or sessions exist unless intended. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Feature tests: list/filter pagination, empty result, invalid filter, tenant scoping; create/submit validation, happy path, duplicate submission; update authorization, partial payload, concurrency/transaction rollback; delete success, already deleted, protected record. + +## Scores + +### FinalController +**File:** `Api/Scores/FinalController.php` +**Purpose:** manage academic scoring/grading workflow for final. +**Public methods:** `index(Request $request)`, `update(ScoreUpdateRequest $request)`, `bySchoolYear(Request $request)` +**Detected dependencies:** Services: `ExamScoreService`, `SemesterScoreService` | Requests: `ScoreUpdateRequest` | Resources: `ScoreStudentResource` | Models: `Student` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Authorization boundaries are the product, not decoration. Test forbidden paths first. +- Avoid leaking whether accounts, roles, or sessions exist unless intended. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Feature tests: list/filter pagination, empty result, invalid filter, tenant scoping; update authorization, partial payload, concurrency/transaction rollback. + +### HomeworkController +**File:** `Api/Scores/HomeworkController.php` +**Purpose:** manage academic scoring/grading workflow for homework. +**Public methods:** `index(Request $request)`, `update(ScoreUpdateRequest $request)`, `addColumn(ScoreAddColumnRequest $request)`, `bySchoolYear(Request $request)` +**Detected dependencies:** Services: `HomeworkScoreService`, `SemesterScoreService` | Requests: `ScoreAddColumnRequest`, `ScoreUpdateRequest` | Resources: `ScoreStudentResource` | Models: `Student` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Authorization boundaries are the product, not decoration. Test forbidden paths first. +- Avoid leaking whether accounts, roles, or sessions exist unless intended. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Feature tests: list/filter pagination, empty result, invalid filter, tenant scoping; update authorization, partial payload, concurrency/transaction rollback. + +### MidtermController +**File:** `Api/Scores/MidtermController.php` +**Purpose:** manage academic scoring/grading workflow for midterm. +**Public methods:** `index(Request $request)`, `update(ScoreUpdateRequest $request)`, `bySchoolYear(Request $request)` +**Detected dependencies:** Services: `ExamScoreService`, `SemesterScoreService` | Requests: `ScoreUpdateRequest` | Resources: `ScoreStudentResource` | Models: `Student` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Authorization boundaries are the product, not decoration. Test forbidden paths first. +- Avoid leaking whether accounts, roles, or sessions exist unless intended. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Feature tests: list/filter pagination, empty result, invalid filter, tenant scoping; update authorization, partial payload, concurrency/transaction rollback. + +### ParticipationController +**File:** `Api/Scores/ParticipationController.php` +**Purpose:** manage academic scoring/grading workflow for participation. +**Public methods:** `index(Request $request)`, `update(ScoreUpdateRequest $request)`, `bySchoolYear(Request $request)` +**Detected dependencies:** Services: `ParticipationScoreService`, `SemesterScoreService` | Requests: `ScoreUpdateRequest` | Resources: `ScoreStudentResource` | Models: `Student` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Authorization boundaries are the product, not decoration. Test forbidden paths first. +- Avoid leaking whether accounts, roles, or sessions exist unless intended. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Feature tests: list/filter pagination, empty result, invalid filter, tenant scoping; update authorization, partial payload, concurrency/transaction rollback. + +### ProjectController +**File:** `Api/Scores/ProjectController.php` +**Purpose:** manage academic scoring/grading workflow for project. +**Public methods:** `index(Request $request)`, `update(ScoreUpdateRequest $request)`, `addColumn(ScoreAddColumnRequest $request)`, `bySchoolYear(Request $request)` +**Detected dependencies:** Services: `ProjectScoreService`, `SemesterScoreService` | Requests: `ScoreAddColumnRequest`, `ScoreUpdateRequest` | Resources: `ScoreStudentResource` | Models: `Student` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Authorization boundaries are the product, not decoration. Test forbidden paths first. +- Avoid leaking whether accounts, roles, or sessions exist unless intended. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Feature tests: list/filter pagination, empty result, invalid filter, tenant scoping; update authorization, partial payload, concurrency/transaction rollback. + +### QuizController +**File:** `Api/Scores/QuizController.php` +**Purpose:** manage academic scoring/grading workflow for quiz. +**Public methods:** `index(Request $request)`, `update(ScoreUpdateRequest $request)`, `addColumn(ScoreAddColumnRequest $request)`, `bySchoolYear(Request $request)` +**Detected dependencies:** Services: `QuizScoreService`, `SemesterScoreService` | Requests: `ScoreAddColumnRequest`, `ScoreUpdateRequest` | Resources: `ScoreStudentResource` | Models: `Student` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Authorization boundaries are the product, not decoration. Test forbidden paths first. +- Avoid leaking whether accounts, roles, or sessions exist unless intended. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Feature tests: list/filter pagination, empty result, invalid filter, tenant scoping; update authorization, partial payload, concurrency/transaction rollback. + +### ScoreCommentController +**File:** `Api/Scores/ScoreCommentController.php` +**Purpose:** manage academic scoring/grading workflow for score comment. +**Public methods:** `index(ScoreCommentListRequest $request)`, `store(ScoreCommentSaveRequest $request)`, `update(ScoreCommentUpdateRequest $request)` +**Detected dependencies:** Services: `ScoreCommentService` | Requests: `ScoreCommentListRequest`, `ScoreCommentSaveRequest`, `ScoreCommentUpdateRequest` | Resources: `ScoreCommentResource` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Mutating endpoints need request validation, service-layer ownership, and database transactions where multiple rows change. +- Feature tests: list/filter pagination, empty result, invalid filter, tenant scoping; create/submit validation, happy path, duplicate submission; update authorization, partial payload, concurrency/transaction rollback. + +### ScoreController +**File:** `Api/Scores/ScoreController.php` +**Purpose:** manage academic scoring/grading workflow for score. +**Public methods:** `overview(ScoreOverviewRequest $request)`, `lock(ScoreLockRequest $request)`, `studentScores(ScoreStudentRequest $request)` +**Detected dependencies:** Services: `ScoreDashboardService` | Requests: `ScoreLockRequest`, `ScoreOverviewRequest`, `ScoreStudentRequest` | Resources: `ScoreStudentResource` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Authorization boundaries are the product, not decoration. Test forbidden paths first. +- Avoid leaking whether accounts, roles, or sessions exist unless intended. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Feature tests: happy path, unauthorized path, invalid input, service exception. + +### ScorePredictorController +**File:** `Api/Scores/ScorePredictorController.php` +**Purpose:** manage academic scoring/grading workflow for score predictor. +**Public methods:** `index(ScorePredictorRequest $request)` +**Detected dependencies:** Services: `ScorePredictorService` | Requests: `ScorePredictorRequest` | Resources: `ScorePredictorStudentResource` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Validate file type, size, storage path, and access ownership. +- Generated documents need reproducible inputs and predictable naming. +- Feature tests: list/filter pagination, empty result, invalid filter, tenant scoping. + +## Settings + +### ConfigurationAdminController +**File:** `Api/Settings/ConfigurationAdminController.php` +**Purpose:** serve configuration admin API responsibilities. +**Public methods:** `index()`, `store(ConfigurationStoreRequest $request)`, `update(ConfigurationUpdateRequest $request, int $id)`, `destroy(int $id)`, `getByKey(string $configKey)` +**Detected dependencies:** Services: `ConfigurationService` | Requests: `ConfigurationStoreRequest`, `ConfigurationUpdateRequest` | Resources: `ConfigurationResource` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Rate-limit sends and record delivery state; retries must not spam families into oblivion. +- Sanitize templates and recipient selection. +- Feature tests: list/filter pagination, empty result, invalid filter, tenant scoping; create/submit validation, happy path, duplicate submission; update authorization, partial payload, concurrency/transaction rollback; delete success, already deleted, protected record. + +### EventController +**File:** `Api/Settings/EventController.php` +**Purpose:** serve event API responsibilities. +**Public methods:** `index(EventIndexRequest $request)`, `show(int $eventId)`, `store(EventStoreRequest $request)`, `update(EventUpdateRequest $request, int $eventId)`, `destroy(int $eventId)`, `charges(EventChargeIndexRequest $request)`, `updateCharges(EventChargeUpdateRequest $request, int $eventId)`, `studentsWithCharges(EventStudentsWithChargesRequest $request)` +**Detected dependencies:** Services: `EventCategoryService`, `EventChargeQueryService`, `EventChargeService`, `EventListService`, `EventManagementService`, `EventStudentChargeService` | Requests: `EventChargeIndexRequest`, `EventChargeUpdateRequest`, `EventIndexRequest`, `EventStoreRequest`, `EventStudentsWithChargesRequest`, `EventUpdateRequest` | Resources: `EventChargeResource`, `EventResource`, `EventStudentChargeResource` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Money operations must be idempotent, auditable, and transaction-safe. +- Never trust client-calculated totals; recompute server-side. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Feature tests: list/filter pagination, empty result, invalid filter, tenant scoping; show success, missing record, unauthorized record; create/submit validation, happy path, duplicate submission; update authorization, partial payload, concurrency/transaction rollback; delete success, already deleted, protected record. + +### PolicyController +**File:** `Api/Settings/PolicyController.php` +**Purpose:** serve policy API responsibilities. +**Public methods:** `school(PolicyShowRequest $request)`, `picture(PolicyShowRequest $request)` +**Detected dependencies:** Services: `PolicyContentService` | Requests: `PolicyShowRequest` | Resources: `PolicyResource` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Feature tests: happy path, unauthorized path, invalid input, service exception. + +### PreferencesController +**File:** `Api/Settings/PreferencesController.php` +**Purpose:** serve preferences API responsibilities. +**Public methods:** `index(PreferencesIndexRequest $request)`, `show()`, `showForUser(int $userId)`, `store(PreferencesUpsertRequest $request)`, `updateForUser(PreferencesUpsertRequest $request, int $userId)`, `destroy(int $userId)` +**Detected dependencies:** Services: `PreferencesCommandService`, `PreferencesQueryService` | Requests: `PreferencesIndexRequest`, `PreferencesUpsertRequest` | Resources: `PreferencesCollection`, `PreferencesResource` | Models: `Preferences` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Authorization boundaries are the product, not decoration. Test forbidden paths first. +- Avoid leaking whether accounts, roles, or sessions exist unless intended. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Feature tests: list/filter pagination, empty result, invalid filter, tenant scoping; show success, missing record, unauthorized record; create/submit validation, happy path, duplicate submission; delete success, already deleted, protected record. + +### SchoolCalendarController +**File:** `Api/Settings/SchoolCalendarController.php` +**Purpose:** serve school calendar API responsibilities. +**Public methods:** `options(SchoolCalendarOptionsRequest $request)`, `index(SchoolCalendarIndexRequest $request)`, `teacherCalendarLegacy(SchoolCalendarIndexRequest $request)`, `show(int $eventId)`, `store(SchoolCalendarStoreRequest $request)`, `update(SchoolCalendarUpdateRequest $request, int $eventId)`, `destroy(int $eventId)` +**Detected dependencies:** Services: `SchoolCalendarContextService`, `SchoolCalendarFormatterService`, `SchoolCalendarMeetingService`, `SchoolCalendarMutationService`, `SchoolCalendarQueryService` | Requests: `SchoolCalendarIndexRequest`, `SchoolCalendarOptionsRequest`, `SchoolCalendarStoreRequest`, `SchoolCalendarUpdateRequest` | Resources: `SchoolCalendarEventDetailResource`, `SchoolCalendarEventResource` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Mutating endpoints need request validation, service-layer ownership, and database transactions where multiple rows change. +- Feature tests: list/filter pagination, empty result, invalid filter, tenant scoping; show success, missing record, unauthorized record; create/submit validation, happy path, duplicate submission; update authorization, partial payload, concurrency/transaction rollback; delete success, already deleted, protected record. + +### SettingsController +**File:** `Api/Settings/SettingsController.php` +**Purpose:** provide CRUD API endpoints for settings. +**Public methods:** `index()`, `update(SettingsUpdateRequest $request)` +**Detected dependencies:** Services: `SettingsService` | Requests: `SettingsUpdateRequest` | Resources: `SettingsResource` | Models: `Setting` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Authorization boundaries are the product, not decoration. Test forbidden paths first. +- Avoid leaking whether accounts, roles, or sessions exist unless intended. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Feature tests: list/filter pagination, empty result, invalid filter, tenant scoping; update authorization, partial payload, concurrency/transaction rollback. + +## Staff + +### StaffController +**File:** `Api/Staff/StaffController.php` +**Purpose:** manage school identity and operations workflow for staff. +**Public methods:** `index(StaffIndexRequest $request)`, `show(int $id)`, `store(StaffStoreRequest $request)`, `update(StaffUpdateRequest $request, int $id)`, `destroy(int $id)` +**Detected dependencies:** Services: `StaffCommandService`, `StaffQueryService` | Requests: `StaffIndexRequest`, `StaffStoreRequest`, `StaffUpdateRequest` | Resources: `StaffCollection`, `StaffResource` | Models: `Staff` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Authorization boundaries are the product, not decoration. Test forbidden paths first. +- Avoid leaking whether accounts, roles, or sessions exist unless intended. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Feature tests: list/filter pagination, empty result, invalid filter, tenant scoping; show success, missing record, unauthorized record; create/submit validation, happy path, duplicate submission; update authorization, partial payload, concurrency/transaction rollback; delete success, already deleted, protected record. + +### TeacherController +**File:** `Api/Staff/TeacherController.php` +**Purpose:** manage school identity and operations workflow for teacher. +**Public methods:** `classes(TeacherClassViewRequest $request)`, `switchClass(TeacherSwitchClassRequest $request)`, `absenceForm()`, `submitAbsence(TeacherAbsenceSubmitRequest $request)` +**Detected dependencies:** Services: `TeacherAbsenceService`, `TeacherDashboardService` | Requests: `TeacherAbsenceSubmitRequest`, `TeacherClassViewRequest`, `TeacherSwitchClassRequest` | Resources: `TeacherAbsenceResource`, `TeacherClassResource`, `TeacherStudentResource` | Models: `User` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Authorization boundaries are the product, not decoration. Test forbidden paths first. +- Avoid leaking whether accounts, roles, or sessions exist unless intended. +- Attendance changes need actor, timestamp, school term, and edit history. +- Define conflict behavior for duplicate scans or same-day edits. +- Feature tests: happy path, unauthorized path, invalid input, service exception. + +### TimeOffNotificationController +**File:** `Api/Staff/TimeOffNotificationController.php` +**Purpose:** manage school identity and operations workflow for time off notification. +**Public methods:** `notify(string $token)` +**Detected dependencies:** Services: `EmailDispatchService`, `StaffTimeOffLinkService` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Add dedicated FormRequest classes where this still uses raw `Request`; inline validation is a trap wearing a tiny fake mustache. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Authorization boundaries are the product, not decoration. Test forbidden paths first. +- Avoid leaking whether accounts, roles, or sessions exist unless intended. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Feature tests: happy path, unauthorized path, invalid input, service exception. + +## Students + +### StudentController +**File:** `Api/Students/StudentController.php` +**Purpose:** manage school identity and operations workflow for student. +**Public methods:** `assignments(StudentAssignmentListRequest $request)`, `index(Request $request)`, `scoreCardSelectable(Request $request)`, `show(int $studentId)`, `store(Request $request)`, `assignClass(StudentAssignClassRequest $request)`, `removeClass(StudentRemoveClassRequest $request)`, `removed(StudentAssignmentListRequest $request)`, `setActive(StudentSetActiveRequest $request)`, `autoDistribute(StudentAutoDistributeRequest $request)`, `promotionTotals(StudentPromotionTotalsRequest $request)`, `update(StudentUpdateRequest $request, int $studentId)`, `destroy(int $studentId)`, `classes(Request $request, int $studentId)`, `assignClassForStudent(Request $request, int $studentId)`, `removeClassForStudent(int $studentId, int $classSectionId)`, `promote(Request $request, int $studentId)`, `attendance(Request $request, int $studentId)`, `incidents(Request $request, int $studentId)`, `scores(Request $request, int $studentId)`, `notes(int $studentId)`, `parents(int $studentId)`, `emergencyContacts(int $studentId)`, `addEmergencyContact(Request $request, int $studentId)`, `updateEmergencyContact(Request $request, int $studentId, int $contactId)`, `updateBadgeScan(Request $request, int $studentId)`, `uploadPhoto(Request $request, int $studentId)`, `photo(int $studentId)`, `scoreCard(int $studentId)` +**Detected dependencies:** Services: `StudentAssignmentService`, `StudentAutoDistributionService`, `StudentDirectoryService`, `StudentProfileService`, `StudentScoreCardService`, `StudentStatusService` | Requests: `StudentAssignClassRequest`, `StudentAssignmentListRequest`, `StudentAutoDistributeRequest`, `StudentPromotionTotalsRequest`, `StudentRemoveClassRequest`, `StudentSetActiveRequest`, `StudentUpdateRequest` | Resources: `StudentAssignmentResource`, `StudentClassSectionResource`, `StudentRemovedResource`, `StudentScoreCardRowResource` | Models: `AttendanceRecord`, `ClassSection`, `EmergencyContact`, `Incident`, `SemesterScore`, `Student`, `StudentClass`, `User` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Authorization boundaries are the product, not decoration. Test forbidden paths first. +- Avoid leaking whether accounts, roles, or sessions exist unless intended. +- Attendance changes need actor, timestamp, school term, and edit history. +- Define conflict behavior for duplicate scans or same-day edits. +- Feature tests: list/filter pagination, empty result, invalid filter, tenant scoping; show success, missing record, unauthorized record; create/submit validation, happy path, duplicate submission; update authorization, partial payload, concurrency/transaction rollback; delete success, already deleted, protected record. + +## Subjects + +### SubjectCurriculumController +**File:** `Api/Subjects/SubjectCurriculumController.php` +**Purpose:** provide CRUD API endpoints for subject curriculum. +**Public methods:** `index()`, `show(int $id)`, `store(SubjectCurriculumStoreRequest $request)`, `update(SubjectCurriculumUpdateRequest $request, int $id)`, `destroy(int $id)` +**Detected dependencies:** Services: `SubjectCurriculumService` | Requests: `SubjectCurriculumStoreRequest`, `SubjectCurriculumUpdateRequest` | Resources: `SubjectClassResource`, `SubjectCurriculumResource` | Models: `SubjectCurriculum` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Rate-limit sends and record delivery state; retries must not spam families into oblivion. +- Sanitize templates and recipient selection. +- Feature tests: list/filter pagination, empty result, invalid filter, tenant scoping; show success, missing record, unauthorized record; create/submit validation, happy path, duplicate submission; update authorization, partial payload, concurrency/transaction rollback; delete success, already deleted, protected record. + +## Support + +### ContactController +**File:** `Api/Support/ContactController.php` +**Purpose:** serve contact API responsibilities. +**Public methods:** `send(ContactSendRequest $request)` +**Detected dependencies:** Services: `ContactMessageService` | Requests: `ContactSendRequest` | Resources: `ContactMessageResource` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Rate-limit sends and record delivery state; retries must not spam families into oblivion. +- Sanitize templates and recipient selection. +- Feature tests: happy path, unauthorized path, invalid input, service exception. + +### SupportController +**File:** `Api/Support/SupportController.php` +**Purpose:** serve support API responsibilities. +**Public methods:** `index(SupportRequestIndexRequest $request)`, `store(SupportRequestStoreRequest $request)`, `teacher()` +**Detected dependencies:** Services: `SupportRequestService` | Requests: `SupportRequestIndexRequest`, `SupportRequestStoreRequest` | Resources: `SupportRequestCollection`, `SupportRequestResource` | Models: `SupportRequest` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Authorization boundaries are the product, not decoration. Test forbidden paths first. +- Avoid leaking whether accounts, roles, or sessions exist unless intended. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Feature tests: list/filter pagination, empty result, invalid filter, tenant scoping; create/submit validation, happy path, duplicate submission. + +## System + +### AccessDeniedController +**File:** `Api/System/AccessDeniedController.php` +**Purpose:** serve access denied API responsibilities. +**Public methods:** `accessDenied()` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Add dedicated FormRequest classes where this still uses raw `Request`; inline validation is a trap wearing a tiny fake mustache. +- Extract business logic into service/action classes if the controller currently queries models or mutates state directly. +- Rate-limit sends and record delivery state; retries must not spam families into oblivion. +- Sanitize templates and recipient selection. +- Feature tests: happy path, unauthorized path, invalid input, service exception. + +### DashboardController +**File:** `Api/System/DashboardController.php` +**Purpose:** serve dashboard API responsibilities. +**Public methods:** `route()` +**Detected dependencies:** Services: `DashboardRouteService` | Resources: `DashboardRouteResource` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Add dedicated FormRequest classes where this still uses raw `Request`; inline validation is a trap wearing a tiny fake mustache. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Authorization boundaries are the product, not decoration. Test forbidden paths first. +- Avoid leaking whether accounts, roles, or sessions exist unless intended. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Feature tests: happy path, unauthorized path, invalid input, service exception. + +### DashboardRedirectController +**File:** `Api/System/DashboardRedirectController.php` +**Purpose:** serve dashboard redirect API responsibilities. +**Public methods:** `dashboard()` +**Detected dependencies:** Services: `ApplicationUrlService`, `DashboardRouteService` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Add dedicated FormRequest classes where this still uses raw `Request`; inline validation is a trap wearing a tiny fake mustache. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Authorization boundaries are the product, not decoration. Test forbidden paths first. +- Avoid leaking whether accounts, roles, or sessions exist unless intended. +- Feature tests: happy path, unauthorized path, invalid input, service exception. + +### DatabaseHealthController +**File:** `Api/System/DatabaseHealthController.php` +**Purpose:** provide CRUD API endpoints for database health. +**Public methods:** `index()` +**Detected dependencies:** Services: `DatabaseHealthService` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Add dedicated FormRequest classes where this still uses raw `Request`; inline validation is a trap wearing a tiny fake mustache. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Feature tests: list/filter pagination, empty result, invalid filter, tenant scoping. + +### HealthController +**File:** `Api/System/HealthController.php` +**Purpose:** provide CRUD API endpoints for health. +**Public methods:** `index()` +**Detected dependencies:** Services: `HealthCheckService` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Add dedicated FormRequest classes where this still uses raw `Request`; inline validation is a trap wearing a tiny fake mustache. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Feature tests: list/filter pagination, empty result, invalid filter, tenant scoping. + +### NavBuilderController +**File:** `Api/System/NavBuilderController.php` +**Purpose:** serve nav builder API responsibilities. +**Public methods:** `menu()`, `data()`, `store(NavItemStoreRequest $request)`, `destroy(int $id)`, `reorder(NavItemReorderRequest $request)` +**Detected dependencies:** Services: `NavBuilderService`, `NavbarService` | Requests: `NavItemReorderRequest`, `NavItemStoreRequest` | Resources: `NavBuilderDataResource`, `NavItemResource` | Models: `NavItem` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Authorization boundaries are the product, not decoration. Test forbidden paths first. +- Avoid leaking whether accounts, roles, or sessions exist unless intended. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Feature tests: create/submit validation, happy path, duplicate submission; delete success, already deleted, protected record. + +### SchoolIdController +**File:** `Api/System/SchoolIdController.php` +**Purpose:** serve school id API responsibilities. +**Public methods:** `assign(SchoolIdAssignRequest $request)`, `generateStudent()`, `generateUser()` +**Detected dependencies:** Services: `SchoolIdAssignmentService`, `SchoolIdGenerationService` | Requests: `SchoolIdAssignRequest` | Resources: `SchoolIdResource` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Rate-limit sends and record delivery state; retries must not spam families into oblivion. +- Sanitize templates and recipient selection. +- Feature tests: happy path, unauthorized path, invalid input, service exception. + +### SemesterRangeController +**File:** `Api/System/SemesterRangeController.php` +**Purpose:** serve semester range API responsibilities. +**Public methods:** `schoolYearRange(SchoolYearRangeRequest $request)`, `semesterRange(SemesterRangeRequest $request)`, `normalize(SemesterNormalizeRequest $request)`, `resolve(SemesterResolveRequest $request)` +**Detected dependencies:** Services: `SemesterRangeService` | Requests: `SchoolYearRangeRequest`, `SemesterNormalizeRequest`, `SemesterRangeRequest`, `SemesterResolveRequest` | Resources: `SemesterRangeResource`, `SemesterResolveResource` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Rate-limit sends and record delivery state; retries must not spam families into oblivion. +- Sanitize templates and recipient selection. +- Feature tests: happy path, unauthorized path, invalid input, service exception. + +### StatsController +**File:** `Api/System/StatsController.php` +**Purpose:** provide CRUD API endpoints for stats. +**Public methods:** `index()` +**Detected dependencies:** Models: `Stats` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Add dedicated FormRequest classes where this still uses raw `Request`; inline validation is a trap wearing a tiny fake mustache. +- Extract business logic into service/action classes if the controller currently queries models or mutates state directly. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Rate-limit sends and record delivery state; retries must not spam families into oblivion. +- Sanitize templates and recipient selection. +- Feature tests: list/filter pagination, empty result, invalid filter, tenant scoping. + +## Ui + +### UiController +**File:** `Api/Ui/UiController.php` +**Purpose:** serve ui API responsibilities. +**Public methods:** `style(UiStyleRequest $request)` +**Detected dependencies:** Services: `UiStyleService` | Requests: `UiStyleRequest` | Resources: `UiStyleResource` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Authorization boundaries are the product, not decoration. Test forbidden paths first. +- Avoid leaking whether accounts, roles, or sessions exist unless intended. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Feature tests: happy path, unauthorized path, invalid input, service exception. + +## Users + +### UserController +**File:** `Api/Users/UserController.php` +**Purpose:** serve user API responsibilities. +**Public methods:** `index(UserListRequest $request)`, `store(UserStoreRequest $request)`, `show(int $userId)`, `update(UserUpdateRequest $request, int $userId)`, `destroy(int $userId)`, `loginActivity(LoginActivityRequest $request)` +**Detected dependencies:** Services: `LoginActivityService`, `UserListService`, `UserManagementService` | Requests: `LoginActivityRequest`, `UserListRequest`, `UserStoreRequest`, `UserUpdateRequest` | Resources: `LoginActivityResource`, `UserWithRolesResource` | Models: `User` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Authorization boundaries are the product, not decoration. Test forbidden paths first. +- Avoid leaking whether accounts, roles, or sessions exist unless intended. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Feature tests: list/filter pagination, empty result, invalid filter, tenant scoping; show success, missing record, unauthorized record; create/submit validation, happy path, duplicate submission; update authorization, partial payload, concurrency/transaction rollback; delete success, already deleted, protected record. + +## Utilities + +### PhoneFormatterController +**File:** `Api/Utilities/PhoneFormatterController.php` +**Purpose:** serve phone formatter API responsibilities. +**Public methods:** `format(PhoneFormatRequest $request)` +**Detected dependencies:** Services: `PhoneFormatterService` | Requests: `PhoneFormatRequest` | Resources: `PhoneFormatResource` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Keep validation in existing FormRequest classes; check required fields, enum values, date ranges, IDs, file constraints, and cross-field rules. +- Keep controller as orchestration only; move rules/calculation/query composition into the detected service layer and unit-test those services directly. +- Scope every query by school/tenant and role. Humans love accidental data leaks. +- Protect PII in logs, exports, and error payloads. +- Feature tests: happy path, unauthorized path, invalid input, service exception. + +### ProofreadController +**File:** `Api/Utilities/ProofreadController.php` +**Purpose:** serve proofread API responsibilities. +**Public methods:** `check(Request $request)` + +**Plan** +- Define endpoint contract for each public method: request fields, response resource, status codes, and failure shape. +- Add dedicated FormRequest classes where this still uses raw `Request`; inline validation is a trap wearing a tiny fake mustache. +- Extract business logic into service/action classes if the controller currently queries models or mutates state directly. +- Feature tests: happy path, unauthorized path, invalid input, service exception. diff --git a/docs/modular_plans/.DS_Store b/docs/modular_plans/.DS_Store new file mode 100644 index 00000000..ace80cd1 Binary files /dev/null and b/docs/modular_plans/.DS_Store differ diff --git a/docs/modular_plans/app_project_plan_global_modular_islamic.md b/docs/modular_plans/app_project_plan_global_modular_islamic.md new file mode 100644 index 00000000..d2298a58 --- /dev/null +++ b/docs/modular_plans/app_project_plan_global_modular_islamic.md @@ -0,0 +1,2308 @@ +# App Project Implementation Plan + + +# Islamic Sunday School Alignment Update + +This plan now treats the extension domain as **Islamic Sunday School**, not a generic or church-oriented Sunday School. + +The reusable core remains `SchoolCore`. Islamic Sunday School behavior belongs in `IslamicSundaySchool` through contracts, policies, resolvers, read models, and extension profile services. + +Core must stay neutral. It must not contain Islamic Sunday School-specific concepts such as Qur'an level, Arabic level, halaqa, masjid family profile, volunteer team, imam/admin notes, Jumu'ah/program session, or Islamic studies track. Those belong in the extension layer. + +Preferred namespace: + +```text +App\Domain\IslamicSundaySchool +``` + +Legacy wording using `SundaySchool` should be treated as a migration target and renamed to `IslamicSundaySchool` as code is touched. + +--- +Generated: 2026-05-29 + +## 1. Scope and Readout + +This plan is based on the uploaded `app/` project slice. The zip contains the Laravel-style application layer, not the full repository. That means routes, migrations, config outside `app/`, composer metadata, tests, CI, frontend assets, and deployment files were not available unless embedded in this directory. So the plan avoids pretending the missing pieces magically exist, a level of honesty software planning could use more often. + +### Inventory + +- Real project files inspected: **1184** +- PHP files inspected: **1183** +- Approximate PHP LOC: **103,161** +- Controllers: **129** +- Models: **133** +- Services: **386** +- Form requests: **298** +- API resources: **167** +- Middleware: **12** +- Console commands: **14** +- Policies: **10** + + +## 2. Project Thesis + +The app is already organized around a service-heavy backend: controllers orchestrate, request classes validate, resources shape responses, and services carry domain logic. That is the right direction. The weak point is not that the code lacks structure. The weak point is that the project is broad enough for rules to drift across domains unless the boundaries are enforced deliberately. + +The plan should therefore optimize for correctness and regression safety, not cosmetic refactors. Refactoring without tests here would be performance art with a stack trace. + + + +## 3. Main Architecture Plan + +### Target shape + +- Controllers stay thin: authorize, validate, call one service action, return one resource/response. +- Services own business rules and transactions. +- Form requests own validation and request normalization. +- Resources own response shape and avoid leaking internal model structure. +- Policies/Gates own permission checks. +- Events/listeners own async or side-effect workflows, especially communications. +- Console commands remain operational entry points only, not hidden domain logic. + +### Global school core with domain extensions + +The service layer must be redesigned as a reusable school platform, not as a Islamic Sunday School application with reusable-looking class names. A name like `StudentService` is not modular if it silently assumes masjid/community terms, semester rules, ministry roles, Islamic Sunday School attendance, or a single grading model. That is just hardcoding wearing a blazer. + +Target architecture: + +```text +app/ + Domain/ + SchoolCore/ + Identity/ + Students/ + Guardians/ + Staff/ + Enrollment/ + Attendance/ + Academics/ + Finance/ + Communication/ + Files/ + Reporting/ + Authorization/ + IslamicSundaySchool/ + Attendance/ + Classes/ + Scripture/ + MinistryRoles/ + LiturgicalCalendar/ + Extensions/ + Shared/ + Contracts/ + DTOs/ + ValueObjects/ + Events/ + Exceptions/ + Support/ +``` + +The `SchoolCore` domain defines generic behavior that any school system can use: students, guardians, staff, enrollment, attendance, classes, grades, invoices, payments, files, communication, and reporting. The `IslamicSundaySchool` domain may extend or configure those behaviors, but it must not leak Islamic Sunday School-specific rules backward into the core. + +Required dependency direction: + +```text +Controllers -> Application Services -> Domain Contracts -> Infrastructure +IslamicSundaySchool -> SchoolCore +SchoolCore -X-> IslamicSundaySchool +``` + +The core must never import from `IslamicSundaySchool`. If that rule is broken, the platform is not modular; it is a themed monolith pretending to be reusable because humans enjoy labels. + +### Service modularity rules + +- Services must expose use-case actions, not generic god-methods. Prefer `RecordPaymentAction`, `EnrollStudentAction`, `MarkAttendanceAction`, `PromoteStudentAction` over giant services with unrelated methods. +- Core services must depend on contracts/interfaces for school-specific rules. Examples: `AttendancePolicy`, `AcademicCalendarProvider`, `GradeScalePolicy`, `TuitionPolicy`, `StudentIdentifierGenerator`, `CommunicationPreferencePolicy`. +- Islamic Sunday School behavior must be implemented as concrete policies/configuration/extensions plugged into those contracts. +- Domain concepts must use neutral names in the core: `ClassGroup`, `AcademicTerm`, `Session`, `Guardian`, `StaffMember`, `AttendanceOccurrence`, `Learner`, `Program`. Islamic Sunday School-specific aliases can exist in the extension layer. +- Every service must declare whether it is `Core`, `Extension`, `Infrastructure`, or `LegacyShim`. +- A service may not read raw request data, return HTTP responses, or know controller/resource classes. That belongs at the API boundary. +- A service may not call unrelated modules directly when a domain event or contract is the safer boundary. Finance should not directly decide messaging behavior; it should emit `PaymentRecorded` and let communication listeners handle notifications. +- All module-specific defaults must come from configuration or policy classes, not hardcoded assumptions. + +### Inheritance and extension strategy + +Use inheritance sparingly. The Islamic Sunday School concept should inherit from the global project only where the base behavior is stable and narrow. For business rules, prefer composition through policy contracts. Inheritance looks elegant until the third override turns it into a family curse. + +Allowed: + +```php +interface AttendancePolicy +{ + public function resolveStatus(AttendanceContext $context): AttendanceDecision; +} + +final class StandardSchoolAttendancePolicy implements AttendancePolicy {} +final class IslamicSundaySchoolAttendancePolicy implements AttendancePolicy {} +``` + +Acceptable but limited: + +```php +abstract class BaseAttendanceService +{ + final public function mark(AttendanceCommand $command): AttendanceResult + { + // shared transaction, audit, idempotency, persistence orchestration + } + + abstract protected function policy(): AttendancePolicy; +} + +final class IslamicSundaySchoolAttendanceService extends BaseAttendanceService +{ + protected function policy(): AttendancePolicy + { + return app(IslamicSundaySchoolAttendancePolicy::class); + } +} +``` + +Forbidden: + +```php +class CoreAttendanceService +{ + public function mark($student, $date) + { + if ($student->islamic_sunday_school_class_id) { + // Islamic Sunday School-specific logic inside core. No. + } + } +} +``` + +### Multi-school reliability model + +To make the app usable by any school system, every major record must be scoped to a school, program, or tenant boundary. The plan must verify this before refactoring services, because beautiful modular code that leaks one school’s data to another is still garbage, only now with namespaces. + +Required checks: + +- Define canonical scope fields: `school_id`, `program_id`, `tenant_id`, or equivalent. +- Every query in core services must apply the correct scope through a shared `SchoolContext`. +- Policies must verify both actor permission and school/program ownership. +- Background jobs, reports, exports, file downloads, and notifications must carry the same context explicitly. +- Add tests proving cross-school isolation for students, guardians, staff, payments, attendance, grades, files, and messages. + +### Service package boundaries + +Each major service package must have four layers: + +1. `Contracts`: interfaces and extension points. +2. `Application`: use-case services/actions and transaction orchestration. +3. `Domain`: value objects, decisions, rules, events, exceptions. +4. `Infrastructure`: Eloquent repositories, storage, mail/SMS providers, external adapters. + +Controllers may use Application services only. Application services may use Domain and Contracts. Infrastructure implements Contracts. Domain must not depend on Laravel HTTP, controllers, requests, or resources. + + +### Non-negotiable rules + +- No money-changing action without an explicit transaction, audit trail, idempotency check, and test. +- No attendance, score, promotion, or enrollment mutation without actor tracking and date boundary tests. +- No file endpoint without MIME, size, storage path, authorization, and download-name tests. +- No communication endpoint without recipient preview, opt-out logic, deduplication, and send-log tests. +- No direct request parsing in controllers where a FormRequest already exists or should exist. +- No raw SQL without parameter binding review and a test proving the query behavior. +- No core service may import or reference a Islamic Sunday School namespace, table-specific shortcut, or Islamic Sunday School-specific concept. +- No reusable service is complete until it has a contract, DTO/value object input, authorization boundary, transaction rule, audit rule, and cross-school isolation test. + + +### Critical Implementation Review Queue + +These items override ordinary module cleanup. Do not spend time prettifying controllers while these can still leak data, corrupt money, or create irreversible school records. Charming architectural theater can wait. + +#### P0: Must review before feature work + +1. **Payment file access authorization** + - Replace filename-only download access with payment-id-based access. + - Load the payment/invoice/family context before resolving any file path. + - Authorize the current actor against the payment record, not against a guessed filename. + - Resolve the file path only from the stored payment record. + - Return `403` for unauthorized access and `404` for missing/unknown files. + - Required tests: finance/admin can download, unrelated parent cannot download, teacher cannot download, guessed filename cannot download, traversal-style filename fails. + +2. **Manual payment edit transaction safety** + - Wrap payment edit and invoice recalculation in one database transaction. + - Lock the payment row and linked invoice row with `lockForUpdate()` or equivalent. + - Recompute balance inside the lock. + - Reject edits that overpay, violate payment method rules, or create invalid invoice state. + - Store before/after audit details with actor ID. + - Required tests: concurrent edit, overpayment rejection, audit creation, invoice balance consistency. + +3. **Student school ID race condition** + - Remove pre-insert `max(id) + 1` generation. + - Create the student first inside a transaction. + - Generate `school_id` from the persisted student ID or a dedicated sequence. + - Add a unique database constraint on `students.school_id`. + - Add retry behavior for rare collisions. + - Required tests: two concurrent student creations cannot produce duplicate school IDs. + +4. **Scanner attendance transaction and idempotency** + - Split scanner orchestration out of the controller. + - Wrap all attendance mutations from one scan in a single transaction. + - Add an idempotency key based on badge/student/staff, scan date, station, and scan window. + - Prevent duplicate late slips, duplicate scan logs, and double attendance counts. + - Required tests: duplicate scan, late scan, staff scan, student scan, invalid badge, semester boundary. + +5. **Role and permission mutation authorization** + - Map every role/permission mutation route to a policy or gate. + - Add wrong-role and wrong-owner negative tests. + - Log actor, target, old role/permission state, and new state. + +6. **Bulk communication recipient safety** + - Require preview/dry-run before bulk send. + - Deduplicate recipients by canonical user/contact identity. + - Respect opt-out and communication preferences. + - Store send logs and failure states. + - Required tests: preview count matches send count, duplicate recipients collapse, opted-out recipients excluded. + +7. **Refund/payment/invoice audit completeness** + - Every mutation must record actor, target, amount, old state, new state, source endpoint/job, and idempotency key where applicable. + - Reports must reconcile against the same canonical ledger behavior as API reads. + +8. **Core/extension boundary enforcement** + - Create a `SchoolCore` service boundary and a separate `IslamicSundaySchool` extension boundary. + - Move reusable behavior into core contracts/actions. + - Move Islamic Sunday School-specific assumptions into policies/configuration/listeners. + - Add a static dependency check that fails if `SchoolCore` imports `IslamicSundaySchool`. + - Required tests: standard school attendance policy works without Sunday rules, Islamic Sunday School policy overrides only extension points, cross-school data isolation holds. + +9. **School context and tenant/program isolation** + - Introduce a `SchoolContext` or equivalent immutable context object. + - Require context in every core service command. + - Apply context to repositories, policies, files, jobs, reports, and notifications. + - Required tests: actor from School A cannot read or mutate School B students, payments, files, attendance, reports, or messages. + +### Module Acceptance Standard + +A module is not considered hardened unless: + +- every public endpoint has a permission rule; +- every mutation has an audit actor; +- every money, date, file, or message operation has edge-case tests; +- every response shape is documented or represented by an API Resource; +- every side effect is logged; +- every async/send action has retry and dedupe behavior; +- every legacy fallback is documented, quarantined, or removed; +- every controller-level validator has either been moved to a FormRequest or explicitly marked as temporary legacy behavior. +- every core service has been classified as reusable school-core behavior or extension-specific behavior; +- every extension-specific behavior is isolated behind a policy, configuration object, event listener, or adapter; +- every query and mutation has explicit school/program/tenant context; +- every module has at least one cross-school isolation test. + + +## 4. Evidence-Based Risk Notes + +These are not accusations; they are where bugs usually crawl out wearing a tiny hat. + +- **Raw SQL / raw query usage:** 59 files, 166 matches. +- **Direct request input access:** 50 files, 194 matches. +- **File operation endpoints:** 18 files, 34 matches. +- **Email or messaging behavior:** 62 files, 192 matches. +- **Auth/Gate/authorization usage:** 325 files, 482 matches. +- **Explicit DB transactions:** 48 files, 72 matches. +- **CodeIgniter compatibility surface:** 41 files, 62 matches. +- **Queued or dispatched work:** 7 files, 8 matches. +- **Cache-dependent behavior:** 5 files, 12 matches. + + +Review priority should follow blast radius: auth, finance, payments, attendance, grading, promotions, messaging, files, then lower-risk CRUD. + + + +## 5. Delivery Phases + +### Phase 0: Baseline and Guardrails + +- Add a project map document describing module ownership, service boundaries, and route ownership. +- Add static analysis to CI: PHPStan or Larastan, Pint/PHP-CS-Fixer, and a dead-code detector if practical. +- Add a minimal smoke test suite covering auth, health checks, and one representative CRUD module. +- Create fixtures/factories for User, Student, Family, Enrollment, Invoice, Payment, AttendanceRecord, Score, and Staff. +- Freeze API response contracts for critical endpoints with JSON snapshot tests. + +### Phase 1: Security and Access Control + +- Inventory every controller method and map required role/permission/policy. +- Build an endpoint authorization matrix with actor role, resource type, ownership boundary, action, policy/gate/middleware, and required negative tests. +- Confirm every mutation endpoint uses authorization before business logic. +- Normalize role switching/session timeout behavior and log security-relevant events. +- Review IP ban, login attempts, password reset cleanup, registration captcha, and auth session flows. +- Add negative tests proving unauthorized roles cannot read or mutate cross-family, cross-student, cross-staff, payment, invoice, file, role, or message data. +- Treat “authenticated” as insufficient for any financial, student, attendance, role, file, or communication mutation. Logged in is not allowed; it is merely present, like a bug report with no reproduction steps. + +### Phase 2: Financial Correctness + +- Define canonical money rules: rounding, currency precision, refund state machine, invoice adjustment rules, and payment reconciliation. +- Decide whether money is stored as integer cents or strict fixed-decimal values; do not use floating-point arithmetic for persistent financial state. +- Wrap invoice/payment/refund/discount/reimbursement mutations in transactions and audit logs. +- Lock invoice/payment/refund rows during balance-changing edits. +- Add idempotency rules to PayPal sync, payment notifications, manual payments, refunds, and invoice generation. +- Replace filename-driven payment file access with record-driven, authorized access. +- Build reconciliation tests: invoice total = tuition + fees + event charges + extras - discounts - refunds - payments. +- Add report consistency tests so PDFs, dashboard totals, and API totals cannot disagree like three humans in a meeting. + +### Phase 3: Student Lifecycle Correctness + +- Model the lifecycle from registration/enrollment through attendance, grading, report cards, and promotion. +- Add date boundary tests for semesters, attendance windows, class assignment windows, grading locks, and promotion deadlines. +- Centralize student/family visibility checks. +- Make promotion eligibility deterministic and explainable through audit logs. +- Add regression tests for edge cases: missing scores, below-60 notifications, class transfers, inactive students, deleted/unverified users. + +### Phase 4: Communications Reliability + +- Standardize recipient resolution for Email, Messaging, Notifications, WhatsApp, and BroadcastEmail. +- Require preview/dry-run paths for bulk sends. +- Add send logs, dedupe keys, failure states, and retry strategy. +- Separate composition from delivery. +- Test opt-outs, guardian/family recipient selection, teacher/staff targeting, and notification cleanup. + +### Phase 5: Operations and Reporting + +- Review PDF generation paths for badges, certificates, finance reports, report cards, slips, stickers, and print requests. +- Validate all file downloads and upload paths. +- Add operational health endpoints for database, config, cleanup scheduler, and time-sensitive jobs. +- Add runbooks for cleanup commands, payment sync, attendance publishing, and notification dispatch. + +### Phase 6: Documentation and API Contracts + +- Generate OpenAPI output from route definitions, once routes are included. +- Document each module: owner, purpose, inputs, outputs, permissions, side effects, and failure modes. +- Document legacy compatibility areas explicitly, especially CodeIgniter adapters and helper shims. +- Add API examples for primary user flows: login, role switch, parent dashboard, attendance submission, invoice payment, grading submission. + + + +## 5A. Modular Platform Refactor Plan + +This section converts the app from a Sunday-school-shaped backend into a reusable school platform with a Islamic Sunday School implementation layered on top. Do this incrementally. A big-bang rewrite would be a bonfire with sprint ceremonies. + +### Phase A: Classify services + +Create a service inventory with these labels: + +- `Core`: reusable for any school. +- `IslamicSundaySchoolExtension`: specific to Islamic Sunday School rules, religious calendar, ministry roles, Islamic Sunday School attendance, scripture/curriculum assumptions, masjid-family/community structures, or parish-specific reporting. +- `Infrastructure`: storage, Eloquent, mail, SMS/WhatsApp, PDF, exports, payment gateways, QR/scanner hardware. +- `LegacyShim`: compatibility wrapper pending removal. + +Acceptance criteria: + +- Every service has a classification. +- No `Core` service contains Islamic Sunday School-specific vocabulary or table assumptions. +- Duplicate services are marked canonical/shim/deprecated. +- CI/static check prevents new dependencies from `SchoolCore` to `IslamicSundaySchool`. + +### Phase B: Introduce core commands and DTOs + +Replace array-shaped service calls with typed command objects. Arrays are flexible, which is another word for “bugs can hide anywhere.” + +Examples: + +```php +final readonly class EnrollStudentCommand +{ + public function __construct( + public SchoolContext $context, + public ActorId $actorId, + public StudentProfileData $student, + public GuardianDataCollection $guardians, + public EnrollmentProgramId $programId, + ) {} +} +``` + +Acceptance criteria: + +- High-risk actions use command objects: payments, enrollment, attendance, promotion, grading, messaging, file access. +- Commands include `SchoolContext` and `ActorId`. +- Services return result DTOs, not HTTP responses. + +### Phase C: Extract policy contracts + +Create extension contracts for behavior likely to differ across school types: + +```text +AttendancePolicy +AcademicCalendarProvider +GradeScalePolicy +PromotionPolicy +TuitionPolicy +PaymentAllocationPolicy +StudentIdentifierGenerator +GuardianRelationshipPolicy +CommunicationPreferencePolicy +ReportCardTemplateProvider +FileAccessPolicy +``` + +Acceptance criteria: + +- Core services call contracts, not Islamic Sunday School-specific classes. +- Islamic Sunday School supplies concrete implementations. +- A basic non-Sunday `StandardSchool` implementation can run core tests. + +### Phase D: Split domain modules + +Refactor toward these stable core modules: + +```text +SchoolCore\Identity +SchoolCore\Students +SchoolCore\Guardians +SchoolCore\Staff +SchoolCore\Enrollment +SchoolCore\Attendance +SchoolCore\Academics +SchoolCore\Finance +SchoolCore\Communication +SchoolCore\Files +SchoolCore\Reporting +SchoolCore\Authorization +``` + +Islamic Sunday School-specific modules should live separately: + +```text +IslamicSundaySchool\Attendance +IslamicSundaySchool\Classes +IslamicSundaySchool\Curriculum +IslamicSundaySchool\MinistryRoles +IslamicSundaySchool\FamilyMasjidProfile +IslamicSundaySchool\Reports +``` + +Acceptance criteria: + +- Core modules can be tested without booting Islamic Sunday School-specific providers. +- Islamic Sunday School modules depend on core contracts/actions. +- Shared events are neutral: `StudentEnrolled`, `AttendanceMarked`, `PaymentRecorded`, not `SundayStudentCheckedIn`. + +### Phase E: Add cross-school isolation tests + +For every reusable module, add tests for: + +- School A actor cannot access School B records. +- Reports filter by context. +- Files cannot be guessed across schools. +- Background jobs preserve context. +- Notifications send only to recipients in context. + +This is not optional. Multi-school systems fail here first, then everyone acts surprised, as if databases spontaneously developed opinions. + +### Phase F: Introduce extension registration + +Use a service provider or module registry to bind school-type implementations: + +```php +final class IslamicSundaySchoolServiceProvider extends ServiceProvider +{ + public function register(): void + { + $this->app->bind(AttendancePolicy::class, IslamicSundaySchoolAttendancePolicy::class); + $this->app->bind(AcademicCalendarProvider::class, IslamicSundaySchoolCalendarProvider::class); + $this->app->bind(StudentIdentifierGenerator::class, IslamicSundaySchoolStudentIdentifierGenerator::class); + } +} +``` + +Acceptance criteria: + +- Extension bindings are centralized. +- Core defaults exist for generic school behavior. +- Tests can swap policy implementations without changing core services. + +### Phase G: Migration strategy + +Do not rewrite everything at once. Use strangler-style migration: + +1. Pick one high-risk module: Attendance or Finance. +2. Define core contracts and commands. +3. Wrap legacy service behind the new contract if needed. +4. Add tests against the contract. +5. Move Islamic Sunday School-specific logic into an extension policy. +6. Replace controller calls with the new application action. +7. Delete or quarantine the old path after route parity is proven. + +Success metric: a generic school implementation can run the same service tests with different policies. + +## 6. Module-by-Module Plan + +Each module gets the same discipline: define ownership, confirm validation, confirm authorization, isolate service logic, test happy paths and failures, document side effects. + + + +### Admin + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 0 controllers, 2 services, 3 requests, 5 resources. + +**Services:** CompetitionWinnersDomain, CompetitionWinnersAdminService +**Requests:** StoreCompetitionWinnerRequest, UpdateCompetitionWinnerRequest, SaveCompetitionScoresRequest +**Resources:** PrintRecipientsResource, TeacherSubmissionReportResource, AdminNotificationAlertsResource, ProgressGroupResource, ProgressReportDetailResource +**Plan:** +- Confirm all inputs are validated through FormRequest classes. +- Confirm all reads and writes are authorized. +- Move business rules out of controllers if any remain. +- Add feature tests for list/show/create/update/delete and key failure paths. + +### Administrator + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 8 controllers, 18 services, 5 requests, 0 resources. + +**Controllers:** AdministratorAbsenceController, AdministratorDashboardController, AdministratorEnrollmentController, AdministratorNotificationController, AdministratorPromotionController, AdministratorTeacherSubmissionController, EmergencyContactController, TeacherClassAssignmentController +**Services:** AdministratorUserSearchService, AdministratorAbsenceService, AdministratorDashboardService, AdministratorMetricsService, AdministratorEnrollmentStatusService, AdministratorEnrollmentEventService, AdministratorNotificationService, AdminNotificationSubjectService, TeacherSubmissionSupportService, AdministratorSharedService, TeacherSubmissionNotificationService, TeacherSubmissionReportService ... +**Requests:** SavePrintRecipientsRequest, SaveAdminNotificationSubjectsRequest, SubmitAdministratorAbsenceRequest, UpdateEnrollmentStatusesRequest, SendTeacherSubmissionNotificationsRequest +**Plan:** +- Centralize identity lookup and cross-entity authorization. +- Test directory/search pagination and visibility filtering. +- Ensure updates cannot orphan related records or break family/student/staff links. +- Add lifecycle tests for active, inactive, newly enrolled, transferred, and deleted/unverified users. + +### Api + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 0 controllers, 0 services, 4 requests, 0 resources. + +**Requests:** SaveAdminNotificationSubjectsRequest, IndexAdminProgressRequest, SendTeacherSubmissionNotificationsRequest, SavePrintNotificationRecipientsRequest +**Plan:** +- Confirm all inputs are validated through FormRequest classes. +- Confirm all reads and writes are authorized. +- Move business rules out of controllers if any remain. +- Add feature tests for list/show/create/update/delete and key failure paths. + +### ApiRoot + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 7 controllers, 0 services, 0 requests, 0 resources. + +**Controllers:** BaseApiController, CiRequestAdapter, RolePermissionController, RoleSwitcherController, ScannerController, StatsController, UserController +**Plan:** +- Quarantine legacy/root controllers and define canonical replacements for duplicate controller names. +- Split `ScannerController` into a FormRequest, scanner orchestration service, badge lookup service, student scan service, staff scan service, late-arrival service, scan logger, and response factory. +- Add transaction boundaries and idempotency around scanner attendance mutations. +- Define whether `BaseApiController`, `CiRequestAdapter`, root role controllers, and namespaced role controllers are canonical or shims. +- Ensure shim classes contain no business logic. +- Add feature tests for scanner flows, role switching, stats visibility, and user visibility. + +#### Scanner refactor acceptance criteria + +- One public scan endpoint delegates to one scanner service action. +- All scan input is validated through a dedicated FormRequest. +- A duplicate scan within the configured duplicate window does not double-count attendance or create duplicate late-slip records. +- Student and staff scan paths have separate service-level tests. +- Scan logs are written once per accepted mutation and include actor/station/source context. + +### Assignment + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 1 controllers, 5 services, 2 requests, 2 resources. + +**Controllers:** AssignmentApiController +**Services:** AssignmentSectionService, AssignmentMetaService, AssignmentService, AssignmentContextService, AssignmentDataLoaderService +**Requests:** StoreAssignmentRequest, StoreStudentAssignmentRequest +**Resources:** AssignmentOverviewResource, AssignmentSectionResource +**Plan:** +- Define the academic workflow state machine and valid transitions. +- Test teacher/admin/parent role boundaries. +- Protect uploads and generated academic files. +- Add audit logs for status changes, submissions, and deadline changes. + +### Attendance + +**Priority:** P1 Student lifecycle +**Detected surface:** 6 controllers, 16 services, 7 requests, 7 resources. + +**Controllers:** AdminAttendanceApiController, AttendanceCommentTemplateController, EarlyDismissalsController, LateSlipLogsController, StaffAttendanceApiController, TeacherAttendanceApiController +**Services:** SemesterRangeService, TeacherAttendanceSubmissionService, AttendanceAutoPublishJobService, AttendanceAutoPublishService, LateSlipLogQueryService, AttendanceConsequenceService, AttendanceRecordSyncService, AttendanceDailySummaryService, AttendanceService, StudentAttendanceWriterService, LateSlipLogCommandService, AttendanceSummaryRebuildService ... +**Requests:** SaveStaffAttendanceCellRequest, UpdateAttendanceManagementRequest, LateSlipLogIndexRequest, TeacherSubmitAttendanceRequest, SaveAdminAttendanceRequest, TeacherUpdateGridRequest, AdminAddAttendanceEntryRequest +**Resources:** DailyAttendanceResource, AdminAttendanceResource, LateSlipLogCollection, LateSlipLogResource, TeacherGridResource, StaffMonthResource, TeacherAttendanceFormResource +**Plan:** +- Lock down date and semester boundary behavior before changing implementation. +- Test teacher/admin/parent visibility separately. +- Ensure auto-publish, notification, late slip, early dismissal, scanner scan, and violation rules are deterministic. +- Record who changed attendance, when, station/source context when relevant, and why. +- Move scan-driven attendance mutations into transactional services shared by scanner and attendance modules where appropriate. +- Add idempotency tests for repeated scanner submissions and repeated auto-publish attempts. + +### AttendanceCommentTemplate + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 0 controllers, 0 services, 2 requests, 0 resources. + +**Requests:** StoreAttendanceCommentTemplateRequest, UpdateAttendanceCommentTemplateRequest +**Plan:** +- Confirm all inputs are validated through FormRequest classes. +- Confirm all reads and writes are authorized. +- Move business rules out of controllers if any remain. +- Add feature tests for list/show/create/update/delete and key failure paths. + +### AttendanceTracking + +**Priority:** P1 Student lifecycle +**Detected surface:** 1 controllers, 12 services, 5 requests, 0 resources. + +**Controllers:** AttendanceTrackingController +**Services:** AttendanceNotificationLogService, AttendanceViolationStudentResolverService, AttendanceNotificationWorkflowService, AttendanceTrackingService, ViolationRuleEngineService, AttendanceMailerService, AttendanceParentLookupService, AttendanceCommunicationSupportService, AttendanceEmailComposerService, AttendancePendingViolationService, AttendanceCaseQueryService, DefaultAttendanceMailerService +**Requests:** RecordAttendanceTrackingRequest, ComposeAttendanceEmailRequest, SendAttendanceManualEmailRequest, ParentsInfoRequest, SaveAttendanceNotificationNoteRequest +**Plan:** +- Lock down date and semester boundary behavior before changing implementation. +- Test teacher/admin/parent visibility separately. +- Ensure auto-publish, notification, late slip, early dismissal, and violation rules are deterministic. +- Record who changed attendance, when, and why. + +### Auth + +**Priority:** P0 Security/Foundation +**Detected surface:** 7 controllers, 9 services, 3 requests, 1 resources. + +**Controllers:** AuthController, AuthSessionController, IpBanController, RegisterController, RolePermissionController, RoleSwitcherController, SessionTimeoutController +**Services:** RegistrationCaptchaService, PermissionCheckService, CodeIgniterApiRegistrationService, RegistrationService, ApiLoginSecurityService, AuthSessionService, UserRoleService, PasswordResetCleanupService, RegistrationFormatterService +**Requests:** RegisterRequest, SetRoleSessionRequest, LoginSessionRequest +**Resources:** RegisterResource +**Plan:** +- Map every endpoint to required auth state, role, and permission. +- Test login throttling, IP bans, password reset cleanup, session timeout, role switching, and registration captcha. +- Confirm security logs include actor, IP, user agent, outcome, and reason. +- Reject role changes that create privilege escalation or stale-session leaks. + +### BadgeScan + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 1 controllers, 1 services, 0 requests, 0 resources. + +**Controllers:** BadgeScanController +**Services:** BadgeScanService +**Plan:** +- Confirm all inputs are validated through FormRequest classes. +- Confirm all reads and writes are authorized. +- Move business rules out of controllers if any remain. +- Add feature tests for list/show/create/update/delete and key failure paths. + +### Badges + +**Priority:** P2 Operations +**Detected surface:** 1 controllers, 7 services, 3 requests, 0 resources. + +**Controllers:** BadgeController +**Services:** BadgeTextFormatter, BadgeStudentLookupService, BadgeFormDataService, BadgePdfService, BadgeUserLookupService, BadgeService, BadgePrintLogService +**Requests:** BadgePrintStatusRequest, GenerateBadgePdfRequest, LogBadgePrintRequest +**Plan:** +- Review upload/download/PDF generation permissions. +- Validate file type, file size, generated names, and storage paths. +- Add tests for inventory movement consistency and purchase-order state transitions where applicable. +- Make printable outputs reproducible from stable data snapshots. + +### Billing + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 0 controllers, 3 services, 0 requests, 0 resources. + +**Services:** BalanceCalculationService, BillingTotalsService, ChargeService +**Plan:** +- Confirm all inputs are validated through FormRequest classes. +- Confirm all reads and writes are authorized. +- Move business rules out of controllers if any remain. +- Add feature tests for list/show/create/update/delete and key failure paths. + +### BroadcastEmail + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 0 controllers, 5 services, 0 requests, 0 resources. + +**Services:** BroadcastEmailComposerService, BroadcastEmailRecipientService, BroadcastEmailDispatchService, BroadcastEmailImageService, BroadcastEmailSenderOptionsService +**Plan:** +- Separate recipient selection, message composition, dispatch, and logging. +- Add preview/dry-run tests before bulk sending. +- Deduplicate sends and persist failure states. +- Verify opt-outs, guardian selection, role targeting, and retry behavior. + +### Certificates + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 1 controllers, 1 services, 0 requests, 0 resources. + +**Controllers:** CertificateController +**Services:** CertificatePdfService +**Plan:** +- Review upload/download/PDF generation permissions. +- Validate file type, file size, generated names, and storage paths. +- Add tests for inventory movement consistency and purchase-order state transitions where applicable. +- Make printable outputs reproducible from stable data snapshots. + +### ClassPrep + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 0 controllers, 2 services, 0 requests, 0 resources. + +**Services:** ClassRosterService, StickerCountService +**Plan:** +- Confirm all inputs are validated through FormRequest classes. +- Confirm all reads and writes are authorized. +- Move business rules out of controllers if any remain. +- Add feature tests for list/show/create/update/delete and key failure paths. + +### ClassPreparation + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 1 controllers, 9 services, 4 requests, 2 resources. + +**Controllers:** ClassPreparationController +**Services:** ClassPreparationAdjustmentWriterService, ClassPreparationCalculatorService, ClassPreparationLogService, ClassPreparationAdjustmentService, ClassPreparationContextService, ClassPreparationService, ClassPreparationPrintService, ClassPreparationRosterService, ClassPreparationInventoryService +**Requests:** ClassPreparationMarkPrintedRequest, ClassPreparationPrintRequest, ClassPreparationIndexRequest, ClassPreparationAdjustmentRequest +**Resources:** ClassPreparationPrintResource, ClassPreparationResultResource +**Plan:** +- Define the academic workflow state machine and valid transitions. +- Test teacher/admin/parent role boundaries. +- Protect uploads and generated academic files. +- Add audit logs for status changes, submissions, and deadline changes. + +### ClassProgress + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 1 controllers, 5 services, 5 requests, 4 resources. + +**Controllers:** ClassProgressController +**Services:** ClassProgressRuleService, ClassProgressAttachmentService, ClassProgressQueryService, ClassProgressMetaService, ClassProgressMutationService +**Requests:** ClassProgressStoreRequest, ClassProgressMetaRequest, ClassProgressIndexRequest, ClassProgressFormRequest, ClassProgressUpdateRequest +**Resources:** ClassProgressGroupCollection, ClassProgressAttachmentResource, ClassProgressShowResource, ClassProgressReportResource +**Plan:** +- Define the academic workflow state machine and valid transitions. +- Test teacher/admin/parent role boundaries. +- Protect uploads and generated academic files. +- Add audit logs for status changes, submissions, and deadline changes. + +### ClassSections + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 0 controllers, 4 services, 0 requests, 0 resources. + +**Services:** ClassSectionCommandService, ClassAttendanceService, ClassSectionSeedService, ClassSectionQueryService +**Plan:** +- Define the academic workflow state machine and valid transitions. +- Test teacher/admin/parent role boundaries. +- Protect uploads and generated academic files. +- Add audit logs for status changes, submissions, and deadline changes. + +### Classes + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 1 controllers, 0 services, 3 requests, 4 resources. + +**Controllers:** ClassController +**Requests:** ClassSectionUpdateRequest, ClassSectionStoreRequest, ClassSectionIndexRequest +**Resources:** ClassSectionCollection, ClassSectionResource, ClassAttendanceResource, ClassAttendanceStudentResource +**Plan:** +- Define the academic workflow state machine and valid transitions. +- Test teacher/admin/parent role boundaries. +- Protect uploads and generated academic files. +- Add audit logs for status changes, submissions, and deadline changes. + +### Communication + +**Priority:** P1 Communications +**Detected surface:** 1 controllers, 6 services, 0 requests, 0 resources. + +**Controllers:** CommunicationController +**Services:** CommunicationFamilyService, CommunicationSendService, CommunicationTemplateService, CommunicationPreviewService, CommunicationStudentService, CommunicationLogService +**Plan:** +- Separate recipient selection, message composition, dispatch, and logging. +- Add preview/dry-run tests before bulk sending. +- Deduplicate sends and persist failure states. +- Verify opt-outs, guardian selection, role targeting, and retry behavior. + +### CompetitionScores + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 1 controllers, 4 services, 0 requests, 0 resources. + +**Controllers:** CompetitionScoresController +**Services:** CompetitionScoresRosterService, CompetitionScoresSaveService, CompetitionScoresContextService, CompetitionScoresQueryService +**Plan:** +- Confirm all inputs are validated through FormRequest classes. +- Confirm all reads and writes are authorized. +- Move business rules out of controllers if any remain. +- Add feature tests for list/show/create/update/delete and key failure paths. + +### Core + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 2 controllers, 0 services, 0 requests, 0 resources. + +**Controllers:** BaseApiController, CiRequestAdapter +**Plan:** +- Confirm config reads/writes are permissioned and validated. +- Add health/config smoke tests. +- Document public versus authenticated surfaces. +- Keep UI/style/static-page services separated from domain mutations. + +### Dashboard + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 0 controllers, 1 services, 0 requests, 0 resources. + +**Services:** DashboardRouteService +**Plan:** +- Confirm config reads/writes are permissioned and validated. +- Add health/config smoke tests. +- Document public versus authenticated surfaces. +- Keep UI/style/static-page services separated from domain mutations. + +### Discounts + +**Priority:** P0 Financial correctness +**Detected surface:** 1 controllers, 5 services, 3 requests, 2 resources. + +**Controllers:** DiscountController +**Services:** DiscountParentService, DiscountApplyService, DiscountInvoiceService, DiscountVoucherService, DiscountContextService +**Requests:** UpdateDiscountVoucherRequest, ApplyDiscountVoucherRequest, StoreDiscountVoucherRequest +**Resources:** DiscountVoucherResource, DiscountParentResource +**Plan:** +- Define one source of truth for monetary calculations and rounding. +- Require transactions around every mutation that changes balance, invoice state, refund state, payment state, or reimbursement status. +- Add audit logs and idempotency keys for external payment/payment-notification flows. +- Build reconciliation tests across API, reports, dashboards, and PDFs. + +### Docs + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 0 controllers, 3 services, 0 requests, 0 resources. + +**Services:** DocsCatalogService, ApiDocsService, OpenApiRouteExporter +**Plan:** +- Confirm config reads/writes are permissioned and validated. +- Add health/config smoke tests. +- Document public versus authenticated surfaces. +- Keep UI/style/static-page services separated from domain mutations. + +### Documentation + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 2 controllers, 0 services, 0 requests, 0 resources. + +**Controllers:** ApiDocsAdminController, DocsCatalogController +**Plan:** +- Confirm config reads/writes are permissioned and validated. +- Add health/config smoke tests. +- Document public versus authenticated surfaces. +- Keep UI/style/static-page services separated from domain mutations. + +### Email + +**Priority:** P1 Communications +**Detected surface:** 3 controllers, 5 services, 2 requests, 3 resources. + +**Controllers:** BroadcastEmailController, EmailController, EmailExtractorController +**Services:** EmailAttachmentService, EmailExtractorService, EmailProfileService, EmailDispatchService, EmailSenderOptionsService +**Requests:** EmailExtractorCompareRequest, EmailSendRequest +**Resources:** EmailSenderResource, EmailExtractorEmailsResource, EmailExtractorCompareResource +**Plan:** +- Separate recipient selection, message composition, dispatch, and logging. +- Add preview/dry-run tests before bulk sending. +- Deduplicate sends and persist failure states. +- Verify opt-outs, guardian selection, role targeting, and retry behavior. + +### EmergencyContacts + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 0 controllers, 2 services, 2 requests, 3 resources. + +**Services:** EmergencyContactDirectoryService, EmergencyContactCrudService +**Requests:** EmergencyContactParentRequest, EmergencyContactUpdateRequest +**Resources:** EmergencyContactGroupResource, EmergencyContactStudentResource, EmergencyContactResource +**Plan:** +- Confirm all inputs are validated through FormRequest classes. +- Confirm all reads and writes are authorized. +- Move business rules out of controllers if any remain. +- Add feature tests for list/show/create/update/delete and key failure paths. + +### Events + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 0 controllers, 6 services, 6 requests, 3 resources. + +**Services:** EventChargeService, EventStudentChargeService, EventCategoryService, EventListService, EventChargeQueryService, EventManagementService +**Requests:** EventIndexRequest, EventUpdateRequest, EventStoreRequest, EventStudentsWithChargesRequest, EventChargeUpdateRequest, EventChargeIndexRequest +**Resources:** EventChargeResource, EventResource, EventStudentChargeResource +**Plan:** +- Confirm all inputs are validated through FormRequest classes. +- Confirm all reads and writes are authorized. +- Move business rules out of controllers if any remain. +- Add feature tests for list/show/create/update/delete and key failure paths. + +### Exams + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 1 controllers, 4 services, 3 requests, 1 resources. + +**Controllers:** ExamDraftController +**Services:** ExamDraftTeacherService, ExamDraftAdminService, ExamDraftConfigService, ExamDraftFileService +**Requests:** ExamDraftTeacherStoreRequest, ExamDraftAdminLegacyRequest, ExamDraftAdminReviewRequest +**Resources:** ExamDraftResource +**Plan:** +- Define the academic workflow state machine and valid transitions. +- Test teacher/admin/parent role boundaries. +- Protect uploads and generated academic files. +- Add audit logs for status changes, submissions, and deadline changes. + +### Expenses + +**Priority:** P0 Financial correctness +**Detected surface:** 1 controllers, 7 services, 3 requests, 2 resources. + +**Controllers:** ExpenseController +**Services:** ExpenseQueryService, ExpenseStaffService, ExpenseRetailorService, ExpenseReceiptService, ExpenseWriteService, ExpenseContextService, ExpenseStatusService +**Requests:** UpdateExpenseRequest, StoreExpenseRequest, UpdateExpenseStatusRequest +**Resources:** ExpenseResource, ExpenseStaffResource +**Plan:** +- Define one source of truth for monetary calculations and rounding. +- Require transactions around every mutation that changes balance, invoice state, refund state, payment state, or reimbursement status. +- Add audit logs and idempotency keys for external payment/payment-notification flows. +- Build reconciliation tests across API, reports, dashboards, and PDFs. + +### ExtraCharges + +**Priority:** P0 Financial correctness +**Detected surface:** 1 controllers, 6 services, 2 requests, 3 resources. + +**Controllers:** ExtraChargesController +**Services:** ExtraChargesMetaService, ExtraChargesInvoiceService, ExtraChargesParentService, ExtraChargesChargeService, ExtraChargesContextService, ExtraChargesListService +**Requests:** UpdateExtraChargeRequest, StoreExtraChargeRequest +**Resources:** ExtraChargeInvoiceOptionResource, ExtraChargeResource, ExtraChargeParentOptionResource +**Plan:** +- Define one source of truth for monetary calculations and rounding. +- Require transactions around every mutation that changes balance, invoice state, refund state, payment state, or reimbursement status. +- Add audit logs and idempotency keys for external payment/payment-notification flows. +- Build reconciliation tests across API, reports, dashboards, and PDFs. + +### Families + +**Priority:** P1 Student lifecycle +**Detected surface:** 0 controllers, 4 services, 14 requests, 7 resources. + +**Services:** FamilyNotificationService, FamilyFinanceService, FamilyMutationService, FamilyQueryService +**Requests:** FamilyImportLegacyRequest, FamilyUnlinkStudentRequest, FamilySetPrimaryHomeRequest, FamilyAdminCardRequest, FamiliesByStudentRequest, FamilyBootstrapRequest, FamilyAttachSecondByEmailRequest, FamilyAdminSearchRequest ... +**Resources:** FamilyPaymentResource, FamilyInvoiceResource, FamilySearchItemResource, FamilyResource, FamilyGuardianResource, FamilyStudentResource, FamilyEmergencyContactResource +**Plan:** +- Centralize identity lookup and cross-entity authorization. +- Test directory/search pagination and visibility filtering. +- Ensure updates cannot orphan related records or break family/student/staff links. +- Add lifecycle tests for active, inactive, newly enrolled, transferred, and deleted/unverified users. + +### Family + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 2 controllers, 0 services, 0 requests, 0 resources. + +**Controllers:** FamilyAdminController, FamilyController +**Plan:** +- Confirm all inputs are validated through FormRequest classes. +- Confirm all reads and writes are authorized. +- Move business rules out of controllers if any remain. +- Add feature tests for list/show/create/update/delete and key failure paths. + +### Fees + +**Priority:** P0 Financial correctness +**Detected surface:** 0 controllers, 6 services, 2 requests, 6 resources. + +**Services:** FeeConfigService, TuitionCalculationService, FeeRefundCalculatorService, FeeGradeService, FeeRefundDetailService, FeeStudentFeeService +**Requests:** FeeTuitionTotalRequest, FeeRefundRequest +**Resources:** FeeRefundResource, FeeFamilyBalanceResource, ChargeActionResource, ChargeListResource, FeeTuitionBreakdownResource, FeeTuitionTotalResource +**Plan:** +- Define one source of truth for monetary calculations and rounding. +- Require transactions around every mutation that changes balance, invoice state, refund state, payment state, or reimbursement status. +- Add audit logs and idempotency keys for external payment/payment-notification flows. +- Build reconciliation tests across API, reports, dashboards, and PDFs. + +### Files + +**Priority:** P2 Operations +**Detected surface:** 0 controllers, 2 services, 1 requests, 1 resources. + +**Services:** FileServeService, ExamDraftDownloadNameService +**Requests:** FileNameRequest +**Resources:** FileMetaResource +**Plan:** +- Review upload/download/PDF generation permissions. +- Validate file type, file size, generated names, and storage paths. +- Add tests for inventory movement consistency and purchase-order state transitions where applicable. +- Make printable outputs reproducible from stable data snapshots. + +### Finance + +**Priority:** P0 Financial correctness +**Detected surface:** 14 controllers, 8 services, 12 requests, 9 resources. + +**Controllers:** ChargeController, FeeCalculationController, FinancialController, InvoiceController, PaymentController, PaymentEventChargesController, PaymentManualController, PaymentNotificationController, PaymentTransactionController, PaypalPaymentController ... +**Services:** FinancialPdfReportService, FinancialSchoolYearService, FinancialPaymentService, FinancialReportService, FinancialSummaryService, FinancialChartService, FinancialDonationRecipientService, FinancialUnpaidParentsService +**Requests:** FinancialUnpaidParentsRequest, FeeTuitionTotalRequest, ChargeListRequest, FinancialReportRequest, FinancialSummaryRequest, MarkEventChargePaidRequest, FeeTuitionBreakdownRequest, StoreExtraChargeRequest ... +**Resources:** FinancialReportResource, FinancialSummaryResource, FinancialExpenseSummaryResource, FinancialInvoiceResource, FinancialPaymentResource, FinancialRefundResource, FinancialUnpaidParentResource, FinancialDiscountResource ... +**Plan:** +- Define one source of truth for monetary calculations and rounding. +- Require transactions around every mutation that changes balance, invoice state, refund state, payment state, or reimbursement status. +- Add audit logs and idempotency keys for external payment/payment-notification flows. +- Harden payment file access before any broader finance refactor: no filename-only authorization, no arbitrary file resolution, no unaudited downloads. +- Lock affected invoice/payment/refund/reimbursement rows during edits and recalculations. +- Normalize all finance responses through resources so dashboards, APIs, reports, and PDFs do not compute separate truths, because apparently one truth was not complicated enough. +- Build reconciliation tests across API, reports, dashboards, and PDFs. + +#### Finance P0 acceptance criteria + +- Manual payment create/edit/delete and invoice recalculation run inside transactions. +- Payment edits create before/after audit records. +- Payment file downloads are authorized by payment/invoice ownership or finance role. +- Wrong-role and wrong-owner tests exist for invoices, payments, refunds, reimbursements, and downloadable files. +- All finance mutation tests assert final ledger/invoice balance, not just HTTP status. + +### Frontend + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 4 controllers, 9 services, 2 requests, 6 resources. + +**Controllers:** FrontendController, InfoIconController, LandingPageController, PageController +**Services:** LandingPageTeacherSummaryService, LandingPageRoleService, LandingPageParentDashboardService, LandingPageService, StaticPageService, FrontendPageService, ContactSubmissionService, LandingPageContextService, ProfileIconService +**Requests:** LandingTeacherRequest, ContactSubmitRequest +**Resources:** LandingPageResource, ContactSubmissionResource, FrontendPageResource, FrontendUserResource, ProfileIconResource, PageContentResource +**Plan:** +- Confirm config reads/writes are permissioned and validated. +- Add health/config smoke tests. +- Document public versus authenticated surfaces. +- Keep UI/style/static-page services separated from domain mutations. + +### Grading + +**Priority:** P1 Student lifecycle +**Detected surface:** 2 controllers, 9 services, 18 requests, 3 resources. + +**Controllers:** GradingController, HomeworkTrackingController +**Services:** BelowSixtyEmailService, GradingLockService, GradingOverviewService, GradingPlacementService, HomeworkTrackingService, GradingScoreService, GradingBelowSixtyService, HomeworkTrackingCalendarService, GradingRefreshService +**Requests:** BelowSixtyListRequest, BelowSixtySendRequest, GradingLockAllRequest, BelowSixtyMeetingRequest, PlacementLevelsRequest, GradingRefreshRequest, GradingScoreShowRequest, HomeworkTrackingRequest ... +**Resources:** BelowSixtyStudentResource, GradingScoreResource, HomeworkTrackingTeacherResource +**Plan:** +- Centralize score formulas and missing-score behavior. +- Add golden test cases for semester score, quiz, homework, project, participation, final/midterm, and prediction risk calculations. +- Prove grading locks block writes but not allowed reads. +- Verify below-60 communication and parent visibility rules. + +### Incidents + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 1 controllers, 4 services, 5 requests, 4 resources. + +**Controllers:** IncidentController +**Services:** CurrentIncidentService, IncidentHistoryService, IncidentLookupService, IncidentAnalysisService +**Requests:** IncidentCancelRequest, IncidentCloseRequest, IncidentStoreRequest, IncidentListRequest, IncidentStateRequest +**Resources:** IncidentStudentOptionResource, IncidentAnalysisStudentResource, IncidentResource, IncidentGradeResource +**Plan:** +- Confirm all inputs are validated through FormRequest classes. +- Confirm all reads and writes are authorized. +- Move business rules out of controllers if any remain. +- Add feature tests for list/show/create/update/delete and key failure paths. + +### Inventory + +**Priority:** P2 Operations +**Detected surface:** 5 controllers, 8 services, 19 requests, 5 resources. + +**Controllers:** InventoryCategoryController, InventoryController, InventoryMovementController, SupplierController, SupplyCategoryController +**Services:** InventorySummaryService, InventoryCategoryService, InventoryItemService, InventoryContextService, SupplierService, InventoryMovementService, SupplyCategoryService, InventoryTeacherDistributionService +**Requests:** InventoryItemIndexRequest, SupplierUpdateRequest, InventoryCategoryStoreRequest, InventoryTeacherDistributionRequest, InventoryItemUpdateRequest, InventoryItemStoreRequest, InventoryTeacherDistributionStoreRequest, InventoryCategoryUpdateRequest ... +**Resources:** InventoryItemResource, InventoryCategoryResource, InventoryMovementResource, SupplierResource, SupplyCategoryResource +**Plan:** +- Review upload/download/PDF generation permissions. +- Validate file type, file size, generated names, and storage paths. +- Add tests for inventory movement consistency and purchase-order state transitions where applicable. +- Make printable outputs reproducible from stable data snapshots. + +### Invoices + +**Priority:** P0 Financial correctness +**Detected surface:** 0 controllers, 8 services, 4 requests, 2 resources. + +**Services:** InvoiceGradeService, InvoiceConfigService, InvoicePaymentService, InvoiceTuitionService, InvoiceManagementService, InvoicePdfService, InvoiceService, InvoiceGenerationService +**Requests:** InvoiceStatusRequest, InvoiceManagementRequest, InvoiceParentRequest, InvoiceGenerateRequest +**Resources:** InvoiceManagementParentResource, InvoiceResource +**Plan:** +- Define one source of truth for monetary calculations and rounding. +- Require transactions around every mutation that changes balance, invoice state, refund state, payment state, or reimbursement status. +- Add audit logs and idempotency keys for external payment/payment-notification flows. +- Build reconciliation tests across API, reports, dashboards, and PDFs. + +### Messaging + +**Priority:** P1 Communications +**Detected surface:** 2 controllers, 3 services, 3 requests, 3 resources. + +**Controllers:** MessagesController, WhatsappController +**Services:** MessageRecipientService, MessageCommandService, MessageQueryService +**Requests:** MessageSendRequest, MessageIndexRequest, MessageUpdateRequest +**Resources:** MessageResource, MessageCollection, RecipientResource +**Plan:** +- Separate recipient selection, message composition, dispatch, and logging. +- Add preview/dry-run tests before bulk sending. +- Deduplicate sends and persist failure states. +- Verify opt-outs, guardian selection, role targeting, and retry behavior. + +### Navigation + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 0 controllers, 2 services, 0 requests, 0 resources. + +**Services:** NavBuilderService, NavbarService +**Plan:** +- Confirm config reads/writes are permissioned and validated. +- Add health/config smoke tests. +- Document public versus authenticated surfaces. +- Keep UI/style/static-page services separated from domain mutations. + +### NonApi + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 3 controllers, 0 services, 0 requests, 0 resources. + +**Controllers:** Controller, DocsController, TeacherCalendarPageController +**Plan:** +- Confirm all inputs are validated through FormRequest classes. +- Confirm all reads and writes are authorized. +- Move business rules out of controllers if any remain. +- Add feature tests for list/show/create/update/delete and key failure paths. + +### Notifications + +**Priority:** P1 Communications +**Detected surface:** 1 controllers, 12 services, 5 requests, 2 resources. + +**Controllers:** NotificationController +**Services:** NotificationDispatchService, NotificationRecipientService, NotificationShowService, NotificationManagementService, NotificationReadService, NotificationUserListService, NotificationCleanupService, UserNotificationDispatchService, NotificationTriggerService, NotificationSendService, NotificationActiveService, NotificationDeletedService +**Requests:** NotificationSendRequest, NotificationActiveRequest, NotificationDeletedRequest, NotificationUpdateRequest, NotificationIndexRequest +**Resources:** UserNotificationResource, NotificationResource +**Plan:** +- Separate recipient selection, message composition, dispatch, and logging. +- Add preview/dry-run tests before bulk sending. +- Deduplicate sends and persist failure states. +- Verify opt-outs, guardian selection, role targeting, and retry behavior. + +### Parents + +**Priority:** P1 Student lifecycle +**Detected surface:** 5 controllers, 14 services, 16 requests, 4 resources. + +**Controllers:** AuthorizedUserInviteController, AuthorizedUsersController, ParentAttendanceReportController, ParentController, ParentPromotionController +**Services:** ParentEventParticipationService, ParentRegistrationService, PrimaryParentUserResolver, ParentAttendanceReportCalendarService, ParentAttendanceService, AuthorizedUsersManagementService, ParentEnrollmentService, ParentInvoiceService, ParentProgressQueryService, ParentAttendanceReportService, ParentReportCardService, ParentConfigService ... +**Requests:** ParentAttendanceReportUpdateRequest, ParentAttendanceReportListRequest, ParentAttendanceReportSubmitRequest, ParentEmergencyContactRequest, UpdateAuthorizedUserRequest, ParentAttendanceReportCheckRequest, ParentProfileUpdateRequest, ParentInvoiceRequest ... +**Resources:** ParentEmergencyContactResource, ParentAttendanceReportResource, ParentStudentResource, ParentAttendanceResource +**Plan:** +- Centralize identity lookup and cross-entity authorization. +- Test directory/search pagination and visibility filtering. +- Ensure updates cannot orphan related records or break family/student/staff links. +- Add lifecycle tests for active, inactive, newly enrolled, transferred, and deleted/unverified users. + +### Payments + +**Priority:** P0 Financial correctness +**Detected surface:** 0 controllers, 14 services, 15 requests, 4 resources. + +**Services:** PaymentNotificationDispatchService, PaymentMissedCheckService, PaymentManualService, PaymentLookupService, PaymentTestNotificationService, PaypalPaymentSyncService, PaymentBalanceService, PaypalPaymentService, PaymentTransactionService, PaymentEnrollmentEventService, PaymentNotificationService, PaymentPlanService ... +**Requests:** PaymentManualSearchRequest, PaymentEventChargesListRequest, PaymentNotificationSendRequest, PaymentNotificationListRequest, PaymentCreateRequest, PaypalTransactionsListRequest, PaymentManualEditRequest, PaymentEventChargesStoreRequest ... +**Resources:** PaymentNotificationLogResource, PaymentTransactionResource, PaypalTransactionResource, PaymentResource +**Plan:** +- Define one source of truth for payment state transitions, balance calculations, file handling, and notification dispatch. +- Wrap manual payment edits and invoice recalculation in one transaction with row locks. +- Replace filename-only check-file downloads with payment-record-based downloads and authorization. +- Add idempotency keys for PayPal sync, notification dispatch, manual payment creation, and retry flows. +- Store audit trails for payment create/edit/delete, refund application, missed-check updates, and external transaction sync. +- Add reconciliation tests across API, reports, dashboards, and PDFs. + +#### PaymentManualService hardening checklist + +- `recordPayment()` and `editPayment()` use the same transaction/audit/recalculation rules. +- `serveCheckFile()` cannot be called with only a user-supplied filename; it must resolve files from a payment record. +- Upload validation covers extension, MIME, size, storage path, and scan-friendly safe names. +- Tests cover unrelated parent access, unrelated staff access, valid finance/admin access, guessed filename, missing file, and duplicate edit attempts. + +### Phone + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 0 controllers, 1 services, 0 requests, 0 resources. + +**Services:** PhoneFormatterService +**Plan:** +- Confirm all inputs are validated through FormRequest classes. +- Confirm all reads and writes are authorized. +- Move business rules out of controllers if any remain. +- Add feature tests for list/show/create/update/delete and key failure paths. + +### Policy + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 0 controllers, 1 services, 0 requests, 0 resources. + +**Services:** PolicyContentService +**Plan:** +- Confirm config reads/writes are permissioned and validated. +- Add health/config smoke tests. +- Document public versus authenticated surfaces. +- Keep UI/style/static-page services separated from domain mutations. + +### Preferences + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 0 controllers, 3 services, 2 requests, 3 resources. + +**Services:** PreferencesCommandService, PreferencesQueryService, PreferencesOptionsService +**Requests:** PreferencesIndexRequest, PreferencesUpsertRequest +**Resources:** PreferencesListResource, PreferencesCollection, PreferencesResource +**Plan:** +- Confirm config reads/writes are permissioned and validated. +- Add health/config smoke tests. +- Document public versus authenticated surfaces. +- Keep UI/style/static-page services separated from domain mutations. + +### PrintRequests + +**Priority:** P2 Operations +**Detected surface:** 1 controllers, 1 services, 0 requests, 0 resources. + +**Controllers:** PrintRequestsController +**Services:** PrintRequestsPortalService +**Plan:** +- Review upload/download/PDF generation permissions. +- Validate file type, file size, generated names, and storage paths. +- Add tests for inventory movement consistency and purchase-order state transitions where applicable. +- Make printable outputs reproducible from stable data snapshots. + +### Promotions + +**Priority:** P1 Student lifecycle +**Detected surface:** 0 controllers, 7 services, 8 requests, 4 resources. + +**Services:** PromotionStatusService, PromotionEnrollmentService, PromotionReminderService, LevelProgressionService, PromotionAuditService, PromotionQueryService, PromotionEligibilityService +**Requests:** SendReminderRequest, ParentPromotionOverviewRequest, AdminListPromotionsRequest, EvaluateEligibilityRequest, UpdatePromotionStatusRequest, ParentEnrollmentStepRequest, SetEnrollmentDeadlineRequest, UpsertLevelProgressionRequest +**Resources:** LevelProgressionResource, PromotionAuditLogResource, PromotionReminderResource, StudentPromotionResource +**Plan:** +- Define the academic workflow state machine and valid transitions. +- Test teacher/admin/parent role boundaries. +- Protect uploads and generated academic files. +- Add audit logs for status changes, submissions, and deadline changes. + +### Public + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 1 controllers, 0 services, 0 requests, 0 resources. + +**Controllers:** PublicWinnersController +**Plan:** +- Confirm config reads/writes are permissioned and validated. +- Add health/config smoke tests. +- Document public versus authenticated surfaces. +- Keep UI/style/static-page services separated from domain mutations. + +### PublicWinners + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 0 controllers, 1 services, 0 requests, 0 resources. + +**Services:** PublicWinnersPortalService +**Plan:** +- Confirm all inputs are validated through FormRequest classes. +- Confirm all reads and writes are authorized. +- Move business rules out of controllers if any remain. +- Add feature tests for list/show/create/update/delete and key failure paths. + +### PurchaseOrders + +**Priority:** P2 Operations +**Detected surface:** 0 controllers, 4 services, 3 requests, 2 resources. + +**Services:** PurchaseOrderStatusService, PurchaseOrderReceiveService, PurchaseOrderCreateService, PurchaseOrderQueryService +**Requests:** PurchaseOrderIndexRequest, PurchaseOrderStoreRequest, PurchaseOrderReceiveRequest +**Resources:** PurchaseOrderResource, PurchaseOrderItemResource +**Plan:** +- Review upload/download/PDF generation permissions. +- Validate file type, file size, generated names, and storage paths. +- Add tests for inventory movement consistency and purchase-order state transitions where applicable. +- Make printable outputs reproducible from stable data snapshots. + +### Refunds + +**Priority:** P0 Financial correctness +**Detected surface:** 0 controllers, 9 services, 6 requests, 1 resources. + +**Services:** RefundSummaryService, RefundInvoiceAdjustmentService, RefundPolicyService, RefundPayoutService, RefundRequestService, RefundQueryService, RefundOverpaymentService, RefundDecisionService, RefundNotificationService +**Requests:** RefundStoreRequest, RefundPaymentRequest, RefundDecisionRequest, RefundRecalculateRequest, RefundIndexRequest, RefundRejectRequest +**Resources:** RefundResource +**Plan:** +- Define one source of truth for monetary calculations and rounding. +- Require transactions around every mutation that changes balance, invoice state, refund state, payment state, or reimbursement status. +- Add audit logs and idempotency keys for external payment/payment-notification flows. +- Build reconciliation tests across API, reports, dashboards, and PDFs. + +### Reimbursements + +**Priority:** P0 Financial correctness +**Detected surface:** 0 controllers, 11 services, 13 requests, 5 resources. + +**Services:** ReimbursementFileService, ReimbursementBatchAssignmentService, ReimbursementContextService, ReimbursementExportService, ReimbursementLookupService, ReimbursementEmailService, ReimbursementRecipientService, ReimbursementBatchService, ReimbursementQueryService, ReimbursementDonationService, ReimbursementCrudService +**Requests:** ReimbursementBatchAdminFileRequest, ReimbursementUpdateRequest, ReimbursementBatchCreateRequest, ReimbursementBatchEmailRequest, ReimbursementProcessRequest, ReimbursementExportBatchRequest, ReimbursementIndexRequest, ReimbursementExportRequest ... +**Resources:** ReimbursementRecipientResource, ReimbursementExpenseResource, ReimbursementBatchResource, ReimbursementResource, ReimbursementUnderProcessingItemResource +**Plan:** +- Define one source of truth for monetary calculations and rounding. +- Require transactions around every mutation that changes balance, invoice state, refund state, payment state, or reimbursement status. +- Add audit logs and idempotency keys for external payment/payment-notification flows. +- Build reconciliation tests across API, reports, dashboards, and PDFs. + +### Reports + +**Priority:** P2 Operations +**Detected surface:** 4 controllers, 9 services, 11 requests, 8 resources. + +**Controllers:** FilesController, ReportCardsController, SlipPrinterController, StickersController +**Services:** SlipPrinterFormatterService, SlipPrinterPdfService, SlipPrinterService, SlipPrinterConfigService, StickerPrintService, StickerPresetService, StickerQueryService, StickerContextService, ReportCardService +**Requests:** SlipPreviewRequest, SlipLogListRequest, SlipPrintRequest, SlipReprintRequest, StickerStudentsByClassRequest, StickerPrintRequest, StickerFormDataRequest, ReportCardCompletenessRequest ... +**Resources:** LateSlipLogResource, StickerStudentResource, StickerPresetResource, StickerClassSectionResource, ReportCardStudentMetaResource, ReportCardAcknowledgementResource, ReportCardClassSectionResource, ReportCardCompletenessStudentResource +**Plan:** +- Review upload/download/PDF generation permissions. +- Validate file type, file size, generated names, and storage paths. +- Add tests for inventory movement consistency and purchase-order state transitions where applicable. +- Make printable outputs reproducible from stable data snapshots. + +### Roles + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 0 controllers, 8 services, 9 requests, 4 resources. + +**Services:** RolePermissionService, RoleDashboardService, RoleCrudService, RoleAssignmentService, RoleSwitchService, RoleStaffService, PermissionCrudService, RoleQueryService +**Requests:** AssignUserRolesRequest, RolePermissionUpdateRequest, UserRoleListRequest, PermissionUpdateRequest, PermissionStoreRequest, RoleSwitchRequest, RoleStoreRequest, RoleUpdateRequest ... +**Resources:** RolePermissionResource, RoleResource, UserWithRolesResource, PermissionResource +**Plan:** +- Confirm all inputs are validated through FormRequest classes. +- Confirm all reads and writes are authorized. +- Move business rules out of controllers if any remain. +- Add feature tests for list/show/create/update/delete and key failure paths. + +### Root + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 0 controllers, 8 services, 1 requests, 1 resources. + +**Services:** FeeCalculationService, SemesterRangeService, ApplicationUrlService, SchoolIdService, AssignmentService, AttendanceCommentTemplateService, PhoneFormatterService, EmailService +**Requests:** ApiFormRequest +**Resources:** TeacherSubmissionRowResource +**Plan:** +- Confirm all inputs are validated through FormRequest classes. +- Confirm all reads and writes are authorized. +- Move business rules out of controllers if any remain. +- Add feature tests for list/show/create/update/delete and key failure paths. + +### School + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 0 controllers, 4 services, 0 requests, 0 resources. + +**Services:** AccountEventService, SemesterSelectionService, EnrollmentEventService, PaymentEventService +**Plan:** +- Confirm config reads/writes are permissioned and validated. +- Add health/config smoke tests. +- Document public versus authenticated surfaces. +- Keep UI/style/static-page services separated from domain mutations. + +### SchoolIds + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 0 controllers, 2 services, 1 requests, 1 resources. + +**Services:** SchoolIdAssignmentService, SchoolIdGenerationService +**Requests:** SchoolIdAssignRequest +**Resources:** SchoolIdResource +**Plan:** +- Confirm config reads/writes are permissioned and validated. +- Add health/config smoke tests. +- Document public versus authenticated surfaces. +- Keep UI/style/static-page services separated from domain mutations. + +### Scores + +**Priority:** P1 Student lifecycle +**Detected surface:** 9 controllers, 16 services, 10 requests, 3 resources. + +**Controllers:** FinalController, HomeworkController, MidtermController, ParticipationController, ProjectController, QuizController, ScoreCommentController, ScoreController, ScorePredictorController +**Services:** ParticipationScoreService, QuizCalculator, QuizScoreService, ScoreCommentService, ScoreDashboardService, HomeworkCalculator, SemesterScoreService, ScorePredictorRiskService, ProjectScoreService, HomeworkScoreService, ScoreTermService, ProjectCalculator ... +**Requests:** ScoreCommentSaveRequest, ScorePredictorRequest, ScoreLockRequest, ScoreUpdateRequest, ScoreOverviewRequest, ScoreStudentRequest, ScoreCommentListRequest, ScoreClassRequest ... +**Resources:** ScorePredictorStudentResource, ScoreCommentResource, ScoreStudentResource +**Plan:** +- Centralize score formulas and missing-score behavior. +- Add golden test cases for semester score, quiz, homework, project, participation, final/midterm, and prediction risk calculations. +- Prove grading locks block writes but not allowed reads. +- Verify below-60 communication and parent visibility rules. + +### Security + +**Priority:** P0 Security/Foundation +**Detected surface:** 0 controllers, 3 services, 3 requests, 2 resources. + +**Services:** IpBanCommandService, IpBanQueryService, Pbkdf2Hasher +**Requests:** IpBanIndexRequest, IpUnbanRequest, IpBanRequest +**Resources:** IpBanCollection, IpBanResource +**Plan:** +- Map every endpoint to required auth state, role, and permission. +- Test login throttling, IP bans, password reset cleanup, session timeout, role switching, and registration captcha. +- Confirm security logs include actor, IP, user agent, outcome, and reason. +- Reject role changes that create privilege escalation or stale-session leaks. + +### Semesters + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 0 controllers, 1 services, 4 requests, 2 resources. + +**Services:** SemesterConfigService +**Requests:** SchoolYearRangeRequest, SemesterResolveRequest, SemesterNormalizeRequest, SemesterRangeRequest +**Resources:** SemesterResolveResource, SemesterRangeResource +**Plan:** +- Confirm config reads/writes are permissioned and validated. +- Add health/config smoke tests. +- Document public versus authenticated surfaces. +- Keep UI/style/static-page services separated from domain mutations. + +### Settings + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 6 controllers, 8 services, 8 requests, 5 resources. + +**Controllers:** ConfigurationAdminController, EventController, PolicyController, PreferencesController, SchoolCalendarController, SettingsController +**Services:** SettingsService, ConfigurationService, SchoolCalendarMutationService, SchoolCalendarNotificationService, SchoolCalendarContextService, SchoolCalendarQueryService, SchoolCalendarFormatterService, SchoolCalendarMeetingService +**Requests:** SchoolCalendarStoreRequest, ConfigurationStoreRequest, SettingsUpdateRequest, SchoolCalendarIndexRequest, ConfigurationUpdateRequest, SchoolCalendarUpdateRequest, PolicyShowRequest, SchoolCalendarOptionsRequest +**Resources:** PolicyResource, SchoolCalendarEventResource, SettingsResource, ConfigurationResource, SchoolCalendarEventDetailResource +**Plan:** +- Confirm config reads/writes are permissioned and validated. +- Add health/config smoke tests. +- Document public versus authenticated surfaces. +- Keep UI/style/static-page services separated from domain mutations. + +### Staff + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 3 controllers, 3 services, 3 requests, 2 resources. + +**Controllers:** StaffController, TeacherController, TimeOffNotificationController +**Services:** StaffQueryService, StaffTimeOffLinkService, StaffCommandService +**Requests:** StaffIndexRequest, StaffUpdateRequest, StaffStoreRequest +**Resources:** StaffCollection, StaffResource +**Plan:** +- Centralize identity lookup and cross-entity authorization. +- Test directory/search pagination and visibility filtering. +- Ensure updates cannot orphan related records or break family/student/staff links. +- Add lifecycle tests for active, inactive, newly enrolled, transferred, and deleted/unverified users. + +### Students + +**Priority:** P1 Student lifecycle +**Detected surface:** 1 controllers, 7 services, 7 requests, 4 resources. + +**Controllers:** StudentController +**Services:** StudentScoreCardService, StudentStatusService, StudentDirectoryService, StudentAutoDistributionService, StudentConfigService, StudentProfileService, StudentAssignmentService +**Requests:** StudentRemoveClassRequest, StudentUpdateRequest, StudentAssignClassRequest, StudentSetActiveRequest, StudentPromotionTotalsRequest, StudentAssignmentListRequest, StudentAutoDistributeRequest +**Resources:** StudentAssignmentResource, StudentRemovedResource, StudentClassSectionResource, StudentScoreCardRowResource +**Plan:** +- Centralize identity lookup and cross-entity authorization. +- Replace any `max(id) + 1` style school ID generation with persisted-ID or database-sequence-based generation. +- Add a unique database constraint on `students.school_id` and retry logic for rare collisions. +- Test directory/search pagination and visibility filtering. +- Ensure updates cannot orphan related records or break family/student/staff links. +- Add lifecycle tests for active, inactive, newly enrolled, transferred, and deleted/unverified users. + +#### Student creation acceptance criteria + +- Student creation runs in a transaction. +- School ID is generated only after the student has a persisted ID or from a dedicated sequence. +- Concurrent student creation cannot create duplicate school IDs. +- Parent/family/student visibility tests prove users cannot create, view, or mutate records outside their boundary. + +### Subjects + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 1 controllers, 1 services, 2 requests, 2 resources. + +**Controllers:** SubjectCurriculumController +**Services:** SubjectCurriculumService +**Requests:** SubjectCurriculumUpdateRequest, SubjectCurriculumStoreRequest +**Resources:** SubjectCurriculumResource, SubjectClassResource +**Plan:** +- Define the academic workflow state machine and valid transitions. +- Test teacher/admin/parent role boundaries. +- Protect uploads and generated academic files. +- Add audit logs for status changes, submissions, and deadline changes. + +### Support + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 2 controllers, 2 services, 3 requests, 3 resources. + +**Controllers:** ContactController, SupportController +**Services:** SupportRequestService, ContactMessageService +**Requests:** SupportRequestStoreRequest, SupportRequestIndexRequest, ContactSendRequest +**Resources:** ContactMessageResource, SupportRequestResource, SupportRequestCollection +**Plan:** +- Confirm all inputs are validated through FormRequest classes. +- Confirm all reads and writes are authorized. +- Move business rules out of controllers if any remain. +- Add feature tests for list/show/create/update/delete and key failure paths. + +### System + +**Priority:** P0 Security/Foundation +**Detected surface:** 9 controllers, 6 services, 2 requests, 4 resources. + +**Controllers:** AccessDeniedController, DashboardController, DashboardRedirectController, DatabaseHealthController, HealthController, NavBuilderController, SchoolIdController, SemesterRangeController, StatsController +**Services:** HealthCheckService, CleanupSchedulerService, GlobalConfigService, DatabaseHealthService, ConfigUpdateService, TimeService +**Requests:** NavItemStoreRequest, NavItemReorderRequest +**Resources:** DashboardRouteResource, NavItemResource, HealthStatusResource, NavBuilderDataResource +**Plan:** +- Confirm config reads/writes are permissioned and validated. +- Add health/config smoke tests. +- Document public versus authenticated surfaces. +- Keep UI/style/static-page services separated from domain mutations. + +### Teachers + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 0 controllers, 4 services, 6 requests, 4 resources. + +**Services:** TeacherAbsenceService, TeacherDashboardService, TeacherConfigService, TeacherAssignmentService +**Requests:** TeacherClassViewRequest, TeacherAbsenceSubmitRequest, TeacherAssignmentDeleteRequest, TeacherAssignmentStoreRequest, TeacherAssignmentListRequest, TeacherSwitchClassRequest +**Resources:** TeacherAbsenceResource, TeacherStudentResource, TeacherClassResource, TeacherAssignmentResource +**Plan:** +- Centralize identity lookup and cross-entity authorization. +- Test directory/search pagination and visibility filtering. +- Ensure updates cannot orphan related records or break family/student/staff links. +- Add lifecycle tests for active, inactive, newly enrolled, transferred, and deleted/unverified users. + +### Ui + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 1 controllers, 1 services, 1 requests, 1 resources. + +**Controllers:** UiController +**Services:** UiStyleService +**Requests:** UiStyleRequest +**Resources:** UiStyleResource +**Plan:** +- Confirm config reads/writes are permissioned and validated. +- Add health/config smoke tests. +- Document public versus authenticated surfaces. +- Keep UI/style/static-page services separated from domain mutations. + +### Users + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 1 controllers, 6 services, 4 requests, 2 resources. + +**Controllers:** UserController +**Services:** UserListService, LoginActivityService, InactiveUserCleanupService, UserManagementService, UserEventService, DeleteUnverifiedUserService +**Requests:** UserStoreRequest, UserListRequest, UserUpdateRequest, LoginActivityRequest +**Resources:** LoginActivityResource, UserWithRolesResource +**Plan:** +- Confirm all inputs are validated through FormRequest classes. +- Confirm all reads and writes are authorized. +- Move business rules out of controllers if any remain. +- Add feature tests for list/show/create/update/delete and key failure paths. + +### Utilities + +**Priority:** P2/P3 Feature hardening +**Detected surface:** 2 controllers, 0 services, 1 requests, 1 resources. + +**Controllers:** PhoneFormatterController, ProofreadController +**Requests:** PhoneFormatRequest +**Resources:** PhoneFormatResource +**Plan:** +- Confirm config reads/writes are permissioned and validated. +- Add health/config smoke tests. +- Document public versus authenticated surfaces. +- Keep UI/style/static-page services separated from domain mutations. + +### Whatsapp + +**Priority:** P1 Communications +**Detected surface:** 0 controllers, 8 services, 7 requests, 5 resources. + +**Services:** WhatsappContactService, WhatsappInviteService, WhatsappLinkService, WhatsappContextService, WhatsappInviteNotificationService, WhatsappMembershipService, WhatsappInviteBundleService, WhatsappInviteEmailService +**Requests:** WhatsappLinkIndexRequest, WhatsappMembershipUpdateRequest, WhatsappLinkStoreRequest, WhatsappLinkUpdateRequest, WhatsappParentContactsRequest, WhatsappParentContactsByClassRequest, WhatsappInviteSendRequest +**Resources:** WhatsappParentContactResource, WhatsappGroupLinkResource, WhatsappClassSectionParentResource, WhatsappGroupLinkCollection, WhatsappClassSectionContactResource +**Plan:** +- Separate recipient selection, message composition, dispatch, and logging. +- Add preview/dry-run tests before bulk sending. +- Deduplicate sends and persist failure states. +- Verify opt-outs, guardian selection, role targeting, and retry behavior. + + +## 7. Cross-Cutting Test Matrix + +| Area | Minimum tests | Failure tests | Regression traps | +|---|---:|---:|---| +| Auth / Security | Login, refresh, logout, role switch, timeout | Invalid creds, banned IP, stale role, unauthorized role | Privilege escalation, session confusion | +| Finance / Payments | Invoice generation, manual payment, PayPal sync, refunds | Duplicate webhook, failed payment, partial refund | Balance mismatch, rounding drift | +| Attendance | Teacher submit, admin edit, parent report | Out-of-window edit, unauthorized class, duplicate submit | Semester boundary, timezone/date errors | +| Grading / Scores | Score calculations, locks, below-60 notification | Missing score, locked write, invalid class | Formula drift, notification spam | +| Students / Families | Profile, guardians, enrollments, authorized users | Cross-family read/write, inactive student | Orphan relationships | +| Communications | Preview, send, log, retry | Invalid recipient, opt-out, duplicate send | Bulk-send mistakes | +| Files / Reports | Upload, download, PDF generation | Bad MIME, oversize file, unauthorized file | Path traversal, stale report data | +| Inventory / Ops | Item movement, PO receive, print requests | Negative stock, duplicate receive | Count mismatch | + + +## 8. Refactor Rules + +- Do not start by renaming everything. That is not architecture; that is sweeping broken glass into a different corner. +- Start with tests around the highest-risk behavior, then refactor behind those tests. +- One module per pull request unless a shared contract demands otherwise. +- For each PR, include: risk summary, affected endpoints, permission impact, migration impact, rollback path, and test evidence. +- Any service over roughly 300 lines should be reviewed for split points, but line count alone is not a crime. Unclear responsibility is the crime. +- Any controller doing calculations, direct DB orchestration, or multi-step workflow should delegate to a service. +- Any duplicated request validation should be consolidated into shared rules or dedicated FormRequests. + +## 9. Immediate Next Work Queue + +Work in this order. Alphabetical cleanup is banned until P0 is closed, because alphabetical order has never once saved a ledger. + +1. Patch the plan into issue tickets for the seven P0 items in the Critical Implementation Review Queue. +2. Add route inventory from `routes/api.php` and `routes/web.php` once the full repo is available. +3. Produce an endpoint-permission matrix from routes + controllers + policies. +4. Write failing/covering tests for payment file access, manual payment edit transaction safety, student school ID concurrency, scanner idempotency, role mutation authorization, bulk communication recipient safety, and refund/payment/invoice auditing. +5. Implement finance/file authorization fixes before controller refactors. +6. Implement transaction/locking fixes for payments, invoices, refunds, and reimbursements. +7. Implement student school ID generation fix and database uniqueness constraint. +8. Extract scanner services and add duplicate-scan/idempotency behavior. +9. Add baseline feature tests for Auth, Finance/Payments, Attendance, Grading/Scores, Students/Families, and Communications. +10. Create factories and seed data for the core school domain. +11. Add OpenAPI generation to CI using the existing Docs/OpenAPI service surface. +12. Review raw SQL and direct request access matches before any large refactor. +13. Add idempotency and audit requirements to payment, invoice, refund, promotion, and messaging mutations. +14. Write runbooks for console commands and operational cleanup jobs. + +## 10. Definition of Done + +A module is not considered done until all of this is true: + +- All public endpoints have route docs and permission mapping. +- All inputs use FormRequests or clearly justified validation. +- All responses use Resources or documented response builders. +- All mutations are transactional where consistency matters. +- All side effects are logged and testable. +- All critical behavior has feature tests and at least one negative authorization test. +- All money/date/file/message behavior has edge-case tests. +- No endpoint relies on undocumented legacy behavior unless that behavior is explicitly preserved and tested. + +## Appendix A: Controller Groups + +- **Administrator:** 8 controllers. AdministratorAbsenceController, AdministratorDashboardController, AdministratorEnrollmentController, AdministratorNotificationController, AdministratorPromotionController, AdministratorTeacherSubmissionController, EmergencyContactController, TeacherClassAssignmentController +- **ApiRoot:** 7 controllers. BaseApiController, CiRequestAdapter, RolePermissionController, RoleSwitcherController, ScannerController, StatsController, UserController +- **Assignment:** 1 controllers. AssignmentApiController +- **Attendance:** 6 controllers. AdminAttendanceApiController, AttendanceCommentTemplateController, EarlyDismissalsController, LateSlipLogsController, StaffAttendanceApiController, TeacherAttendanceApiController +- **AttendanceTracking:** 1 controllers. AttendanceTrackingController +- **Auth:** 7 controllers. AuthController, AuthSessionController, IpBanController, RegisterController, RolePermissionController, RoleSwitcherController, SessionTimeoutController +- **BadgeScan:** 1 controllers. BadgeScanController +- **Badges:** 1 controllers. BadgeController +- **Certificates:** 1 controllers. CertificateController +- **ClassPreparation:** 1 controllers. ClassPreparationController +- **ClassProgress:** 1 controllers. ClassProgressController +- **Classes:** 1 controllers. ClassController +- **Communication:** 1 controllers. CommunicationController +- **CompetitionScores:** 1 controllers. CompetitionScoresController +- **Core:** 2 controllers. BaseApiController, CiRequestAdapter +- **Discounts:** 1 controllers. DiscountController +- **Documentation:** 2 controllers. ApiDocsAdminController, DocsCatalogController +- **Email:** 3 controllers. BroadcastEmailController, EmailController, EmailExtractorController +- **Exams:** 1 controllers. ExamDraftController +- **Expenses:** 1 controllers. ExpenseController +- **ExtraCharges:** 1 controllers. ExtraChargesController +- **Family:** 2 controllers. FamilyAdminController, FamilyController +- **Finance:** 14 controllers. ChargeController, FeeCalculationController, FinancialController, InvoiceController, PaymentController, PaymentEventChargesController, PaymentManualController, PaymentNotificationController, PaymentTransactionController, PaypalPaymentController, PaypalTransactionsController, PurchaseOrderController, RefundController, ReimbursementController +- **Frontend:** 4 controllers. FrontendController, InfoIconController, LandingPageController, PageController +- **Grading:** 2 controllers. GradingController, HomeworkTrackingController +- **Incidents:** 1 controllers. IncidentController +- **Inventory:** 5 controllers. InventoryCategoryController, InventoryController, InventoryMovementController, SupplierController, SupplyCategoryController +- **Messaging:** 2 controllers. MessagesController, WhatsappController +- **NonApi:** 3 controllers. Controller, DocsController, TeacherCalendarPageController +- **Notifications:** 1 controllers. NotificationController +- **Parents:** 5 controllers. AuthorizedUserInviteController, AuthorizedUsersController, ParentAttendanceReportController, ParentController, ParentPromotionController +- **PrintRequests:** 1 controllers. PrintRequestsController +- **Public:** 1 controllers. PublicWinnersController +- **Reports:** 4 controllers. FilesController, ReportCardsController, SlipPrinterController, StickersController +- **Scores:** 9 controllers. FinalController, HomeworkController, MidtermController, ParticipationController, ProjectController, QuizController, ScoreCommentController, ScoreController, ScorePredictorController +- **Settings:** 6 controllers. ConfigurationAdminController, EventController, PolicyController, PreferencesController, SchoolCalendarController, SettingsController +- **Staff:** 3 controllers. StaffController, TeacherController, TimeOffNotificationController +- **Students:** 1 controllers. StudentController +- **Subjects:** 1 controllers. SubjectCurriculumController +- **Support:** 2 controllers. ContactController, SupportController +- **System:** 9 controllers. AccessDeniedController, DashboardController, DashboardRedirectController, DatabaseHealthController, HealthController, NavBuilderController, SchoolIdController, SemesterRangeController, StatsController +- **Ui:** 1 controllers. UiController +- **Users:** 1 controllers. UserController +- **Utilities:** 2 controllers. PhoneFormatterController, ProofreadController + +## Appendix B: Service Groups + +- **Admin:** 2 services, 1,089 LOC. CompetitionWinnersDomain, CompetitionWinnersAdminService +- **Administrator:** 18 services, 2,196 LOC. AdministratorUserSearchService, AdministratorAbsenceService, AdministratorDashboardService, AdministratorMetricsService, AdministratorEnrollmentStatusService, AdministratorEnrollmentEventService, AdministratorNotificationService, AdminNotificationSubjectService, TeacherSubmissionSupportService, AdministratorSharedService, TeacherSubmissionNotificationService, TeacherSubmissionReportService, AdminNotificationUserService, AdministratorEnrollmentRefundService, AdminPrintRecipientService, AdministratorTeacherSubmissionService, AdministratorEnrollmentService, AdministratorEnrollmentQueryService +- **Assignment:** 5 services, 421 LOC. AssignmentSectionService, AssignmentMetaService, AssignmentService, AssignmentContextService, AssignmentDataLoaderService +- **Attendance:** 16 services, 3,241 LOC. SemesterRangeService, TeacherAttendanceSubmissionService, AttendanceAutoPublishJobService, AttendanceAutoPublishService, LateSlipLogQueryService, AttendanceConsequenceService, AttendanceRecordSyncService, AttendanceDailySummaryService, AttendanceService, StudentAttendanceWriterService, LateSlipLogCommandService, AttendanceSummaryRebuildService, StaffAttendanceService, AttendanceQueryService, AttendancePolicyService, AttendanceCommentService +- **AttendanceTracking:** 12 services, 2,979 LOC. AttendanceNotificationLogService, AttendanceViolationStudentResolverService, AttendanceNotificationWorkflowService, AttendanceTrackingService, ViolationRuleEngineService, AttendanceMailerService, AttendanceParentLookupService, AttendanceCommunicationSupportService, AttendanceEmailComposerService, AttendancePendingViolationService, AttendanceCaseQueryService, DefaultAttendanceMailerService +- **Auth:** 9 services, 870 LOC. RegistrationCaptchaService, PermissionCheckService, CodeIgniterApiRegistrationService, RegistrationService, ApiLoginSecurityService, AuthSessionService, UserRoleService, PasswordResetCleanupService, RegistrationFormatterService +- **BadgeScan:** 1 services, 108 LOC. BadgeScanService +- **Badges:** 7 services, 2,815 LOC. BadgeTextFormatter, BadgeStudentLookupService, BadgeFormDataService, BadgePdfService, BadgeUserLookupService, BadgeService, BadgePrintLogService +- **Billing:** 3 services, 751 LOC. BalanceCalculationService, BillingTotalsService, ChargeService +- **BroadcastEmail:** 5 services, 241 LOC. BroadcastEmailComposerService, BroadcastEmailRecipientService, BroadcastEmailDispatchService, BroadcastEmailImageService, BroadcastEmailSenderOptionsService +- **Certificates:** 1 services, 144 LOC. CertificatePdfService +- **ClassPrep:** 2 services, 205 LOC. ClassRosterService, StickerCountService +- **ClassPreparation:** 9 services, 653 LOC. ClassPreparationAdjustmentWriterService, ClassPreparationCalculatorService, ClassPreparationLogService, ClassPreparationAdjustmentService, ClassPreparationContextService, ClassPreparationService, ClassPreparationPrintService, ClassPreparationRosterService, ClassPreparationInventoryService +- **ClassProgress:** 5 services, 941 LOC. ClassProgressRuleService, ClassProgressAttachmentService, ClassProgressQueryService, ClassProgressMetaService, ClassProgressMutationService +- **ClassSections:** 4 services, 190 LOC. ClassSectionCommandService, ClassAttendanceService, ClassSectionSeedService, ClassSectionQueryService +- **Communication:** 6 services, 374 LOC. CommunicationFamilyService, CommunicationSendService, CommunicationTemplateService, CommunicationPreviewService, CommunicationStudentService, CommunicationLogService +- **CompetitionScores:** 4 services, 325 LOC. CompetitionScoresRosterService, CompetitionScoresSaveService, CompetitionScoresContextService, CompetitionScoresQueryService +- **Dashboard:** 1 services, 70 LOC. DashboardRouteService +- **Discounts:** 5 services, 792 LOC. DiscountParentService, DiscountApplyService, DiscountInvoiceService, DiscountVoucherService, DiscountContextService +- **Docs:** 3 services, 244 LOC. DocsCatalogService, ApiDocsService, OpenApiRouteExporter +- **Email:** 5 services, 395 LOC. EmailAttachmentService, EmailExtractorService, EmailProfileService, EmailDispatchService, EmailSenderOptionsService +- **EmergencyContacts:** 2 services, 142 LOC. EmergencyContactDirectoryService, EmergencyContactCrudService +- **Events:** 6 services, 496 LOC. EventChargeService, EventStudentChargeService, EventCategoryService, EventListService, EventChargeQueryService, EventManagementService +- **Exams:** 4 services, 532 LOC. ExamDraftTeacherService, ExamDraftAdminService, ExamDraftConfigService, ExamDraftFileService +- **Expenses:** 7 services, 231 LOC. ExpenseQueryService, ExpenseStaffService, ExpenseRetailorService, ExpenseReceiptService, ExpenseWriteService, ExpenseContextService, ExpenseStatusService +- **ExtraCharges:** 6 services, 475 LOC. ExtraChargesMetaService, ExtraChargesInvoiceService, ExtraChargesParentService, ExtraChargesChargeService, ExtraChargesContextService, ExtraChargesListService +- **Families:** 4 services, 827 LOC. FamilyNotificationService, FamilyFinanceService, FamilyMutationService, FamilyQueryService +- **Fees:** 6 services, 952 LOC. FeeConfigService, TuitionCalculationService, FeeRefundCalculatorService, FeeGradeService, FeeRefundDetailService, FeeStudentFeeService +- **Files:** 2 services, 159 LOC. FileServeService, ExamDraftDownloadNameService +- **Finance:** 8 services, 1,150 LOC. FinancialPdfReportService, FinancialSchoolYearService, FinancialPaymentService, FinancialReportService, FinancialSummaryService, FinancialChartService, FinancialDonationRecipientService, FinancialUnpaidParentsService +- **Frontend:** 9 services, 1,011 LOC. LandingPageTeacherSummaryService, LandingPageRoleService, LandingPageParentDashboardService, LandingPageService, StaticPageService, FrontendPageService, ContactSubmissionService, LandingPageContextService, ProfileIconService +- **Grading:** 9 services, 1,969 LOC. BelowSixtyEmailService, GradingLockService, GradingOverviewService, GradingPlacementService, HomeworkTrackingService, GradingScoreService, GradingBelowSixtyService, HomeworkTrackingCalendarService, GradingRefreshService +- **Incidents:** 4 services, 499 LOC. CurrentIncidentService, IncidentHistoryService, IncidentLookupService, IncidentAnalysisService +- **Inventory:** 8 services, 1,449 LOC. InventorySummaryService, InventoryCategoryService, InventoryItemService, InventoryContextService, SupplierService, InventoryMovementService, SupplyCategoryService, InventoryTeacherDistributionService +- **Invoices:** 8 services, 1,248 LOC. InvoiceGradeService, InvoiceConfigService, InvoicePaymentService, InvoiceTuitionService, InvoiceManagementService, InvoicePdfService, InvoiceService, InvoiceGenerationService +- **Messaging:** 3 services, 334 LOC. MessageRecipientService, MessageCommandService, MessageQueryService +- **Navigation:** 2 services, 273 LOC. NavBuilderService, NavbarService +- **Notifications:** 12 services, 451 LOC. NotificationDispatchService, NotificationRecipientService, NotificationShowService, NotificationManagementService, NotificationReadService, NotificationUserListService, NotificationCleanupService, UserNotificationDispatchService, NotificationTriggerService, NotificationSendService, NotificationActiveService, NotificationDeletedService +- **Parents:** 14 services, 2,306 LOC. ParentEventParticipationService, ParentRegistrationService, PrimaryParentUserResolver, ParentAttendanceReportCalendarService, ParentAttendanceService, AuthorizedUsersManagementService, ParentEnrollmentService, ParentInvoiceService, ParentProgressQueryService, ParentAttendanceReportService, ParentReportCardService, ParentConfigService, ParentProfileService, ParentEmergencyContactService +- **Payments:** 14 services, 2,096 LOC. PaymentNotificationDispatchService, PaymentMissedCheckService, PaymentManualService, PaymentLookupService, PaymentTestNotificationService, PaypalPaymentSyncService, PaymentBalanceService, PaypalPaymentService, PaymentTransactionService, PaymentEnrollmentEventService, PaymentNotificationService, PaymentPlanService, PaymentEventChargesService, PaypalTransactionsService +- **Phone:** 1 services, 23 LOC. PhoneFormatterService +- **Policy:** 1 services, 51 LOC. PolicyContentService +- **Preferences:** 3 services, 190 LOC. PreferencesCommandService, PreferencesQueryService, PreferencesOptionsService +- **PrintRequests:** 1 services, 558 LOC. PrintRequestsPortalService +- **Promotions:** 7 services, 1,740 LOC. PromotionStatusService, PromotionEnrollmentService, PromotionReminderService, LevelProgressionService, PromotionAuditService, PromotionQueryService, PromotionEligibilityService +- **PublicWinners:** 1 services, 115 LOC. PublicWinnersPortalService +- **PurchaseOrders:** 4 services, 252 LOC. PurchaseOrderStatusService, PurchaseOrderReceiveService, PurchaseOrderCreateService, PurchaseOrderQueryService +- **Refunds:** 9 services, 1,210 LOC. RefundSummaryService, RefundInvoiceAdjustmentService, RefundPolicyService, RefundPayoutService, RefundRequestService, RefundQueryService, RefundOverpaymentService, RefundDecisionService, RefundNotificationService +- **Reimbursements:** 11 services, 1,776 LOC. ReimbursementFileService, ReimbursementBatchAssignmentService, ReimbursementContextService, ReimbursementExportService, ReimbursementLookupService, ReimbursementEmailService, ReimbursementRecipientService, ReimbursementBatchService, ReimbursementQueryService, ReimbursementDonationService, ReimbursementCrudService +- **Reports:** 9 services, 2,445 LOC. SlipPrinterFormatterService, SlipPrinterPdfService, SlipPrinterService, SlipPrinterConfigService, StickerPrintService, StickerPresetService, StickerQueryService, StickerContextService, ReportCardService +- **Roles:** 8 services, 545 LOC. RolePermissionService, RoleDashboardService, RoleCrudService, RoleAssignmentService, RoleSwitchService, RoleStaffService, PermissionCrudService, RoleQueryService +- **Root:** 8 services, 550 LOC. FeeCalculationService, SemesterRangeService, ApplicationUrlService, SchoolIdService, AssignmentService, AttendanceCommentTemplateService, PhoneFormatterService, EmailService +- **School:** 4 services, 990 LOC. AccountEventService, SemesterSelectionService, EnrollmentEventService, PaymentEventService +- **SchoolIds:** 2 services, 41 LOC. SchoolIdAssignmentService, SchoolIdGenerationService +- **Scores:** 16 services, 2,846 LOC. ParticipationScoreService, QuizCalculator, QuizScoreService, ScoreCommentService, ScoreDashboardService, HomeworkCalculator, SemesterScoreService, ScorePredictorRiskService, ProjectScoreService, HomeworkScoreService, ScoreTermService, ProjectCalculator, ScorePredictorDataService, ScorePredictorService, ExamScoreService, AttendanceCalculator +- **Security:** 3 services, 171 LOC. IpBanCommandService, IpBanQueryService, Pbkdf2Hasher +- **Semesters:** 1 services, 42 LOC. SemesterConfigService +- **Settings:** 8 services, 656 LOC. SettingsService, ConfigurationService, SchoolCalendarMutationService, SchoolCalendarNotificationService, SchoolCalendarContextService, SchoolCalendarQueryService, SchoolCalendarFormatterService, SchoolCalendarMeetingService +- **Staff:** 3 services, 325 LOC. StaffQueryService, StaffTimeOffLinkService, StaffCommandService +- **Students:** 7 services, 1,254 LOC. StudentScoreCardService, StudentStatusService, StudentDirectoryService, StudentAutoDistributionService, StudentConfigService, StudentProfileService, StudentAssignmentService +- **Subjects:** 1 services, 80 LOC. SubjectCurriculumService +- **Support:** 2 services, 123 LOC. SupportRequestService, ContactMessageService +- **System:** 6 services, 353 LOC. HealthCheckService, CleanupSchedulerService, GlobalConfigService, DatabaseHealthService, ConfigUpdateService, TimeService +- **Teachers:** 4 services, 641 LOC. TeacherAbsenceService, TeacherDashboardService, TeacherConfigService, TeacherAssignmentService +- **Ui:** 1 services, 95 LOC. UiStyleService +- **Users:** 6 services, 647 LOC. UserListService, LoginActivityService, InactiveUserCleanupService, UserManagementService, UserEventService, DeleteUnverifiedUserService +- **Whatsapp:** 8 services, 1,407 LOC. WhatsappContactService, WhatsappInviteService, WhatsappLinkService, WhatsappContextService, WhatsappInviteNotificationService, WhatsappMembershipService, WhatsappInviteBundleService, WhatsappInviteEmailService + +## Appendix C: Request Groups + +- **Admin:** 3 requests. StoreCompetitionWinnerRequest, UpdateCompetitionWinnerRequest, SaveCompetitionScoresRequest +- **Administrator:** 5 requests. SavePrintRecipientsRequest, SaveAdminNotificationSubjectsRequest, SubmitAdministratorAbsenceRequest, UpdateEnrollmentStatusesRequest, SendTeacherSubmissionNotificationsRequest +- **Api:** 4 requests. SaveAdminNotificationSubjectsRequest, IndexAdminProgressRequest, SendTeacherSubmissionNotificationsRequest, SavePrintNotificationRecipientsRequest +- **Assignment:** 2 requests. StoreAssignmentRequest, StoreStudentAssignmentRequest +- **Attendance:** 7 requests. SaveStaffAttendanceCellRequest, UpdateAttendanceManagementRequest, LateSlipLogIndexRequest, TeacherSubmitAttendanceRequest, SaveAdminAttendanceRequest, TeacherUpdateGridRequest, AdminAddAttendanceEntryRequest +- **AttendanceCommentTemplate:** 2 requests. StoreAttendanceCommentTemplateRequest, UpdateAttendanceCommentTemplateRequest +- **AttendanceTracking:** 5 requests. RecordAttendanceTrackingRequest, ComposeAttendanceEmailRequest, SendAttendanceManualEmailRequest, ParentsInfoRequest, SaveAttendanceNotificationNoteRequest +- **Auth:** 3 requests. RegisterRequest, SetRoleSessionRequest, LoginSessionRequest +- **Badges:** 3 requests. BadgePrintStatusRequest, GenerateBadgePdfRequest, LogBadgePrintRequest +- **ClassPreparation:** 4 requests. ClassPreparationMarkPrintedRequest, ClassPreparationPrintRequest, ClassPreparationIndexRequest, ClassPreparationAdjustmentRequest +- **ClassProgress:** 5 requests. ClassProgressStoreRequest, ClassProgressMetaRequest, ClassProgressIndexRequest, ClassProgressFormRequest, ClassProgressUpdateRequest +- **Classes:** 3 requests. ClassSectionUpdateRequest, ClassSectionStoreRequest, ClassSectionIndexRequest +- **Discounts:** 3 requests. UpdateDiscountVoucherRequest, ApplyDiscountVoucherRequest, StoreDiscountVoucherRequest +- **Email:** 2 requests. EmailExtractorCompareRequest, EmailSendRequest +- **EmergencyContacts:** 2 requests. EmergencyContactParentRequest, EmergencyContactUpdateRequest +- **Events:** 6 requests. EventIndexRequest, EventUpdateRequest, EventStoreRequest, EventStudentsWithChargesRequest, EventChargeUpdateRequest, EventChargeIndexRequest +- **Exams:** 3 requests. ExamDraftTeacherStoreRequest, ExamDraftAdminLegacyRequest, ExamDraftAdminReviewRequest +- **Expenses:** 3 requests. UpdateExpenseRequest, StoreExpenseRequest, UpdateExpenseStatusRequest +- **ExtraCharges:** 2 requests. UpdateExtraChargeRequest, StoreExtraChargeRequest +- **Families:** 14 requests. FamilyImportLegacyRequest, FamilyUnlinkStudentRequest, FamilySetPrimaryHomeRequest, FamilyAdminCardRequest, FamiliesByStudentRequest, FamilyBootstrapRequest, FamilyAttachSecondByEmailRequest, FamilyAdminSearchRequest, FamilySetGuardianFlagsRequest, FamilyAdminIndexRequest, FamilyComposeEmailRequest, FamilyUnlinkGuardianRequest, GuardiansByFamilyRequest, FamilyAttachSecondByUserRequest +- **Fees:** 2 requests. FeeTuitionTotalRequest, FeeRefundRequest +- **Files:** 1 requests. FileNameRequest +- **Finance:** 12 requests. FinancialUnpaidParentsRequest, FeeTuitionTotalRequest, ChargeListRequest, FinancialReportRequest, FinancialSummaryRequest, MarkEventChargePaidRequest, FeeTuitionBreakdownRequest, StoreExtraChargeRequest, StoreEventChargeRequest, FeeFamilyBalanceRequest, ApplyExtraChargeRequest, FeeRefundRequest +- **Frontend:** 2 requests. LandingTeacherRequest, ContactSubmitRequest +- **Grading:** 18 requests. BelowSixtyListRequest, BelowSixtySendRequest, GradingLockAllRequest, BelowSixtyMeetingRequest, PlacementLevelsRequest, GradingRefreshRequest, GradingScoreShowRequest, HomeworkTrackingRequest, GradingScoreUpdateRequest, PlacementRequest, PlacementBatchUpdateRequest, BelowSixtyEmailRequest, BelowSixtyStatusRequest, PlacementBatchRequest, GradingLockRequest, PlacementLevelRequest, BelowSixtyMeetingSaveRequest, GradingOverviewRequest +- **Incidents:** 5 requests. IncidentCancelRequest, IncidentCloseRequest, IncidentStoreRequest, IncidentListRequest, IncidentStateRequest +- **Inventory:** 19 requests. InventoryItemIndexRequest, SupplierUpdateRequest, InventoryCategoryStoreRequest, InventoryTeacherDistributionRequest, InventoryItemUpdateRequest, InventoryItemStoreRequest, InventoryTeacherDistributionStoreRequest, InventoryCategoryUpdateRequest, SupplyCategoryStoreRequest, InventoryCategoryIndexRequest, InventoryAdjustRequest, InventoryAuditRequest, SupplierIndexRequest, InventoryMovementIndexRequest, InventoryMovementBulkDeleteRequest, SupplierStoreRequest, InventoryMovementUpdateRequest, InventoryMovementStoreRequest ... +- **Invoices:** 4 requests. InvoiceStatusRequest, InvoiceManagementRequest, InvoiceParentRequest, InvoiceGenerateRequest +- **Messaging:** 3 requests. MessageSendRequest, MessageIndexRequest, MessageUpdateRequest +- **Notifications:** 5 requests. NotificationSendRequest, NotificationActiveRequest, NotificationDeletedRequest, NotificationUpdateRequest, NotificationIndexRequest +- **Parents:** 16 requests. ParentAttendanceReportUpdateRequest, ParentAttendanceReportListRequest, ParentAttendanceReportSubmitRequest, ParentEmergencyContactRequest, UpdateAuthorizedUserRequest, ParentAttendanceReportCheckRequest, ParentProfileUpdateRequest, ParentInvoiceRequest, ParentEnrollmentRequest, SignParentReportCardRequest, ParentAttendanceRequest, StoreAuthorizedUserRequest, ParentStudentUpdateRequest, ParentEventParticipationRequest, ParentEnrollmentActionRequest, ParentRegistrationRequest +- **Payments:** 15 requests. PaymentManualSearchRequest, PaymentEventChargesListRequest, PaymentNotificationSendRequest, PaymentNotificationListRequest, PaymentCreateRequest, PaypalTransactionsListRequest, PaymentManualEditRequest, PaymentEventChargesStoreRequest, PaymentUpdateBalanceRequest, PaymentManualUpdateRequest, PaymentManualSuggestRequest, PaypalExecuteRequest, PaymentByParentRequest, PaymentTransactionStatusRequest, PaymentTransactionCreateRequest +- **Preferences:** 2 requests. PreferencesIndexRequest, PreferencesUpsertRequest +- **Promotions:** 8 requests. SendReminderRequest, ParentPromotionOverviewRequest, AdminListPromotionsRequest, EvaluateEligibilityRequest, UpdatePromotionStatusRequest, ParentEnrollmentStepRequest, SetEnrollmentDeadlineRequest, UpsertLevelProgressionRequest +- **PurchaseOrders:** 3 requests. PurchaseOrderIndexRequest, PurchaseOrderStoreRequest, PurchaseOrderReceiveRequest +- **Refunds:** 6 requests. RefundStoreRequest, RefundPaymentRequest, RefundDecisionRequest, RefundRecalculateRequest, RefundIndexRequest, RefundRejectRequest +- **Reimbursements:** 13 requests. ReimbursementBatchAdminFileRequest, ReimbursementUpdateRequest, ReimbursementBatchCreateRequest, ReimbursementBatchEmailRequest, ReimbursementProcessRequest, ReimbursementExportBatchRequest, ReimbursementIndexRequest, ReimbursementExportRequest, ReimbursementMarkDonationRequest, ReimbursementBatchAssignmentRequest, ReimbursementStoreRequest, ReimbursementBatchLockRequest, ReimbursementUnderProcessingRequest +- **Reports:** 11 requests. SlipPreviewRequest, SlipLogListRequest, SlipPrintRequest, SlipReprintRequest, StickerStudentsByClassRequest, StickerPrintRequest, StickerFormDataRequest, ReportCardCompletenessRequest, ReportCardPdfRequest, ReportCardAcknowledgementRequest, ReportCardMetaRequest +- **Roles:** 9 requests. AssignUserRolesRequest, RolePermissionUpdateRequest, UserRoleListRequest, PermissionUpdateRequest, PermissionStoreRequest, RoleSwitchRequest, RoleStoreRequest, RoleUpdateRequest, RoleListRequest +- **Root:** 1 requests. ApiFormRequest +- **SchoolIds:** 1 requests. SchoolIdAssignRequest +- **Scores:** 10 requests. ScoreCommentSaveRequest, ScorePredictorRequest, ScoreLockRequest, ScoreUpdateRequest, ScoreOverviewRequest, ScoreStudentRequest, ScoreCommentListRequest, ScoreClassRequest, ScoreCommentUpdateRequest, ScoreAddColumnRequest +- **Security:** 3 requests. IpBanIndexRequest, IpUnbanRequest, IpBanRequest +- **Semesters:** 4 requests. SchoolYearRangeRequest, SemesterResolveRequest, SemesterNormalizeRequest, SemesterRangeRequest +- **Settings:** 8 requests. SchoolCalendarStoreRequest, ConfigurationStoreRequest, SettingsUpdateRequest, SchoolCalendarIndexRequest, ConfigurationUpdateRequest, SchoolCalendarUpdateRequest, PolicyShowRequest, SchoolCalendarOptionsRequest +- **Staff:** 3 requests. StaffIndexRequest, StaffUpdateRequest, StaffStoreRequest +- **Students:** 7 requests. StudentRemoveClassRequest, StudentUpdateRequest, StudentAssignClassRequest, StudentSetActiveRequest, StudentPromotionTotalsRequest, StudentAssignmentListRequest, StudentAutoDistributeRequest +- **Subjects:** 2 requests. SubjectCurriculumUpdateRequest, SubjectCurriculumStoreRequest +- **Support:** 3 requests. SupportRequestStoreRequest, SupportRequestIndexRequest, ContactSendRequest +- **System:** 2 requests. NavItemStoreRequest, NavItemReorderRequest +- **Teachers:** 6 requests. TeacherClassViewRequest, TeacherAbsenceSubmitRequest, TeacherAssignmentDeleteRequest, TeacherAssignmentStoreRequest, TeacherAssignmentListRequest, TeacherSwitchClassRequest +- **Ui:** 1 requests. UiStyleRequest +- **Users:** 4 requests. UserStoreRequest, UserListRequest, UserUpdateRequest, LoginActivityRequest +- **Utilities:** 1 requests. PhoneFormatRequest +- **Whatsapp:** 7 requests. WhatsappLinkIndexRequest, WhatsappMembershipUpdateRequest, WhatsappLinkStoreRequest, WhatsappLinkUpdateRequest, WhatsappParentContactsRequest, WhatsappParentContactsByClassRequest, WhatsappInviteSendRequest + +## Appendix D: Resource Groups + +- **Admin:** 5 resources. PrintRecipientsResource, TeacherSubmissionReportResource, AdminNotificationAlertsResource, ProgressGroupResource, ProgressReportDetailResource +- **Assignment:** 2 resources. AssignmentOverviewResource, AssignmentSectionResource +- **Attendance:** 7 resources. DailyAttendanceResource, AdminAttendanceResource, LateSlipLogCollection, LateSlipLogResource, TeacherGridResource, StaffMonthResource, TeacherAttendanceFormResource +- **Auth:** 1 resources. RegisterResource +- **ClassPreparation:** 2 resources. ClassPreparationPrintResource, ClassPreparationResultResource +- **ClassProgress:** 4 resources. ClassProgressGroupCollection, ClassProgressAttachmentResource, ClassProgressShowResource, ClassProgressReportResource +- **Classes:** 4 resources. ClassSectionCollection, ClassSectionResource, ClassAttendanceResource, ClassAttendanceStudentResource +- **Discounts:** 2 resources. DiscountVoucherResource, DiscountParentResource +- **Email:** 3 resources. EmailSenderResource, EmailExtractorEmailsResource, EmailExtractorCompareResource +- **EmergencyContacts:** 3 resources. EmergencyContactGroupResource, EmergencyContactStudentResource, EmergencyContactResource +- **Events:** 3 resources. EventChargeResource, EventResource, EventStudentChargeResource +- **Exams:** 1 resources. ExamDraftResource +- **Expenses:** 2 resources. ExpenseResource, ExpenseStaffResource +- **ExtraCharges:** 3 resources. ExtraChargeInvoiceOptionResource, ExtraChargeResource, ExtraChargeParentOptionResource +- **Families:** 7 resources. FamilyPaymentResource, FamilyInvoiceResource, FamilySearchItemResource, FamilyResource, FamilyGuardianResource, FamilyStudentResource, FamilyEmergencyContactResource +- **Fees:** 6 resources. FeeRefundResource, FeeFamilyBalanceResource, ChargeActionResource, ChargeListResource, FeeTuitionBreakdownResource, FeeTuitionTotalResource +- **Files:** 1 resources. FileMetaResource +- **Finance:** 9 resources. FinancialReportResource, FinancialSummaryResource, FinancialExpenseSummaryResource, FinancialInvoiceResource, FinancialPaymentResource, FinancialRefundResource, FinancialUnpaidParentResource, FinancialDiscountResource, FinancialReimbursementSummaryResource +- **Frontend:** 6 resources. LandingPageResource, ContactSubmissionResource, FrontendPageResource, FrontendUserResource, ProfileIconResource, PageContentResource +- **Grading:** 3 resources. BelowSixtyStudentResource, GradingScoreResource, HomeworkTrackingTeacherResource +- **Incidents:** 4 resources. IncidentStudentOptionResource, IncidentAnalysisStudentResource, IncidentResource, IncidentGradeResource +- **Inventory:** 5 resources. InventoryItemResource, InventoryCategoryResource, InventoryMovementResource, SupplierResource, SupplyCategoryResource +- **Invoices:** 2 resources. InvoiceManagementParentResource, InvoiceResource +- **Messaging:** 3 resources. MessageResource, MessageCollection, RecipientResource +- **Notifications:** 2 resources. UserNotificationResource, NotificationResource +- **Parents:** 4 resources. ParentEmergencyContactResource, ParentAttendanceReportResource, ParentStudentResource, ParentAttendanceResource +- **Payments:** 4 resources. PaymentNotificationLogResource, PaymentTransactionResource, PaypalTransactionResource, PaymentResource +- **Preferences:** 3 resources. PreferencesListResource, PreferencesCollection, PreferencesResource +- **Promotions:** 4 resources. LevelProgressionResource, PromotionAuditLogResource, PromotionReminderResource, StudentPromotionResource +- **PurchaseOrders:** 2 resources. PurchaseOrderResource, PurchaseOrderItemResource +- **Refunds:** 1 resources. RefundResource +- **Reimbursements:** 5 resources. ReimbursementRecipientResource, ReimbursementExpenseResource, ReimbursementBatchResource, ReimbursementResource, ReimbursementUnderProcessingItemResource +- **Reports:** 8 resources. LateSlipLogResource, StickerStudentResource, StickerPresetResource, StickerClassSectionResource, ReportCardStudentMetaResource, ReportCardAcknowledgementResource, ReportCardClassSectionResource, ReportCardCompletenessStudentResource +- **Roles:** 4 resources. RolePermissionResource, RoleResource, UserWithRolesResource, PermissionResource +- **Root:** 1 resources. TeacherSubmissionRowResource +- **SchoolIds:** 1 resources. SchoolIdResource +- **Scores:** 3 resources. ScorePredictorStudentResource, ScoreCommentResource, ScoreStudentResource +- **Security:** 2 resources. IpBanCollection, IpBanResource +- **Semesters:** 2 resources. SemesterResolveResource, SemesterRangeResource +- **Settings:** 5 resources. PolicyResource, SchoolCalendarEventResource, SettingsResource, ConfigurationResource, SchoolCalendarEventDetailResource +- **Staff:** 2 resources. StaffCollection, StaffResource +- **Students:** 4 resources. StudentAssignmentResource, StudentRemovedResource, StudentClassSectionResource, StudentScoreCardRowResource +- **Subjects:** 2 resources. SubjectCurriculumResource, SubjectClassResource +- **Support:** 3 resources. ContactMessageResource, SupportRequestResource, SupportRequestCollection +- **System:** 4 resources. DashboardRouteResource, NavItemResource, HealthStatusResource, NavBuilderDataResource +- **Teachers:** 4 resources. TeacherAbsenceResource, TeacherStudentResource, TeacherClassResource, TeacherAssignmentResource +- **Ui:** 1 resources. UiStyleResource +- **Users:** 2 resources. LoginActivityResource, UserWithRolesResource +- **Utilities:** 1 resources. PhoneFormatResource +- **Whatsapp:** 5 resources. WhatsappParentContactResource, WhatsappGroupLinkResource, WhatsappClassSectionParentResource, WhatsappGroupLinkCollection, WhatsappClassSectionContactResource + +## Appendix E: Implementation Fix Tickets + +These tickets are the plan’s mandatory implementation layer. Each one should become a tracked issue with tests before code changes begin. + +### Ticket P0-001: Payment file access hardening + +**Problem:** Filename-driven file serving can allow users to request files without proving access to the payment record. + +**Implementation:** + +- Change the endpoint contract from `{filename}` to `{payment}` or `{payment}/file`. +- Load the payment with invoice/family context. +- Authorize using a policy or gate before resolving file path. +- Resolve file path from the stored payment record only. +- Keep basename/path traversal protection as defense-in-depth, not as the primary access control. +- Log download attempts with actor, payment, result, and source IP where available. + +**Tests:** + +- finance/admin success; +- owning parent success if business rules allow it; +- unrelated parent forbidden; +- teacher/staff forbidden unless explicitly allowed; +- guessed filename forbidden; +- traversal filename rejected; +- missing file returns `404` without leaking storage path. + +### Ticket P0-002: Manual payment edit transaction safety + +**Problem:** Payment edit and invoice recalculation must not be split across unprotected operations. + +**Implementation:** + +- Wrap edit, invoice lookup, balance recompute, payment update, and audit insert in one transaction. +- Lock payment and invoice rows before recalculation. +- Recalculate after mutation inside the same transaction. +- Enforce no-overpayment and valid method/status transitions. +- Write before/after audit data. + +**Tests:** + +- successful edit updates invoice balance; +- rejected edit leaves payment and invoice unchanged; +- two concurrent edits do not corrupt balance; +- audit row contains actor and before/after values. + +### Ticket P0-003: Student school ID generation + +**Problem:** `max(id) + 1` style generation is race-prone. + +**Implementation:** + +- Create the student first, then derive school ID from the persisted ID, or use a database-backed sequence. +- Add a unique constraint to `students.school_id`. +- Save school ID inside the same transaction. +- Add retry behavior for collision. + +**Tests:** + +- normal create assigns expected school ID format; +- duplicate school ID violates database constraint; +- concurrent create does not produce duplicates; +- rollback does not leave orphaned related records. + +### Ticket P0-004: Scanner service extraction and idempotency + +**Problem:** Scanner logic is too large for a controller and mixes validation, lookup, mutation, late-slip behavior, logging, and response formatting. + +**Implementation:** + +- Add `ScannerRequest`. +- Create `ScannerService` as the orchestration entry point. +- Extract `BadgeLookupService`, `StudentScanService`, `StaffScanService`, `LateArrivalService`, `AttendanceScanLogger`, and `ScannerResponseFactory`. +- Put attendance mutations inside one transaction. +- Add duplicate-scan/idempotency protection. + +**Tests:** + +- valid student scan; +- valid staff scan; +- invalid badge; +- duplicate scan; +- late arrival; +- semester/date boundary; +- scan log written exactly once per accepted mutation. + +### Ticket P0-005: Role and permission mutation authorization + +**Problem:** Role/permission mutations have high blast radius and must not rely on casual controller checks. + +**Implementation:** + +- Define canonical role/permission controllers. +- Mark duplicate root/auth controllers as canonical, shim, deprecated, or accidental duplicate. +- Route only to canonical controllers. +- Use a policy/gate for every mutation. +- Log actor, target, old state, and new state. + +**Tests:** + +- super-admin/admin allowed according to policy; +- lower-role forbidden; +- user cannot mutate own privilege unless explicitly allowed; +- deprecated/shim routes cannot bypass policy. + +### Ticket P0-006: Bulk communication safety + +**Problem:** Bulk send behavior can accidentally spam, duplicate, or target the wrong recipients. + +**Implementation:** + +- Require preview/dry-run before send. +- Canonicalize and deduplicate recipients. +- Apply opt-outs/preferences. +- Log send attempts, failures, and retries. +- Separate message composition from delivery. + +**Tests:** + +- preview count equals eligible send count; +- duplicate guardians collapse; +- opted-out recipients are excluded; +- failed sends are logged and retryable; +- wrong-role sender cannot bulk send. + +### Ticket P0-007: Refund/payment/invoice audit completeness + +**Problem:** Financial state must be reconstructable after the fact. “Trust me, it updated” is not an audit strategy, it is a confession. + +**Implementation:** + +- Standardize audit fields across payments, invoices, refunds, discounts, reimbursements, and external sync. +- Include actor, target, amount, old state, new state, source, and idempotency key. +- Ensure reports use the same canonical ledger/balance behavior as API reads. + +**Tests:** + +- each financial mutation creates audit data; +- reports reconcile with API totals; +- repeated external sync does not double-apply; +- failed mutation does not write misleading audit success. + +## Appendix E: Notable Review Queues + +### Raw SQL review queue + +- `app/Services/Finance/FinancialSummaryService.php`: 28 matches +- `app/Services/Grading/GradingOverviewService.php`: 11 matches +- `app/Services/Finance/FinancialReportService.php`: 10 matches +- `app/Models/User.php`: 7 matches +- `app/Models/AttendanceData.php`: 6 matches +- `app/Services/Refunds/RefundOverpaymentService.php`: 6 matches +- `app/Services/Reports/ReportCards/ReportCardService.php`: 6 matches +- `app/Services/AttendanceTracking/ViolationRuleEngineService.php`: 5 matches +- `app/Services/AttendanceTracking/AttendanceCaseQueryService.php`: 4 matches +- `app/Services/Grading/GradingBelowSixtyService.php`: 4 matches +- `app/Services/Grading/HomeworkTrackingService.php`: 4 matches +- `app/Models/Payment.php`: 3 matches +- `app/Services/Attendance/AttendanceQueryService.php`: 3 matches +- `app/Services/Inventory/InventoryItemService.php`: 3 matches +- `app/Services/Inventory/InventoryMovementService.php`: 3 matches +- `app/Services/Payments/PaymentMissedCheckService.php`: 3 matches +- `app/Services/Scores/ScoreDashboardService.php`: 3 matches +- `app/Services/Scores/ScorePredictorDataService.php`: 3 matches +- `app/Models/Invoice.php`: 2 matches +- `app/Models/Role.php`: 2 matches +- `app/Models/TeacherClass.php`: 2 matches +- `app/Services/Admin/CompetitionWinners/CompetitionWinnersAdminService.php`: 2 matches +- `app/Services/Attendance/AttendanceDailySummaryService.php`: 2 matches +- `app/Services/AttendanceTracking/AttendancePendingViolationService.php`: 2 matches +- `app/Services/Families/FamilyQueryService.php`: 2 matches +- `app/Services/Finance/FinancialPaymentService.php`: 2 matches +- `app/Services/Promotions/PromotionQueryService.php`: 2 matches +- `app/Services/Refunds/RefundPayoutService.php`: 2 matches +- `app/Services/Refunds/RefundQueryService.php`: 2 matches +- `app/Services/Staff/StaffQueryService.php`: 2 matches +- `app/Services/Whatsapp/WhatsappInviteBundleService.php`: 2 matches +- `app/Models/AdditionalCharge.php`: 1 matches +- `app/Models/AttendanceRecord.php`: 1 matches +- `app/Models/AttendanceTracking.php`: 1 matches +- `app/Models/Project.php`: 1 matches +- `app/Models/RoleNavItem.php`: 1 matches +- `app/Models/StaffAttendance.php`: 1 matches +- `app/Models/StudentAllergy.php`: 1 matches +- `app/Models/StudentMedicalCondition.php`: 1 matches +- `app/Models/WhatsappGroupMembership.php`: 1 matches +- ... 19 more files + +### Direct request access review queue + +- `app/Http/Controllers/Api/AttendanceTracking/AttendanceTrackingController.php`: 16 matches +- `app/Http/Controllers/Api/Attendance/AttendanceCommentTemplateController.php`: 15 matches +- `app/Http/Controllers/Api/Communication/CommunicationController.php`: 12 matches +- `app/Http/Controllers/Api/Badges/BadgeController.php`: 9 matches +- `app/Http/Controllers/Api/Email/BroadcastEmailController.php`: 9 matches +- `app/Http/Controllers/Api/ExtraCharges/ExtraChargesController.php`: 9 matches +- `app/Http/Controllers/Api/Students/StudentController.php`: 9 matches +- `app/Http/Controllers/Api/Attendance/StaffAttendanceApiController.php`: 8 matches +- `app/Http/Controllers/Api/Attendance/EarlyDismissalsController.php`: 7 matches +- `app/Http/Controllers/Api/ClassPreparation/ClassPreparationController.php`: 7 matches +- `app/Http/Controllers/Api/Attendance/TeacherAttendanceApiController.php`: 5 matches +- `app/Http/Controllers/Api/Certificates/CertificateController.php`: 5 matches +- `app/Http/Controllers/Api/Reports/ReportCardsController.php`: 5 matches +- `app/Http/Controllers/Api/CompetitionScores/CompetitionScoresController.php`: 4 matches +- `app/Http/Controllers/Api/Finance/ReimbursementController.php`: 4 matches +- `app/Http/Controllers/Api/PrintRequests/PrintRequestsController.php`: 4 matches +- `app/Http/Controllers/Api/Administrator/AdministratorEnrollmentController.php`: 3 matches +- `app/Http/Controllers/Api/Assignment/AssignmentApiController.php`: 3 matches +- `app/Http/Controllers/Api/Finance/InvoiceController.php`: 3 matches +- `app/Http/Controllers/Api/Finance/PaymentManualController.php`: 3 matches +- `app/Http/Controllers/Api/Parents/AuthorizedUserInviteController.php`: 3 matches +- `app/Http/Controllers/Api/Scores/ScoreCommentController.php`: 3 matches +- `app/Http/Controllers/Api/Scores/ScoreController.php`: 3 matches +- `app/Services/Administrator/AdministratorAbsenceService.php`: 3 matches +- `app/Http/Controllers/Api/Administrator/AdministratorNotificationController.php`: 2 matches +- `app/Http/Controllers/Api/Attendance/AdminAttendanceApiController.php`: 2 matches +- `app/Http/Controllers/Api/Auth/AuthSessionController.php`: 2 matches +- `app/Http/Controllers/Api/Auth/RegisterController.php`: 2 matches +- `app/Http/Controllers/Api/Classes/ClassController.php`: 2 matches +- `app/Http/Controllers/Api/Finance/PaymentController.php`: 2 matches +- `app/Http/Controllers/Api/Finance/PaymentTransactionController.php`: 2 matches +- `app/Http/Controllers/Api/Finance/PurchaseOrderController.php`: 2 matches +- `app/Http/Controllers/Api/Incidents/IncidentController.php`: 2 matches +- `app/Http/Controllers/Api/Scores/FinalController.php`: 2 matches +- `app/Http/Controllers/Api/Scores/HomeworkController.php`: 2 matches +- `app/Http/Controllers/Api/Scores/MidtermController.php`: 2 matches +- `app/Http/Controllers/Api/Scores/ParticipationController.php`: 2 matches +- `app/Http/Controllers/Api/Scores/ProjectController.php`: 2 matches +- `app/Http/Controllers/Api/Scores/QuizController.php`: 2 matches +- `app/Services/Administrator/TeacherSubmissionNotificationService.php`: 2 matches +- ... 10 more files + +### File operation review queue + +- `app/Services/Reimbursements/ReimbursementEmailService.php`: 5 matches +- `app/Http/Controllers/Api/Students/StudentController.php`: 3 matches +- `app/Services/Badges/BadgePdfService.php`: 3 matches +- `app/ThirdParty/fpdf/fpdf.php`: 3 matches +- `app/Http/Controllers/Api/ClassProgress/ClassProgressController.php`: 2 matches +- `app/Http/Controllers/Api/Finance/ReimbursementController.php`: 2 matches +- `app/Services/Payments/PaymentManualService.php`: 2 matches +- `app/Services/PrintRequests/PrintRequestsPortalService.php`: 2 matches +- `app/Services/System/ConfigUpdateService.php`: 2 matches +- `app/Services/Whatsapp/WhatsappInviteEmailService.php`: 2 matches +- `app/Http/Controllers/Api/Attendance/EarlyDismissalsController.php`: 1 matches +- `app/Http/Controllers/Api/Finance/FinancialController.php`: 1 matches +- `app/Http/Controllers/Api/Finance/PaypalTransactionsController.php`: 1 matches +- `app/Http/Controllers/Api/PrintRequests/PrintRequestsController.php`: 1 matches +- `app/Services/Attendance/StaffAttendanceService.php`: 1 matches +- `app/Services/Finance/FinancialChartService.php`: 1 matches +- `app/Services/Parents/ParentAttendanceReportService.php`: 1 matches +- `app/Services/Reimbursements/ReimbursementFileService.php`: 1 matches + +### Queue/dispatch review queue + +- `app/Services/Promotions/PromotionReminderService.php`: 2 matches +- `app/Console/Commands/DeleteUnverifiedUsersCommand.php`: 1 matches +- `app/Services/Administrator/AdministratorEnrollmentEventService.php`: 1 matches +- `app/Services/Grading/GradingBelowSixtyService.php`: 1 matches +- `app/Services/Notifications/NotificationTriggerService.php`: 1 matches +- `app/Services/Whatsapp/WhatsappInviteNotificationService.php`: 1 matches +- `app/Services/Whatsapp/WhatsappInviteService.php`: 1 matches + +### Legacy compatibility review queue + +- `app/Services/Parents/ParentAttendanceReportService.php`: 4 matches +- `app/Config/Database.php`: 3 matches +- `app/Helpers/ci_helpers.php`: 3 matches +- `app/Http/Controllers/Api/Attendance/AttendanceCommentTemplateController.php`: 3 matches +- `app/Http/Controllers/Api/Auth/RegisterController.php`: 3 matches +- `app/Http/Controllers/Api/BaseApiController.php`: 3 matches +- `app/Http/Controllers/Api/Core/BaseApiController.php`: 2 matches +- `app/Services/Admin/CompetitionWinners/CompetitionWinnersAdminService.php`: 2 matches +- `app/Services/AttendanceTracking/AttendanceTrackingService.php`: 2 matches +- `app/Services/Auth/ApiLoginSecurityService.php`: 2 matches +- `app/Services/Auth/CodeIgniterApiRegistrationService.php`: 2 matches +- `app/Services/Discounts/DiscountApplyService.php`: 2 matches +- `app/Services/ExtraCharges/ExtraChargesChargeService.php`: 2 matches +- `app/Services/Payments/PaymentManualService.php`: 2 matches +- `app/Http/Controllers/Api/Auth/AuthController.php`: 1 matches +- `app/Http/Controllers/Api/Auth/AuthSessionController.php`: 1 matches +- `app/Http/Controllers/Api/CiRequestAdapter.php`: 1 matches +- `app/Http/Controllers/Api/Core/CiRequestAdapter.php`: 1 matches +- `app/Http/Controllers/Api/System/HealthController.php`: 1 matches +- `app/Http/Controllers/Api/Utilities/ProofreadController.php`: 1 matches +- `app/Http/Middleware/EnsureClassProgressTeacherPortalAccess.php`: 1 matches +- `app/Http/Middleware/EnsureParentProgressAccess.php`: 1 matches +- `app/Http/Middleware/EnsurePrintRequestsAdminAccess.php`: 1 matches +- `app/Http/Middleware/EnsurePrintRequestsTeacherAccess.php`: 1 matches +- `app/Http/Middleware/TeacherPortalAuthenticate.php`: 1 matches +- `app/Http/Requests/Admin/SaveCompetitionScoresRequest.php`: 1 matches +- `app/Http/Requests/Admin/StoreCompetitionWinnerRequest.php`: 1 matches +- `app/Http/Requests/Admin/UpdateCompetitionWinnerRequest.php`: 1 matches +- `app/Models/CiDatabase.php`: 1 matches +- `app/Models/User.php`: 1 matches +- `app/Services/Admin/CompetitionWinners/CompetitionWinnersDomain.php`: 1 matches +- `app/Services/Attendance/StaffAttendanceService.php`: 1 matches +- `app/Services/Auth/AuthSessionService.php`: 1 matches +- `app/Services/BadgeScan/BadgeScanService.php`: 1 matches +- `app/Services/Docs/DocsCatalogService.php`: 1 matches +- `app/Services/Parents/AuthorizedUsersManagementService.php`: 1 matches +- `app/Services/Parents/ParentProgressQueryService.php`: 1 matches +- `app/Services/Parents/ParentReportCardService.php`: 1 matches +- `app/Services/Parents/PrimaryParentUserResolver.php`: 1 matches +- `app/Services/PrintRequests/PrintRequestsPortalService.php`: 1 matches +- ... 1 more files + + +## Appendix B: Reusable School Platform Tickets + +### MSP-001: Create `SchoolContext` + +**Goal:** Provide one immutable context object for school/program/tenant scoping. + +**Acceptance criteria:** + +- Contains school/program/tenant identifiers as required by the chosen model. +- Created at API boundary after authentication. +- Passed into all high-risk service commands. +- Used by repositories, policies, jobs, files, reports, and notifications. +- Cross-school access tests fail before the fix and pass after. + +### MSP-002: Classify all services + +**Goal:** Separate reusable core services from Islamic Sunday School extension services and infrastructure adapters. + +**Acceptance criteria:** + +- Every service is labeled `Core`, `IslamicSundaySchoolExtension`, `Infrastructure`, or `LegacyShim`. +- Each duplicate service has a canonical owner. +- CI/static check blocks `SchoolCore -> IslamicSundaySchool` dependencies. + +### MSP-003: Extract attendance policy architecture + +**Goal:** Make attendance reusable for daily schools, weekly programs, Islamic Sunday School, tutoring programs, and hybrid systems. + +**Acceptance criteria:** + +- `AttendancePolicy` contract exists. +- Standard and Islamic Sunday School implementations exist. +- Scanner flow uses a command object with context and actor. +- Duplicate scan/idempotency tests pass for both standard and Sunday policies. + +### MSP-004: Extract finance policy architecture + +**Goal:** Make invoices/payments/refunds reusable across school types. + +**Acceptance criteria:** + +- `TuitionPolicy`, `PaymentAllocationPolicy`, and `LedgerPolicy` or equivalent contracts exist. +- Manual payment edit remains transaction-safe. +- Islamic Sunday School-specific discounts/fees are extension rules, not core assumptions. +- Ledger reconciliation tests pass across at least two policy configurations. + +### MSP-005: Extract student identifier generation + +**Goal:** Remove hardcoded ID generation and support different school ID formats. + +**Acceptance criteria:** + +- `StudentIdentifierGenerator` contract exists. +- Core generator is sequence-safe. +- Islamic Sunday School generator is a concrete extension binding if it uses a special format. +- Unique DB constraint and retry behavior exist. + +### MSP-006: Create extension registration provider + +**Goal:** Make Islamic Sunday School a plugin-style implementation of generic school contracts. + +**Acceptance criteria:** + +- All Islamic Sunday School-specific bindings live in one or more providers. +- Core services can run with default bindings. +- Tests can swap bindings without editing service code. + +### MSP-007: Neutralize domain vocabulary in core + +**Goal:** Ensure core model/service names work for any school system. + +**Acceptance criteria:** + +- Core uses neutral terms: `Student`, `Guardian`, `StaffMember`, `ClassGroup`, `Session`, `AcademicTerm`, `Program`. +- Islamic Sunday School-specific aliases remain in extension/API presentation only. +- No masjid/community/ministry/Sunday vocabulary exists inside `SchoolCore`. diff --git a/docs/modular_plans/islamic_sunday_school_update_index.md b/docs/modular_plans/islamic_sunday_school_update_index.md new file mode 100644 index 00000000..edb9587e --- /dev/null +++ b/docs/modular_plans/islamic_sunday_school_update_index.md @@ -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 +``` diff --git a/docs/modular_plans/laravel_school_context_phase1.zip b/docs/modular_plans/laravel_school_context_phase1.zip new file mode 100644 index 00000000..1241f32d Binary files /dev/null and b/docs/modular_plans/laravel_school_context_phase1.zip differ diff --git a/docs/modular_plans/laravel_school_context_phase1/.DS_Store b/docs/modular_plans/laravel_school_context_phase1/.DS_Store new file mode 100644 index 00000000..5008ddfc Binary files /dev/null and b/docs/modular_plans/laravel_school_context_phase1/.DS_Store differ diff --git a/docs/modular_plans/laravel_school_context_phase1/.github/CODEOWNERS b/docs/modular_plans/laravel_school_context_phase1/.github/CODEOWNERS new file mode 100644 index 00000000..540fad9f --- /dev/null +++ b/docs/modular_plans/laravel_school_context_phase1/.github/CODEOWNERS @@ -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 diff --git a/docs/modular_plans/laravel_school_context_phase1/.github/pull_request_template.md b/docs/modular_plans/laravel_school_context_phase1/.github/pull_request_template.md new file mode 100644 index 00000000..5b3ea7a6 --- /dev/null +++ b/docs/modular_plans/laravel_school_context_phase1/.github/pull_request_template.md @@ -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. diff --git a/docs/modular_plans/laravel_school_context_phase1/.github/workflows/architecture.yml b/docs/modular_plans/laravel_school_context_phase1/.github/workflows/architecture.yml new file mode 100644 index 00000000..0620acf7 --- /dev/null +++ b/docs/modular_plans/laravel_school_context_phase1/.github/workflows/architecture.yml @@ -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 diff --git a/docs/modular_plans/laravel_school_context_phase1/.github/workflows/release-gate.yml b/docs/modular_plans/laravel_school_context_phase1/.github/workflows/release-gate.yml new file mode 100644 index 00000000..89f6494a --- /dev/null +++ b/docs/modular_plans/laravel_school_context_phase1/.github/workflows/release-gate.yml @@ -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 diff --git a/docs/modular_plans/laravel_school_context_phase1/.github/workflows/release-readiness.yml b/docs/modular_plans/laravel_school_context_phase1/.github/workflows/release-readiness.yml new file mode 100644 index 00000000..d5acb4d3 --- /dev/null +++ b/docs/modular_plans/laravel_school_context_phase1/.github/workflows/release-readiness.yml @@ -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 diff --git a/docs/modular_plans/laravel_school_context_phase1_phase2.zip b/docs/modular_plans/laravel_school_context_phase1_phase2.zip new file mode 100644 index 00000000..0ddf1d0c Binary files /dev/null and b/docs/modular_plans/laravel_school_context_phase1_phase2.zip differ diff --git a/docs/modular_plans/laravel_school_context_phase1_phase2_phase3.zip b/docs/modular_plans/laravel_school_context_phase1_phase2_phase3.zip new file mode 100644 index 00000000..eec7b1f4 Binary files /dev/null and b/docs/modular_plans/laravel_school_context_phase1_phase2_phase3.zip differ diff --git a/docs/modular_plans/laravel_school_context_phase1_phase2_phase3_phase4.zip b/docs/modular_plans/laravel_school_context_phase1_phase2_phase3_phase4.zip new file mode 100644 index 00000000..3049e133 Binary files /dev/null and b/docs/modular_plans/laravel_school_context_phase1_phase2_phase3_phase4.zip differ diff --git a/docs/modular_plans/laravel_school_context_phase1_phase2_phase3_phase4_phase5.zip b/docs/modular_plans/laravel_school_context_phase1_phase2_phase3_phase4_phase5.zip new file mode 100644 index 00000000..c2e92e55 Binary files /dev/null and b/docs/modular_plans/laravel_school_context_phase1_phase2_phase3_phase4_phase5.zip differ diff --git a/docs/modular_plans/laravel_school_context_phase1_phase2_phase3_phase4_phase5_phase6.zip b/docs/modular_plans/laravel_school_context_phase1_phase2_phase3_phase4_phase5_phase6.zip new file mode 100644 index 00000000..f8f7630f Binary files /dev/null and b/docs/modular_plans/laravel_school_context_phase1_phase2_phase3_phase4_phase5_phase6.zip differ diff --git a/docs/modular_plans/laravel_school_context_phase1_phase2_phase3_phase4_phase5_phase6_phase7.zip b/docs/modular_plans/laravel_school_context_phase1_phase2_phase3_phase4_phase5_phase6_phase7.zip new file mode 100644 index 00000000..83782677 Binary files /dev/null and b/docs/modular_plans/laravel_school_context_phase1_phase2_phase3_phase4_phase5_phase6_phase7.zip differ diff --git a/docs/modular_plans/laravel_school_context_phase1_phase2_phase3_phase4_phase5_phase6_phase7_phase8.zip b/docs/modular_plans/laravel_school_context_phase1_phase2_phase3_phase4_phase5_phase6_phase7_phase8.zip new file mode 100644 index 00000000..9cb4a9a4 Binary files /dev/null and b/docs/modular_plans/laravel_school_context_phase1_phase2_phase3_phase4_phase5_phase6_phase7_phase8.zip differ diff --git a/docs/modular_plans/laravel_school_context_phase1_phase2_phase3_phase4_phase5_phase6_phase7_phase8_phase9.zip b/docs/modular_plans/laravel_school_context_phase1_phase2_phase3_phase4_phase5_phase6_phase7_phase8_phase9.zip new file mode 100644 index 00000000..5d64cd91 Binary files /dev/null and b/docs/modular_plans/laravel_school_context_phase1_phase2_phase3_phase4_phase5_phase6_phase7_phase8_phase9.zip differ diff --git a/docs/modular_plans/laravel_school_context_phase1_phase2_phase3_phase4_phase5_phase6_phase7_phase8_phase9_phase10.zip b/docs/modular_plans/laravel_school_context_phase1_phase2_phase3_phase4_phase5_phase6_phase7_phase8_phase9_phase10.zip new file mode 100644 index 00000000..51c9e6d9 Binary files /dev/null and b/docs/modular_plans/laravel_school_context_phase1_phase2_phase3_phase4_phase5_phase6_phase7_phase8_phase9_phase10.zip differ diff --git a/docs/modular_plans/phase_10_migration_release_readiness_execution_plan_islamic.md b/docs/modular_plans/phase_10_migration_release_readiness_execution_plan_islamic.md new file mode 100644 index 00000000..06e5eea1 --- /dev/null +++ b/docs/modular_plans/phase_10_migration_release_readiness_execution_plan_islamic.md @@ -0,0 +1,1561 @@ +# Phase 10 Execution Plan: Migration, Release, and Production Readiness + +## 1. Purpose + +Phase 10 turns the modular architecture work into a controlled production rollout. + +Earlier phases define and enforce the architecture: + +```text +Phase 1: SchoolContext +Phase 2: Core contracts +Phase 3: Finance +Phase 4: Attendance +Phase 5: Student lifecycle +Phase 6: Communication +Phase 7: Reporting +Phase 8: Controller/API cleanup +Phase 9: Boundary enforcement +``` + +Phase 10 answers the uncomfortable operational questions: + +```text +How do we migrate without breaking the current app? +How do we cut over safely? +How do we validate old vs new behavior? +How do we rollback? +How do we monitor correctness? +How do we prove Islamic Sunday School still works? +How do we prevent this from becoming a never-ending refactor swamp? +``` + +The goal is not to “finish architecture.” Architecture is never finished. It just becomes less embarrassing when guarded properly. + +--- + +## 2. Release Principle + +Do not big-bang rewrite the platform. + +Use controlled vertical releases: + +```text +1. build modular slice +2. run in shadow/parallel where possible +3. compare outputs +4. enable behind feature flag +5. roll out to internal/test school +6. roll out to Islamic Sunday School production tenant +7. monitor +8. remove legacy path after confidence window +``` + +A big-bang release is just a theatrical way to discover all integration bugs at once. Very efficient, in the same way driving into a lake is efficient transportation to the bottom. + +--- + +## 3. Release Scope + +Phase 10 applies to: + +```text +feature flags +data migration +compatibility adapters +parallel run validation +production rollout +rollback plans +monitoring and observability +audit verification +performance checks +security checks +user acceptance testing +staff/admin training +support playbooks +legacy deprecation/removal +post-release governance +``` + +Covered modules: + +```text +SchoolContext +Core Contracts +Finance +Attendance +Students +Communication +Reporting +Controller/API layer +Boundary enforcement +IslamicSundaySchool extension +``` + +--- + +## 4. Target Release Model + +Recommended release model: + +```text +Release Track A: internal/dev validation +Release Track B: staging with production-like data copy +Release Track C: pilot school/tenant +Release Track D: Islamic Sunday School production rollout +Release Track E: broader multi-school rollout +Release Track F: legacy removal +``` + +Each track has explicit entry and exit criteria. + +No track advances because “it seems fine.” That phrase belongs in horror movies and production postmortems. + +--- + +## 5. Feature Flag Strategy + +Use feature flags to control rollout by school/domain/module. + +Required flags: + +```text +school_context.enabled +core_contracts.enabled +finance.modular_payments.enabled +finance.modular_file_access.enabled +attendance.modular_scanner.enabled +attendance.modular_manual_override.enabled +students.modular_creation.enabled +students.modular_enrollment.enabled +communication.preview_required.enabled +communication.modular_send.enabled +reporting.modular_reports.enabled +api.v2.enabled +islamic_sunday_school.extension.enabled +boundary_enforcement.fail_ci.enabled +``` + +Flag dimensions: + +```text +global +environment +school_id +domain_profile +user role +route group +module +``` + +Rules: + +```text +- every risky module cutover must be flag-controlled +- flags must default safe +- flags must be auditable +- flags must have owners +- flags must have removal dates +``` + +Feature flags without removal dates become permanent haunted switches. + +--- + +## 6. Migration Environments + +### 6.1 Development + +Purpose: + +```text +verify compile, tests, architecture checks, developer flow +``` + +Exit criteria: + +```text +unit tests pass +architecture tests pass +static analysis passes +module contracts compile +feature flags work +``` + +### 6.2 Staging + +Purpose: + +```text +validate migrations and behavior using production-like data +``` + +Exit criteria: + +```text +database migrations pass +rollbacks tested +shadow comparisons completed +performance acceptable +security checks passed +UAT completed +``` + +### 6.3 Pilot Tenant + +Purpose: + +```text +validate with limited real users and real workflows +``` + +Exit criteria: + +```text +no P0/P1 defects +support playbook validated +finance and attendance reconciled +communication sends verified +reports match expected data +``` + +### 6.4 Production + +Purpose: + +```text +controlled rollout and monitoring +``` + +Exit criteria: + +```text +module rollout stable +legacy adapters retired according to schedule +post-release audit complete +``` + +--- + +## 7. Data Migration Strategy + +### 7.1 General migration rules + +```text +- migrations must be backward-compatible during rollout +- additive schema changes before destructive changes +- destructive changes only after legacy removal +- every migration has rollback plan +- every migration has validation query +- every sensitive table migration has backup checkpoint +``` + +Use expand-and-contract: + +```text +1. expand schema +2. write to old and new paths if needed +3. backfill +4. validate +5. read from new path +6. stop writing old path +7. remove old path later +``` + +### 7.2 Finance migration rules + +Finance migrations require stronger controls: + +```text +- backup before migration +- balance reconciliation before and after +- audit log verification +- payment file access validation +- no destructive payment/invoice migration during business hours +``` + +Required validation: + +```text +total invoice count +total payment count +sum invoice totals +sum payment amounts +outstanding balances by school +refund/reversal counts +payment file count +finance audit count +``` + +### 7.3 Attendance migration rules + +Required validation: + +```text +attendance session count +attendance record count +scan log count +duplicate scan handling +student/staff attendance count by date +wrong-school scan isolation +manual override audit count +``` + +### 7.4 Student lifecycle migration rules + +Required validation: + +```text +student count by school +guardian count by school +student_guardian links +enrollment count +assignment count +student identifier uniqueness +Islamic profile extension row count +sensitive field authorization +``` + +### 7.5 Communication migration rules + +Required validation: + +```text +message count +recipient log count +delivery attempt count +preference count +template count/version +preview snapshot count +provider callback mapping +``` + +### 7.6 Reporting migration rules + +Required validation: + +```text +report registry entries +core report availability +Islamic report availability +report snapshot access +export audit logs +scheduled report recipient validation +``` + +--- + +## 8. Parallel Run and Shadow Validation + +For high-risk modules, run old and new logic in parallel before cutover where practical. + +### 8.1 Finance shadow mode + +Compare: + +```text +legacy invoice balance +modular BalanceCalculator result +legacy payment edit behavior +modular PaymentService result simulation +legacy finance report +modular finance report +``` + +Do not double-write real payments in shadow mode unless explicitly designed. Simulate and compare. Duplicating payments because of “testing” would be a bold new genre of self-harm. + +### 8.2 Attendance shadow mode + +Compare: + +```text +legacy scanner result +modular ScannerService result +legacy late/present status +modular AttendanceStatusPolicy result +legacy attendance summary +modular Attendance read model summary +``` + +### 8.3 Student lifecycle shadow mode + +Compare: + +```text +legacy student creation payload +modular DTO mapping +legacy generated identifier +modular identifier format +legacy enrollment assignment +modular enrollment/assignment result +``` + +Do not create duplicate students in production shadow mode. + +### 8.4 Communication shadow mode + +Compare: + +```text +legacy recipient query +modular recipient preview +legacy recipient count +modular deduped count +legacy excluded recipients +modular preference exclusions +``` + +Do not send duplicate messages. Shadow recipient resolution only. + +### 8.5 Reporting shadow mode + +Compare: + +```text +legacy student roster +modular student roster +legacy attendance summary +modular attendance summary +legacy finance balance report +modular outstanding balance report +``` + +--- + +## 9. Cutover Strategy + +### 9.1 Cutover order + +Recommended order: + +```text +1. SchoolContext read-only enforcement +2. Payment file access hardening +3. Communication preview-before-send +4. Student identifier generation +5. Finance payment recording/editing +6. Attendance scanner and manual override +7. Student enrollment/assignment/promotion +8. Reporting registry and exports +9. API/controller v2 routes +10. Islamic Sunday School extension routes/reports +11. Legacy route deprecation/removal +``` + +Why this order: + +```text +- security fixes first +- low-side-effect safety improvements early +- money and attendance after validation +- reports after domain truth is stable +- extension routes after core contracts are stable +``` + +### 9.2 Cutover checklist + +Before enabling a module flag: + +```text +- tests pass +- architecture checks pass +- migration validation passes +- shadow comparison reviewed +- rollback procedure tested +- monitoring dashboard ready +- support team briefed +- owner approval recorded +``` + +### 9.3 Cutover window + +For high-risk modules: + +```text +finance: low-activity window +attendance/scanner: outside active program/class session +communication: never immediately before critical announcement +student lifecycle: outside registration rush if possible +reporting: normal hours acceptable if read-only +``` + +Apparently releasing attendance scanner changes five minutes before morning check-in is considered “bad.” Humanity retains some instincts. + +--- + +## 10. Rollback Strategy + +Every module must have rollback steps. + +Rollback types: + +```text +feature flag rollback +route rollback +service binding rollback +database rollback +data repair +provider integration rollback +``` + +### 10.1 Feature flag rollback + +Preferred rollback: + +```text +disable modular feature flag +route traffic back to adapter/legacy path +preserve new logs for analysis +``` + +### 10.2 Database rollback + +Avoid destructive rollback when production writes have occurred. + +Use: + +```text +forward-fix migration +compatibility read adapters +data repair scripts +manual reconciliation for finance +``` + +### 10.3 Finance rollback + +Finance rollback must include: + +```text +payment/invoice reconciliation +audit log review +idempotency key review +manual correction plan +finance owner approval +``` + +### 10.4 Communication rollback + +Communication rollback must avoid duplicate sends. + +Required: + +```text +check idempotency records +check message logs +disable scheduled sends if needed +verify no retry storm +``` + +### 10.5 Attendance rollback + +Required: + +```text +preserve scan logs +compare attendance records +repair duplicate/missing records +notify operations if scanner behavior affected +``` + +--- + +## 11. Observability and Monitoring + +Create dashboards and alerts before production rollout. + +### 11.1 Platform-level metrics + +```text +request count by route +error rate by route +latency by route +auth failures +authorization failures +school context resolution failures +wrong-school access attempts +feature flag state by school +``` + +### 11.2 Finance metrics + +```text +payment create count +payment edit count +payment reversal/refund count +invoice recalculation count +finance audit log count +payment file download count +payment file authorization denial count +balance mismatch count +idempotency conflict count +``` + +Critical alerts: + +```text +balance mismatch detected +payment mutation without audit log +payment file wrong-school access attempt +finance exception spike +``` + +### 11.3 Attendance metrics + +```text +scan count +duplicate scan count +scan failure count +badge not found count +late status count +manual override count +attendance audit log count +scanner latency +``` + +Critical alerts: + +```text +scanner error spike +duplicate scan spike +attendance mutation without audit +wrong-school badge attempt +``` + +### 11.4 Student lifecycle metrics + +```text +student create count +identifier collision count +guardian link count +enrollment count +assignment count +promotion count +status transition count +student audit count +``` + +Critical alerts: + +```text +identifier collision spike +student creation failure spike +wrong-school guardian link attempt +``` + +### 11.5 Communication metrics + +```text +recipient preview count +bulk send count +recipient dedupe count +preference exclusion count +delivery failure count +retry count +provider error count +scheduled send count +``` + +Critical alerts: + +```text +bulk send without preview attempt +provider failure spike +retry storm +wrong-school recipient attempt +``` + +### 11.6 Reporting metrics + +```text +report run count +export count +snapshot count +scheduled report count +sensitive export count +report authorization denial count +cache hit/miss +``` + +Critical alerts: + +```text +sensitive export by unauthorized actor +report export without audit +cross-school report attempt +scheduled report failure spike +``` + +--- + +## 12. Audit Verification + +Audit logs are part of production readiness. + +Verify audits for: + +```text +finance mutations +payment file downloads +attendance mutations +manual overrides +student lifecycle changes +guardian links +bulk communication sends +report exports +sensitive Islamic Sunday School profile access +role/permission changes +``` + +Audit verification checklist: + +```text +- audit row created +- actor_user_id present +- school_id present +- action present +- entity type/id present +- before/after present where required +- reason present where required +- request_id present +``` + +If an operation mutates important data and does not create audit logs, it is not production-ready. It is just leaving footprints in wet cement and hoping nobody checks. + +--- + +## 13. Security Readiness + +Security checks before production: + +```text +cross-school access tests +wrong-role access tests +file download authorization +bulk communication authorization +report export authorization +extension route domain-profile checks +sensitive field access checks +rate limits on risky endpoints +provider callback signature verification +``` + +High-risk Islamic Sunday School extension data: + +```text +imam/admin sensitive notes +Qur'an/Arabic/Islamic studies placement details +scholarship/sadaqah support indicators +family/community profile notes +guardian contact information +``` + +These require explicit permissions and audit on access/export. + +--- + +## 14. Performance Readiness + +Baseline performance before rollout. + +Targets should be defined per project, but recommended checks: + +```text +student list response time +scanner process latency +payment record latency +recipient preview generation time +bulk send queueing time +report run time +export generation time +SchoolContext resolution overhead +``` + +Performance risks: + +```text +unscoped queries +N+1 relationships in resources +large recipient previews +large exports +report joins across extension tables +scanner latency during check-in rush +``` + +Mitigation: + +```text +indexes +read models +chunking +queues +pagination +export jobs +caching only when safe +``` + +Fast wrong answers remain wrong. Optimize after correctness unless the system is timing out, in which case enjoy doing both at once, nature’s punishment for optimism. + +--- + +## 15. User Acceptance Testing + +UAT must be scenario-based, not vague clicking. + +### 15.1 Admin scenarios + +```text +create student +link guardian +enroll student +assign class/group +record payment +edit payment +download payment file +run attendance report +send bulk communication with preview +export student roster +``` + +### 15.2 Teacher/staff scenarios + +```text +view assigned students +record attendance +scanner check-in/check-out +manual override if permitted +view attendance summary +send class communication if permitted +``` + +### 15.3 Finance user scenarios + +```text +record payment +edit payment with reason +reverse/refund payment +view invoice balance +download payment file +export outstanding balance report +``` + +### 15.4 Islamic Sunday School scenarios + +```text +create Islamic Sunday School student profile +assign Qur'an level +assign Arabic level +assign Islamic studies track +assign halaqa/group +record Sunday program attendance +view Qur'an progress report +view halaqa attendance report +message Qur'an class guardians with preview +export masjid family/community summary with permissions +``` + +### 15.5 Guardian/parent scenarios, if applicable + +```text +view own student profile +view allowed attendance +view allowed finance info +receive communication +not see unrelated students +not see sensitive notes +``` + +--- + +## 16. Training and Support + +Prepare support materials before rollout. + +Required materials: + +```text +admin quick guide +finance quick guide +attendance/scanner guide +student lifecycle guide +communication preview/send guide +reporting/export guide +Islamic Sunday School extension guide +support escalation guide +known changes/changelog +``` + +Support playbooks: + +```text +payment balance discrepancy +scanner not recognizing badge +duplicate scan complaint +guardian cannot see student +bulk message sent to unexpected recipients +report export missing data +Islamic profile field missing/unauthorized +``` + +--- + +## 17. Data Reconciliation + +Run reconciliation before and after module cutovers. + +### 17.1 Finance reconciliation + +```text +invoice totals match +payment totals match +balances match +refund/reversal totals match +audit counts match mutation counts +payment files accessible only through authorized routes +``` + +### 17.2 Attendance reconciliation + +```text +attendance counts by date/session match +duplicate scans not counted +manual overrides preserved +staff/student attendance separated correctly +audit counts match mutation counts +``` + +### 17.3 Student reconciliation + +```text +student count stable +guardian links stable +enrollments stable +assignments stable +identifiers unique +Islamic extension profiles linked correctly +``` + +### 17.4 Communication reconciliation + +```text +recipient previews match expected audiences +dedupe counts reviewed +preference exclusions correct +message logs complete +delivery attempts tracked +``` + +### 17.5 Reporting reconciliation + +```text +core reports match source domain read models +Islamic reports match extension read models +exports match report view +sensitive columns filtered +``` + +--- + +## 18. Production Readiness Review + +Hold a readiness review before each major module rollout. + +Required attendees: + +```text +platform owner +module owner +security reviewer +QA/test owner +support owner +Islamic Sunday School domain owner, if extension affected +finance owner, if finance affected +``` + +Review checklist: + +```text +tests passing +known issues reviewed +migration plan approved +rollback plan approved +monitoring ready +support docs ready +UAT signoff complete +owner approval recorded +``` + +--- + +## 19. Legacy Removal Strategy + +Legacy removal must be deliberate. + +Removal order: + +```text +1. disable legacy route behind flag +2. monitor for usage +3. add deprecation warning/header +4. notify clients/admins if needed +5. remove route +6. remove adapter controller +7. remove legacy service method +8. remove old tests +9. remove old schema only after data retention review +``` + +Never remove old paths while unknown clients still use them unless the route is unsafe. In that case, security wins and the support team gets coffee. + +--- + +## 20. API Client Migration + +If external/admin clients consume the API, provide migration guidance. + +Required docs: + +```text +v1 to v2 route mapping +request payload changes +response envelope changes +error envelope changes +deprecation dates +new auth/permission requirements +new Islamic extension endpoints +``` + +For each breaking change: + +```text +old endpoint +new endpoint +old payload +new payload +old response +new response +migration notes +sunset date +``` + +--- + +## 21. Incident Response Plan + +Define incident categories. + +### P0 incidents + +```text +cross-school data leak +unauthorized payment file access +incorrect payment balances at scale +bulk communication sent to wrong audience +sensitive report export leak +scanner outage during active session +student data corruption +``` + +### P1 incidents + +```text +module unavailable +report incorrect for subset +payment edit failure +attendance duplicate spike +communication provider failure +Islamic extension profile unavailable +``` + +### Response steps + +```text +1. disable feature flag if applicable +2. stop affected queues/jobs +3. preserve logs and audit evidence +4. notify owner/security/support +5. run reconciliation +6. apply rollback or forward fix +7. document root cause +8. add regression test +``` + +--- + +## 22. Post-Release Review + +After each module rollout, run a post-release review. + +Review: + +```text +incidents +support tickets +performance metrics +audit completeness +reconciliation results +missed tests +missed docs +flag cleanup +legacy usage +``` + +Output: + +```text +post-release report +follow-up tickets +test additions +docs corrections +legacy removal decisions +``` + +No “we survived, ship the next one” ritual. Survival is not evidence of quality. Cockroaches survive too. + +--- + +## 23. Final Modular Platform Acceptance Criteria + +The modular platform is production-ready when: + +```text +SchoolContext is enforced. +SchoolCore contracts exist and are used by controllers. +IslamicSundaySchool is isolated as extension layer. +Finance file access is payment-scoped and authorized. +Finance payment mutation is transactional and audited. +Attendance scanner is idempotent and service-owned. +Student identifier generation is race-safe. +Student lifecycle behavior is service-owned. +Communication bulk sends require preview and dedupe. +Reporting consumes read models/domain truth. +Controllers are adapters. +Architecture tests and CI gates are active. +Route inventory and OpenAPI docs are current. +Cross-school access tests pass. +Sensitive data access is audited. +Legacy unsafe routes are removed or disabled. +Production monitoring is active. +Rollback plans are tested. +``` + +--- + +## 24. Implementation Tickets + +## REL-001: Create feature flag rollout map + +### Description + +Define module flags, owners, default states, rollout scope, and removal dates. + +### Acceptance Criteria + +```text +- All high-risk modules have flags. +- Flags support school/domain rollout. +- Each flag has owner and removal target. +- Flag state is auditable. +``` + +--- + +## REL-002: Create migration validation scripts + +### Description + +Create validation scripts for finance, attendance, students, communication, and reporting. + +### Acceptance Criteria + +```text +- Scripts run in staging. +- Scripts compare pre/post counts. +- Scripts detect cross-school anomalies. +- Finance validation includes balance totals. +``` + +--- + +## REL-003: Add shadow comparison jobs + +### Description + +Add jobs or commands to compare legacy and modular outputs. + +### Acceptance Criteria + +```text +- Finance balance comparison exists. +- Attendance scanner comparison exists. +- Communication recipient comparison exists. +- Reporting comparison exists. +- Output includes mismatch details. +``` + +--- + +## REL-004: Build production monitoring dashboards + +### Description + +Create dashboards and alerts for modular rollout. + +### Acceptance Criteria + +```text +- Platform metrics visible. +- Finance alerts configured. +- Attendance alerts configured. +- Communication alerts configured. +- Reporting/export alerts configured. +- SchoolContext failures visible. +``` + +--- + +## REL-005: Create rollback playbooks + +### Description + +Create module-specific rollback procedures. + +### Acceptance Criteria + +```text +- Finance rollback playbook exists. +- Attendance rollback playbook exists. +- Student rollback playbook exists. +- Communication rollback playbook exists. +- Reporting rollback playbook exists. +- Rollback tested in staging. +``` + +--- + +## REL-006: Run security readiness review + +### Description + +Validate authorization, file access, report exports, extension routes, and sensitive fields. + +### Acceptance Criteria + +```text +- Cross-school tests pass. +- Sensitive Islamic Sunday School fields protected. +- Payment file access protected. +- Bulk send protected. +- Report export protected. +``` + +--- + +## REL-007: Run performance readiness review + +### Description + +Baseline key endpoint and job performance. + +### Acceptance Criteria + +```text +- Scanner latency acceptable. +- Payment mutation latency acceptable. +- Recipient preview performance acceptable. +- Report/export performance acceptable. +- Slow query risks documented. +``` + +--- + +## REL-008: Complete UAT scenarios + +### Description + +Run scenario-based UAT for admin, finance, attendance, students, communication, reporting, and Islamic Sunday School extension. + +### Acceptance Criteria + +```text +- UAT scenarios completed. +- P0/P1 defects fixed or rollout blocked. +- Islamic Sunday School owner signs off extension workflows. +``` + +--- + +## REL-009: Prepare support and training material + +### Description + +Create guides and support playbooks. + +### Acceptance Criteria + +```text +- Admin guide exists. +- Finance guide exists. +- Attendance/scanner guide exists. +- Communication guide exists. +- Reporting/export guide exists. +- Islamic Sunday School guide exists. +- Support escalation playbook exists. +``` + +--- + +## REL-010: Execute pilot rollout + +### Description + +Enable modular features for a controlled pilot scope. + +### Acceptance Criteria + +```text +- Pilot school/tenant selected. +- Flags enabled progressively. +- Monitoring reviewed daily during pilot. +- Reconciliation passes. +- No unresolved P0/P1 defects. +``` + +--- + +## REL-011: Execute Islamic Sunday School production rollout + +### Description + +Roll out Islamic Sunday School extension with core modular services. + +### Acceptance Criteria + +```text +- islamic_sunday_school domain profile enabled. +- Extension routes active. +- Qur'an/Arabic/Islamic studies/halaqa workflows validated. +- Masjid family/community reports validated. +- Sensitive permissions verified. +``` + +--- + +## REL-012: Remove legacy unsafe routes + +### Description + +Disable or remove legacy routes that are unsafe or superseded. + +### Acceptance Criteria + +```text +- filename-based payment file route removed/disabled. +- blind bulk send route removed/disabled. +- unsafe student identifier path removed. +- legacy scanner mutation path removed/disabled. +- deprecated routes have replacements. +``` + +--- + +## REL-013: Complete post-release review + +### Description + +Review rollout metrics, incidents, support tickets, and cleanup items. + +### Acceptance Criteria + +```text +- Post-release report exists. +- Follow-up tickets created. +- Regression tests added for incidents. +- Legacy removal schedule updated. +``` + +--- + +## 25. Rollout Timeline Template + +Use this as a planning template, not a divine prophecy. + +```text +Week 1: + - finalize flags + - complete validation scripts + - run staging migrations + +Week 2: + - run shadow comparisons + - fix mismatches + - complete monitoring dashboards + +Week 3: + - complete security/performance readiness + - complete UAT + - prepare support docs + +Week 4: + - pilot rollout + - monitor and reconcile + - patch defects + +Week 5: + - Islamic Sunday School production rollout + - monitor and reconcile + - freeze unsafe legacy paths + +Week 6: + - broader rollout + - remove/deprecate legacy routes + - post-release review +``` + +Actual timing depends on defect count, data quality, team size, and how many legacy surprises crawl out from under the floorboards. + +--- + +## 26. Definition of Done for Phase 10 + +Phase 10 is done when: + +```text +- Feature flag rollout map exists. +- Migration validation scripts exist. +- Shadow comparison jobs exist for high-risk modules. +- Production dashboards and alerts exist. +- Rollback playbooks exist and are tested in staging. +- Security readiness review is complete. +- Performance readiness review is complete. +- UAT scenarios are complete. +- Support/training materials exist. +- Pilot rollout completed successfully. +- Islamic Sunday School production rollout completed successfully. +- Legacy unsafe routes are removed or disabled. +- Post-release review is complete. +- Follow-up cleanup tickets are created. +``` + +--- + +## 27. Non-Goals + +Do not include in Phase 10: + +```text +- brand-new feature development +- major UI redesign +- full external SIS integration +- full analytics warehouse +- provider migration unrelated to modular rollout +- complete rewrite of all legacy modules +- speculative optimizations without measured bottlenecks +``` + +Phase 10 is about safe release, not ambition cosplay. + +--- + +## 28. Main Risks + +### Risk: data migration corrupts production data + +Mitigation: + +```text +- additive migrations +- backups +- validation scripts +- staging rehearsal +- rollback/forward-fix plan +``` + +### Risk: modular and legacy paths diverge + +Mitigation: + +```text +- shadow comparisons +- compatibility adapters +- feature flags +- mismatch reports +``` + +### Risk: finance balances differ + +Mitigation: + +```text +- finance reconciliation +- BalanceCalculator comparison +- manual review threshold +- rollback flag +``` + +### Risk: scanner disruption + +Mitigation: + +```text +- rollout outside active session +- scanner latency monitoring +- duplicate scan monitoring +- legacy scanner fallback flag +``` + +### Risk: accidental communication blast + +Mitigation: + +```text +- preview required +- confirmation token +- idempotency +- send limit/rate controls +- pilot audience first +``` + +### Risk: Islamic Sunday School extension leaks sensitive data + +Mitigation: + +```text +- explicit permissions +- extension route middleware +- sensitive export audit +- UAT with domain owner +``` + +### Risk: team leaves feature flags forever + +Mitigation: + +```text +- flag removal dates +- post-release cleanup tickets +- release gate warning on expired flags +``` + +--- + +## 29. Immediate Next Work Queue + +Start here: + +```text +1. REL-001 Create feature flag rollout map +2. REL-002 Create migration validation scripts +3. REL-003 Add shadow comparison jobs +4. REL-004 Build production monitoring dashboards +5. REL-005 Create rollback playbooks +6. REL-006 Run security readiness review +7. REL-007 Run performance readiness review +8. REL-008 Complete UAT scenarios +9. REL-009 Prepare support and training material +10. REL-010 Execute pilot rollout +11. REL-011 Execute Islamic Sunday School production rollout +12. REL-012 Remove legacy unsafe routes +13. REL-013 Complete post-release review +``` + +Start with flags, validation, and rollback. If you cannot turn it off, prove it, and recover from it, you are not ready to ship it. You are ready to gamble with extra steps. diff --git a/docs/modular_plans/phase_1_school_context_execution_plan_islamic.md b/docs/modular_plans/phase_1_school_context_execution_plan_islamic.md new file mode 100644 index 00000000..cb728682 --- /dev/null +++ b/docs/modular_plans/phase_1_school_context_execution_plan_islamic.md @@ -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. diff --git a/docs/modular_plans/phase_2_core_contracts_execution_plan_islamic.md b/docs/modular_plans/phase_2_core_contracts_execution_plan_islamic.md new file mode 100644 index 00000000..9065e9ad --- /dev/null +++ b/docs/modular_plans/phase_2_core_contracts_execution_plan_islamic.md @@ -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. diff --git a/docs/modular_plans/phase_3_modular_finance_execution_plan_islamic.md b/docs/modular_plans/phase_3_modular_finance_execution_plan_islamic.md new file mode 100644 index 00000000..9de46432 --- /dev/null +++ b/docs/modular_plans/phase_3_modular_finance_execution_plan_islamic.md @@ -0,0 +1,1511 @@ +# Phase 3 Execution Plan: Modular Finance Extraction + + +# Islamic Sunday School Alignment Update + +Phase 3 finance now treats the extension as **Islamic Sunday School**. + +`SchoolCore\Finance` owns financial correctness: invoices, payments, refunds, allocation, files, transactions, audit, idempotency, and balance calculation. + +`IslamicSundaySchool\Finance` may customize labels and policies for: + +```text +tuition +registration fees +supply/book fees +Qur'an/Arabic/Islamic studies program fees +sibling discounts +need-based scholarships +sadaqah/zakat-sponsored scholarships, if the organization supports them +masjid-family reporting labels +``` + +Do not put religious finance labels into core contracts. Core should use neutral concepts like charge, payment, discount, scholarship, donation classification, and allocation policy. The Islamic Sunday School layer maps those to local terminology. + +--- +## 1. Purpose + +Phase 3 extracts finance into a reusable `SchoolCore` module that can support any school system while allowing Islamic Sunday School-specific finance behavior through policies and extension services. + +The finance module must own the invariant parts of school finance: + +- invoices +- invoice items +- charges +- discounts +- payments +- refunds +- balances +- payment files +- financial audit logs +- payment allocation +- transaction safety +- authorization boundaries + +Islamic Sunday School may customize labels, tuition rules, donation classification, family/group billing behavior, scholarships, and reporting views, but it must not alter core payment correctness. + +The goal is not to make finance merely “cleaner.” The goal is to make it hard to corrupt money, leak files, or fork business logic across controllers like a spreadsheet that learned HTTP and got overconfident. + +--- + +## 2. Required Dependency Direction + +Finance must follow the modular platform boundary: + +```text +App\Domain\IslamicIslamicSundaySchool\Finance + ↓ depends on +App\Domain\SchoolCore\Finance + ↓ depends on +App\Domain\SchoolCore\Shared +``` + +Forbidden dependencies: + +```text +SchoolCore\Finance -> IslamicSundaySchool\* +SchoolCore\Finance -> Http\Request +SchoolCore\Finance -> auth() +SchoolCore\Finance -> controller classes +SchoolCore\Finance -> Islamic Sunday School vocabulary +SchoolCore\Finance -> direct unscoped school queries +``` + +Allowed dependencies: + +```text +IslamicSundaySchool\Finance -> SchoolCore\Finance contracts +Http\Controllers -> finance contracts +Jobs -> finance contracts +Finance services -> SchoolContext +Finance services -> shared audit/transaction/file contracts +``` + +--- + +## 3. Target Namespace Structure + +Create the finance module under `SchoolCore`: + +```text +app/ + Domain/ + SchoolCore/ + Finance/ + Contracts/ + InvoiceServiceContract.php + PaymentServiceContract.php + RefundServiceContract.php + ChargeServiceContract.php + PaymentAllocationPolicyContract.php + TuitionPolicyContract.php + FinanceAuditLoggerContract.php + FinanceFileServiceContract.php + DTO/ + CreateInvoiceData.php + InvoiceItemData.php + RecordPaymentData.php + EditPaymentData.php + RefundPaymentData.php + PaymentFileData.php + FinanceActorData.php + MoneyData.php + Enums/ + InvoiceStatus.php + PaymentStatus.php + PaymentMethod.php + LedgerDirection.php + FinanceAuditAction.php + Exceptions/ + FinanceException.php + InvoiceNotFoundException.php + PaymentNotFoundException.php + UnauthorizedFinanceAccessException.php + InvalidPaymentAmountException.php + InvoiceOverpaymentException.php + FinanceConcurrencyException.php + Services/ + InvoiceService.php + PaymentService.php + RefundService.php + ChargeService.php + BalanceCalculator.php + PaymentFileService.php + FinanceAuditLogger.php + Policies/ + DefaultPaymentAllocationPolicy.php + DefaultTuitionPolicy.php + Repositories/ + InvoiceRepository.php + PaymentRepository.php + FinanceReadRepository.php + Support/ + Money.php + FinanceTransactionRunner.php + FinanceIdempotencyKey.php + PaymentFileLocator.php +``` + +Islamic Sunday School-specific finance goes here: + +```text +app/ + Domain/ + IslamicSundaySchool/ + Finance/ + Policies/ + IslamicSundaySchoolTuitionPolicy.php + IslamicSundaySchoolPaymentAllocationPolicy.php + Reports/ + IslamicSundaySchoolFinanceReportBuilder.php + Providers/ + IslamicSundaySchoolFinanceServiceProvider.php +``` + +--- + +## 4. Core Design Rules + +### 4.1 Finance services must require `SchoolContext` + +Every public finance service method must receive or resolve `SchoolContext`. + +Example: + +```php +public function recordPayment( + SchoolContext $context, + RecordPaymentData $data +): PaymentResult; +``` + +A finance operation without context is invalid. + +### 4.2 Money must use fixed precision + +No float math in finance service logic. + +Allowed: + +- integer cents +- fixed decimal string +- database decimal with explicit rounding strategy +- dedicated `Money` value object + +Forbidden: + +```php +$amount = (float) $request->amount; +$total += $amount; +``` + +That is not financial software. That is numerology with database writes. + +### 4.3 Every mutation must be transactional + +Required transactional operations: + +- invoice creation +- invoice item update +- charge application +- discount application +- payment recording +- payment editing +- payment deletion/reversal +- refund creation +- invoice recalculation +- payment file association +- financial audit log creation + +### 4.4 Row locking is required for invoice/payment mutation + +Any mutation that changes invoice balance or payment amount must lock the relevant records. + +Required pattern: + +```php +DB::transaction(function () use ($context, $invoiceId, $data) { + $invoice = Invoice::query() + ->where('school_id', $context->schoolId()) + ->whereKey($invoiceId) + ->lockForUpdate() + ->firstOrFail(); + + // mutate payment/invoice safely here +}); +``` + +### 4.5 Controllers must not calculate balances + +Controllers may: + +- validate request +- authorize route +- build DTO +- call finance contract +- return resource + +Controllers may not: + +- calculate invoice totals +- calculate remaining balance +- determine payment allocation +- decide finance status +- serve files by arbitrary filename +- update invoice after payment manually + +--- + +## 5. Core Contracts + +### 5.1 `InvoiceServiceContract` + +```php +namespace App\Domain\SchoolCore\Finance\Contracts; + +use App\Domain\SchoolCore\Shared\Context\SchoolContext; +use App\Domain\SchoolCore\Finance\DTO\CreateInvoiceData; + +interface InvoiceServiceContract +{ + public function createInvoice(SchoolContext $context, CreateInvoiceData $data): InvoiceResult; + + public function recalculateInvoice(SchoolContext $context, int $invoiceId): InvoiceBalanceResult; + + public function voidInvoice(SchoolContext $context, int $invoiceId, string $reason): InvoiceResult; + + public function getInvoice(SchoolContext $context, int $invoiceId): InvoiceView; +} +``` + +### 5.2 `PaymentServiceContract` + +```php +namespace App\Domain\SchoolCore\Finance\Contracts; + +use App\Domain\SchoolCore\Shared\Context\SchoolContext; +use App\Domain\SchoolCore\Finance\DTO\RecordPaymentData; +use App\Domain\SchoolCore\Finance\DTO\EditPaymentData; + +interface PaymentServiceContract +{ + public function recordPayment(SchoolContext $context, RecordPaymentData $data): PaymentResult; + + public function editPayment(SchoolContext $context, int $paymentId, EditPaymentData $data): PaymentResult; + + public function reversePayment(SchoolContext $context, int $paymentId, string $reason): PaymentResult; + + public function getPayment(SchoolContext $context, int $paymentId): PaymentView; +} +``` + +### 5.3 `RefundServiceContract` + +```php +namespace App\Domain\SchoolCore\Finance\Contracts; + +use App\Domain\SchoolCore\Shared\Context\SchoolContext; +use App\Domain\SchoolCore\Finance\DTO\RefundPaymentData; + +interface RefundServiceContract +{ + public function refundPayment(SchoolContext $context, RefundPaymentData $data): RefundResult; +} +``` + +### 5.4 `PaymentAllocationPolicyContract` + +```php +namespace App\Domain\SchoolCore\Finance\Contracts; + +use App\Domain\SchoolCore\Shared\Context\SchoolContext; + +interface PaymentAllocationPolicyContract +{ + public function allocate( + SchoolContext $context, + InvoiceView $invoice, + Money $paymentAmount + ): PaymentAllocationResult; +} +``` + +Islamic Sunday School may override this contract to support family-level rules, scholarships, donations, or program-specific payment behavior. + +### 5.5 `TuitionPolicyContract` + +```php +namespace App\Domain\SchoolCore\Finance\Contracts; + +use App\Domain\SchoolCore\Shared\Context\SchoolContext; + +interface TuitionPolicyContract +{ + public function calculateExpectedCharges( + SchoolContext $context, + StudentFinanceProfile $student + ): ChargeCalculationResult; +} +``` + +### 5.6 `FinanceFileServiceContract` + +```php +namespace App\Domain\SchoolCore\Finance\Contracts; + +use App\Domain\SchoolCore\Shared\Context\SchoolContext; +use Symfony\Component\HttpFoundation\BinaryFileResponse; + +interface FinanceFileServiceContract +{ + public function attachPaymentFile( + SchoolContext $context, + int $paymentId, + PaymentFileData $file + ): PaymentFileResult; + + public function downloadPaymentFile( + SchoolContext $context, + int $paymentId, + string $mode + ): BinaryFileResponse; +} +``` + +Critical rule: payment files are accessed by payment ID or file record ID, never by arbitrary filename. + +--- + +## 6. DTO Requirements + +Create DTOs for finance inputs. Do not pass raw request arrays into finance services. + +### 6.1 `RecordPaymentData` + +Fields: + +```text +invoice_id +student_id, nullable +guardian_id, nullable +amount +currency +payment_method +payment_date +reference_number, nullable +notes, nullable +idempotency_key, nullable +uploaded_file, nullable +metadata, nullable +``` + +Validation belongs in FormRequest, but DTO construction must still guard required invariants. + +### 6.2 `EditPaymentData` + +Fields: + +```text +amount, nullable +payment_method, nullable +payment_date, nullable +reference_number, nullable +notes, nullable +replacement_file, nullable +edit_reason +metadata, nullable +``` + +`edit_reason` is required for auditability. + +### 6.3 `MoneyData` + +Fields: + +```text +amount_minor +currency +scale +``` + +Example: + +```text +amount_minor = 12500 +currency = USD +scale = 2 +``` + +This represents `$125.00`. + +--- + +## 7. Payment File Access Hardening + +Current risk: file access based on filename can allow unauthorized users to guess or retrieve files if route authorization is incomplete. + +New rule: + +```text +No finance file download may accept only a filename. +``` + +Target route: + +```text +GET /api/finance/payments/{payment}/file +``` + +Required flow: + +```text +1. Resolve SchoolContext. +2. Load payment scoped to school_id. +3. Authorize actor against payment/invoice/student/guardian. +4. Resolve file from payment record. +5. Confirm file path belongs to configured finance storage disk/path. +6. Log access. +7. Stream/download file. +``` + +Forbidden flow: + +```text +1. Accept filename. +2. basename(filename). +3. Search upload folders. +4. Download first matching file. +``` + +Basename prevents traversal. It does not prove authorization. This is apparently a distinction computers need humans to explain. + +--- + +## 8. Transaction and Locking Strategy + +### 8.1 Payment recording + +Required transaction: + +```text +BEGIN + lock invoice + validate invoice belongs to school + validate actor authorization + validate payment amount + create payment + attach payment file, if provided + allocate payment + recalculate invoice + write audit log +COMMIT +``` + +### 8.2 Payment editing + +Required transaction: + +```text +BEGIN + lock payment + lock invoice + capture before state + validate actor authorization + validate new amount/method/date + update payment + reallocate payment if needed + recalculate invoice + write audit log with before/after +COMMIT +``` + +### 8.3 Payment reversal + +Required transaction: + +```text +BEGIN + lock payment + lock invoice + mark payment reversed + recalculate invoice + write audit log +COMMIT +``` + +### 8.4 Refund + +Required transaction: + +```text +BEGIN + lock payment + lock invoice + validate refundable amount + create refund record + update payment refunded amount/status + recalculate invoice + write audit log +COMMIT +``` + +--- + +## 9. Audit Logging Requirements + +Every finance mutation must create a finance audit event. + +Required audit fields: + +```text +school_id +academic_year_id +term_id, nullable +actor_user_id +actor_role_snapshot +action +entity_type +entity_id +before_json, nullable +after_json, nullable +reason, nullable +request_id +ip_address, nullable +user_agent, nullable +idempotency_key, nullable +created_at +``` + +Required actions: + +```text +invoice.created +invoice.updated +invoice.voided +invoice.recalculated +payment.recorded +payment.edited +payment.reversed +payment.file_attached +payment.file_downloaded +refund.created +charge.applied +discount.applied +``` + +Audit logs are append-only. Do not update old audit rows unless the project enjoys inventing evidence after the fact. + +--- + +## 10. Idempotency Requirements + +Payment mutations must support idempotency. + +Minimum implementation: + +```text +idempotency_key +actor_user_id +school_id +operation +payload_hash +result_entity_type +result_entity_id +created_at +expires_at +``` + +Behavior: + +```text +- Same key + same operation + same payload returns original result. +- Same key + same operation + different payload fails. +- Expired keys may be purged. +- Missing idempotency key is allowed for legacy endpoints during transition, but new clients should send one. +``` + +Apply first to: + +```text +- record payment +- edit payment +- refund payment +``` + +--- + +## 11. Extension Model for Islamic Sunday School + +Islamic Sunday School finance must extend the core through policies and adapters, not by modifying core services. + +### 11.1 Islamic Sunday School allowed customizations + +```text +- family-based billing labels +- donation classification +- ministry scholarship logic +- Islamic Sunday School-specific tuition plans +- custom discount labels +- custom finance dashboards +- masjid/family reports +``` + +### 11.2 Islamic Sunday School forbidden customizations + +```text +- bypassing invoice row locks +- bypassing payment audit logs +- downloading files by filename +- calculating balances in controllers +- writing payments without SchoolContext +- storing Islamic Sunday School terms inside core contracts +``` + +### 11.3 Binding example + +```php +$this->app->bind( + TuitionPolicyContract::class, + IslamicSundaySchoolTuitionPolicy::class +); + +$this->app->bind( + PaymentAllocationPolicyContract::class, + IslamicSundaySchoolPaymentAllocationPolicy::class +); +``` + +--- + +## 12. Migration Strategy + +### 12.1 Do not rewrite all finance at once + +Use vertical slices. + +Recommended order: + +```text +1. Payment file access +2. Payment recording +3. Payment editing +4. Invoice recalculation +5. Refunds/reversals +6. Discounts/charges +7. Reports +``` + +Why this order: + +- File access is a security boundary. +- Payment recording/editing affects money. +- Invoice recalculation must become single-source-of-truth. +- Refunds and discounts build on stable payment/invoice primitives. +- Reports should consume stable finance read models last. + +### 12.2 Compatibility layer + +Existing controllers may remain temporarily, but must delegate to new contracts. + +Example: + +```text +PaymentManualController + -> RecordPaymentRequest + -> SchoolContext + -> RecordPaymentData + -> PaymentServiceContract::recordPayment() + -> PaymentResource +``` + +Legacy service methods should become wrappers or be deprecated. + +### 12.3 Deprecation rule + +Every legacy finance method must be labeled as one of: + +```text +canonical +adapter +deprecated +remove +``` + +No unlabeled duplicate finance logic. + +--- + +## 13. First Implementation Slice: Payment File Access + +### Objective + +Replace filename-based payment file download with payment-scoped, authorized access. + +### Tasks + +```text +FIN-001. Create FinanceFileServiceContract. +FIN-002. Create PaymentFileService implementation. +FIN-003. Create FileAccessPolicyContract if not already present from Phase 2. +FIN-004. Add payment-file route using payment ID. +FIN-005. Add FormRequest for file mode validation. +FIN-006. Update controller to call FinanceFileServiceContract. +FIN-007. Add audit log action payment.file_downloaded. +FIN-008. Mark old filename route deprecated or disable it behind compatibility flag. +``` + +### Required tests + +```text +- admin can download payment file in same school +- finance user can download payment file in same school +- parent can download payment file only for own student/family, if policy allows +- unrelated parent cannot download +- teacher cannot download unless explicitly allowed +- user from another school cannot download +- guessed filename cannot download +- missing file returns 404 +- payment without file returns 404 +- invalid mode fails validation +- file download creates audit log +``` + +### Done when + +```text +- No active endpoint downloads payment files by filename alone. +- Payment file authorization is policy-based. +- Cross-school file access is blocked by test. +- Access is audited. +``` + +--- + +## 14. Second Implementation Slice: Payment Recording + +### Objective + +Move manual payment recording into modular finance service. + +### Tasks + +```text +FIN-009. Create RecordPaymentData. +FIN-010. Create PaymentServiceContract. +FIN-011. Implement PaymentService::recordPayment(). +FIN-012. Create Money value object. +FIN-013. Add FinanceTransactionRunner. +FIN-014. Lock invoice during payment recording. +FIN-015. Move invoice recalculation into BalanceCalculator. +FIN-016. Add payment.recorded audit event. +FIN-017. Add idempotency support. +FIN-018. Update PaymentManualController to delegate. +``` + +### Required tests + +```text +- records valid payment +- rejects zero amount +- rejects negative amount +- rejects overpayment unless policy permits +- rejects invoice from another school +- recalculates invoice balance +- creates audit log +- creates same result for repeated idempotency key +- rejects same idempotency key with different payload +- concurrent payment attempts do not corrupt invoice balance +``` + +### Done when + +```text +- Controller no longer calculates payment balance. +- Payment creation is transactional. +- Invoice recalculation is single-source. +- Duplicate payment submission is idempotent. +``` + +--- + +## 15. Third Implementation Slice: Payment Editing + +### Objective + +Make payment editing transaction-safe and auditable. + +### Tasks + +```text +FIN-019. Create EditPaymentData. +FIN-020. Implement PaymentService::editPayment(). +FIN-021. Require edit reason. +FIN-022. Lock payment and invoice rows. +FIN-023. Capture before/after state. +FIN-024. Recalculate invoice inside transaction. +FIN-025. Add payment.edited audit event. +FIN-026. Update controller to delegate. +``` + +### Required tests + +```text +- edits amount safely +- edits method safely +- rejects unauthorized actor +- rejects cross-school payment edit +- rejects edit without reason +- recalculates invoice balance +- creates before/after audit log +- concurrent edits do not corrupt balance +``` + +### Done when + +```text +- Payment edit is fully transactional. +- Audit record includes before and after values. +- Invoice balance cannot drift during concurrent edits. +``` + +--- + +## 16. Fourth Implementation Slice: Invoice Recalculation + +### Objective + +Centralize all invoice balance calculations. + +### Tasks + +```text +FIN-027. Create BalanceCalculator. +FIN-028. Define invoice total calculation. +FIN-029. Define paid amount calculation. +FIN-030. Define refund/reversal handling. +FIN-031. Define status transition rules. +FIN-032. Replace scattered recalculation calls. +FIN-033. Add invoice.recalculated audit event where mutation-triggered. +``` + +### Invoice status rules + +Example baseline: + +```text +draft: invoice created but not issued +open: invoice issued with outstanding balance +partial: invoice has payment but balance remains +paid: balance is zero +overpaid: paid amount exceeds expected amount, only if policy permits +void: invoice intentionally canceled +``` + +### Required tests + +```text +- invoice total includes invoice items +- discount reduces balance correctly +- payment reduces balance correctly +- reversed payment restores balance +- refund restores balance correctly +- paid status is applied correctly +- partial status is applied correctly +- void invoice cannot receive payment unless policy permits +``` + +### Done when + +```text +- Invoice balance is calculated by one service. +- Controllers and reports do not duplicate balance logic. +- Finance reports use read models derived from the calculator. +``` + +--- + +## 17. Fifth Implementation Slice: Refunds and Reversals + +### Objective + +Add safe, auditable reversal/refund behavior. + +### Tasks + +```text +FIN-034. Create RefundServiceContract. +FIN-035. Create RefundPaymentData. +FIN-036. Implement refundPayment(). +FIN-037. Implement reversePayment(). +FIN-038. Add reason requirement. +FIN-039. Add refund.created and payment.reversed audit events. +FIN-040. Add refund/reversal status handling to BalanceCalculator. +``` + +### Required tests + +```text +- refund cannot exceed paid amount +- refund requires reason +- reversal requires reason +- reversed payment is excluded from paid total +- refunded amount affects balance +- refund creates audit log +- reversal creates audit log +- cross-school refund fails +- unauthorized refund fails +``` + +### Done when + +```text +- Refund and reversal behavior is explicit. +- Existing payment rows are not silently deleted. +- Financial history remains auditable. +``` + +--- + +## 18. Finance Reports + +Reports must not define finance truth. They display finance truth. + +### Tasks + +```text +FIN-041. Create FinanceReadRepository. +FIN-042. Create invoice balance read model. +FIN-043. Create payment summary read model. +FIN-044. Create outstanding balance read model. +FIN-045. Update finance reports to consume read models. +FIN-046. Add school context to all report queries. +``` + +### Required tests + +```text +- finance report excludes other schools +- outstanding balance matches BalanceCalculator +- payment report handles refunds/reversals +- report authorization is enforced +- export output matches approved snapshot +``` + +--- + +## 19. Database and Schema Requirements + +### 19.1 Required constraints + +Add or verify: + +```text +payments.school_id indexed +payments.invoice_id indexed +payments.student_id indexed, nullable +payments.guardian_id indexed, nullable +payments.status indexed +payments.idempotency_key indexed, nullable +payments.amount uses decimal or integer minor amount + +invoices.school_id indexed +invoices.student_id indexed, nullable +invoices.status indexed +invoices.total_amount fixed precision +invoices.balance_amount fixed precision + +finance_audit_logs.school_id indexed +finance_audit_logs.actor_user_id indexed +finance_audit_logs.entity_type/entity_id indexed +finance_audit_logs.created_at indexed + +payment_files.school_id indexed +payment_files.payment_id indexed +payment_files.storage_disk +payment_files.storage_path +``` + +### 19.2 Unique constraints + +Recommended: + +```text +unique(school_id, idempotency_key, operation) where idempotency_key is not null +unique(payment_id, file_type) if only one file per type is allowed +``` + +### 19.3 Migration caution + +Do not rename columns casually in the first slice. Wrap legacy schema with repositories/adapters first. Database heroics during finance extraction are how downtime earns a dramatic soundtrack. + +--- + +## 20. Authorization Requirements + +Finance authorization must be policy-based. + +Minimum actions: + +```text +invoice.view +invoice.create +invoice.update +invoice.void +payment.view +payment.record +payment.edit +payment.reverse +payment.refund +payment.file.download +finance.report.view +``` + +Every finance endpoint must have tests for: + +```text +unauthenticated +wrong school +wrong role +wrong owner/family +authorized admin +authorized finance staff +``` + +Authorization must check both: + +```text +actor role/capability +resource ownership/school boundary +``` + +--- + +## 21. FormRequest Requirements + +Create or update: + +```text +RecordPaymentRequest +EditPaymentRequest +RefundPaymentRequest +ReversePaymentRequest +DownloadPaymentFileRequest +CreateInvoiceRequest +VoidInvoiceRequest +ApplyChargeRequest +ApplyDiscountRequest +``` + +Each FormRequest must define: + +```text +authorize() +rules() +messages(), if needed +toDTO(), optional but recommended +``` + +Controllers should use: + +```php +$data = $request->toDTO(); +``` + +Not: + +```php +$data = array_merge($request->all(), $request->query->all(), $request->json()->all()); +``` + +That merge pattern should be retired, preferably with a small ceremony and no witnesses. + +--- + +## 22. Service Provider Bindings + +Create or update: + +```php +namespace App\Providers; + +use Illuminate\Support\ServiceProvider; +use App\Domain\SchoolCore\Finance\Contracts\PaymentServiceContract; +use App\Domain\SchoolCore\Finance\Services\PaymentService; + +final class SchoolCoreFinanceServiceProvider extends ServiceProvider +{ + public function register(): void + { + $this->app->bind(PaymentServiceContract::class, PaymentService::class); + $this->app->bind(InvoiceServiceContract::class, InvoiceService::class); + $this->app->bind(RefundServiceContract::class, RefundService::class); + $this->app->bind(FinanceFileServiceContract::class, PaymentFileService::class); + $this->app->bind(FinanceAuditLoggerContract::class, FinanceAuditLogger::class); + + $this->app->bind( + PaymentAllocationPolicyContract::class, + DefaultPaymentAllocationPolicy::class + ); + + $this->app->bind( + TuitionPolicyContract::class, + DefaultTuitionPolicy::class + ); + } +} +``` + +Islamic Sunday School may override policy bindings in its own provider. + +--- + +## 23. Architecture Tests + +Add tests that fail if finance boundaries are violated. + +Required assertions: + +```text +SchoolCore\Finance must not depend on Domain\IslamicIslamicSundaySchool +SchoolCore\Finance must not depend on Illuminate\Http\Request +SchoolCore\Finance services must not call auth() +SchoolCore\Finance services must not call request() +Controllers must not instantiate finance concrete services directly +Finance mutations must use FinanceTransactionRunner or DB transaction +Payment file downloads must go through FinanceFileServiceContract +``` + +Optional static checks: + +```text +No float casts in SchoolCore\Finance +No direct response()->download in finance controllers +No filename-only payment file route +No raw DB update for invoice balance outside BalanceCalculator +``` + +--- + +## 24. Rollout Plan + +### Step 1: Add finance module scaffolding + +```text +- create namespaces +- create contracts +- create DTOs +- create service provider +- bind default implementations +``` + +### Step 2: Add tests around existing behavior + +```text +- payment file download tests +- payment record tests +- payment edit tests +- invoice recalculation tests +``` + +Some tests may expose current failures. Good. That is the point. A failing test is cheaper than explaining to a parent why their invoice time-traveled. + +### Step 3: Implement payment file slice + +```text +- safest isolated security win +- minimal balance/invoice interaction +``` + +### Step 4: Implement payment record slice + +```text +- introduce transaction runner +- introduce money value object +- introduce audit log +``` + +### Step 5: Implement payment edit slice + +```text +- fix transaction safety +- before/after audit +``` + +### Step 6: Centralize invoice recalculation + +```text +- remove duplicate balance logic +- point reports toward read models +``` + +### Step 7: Add refunds/reversals + +```text +- build on stable payment/invoice model +``` + +### Step 8: Deprecate legacy methods + +```text +- mark adapters +- remove unreachable duplicate logic +- update docs +``` + +--- + +## 25. Implementation Tickets + +## FIN-001: Create finance module scaffold + +### Description + +Create the `SchoolCore\Finance` namespace and initial empty contracts, DTO folders, service folders, and provider. + +### Acceptance Criteria + +- Namespace exists. +- Service provider is registered. +- Empty contracts compile. +- No controller behavior changes yet. + +--- + +## FIN-002: Add Money value object + +### Description + +Create a `Money` value object using integer minor units or fixed decimal strategy. + +### Acceptance Criteria + +- No float input accepted without explicit parsing. +- Supports add, subtract, compare, equals, isZero, isPositive. +- Currency mismatch throws exception. +- Unit tests cover rounding and comparison. + +--- + +## FIN-003: Add finance audit logger + +### Description + +Create append-only finance audit log service. + +### Acceptance Criteria + +- Logs actor, context, action, entity, before/after, reason. +- Audit rows are school-scoped. +- Unit tests verify required fields. +- Mutation services can call logger inside transaction. + +--- + +## FIN-004: Replace payment file download flow + +### Description + +Implement payment-scoped file download. + +### Acceptance Criteria + +- Route uses payment ID. +- File is resolved from payment/file record. +- Authorization uses policy. +- Cross-school access fails. +- Filename guessing fails. +- Audit event is written. + +--- + +## FIN-005: Extract payment recording + +### Description + +Move payment recording into `PaymentService`. + +### Acceptance Criteria + +- Uses `SchoolContext`. +- Uses transaction. +- Locks invoice. +- Uses `Money`. +- Writes audit log. +- Supports idempotency. +- Controller delegates only. + +--- + +## FIN-006: Extract payment editing + +### Description + +Move payment editing into `PaymentService`. + +### Acceptance Criteria + +- Uses transaction. +- Locks payment and invoice. +- Requires reason. +- Writes before/after audit log. +- Recalculates invoice inside lock. +- Concurrent edit test passes. + +--- + +## FIN-007: Centralize invoice recalculation + +### Description + +Create `BalanceCalculator` and remove duplicate invoice balance logic. + +### Acceptance Criteria + +- All payment/refund/discount operations use same calculator. +- Tests cover partial, paid, overpaid, void, refund, reversed. +- No controller calculates invoice balance. + +--- + +## FIN-008: Add refunds and reversals + +### Description + +Create explicit refund and reversal flows. + +### Acceptance Criteria + +- Refunds cannot exceed refundable amount. +- Reversals require reason. +- No silent deletion of financial history. +- Audit logs exist. +- Balance recalculation is correct. + +--- + +## FIN-009: Add finance architecture tests + +### Description + +Enforce modular finance boundaries in CI. + +### Acceptance Criteria + +- `SchoolCore\Finance` cannot import `IslamicSundaySchool`. +- Services cannot call `auth()` or `request()`. +- No filename-only payment file download route remains active. +- No float casts in finance module. +- CI fails on boundary violation. + +--- + +## 26. Definition of Done for Phase 3 + +Phase 3 is done when: + +```text +- Finance core contracts exist. +- Payment file access is payment-scoped and authorized. +- Payment recording uses modular service. +- Payment editing uses modular service. +- Invoice recalculation is centralized. +- Finance audit logs are append-only and used by mutations. +- Money math avoids floats. +- Finance services require SchoolContext. +- Islamic Sunday School finance behavior is implemented through policies. +- Cross-school finance access is blocked by tests. +- Architecture tests prevent SchoolCore from depending on IslamicSundaySchool. +- Legacy finance methods are labeled as canonical, adapter, deprecated, or remove. +``` + +--- + +## 27. Non-Goals + +Do not include in Phase 3: + +```text +- complete finance UI redesign +- total database schema rewrite +- full accounting ledger replacement +- payroll +- external payment gateway integration +- donation tax receipt system +- masjid/community management CRM +``` + +Those may come later. Phase 3 is about building the reliable reusable finance core, not adopting every financial ambition a committee can imagine after coffee. + +--- + +## 28. Main Risks + +### Risk: accidental behavior break + +Mitigation: + +```text +- snapshot current API responses +- migrate one vertical slice at a time +- keep legacy adapters temporarily +``` + +### Risk: cross-school leakage + +Mitigation: + +```text +- mandatory SchoolContext +- school-scoped repositories +- authorization tests +``` + +### Risk: balance drift + +Mitigation: + +```text +- one BalanceCalculator +- transactions +- row locks +- concurrency tests +``` + +### Risk: Islamic Sunday School leakage into core + +Mitigation: + +```text +- contract vocabulary review +- architecture tests +- Islamic Sunday School policies only +``` + +### Risk: duplicated old/new finance logic + +Mitigation: + +```text +- legacy method labels +- controller delegation +- deprecation tickets +``` + +--- + +## 29. Immediate Next Work Queue + +Start here: + +```text +1. FIN-001 Create finance module scaffold +2. FIN-002 Create Money value object +3. FIN-003 Create finance audit logger +4. FIN-004 Replace payment file download flow +5. FIN-005 Extract payment recording +6. FIN-006 Extract payment editing +7. FIN-007 Centralize invoice recalculation +8. FIN-009 Add finance architecture tests +``` + +Do not start with reports. Reports consume truth. They do not create it. Starting with reports while payment mutation is unsafe is like painting a house while the foundation is trying to leave. diff --git a/docs/modular_plans/phase_4_modular_attendance_execution_plan_islamic.md b/docs/modular_plans/phase_4_modular_attendance_execution_plan_islamic.md new file mode 100644 index 00000000..26d9f13e --- /dev/null +++ b/docs/modular_plans/phase_4_modular_attendance_execution_plan_islamic.md @@ -0,0 +1,1799 @@ +# Phase 4 Execution Plan: Modular Attendance and Scanner Extraction + + +# Islamic Sunday School Alignment Update + +Phase 4 attendance now targets **Islamic Sunday School** as the extension layer. + +`SchoolCore\Attendance` owns scanner behavior, attendance records, sessions, status, idempotency, audit, and cross-school isolation. + +`IslamicSundaySchool\Attendance` may customize: + +```text +Sunday program sessions +Qur'an class sessions +Arabic class sessions +Islamic studies sessions +halaqa/group attendance +Jumu'ah or special-program attendance, if used by the school +volunteer attendance +late arrival windows +masjid calendar rules +``` + +The core must use neutral terms such as session, group, program, student, staff, volunteer, and attendance status. Islamic-specific vocabulary belongs in extension resolvers and policies. + +--- +## 1. Purpose + +Phase 4 extracts attendance and scanner behavior into a reusable `SchoolCore` module that can support any school system while allowing Islamic Sunday School-specific attendance behavior through policies and extension services. + +The attendance module must own the invariant parts of attendance: + +- attendance sessions/days +- student attendance +- staff attendance +- check-in/check-out events +- scan logs +- badge lookup +- late/absent/present status calculation +- idempotency +- audit logging +- cross-school isolation +- authorization boundaries + +Islamic Sunday School may customize: + +- Islamic Sunday School program session rules +- program/session attendance grouping +- program volunteer attendance +- late arrival windows +- class/session mapping +- masjid/family reporting labels +- event-specific attendance rules + +Islamic Sunday School must not own the core attendance mutation rules. That way the system can serve schools, academies, training centers, camps, daycare programs, Islamic Sunday Schools, and future human organizational inventions that will somehow still need attendance. + +--- + +## 2. Required Dependency Direction + +Attendance must follow the modular platform boundary: + +```text +App\Domain\IslamicIslamicSundaySchool\Attendance + ↓ depends on +App\Domain\SchoolCore\Attendance + ↓ depends on +App\Domain\SchoolCore\Shared +``` + +Forbidden dependencies: + +```text +SchoolCore\Attendance -> IslamicSundaySchool\* +SchoolCore\Attendance -> Http\Request +SchoolCore\Attendance -> auth() +SchoolCore\Attendance -> controller classes +SchoolCore\Attendance -> Islamic Sunday School vocabulary +SchoolCore\Attendance -> direct unscoped school queries +``` + +Allowed dependencies: + +```text +IslamicSundaySchool\Attendance -> SchoolCore\Attendance contracts +Http\Controllers -> attendance contracts +Jobs -> attendance contracts +Attendance services -> SchoolContext +Attendance services -> shared audit/transaction contracts +Attendance services -> academic calendar provider contract +``` + +--- + +## 3. Target Namespace Structure + +Create the attendance module under `SchoolCore`: + +```text +app/ + Domain/ + SchoolCore/ + Attendance/ + Contracts/ + AttendanceServiceContract.php + ScannerServiceContract.php + BadgeLookupServiceContract.php + AttendancePolicyContract.php + AttendanceSessionResolverContract.php + AttendanceStatusPolicyContract.php + AttendanceAuditLoggerContract.php + AttendanceIdempotencyServiceContract.php + StaffAttendanceServiceContract.php + StudentAttendanceServiceContract.php + DTO/ + RecordStudentAttendanceData.php + RecordStaffAttendanceData.php + ProcessScanData.php + BadgeLookupData.php + AttendanceSessionData.php + AttendanceStatusData.php + AttendanceActorData.php + AttendanceAuditData.php + AttendanceIdempotencyData.php + Enums/ + AttendanceSubjectType.php + AttendanceStatus.php + ScanAction.php + ScanSource.php + AttendanceAuditAction.php + AttendanceSessionStatus.php + Exceptions/ + AttendanceException.php + AttendanceSessionNotFoundException.php + BadgeNotFoundException.php + DuplicateScanException.php + UnauthorizedAttendanceAccessException.php + InvalidAttendanceStatusException.php + AttendanceConcurrencyException.php + Services/ + AttendanceService.php + ScannerService.php + BadgeLookupService.php + StudentAttendanceService.php + StaffAttendanceService.php + AttendanceSessionResolver.php + AttendanceStatusResolver.php + AttendanceAuditLogger.php + AttendanceIdempotencyService.php + LateArrivalEvaluator.php + Policies/ + DefaultAttendancePolicy.php + DefaultAttendanceStatusPolicy.php + DefaultAttendanceSessionPolicy.php + Repositories/ + AttendanceRepository.php + AttendanceReadRepository.php + BadgeRepository.php + AttendanceSessionRepository.php + Support/ + AttendanceTransactionRunner.php + AttendanceIdempotencyKey.php + ScanFingerprint.php + AttendanceClock.php +``` + +Islamic Sunday School-specific attendance goes here: + +```text +app/ + Domain/ + IslamicSundaySchool/ + Attendance/ + Policies/ + IslamicSundaySchoolAttendancePolicy.php + IslamicSundaySchoolAttendanceStatusPolicy.php + IslamicSundaySchoolSessionPolicy.php + Services/ + IslamicSundaySchoolSessionResolver.php + IslamicSundaySchoolLateArrivalEvaluator.php + Reports/ + IslamicSundaySchoolAttendanceReportBuilder.php + Providers/ + IslamicSundaySchoolAttendanceServiceProvider.php +``` + +--- + +## 4. Core Design Rules + +### 4.1 Attendance services must require `SchoolContext` + +Every public attendance service method must receive `SchoolContext`. + +Example: + +```php +public function processScan( + SchoolContext $context, + ProcessScanData $data +): ScanResult; +``` + +A scan without context is invalid. A scan with the wrong context is worse. + +### 4.2 Controllers must not decide attendance state + +Controllers may: + +- validate request +- authorize route +- build DTO +- call service contract +- return resource + +Controllers may not: + +- decide present/late/absent +- create attendance records directly +- create scan logs directly +- lookup badges directly +- calculate session dates +- mutate attendance day/session state +- decide duplicate scan handling + +### 4.3 Attendance writes must be transactional + +Required transactional operations: + +- scanner check-in/check-out +- student attendance manual edit +- staff attendance manual edit +- late slip/log creation +- attendance day/session creation +- bulk attendance import +- absence marking +- status recalculation + +### 4.4 Scanner flow must be idempotent + +Duplicate scans must not double-count attendance. + +Minimum duplicate identity: + +```text +school_id +subject_type +subject_id +session_id or attendance_date +station_id, nullable +scan_action +scan_source +scan_time_window +``` + +Example behavior: + +```text +- same student scans same station twice within configured duplicate window + => return existing result or duplicate warning, do not create second attendance mutation +``` + +### 4.5 Attendance must separate subject from role + +Attendance subjects are not always students. + +Core supported subject types: + +```text +student +staff +volunteer +visitor +``` + +Islamic Sunday School may map program volunteers to `volunteer` or `staff`, but core must not hardcode ministry language. + +--- + +## 5. Core Contracts + +### 5.1 `ScannerServiceContract` + +```php +namespace App\Domain\SchoolCore\Attendance\Contracts; + +use App\Domain\SchoolCore\Shared\Context\SchoolContext; +use App\Domain\SchoolCore\Attendance\DTO\ProcessScanData; + +interface ScannerServiceContract +{ + public function processScan(SchoolContext $context, ProcessScanData $data): ScanResult; +} +``` + +### 5.2 `AttendanceServiceContract` + +```php +namespace App\Domain\SchoolCore\Attendance\Contracts; + +use App\Domain\SchoolCore\Shared\Context\SchoolContext; + +interface AttendanceServiceContract +{ + public function getSessionAttendance(SchoolContext $context, int $sessionId): AttendanceSessionView; + + public function summarizeAttendance(SchoolContext $context, AttendanceSummaryQuery $query): AttendanceSummaryResult; +} +``` + +### 5.3 `StudentAttendanceServiceContract` + +```php +namespace App\Domain\SchoolCore\Attendance\Contracts; + +use App\Domain\SchoolCore\Shared\Context\SchoolContext; +use App\Domain\SchoolCore\Attendance\DTO\RecordStudentAttendanceData; + +interface StudentAttendanceServiceContract +{ + public function recordStudentAttendance( + SchoolContext $context, + RecordStudentAttendanceData $data + ): AttendanceRecordResult; +} +``` + +### 5.4 `StaffAttendanceServiceContract` + +```php +namespace App\Domain\SchoolCore\Attendance\Contracts; + +use App\Domain\SchoolCore\Shared\Context\SchoolContext; +use App\Domain\SchoolCore\Attendance\DTO\RecordStaffAttendanceData; + +interface StaffAttendanceServiceContract +{ + public function recordStaffAttendance( + SchoolContext $context, + RecordStaffAttendanceData $data + ): AttendanceRecordResult; +} +``` + +### 5.5 `BadgeLookupServiceContract` + +```php +namespace App\Domain\SchoolCore\Attendance\Contracts; + +use App\Domain\SchoolCore\Shared\Context\SchoolContext; +use App\Domain\SchoolCore\Attendance\DTO\BadgeLookupData; + +interface BadgeLookupServiceContract +{ + public function lookup(SchoolContext $context, BadgeLookupData $data): BadgeLookupResult; +} +``` + +### 5.6 `AttendanceSessionResolverContract` + +```php +namespace App\Domain\SchoolCore\Attendance\Contracts; + +use App\Domain\SchoolCore\Shared\Context\SchoolContext; + +interface AttendanceSessionResolverContract +{ + public function resolveForScan( + SchoolContext $context, + ScanSubjectView $subject, + ScanTimestamp $timestamp + ): AttendanceSessionView; +} +``` + +Islamic Sunday School may bind this contract to a Islamic Sunday School-specific resolver. + +### 5.7 `AttendancePolicyContract` + +```php +namespace App\Domain\SchoolCore\Attendance\Contracts; + +use App\Domain\SchoolCore\Shared\Context\SchoolContext; + +interface AttendancePolicyContract +{ + public function canRecordAttendance( + SchoolContext $context, + AttendanceSubjectView $subject, + AttendanceSessionView $session + ): bool; + + public function duplicateScanWindowSeconds(SchoolContext $context): int; + + public function allowCheckout(SchoolContext $context): bool; + + public function allowManualOverride(SchoolContext $context): bool; +} +``` + +### 5.8 `AttendanceStatusPolicyContract` + +```php +namespace App\Domain\SchoolCore\Attendance\Contracts; + +use App\Domain\SchoolCore\Shared\Context\SchoolContext; + +interface AttendanceStatusPolicyContract +{ + public function statusForScan( + SchoolContext $context, + AttendanceSessionView $session, + ScanTimestamp $timestamp + ): AttendanceStatusResult; +} +``` + +--- + +## 6. DTO Requirements + +Do not pass raw request arrays into attendance services. + +### 6.1 `ProcessScanData` + +Fields: + +```text +badge_value +station_id, nullable +scan_action +scan_source +scanned_at +device_id, nullable +client_request_id, nullable +metadata, nullable +idempotency_key, nullable +``` + +### 6.2 `RecordStudentAttendanceData` + +Fields: + +```text +student_id +session_id, nullable +attendance_date +status +source +recorded_at +reason, nullable +notes, nullable +override_reason, nullable +idempotency_key, nullable +metadata, nullable +``` + +### 6.3 `RecordStaffAttendanceData` + +Fields: + +```text +staff_id +session_id, nullable +attendance_date +status +source +recorded_at +reason, nullable +notes, nullable +override_reason, nullable +idempotency_key, nullable +metadata, nullable +``` + +### 6.4 `BadgeLookupData` + +Fields: + +```text +badge_value +badge_type, nullable +subject_type_hint, nullable +station_id, nullable +``` + +--- + +## 7. Scanner Flow Target Design + +Current risk: scanner logic can become a giant controller/service blob that mixes lookup, validation, attendance mutation, late logic, logging, and response formatting. + +Target flow: + +```text +ScannerController + -> ProcessScanRequest + -> SchoolContext + -> ProcessScanData + -> ScannerServiceContract::processScan() + -> ScanResource +``` + +Inside `ScannerService`: + +```text +1. Validate SchoolContext. +2. Normalize scan timestamp. +3. Build idempotency fingerprint. +4. Check duplicate scan. +5. Lookup badge. +6. Resolve attendance subject. +7. Resolve session/day. +8. Authorize attendance recording. +9. Determine scan status through policy. +10. Start transaction. +11. Lock subject/session attendance row. +12. Create or update attendance record. +13. Create scan log. +14. Create late/event log if needed. +15. Write audit log. +16. Store idempotency result. +17. Commit. +18. Return stable scan result. +``` + +Controllers should not know any of that. They have one job: receive chaos from the internet and pass it to adults. + +--- + +## 8. Attendance Session Model + +Core must support multiple attendance models: + +```text +daily attendance +period attendance +session attendance +event attendance +check-in/check-out attendance +``` + +Suggested neutral terms: + +```text +attendance_session +attendance_record +attendance_event +scan_log +attendance_subject +``` + +Avoid core terms like: + +```text +Sunday program +liturgy +Islamic Sunday School class +volunteer/program group +homeroom-only +semester-only +``` + +Those can exist in extensions. + +### 8.1 Session fields + +Suggested fields: + +```text +id +school_id +academic_year_id +term_id, nullable +name +session_type +starts_at +ends_at +timezone +status +metadata +created_at +updated_at +``` + +### 8.2 Record fields + +Suggested fields: + +```text +id +school_id +attendance_session_id, nullable +attendance_date +subject_type +subject_id +status +source +first_seen_at, nullable +last_seen_at, nullable +recorded_by_user_id, nullable +override_reason, nullable +metadata +created_at +updated_at +``` + +### 8.3 Event/scan log fields + +Suggested fields: + +```text +id +school_id +attendance_record_id, nullable +subject_type +subject_id +badge_id, nullable +station_id, nullable +scan_action +scan_source +scanned_at +status_result +duplicate_of_id, nullable +raw_payload_json, nullable +created_at +``` + +--- + +## 9. Attendance Status Rules + +Core statuses: + +```text +present +late +absent +excused +unexcused +checked_in +checked_out +unknown +``` + +Optional extended statuses: + +```text +left_early +medical +field_trip +remote +tardy_excused +tardy_unexcused +``` + +Rules: + +```text +- Status calculation belongs to AttendanceStatusPolicyContract. +- Manual override requires reason. +- Override must create audit log. +- Status transitions must be explicit. +- Reports must use canonical status codes, not display labels. +``` + +Islamic Sunday School can map statuses to friendly labels, but the core should use neutral stable codes. + +--- + +## 10. Idempotency and Duplicate Scan Rules + +### 10.1 Idempotency key + +Preferred: + +```text +client_request_id or idempotency_key from scanner device/client +``` + +Fallback fingerprint: + +```text +school_id +badge_value_hash +station_id +scan_action +scan_source +rounded_scanned_at_window +``` + +### 10.2 Duplicate scan behavior + +Within duplicate window: + +```text +- do not mutate attendance again +- return original scan result, or explicit duplicate result +- create optional duplicate scan log pointing to original scan +``` + +Outside duplicate window: + +```text +- allow check-in/check-out flow if policy allows +- otherwise return current attendance state without double-counting +``` + +### 10.3 Required tests + +```text +- duplicate scan within window does not create second record +- duplicate scan within window returns stable result +- duplicate scan from another station follows policy +- same badge next session creates new record +- same badge different school cannot collide +``` + +--- + +## 11. Transaction and Locking Strategy + +### 11.1 Scanner check-in + +Required transaction: + +```text +BEGIN + lock attendance session + lock or create attendance record for subject/session/date + write attendance event/scan log + update first_seen_at/last_seen_at/status + write late/event log if needed + write audit log + store idempotency result +COMMIT +``` + +### 11.2 Manual attendance edit + +Required transaction: + +```text +BEGIN + lock attendance record + capture before state + validate actor authorization + apply status update + write override reason + write audit log before/after +COMMIT +``` + +### 11.3 Bulk attendance marking + +Required transaction strategy: + +```text +- process in chunks +- each chunk runs in transaction +- every row scoped to school_id +- failures return row-level error report +- audit each changed row or write batch audit plus row details +``` + +--- + +## 12. Audit Logging Requirements + +Every attendance mutation must create an audit event. + +Required audit fields: + +```text +school_id +academic_year_id +term_id, nullable +actor_user_id, nullable +actor_type +action +subject_type +subject_id +attendance_session_id, nullable +attendance_record_id, nullable +before_json, nullable +after_json, nullable +reason, nullable +source +station_id, nullable +request_id, nullable +ip_address, nullable +user_agent, nullable +idempotency_key, nullable +created_at +``` + +Required actions: + +```text +attendance.scan.processed +attendance.scan.duplicate +attendance.student.recorded +attendance.student.edited +attendance.staff.recorded +attendance.staff.edited +attendance.status.overridden +attendance.session.created +attendance.session.closed +attendance.bulk.updated +``` + +Audit logs are append-only. + +--- + +## 13. Authorization Requirements + +Minimum actions: + +```text +attendance.scan.process +attendance.student.view +attendance.student.record +attendance.student.override +attendance.staff.view +attendance.staff.record +attendance.staff.override +attendance.session.view +attendance.session.manage +attendance.report.view +``` + +Every attendance endpoint must have tests for: + +```text +unauthenticated +wrong school +wrong role +wrong owner/family +authorized admin +authorized staff +authorized teacher +scanner/device token, if applicable +``` + +Authorization must check: + +```text +actor role/capability +school boundary +resource relationship +session state +device/station permission, if scanner devices are used +``` + +--- + +## 14. Islamic Sunday School Extension Model + +Islamic Sunday School attendance extends core through policies. + +### 14.1 Islamic Sunday School allowed customizations + +```text +- Islamic Sunday School program session resolver +- masjid/community calendar mapping +- class/session grouping +- program volunteer attendance +- program session attendance labels +- late window policy +- family attendance summaries +- curriculum attendance reports +``` + +### 14.2 Islamic Sunday School forbidden customizations + +```text +- bypassing attendance transactions +- bypassing audit logs +- creating scan logs without core scanner service +- hardcoding Islamic Sunday School terminology into SchoolCore contracts +- bypassing SchoolContext +- reading request/auth directly in domain services +``` + +### 14.3 Binding example + +```php +$this->app->bind( + AttendanceSessionResolverContract::class, + IslamicSundaySchoolSessionResolver::class +); + +$this->app->bind( + AttendanceStatusPolicyContract::class, + IslamicSundaySchoolAttendanceStatusPolicy::class +); + +$this->app->bind( + AttendancePolicyContract::class, + IslamicSundaySchoolAttendancePolicy::class +); +``` + +--- + +## 15. Migration Strategy + +### 15.1 Do not rewrite all attendance at once + +Use vertical slices. + +Recommended order: + +```text +1. Scanner request/controller delegation +2. Badge lookup service +3. Scan idempotency service +4. Student attendance recording +5. Staff attendance recording +6. Attendance status policy +7. Attendance session resolver +8. Manual override flows +9. Reports/read models +``` + +Why this order: + +```text +- Scanner is the current high-risk blob. +- Badge lookup creates a stable seam. +- Idempotency prevents duplicate reality. +- Student/staff recording are core reusable mutations. +- Status/session policies allow Islamic Sunday School customization. +- Reports should consume stable attendance data last. +``` + +### 15.2 Compatibility layer + +Existing controllers may remain temporarily, but must delegate to new contracts. + +Example: + +```text +ScannerController + -> ProcessScanRequest + -> SchoolContext + -> ProcessScanData + -> ScannerServiceContract::processScan() + -> ScanResource +``` + +Legacy private methods should move into services or be marked deprecated. + +### 15.3 Deprecation rule + +Every legacy attendance/scanner method must be labeled as one of: + +```text +canonical +adapter +deprecated +remove +``` + +No unlabeled duplicate attendance logic. + +--- + +## 16. First Implementation Slice: Scanner Controller Delegation + +### Objective + +Make the scanner controller thin without changing behavior. + +### Tasks + +```text +ATT-001. Create attendance module scaffold. +ATT-002. Create ScannerServiceContract. +ATT-003. Create ProcessScanData. +ATT-004. Create ProcessScanRequest. +ATT-005. Create ScannerService shell using existing logic internally. +ATT-006. Update ScannerController to delegate. +ATT-007. Add response resource/snapshot tests. +``` + +### Required tests + +```text +- valid student scan still works +- valid staff scan still works +- invalid badge still fails correctly +- existing response shape remains compatible +- unauthenticated scan behavior remains as intended +- school context is resolved +``` + +### Done when + +```text +- ScannerController has no private domain methods. +- Existing scanner API remains compatible. +- ScannerService is the only scanner orchestration entry point. +``` + +--- + +## 17. Second Implementation Slice: Badge Lookup Service + +### Objective + +Extract badge lookup into reusable service. + +### Tasks + +```text +ATT-008. Create BadgeLookupServiceContract. +ATT-009. Create BadgeLookupData. +ATT-010. Create BadgeRepository. +ATT-011. Scope badge lookup by school_id. +ATT-012. Return neutral subject result. +ATT-013. Add tests for student/staff/unknown badge. +``` + +### Required tests + +```text +- finds student badge in same school +- finds staff badge in same school +- does not find badge from another school +- handles inactive badge +- handles duplicate badge conflict explicitly +- unknown badge returns controlled error +``` + +### Done when + +```text +- ScannerService no longer performs direct badge queries. +- Badge result does not contain Islamic Sunday School-specific assumptions. +``` + +--- + +## 18. Third Implementation Slice: Scan Idempotency + +### Objective + +Prevent duplicate scanner mutations. + +### Tasks + +```text +ATT-014. Create AttendanceIdempotencyServiceContract. +ATT-015. Create ScanFingerprint. +ATT-016. Add idempotency key/fingerprint table if needed. +ATT-017. Store scan result for repeated calls. +ATT-018. Add duplicate scan log behavior. +``` + +### Required tests + +```text +- repeated scan with same key returns same result +- same key with different payload fails +- repeated scan within duplicate window does not double-count +- repeated scan after duplicate window follows policy +- duplicate detection is school-scoped +``` + +### Done when + +```text +- Duplicate scans cannot create duplicate attendance records. +- Idempotency behavior is deterministic and tested. +``` + +--- + +## 19. Fourth Implementation Slice: Student Attendance Recording + +### Objective + +Move student attendance mutation into modular service. + +### Tasks + +```text +ATT-019. Create StudentAttendanceServiceContract. +ATT-020. Create RecordStudentAttendanceData. +ATT-021. Implement StudentAttendanceService. +ATT-022. Add transaction runner. +ATT-023. Lock attendance record/session during mutation. +ATT-024. Add attendance.student.recorded audit event. +ATT-025. Make ScannerService call StudentAttendanceService. +``` + +### Required tests + +```text +- records present status +- records late status +- rejects invalid status +- rejects cross-school student +- creates audit log +- duplicate scan does not double mutate +- concurrent scan does not duplicate record +``` + +### Done when + +```text +- Student attendance mutation is in one service. +- Scanner and manual endpoints share the same mutation path where practical. +``` + +--- + +## 20. Fifth Implementation Slice: Staff Attendance Recording + +### Objective + +Move staff attendance mutation into modular service. + +### Tasks + +```text +ATT-026. Create StaffAttendanceServiceContract. +ATT-027. Create RecordStaffAttendanceData. +ATT-028. Implement StaffAttendanceService. +ATT-029. Add staff attendance audit events. +ATT-030. Make ScannerService call StaffAttendanceService. +``` + +### Required tests + +```text +- records staff present status +- records staff late status +- rejects cross-school staff +- rejects inactive staff if policy says so +- creates audit log +- duplicate scan does not double mutate +``` + +### Done when + +```text +- Staff attendance uses modular service. +- Staff and student attendance share common audit/idempotency infrastructure. +``` + +--- + +## 21. Sixth Implementation Slice: Attendance Session Resolver + +### Objective + +Create neutral session resolution that Islamic Sunday School can override. + +### Tasks + +```text +ATT-031. Create AttendanceSessionResolverContract. +ATT-032. Implement DefaultAttendanceSessionResolver. +ATT-033. Implement IslamicSundaySchoolSessionResolver. +ATT-034. Bind resolver through service provider. +ATT-035. Add tests for standard school and Islamic Sunday School sessions. +``` + +### Required tests + +```text +- resolves daily session for standard school +- resolves Islamic Sunday School program session for Islamic Sunday School profile +- rejects scan outside valid session if policy requires +- handles timezone correctly +- does not resolve another school's session +``` + +### Done when + +```text +- Core scanner does not hardcode Sunday/session logic. +- Islamic Sunday School session behavior is policy/resolver-driven. +``` + +--- + +## 22. Seventh Implementation Slice: Attendance Status Policy + +### Objective + +Move late/present/absent decision-making behind policy. + +### Tasks + +```text +ATT-036. Create AttendanceStatusPolicyContract. +ATT-037. Implement DefaultAttendanceStatusPolicy. +ATT-038. Implement IslamicSundaySchoolAttendanceStatusPolicy if needed. +ATT-039. Move late window logic out of controller/legacy methods. +ATT-040. Add status transition tests. +``` + +### Required tests + +```text +- scan before start is handled by policy +- scan during on-time window returns present +- scan after late threshold returns late +- excused/manual status requires override path +- Islamic Sunday School late window differs without changing core +``` + +### Done when + +```text +- Late/present logic is not hardcoded in scanner orchestration. +- Different school profiles can define status rules. +``` + +--- + +## 23. Eighth Implementation Slice: Manual Overrides + +### Objective + +Standardize manual attendance edits. + +### Tasks + +```text +ATT-041. Create manual override FormRequests. +ATT-042. Require override reason. +ATT-043. Use StudentAttendanceService/StaffAttendanceService. +ATT-044. Capture before/after audit logs. +ATT-045. Add authorization policy checks. +``` + +### Required tests + +```text +- manual override requires reason +- unauthorized teacher cannot override outside scope +- admin can override +- cross-school override fails +- before/after audit is written +- invalid transition fails +``` + +### Done when + +```text +- Manual edits and scanner writes use consistent rules. +- Override history is auditable. +``` + +--- + +## 24. Attendance Reports + +Reports must consume attendance read models, not invent attendance truth. + +### Tasks + +```text +ATT-046. Create AttendanceReadRepository. +ATT-047. Create attendance summary read model. +ATT-048. Create student attendance history read model. +ATT-049. Create staff attendance summary read model. +ATT-050. Update attendance reports to use read models. +ATT-051. Add school context to all report queries. +``` + +### Required tests + +```text +- report excludes other schools +- report status counts match AttendanceService +- duplicate scans do not inflate report +- Islamic Sunday School report labels do not leak into core +- export output matches approved snapshot +``` + +--- + +## 25. Database and Schema Requirements + +### 25.1 Required indexes + +Add or verify: + +```text +attendance_sessions.school_id indexed +attendance_sessions.academic_year_id indexed +attendance_sessions.term_id indexed, nullable +attendance_sessions.starts_at indexed +attendance_sessions.status indexed + +attendance_records.school_id indexed +attendance_records.attendance_session_id indexed, nullable +attendance_records.attendance_date indexed +attendance_records.subject_type indexed +attendance_records.subject_id indexed +attendance_records.status indexed + +attendance_scan_logs.school_id indexed +attendance_scan_logs.subject_type/subject_id indexed +attendance_scan_logs.badge_id indexed, nullable +attendance_scan_logs.station_id indexed, nullable +attendance_scan_logs.scanned_at indexed +attendance_scan_logs.duplicate_of_id indexed, nullable + +attendance_audit_logs.school_id indexed +attendance_audit_logs.actor_user_id indexed, nullable +attendance_audit_logs.subject_type/subject_id indexed +attendance_audit_logs.created_at indexed +``` + +### 25.2 Recommended unique constraints + +```text +unique(school_id, attendance_session_id, subject_type, subject_id) +unique(school_id, attendance_date, subject_type, subject_id) for daily-only records, if used +unique(school_id, idempotency_key, operation) where idempotency_key is not null +``` + +### 25.3 Migration caution + +Do not aggressively delete or rename existing attendance tables in Phase 4. Add adapters/read repositories first. Attendance history is institutional memory, and apparently institutions become cranky when memory gets truncated by “cleanup.” + +--- + +## 26. FormRequest Requirements + +Create or update: + +```text +ProcessScanRequest +RecordStudentAttendanceRequest +RecordStaffAttendanceRequest +ManualStudentAttendanceOverrideRequest +ManualStaffAttendanceOverrideRequest +AttendanceSummaryRequest +AttendanceSessionRequest +``` + +Each FormRequest must define: + +```text +authorize() +rules() +messages(), if needed +toDTO(), optional but recommended +``` + +Controllers should use: + +```php +$data = $request->toDTO(); +``` + +Not raw request array merging. + +--- + +## 27. Service Provider Bindings + +Create: + +```php +namespace App\Providers; + +use Illuminate\Support\ServiceProvider; +use App\Domain\SchoolCore\Attendance\Contracts\ScannerServiceContract; +use App\Domain\SchoolCore\Attendance\Services\ScannerService; + +final class SchoolCoreAttendanceServiceProvider extends ServiceProvider +{ + public function register(): void + { + $this->app->bind(ScannerServiceContract::class, ScannerService::class); + $this->app->bind(BadgeLookupServiceContract::class, BadgeLookupService::class); + $this->app->bind(StudentAttendanceServiceContract::class, StudentAttendanceService::class); + $this->app->bind(StaffAttendanceServiceContract::class, StaffAttendanceService::class); + $this->app->bind(AttendanceSessionResolverContract::class, DefaultAttendanceSessionResolver::class); + $this->app->bind(AttendanceStatusPolicyContract::class, DefaultAttendanceStatusPolicy::class); + $this->app->bind(AttendancePolicyContract::class, DefaultAttendancePolicy::class); + $this->app->bind(AttendanceAuditLoggerContract::class, AttendanceAuditLogger::class); + $this->app->bind(AttendanceIdempotencyServiceContract::class, AttendanceIdempotencyService::class); + } +} +``` + +Islamic Sunday School may override policy/resolver bindings in its own provider. + +--- + +## 28. Architecture Tests + +Add tests that fail if attendance boundaries are violated. + +Required assertions: + +```text +SchoolCore\Attendance must not depend on Domain\IslamicIslamicSundaySchool +SchoolCore\Attendance must not depend on Illuminate\Http\Request +SchoolCore\Attendance services must not call auth() +SchoolCore\Attendance services must not call request() +Controllers must not instantiate attendance concrete services directly +ScannerController must not contain domain private methods +Attendance mutations must use transaction runner or DB transaction +Scanner writes must go through ScannerServiceContract +``` + +Optional static checks: + +```text +No Sunday/masjid/community/ministry vocabulary inside SchoolCore\Attendance contracts +No direct attendance DB writes inside controllers +No duplicate scan mutation without idempotency check +No manual override without reason +``` + +--- + +## 29. Rollout Plan + +### Step 1: Add attendance module scaffolding + +```text +- create namespaces +- create contracts +- create DTOs +- create provider +- bind default implementations +``` + +### Step 2: Add regression tests around current scanner behavior + +```text +- student scan +- staff scan +- invalid badge +- duplicate scan +- late scan +- cross-school badge +``` + +Some tests may fail because current behavior is unsafe or ambiguous. Good. Ambiguity is a bug wearing a cardigan. + +### Step 3: Delegate ScannerController + +```text +- move orchestration behind ScannerServiceContract +- preserve response shape initially +``` + +### Step 4: Extract badge lookup + +```text +- centralize badge resolution +- school-scope lookup +``` + +### Step 5: Add idempotency + +```text +- prevent duplicate scan mutation +- add scan fingerprinting +``` + +### Step 6: Extract student/staff attendance services + +```text +- shared mutation path +- transactions +- audit logging +``` + +### Step 7: Add session/status policies + +```text +- enable Islamic Sunday School extension +- remove hardcoded late/session logic +``` + +### Step 8: Manual override standardization + +```text +- require reasons +- before/after audit +- policy authorization +``` + +### Step 9: Reports/read models + +```text +- consume stable attendance truth +- school context enforced +``` + +--- + +## 30. Implementation Tickets + +## ATT-001: Create attendance module scaffold + +### Description + +Create the `SchoolCore\Attendance` namespace and initial contracts, DTO folders, service folders, enum folders, exception folders, and provider. + +### Acceptance Criteria + +```text +- Namespace exists. +- Service provider is registered. +- Empty contracts compile. +- No controller behavior changes yet. +``` + +--- + +## ATT-002: Delegate ScannerController to ScannerService + +### Description + +Move scanner orchestration behind `ScannerServiceContract` while preserving current response behavior. + +### Acceptance Criteria + +```text +- ScannerController validates and delegates only. +- Existing scanner responses remain compatible. +- ScannerService is the only orchestration entry point. +- Tests cover student/staff/invalid badge scans. +``` + +--- + +## ATT-003: Extract BadgeLookupService + +### Description + +Centralize badge lookup and scope it by school. + +### Acceptance Criteria + +```text +- Student badge lookup works. +- Staff badge lookup works. +- Unknown badge returns controlled error. +- Cross-school badge lookup fails. +- Inactive badges are handled explicitly. +``` + +--- + +## ATT-004: Add scanner idempotency + +### Description + +Prevent duplicate scans from creating duplicate attendance mutations. + +### Acceptance Criteria + +```text +- Same idempotency key returns same result. +- Same key with different payload fails. +- Duplicate scan within window does not double-count. +- Duplicate scan is logged. +- Idempotency is school-scoped. +``` + +--- + +## ATT-005: Extract StudentAttendanceService + +### Description + +Move student attendance mutation into modular service. + +### Acceptance Criteria + +```text +- Uses SchoolContext. +- Uses transaction. +- Locks relevant record/session. +- Creates audit log. +- Rejects cross-school student. +- Handles duplicate scan safely. +``` + +--- + +## ATT-006: Extract StaffAttendanceService + +### Description + +Move staff attendance mutation into modular service. + +### Acceptance Criteria + +```text +- Uses SchoolContext. +- Uses transaction. +- Creates audit log. +- Rejects cross-school staff. +- Shares idempotency/audit infrastructure. +``` + +--- + +## ATT-007: Add AttendanceSessionResolver + +### Description + +Create neutral session resolution with Islamic Sunday School override support. + +### Acceptance Criteria + +```text +- Default resolver works for generic school. +- Islamic Sunday School resolver works through binding. +- Timezone is respected. +- Cross-school sessions fail. +- Core resolver contains no Islamic Sunday School vocabulary. +``` + +--- + +## ATT-008: Add AttendanceStatusPolicy + +### Description + +Move status and late logic behind policy. + +### Acceptance Criteria + +```text +- Present/late logic is policy-driven. +- Islamic Sunday School can use different late window. +- Manual-only statuses cannot be set through scan. +- Tests cover status boundaries. +``` + +--- + +## ATT-009: Standardize manual overrides + +### Description + +Move manual edit flows through attendance services with audit trail. + +### Acceptance Criteria + +```text +- Override requires reason. +- Authorization is enforced. +- Before/after audit is recorded. +- Cross-school override fails. +``` + +--- + +## ATT-010: Add attendance architecture tests + +### Description + +Enforce attendance module boundaries in CI. + +### Acceptance Criteria + +```text +- SchoolCore\Attendance cannot import IslamicSundaySchool. +- Attendance services cannot call auth() or request(). +- Controllers cannot write attendance rows directly. +- ScannerController has no domain private methods. +- CI fails on boundary violation. +``` + +--- + +## 31. Definition of Done for Phase 4 + +Phase 4 is done when: + +```text +- Attendance core contracts exist. +- Scanner controller delegates to ScannerService. +- Badge lookup is modular and school-scoped. +- Scanner idempotency prevents duplicate mutations. +- Student attendance mutation is modular. +- Staff attendance mutation is modular. +- Attendance sessions are resolved through a contract. +- Attendance status is policy-driven. +- Manual overrides require reason and audit trail. +- Attendance services require SchoolContext. +- Islamic Sunday School attendance behavior is implemented through policies/resolvers. +- Cross-school attendance access is blocked by tests. +- Architecture tests prevent SchoolCore from depending on IslamicSundaySchool. +- Legacy attendance/scanner methods are labeled canonical, adapter, deprecated, or remove. +``` + +--- + +## 32. Non-Goals + +Do not include in Phase 4: + +```text +- full attendance UI redesign +- total attendance schema rewrite +- biometric device integration +- facial recognition +- external hardware vendor SDK integration +- parent-facing attendance portal redesign +- full reporting redesign +``` + +Those may happen later. Phase 4 is about making attendance reliable, reusable, scoped, and extensible. + +--- + +## 33. Main Risks + +### Risk: duplicate attendance records + +Mitigation: + +```text +- idempotency key +- scan fingerprint +- unique constraints +- row locks +- duplicate tests +``` + +### Risk: cross-school leakage + +Mitigation: + +```text +- mandatory SchoolContext +- school-scoped repositories +- authorization tests +``` + +### Risk: scanner behavior break + +Mitigation: + +```text +- snapshot current responses +- delegate first, refactor internals second +- preserve compatibility adapter +``` + +### Risk: Islamic Sunday School leakage into core + +Mitigation: + +```text +- contract vocabulary review +- architecture tests +- Islamic Sunday School resolver/policies only +``` + +### Risk: status logic drift + +Mitigation: + +```text +- single AttendanceStatusPolicyContract +- tests for status boundary times +- report counts based on canonical statuses +``` + +--- + +## 34. Immediate Next Work Queue + +Start here: + +```text +1. ATT-001 Create attendance module scaffold +2. ATT-002 Delegate ScannerController to ScannerService +3. ATT-003 Extract BadgeLookupService +4. ATT-004 Add scanner idempotency +5. ATT-005 Extract StudentAttendanceService +6. ATT-006 Extract StaffAttendanceService +7. ATT-007 Add AttendanceSessionResolver +8. ATT-008 Add AttendanceStatusPolicy +9. ATT-010 Add attendance architecture tests +``` + +Do not start with reports. Reports are mirrors. Fix the face first. diff --git a/docs/modular_plans/phase_5_modular_student_lifecycle_execution_plan_islamic.md b/docs/modular_plans/phase_5_modular_student_lifecycle_execution_plan_islamic.md new file mode 100644 index 00000000..315a21a5 --- /dev/null +++ b/docs/modular_plans/phase_5_modular_student_lifecycle_execution_plan_islamic.md @@ -0,0 +1,2017 @@ +# Phase 5 Execution Plan: Modular Student Lifecycle Extraction + + +# Islamic Sunday School Alignment Update + +Phase 5 student lifecycle now treats Sunday School as **Islamic Sunday School**. + +`SchoolCore\Students` owns neutral student identity, guardians, households, enrollment, assignment, status transitions, promotion, and audit. + +`IslamicSundaySchool\Students` may extend student profiles with: + +```text +masjid family/community profile +Qur'an level +Arabic level +Islamic studies track +halaqa/group +memorization progress summary +recitation placement +volunteer/family participation flags +sensitive imam/admin notes, only with strict authorization +``` + +Do not store these fields in the core student model. Core should remain reusable for any school system. + +--- +## 1. Purpose + +Phase 5 extracts student lifecycle behavior into a reusable `SchoolCore` module that can support any school system while allowing Islamic Sunday School-specific student profile behavior through extension tables, policies, and domain services. + +The student lifecycle module must own the invariant parts of student management: + +- student identity +- student profile +- guardian relationships +- family/household relationships +- enrollment +- class/group assignment +- student identifiers +- status transitions +- promotion +- transfer +- withdrawal/archive +- student search/read models +- audit logging +- cross-school isolation +- authorization boundaries + +Islamic Sunday School may customize: + +- masjid family/community profile fields +- Qur'an, Arabic, Islamic studies, halaqa, and masjid-family fields +- Islamic Sunday School class grouping +- Qur'an/Islamic studies curriculum level +- family/masjid/community relationship labels +- Islamic Sunday School promotion rules +- imam/admin notes, if allowed by policy +- ministry participation flags + +Islamic Sunday School must not define the global student model. It extends it. That distinction matters unless the plan is to make every school system pretend it is a Islamic Sunday School wearing a fake mustache. + +--- + +## 2. Required Dependency Direction + +Student lifecycle must follow the modular platform boundary: + +```text +App\Domain\IslamicIslamicSundaySchool\Students + ↓ depends on +App\Domain\SchoolCore\Students + ↓ depends on +App\Domain\SchoolCore\Shared +``` + +Forbidden dependencies: + +```text +SchoolCore\Students -> IslamicSundaySchool\* +SchoolCore\Students -> Http\Request +SchoolCore\Students -> auth() +SchoolCore\Students -> controller classes +SchoolCore\Students -> Islamic Sunday School vocabulary +SchoolCore\Students -> direct unscoped school queries +``` + +Allowed dependencies: + +```text +IslamicSundaySchool\Students -> SchoolCore\Students contracts +Http\Controllers -> student lifecycle contracts +Jobs -> student lifecycle contracts +Student services -> SchoolContext +Student services -> shared audit/transaction contracts +Student services -> enrollment/promotion policy contracts +``` + +--- + +## 3. Target Namespace Structure + +Create the student lifecycle module under `SchoolCore`: + +```text +app/ + Domain/ + SchoolCore/ + Students/ + Contracts/ + StudentServiceContract.php + StudentProfileServiceContract.php + StudentIdentifierGeneratorContract.php + GuardianServiceContract.php + HouseholdServiceContract.php + EnrollmentServiceContract.php + AssignmentServiceContract.php + PromotionServiceContract.php + PromotionPolicyContract.php + StudentLifecycleAuditLoggerContract.php + StudentReadServiceContract.php + DTO/ + CreateStudentData.php + UpdateStudentData.php + StudentProfileData.php + CreateGuardianData.php + UpdateGuardianData.php + LinkGuardianData.php + HouseholdData.php + EnrollmentData.php + AssignmentData.php + PromotionData.php + StudentStatusTransitionData.php + StudentSearchQuery.php + Enums/ + StudentStatus.php + GuardianRelationshipType.php + EnrollmentStatus.php + AssignmentStatus.php + PromotionResult.php + StudentAuditAction.php + Exceptions/ + StudentException.php + StudentNotFoundException.php + StudentIdentifierCollisionException.php + InvalidStudentStatusTransitionException.php + UnauthorizedStudentAccessException.php + EnrollmentException.php + GuardianException.php + PromotionException.php + Services/ + StudentService.php + StudentProfileService.php + StudentIdentifierService.php + GuardianService.php + HouseholdService.php + EnrollmentService.php + AssignmentService.php + PromotionService.php + StudentLifecycleAuditLogger.php + StudentReadService.php + Policies/ + DefaultPromotionPolicy.php + DefaultStudentIdentifierGenerator.php + DefaultEnrollmentPolicy.php + Repositories/ + StudentRepository.php + GuardianRepository.php + HouseholdRepository.php + EnrollmentRepository.php + AssignmentRepository.php + StudentReadRepository.php + Support/ + StudentTransactionRunner.php + StudentIdentifier.php + StudentLifecycleStateMachine.php + StudentSearchNormalizer.php +``` + +Islamic Sunday School-specific student lifecycle behavior goes here: + +```text +app/ + Domain/ + IslamicSundaySchool/ + Students/ + DTO/ + IslamicSundaySchoolStudentProfileData.php + Policies/ + IslamicSundaySchoolPromotionPolicy.php + IslamicSundaySchoolEnrollmentPolicy.php + IslamicSundaySchoolStudentIdentifierGenerator.php + Services/ + IslamicSundaySchoolStudentProfileService.php + IslamicSundaySchoolFamilyProfileService.php + Reports/ + IslamicSundaySchoolStudentReportBuilder.php + Providers/ + IslamicSundaySchoolStudentServiceProvider.php +``` + +--- + +## 4. Core Design Rules + +### 4.1 Student services must require `SchoolContext` + +Every public student lifecycle service method must receive `SchoolContext`. + +Example: + +```php +public function createStudent( + SchoolContext $context, + CreateStudentData $data +): StudentResult; +``` + +A student operation without `SchoolContext` is invalid. A student operation with loose school scoping is how platforms become accidental data-sharing programs. + +### 4.2 Core student model must stay school-neutral + +Core may contain: + +```text +name +date_of_birth +gender, optional +contact fields, if applicable +status +school_id +external_id, optional +student_identifier +profile photo reference +metadata, constrained +``` + +Core must not contain Islamic Sunday School-only fields like: + +```text +quran_level_id +arabic_level_id +masjid/community_membership_status +ministry_group +islamic_studies_track_id +religious/community_notes +favorite_surah +``` + +Those belong in `IslamicSundaySchool` extension profile storage. + +### 4.3 Student identifiers must be race-safe + +Current-style identifier generation using `max(id) + 1` is forbidden. + +Required strategy: + +```text +1. Create student row inside transaction. +2. Generate identifier from actual persisted ID or sequence. +3. Save identifier. +4. Enforce unique constraint. +5. Retry on collision if policy permits. +``` + +A generated identifier without a database unique constraint is just a polite suggestion. + +### 4.4 Controllers must not orchestrate lifecycle rules + +Controllers may: + +```text +- validate request +- authorize route +- build DTO +- call service contract +- return resource +``` + +Controllers may not: + +```text +- generate student IDs +- create guardians directly during student creation +- assign classes directly +- decide promotion eligibility +- mutate status transitions directly +- write Islamic Sunday School profile fields into core student table +``` + +### 4.5 Lifecycle transitions must be explicit + +Student status changes must pass through a state machine or transition service. + +Example statuses: + +```text +prospective +active +inactive +withdrawn +graduated +archived +transferred +``` + +Forbidden: + +```php +$student->status = $request->status; +$student->save(); +``` + +That is not lifecycle management. That is letting HTTP edit reality with a crayon. + +--- + +## 5. Core Contracts + +### 5.1 `StudentServiceContract` + +```php +namespace App\Domain\SchoolCore\Students\Contracts; + +use App\Domain\SchoolCore\Shared\Context\SchoolContext; +use App\Domain\SchoolCore\Students\DTO\CreateStudentData; +use App\Domain\SchoolCore\Students\DTO\UpdateStudentData; +use App\Domain\SchoolCore\Students\DTO\StudentStatusTransitionData; + +interface StudentServiceContract +{ + public function createStudent(SchoolContext $context, CreateStudentData $data): StudentResult; + + public function updateStudent(SchoolContext $context, int $studentId, UpdateStudentData $data): StudentResult; + + public function transitionStatus( + SchoolContext $context, + int $studentId, + StudentStatusTransitionData $data + ): StudentResult; + + public function getStudent(SchoolContext $context, int $studentId): StudentView; +} +``` + +### 5.2 `StudentIdentifierGeneratorContract` + +```php +namespace App\Domain\SchoolCore\Students\Contracts; + +use App\Domain\SchoolCore\Shared\Context\SchoolContext; + +interface StudentIdentifierGeneratorContract +{ + public function generateForPersistedStudent( + SchoolContext $context, + StudentView $student + ): StudentIdentifier; +} +``` + +### 5.3 `GuardianServiceContract` + +```php +namespace App\Domain\SchoolCore\Students\Contracts; + +use App\Domain\SchoolCore\Shared\Context\SchoolContext; +use App\Domain\SchoolCore\Students\DTO\CreateGuardianData; +use App\Domain\SchoolCore\Students\DTO\LinkGuardianData; + +interface GuardianServiceContract +{ + public function createGuardian(SchoolContext $context, CreateGuardianData $data): GuardianResult; + + public function linkGuardianToStudent( + SchoolContext $context, + LinkGuardianData $data + ): GuardianLinkResult; + + public function unlinkGuardianFromStudent( + SchoolContext $context, + int $studentId, + int $guardianId, + string $reason + ): GuardianLinkResult; +} +``` + +### 5.4 `HouseholdServiceContract` + +```php +namespace App\Domain\SchoolCore\Students\Contracts; + +use App\Domain\SchoolCore\Shared\Context\SchoolContext; +use App\Domain\SchoolCore\Students\DTO\HouseholdData; + +interface HouseholdServiceContract +{ + public function createHousehold(SchoolContext $context, HouseholdData $data): HouseholdResult; + + public function addStudentToHousehold( + SchoolContext $context, + int $householdId, + int $studentId + ): HouseholdResult; + + public function addGuardianToHousehold( + SchoolContext $context, + int $householdId, + int $guardianId + ): HouseholdResult; +} +``` + +### 5.5 `EnrollmentServiceContract` + +```php +namespace App\Domain\SchoolCore\Students\Contracts; + +use App\Domain\SchoolCore\Shared\Context\SchoolContext; +use App\Domain\SchoolCore\Students\DTO\EnrollmentData; + +interface EnrollmentServiceContract +{ + public function enrollStudent(SchoolContext $context, EnrollmentData $data): EnrollmentResult; + + public function withdrawStudent(SchoolContext $context, int $studentId, string $reason): EnrollmentResult; + + public function transferStudent(SchoolContext $context, int $studentId, EnrollmentData $data): EnrollmentResult; +} +``` + +### 5.6 `AssignmentServiceContract` + +```php +namespace App\Domain\SchoolCore\Students\Contracts; + +use App\Domain\SchoolCore\Shared\Context\SchoolContext; +use App\Domain\SchoolCore\Students\DTO\AssignmentData; + +interface AssignmentServiceContract +{ + public function assignStudent(SchoolContext $context, AssignmentData $data): AssignmentResult; + + public function unassignStudent(SchoolContext $context, int $assignmentId, string $reason): AssignmentResult; +} +``` + +### 5.7 `PromotionServiceContract` + +```php +namespace App\Domain\SchoolCore\Students\Contracts; + +use App\Domain\SchoolCore\Shared\Context\SchoolContext; +use App\Domain\SchoolCore\Students\DTO\PromotionData; + +interface PromotionServiceContract +{ + public function evaluatePromotion(SchoolContext $context, int $studentId): PromotionEvaluationResult; + + public function promoteStudent(SchoolContext $context, PromotionData $data): PromotionResult; + + public function bulkPromote(SchoolContext $context, BulkPromotionData $data): BulkPromotionResult; +} +``` + +### 5.8 `PromotionPolicyContract` + +```php +namespace App\Domain\SchoolCore\Students\Contracts; + +use App\Domain\SchoolCore\Shared\Context\SchoolContext; + +interface PromotionPolicyContract +{ + public function evaluate( + SchoolContext $context, + StudentView $student, + EnrollmentView $enrollment + ): PromotionEvaluationResult; +} +``` + +Islamic Sunday School may bind this to a Islamic Sunday School-specific policy. + +--- + +## 6. DTO Requirements + +Do not pass raw request arrays into student services. + +### 6.1 `CreateStudentData` + +Fields: + +```text +first_name +middle_name, nullable +last_name +preferred_name, nullable +date_of_birth, nullable +gender, nullable +primary_language, nullable +photo_file, nullable +external_id, nullable +initial_status +guardians, nullable array +household_id, nullable +enrollment, nullable +profile_metadata, nullable +extension_profile, nullable +idempotency_key, nullable +``` + +### 6.2 `UpdateStudentData` + +Fields: + +```text +first_name, nullable +middle_name, nullable +last_name, nullable +preferred_name, nullable +date_of_birth, nullable +gender, nullable +primary_language, nullable +photo_file, nullable +profile_metadata, nullable +update_reason +``` + +`update_reason` should be required for sensitive profile changes if policy requires it. + +### 6.3 `CreateGuardianData` + +Fields: + +```text +first_name +last_name +email, nullable +phone, nullable +relationship_type, nullable +address, nullable +emergency_contact, bool +pickup_authorized, bool +metadata, nullable +``` + +### 6.4 `LinkGuardianData` + +Fields: + +```text +student_id +guardian_id +relationship_type +emergency_contact +pickup_authorized +financial_responsibility, nullable bool +academic_contact, nullable bool +notes, nullable +``` + +### 6.5 `EnrollmentData` + +Fields: + +```text +student_id +academic_year_id +term_id, nullable +grade_level_id, nullable +program_id, nullable +class_group_id, nullable +enrollment_date +status +reason, nullable +metadata, nullable +``` + +### 6.6 `PromotionData` + +Fields: + +```text +student_id +from_academic_year_id +to_academic_year_id +from_level_id, nullable +to_level_id, nullable +from_group_id, nullable +to_group_id, nullable +effective_date +reason +override_policy, bool +override_reason, nullable +``` + +--- + +## 7. Student Identity and Identifier Strategy + +### 7.1 Identifier requirements + +Student identifiers must be: + +```text +school-scoped +unique per school +generated through contract +race-safe +auditable +replaceable by policy +not dependent on max(id) + 1 +``` + +### 7.2 Recommended default algorithm + +```text +1. Insert student without public identifier. +2. Use persisted student ID and school prefix to generate identifier. +3. Save identifier. +4. Enforce unique(school_id, student_identifier). +5. Retry on collision if necessary. +``` + +Example format: + +```text +SCH-0000123 +``` + +Islamic Sunday School may override with: + +```text +SS-2026-000123 +``` + +But it must use the same contract and uniqueness rules. + +### 7.3 Tests + +```text +- creates unique identifier +- two concurrent student creations do not collide +- identifier is unique per school +- same identifier can exist in different schools only if policy permits and DB constraint supports it +- collision retry works +``` + +--- + +## 8. Student Profile Extension Strategy + +### 8.1 Core profile + +Core profile is school-neutral. + +Suggested tables or model concepts: + +```text +students +student_profiles +guardians +households +student_guardian +enrollments +student_assignments +student_audit_logs +``` + +### 8.2 Islamic Sunday School profile + +Islamic Sunday School-specific profile belongs outside core. + +Possible table: + +```text +islamic_sunday_school_student_profiles +``` + +Suggested fields: + +```text +school_id +student_id +masjid/community_family_id, nullable +quran_level_id, nullable +arabic_level_id, nullable +ministry_group_id, nullable +bible_curriculum_level_id, nullable +religious/community_notes, nullable with strict authorization +metadata, nullable +created_at +updated_at +``` + +Rules: + +```text +- every extension profile row must include school_id +- extension profile must reference a core student +- extension profile access must be policy-controlled +- sensitive fields require explicit authorization +``` + +### 8.3 Profile composition + +Read models may compose core + extension: + +```text +Core StudentView + + IslamicSundaySchoolStudentProfileView + = IslamicSundaySchoolStudentView +``` + +Core must not know about `IslamicSundaySchoolStudentView`. + +--- + +## 9. Guardian and Household Model + +### 9.1 Guardian rules + +Guardians are reusable people records connected to students. + +A guardian may be linked to: + +```text +one student +multiple siblings +multiple households, if policy allows +financial responsibility +academic contact +emergency contact +pickup authorization +``` + +### 9.2 Required relationship metadata + +```text +relationship_type +is_primary +emergency_contact +pickup_authorized +financial_responsibility +academic_contact +effective_from +effective_to, nullable +notes, nullable +``` + +### 9.3 Authorization concerns + +Guardian/student access must be scoped by: + +```text +school_id +relationship +role +custody/contact rules, if applicable +sensitive data policy +``` + +Do not assume every guardian can see every student field. Family data gets legally spicy very quickly, because apparently humans turned family structure into a distributed permissions problem. + +--- + +## 10. Enrollment and Assignment Model + +### 10.1 Enrollment + +Enrollment answers: + +```text +Is this student part of this school/program/year/term? +``` + +Core fields: + +```text +school_id +student_id +academic_year_id +term_id, nullable +program_id, nullable +grade_level_id, nullable +status +enrolled_at +withdrawn_at, nullable +reason, nullable +metadata +``` + +### 10.2 Assignment + +Assignment answers: + +```text +Where is this student placed? +``` + +Examples: + +```text +class +group +section +homeroom +cohort +session group +volunteer/program group extension +``` + +Core should use neutral terms: + +```text +assignment_type +assignable_type +assignable_id +effective_from +effective_to +status +``` + +Islamic Sunday School can map class/ministry labels through extension configuration. + +### 10.3 Assignment rules + +```text +- no active duplicate assignment for same student/type/period unless policy allows +- assignment must belong to same school +- assignment must fit enrollment period +- assignment changes require audit log +``` + +--- + +## 11. Promotion and Progression + +### 11.1 Core promotion concept + +Promotion is a transition from one enrollment/level/group context to another. + +Core must support: + +```text +grade promotion +level promotion +group movement +program completion +graduation +retention +manual override +``` + +### 11.2 Policy-driven promotion + +Promotion decision belongs to `PromotionPolicyContract`. + +Default policy may use: + +```text +academic year completion +current level +attendance threshold, optional +academic completion status, optional +manual review flag +``` + +Islamic Sunday School policy may use: + +```text +Qur'an/Islamic studies curriculum completion +age group +Islamic Sunday School attendance +teacher recommendation +program readiness +``` + +But the core only sees policy result, not Islamic Sunday School-specific rules. + +### 11.3 Promotion audit + +Every promotion must log: + +```text +from state +to state +actor +policy result +override flag +override reason +effective date +``` + +--- + +## 12. Transaction Strategy + +### 12.1 Student creation + +Required transaction: + +```text +BEGIN + create student + generate student identifier + save identifier + create profile + create or link guardians, if included + create or link household, if included + create enrollment, if included + create extension profile through extension hook, if provided + write audit log +COMMIT +``` + +### 12.2 Student update + +Required transaction: + +```text +BEGIN + lock student + capture before state + validate transition/update policy + update core profile + update extension profile through extension service, if applicable + write before/after audit log +COMMIT +``` + +### 12.3 Enrollment change + +Required transaction: + +```text +BEGIN + lock student + lock active enrollment rows + close or update previous enrollment + create new enrollment + update assignments if needed + write audit log +COMMIT +``` + +### 12.4 Promotion + +Required transaction: + +```text +BEGIN + lock student + lock current enrollment + evaluate or confirm promotion result + close old enrollment/assignment + create new enrollment/assignment + write audit log +COMMIT +``` + +--- + +## 13. Audit Logging Requirements + +Every student lifecycle mutation must create an audit event. + +Required audit fields: + +```text +school_id +academic_year_id, nullable +term_id, nullable +actor_user_id +actor_role_snapshot +action +entity_type +entity_id +before_json, nullable +after_json, nullable +reason, nullable +request_id +ip_address, nullable +user_agent, nullable +idempotency_key, nullable +created_at +``` + +Required actions: + +```text +student.created +student.updated +student.status_changed +student.archived +student.transferred +guardian.created +guardian.updated +guardian.linked +guardian.unlinked +household.created +household.updated +student.enrolled +student.withdrawn +student.assigned +student.unassigned +student.promoted +student.retained +student.graduated +student.extension_profile.updated +``` + +Audit logs are append-only. + +--- + +## 14. Authorization Requirements + +Minimum actions: + +```text +student.view +student.create +student.update +student.archive +student.transfer +student.sensitive.view +guardian.view +guardian.create +guardian.update +guardian.link +guardian.unlink +household.view +household.manage +enrollment.view +enrollment.manage +assignment.view +assignment.manage +promotion.evaluate +promotion.execute +student.report.view +``` + +Every student endpoint must have tests for: + +```text +unauthenticated +wrong school +wrong role +wrong owner/family +authorized admin +authorized staff +authorized teacher within assignment scope +authorized guardian for own student, if allowed +``` + +Sensitive Islamic Sunday School extension fields need separate permissions. + +Example: + +```text +student.islamic_sunday_school_profile.view +student.religious/community_notes.view +student.religious/community_notes.update +``` + +--- + +## 15. Idempotency Requirements + +Student creation should support idempotency because duplicate form submissions create duplicate children, which is less charming in databases than in sitcoms. + +Minimum implementation: + +```text +idempotency_key +actor_user_id +school_id +operation +payload_hash +result_entity_type +result_entity_id +created_at +expires_at +``` + +Apply first to: + +```text +student creation +guardian creation +enrollment creation +bulk promotion +``` + +Behavior: + +```text +- same key + same operation + same payload returns original result +- same key + same operation + different payload fails +- missing key allowed for legacy clients during transition +``` + +--- + +## 16. Migration Strategy + +### 16.1 Use vertical slices + +Recommended order: + +```text +1. Student identifier generation +2. Student creation orchestration +3. Guardian linking +4. Enrollment creation/update +5. Assignment service +6. Student status transitions +7. Promotion service +8. Islamic Sunday School profile extension +9. Student read models and reports +``` + +Why this order: + +```text +- identifier generation has a known race bug +- creation is the root lifecycle flow +- guardians/enrollment are part of creation but can be extracted behind services +- promotion builds on enrollment/assignment +- reports should consume stable read models last +``` + +### 16.2 Compatibility layer + +Existing controllers may remain temporarily, but must delegate to new contracts. + +Example: + +```text +StudentController + -> CreateStudentRequest + -> SchoolContext + -> CreateStudentData + -> StudentServiceContract::createStudent() + -> StudentResource +``` + +Legacy service methods should become wrappers or be deprecated. + +### 16.3 Deprecation rule + +Every legacy student lifecycle method must be labeled as one of: + +```text +canonical +adapter +deprecated +remove +``` + +No unlabeled duplicate student creation, guardian linking, assignment, enrollment, or promotion logic. + +--- + +## 17. First Implementation Slice: Race-Safe Student Identifier + +### Objective + +Replace unsafe student identifier generation. + +### Tasks + +```text +STU-001. Create Students module scaffold. +STU-002. Create StudentIdentifierGeneratorContract. +STU-003. Implement DefaultStudentIdentifierGenerator. +STU-004. Add unique constraint on school_id + student_identifier. +STU-005. Update student creation flow to generate identifier after insert. +STU-006. Add collision retry behavior. +STU-007. Add concurrency tests. +``` + +### Required tests + +```text +- creates student identifier after insert +- two concurrent student creations do not receive same identifier +- unique constraint prevents duplicate identifier +- identifier is scoped by school +- collision retry works +``` + +### Done when + +```text +- no active creation path uses max(id) + 1 +- identifier generation is contract-based +- concurrent creation test passes +``` + +--- + +## 18. Second Implementation Slice: Student Creation Service + +### Objective + +Move student creation orchestration into modular core service. + +### Tasks + +```text +STU-008. Create StudentServiceContract. +STU-009. Create CreateStudentData. +STU-010. Implement StudentService::createStudent(). +STU-011. Add StudentTransactionRunner. +STU-012. Add student.created audit event. +STU-013. Update StudentController to delegate. +STU-014. Preserve response compatibility. +``` + +### Required tests + +```text +- creates basic student +- creates student with guardian link +- creates student with enrollment +- rejects cross-school enrollment context +- writes audit log +- returns compatible API response +- supports idempotency key +``` + +### Done when + +```text +- controller no longer orchestrates student creation +- student creation is transactional +- identifier generation happens through contract +``` + +--- + +## 19. Third Implementation Slice: Guardian and Household Services + +### Objective + +Extract guardian and household behavior into modular services. + +### Tasks + +```text +STU-015. Create GuardianServiceContract. +STU-016. Create HouseholdServiceContract. +STU-017. Create CreateGuardianData and LinkGuardianData. +STU-018. Implement guardian create/link/unlink. +STU-019. Implement household create/link. +STU-020. Add guardian/household audit events. +STU-021. Update student creation to use GuardianService. +``` + +### Required tests + +```text +- creates guardian +- links guardian to student +- prevents cross-school guardian link +- prevents duplicate active guardian relationship if policy disallows +- unlink requires reason +- guardian link creates audit log +``` + +### Done when + +```text +- guardian/student links are service-owned +- household behavior is not embedded in StudentController +``` + +--- + +## 20. Fourth Implementation Slice: Enrollment Service + +### Objective + +Move enrollment and withdrawal behavior into modular service. + +### Tasks + +```text +STU-022. Create EnrollmentServiceContract. +STU-023. Create EnrollmentData. +STU-024. Implement enrollStudent(). +STU-025. Implement withdrawStudent(). +STU-026. Add enrollment audit events. +STU-027. Add school/year/term validation. +``` + +### Required tests + +```text +- enrolls student into academic year +- enrolls student into term if provided +- rejects academic year from another school +- prevents duplicate active enrollment if policy disallows +- withdrawal requires reason +- writes audit log +``` + +### Done when + +```text +- enrollment rules are centralized +- student creation delegates enrollment to EnrollmentService +``` + +--- + +## 21. Fifth Implementation Slice: Assignment Service + +### Objective + +Move class/group assignment into modular service. + +### Tasks + +```text +STU-028. Create AssignmentServiceContract. +STU-029. Create AssignmentData. +STU-030. Implement assignStudent(). +STU-031. Implement unassignStudent(). +STU-032. Add assignment audit events. +STU-033. Add assignment conflict checks. +``` + +### Required tests + +```text +- assigns student to class/group +- rejects assignment from another school +- rejects duplicate active assignment if policy disallows +- unassign requires reason +- writes audit log +``` + +### Done when + +```text +- assignments are service-owned +- Islamic Sunday School class/group behavior can map through extension configuration +``` + +--- + +## 22. Sixth Implementation Slice: Status Transition Service + +### Objective + +Make student lifecycle state transitions explicit. + +### Tasks + +```text +STU-034. Create StudentLifecycleStateMachine. +STU-035. Create StudentStatusTransitionData. +STU-036. Implement transitionStatus(). +STU-037. Add status_changed audit event. +STU-038. Replace direct status updates. +``` + +### Required tests + +```text +- active can become withdrawn with reason +- withdrawn cannot become graduated unless policy allows +- archived student cannot be updated without special permission +- invalid transition fails +- status transition writes before/after audit +``` + +### Done when + +```text +- no active controller directly assigns student status +- invalid transitions are blocked centrally +``` + +--- + +## 23. Seventh Implementation Slice: Promotion Service + +### Objective + +Move promotion behavior into policy-driven modular service. + +### Tasks + +```text +STU-039. Create PromotionServiceContract. +STU-040. Create PromotionPolicyContract. +STU-041. Implement DefaultPromotionPolicy. +STU-042. Implement promoteStudent(). +STU-043. Implement bulkPromote(). +STU-044. Add student.promoted/student.retained audit events. +STU-045. Add IslamicSundaySchoolPromotionPolicy binding. +``` + +### Required tests + +```text +- evaluates promotion +- promotes eligible student +- retains ineligible student +- override requires reason +- cross-school promotion fails +- bulk promotion returns row-level results +- Islamic Sunday School policy can change eligibility without core changes +``` + +### Done when + +```text +- promotion rules are policy-driven +- core does not contain Islamic Sunday School promotion logic +``` + +--- + +## 24. Eighth Implementation Slice: Islamic Sunday School Profile Extension + +### Objective + +Move Islamic Sunday School-only student fields into extension layer. + +### Tasks + +```text +STU-046. Create IslamicSundaySchoolStudentProfileData. +STU-047. Create IslamicSundaySchoolStudentProfileService. +STU-048. Create extension profile table/model if needed. +STU-049. Add policy checks for sensitive fields. +STU-050. Compose IslamicSundaySchoolStudentView from core StudentView + extension profile. +STU-051. Update Islamic Sunday School endpoints to use extension service. +``` + +### Required tests + +```text +- creates Islamic Sunday School profile for student +- rejects profile for student from another school +- sensitive field requires permission +- core StudentService does not import IslamicSundaySchool profile classes +- IslamicSundaySchoolStudentView composes core + extension data +``` + +### Done when + +```text +- Islamic Sunday School-only fields are not required by core student creation +- Islamic Sunday School profile behavior lives outside SchoolCore +``` + +--- + +## 25. Student Read Models and Reports + +Reports must consume student read models, not re-implement lifecycle logic. + +### Tasks + +```text +STU-052. Create StudentReadServiceContract. +STU-053. Create StudentReadRepository. +STU-054. Create student search query DTO. +STU-055. Add school context to every query. +STU-056. Add core student summary read model. +STU-057. Add Islamic Sunday School student summary extension read model. +``` + +### Required tests + +```text +- student list excludes other schools +- guardian sees only allowed student fields +- teacher sees only assigned students if policy says so +- archived students excluded by default +- Islamic Sunday School report fields do not appear in core report +``` + +--- + +## 26. Database and Schema Requirements + +### 26.1 Required indexes + +Add or verify: + +```text +students.school_id indexed +students.student_identifier indexed +students.status indexed +students.created_at indexed + +guardians.school_id indexed +guardians.email indexed, nullable +guardians.phone indexed, nullable + +student_guardian.school_id indexed +student_guardian.student_id indexed +student_guardian.guardian_id indexed +student_guardian.relationship_type indexed + +households.school_id indexed + +enrollments.school_id indexed +enrollments.student_id indexed +enrollments.academic_year_id indexed +enrollments.term_id indexed, nullable +enrollments.status indexed + +student_assignments.school_id indexed +student_assignments.student_id indexed +student_assignments.assignment_type indexed +student_assignments.assignable_type/assignable_id indexed +student_assignments.status indexed + +student_audit_logs.school_id indexed +student_audit_logs.actor_user_id indexed +student_audit_logs.entity_type/entity_id indexed +student_audit_logs.created_at indexed +``` + +### 26.2 Required unique constraints + +Recommended: + +```text +unique(students.school_id, students.student_identifier) +unique(enrollments.school_id, student_id, academic_year_id, term_id, status) where status is active, if DB supports partial indexes +unique(student_assignments.school_id, student_id, assignment_type, assignable_type, assignable_id, effective_from) where status is active, if policy requires uniqueness +``` + +### 26.3 Migration caution + +Do not immediately flatten or rename legacy student fields. Add service/repository adapters first. Student data tends to be referenced by half the app, several reports, and someone’s exported spreadsheet from 2017 that has somehow become sacred scripture. + +--- + +## 27. FormRequest Requirements + +Create or update: + +```text +CreateStudentRequest +UpdateStudentRequest +StudentStatusTransitionRequest +CreateGuardianRequest +LinkGuardianRequest +CreateHouseholdRequest +EnrollmentRequest +AssignmentRequest +PromotionRequest +BulkPromotionRequest +IslamicSundaySchoolStudentProfileRequest +StudentSearchRequest +``` + +Each FormRequest must define: + +```text +authorize() +rules() +messages(), if needed +toDTO(), optional but recommended +``` + +Controllers should use: + +```php +$data = $request->toDTO(); +``` + +Not raw request merging. + +--- + +## 28. Service Provider Bindings + +Create: + +```php +namespace App\Providers; + +use Illuminate\Support\ServiceProvider; +use App\Domain\SchoolCore\Students\Contracts\StudentServiceContract; +use App\Domain\SchoolCore\Students\Services\StudentService; + +final class SchoolCoreStudentServiceProvider extends ServiceProvider +{ + public function register(): void + { + $this->app->bind(StudentServiceContract::class, StudentService::class); + $this->app->bind(StudentProfileServiceContract::class, StudentProfileService::class); + $this->app->bind(StudentIdentifierGeneratorContract::class, DefaultStudentIdentifierGenerator::class); + $this->app->bind(GuardianServiceContract::class, GuardianService::class); + $this->app->bind(HouseholdServiceContract::class, HouseholdService::class); + $this->app->bind(EnrollmentServiceContract::class, EnrollmentService::class); + $this->app->bind(AssignmentServiceContract::class, AssignmentService::class); + $this->app->bind(PromotionServiceContract::class, PromotionService::class); + $this->app->bind(PromotionPolicyContract::class, DefaultPromotionPolicy::class); + $this->app->bind(StudentLifecycleAuditLoggerContract::class, StudentLifecycleAuditLogger::class); + $this->app->bind(StudentReadServiceContract::class, StudentReadService::class); + } +} +``` + +Islamic Sunday School may override policy/extension bindings in its own provider. + +--- + +## 29. Architecture Tests + +Add tests that fail if student lifecycle boundaries are violated. + +Required assertions: + +```text +SchoolCore\Students must not depend on Domain\IslamicIslamicSundaySchool +SchoolCore\Students must not depend on Illuminate\Http\Request +SchoolCore\Students services must not call auth() +SchoolCore\Students services must not call request() +Controllers must not instantiate student concrete services directly +StudentController must not generate identifiers directly +Core student contracts must not contain Islamic Sunday School vocabulary +Student creation must go through StudentServiceContract +Promotion must go through PromotionServiceContract +``` + +Optional static checks: + +```text +No max(id) + 1 identifier generation +No direct student status assignment in controllers +No Sunday/masjid/community/ministry/baptism vocabulary inside SchoolCore\Students contracts +No guardian/student relationship mutation outside GuardianService +No enrollment mutation outside EnrollmentService +``` + +--- + +## 30. Rollout Plan + +### Step 1: Add student lifecycle module scaffolding + +```text +- create namespaces +- create contracts +- create DTOs +- create provider +- bind default implementations +``` + +### Step 2: Add regression tests around current student creation + +```text +- basic student creation +- student with guardian +- student with class/group assignment +- duplicate school ID race scenario +- student list/read response compatibility +``` + +### Step 3: Fix identifier generation + +```text +- remove max(id) + 1 +- generate after insert +- add unique constraint +- add concurrency tests +``` + +### Step 4: Extract student creation service + +```text +- move orchestration into StudentService +- keep controller compatibility +``` + +### Step 5: Extract guardian/household services + +```text +- centralize family relationships +- prevent cross-school linking +``` + +### Step 6: Extract enrollment and assignment services + +```text +- centralize placement and school-year behavior +``` + +### Step 7: Add status transition and promotion services + +```text +- explicit lifecycle transitions +- policy-driven promotion +``` + +### Step 8: Add Islamic Sunday School extension profile + +```text +- remove Islamic Sunday School-specific assumptions from core +- compose extension read models +``` + +### Step 9: Add architecture tests and deprecation labels + +```text +- enforce boundaries +- mark legacy methods +``` + +--- + +## 31. Implementation Tickets + +## STU-001: Create student lifecycle module scaffold + +### Description + +Create the `SchoolCore\Students` namespace and initial contracts, DTO folders, service folders, enum folders, exception folders, and provider. + +### Acceptance Criteria + +```text +- Namespace exists. +- Service provider is registered. +- Empty contracts compile. +- No controller behavior changes yet. +``` + +--- + +## STU-002: Replace unsafe student identifier generation + +### Description + +Replace pre-insert `max(id) + 1` identifier generation with contract-based, race-safe generation. + +### Acceptance Criteria + +```text +- Identifier is generated after student insert. +- Unique constraint exists. +- Collision retry exists. +- Concurrency test passes. +- No active path uses max(id) + 1. +``` + +--- + +## STU-003: Extract StudentService create flow + +### Description + +Move student creation orchestration into `StudentService`. + +### Acceptance Criteria + +```text +- Uses SchoolContext. +- Uses transaction. +- Creates audit log. +- Supports guardian/enrollment delegation. +- Controller delegates only. +- Existing response shape remains compatible. +``` + +--- + +## STU-004: Extract GuardianService and HouseholdService + +### Description + +Move guardian and household relationship management into modular services. + +### Acceptance Criteria + +```text +- Guardian creation is service-owned. +- Guardian linking is service-owned. +- Cross-school links fail. +- Unlink requires reason. +- Audit logs are written. +``` + +--- + +## STU-005: Extract EnrollmentService + +### Description + +Move enrollment and withdrawal behavior into modular service. + +### Acceptance Criteria + +```text +- Enrollment is school/year scoped. +- Duplicate active enrollment is prevented where policy disallows. +- Withdrawal requires reason. +- Audit logs are written. +``` + +--- + +## STU-006: Extract AssignmentService + +### Description + +Move class/group assignment behavior into modular service. + +### Acceptance Criteria + +```text +- Assignments are school-scoped. +- Assignment conflicts are detected. +- Unassignment requires reason. +- Audit logs are written. +``` + +--- + +## STU-007: Add StudentLifecycleStateMachine + +### Description + +Centralize student status transition rules. + +### Acceptance Criteria + +```text +- Invalid transitions fail. +- Direct controller status mutation is removed. +- Status changes require reason where policy requires. +- Before/after audit is written. +``` + +--- + +## STU-008: Extract PromotionService + +### Description + +Move promotion behavior into policy-driven modular service. + +### Acceptance Criteria + +```text +- Promotion evaluation uses PromotionPolicyContract. +- Bulk promotion returns row-level results. +- Override requires reason. +- Islamic Sunday School can override policy without changing core. +``` + +--- + +## STU-009: Add Islamic Sunday School student profile extension + +### Description + +Move Islamic Sunday School-only student fields into extension layer. + +### Acceptance Criteria + +```text +- Extension profile is school-scoped. +- Sensitive fields require explicit permission. +- Core student creation does not require Islamic Sunday School fields. +- SchoolCore does not import IslamicSundaySchool classes. +``` + +--- + +## STU-010: Add student lifecycle architecture tests + +### Description + +Enforce student module boundaries in CI. + +### Acceptance Criteria + +```text +- SchoolCore\Students cannot import IslamicSundaySchool. +- Student services cannot call auth() or request(). +- Core contracts contain no Islamic Sunday School vocabulary. +- StudentController does not generate identifiers. +- CI fails on boundary violation. +``` + +--- + +## 32. Definition of Done for Phase 5 + +Phase 5 is done when: + +```text +- Student lifecycle core contracts exist. +- Student identifier generation is race-safe. +- Student creation uses StudentService. +- Guardian behavior uses GuardianService. +- Household behavior uses HouseholdService. +- Enrollment behavior uses EnrollmentService. +- Assignment behavior uses AssignmentService. +- Student status transitions are centralized. +- Promotion is policy-driven. +- Islamic Sunday School profile behavior lives outside SchoolCore. +- Student services require SchoolContext. +- Cross-school student/guardian/enrollment access is blocked by tests. +- Architecture tests prevent SchoolCore from depending on IslamicSundaySchool. +- Legacy student lifecycle methods are labeled canonical, adapter, deprecated, or remove. +``` + +--- + +## 33. Non-Goals + +Do not include in Phase 5: + +```text +- full student UI redesign +- full admissions system +- parent portal redesign +- external SIS integration +- biometric/student ID card printing +- full medical record system +- full masjid/community affiliation CRM +``` + +Those may happen later. Phase 5 is about making the student lifecycle reliable, reusable, school-scoped, and extensible. + +--- + +## 34. Main Risks + +### Risk: duplicate student identifiers + +Mitigation: + +```text +- generate after insert +- unique constraint +- collision retry +- concurrency tests +``` + +### Risk: Islamic Sunday School fields pollute core + +Mitigation: + +```text +- extension profile table/service +- architecture tests +- vocabulary review +``` + +### Risk: guardian privacy leakage + +Mitigation: + +```text +- relationship-aware authorization +- sensitive field policies +- guardian-specific read models +``` + +### Risk: enrollment/assignment drift + +Mitigation: + +```text +- centralized enrollment and assignment services +- unique constraints where appropriate +- audit logs +``` + +### Risk: behavior break during extraction + +Mitigation: + +```text +- snapshot current responses +- controller adapters +- vertical slices +``` + +--- + +## 35. Immediate Next Work Queue + +Start here: + +```text +1. STU-001 Create student lifecycle module scaffold +2. STU-002 Replace unsafe student identifier generation +3. STU-003 Extract StudentService create flow +4. STU-004 Extract GuardianService and HouseholdService +5. STU-005 Extract EnrollmentService +6. STU-006 Extract AssignmentService +7. STU-007 Add StudentLifecycleStateMachine +8. STU-008 Extract PromotionService +9. STU-009 Add Islamic Sunday School student profile extension +10. STU-010 Add student lifecycle architecture tests +``` + +Do not start with reports or cosmetic model renaming. Fix identity, creation, relationships, enrollment, and promotion first. Reports can wait their turn like everyone else in the bureaucracy. diff --git a/docs/modular_plans/phase_6_modular_communication_execution_plan_islamic.md b/docs/modular_plans/phase_6_modular_communication_execution_plan_islamic.md new file mode 100644 index 00000000..dc0dc940 --- /dev/null +++ b/docs/modular_plans/phase_6_modular_communication_execution_plan_islamic.md @@ -0,0 +1,2056 @@ +# Phase 6 Execution Plan: Modular Communication Extraction + + +# Islamic Sunday School Alignment Update + +Phase 6 communication now treats the extension as **Islamic Sunday School**. + +`SchoolCore\Communication` owns recipient preview, recipient resolution, dedupe, preferences, sending, logs, delivery status, retries, templates, and audit. + +`IslamicSundaySchool\Communication` may add recipient resolvers and templates for: + +```text +Qur'an class guardians +Arabic class guardians +Islamic studies class guardians +halaqa groups +volunteer teams +masjid family/community groups +Sunday program attendees +special program reminders +``` + +No bulk send may bypass preview just because the audience is familiar. Familiar audiences are how people accidentally message the entire masjid about one student's missing notebook. Technology remains undefeated at embarrassment. + +--- +## 1. Purpose + +Phase 6 extracts communication behavior into a reusable `SchoolCore` module that can support any school system while allowing Islamic Sunday School-specific communication behavior through recipient resolvers, policies, templates, and extension services. + +The communication module must own the invariant parts of school communication: + +- message composition +- message templates +- recipient resolution +- recipient preview +- recipient deduplication +- channel selection +- email delivery +- SMS/WhatsApp adapter boundaries +- notification delivery +- communication preferences +- send logs +- delivery status +- retry behavior +- idempotency +- audit logging +- authorization boundaries +- cross-school isolation + +Islamic Sunday School may customize: + +- ministry recipient groups +- masjid-family/community recipient groups +- Islamic Sunday School class rosters +- volunteer/team messaging +- religious/community announcement templates +- service/event reminders +- curriculum reminders +- family/masjid/community labels + +Islamic Sunday School must not own the global communication engine. It plugs into it. Otherwise one day every school using the platform will discover its “global” notification system thinks every recipient belongs to a volunteer team, because apparently hardcoding is a lifestyle. + +--- + +## 2. Required Dependency Direction + +Communication must follow the modular platform boundary: + +```text +App\Domain\IslamicIslamicSundaySchool\Communication + ↓ depends on +App\Domain\SchoolCore\Communication + ↓ depends on +App\Domain\SchoolCore\Shared +``` + +Forbidden dependencies: + +```text +SchoolCore\Communication -> IslamicSundaySchool\* +SchoolCore\Communication -> Http\Request +SchoolCore\Communication -> auth() +SchoolCore\Communication -> controller classes +SchoolCore\Communication -> Islamic Sunday School vocabulary +SchoolCore\Communication -> direct unscoped school queries +``` + +Allowed dependencies: + +```text +IslamicSundaySchool\Communication -> SchoolCore\Communication contracts +Http\Controllers -> communication contracts +Jobs -> communication contracts +Communication services -> SchoolContext +Communication services -> shared audit/transaction contracts +Communication services -> queue/mail/SMS/WhatsApp adapters +``` + +--- + +## 3. Target Namespace Structure + +Create the communication module under `SchoolCore`: + +```text +app/ + Domain/ + SchoolCore/ + Communication/ + Contracts/ + CommunicationServiceContract.php + MessageTemplateServiceContract.php + RecipientResolverContract.php + RecipientPreviewServiceContract.php + RecipientDedupeServiceContract.php + CommunicationPreferencePolicyContract.php + ChannelRouterContract.php + EmailChannelContract.php + SmsChannelContract.php + WhatsAppChannelContract.php + NotificationChannelContract.php + DeliveryStatusServiceContract.php + CommunicationAuditLoggerContract.php + CommunicationIdempotencyServiceContract.php + DTO/ + ComposeMessageData.php + SendMessageData.php + BulkSendMessageData.php + MessageTemplateData.php + RecipientQueryData.php + RecipientPreviewData.php + RecipientData.php + ChannelMessageData.php + CommunicationPreferenceData.php + DeliveryReceiptData.php + CommunicationAuditData.php + Enums/ + CommunicationChannel.php + CommunicationStatus.php + RecipientType.php + MessagePriority.php + MessageAudienceType.php + CommunicationAuditAction.php + DeliveryFailureReason.php + Exceptions/ + CommunicationException.php + RecipientResolutionException.php + RecipientPreviewRequiredException.php + UnauthorizedCommunicationAccessException.php + MissingCommunicationPreferenceException.php + ChannelDeliveryException.php + DuplicateSendException.php + Services/ + CommunicationService.php + MessageTemplateService.php + RecipientPreviewService.php + RecipientDedupeService.php + ChannelRouter.php + DeliveryStatusService.php + CommunicationAuditLogger.php + CommunicationIdempotencyService.php + Resolvers/ + StudentRecipientResolver.php + GuardianRecipientResolver.php + StaffRecipientResolver.php + ClassRecipientResolver.php + SchoolWideRecipientResolver.php + Channels/ + EmailChannel.php + SmsChannel.php + WhatsAppChannel.php + NotificationChannel.php + Policies/ + DefaultCommunicationPreferencePolicy.php + DefaultRecipientAuthorizationPolicy.php + Repositories/ + MessageTemplateRepository.php + CommunicationLogRepository.php + RecipientReadRepository.php + Support/ + CommunicationTransactionRunner.php + RecipientSet.php + RecipientFingerprint.php + TemplateRenderer.php + MessageSanitizer.php + DeliveryRetryPolicy.php +``` + +Islamic Sunday School-specific communication behavior goes here: + +```text +app/ + Domain/ + IslamicSundaySchool/ + Communication/ + Resolvers/ + SundayClassRecipientResolver.php + VolunteerTeamRecipientResolver.php + MasjidFamilyRecipientResolver.php + VolunteerRecipientResolver.php + IslamicSundaySchoolProgramRecipientResolver.php + Policies/ + IslamicSundaySchoolCommunicationPreferencePolicy.php + IslamicSundaySchoolRecipientAuthorizationPolicy.php + Templates/ + IslamicSundaySchoolTemplateCatalog.php + Providers/ + IslamicSundaySchoolCommunicationServiceProvider.php +``` + +--- + +## 4. Core Design Rules + +### 4.1 Communication services must require `SchoolContext` + +Every public communication method must receive `SchoolContext`. + +Example: + +```php +public function previewRecipients( + SchoolContext $context, + RecipientQueryData $query +): RecipientPreviewResult; +``` + +A message without `SchoolContext` is invalid. A bulk message without it is basically a confetti cannon pointed at privacy law. + +### 4.2 Bulk sends must require preview + +No bulk communication may send directly from raw filters. + +Required flow: + +```text +1. Resolve recipient query. +2. Generate recipient preview. +3. Deduplicate recipients. +4. Apply communication preferences. +5. Store preview snapshot. +6. Confirm/send using preview snapshot ID. +7. Send through channel router. +8. Log every recipient delivery attempt. +``` + +Forbidden flow: + +```text +Controller receives filters -> loops users -> sends emails +``` + +That flow looks productive until it sends 400 duplicate reminders to one guardian and misses everyone else. + +### 4.3 Recipient resolution must be modular + +Core recipient types: + +```text +students +guardians +staff +teachers +classes/groups +school-wide audience +custom uploaded recipients, if allowed +``` + +Islamic Sunday School recipient types: + +```text +Islamic Sunday School classes +volunteer teams +masjid/community families +volunteers +program attendees +curriculum groups +``` + +Islamic Sunday School recipient resolvers register through the resolver registry. Core must not know their names. + +### 4.4 Communication preferences must be enforced + +Every send must check: + +```text +channel opt-in/opt-out +recipient contact availability +student/guardian relationship visibility +emergency override rules +quiet hours, if configured +language preference, if configured +legal/sensitive category restrictions +``` + +Emergency messages may override some preferences only if policy allows it and audit logs record it. + +### 4.5 Controllers must not send directly + +Controllers may: + +```text +- validate request +- authorize route +- build DTO +- call communication contract +- return resource +``` + +Controllers may not: + +```text +- call Mail directly +- call WhatsApp/SMS clients directly +- loop over recipients +- resolve recipient queries directly +- deduplicate recipients directly +- apply preferences directly +- build final templates directly +``` + +--- + +## 5. Core Contracts + +### 5.1 `CommunicationServiceContract` + +```php +namespace App\Domain\SchoolCore\Communication\Contracts; + +use App\Domain\SchoolCore\Shared\Context\SchoolContext; +use App\Domain\SchoolCore\Communication\DTO\SendMessageData; +use App\Domain\SchoolCore\Communication\DTO\BulkSendMessageData; + +interface CommunicationServiceContract +{ + public function sendMessage(SchoolContext $context, SendMessageData $data): SendResult; + + public function sendBulkMessage(SchoolContext $context, BulkSendMessageData $data): BulkSendResult; + + public function scheduleBulkMessage(SchoolContext $context, BulkSendMessageData $data): ScheduledMessageResult; +} +``` + +### 5.2 `RecipientPreviewServiceContract` + +```php +namespace App\Domain\SchoolCore\Communication\Contracts; + +use App\Domain\SchoolCore\Shared\Context\SchoolContext; +use App\Domain\SchoolCore\Communication\DTO\RecipientQueryData; + +interface RecipientPreviewServiceContract +{ + public function preview(SchoolContext $context, RecipientQueryData $query): RecipientPreviewResult; + + public function getPreview(SchoolContext $context, string $previewId): RecipientPreviewResult; +} +``` + +### 5.3 `RecipientResolverContract` + +```php +namespace App\Domain\SchoolCore\Communication\Contracts; + +use App\Domain\SchoolCore\Shared\Context\SchoolContext; +use App\Domain\SchoolCore\Communication\DTO\RecipientQueryData; + +interface RecipientResolverContract +{ + public function supports(RecipientQueryData $query): bool; + + public function resolve(SchoolContext $context, RecipientQueryData $query): RecipientSet; +} +``` + +### 5.4 `RecipientDedupeServiceContract` + +```php +namespace App\Domain\SchoolCore\Communication\Contracts; + +interface RecipientDedupeServiceContract +{ + public function dedupe(RecipientSet $recipients): RecipientSet; +} +``` + +### 5.5 `CommunicationPreferencePolicyContract` + +```php +namespace App\Domain\SchoolCore\Communication\Contracts; + +use App\Domain\SchoolCore\Shared\Context\SchoolContext; + +interface CommunicationPreferencePolicyContract +{ + public function filterAllowedRecipients( + SchoolContext $context, + RecipientSet $recipients, + CommunicationChannel $channel, + MessageCategory $category + ): RecipientSet; + + public function canOverridePreferences( + SchoolContext $context, + MessageCategory $category + ): bool; +} +``` + +### 5.6 `ChannelRouterContract` + +```php +namespace App\Domain\SchoolCore\Communication\Contracts; + +use App\Domain\SchoolCore\Shared\Context\SchoolContext; + +interface ChannelRouterContract +{ + public function route(SchoolContext $context, ChannelMessageData $message): DeliveryResult; +} +``` + +### 5.7 Channel contracts + +```php +interface EmailChannelContract +{ + public function send(SchoolContext $context, ChannelMessageData $message): DeliveryResult; +} + +interface SmsChannelContract +{ + public function send(SchoolContext $context, ChannelMessageData $message): DeliveryResult; +} + +interface WhatsAppChannelContract +{ + public function send(SchoolContext $context, ChannelMessageData $message): DeliveryResult; +} + +interface NotificationChannelContract +{ + public function send(SchoolContext $context, ChannelMessageData $message): DeliveryResult; +} +``` + +--- + +## 6. DTO Requirements + +Do not pass raw request arrays into communication services. + +### 6.1 `RecipientQueryData` + +Fields: + +```text +audience_type +audience_filters +channel +message_category +include_inactive, default false +include_archived, default false +include_guardians, default true +include_students, default false +include_staff, default false +language, nullable +metadata, nullable +``` + +### 6.2 `RecipientData` + +Fields: + +```text +recipient_type +recipient_id +display_name +email, nullable +phone, nullable +preferred_language, nullable +guardian_student_ids, nullable array +staff_id, nullable +student_id, nullable +school_id +delivery_channels +preference_flags +metadata, nullable +``` + +### 6.3 `ComposeMessageData` + +Fields: + +```text +template_id, nullable +subject, nullable +body +variables, nullable +message_category +priority +language, nullable +attachments, nullable array +metadata, nullable +``` + +### 6.4 `BulkSendMessageData` + +Fields: + +```text +preview_id +channel +subject, nullable +body +template_id, nullable +variables, nullable +message_category +priority +scheduled_at, nullable +idempotency_key, nullable +confirmation_token +metadata, nullable +``` + +### 6.5 `DeliveryReceiptData` + +Fields: + +```text +message_id +recipient_id +channel +provider_message_id, nullable +status +failure_reason, nullable +raw_provider_payload, nullable +delivered_at, nullable +failed_at, nullable +``` + +--- + +## 7. Recipient Preview Model + +### 7.1 Required preview behavior + +Preview must show: + +```text +total resolved recipients +total after dedupe +total blocked by preference +total missing contact method +total unauthorized/excluded +sample recipients +recipient groups included +recipient groups excluded +estimated channel cost, if available +warning flags +preview expires_at +``` + +### 7.2 Preview snapshot fields + +Suggested table: + +```text +communication_recipient_previews +``` + +Fields: + +```text +id +school_id +actor_user_id +audience_type +audience_filters_json +channel +message_category +recipient_count +deduped_count +blocked_count +missing_contact_count +snapshot_json +confirmation_token_hash +expires_at +created_at +``` + +The send step must use the preview snapshot, not rerun a mutable query without warning. Otherwise “previewed 38 recipients” becomes “sent to 417 recipients” because someone changed a class assignment mid-click. Lovely. + +### 7.3 Preview confirmation + +Bulk send requires: + +```text +preview_id +confirmation_token +idempotency_key +``` + +The confirmation token proves the user is sending the set they previewed. + +--- + +## 8. Recipient Resolution Strategy + +### 8.1 Resolver registry + +Create a registry that holds all resolvers. + +Resolution flow: + +```text +1. Receive RecipientQueryData. +2. Find resolver that supports audience_type/filter. +3. Resolve raw recipient set. +4. Scope to SchoolContext. +5. Apply authorization filters. +6. Dedupe. +7. Apply communication preferences. +8. Return preview. +``` + +### 8.2 Core resolvers + +Core should include: + +```text +StudentRecipientResolver +GuardianRecipientResolver +StaffRecipientResolver +ClassRecipientResolver +SchoolWideRecipientResolver +``` + +### 8.3 Islamic Sunday School resolvers + +Islamic Sunday School may register: + +```text +SundayClassRecipientResolver +VolunteerTeamRecipientResolver +MasjidFamilyRecipientResolver +VolunteerRecipientResolver +IslamicSundaySchoolProgramRecipientResolver +``` + +These must implement `RecipientResolverContract`. + +--- + +## 9. Deduplication Rules + +Recipient dedupe must handle: + +```text +same guardian linked to multiple students +same email used by multiple contacts +same phone used by multiple contacts +staff member also guardian +recipient selected through multiple groups +``` + +Default dedupe priority: + +```text +1. explicit recipient ID +2. verified email +3. verified phone +4. normalized name + contact fallback +``` + +Dedupe result must preserve reason metadata: + +```text +recipient selected by class A +recipient selected by class B +deduped into one delivery +``` + +This matters because people will ask why they received a message, usually right after the message was unnecessary. + +--- + +## 10. Communication Preferences + +### 10.1 Preference dimensions + +Preferences may include: + +```text +channel +message category +language +quiet hours +emergency override +student-specific guardian communication rules +staff communication rules +unsubscribe status +``` + +Message categories: + +```text +academic +attendance +finance +emergency +general +event +system +religious_or_sensitive, extension-controlled +``` + +### 10.2 Required enforcement + +Before sending: + +```text +- remove opted-out recipients unless override is allowed +- remove recipients missing channel contact method +- remove guardians not authorized for selected student/category +- respect quiet hours if configured +- record every exclusion reason +``` + +### 10.3 Emergency behavior + +Emergency messages may override preferences only if: + +```text +- actor has permission +- category is emergency +- audit log records override +- message copy is preserved +``` + +--- + +## 11. Template and Rendering Rules + +### 11.1 Template ownership + +Core templates: + +```text +attendance notice +finance notice +general announcement +event reminder +system notice +``` + +Islamic Sunday School templates: + +```text +Islamic Sunday School class reminder +program volunteer reminder +curriculum update +masjid family/community announcement +program attendance follow-up +``` + +### 11.2 Rendering rules + +Template rendering must: + +```text +- use safe variable interpolation +- reject unknown variables unless policy allows fallback +- support language variants +- render preview before send +- log template version used +``` + +Forbidden: + +```text +eval() +raw user HTML without sanitization +template rendering inside controller loops +``` + +A message template should not be a tiny remote-code-execution shrine. We have enough problems. + +--- + +## 12. Channel Delivery Strategy + +### 12.1 Channel router + +The router chooses delivery channel based on: + +```text +requested channel +recipient allowed channels +recipient contact availability +school provider configuration +message category +priority +fallback policy +``` + +### 12.2 Email channel + +Must support: + +```text +subject +body +attachments, if allowed +reply-to +template version +delivery status tracking, if provider supports it +``` + +### 12.3 SMS/WhatsApp channels + +Must support: + +```text +phone normalization +provider configuration by school +message length rules +template approval requirements, if provider requires it +delivery status callbacks +rate limits +``` + +### 12.4 Notification channel + +Must support: + +```text +in-app notifications +push notifications, if configured +read status +deep link target +``` + +--- + +## 13. Send Logging Requirements + +Every send attempt must create a log row. + +Suggested tables: + +```text +communication_messages +communication_message_recipients +communication_delivery_attempts +communication_recipient_previews +communication_preferences +communication_templates +``` + +### 13.1 Message log fields + +```text +id +school_id +academic_year_id, nullable +term_id, nullable +actor_user_id +channel +message_category +priority +subject, nullable +body_snapshot +template_id, nullable +template_version, nullable +preview_id, nullable +status +scheduled_at, nullable +sent_at, nullable +created_at +``` + +### 13.2 Recipient log fields + +```text +id +message_id +school_id +recipient_type +recipient_id +delivery_address_hash +delivery_address_masked +status +excluded_reason, nullable +provider_message_id, nullable +sent_at, nullable +delivered_at, nullable +failed_at, nullable +created_at +``` + +Do not store sensitive full delivery addresses in every log table unless there is a clear policy. Store masked/hash where practical. Privacy is not a garnish. + +--- + +## 14. Retry and Failure Handling + +### 14.1 Retry policy + +Retries should be handled by channel and failure type. + +Retryable: + +```text +provider timeout +temporary provider failure +rate limit +network error +``` + +Not retryable: + +```text +invalid email +invalid phone +recipient opted out +unauthorized recipient +template rejected permanently +``` + +### 14.2 Required behavior + +```text +- retry count is capped +- every retry attempt is logged +- final failure reason is stored +- provider message ID is stored where available +- bulk send result includes success/failure counts +``` + +--- + +## 15. Idempotency Requirements + +Bulk sends and direct sends must support idempotency. + +Minimum implementation: + +```text +idempotency_key +actor_user_id +school_id +operation +payload_hash +result_entity_type +result_entity_id +created_at +expires_at +``` + +Behavior: + +```text +- same key + same operation + same payload returns original result +- same key + same operation + different payload fails +- missing key allowed for legacy clients during transition +``` + +Apply first to: + +```text +bulk send +single send +scheduled send +``` + +--- + +## 16. Audit Logging Requirements + +Every communication action must create an audit event. + +Required audit fields: + +```text +school_id +academic_year_id, nullable +term_id, nullable +actor_user_id +actor_role_snapshot +action +entity_type +entity_id +before_json, nullable +after_json, nullable +reason, nullable +request_id +ip_address, nullable +user_agent, nullable +idempotency_key, nullable +created_at +``` + +Required actions: + +```text +communication.preview.created +communication.message.created +communication.bulk_send.confirmed +communication.message.sent +communication.message.scheduled +communication.message.cancelled +communication.delivery.failed +communication.preference.updated +communication.template.created +communication.template.updated +communication.template.archived +``` + +--- + +## 17. Authorization Requirements + +Minimum actions: + +```text +communication.preview +communication.send.single +communication.send.bulk +communication.schedule +communication.cancel +communication.template.view +communication.template.manage +communication.preference.view +communication.preference.manage +communication.delivery.view +communication.audit.view +``` + +Every communication endpoint must have tests for: + +```text +unauthenticated +wrong school +wrong role +wrong audience scope +wrong message category permission +authorized admin +authorized staff +authorized teacher within assigned class scope +authorized finance user for finance messages +authorized attendance user for attendance messages +``` + +Sensitive audience categories require stronger permission: + +```text +emergency +finance +religious_or_sensitive +student discipline +medical, if ever supported +``` + +--- + +## 18. Islamic Sunday School Extension Model + +Islamic Sunday School communication extends core through resolvers, templates, and policies. + +### 18.1 Islamic Sunday School allowed customizations + +```text +- Islamic Sunday School class recipient resolver +- volunteer team recipient resolver +- masjid family/community recipient resolver +- volunteer recipient resolver +- Sunday program attendance follow-up resolver +- masjid/family announcement templates +- volunteer/program reminder templates +- religious/sensitive category policy +``` + +### 18.2 Islamic Sunday School forbidden customizations + +```text +- bypassing recipient preview +- bypassing dedupe +- bypassing preferences +- bypassing audit logs +- sending directly from controllers +- hardcoding ministry/masjid/community vocabulary into SchoolCore contracts +- querying recipients without SchoolContext +``` + +### 18.3 Binding example + +```php +$this->app->tag([ + SundayClassRecipientResolver::class, + VolunteerTeamRecipientResolver::class, + MasjidFamilyRecipientResolver::class, + VolunteerRecipientResolver::class, +], 'communication.recipient_resolvers'); + +$this->app->bind( + CommunicationPreferencePolicyContract::class, + IslamicSundaySchoolCommunicationPreferencePolicy::class +); +``` + +--- + +## 19. Migration Strategy + +### 19.1 Use vertical slices + +Recommended order: + +```text +1. Recipient preview service +2. Recipient resolver registry +3. Recipient dedupe service +4. Communication preference policy +5. Email channel extraction +6. Bulk send flow +7. WhatsApp/SMS adapter boundaries +8. Template service +9. Delivery status/retries +10. Islamic Sunday School recipient extensions +``` + +Why this order: + +```text +- preview prevents accidental mass sends +- resolver/dedupe creates stable recipient set +- preferences prevent unauthorized/unwanted sends +- email is usually easiest to stabilize first +- SMS/WhatsApp provider behavior needs adapter boundaries +- templates and delivery status build on stable sends +``` + +### 19.2 Compatibility layer + +Existing controllers may remain temporarily, but must delegate to new contracts. + +Example: + +```text +BroadcastController + -> BulkSendMessageRequest + -> SchoolContext + -> RecipientPreviewServiceContract::preview() + -> CommunicationServiceContract::sendBulkMessage() + -> MessageResource +``` + +Legacy direct-send methods should become wrappers or be deprecated. + +### 19.3 Deprecation rule + +Every legacy communication method must be labeled as one of: + +```text +canonical +adapter +deprecated +remove +``` + +No unlabeled duplicate send loops. + +--- + +## 20. First Implementation Slice: Recipient Preview + +### Objective + +Introduce preview-before-send for all bulk communication. + +### Tasks + +```text +COM-001. Create Communication module scaffold. +COM-002. Create RecipientPreviewServiceContract. +COM-003. Create RecipientQueryData. +COM-004. Create preview snapshot table/model. +COM-005. Implement basic preview for existing audience filters. +COM-006. Add confirmation token. +COM-007. Update bulk send endpoints to require preview_id. +``` + +### Required tests + +```text +- preview returns recipient count +- preview is school-scoped +- preview excludes unauthorized audience +- preview stores snapshot +- expired preview cannot be used +- send without preview fails for bulk messages +``` + +### Done when + +```text +- no active bulk send path can bypass preview +- preview snapshot is used for send confirmation +``` + +--- + +## 21. Second Implementation Slice: Resolver Registry + +### Objective + +Centralize recipient resolution through pluggable resolvers. + +### Tasks + +```text +COM-008. Create RecipientResolverContract. +COM-009. Create resolver registry. +COM-010. Add core student/guardian/staff/class resolvers. +COM-011. Scope all resolver queries by school_id. +COM-012. Add resolver tests. +``` + +### Required tests + +```text +- resolves guardians for class +- resolves staff group +- resolves school-wide audience +- rejects unsupported audience type +- does not include another school's users +``` + +### Done when + +```text +- recipient queries do not live in controllers +- Islamic Sunday School can register custom resolvers +``` + +--- + +## 22. Third Implementation Slice: Dedupe and Preferences + +### Objective + +Prevent duplicate and unauthorized recipient delivery. + +### Tasks + +```text +COM-013. Create RecipientDedupeServiceContract. +COM-014. Implement dedupe rules. +COM-015. Create CommunicationPreferencePolicyContract. +COM-016. Implement default preference policy. +COM-017. Add exclusion reason tracking. +``` + +### Required tests + +```text +- same guardian selected twice receives one message +- same email selected through two students dedupes correctly +- opted-out recipient is excluded +- missing email/phone is excluded for relevant channel +- emergency override follows policy +``` + +### Done when + +```text +- bulk preview shows deduped and excluded counts +- send uses deduped/preference-filtered recipient set +``` + +--- + +## 23. Fourth Implementation Slice: Email Channel Extraction + +### Objective + +Move email sending behind a reusable channel contract. + +### Tasks + +```text +COM-018. Create ChannelRouterContract. +COM-019. Create EmailChannelContract. +COM-020. Implement EmailChannel. +COM-021. Create message and recipient logs. +COM-022. Add communication.message.sent audit event. +COM-023. Update email send endpoints to delegate. +``` + +### Required tests + +```text +- sends single email +- sends bulk email from preview +- logs each recipient attempt +- blocks cross-school recipient +- respects preferences +- idempotency prevents duplicate send +``` + +### Done when + +```text +- controllers do not call Mail directly +- email sends are logged and auditable +``` + +--- + +## 24. Fifth Implementation Slice: Bulk Send Flow + +### Objective + +Standardize bulk send orchestration. + +### Tasks + +```text +COM-024. Create CommunicationServiceContract. +COM-025. Create BulkSendMessageData. +COM-026. Implement sendBulkMessage(). +COM-027. Require preview_id and confirmation_token. +COM-028. Add idempotency support. +COM-029. Add summary result counts. +``` + +### Required tests + +```text +- bulk send requires preview +- invalid confirmation token fails +- expired preview fails +- repeated idempotency key returns same result +- same key with different payload fails +- bulk result includes sent/excluded/failed counts +``` + +### Done when + +```text +- bulk send has one orchestration path +- direct loops are deprecated +``` + +--- + +## 25. Sixth Implementation Slice: SMS/WhatsApp Boundaries + +### Objective + +Move SMS and WhatsApp delivery behind channel contracts. + +### Tasks + +```text +COM-030. Create SmsChannelContract. +COM-031. Create WhatsAppChannelContract. +COM-032. Implement provider adapter wrappers. +COM-033. Add phone normalization. +COM-034. Add provider failure mapping. +COM-035. Add delivery attempt logs. +``` + +### Required tests + +```text +- invalid phone is excluded +- provider timeout is retryable +- opt-out blocks SMS/WhatsApp +- WhatsApp provider failure is logged +- school-specific provider configuration is respected +``` + +### Done when + +```text +- controllers/services do not call provider SDKs directly +- channel failures are normalized +``` + +--- + +## 26. Seventh Implementation Slice: Template Service + +### Objective + +Centralize templates and rendering. + +### Tasks + +```text +COM-036. Create MessageTemplateServiceContract. +COM-037. Create TemplateRenderer. +COM-038. Add template versioning. +COM-039. Add variable validation. +COM-040. Add template audit logs. +``` + +### Required tests + +```text +- renders valid template +- rejects unknown required variable +- stores template version used +- supports language variant +- unsafe HTML is sanitized or rejected by policy +``` + +### Done when + +```text +- templates are reusable and versioned +- send logs preserve rendered message snapshot +``` + +--- + +## 27. Eighth Implementation Slice: Delivery Status and Retries + +### Objective + +Track delivery status and retry transient failures. + +### Tasks + +```text +COM-041. Create DeliveryStatusServiceContract. +COM-042. Create DeliveryRetryPolicy. +COM-043. Add delivery receipt endpoint/handler where provider supports it. +COM-044. Add retry job. +COM-045. Add final failure handling. +``` + +### Required tests + +```text +- delivered receipt updates status +- failed receipt updates status +- retryable failure schedules retry +- non-retryable failure does not retry +- retry limit is enforced +``` + +### Done when + +```text +- delivery status is not guessed +- retry behavior is deterministic +``` + +--- + +## 28. Ninth Implementation Slice: Islamic Sunday School Recipient Extensions + +### Objective + +Add Islamic Sunday School recipient behavior through resolvers and policies. + +### Tasks + +```text +COM-046. Create SundayClassRecipientResolver. +COM-047. Create VolunteerTeamRecipientResolver. +COM-048. Create MasjidFamilyRecipientResolver. +COM-049. Create IslamicSundaySchoolCommunicationPreferencePolicy. +COM-050. Register resolvers through provider tags. +COM-051. Add Islamic Sunday School-specific templates. +``` + +### Required tests + +```text +- resolves Islamic Sunday School class guardians +- resolves volunteer team members +- resolves masjid family/community group +- Islamic Sunday School resolver cannot return another school's recipients +- SchoolCore does not import IslamicSundaySchool resolver classes +``` + +### Done when + +```text +- Islamic Sunday School communication behavior is extension-driven +- core communication remains school-neutral +``` + +--- + +## 29. Database and Schema Requirements + +### 29.1 Required indexes + +Add or verify: + +```text +communication_messages.school_id indexed +communication_messages.actor_user_id indexed +communication_messages.channel indexed +communication_messages.message_category indexed +communication_messages.status indexed +communication_messages.created_at indexed + +communication_message_recipients.school_id indexed +communication_message_recipients.message_id indexed +communication_message_recipients.recipient_type/recipient_id indexed +communication_message_recipients.status indexed + +communication_delivery_attempts.message_recipient_id indexed +communication_delivery_attempts.provider_message_id indexed, nullable +communication_delivery_attempts.status indexed +communication_delivery_attempts.created_at indexed + +communication_recipient_previews.school_id indexed +communication_recipient_previews.actor_user_id indexed +communication_recipient_previews.expires_at indexed + +communication_preferences.school_id indexed +communication_preferences.recipient_type/recipient_id indexed +communication_preferences.channel indexed + +communication_templates.school_id indexed, nullable +communication_templates.template_key indexed +communication_templates.status indexed +``` + +### 29.2 Recommended unique constraints + +```text +unique(school_id, idempotency_key, operation) where idempotency_key is not null +unique(school_id, template_key, version) +unique(message_id, recipient_type, recipient_id, channel) if one delivery per channel is required +``` + +--- + +## 30. FormRequest Requirements + +Create or update: + +```text +RecipientPreviewRequest +BulkSendMessageRequest +SendMessageRequest +ScheduleMessageRequest +CancelScheduledMessageRequest +CreateMessageTemplateRequest +UpdateMessageTemplateRequest +UpdateCommunicationPreferenceRequest +DeliveryReceiptRequest +``` + +Each FormRequest must define: + +```text +authorize() +rules() +messages(), if needed +toDTO(), optional but recommended +``` + +Controllers should use: + +```php +$data = $request->toDTO(); +``` + +Not raw request merging. + +--- + +## 31. Service Provider Bindings + +Create: + +```php +namespace App\Providers; + +use Illuminate\Support\ServiceProvider; +use App\Domain\SchoolCore\Communication\Contracts\CommunicationServiceContract; +use App\Domain\SchoolCore\Communication\Services\CommunicationService; + +final class SchoolCoreCommunicationServiceProvider extends ServiceProvider +{ + public function register(): void + { + $this->app->bind(CommunicationServiceContract::class, CommunicationService::class); + $this->app->bind(RecipientPreviewServiceContract::class, RecipientPreviewService::class); + $this->app->bind(RecipientDedupeServiceContract::class, RecipientDedupeService::class); + $this->app->bind(CommunicationPreferencePolicyContract::class, DefaultCommunicationPreferencePolicy::class); + $this->app->bind(ChannelRouterContract::class, ChannelRouter::class); + $this->app->bind(EmailChannelContract::class, EmailChannel::class); + $this->app->bind(SmsChannelContract::class, SmsChannel::class); + $this->app->bind(WhatsAppChannelContract::class, WhatsAppChannel::class); + $this->app->bind(NotificationChannelContract::class, NotificationChannel::class); + $this->app->bind(MessageTemplateServiceContract::class, MessageTemplateService::class); + $this->app->bind(DeliveryStatusServiceContract::class, DeliveryStatusService::class); + $this->app->bind(CommunicationAuditLoggerContract::class, CommunicationAuditLogger::class); + $this->app->bind(CommunicationIdempotencyServiceContract::class, CommunicationIdempotencyService::class); + + $this->app->tag([ + StudentRecipientResolver::class, + GuardianRecipientResolver::class, + StaffRecipientResolver::class, + ClassRecipientResolver::class, + SchoolWideRecipientResolver::class, + ], 'communication.recipient_resolvers'); + } +} +``` + +Islamic Sunday School may add tagged resolvers in its own provider. + +--- + +## 32. Architecture Tests + +Add tests that fail if communication boundaries are violated. + +Required assertions: + +```text +SchoolCore\Communication must not depend on Domain\IslamicIslamicSundaySchool +SchoolCore\Communication must not depend on Illuminate\Http\Request +SchoolCore\Communication services must not call auth() +SchoolCore\Communication services must not call request() +Controllers must not instantiate communication concrete services directly +Bulk send must go through CommunicationServiceContract +Bulk send must require RecipientPreviewServiceContract result +Controllers must not call Mail/SMS/WhatsApp providers directly +``` + +Optional static checks: + +```text +No Sunday/masjid/community/ministry vocabulary inside SchoolCore\Communication contracts +No direct send loops in controllers +No bulk send without preview_id +No provider SDK calls outside channel adapters +No raw recipient query outside resolver classes +``` + +--- + +## 33. Rollout Plan + +### Step 1: Add communication module scaffolding + +```text +- create namespaces +- create contracts +- create DTOs +- create provider +- bind default implementations +``` + +### Step 2: Add regression tests around current sends + +```text +- existing email send +- bulk send +- WhatsApp/SMS send, if present +- recipient targeting +- duplicate recipient behavior +``` + +Some tests may reveal the current system is doing “creative recipient math.” That is not surprising. That is why this phase exists. + +### Step 3: Add preview-before-send + +```text +- safest high-value boundary +- prevents accidental mass sends +``` + +### Step 4: Extract resolver registry + +```text +- centralize recipient queries +- enable Islamic Sunday School extensions +``` + +### Step 5: Add dedupe/preferences + +```text +- prevent duplicates +- respect opt-outs and contact rules +``` + +### Step 6: Extract email channel + +```text +- stabilize one delivery path first +``` + +### Step 7: Standardize bulk send + +```text +- confirmation token +- idempotency +- summary result +``` + +### Step 8: Extract SMS/WhatsApp adapters + +```text +- normalize provider failures +- add retry handling +``` + +### Step 9: Add template service + +```text +- version templates +- preserve rendered snapshots +``` + +### Step 10: Add Islamic Sunday School recipient extensions + +```text +- resolvers and templates outside core +``` + +--- + +## 34. Implementation Tickets + +## COM-001: Create communication module scaffold + +### Description + +Create the `SchoolCore\Communication` namespace and initial contracts, DTO folders, services, channels, resolvers, enums, exceptions, and provider. + +### Acceptance Criteria + +```text +- Namespace exists. +- Service provider is registered. +- Empty contracts compile. +- No controller behavior changes yet. +``` + +--- + +## COM-002: Add recipient preview service + +### Description + +Create preview-before-send behavior for bulk communication. + +### Acceptance Criteria + +```text +- Preview is school-scoped. +- Preview stores recipient snapshot. +- Preview has confirmation token. +- Expired preview cannot be used. +- Bulk send without preview fails. +``` + +--- + +## COM-003: Add recipient resolver registry + +### Description + +Centralize recipient resolution through pluggable resolvers. + +### Acceptance Criteria + +```text +- Core resolvers are tagged/registered. +- Unsupported audience fails safely. +- Cross-school recipients are excluded. +- Islamic Sunday School can register resolvers without core changes. +``` + +--- + +## COM-004: Add recipient dedupe service and preference policy + +### Description + +Deduplicate recipients and enforce communication preferences. + +### Acceptance Criteria + +```text +- Duplicate guardian/contact receives one message. +- Opted-out recipients are excluded. +- Missing contact method is excluded. +- Exclusion reasons are visible in preview. +``` + +--- + +## COM-005: Extract email channel + +### Description + +Move email sending behind `EmailChannelContract`. + +### Acceptance Criteria + +```text +- Controllers no longer call Mail directly. +- Email sends create message and recipient logs. +- Idempotency prevents duplicate send. +- Preferences are enforced. +``` + +--- + +## COM-006: Standardize bulk send flow + +### Description + +Create single bulk send orchestration path using preview snapshot. + +### Acceptance Criteria + +```text +- Requires preview_id and confirmation_token. +- Uses preview snapshot. +- Returns sent/excluded/failed counts. +- Uses idempotency. +``` + +--- + +## COM-007: Extract SMS and WhatsApp adapters + +### Description + +Move SMS/WhatsApp delivery behind channel contracts. + +### Acceptance Criteria + +```text +- Provider SDK calls are isolated. +- Phone normalization exists. +- Provider failures are normalized. +- Delivery attempts are logged. +``` + +--- + +## COM-008: Add template service + +### Description + +Create reusable versioned message template service. + +### Acceptance Criteria + +```text +- Templates are versioned. +- Variables are validated. +- Rendered message snapshot is logged. +- Unsafe content is sanitized or rejected. +``` + +--- + +## COM-009: Add delivery status and retry handling + +### Description + +Track delivery status and retry transient failures. + +### Acceptance Criteria + +```text +- Delivery receipts update status. +- Retryable failures schedule retry. +- Non-retryable failures do not retry. +- Retry limit is enforced. +``` + +--- + +## COM-010: Add Islamic Sunday School communication extensions + +### Description + +Add Islamic Sunday School recipient resolvers, templates, and preference policy. + +### Acceptance Criteria + +```text +- Islamic Sunday School resolvers register through provider tags. +- Core does not import IslamicSundaySchool classes. +- Islamic Sunday School recipients are school-scoped. +- Islamic Sunday School templates are extension-owned. +``` + +--- + +## COM-011: Add communication architecture tests + +### Description + +Enforce communication module boundaries in CI. + +### Acceptance Criteria + +```text +- SchoolCore\Communication cannot import IslamicSundaySchool. +- Communication services cannot call auth() or request(). +- Controllers cannot call provider SDKs directly. +- Bulk send cannot bypass preview. +- CI fails on boundary violation. +``` + +--- + +## 35. Definition of Done for Phase 6 + +Phase 6 is done when: + +```text +- Communication core contracts exist. +- Bulk communication requires recipient preview. +- Recipient resolution is modular. +- Recipients are deduped. +- Communication preferences are enforced. +- Email delivery uses channel contract. +- SMS/WhatsApp delivery uses channel contracts or adapter boundaries. +- Bulk send uses preview snapshot, confirmation token, and idempotency. +- Message, recipient, and delivery attempt logs exist. +- Delivery retry behavior is deterministic. +- Templates are versioned and rendered safely. +- Islamic Sunday School recipient behavior is implemented through resolvers/policies/templates. +- Communication services require SchoolContext. +- Cross-school recipient leakage is blocked by tests. +- Architecture tests prevent SchoolCore from depending on IslamicSundaySchool. +- Legacy communication methods are labeled canonical, adapter, deprecated, or remove. +``` + +--- + +## 36. Non-Goals + +Do not include in Phase 6: + +```text +- full marketing campaign system +- advanced newsletter builder +- CRM replacement +- donation campaign tooling +- social media posting +- AI message generation +- complete parent portal redesign +- full emergency alert compliance program +``` + +Those may come later. Phase 6 is about making communication safe, reusable, previewable, and auditable. + +--- + +## 37. Main Risks + +### Risk: accidental mass send + +Mitigation: + +```text +- mandatory preview +- confirmation token +- recipient snapshot +- authorization tests +``` + +### Risk: duplicate recipients + +Mitigation: + +```text +- dedupe service +- recipient fingerprint +- preview counts +``` + +### Risk: cross-school leakage + +Mitigation: + +```text +- mandatory SchoolContext +- school-scoped resolvers +- architecture tests +``` + +### Risk: preference violations + +Mitigation: + +```text +- preference policy +- exclusion reasons +- emergency override audit +``` + +### Risk: provider chaos + +Mitigation: + +```text +- channel adapters +- normalized delivery results +- retry policy +- delivery logs +``` + +### Risk: Islamic Sunday School leakage into core + +Mitigation: + +```text +- extension resolvers +- template catalog outside core +- vocabulary architecture tests +``` + +--- + +## 38. Immediate Next Work Queue + +Start here: + +```text +1. COM-001 Create communication module scaffold +2. COM-002 Add recipient preview service +3. COM-003 Add recipient resolver registry +4. COM-004 Add recipient dedupe service and preference policy +5. COM-005 Extract email channel +6. COM-006 Standardize bulk send flow +7. COM-007 Extract SMS and WhatsApp adapters +8. COM-008 Add template service +9. COM-009 Add delivery status and retry handling +10. COM-010 Add Islamic Sunday School communication extensions +11. COM-011 Add communication architecture tests +``` + +Do not start by making nicer templates. A beautiful message sent to the wrong people is still a privacy incident with better typography. diff --git a/docs/modular_plans/phase_7_modular_reporting_execution_plan_islamic.md b/docs/modular_plans/phase_7_modular_reporting_execution_plan_islamic.md new file mode 100644 index 00000000..3a575280 --- /dev/null +++ b/docs/modular_plans/phase_7_modular_reporting_execution_plan_islamic.md @@ -0,0 +1,2187 @@ +# Phase 7 Execution Plan: Modular Reporting Extraction + +## 1. Purpose + +Phase 7 extracts reporting into a reusable `SchoolCore` module that can support any school system while allowing **Islamic Sunday School**-specific reports through extension read models, report builders, labels, and export templates. + +The reporting module must own the invariant parts of reporting: + +- report definitions +- report authorization +- report input validation +- read models +- query objects +- school-scoped report queries +- export generation +- report snapshots +- report audit logs +- scheduled reports +- report caching rules +- cross-school isolation +- consistent calculations sourced from domain modules + +Islamic Sunday School may customize: + +- Qur’an progress reports +- Arabic level reports +- Islamic studies progress reports +- halaqa/group reports +- Sunday program attendance reports +- masjid family/community reports +- volunteer/program team reports +- scholarship/sadaqah-supported fee reports, if applicable +- Islamic Sunday School dashboard labels + +Reporting must not become the place where business rules go to escape tests. Reports should display truth produced by services, policies, and read models. They should not invent finance balances, attendance counts, promotion rules, or student statuses with random raw SQL and optimism. + +--- + +## 2. Required Dependency Direction + +Reporting must follow the modular platform boundary: + +```text +App\Domain\IslamicSundaySchool\Reporting + ↓ depends on +App\Domain\SchoolCore\Reporting + ↓ depends on +App\Domain\SchoolCore\Shared +``` + +Allowed reporting dependencies: + +```text +SchoolCore\Reporting -> SchoolCore\Students read models/contracts +SchoolCore\Reporting -> SchoolCore\Attendance read models/contracts +SchoolCore\Reporting -> SchoolCore\Finance read models/contracts +SchoolCore\Reporting -> SchoolCore\Communication read models/contracts +SchoolCore\Reporting -> SchoolCore\Shared context/audit/export contracts +IslamicSundaySchool\Reporting -> SchoolCore\Reporting contracts +``` + +Forbidden dependencies: + +```text +SchoolCore\Reporting -> IslamicSundaySchool\* +SchoolCore\Reporting -> Http\Request +SchoolCore\Reporting -> auth() +SchoolCore\Reporting -> controller classes +SchoolCore\Reporting -> Islamic Sunday School vocabulary +SchoolCore\Reporting -> direct unscoped school queries +SchoolCore\Reporting -> mutation services for write-side behavior +``` + +Reporting may consume read-side contracts. It must not call mutation services to “calculate” state by causing side effects. Yes, that has happened in real systems. Humanity has been unsupervised near databases for too long. + +--- + +## 3. Target Namespace Structure + +Create the reporting module under `SchoolCore`: + +```text +app/ + Domain/ + SchoolCore/ + Reporting/ + Contracts/ + ReportRegistryContract.php + ReportDefinitionContract.php + ReportRunnerContract.php + ReportAuthorizationPolicyContract.php + ReportExportServiceContract.php + ReportSnapshotServiceContract.php + ScheduledReportServiceContract.php + ReportAuditLoggerContract.php + ReportCacheServiceContract.php + DTO/ + ReportRequestData.php + ReportFilterData.php + ReportColumnData.php + ReportSortData.php + ReportResultData.php + ReportExportData.php + ReportSnapshotData.php + ScheduledReportData.php + ReportAuditData.php + Enums/ + ReportCategory.php + ReportFormat.php + ReportStatus.php + ReportVisibility.php + ReportAuditAction.php + ReportScheduleFrequency.php + Exceptions/ + ReportingException.php + ReportNotFoundException.php + UnauthorizedReportAccessException.php + InvalidReportFilterException.php + ReportExportException.php + ReportTimeoutException.php + Services/ + ReportRegistry.php + ReportRunner.php + ReportExportService.php + ReportSnapshotService.php + ScheduledReportService.php + ReportAuditLogger.php + ReportCacheService.php + Reports/ + Students/ + StudentRosterReport.php + StudentEnrollmentReport.php + GuardianContactReport.php + Attendance/ + AttendanceSummaryReport.php + StudentAttendanceHistoryReport.php + StaffAttendanceSummaryReport.php + Finance/ + InvoiceSummaryReport.php + OutstandingBalanceReport.php + PaymentSummaryReport.php + Communication/ + MessageDeliveryReport.php + RecipientEngagementReport.php + ReadModels/ + StudentReportReadModel.php + AttendanceReportReadModel.php + FinanceReportReadModel.php + CommunicationReportReadModel.php + Repositories/ + ReportReadRepository.php + ReportSnapshotRepository.php + ScheduledReportRepository.php + Support/ + ReportQueryBuilder.php + ReportColumnFormatter.php + ReportFilterValidator.php + ReportExportFilenameBuilder.php + ReportContext.php +``` + +Islamic Sunday School-specific reporting goes here: + +```text +app/ + Domain/ + IslamicSundaySchool/ + Reporting/ + Reports/ + QuranProgressReport.php + ArabicLevelReport.php + IslamicStudiesProgressReport.php + HalaqaAttendanceReport.php + SundayProgramAttendanceReport.php + MasjidFamilySummaryReport.php + VolunteerTeamReport.php + IslamicSundaySchoolDashboardReport.php + ReadModels/ + QuranProgressReadModel.php + ArabicProgressReadModel.php + IslamicStudiesProgressReadModel.php + HalaqaReportReadModel.php + MasjidFamilyReportReadModel.php + ExportTemplates/ + QuranProgressExportTemplate.php + FamilySummaryExportTemplate.php + Providers/ + IslamicSundaySchoolReportingServiceProvider.php +``` + +--- + +## 4. Core Design Rules + +### 4.1 Reporting services must require `SchoolContext` + +Every public reporting method must receive `SchoolContext`. + +Example: + +```php +public function run( + SchoolContext $context, + ReportRequestData $data +): ReportResult; +``` + +A report without `SchoolContext` is invalid. A report without school scoping is a data leak wearing a chart. + +### 4.2 Reports must be read-only + +Reports may: + +```text +- query read models +- call read-only repositories +- format output +- export files +- create report snapshots +- create report audit logs +``` + +Reports may not: + +```text +- mutate invoices +- recalculate balances directly +- modify attendance records +- promote students +- update communication status +- create domain records +``` + +If a report needs a calculated value, it must come from a domain read model or calculation service owned by that domain. + +### 4.3 Reports must not duplicate business logic + +Reporting must consume: + +```text +Finance BalanceCalculator/read model +Attendance summary read model +Student lifecycle read model +Communication delivery read model +``` + +Forbidden: + +```text +A report writing its own invoice balance calculation +A report counting attendance differently from AttendanceService +A report deciding promotion eligibility +A report inferring communication delivery status from provider payloads directly +``` + +That is how dashboards become folklore. + +### 4.4 Islamic Sunday School reports must live in extension layer + +Core reports may use neutral concepts: + +```text +student +guardian +household +enrollment +assignment +attendance session +finance balance +communication delivery +program +group +level +``` + +Islamic Sunday School reports may use extension concepts: + +```text +Qur'an level +Arabic level +Islamic studies track +halaqa +masjid family/community +Sunday program +volunteer team +``` + +Core must not import or hardcode those concepts. + +### 4.5 Report exports must be auditable + +Every export must log: + +```text +who exported +what report +filters used +record count +format +timestamp +school context +``` + +Exports often contain sensitive student, guardian, finance, or attendance data. Treat exports like controlled data access, not “just a CSV,” because CSV files are how private data escapes wearing business casual. + +--- + +## 5. Core Contracts + +### 5.1 `ReportRegistryContract` + +```php +namespace App\Domain\SchoolCore\Reporting\Contracts; + +use App\Domain\SchoolCore\Shared\Context\SchoolContext; + +interface ReportRegistryContract +{ + /** + * @return ReportDefinitionContract[] + */ + public function availableReports(SchoolContext $context): array; + + public function get(string $reportKey): ReportDefinitionContract; + + public function register(ReportDefinitionContract $definition): void; +} +``` + +### 5.2 `ReportDefinitionContract` + +```php +namespace App\Domain\SchoolCore\Reporting\Contracts; + +interface ReportDefinitionContract +{ + public function key(): string; + + public function name(): string; + + public function category(): ReportCategory; + + public function filters(): array; + + public function columns(): array; + + public function supportedFormats(): array; + + public function visibility(): ReportVisibility; +} +``` + +### 5.3 `ReportRunnerContract` + +```php +namespace App\Domain\SchoolCore\Reporting\Contracts; + +use App\Domain\SchoolCore\Shared\Context\SchoolContext; +use App\Domain\SchoolCore\Reporting\DTO\ReportRequestData; + +interface ReportRunnerContract +{ + public function run(SchoolContext $context, ReportRequestData $data): ReportResult; +} +``` + +### 5.4 `ReportAuthorizationPolicyContract` + +```php +namespace App\Domain\SchoolCore\Reporting\Contracts; + +use App\Domain\SchoolCore\Shared\Context\SchoolContext; + +interface ReportAuthorizationPolicyContract +{ + public function canViewReport( + SchoolContext $context, + ReportDefinitionContract $report + ): bool; + + public function canExportReport( + SchoolContext $context, + ReportDefinitionContract $report + ): bool; + + public function filterAllowedColumns( + SchoolContext $context, + ReportDefinitionContract $report, + array $columns + ): array; +} +``` + +### 5.5 `ReportExportServiceContract` + +```php +namespace App\Domain\SchoolCore\Reporting\Contracts; + +use App\Domain\SchoolCore\Shared\Context\SchoolContext; +use App\Domain\SchoolCore\Reporting\DTO\ReportExportData; + +interface ReportExportServiceContract +{ + public function export(SchoolContext $context, ReportExportData $data): ReportExportResult; +} +``` + +### 5.6 `ReportSnapshotServiceContract` + +```php +namespace App\Domain\SchoolCore\Reporting\Contracts; + +use App\Domain\SchoolCore\Shared\Context\SchoolContext; + +interface ReportSnapshotServiceContract +{ + public function createSnapshot( + SchoolContext $context, + ReportResult $result + ): ReportSnapshotResult; + + public function getSnapshot( + SchoolContext $context, + string $snapshotId + ): ReportSnapshotView; +} +``` + +### 5.7 `ScheduledReportServiceContract` + +```php +namespace App\Domain\SchoolCore\Reporting\Contracts; + +use App\Domain\SchoolCore\Shared\Context\SchoolContext; +use App\Domain\SchoolCore\Reporting\DTO\ScheduledReportData; + +interface ScheduledReportServiceContract +{ + public function schedule(SchoolContext $context, ScheduledReportData $data): ScheduledReportResult; + + public function cancel(SchoolContext $context, int $scheduleId): ScheduledReportResult; +} +``` + +--- + +## 6. DTO Requirements + +Do not pass raw request arrays into reporting services. + +### 6.1 `ReportRequestData` + +Fields: + +```text +report_key +filters +columns, nullable +sorts, nullable +page, nullable +per_page, nullable +include_totals, default false +timezone, nullable +locale, nullable +snapshot, default false +metadata, nullable +``` + +### 6.2 `ReportFilterData` + +Fields: + +```text +field +operator +value +value_to, nullable +required, bool +``` + +Supported operators should be explicit: + +```text +equals +not_equals +in +not_in +between +date_between +contains +starts_with +is_null +is_not_null +``` + +### 6.3 `ReportExportData` + +Fields: + +```text +report_key +filters +columns, nullable +format +snapshot_id, nullable +filename, nullable +include_sensitive_columns, default false +idempotency_key, nullable +metadata, nullable +``` + +### 6.4 `ScheduledReportData` + +Fields: + +```text +report_key +filters +columns, nullable +format +frequency +run_at +recipient_user_ids +recipient_emails, nullable if policy allows +expires_at, nullable +metadata, nullable +``` + +### 6.5 `ReportResultData` + +Fields: + +```text +report_key +columns +rows +totals, nullable +page, nullable +per_page, nullable +total_rows, nullable +generated_at +snapshot_id, nullable +warnings, nullable array +``` + +--- + +## 7. Report Categories + +Core report categories: + +```text +students +guardians +enrollment +attendance +finance +communication +staff +operations +audit +``` + +Islamic Sunday School extension categories: + +```text +quran +arabic +islamic_studies +halaqa +program_attendance +masjid_family +volunteer_programs +``` + +These extension categories may appear in the registry only when the `islamic_sunday_school` domain profile is active or when the extension provider is installed and authorized. + +--- + +## 8. Report Registry Strategy + +### 8.1 Core registry + +The registry must support: + +```text +core reports +extension reports +visibility filtering +authorization filtering +school/domain profile filtering +``` + +### 8.2 Registration flow + +Core provider registers core reports: + +```php +$this->app->tag([ + StudentRosterReport::class, + AttendanceSummaryReport::class, + InvoiceSummaryReport::class, + MessageDeliveryReport::class, +], 'schoolcore.reports'); +``` + +Islamic Sunday School provider registers extension reports: + +```php +$this->app->tag([ + QuranProgressReport::class, + ArabicLevelReport::class, + IslamicStudiesProgressReport::class, + HalaqaAttendanceReport::class, + MasjidFamilySummaryReport::class, +], 'schoolcore.reports'); +``` + +### 8.3 Registry rules + +```text +- Report keys must be globally unique. +- Extension report keys should be prefixed. +- Report definitions must declare required permissions. +- Registry must filter unavailable extension reports by domain profile. +``` + +Example keys: + +```text +students.roster +attendance.summary +finance.outstanding_balance +communication.delivery +islamic.quran_progress +islamic.arabic_level +islamic.halaqa_attendance +islamic.masjid_family_summary +``` + +--- + +## 9. Read Model Strategy + +Reports should query read models instead of raw operational tables wherever practical. + +### 9.1 Core read models + +Required read model contracts or repositories: + +```text +StudentReportReadModel +AttendanceReportReadModel +FinanceReportReadModel +CommunicationReportReadModel +StaffReportReadModel +``` + +### 9.2 Extension read models + +Islamic Sunday School read models: + +```text +QuranProgressReadModel +ArabicProgressReadModel +IslamicStudiesProgressReadModel +HalaqaReportReadModel +MasjidFamilyReportReadModel +``` + +### 9.3 Read model rules + +```text +- every query must include school_id from SchoolContext +- every date filter must respect context timezone +- sensitive columns require explicit authorization +- reports must not join extension tables unless inside extension report/read model +- totals must match owning domain calculation services +``` + +--- + +## 10. Core Reports + +### 10.1 Student roster report + +Purpose: + +```text +List active students by school, enrollment, assignment, guardian summary, and status. +``` + +Must support filters: + +```text +academic_year +term +program +group/class +status +enrollment_status +age range +guardian contact availability +``` + +Must not include by default: + +```text +sensitive notes +medical data +religious extension fields +finance balance +``` + +### 10.2 Guardian contact report + +Purpose: + +```text +List guardian contact information according to authorization and communication policy. +``` + +Required protections: + +```text +mask contact fields where policy requires +respect guardian relationship visibility +exclude restricted contacts +audit exports +``` + +### 10.3 Attendance summary report + +Purpose: + +```text +Show attendance counts and rates by session/date/group/student/staff. +``` + +Must consume: + +```text +SchoolCore\Attendance read model +``` + +Must not: + +```text +recalculate duplicate scan behavior +invent status mappings +count raw scan logs as attendance records +``` + +### 10.4 Finance outstanding balance report + +Purpose: + +```text +Show outstanding balances by student/guardian/invoice. +``` + +Must consume: + +```text +SchoolCore\Finance read model or BalanceCalculator output +``` + +Must not: + +```text +calculate balances using ad hoc SQL +ignore refunds/reversals +show finance fields to unauthorized actors +``` + +### 10.5 Communication delivery report + +Purpose: + +```text +Show message delivery status, excluded recipients, failed deliveries, and retry state. +``` + +Must consume: + +```text +SchoolCore\Communication delivery read model +``` + +Must not: + +```text +query provider payloads directly outside the delivery status model +``` + +--- + +## 11. Islamic Sunday School Reports + +### 11.1 Qur’an progress report + +Purpose: + +```text +Track Qur’an level, memorization progress summary, recitation placement, teacher/group, and progress status. +``` + +Rules: + +```text +- lives in IslamicSundaySchool\Reporting +- reads from IslamicSundaySchool student/curriculum read models +- must be school-scoped +- sensitive notes require explicit permission +- may join core student/enrollment read models through contracts +``` + +### 11.2 Arabic level report + +Purpose: + +```text +Track Arabic level placement, progress, teacher/group, and placement changes. +``` + +Rules: + +```text +- core only knows neutral level/program concepts +- extension maps Arabic-specific levels and labels +``` + +### 11.3 Islamic studies progress report + +Purpose: + +```text +Track Islamic studies track, class/group, completion status, and teacher evaluation summary. +``` + +Rules: + +```text +- no religious-specific concepts in SchoolCore +- extension owns labels and calculations +``` + +### 11.4 Halaqa attendance report + +Purpose: + +```text +Show attendance by halaqa/group/session for Islamic Sunday School programs. +``` + +Rules: + +```text +- attendance facts come from SchoolCore\Attendance +- halaqa/group mapping comes from IslamicSundaySchool extension +- duplicate scans must not inflate counts +``` + +### 11.5 Masjid family/community summary report + +Purpose: + +```text +Summarize students and guardians by masjid family/community profile, household, volunteer participation, and program enrollment. +``` + +Rules: + +```text +- contact information must obey guardian/communication policies +- sensitive family/community notes require strict permission +``` + +### 11.6 Volunteer team report + +Purpose: + +```text +Show volunteer/program team members, assigned sessions, attendance, and communication status. +``` + +Rules: + +```text +- volunteers should map to neutral staff/volunteer subject types in core +- extension owns Islamic Sunday School labels and grouping +``` + +--- + +## 12. Report Authorization + +Minimum report permissions: + +```text +report.view +report.export +report.schedule +report.audit.view +report.students.view +report.guardians.view +report.attendance.view +report.finance.view +report.communication.view +report.staff.view +report.sensitive.view +``` + +Islamic Sunday School extension permissions: + +```text +report.islamic.quran.view +report.islamic.arabic.view +report.islamic.studies.view +report.islamic.halaqa.view +report.islamic.masjid_family.view +report.islamic.volunteer.view +report.islamic.sensitive_notes.view +``` + +Every report must define: + +```text +required permission +allowed roles +sensitive columns +export permission +school/domain profile availability +``` + +Every report endpoint must test: + +```text +unauthenticated +wrong school +wrong role +wrong domain profile +allowed admin +allowed staff +teacher assigned-scope view, if supported +guardian own-student view, if supported +export denied when view-only +``` + +--- + +## 13. Sensitive Data Rules + +Reports may expose sensitive data accidentally because tables are easy to join and humans are easy to disappoint. + +Sensitive categories: + +```text +student personal data +guardian contact data +finance balances/payments +attendance history +communication delivery logs +sensitive imam/admin notes +scholarship/sadaqah support indicators +discipline/behavior notes, if present +medical data, if ever supported +``` + +Rules: + +```text +- sensitive columns require explicit permission +- export requires stronger permission than view +- exports must be audited +- reports should mask fields by default where practical +- snapshots containing sensitive fields must have retention rules +``` + +--- + +## 14. Export Strategy + +Supported formats: + +```text +csv +xlsx +pdf, optional +json, internal/admin only if allowed +``` + +Export requirements: + +```text +- use ReportExportServiceContract +- validate format against report definition +- include generated_at and school context metadata +- include filters used +- use safe filenames +- store or stream based on policy +- audit every export +``` + +Export filename format: + +```text +{school_slug}_{report_key}_{date}_{snapshot_id}.{format} +``` + +Example: + +```text +al-noor_islamic.quran_progress_2026-05-29_ab12cd.xlsx +``` + +### 14.1 Export retention + +Define per report: + +```text +not stored / streamed only +stored for 24 hours +stored for 7 days +stored until manually deleted +``` + +Sensitive exports should default to short retention. + +--- + +## 15. Report Snapshots + +### 15.1 Why snapshots exist + +Snapshots preserve what the user saw at the time of generation. + +Required for: + +```text +bulk communication recipient previews +finance reports used for reconciliation +attendance reports used for official records +scheduled reports +exports +``` + +### 15.2 Snapshot fields + +Suggested table: + +```text +report_snapshots +``` + +Fields: + +```text +id +school_id +actor_user_id +report_key +filters_json +columns_json +result_summary_json +snapshot_storage_path, nullable +sensitive_data_flag +expires_at, nullable +created_at +``` + +### 15.3 Snapshot rules + +```text +- snapshots must be school-scoped +- sensitive snapshots require retention policy +- snapshot access must re-check authorization +- snapshots should not bypass current permission rules +``` + +--- + +## 16. Scheduled Reports + +Scheduled reports are useful and dangerous, the perfect combination for a feature request. + +Required behavior: + +```text +- schedule owner must have report.schedule permission +- recipients must be authorized to receive the report +- every scheduled run resolves fresh SchoolContext +- every scheduled run creates audit log +- delivery uses Communication module where possible +- sensitive reports require explicit recipient validation +``` + +Do not email finance or student-sensitive exports to arbitrary addresses without policy approval. + +--- + +## 17. Caching Strategy + +Reports may be cached only when safe. + +Cache key must include: + +```text +school_id +academic_year_id +term_id, nullable +actor permission scope +report_key +filters +columns +locale +timezone +``` + +Rules: + +```text +- do not share cached sensitive reports across actors unless policy allows +- invalidate finance reports after finance mutation +- invalidate attendance reports after attendance mutation +- invalidate student reports after lifecycle mutation +- invalidate communication reports after delivery status update +``` + +If cache correctness is hard to prove, do not cache. Slow truth beats fast lies, a lesson dashboards learn only after leadership screenshots them. + +--- + +## 18. Audit Logging Requirements + +Every report action must create an audit event. + +Required audit fields: + +```text +school_id +academic_year_id, nullable +term_id, nullable +actor_user_id +actor_role_snapshot +action +report_key +entity_type, nullable +entity_id, nullable +filters_json, nullable +columns_json, nullable +format, nullable +row_count, nullable +sensitive_data_flag +request_id +ip_address, nullable +user_agent, nullable +created_at +``` + +Required actions: + +```text +report.viewed +report.exported +report.snapshot.created +report.snapshot.viewed +report.scheduled +report.schedule.cancelled +report.schedule.failed +``` + +Audit logs are append-only. + +--- + +## 19. FormRequest Requirements + +Create or update: + +```text +RunReportRequest +ExportReportRequest +ScheduleReportRequest +CancelScheduledReportRequest +ViewReportSnapshotRequest +ListReportsRequest +``` + +Each FormRequest must define: + +```text +authorize() +rules() +messages(), if needed +toDTO(), optional but recommended +``` + +Controllers should use: + +```php +$data = $request->toDTO(); +``` + +Not raw request array merging, because apparently arrays with no boundaries are still the default foot-gun. + +--- + +## 20. Controller Target Shape + +Target flow: + +```text +ReportController + -> RunReportRequest + -> SchoolContext + -> ReportRequestData + -> ReportRunnerContract::run() + -> ReportResource +``` + +Export flow: + +```text +ReportExportController + -> ExportReportRequest + -> SchoolContext + -> ReportExportData + -> ReportExportServiceContract::export() + -> file response or export resource +``` + +Controllers may not: + +```text +- build raw report SQL +- authorize only by logged-in status +- expose all columns by default +- export without audit +- call extension reports directly by concrete class +``` + +--- + +## 21. Migration Strategy + +### 21.1 Use vertical slices + +Recommended order: + +```text +1. Report registry and definitions +2. Report authorization policy +3. Student roster report +4. Attendance summary report +5. Finance outstanding balance report +6. Communication delivery report +7. Export service +8. Report snapshots +9. Islamic Sunday School extension reports +10. Scheduled reports and caching +``` + +Why this order: + +```text +- registry creates the seam +- authorization prevents report leaks +- student/attendance/finance/communication reports validate read-model integration +- exports need audit and retention before broad use +- Islamic reports should extend proven core mechanisms +- scheduling/caching comes last because stale scheduled sensitive exports are a very stupid way to create an incident +``` + +### 21.2 Compatibility layer + +Existing report controllers may remain temporarily, but must delegate to new contracts. + +Example: + +```text +LegacyReportController + -> RunReportRequest + -> SchoolContext + -> ReportRunnerContract +``` + +Legacy report methods must be labeled: + +```text +canonical +adapter +deprecated +remove +``` + +No unlabeled duplicate report logic. + +--- + +## 22. First Implementation Slice: Report Registry + +### Objective + +Create the report registry and define report metadata. + +### Tasks + +```text +REP-001. Create Reporting module scaffold. +REP-002. Create ReportDefinitionContract. +REP-003. Create ReportRegistryContract. +REP-004. Implement ReportRegistry. +REP-005. Add report provider tagging. +REP-006. Register initial core report definitions. +REP-007. Add tests for registry lookup and availability filtering. +``` + +### Required tests + +```text +- lists available reports +- returns report by key +- rejects unknown report key +- filters reports by permission +- filters extension reports by domain profile +- report keys are unique +``` + +### Done when + +```text +- reports are discoverable through registry +- Islamic Sunday School reports can register without core imports +``` + +--- + +## 23. Second Implementation Slice: Report Authorization + +### Objective + +Prevent report data leakage before adding more reports. + +### Tasks + +```text +REP-008. Create ReportAuthorizationPolicyContract. +REP-009. Implement default policy. +REP-010. Add column-level authorization. +REP-011. Add export-specific authorization. +REP-012. Add sensitive data flagging. +``` + +### Required tests + +```text +- unauthenticated user cannot view reports +- wrong-school user cannot view report +- view-only user cannot export +- sensitive columns are removed without permission +- Islamic report unavailable outside Islamic Sunday School profile +``` + +### Done when + +```text +- every report requires explicit permission +- export permission is separate from view permission +``` + +--- + +## 24. Third Implementation Slice: Student Roster Report + +### Objective + +Create a reusable core student roster report. + +### Tasks + +```text +REP-013. Create StudentReportReadModel. +REP-014. Create StudentRosterReport. +REP-015. Add filters for status, enrollment, group, academic year, term. +REP-016. Add report result resource. +REP-017. Add tests for school scoping and field permissions. +``` + +### Required tests + +```text +- returns active students in school +- excludes other schools +- filters by enrollment status +- filters by group/class +- guardian contact fields follow permission policy +- archived students excluded by default +``` + +### Done when + +```text +- student roster report uses read model +- no student lifecycle rules are duplicated in the report +``` + +--- + +## 25. Fourth Implementation Slice: Attendance Summary Report + +### Objective + +Create core attendance reporting from attendance read models. + +### Tasks + +```text +REP-018. Create AttendanceReportReadModel. +REP-019. Create AttendanceSummaryReport. +REP-020. Add filters by date range, session, group, subject type. +REP-021. Ensure duplicate scan logs do not inflate attendance. +REP-022. Add tests against AttendanceService/read-model truth. +``` + +### Required tests + +```text +- attendance count matches Attendance module +- duplicate scans are not double-counted +- filters by session/date range +- excludes other schools +- status counts use canonical statuses +``` + +### Done when + +```text +- attendance reports do not query raw scan logs as source of truth +``` + +--- + +## 26. Fifth Implementation Slice: Finance Outstanding Balance Report + +### Objective + +Create core finance reporting from finance read models. + +### Tasks + +```text +REP-023. Create FinanceReportReadModel. +REP-024. Create OutstandingBalanceReport. +REP-025. Pull balances from Finance read model/BalanceCalculator output. +REP-026. Add filters by student, guardian, status, date range. +REP-027. Add export-sensitive authorization. +``` + +### Required tests + +```text +- balance matches Finance module calculation +- refunds/reversals are reflected correctly +- finance data hidden from unauthorized role +- excludes other schools +- export is audited +``` + +### Done when + +```text +- finance report does not duplicate invoice balance logic +``` + +--- + +## 27. Sixth Implementation Slice: Communication Delivery Report + +### Objective + +Create core communication delivery reporting. + +### Tasks + +```text +REP-028. Create CommunicationReportReadModel. +REP-029. Create MessageDeliveryReport. +REP-030. Add filters by channel, status, category, date range. +REP-031. Include excluded recipient counts. +REP-032. Add delivery failure summaries. +``` + +### Required tests + +```text +- shows sent/delivered/failed/excluded counts +- filters by channel +- excludes other schools +- hides recipient details without permission +- matches Communication module delivery logs +``` + +### Done when + +```text +- delivery report consumes Communication read model +``` + +--- + +## 28. Seventh Implementation Slice: Export Service + +### Objective + +Create safe, auditable report exports. + +### Tasks + +```text +REP-033. Create ReportExportServiceContract. +REP-034. Implement CSV export. +REP-035. Implement XLSX export if required. +REP-036. Add safe filename builder. +REP-037. Add export audit logs. +REP-038. Add retention policy. +``` + +### Required tests + +```text +- exports allowed report +- rejects unauthorized export +- rejects unsupported format +- masks/excludes sensitive columns +- writes report.exported audit log +- generated file name is safe +``` + +### Done when + +```text +- exports go through ReportExportService +- export access is audited +``` + +--- + +## 29. Eighth Implementation Slice: Report Snapshots + +### Objective + +Preserve report results for exports, audits, and scheduled reports. + +### Tasks + +```text +REP-039. Create ReportSnapshotServiceContract. +REP-040. Create report_snapshots table/model. +REP-041. Store snapshot metadata and result reference. +REP-042. Add snapshot authorization. +REP-043. Add retention behavior. +``` + +### Required tests + +```text +- creates snapshot +- retrieves snapshot in same school +- rejects snapshot from another school +- re-checks authorization on access +- expires snapshot according to policy +``` + +### Done when + +```text +- report snapshots are school-scoped and permission-checked +``` + +--- + +## 30. Ninth Implementation Slice: Islamic Sunday School Reports + +### Objective + +Add Islamic Sunday School-specific reporting through extension reports and read models. + +### Tasks + +```text +REP-044. Create IslamicSundaySchool Reporting namespace. +REP-045. Create QuranProgressReport. +REP-046. Create ArabicLevelReport. +REP-047. Create IslamicStudiesProgressReport. +REP-048. Create HalaqaAttendanceReport. +REP-049. Create MasjidFamilySummaryReport. +REP-050. Register reports through IslamicSundaySchoolReportingServiceProvider. +``` + +### Required tests + +```text +- Islamic reports only available for islamic_sunday_school profile +- Qur'an report excludes other schools +- Arabic report uses extension read model +- Halaqa attendance report uses Attendance read model facts +- Masjid family report respects contact permissions +- SchoolCore does not import IslamicSundaySchool reporting classes +``` + +### Done when + +```text +- Islamic Sunday School reports extend registry without core changes +``` + +--- + +## 31. Tenth Implementation Slice: Scheduled Reports and Caching + +### Objective + +Add scheduled report support and safe caching after core reporting is stable. + +### Tasks + +```text +REP-051. Create ScheduledReportServiceContract. +REP-052. Create scheduled_reports table/model. +REP-053. Add scheduled report job. +REP-054. Add authorized recipient validation. +REP-055. Add ReportCacheServiceContract. +REP-056. Add cache invalidation hooks. +``` + +### Required tests + +```text +- schedules allowed report +- rejects unauthorized schedule +- scheduled run resolves SchoolContext +- scheduled run creates audit log +- sensitive report cannot be sent to unauthorized recipient +- cache key includes school and permission scope +``` + +### Done when + +```text +- scheduled reports are safe enough to exist +- caching cannot cross permission/school boundaries +``` + +--- + +## 32. Database and Schema Requirements + +### 32.1 Required indexes + +Add or verify: + +```text +report_snapshots.school_id indexed +report_snapshots.actor_user_id indexed +report_snapshots.report_key indexed +report_snapshots.expires_at indexed +report_snapshots.created_at indexed + +scheduled_reports.school_id indexed +scheduled_reports.actor_user_id indexed +scheduled_reports.report_key indexed +scheduled_reports.next_run_at indexed +scheduled_reports.status indexed + +report_audit_logs.school_id indexed +report_audit_logs.actor_user_id indexed +report_audit_logs.report_key indexed +report_audit_logs.created_at indexed +``` + +### 32.2 Recommended unique constraints + +```text +unique(school_id, report_key, actor_user_id, idempotency_key) where idempotency_key is not null +unique(school_id, scheduled_report_name) if user-defined names are supported +``` + +--- + +## 33. Service Provider Bindings + +Create: + +```php +namespace App\Providers; + +use Illuminate\Support\ServiceProvider; +use App\Domain\SchoolCore\Reporting\Contracts\ReportRunnerContract; +use App\Domain\SchoolCore\Reporting\Services\ReportRunner; + +final class SchoolCoreReportingServiceProvider extends ServiceProvider +{ + public function register(): void + { + $this->app->singleton(ReportRegistryContract::class, ReportRegistry::class); + $this->app->bind(ReportRunnerContract::class, ReportRunner::class); + $this->app->bind(ReportAuthorizationPolicyContract::class, DefaultReportAuthorizationPolicy::class); + $this->app->bind(ReportExportServiceContract::class, ReportExportService::class); + $this->app->bind(ReportSnapshotServiceContract::class, ReportSnapshotService::class); + $this->app->bind(ScheduledReportServiceContract::class, ScheduledReportService::class); + $this->app->bind(ReportAuditLoggerContract::class, ReportAuditLogger::class); + $this->app->bind(ReportCacheServiceContract::class, ReportCacheService::class); + + $this->app->tag([ + StudentRosterReport::class, + GuardianContactReport::class, + AttendanceSummaryReport::class, + OutstandingBalanceReport::class, + MessageDeliveryReport::class, + ], 'schoolcore.reports'); + } +} +``` + +Islamic Sunday School provider: + +```php +final class IslamicSundaySchoolReportingServiceProvider extends ServiceProvider +{ + public function register(): void + { + $this->app->tag([ + QuranProgressReport::class, + ArabicLevelReport::class, + IslamicStudiesProgressReport::class, + HalaqaAttendanceReport::class, + MasjidFamilySummaryReport::class, + VolunteerTeamReport::class, + ], 'schoolcore.reports'); + } +} +``` + +--- + +## 34. Architecture Tests + +Add tests that fail if reporting boundaries are violated. + +Required assertions: + +```text +SchoolCore\Reporting must not depend on Domain\IslamicSundaySchool +SchoolCore\Reporting must not depend on Illuminate\Http\Request +SchoolCore\Reporting services must not call auth() +SchoolCore\Reporting services must not call request() +Controllers must not instantiate report concrete classes directly +Report controllers must call ReportRunnerContract or ReportExportServiceContract +Core report contracts must not contain Islamic Sunday School vocabulary +Reports must include SchoolContext +Exports must go through ReportExportServiceContract +``` + +Optional static checks: + +```text +No direct finance balance calculation in reports +No raw attendance scan-log counting as attendance result +No report export without audit log +No scheduled report to arbitrary email without policy +No extension report registered from SchoolCore provider +``` + +--- + +## 35. Rollout Plan + +### Step 1: Add reporting module scaffolding + +```text +- create namespaces +- create contracts +- create DTOs +- create provider +- create registry +``` + +### Step 2: Add report authorization + +```text +- view/export permissions +- sensitive column filtering +- school context enforcement +``` + +### Step 3: Migrate student roster + +```text +- low-risk read-only core report +- proves registry/read model/resource flow +``` + +### Step 4: Migrate attendance summary + +```text +- prove attendance read model integration +- prevent duplicate scan inflation +``` + +### Step 5: Migrate finance outstanding balance + +```text +- prove finance read model integration +- protect sensitive finance fields +``` + +### Step 6: Migrate communication delivery + +```text +- prove communication logs/read models +``` + +### Step 7: Add export service + +```text +- CSV/XLSX +- audit +- retention +``` + +### Step 8: Add snapshots + +```text +- stable exports +- audit support +``` + +### Step 9: Add Islamic Sunday School reports + +```text +- Qur'an, Arabic, Islamic studies, halaqa, masjid family/community +- extension-only concepts remain outside core +``` + +### Step 10: Add scheduled reports and caching + +```text +- only after authorization/export behavior is stable +``` + +--- + +## 36. Implementation Tickets + +## REP-001: Create reporting module scaffold + +### Description + +Create the `SchoolCore\Reporting` namespace and initial contracts, DTO folders, report folders, read model folders, services, exceptions, enums, and provider. + +### Acceptance Criteria + +```text +- Namespace exists. +- Service provider is registered. +- Empty contracts compile. +- No controller behavior changes yet. +``` + +--- + +## REP-002: Add report registry + +### Description + +Create report registration and discovery system. + +### Acceptance Criteria + +```text +- Reports can be registered by provider. +- Reports can be fetched by key. +- Duplicate keys fail. +- Islamic Sunday School reports can register through extension provider. +``` + +--- + +## REP-003: Add report authorization policy + +### Description + +Add report view/export/column-level authorization. + +### Acceptance Criteria + +```text +- View and export are separate permissions. +- Sensitive columns are filtered. +- Wrong-school access fails. +- Extension reports filtered by domain profile. +``` + +--- + +## REP-004: Create student roster report + +### Description + +Create first core report using student read model. + +### Acceptance Criteria + +```text +- School-scoped. +- Filters by status/enrollment/group. +- Excludes archived students by default. +- Does not expose restricted guardian data. +``` + +--- + +## REP-005: Create attendance summary report + +### Description + +Create attendance report using attendance read model. + +### Acceptance Criteria + +```text +- Counts match Attendance module. +- Duplicate scans do not inflate counts. +- Canonical attendance statuses are used. +- Cross-school data is excluded. +``` + +--- + +## REP-006: Create finance outstanding balance report + +### Description + +Create finance report using finance read model. + +### Acceptance Criteria + +```text +- Balances match Finance module. +- Refunds/reversals are reflected. +- Unauthorized finance access fails. +- Export is audited. +``` + +--- + +## REP-007: Create communication delivery report + +### Description + +Create delivery report using communication read model. + +### Acceptance Criteria + +```text +- Shows sent/delivered/failed/excluded counts. +- Filters by channel/status/category/date. +- Hides recipient details without permission. +``` + +--- + +## REP-008: Add export service + +### Description + +Create safe CSV/XLSX export flow. + +### Acceptance Criteria + +```text +- Export requires permission. +- Unsupported formats fail. +- Sensitive columns are filtered. +- Export audit log is written. +- Safe filename is generated. +``` + +--- + +## REP-009: Add report snapshots + +### Description + +Create report snapshot storage and access controls. + +### Acceptance Criteria + +```text +- Snapshot is school-scoped. +- Snapshot access re-checks authorization. +- Sensitive snapshots follow retention rule. +``` + +--- + +## REP-010: Add Islamic Sunday School reports + +### Description + +Add Qur'an, Arabic, Islamic studies, halaqa, and masjid family/community reports as extension reports. + +### Acceptance Criteria + +```text +- Reports are registered by IslamicSundaySchool provider. +- Reports only available for islamic_sunday_school profile. +- SchoolCore does not import IslamicSundaySchool classes. +- Sensitive extension fields require explicit permission. +``` + +--- + +## REP-011: Add scheduled reports and caching + +### Description + +Add scheduled report execution and safe caching. + +### Acceptance Criteria + +```text +- Scheduled runs resolve SchoolContext. +- Recipients are authorized. +- Audit log is written. +- Cache key includes school and permission scope. +- Sensitive reports are not shared across actors incorrectly. +``` + +--- + +## REP-012: Add reporting architecture tests + +### Description + +Enforce reporting module boundaries in CI. + +### Acceptance Criteria + +```text +- SchoolCore\Reporting cannot import IslamicSundaySchool. +- Reporting services cannot call auth() or request(). +- Core contracts contain no Islamic Sunday School vocabulary. +- Reports cannot export without audit. +- CI fails on boundary violation. +``` + +--- + +## 37. Definition of Done for Phase 7 + +Phase 7 is done when: + +```text +- Reporting core contracts exist. +- Report registry exists. +- Report authorization policy exists. +- Student roster report uses read model. +- Attendance summary report uses Attendance read model. +- Finance outstanding balance report uses Finance read model. +- Communication delivery report uses Communication read model. +- Export service is auditable and permission-controlled. +- Report snapshots are school-scoped and permission-checked. +- Islamic Sunday School reports live in extension layer. +- Scheduled reports resolve SchoolContext and validate recipients. +- Report caching cannot cross school or permission boundaries. +- Architecture tests prevent SchoolCore from depending on IslamicSundaySchool. +- Legacy report methods are labeled canonical, adapter, deprecated, or remove. +``` + +--- + +## 38. Non-Goals + +Do not include in Phase 7: + +```text +- full BI/data warehouse platform +- drag-and-drop report builder +- public analytics dashboards +- machine learning predictions +- advanced charting library migration +- full PDF design system +- external accounting export suite +- parent-facing report portal redesign +``` + +Those can come later. Phase 7 is about trustworthy, scoped, reusable reporting. Fancy charts can wait until the numbers stop lying. + +--- + +## 39. Main Risks + +### Risk: reports duplicate business logic + +Mitigation: + +```text +- read model contracts +- domain-owned calculation services +- architecture/static checks +``` + +### Risk: cross-school data leakage + +Mitigation: + +```text +- mandatory SchoolContext +- school-scoped read repositories +- wrong-school tests +``` + +### Risk: sensitive export leakage + +Mitigation: + +```text +- export authorization +- sensitive column filtering +- audit logs +- retention policy +``` + +### Risk: Islamic Sunday School leakage into core + +Mitigation: + +```text +- extension reports +- architecture tests +- neutral core vocabulary +``` + +### Risk: stale cached reports + +Mitigation: + +```text +- permission-aware cache keys +- domain mutation invalidation +- disable caching when correctness is unclear +``` + +### Risk: scheduled reports sent to wrong recipients + +Mitigation: + +```text +- recipient authorization validation +- Communication module integration +- audit every scheduled run +``` + +--- + +## 40. Immediate Next Work Queue + +Start here: + +```text +1. REP-001 Create reporting module scaffold +2. REP-002 Add report registry +3. REP-003 Add report authorization policy +4. REP-004 Create student roster report +5. REP-005 Create attendance summary report +6. REP-006 Create finance outstanding balance report +7. REP-007 Create communication delivery report +8. REP-008 Add export service +9. REP-009 Add report snapshots +10. REP-010 Add Islamic Sunday School reports +11. REP-012 Add reporting architecture tests +12. REP-011 Add scheduled reports and caching +``` + +Do not start with PDF styling, charts, dashboards, or executive summary sparkle. First make the numbers correct, scoped, authorized, and auditable. Then the graphs can wear a little hat. diff --git a/docs/modular_plans/phase_8_controller_api_cleanup_execution_plan_islamic.md b/docs/modular_plans/phase_8_controller_api_cleanup_execution_plan_islamic.md new file mode 100644 index 00000000..dff48dfe --- /dev/null +++ b/docs/modular_plans/phase_8_controller_api_cleanup_execution_plan_islamic.md @@ -0,0 +1,1850 @@ +# Phase 8 Execution Plan: Controller and API Cleanup + +## 1. Purpose + +Phase 8 cleans up the controller and API layer after the modular core has started taking shape. + +The goal is to make controllers thin, predictable, secure, testable, and compatible with the modular platform architecture: + +```text +Http Controllers + ↓ +FormRequests / Resources + ↓ +SchoolContext + ↓ +SchoolCore contracts + ↓ +IslamicSundaySchool extension contracts/policies where applicable +``` + +Controllers should not contain domain behavior. They are delivery adapters. They receive HTTP noise, validate it, authorize it, pass a DTO to an application/domain service, and return a response. + +That is the whole job. Controllers trying to calculate balances, record attendance, resolve recipients, generate student IDs, or build reports are not “pragmatic.” They are tiny procedural empires squatting in the web layer. + +--- + +## 2. Scope + +Phase 8 applies to: + +```text +API controllers +legacy compatibility controllers +FormRequests +API Resources +response envelopes +route definitions +authorization middleware/policies +OpenAPI/API documentation +controller tests +route inventory +legacy duplicate controller cleanup +``` + +This phase does not rewrite the domain modules. It forces controllers to use the contracts created in earlier phases. + +Covered modules: + +```text +SchoolContext +Core Contracts +Finance +Attendance +Students +Communication +Reporting +IslamicSundaySchool extension endpoints +``` + +--- + +## 3. Required Dependency Direction + +Controllers may depend on: + +```text +SchoolCore contracts +IslamicSundaySchool extension contracts, only for extension-specific routes +FormRequests +Resources +SchoolContext resolver/store +Authorization policies/gates +``` + +Controllers must not depend on: + +```text +Eloquent models for domain mutation, except simple legacy adapters during migration +raw DB queries +service concrete classes when contract exists +Http\Request directly for validated domain data +auth() calls scattered inside methods +business rule helpers +private domain orchestration methods +provider SDKs +file system paths directly +``` + +Allowed: + +```text +Controller -> Contract -> Service +Controller -> FormRequest -> DTO +Controller -> Resource +Controller -> Policy/Gate +``` + +Forbidden: + +```text +Controller -> DB::table(...) +Controller -> Mail::send(...) +Controller -> WhatsAppProvider +Controller -> Invoice::update(balance) +Controller -> Student::max(id) + 1 +Controller -> attendance mutation queries +Controller -> report SQL +``` + +Yes, controllers can technically do all of that. A shovel can technically be used as a dinner utensil too. + +--- + +## 4. Controller Target Shape + +Every controller action should follow this shape: + +```php +public function store(CreateStudentRequest $request): StudentResource +{ + $context = $this->schoolContext->current(); + + $result = $this->students->createStudent( + $context, + $request->toDTO() + ); + + return new StudentResource($result->student); +} +``` + +Allowed responsibilities: + +```text +1. Accept FormRequest. +2. Resolve SchoolContext. +3. Authorize request/resource. +4. Build DTO. +5. Call contract. +6. Return Resource or standardized API response. +``` + +Forbidden responsibilities: + +```text +- domain calculations +- multi-step business transactions +- direct file lookup +- direct provider calls +- duplicate validation logic +- raw request merging +- direct cross-module orchestration +- generating IDs +- applying role logic manually +``` + +--- + +## 5. API Response Standard + +The project must choose one response envelope and enforce it. + +Recommended envelope: + +```json +{ + "success": true, + "data": {}, + "message": null, + "meta": {}, + "errors": null +} +``` + +For errors: + +```json +{ + "success": false, + "data": null, + "message": "Validation failed.", + "meta": {}, + "errors": { + "field": ["Error message."] + } +} +``` + +Rules: + +```text +- Resources own response data shape. +- Controllers do not hand-build inconsistent arrays. +- Paginated responses include consistent pagination metadata. +- Error responses use centralized exception handling. +- Legacy response shapes may be preserved temporarily by adapters. +``` + +Compatibility warning: + +```text +Do not break existing clients casually. If response shape changes, version the API or provide compatibility resources. +``` + +Breaking mobile/admin clients because the backend suddenly became “clean” is not modernization. It is vandalism with a linter. + +--- + +## 6. API Versioning Strategy + +Recommended versioning: + +```text +/api/v1/... +/api/v2/... +``` + +Phase 8 should not force a full v2 unless breaking changes are unavoidable. + +Migration strategy: + +```text +v1 routes -> legacy-compatible resources/adapters +v2 routes -> modular standardized resources +``` + +Rules: + +```text +- v1 may preserve old response envelope where required. +- v2 uses standardized envelope. +- New modular endpoints should be v2-ready. +- Deprecated routes must include removal target. +``` + +--- + +## 7. Route Inventory + +Before cleanup, create a complete route inventory. + +Inventory fields: + +```text +HTTP method +URI +controller +action +middleware +auth requirement +permission/policy +request class +resource class +domain contract called +response envelope +legacy/canonical status +module +risk level +test coverage status +``` + +Suggested output: + +```text +storage/app/api-route-inventory.json +docs/api-route-inventory.md +``` + +Each route must be categorized: + +```text +canonical +adapter +deprecated +remove +unknown +``` + +No route should remain `unknown` after Phase 8. Unknown routes are abandoned doors into the app, and abandoned doors are how raccoons and data leaks get in. + +--- + +## 8. Controller Classification + +Every controller should be classified. + +Categories: + +```text +Canonical Controller +Adapter Controller +Deprecated Controller +Remove Candidate +Extension Controller +``` + +### 8.1 Canonical Controller + +Definition: + +```text +Current preferred controller for a module/route. +Uses FormRequests, Resources, SchoolContext, contracts, and policies. +``` + +### 8.2 Adapter Controller + +Definition: + +```text +Preserves old route/response behavior but delegates to modular contracts. +Contains no domain logic. +``` + +### 8.3 Deprecated Controller + +Definition: + +```text +Still reachable but scheduled for replacement/removal. +Must include deprecation comment and target replacement route. +``` + +### 8.4 Remove Candidate + +Definition: + +```text +Duplicate, unused, unsafe, or superseded controller. +No active route should point to it. +``` + +### 8.5 Extension Controller + +Definition: + +```text +Islamic Sunday School-specific controller. +May depend on IslamicSundaySchool contracts. +Must not be used by SchoolCore routes. +``` + +--- + +## 9. Duplicate Controller Cleanup + +Known duplicate-ish controller patterns must be resolved. + +Examples to review: + +```text +BaseApiController variants +RolePermissionController variants +RoleSwitcherController variants +StatsController variants +UserController variants +Finance/payment controllers +Attendance/scanner controllers +Student controllers +Report controllers +Communication/broadcast controllers +``` + +Resolution process: + +```text +1. Find all routes pointing to duplicate controllers. +2. Identify canonical route/controller. +3. Convert old controller to adapter if compatibility is needed. +4. Remove domain logic from adapter. +5. Mark deprecated route with replacement. +6. Add tests proving old and new route behavior. +7. Remove unused controller after deprecation window. +``` + +Rule: + +```text +A shim may contain mapping code. It may not contain business logic. +``` + +--- + +## 10. FormRequest Standard + +Every route that accepts input must use a FormRequest unless explicitly exempted. + +Required methods: + +```php +public function authorize(): bool; + +public function rules(): array; + +public function toDTO(): SomeData; +``` + +Optional: + +```php +public function messages(): array; + +protected function prepareForValidation(): void; +``` + +Rules: + +```text +- validation lives in FormRequest +- authorization gate/policy check may begin in FormRequest +- controllers use validated data only +- no raw array merging in controllers +- no Validator::make inside controller except temporary documented adapter +``` + +Forbidden pattern: + +```php +$data = array_merge($request->query->all(), $request->all(), $request->json()->all()); +$validator = Validator::make($data, [...]); +``` + +That pattern is not flexibility. It is a custody battle between query params and JSON. + +--- + +## 11. Resource Standard + +Every response returning domain data should use a Resource or ResourceCollection. + +Examples: + +```text +StudentResource +GuardianResource +InvoiceResource +PaymentResource +AttendanceRecordResource +ScanResultResource +MessageResource +RecipientPreviewResource +ReportResultResource +IslamicSundaySchoolStudentProfileResource +QuranProgressReportResource +``` + +Rules: + +```text +- Resources decide shape. +- Resources respect authorization/field visibility. +- Sensitive fields are hidden unless allowed. +- Extension fields appear only in extension resources or composed views. +- Controllers do not manually build nested response arrays. +``` + +Resource versioning: + +```text +App\Http\Resources\V1\... +App\Http\Resources\V2\... +``` + +Use versioned resources if compatibility requires it. + +--- + +## 12. Authorization Matrix + +Every route needs explicit authorization. + +Route authorization matrix fields: + +```text +route +action +resource type +permission +policy/gate +ownership rule +school boundary rule +role allowance +negative tests +``` + +Minimum negative tests per mutation route: + +```text +unauthenticated +wrong school +wrong role +wrong owner/scope +authorized actor +``` + +High-risk modules requiring immediate authorization review: + +```text +finance +payment files +student profile +guardian contact data +attendance override +role/permission mutation +bulk communication +report export +Islamic Sunday School sensitive profile fields +imam/admin notes +scholarship/sadaqah indicators +``` + +“User is logged in” is not authorization. It is merely proof that someone found the door. + +--- + +## 13. SchoolContext Enforcement + +Every controller must resolve or receive `SchoolContext`. + +Allowed patterns: + +```text +middleware resolves context +controller receives context store +FormRequest validates context-bound inputs +services receive context explicitly +``` + +Required checks: + +```text +school exists +actor belongs to school +academic year belongs to school +term/session belongs to school +resource belongs to school +extension route matches domain profile if applicable +``` + +Extension route example: + +```text +/api/v2/islamic-sunday-school/quran-progress +``` + +Must verify: + +```text +context.domain_profile == islamic_sunday_school +``` + +Or equivalent extension availability rule. + +--- + +## 14. Module-Specific Controller Cleanup + +## 14.1 Finance Controllers + +Target contracts: + +```text +InvoiceServiceContract +PaymentServiceContract +RefundServiceContract +FinanceFileServiceContract +ReportRunnerContract for finance reports +``` + +Controllers must not: + +```text +calculate invoice balances +edit payments directly +serve files by filename +apply discounts directly +delete financial history silently +``` + +Required cleanup: + +```text +- replace filename-based payment file download with payment-id route +- use PaymentServiceContract for record/edit/reverse +- use RefundServiceContract for refunds +- use FinanceFileServiceContract for downloads +- use Resources for invoices/payments +``` + +Required tests: + +```text +wrong-school finance access fails +payment edit requires permission +payment file download is payment-scoped +finance export requires permission +``` + +--- + +## 14.2 Attendance and Scanner Controllers + +Target contracts: + +```text +ScannerServiceContract +StudentAttendanceServiceContract +StaffAttendanceServiceContract +AttendanceServiceContract +``` + +Controllers must not: + +```text +lookup badges directly +determine late/present status +write attendance records +create scan logs directly +handle duplicate scan logic +``` + +Required cleanup: + +```text +- ScannerController delegates to ScannerServiceContract +- manual override routes use FormRequests +- override reason required +- ScanResultResource standardizes response +``` + +Required tests: + +```text +duplicate scan does not double-count +wrong-school badge fails +manual override requires permission and reason +scanner response remains compatible where needed +``` + +--- + +## 14.3 Student Lifecycle Controllers + +Target contracts: + +```text +StudentServiceContract +GuardianServiceContract +HouseholdServiceContract +EnrollmentServiceContract +AssignmentServiceContract +PromotionServiceContract +``` + +Controllers must not: + +```text +generate student identifiers +link guardians directly +create enrollments directly +assign groups/classes directly +mutate status directly +promote students directly +``` + +Required cleanup: + +```text +- student creation delegates to StudentServiceContract +- guardian linking delegates to GuardianServiceContract +- enrollment routes delegate to EnrollmentServiceContract +- promotion routes delegate to PromotionServiceContract +- Islamic profile routes use IslamicSundaySchool extension contract +``` + +Required tests: + +```text +concurrent student creation safe +wrong-school guardian link fails +status transition invalid path fails +Islamic profile sensitive fields require permission +``` + +--- + +## 14.4 Communication Controllers + +Target contracts: + +```text +RecipientPreviewServiceContract +CommunicationServiceContract +MessageTemplateServiceContract +DeliveryStatusServiceContract +``` + +Controllers must not: + +```text +loop recipients +call Mail directly +call SMS/WhatsApp providers directly +send bulk messages without preview +resolve audience queries directly +``` + +Required cleanup: + +```text +- bulk send requires preview_id and confirmation_token +- recipient preview route added +- send route uses CommunicationServiceContract +- channel provider callbacks use DeliveryStatusServiceContract +``` + +Required tests: + +```text +bulk send without preview fails +duplicate recipients are deduped +opt-outs are enforced +wrong-school recipients excluded +provider callbacks cannot mutate wrong-school message +``` + +--- + +## 14.5 Reporting Controllers + +Target contracts: + +```text +ReportRegistryContract +ReportRunnerContract +ReportExportServiceContract +ReportSnapshotServiceContract +ScheduledReportServiceContract +``` + +Controllers must not: + +```text +build report SQL +export directly +bypass report authorization +show extension reports outside domain profile +``` + +Required cleanup: + +```text +- report listing uses ReportRegistryContract +- run report uses ReportRunnerContract +- export uses ReportExportServiceContract +- snapshots use ReportSnapshotServiceContract +``` + +Required tests: + +```text +wrong-school report access fails +view-only user cannot export +Islamic reports hidden outside islamic_sunday_school profile +sensitive columns filtered +``` + +--- + +## 14.6 Islamic Sunday School Extension Controllers + +Allowed extension route examples: + +```text +/api/v2/islamic-sunday-school/students/{student}/profile +/api/v2/islamic-sunday-school/quran-progress +/api/v2/islamic-sunday-school/arabic-levels +/api/v2/islamic-sunday-school/halaqa-attendance +/api/v2/islamic-sunday-school/masjid-family-summary +``` + +Rules: + +```text +- extension routes depend on IslamicSundaySchool contracts +- extension routes require islamic_sunday_school domain profile or installed extension +- extension controllers may compose core resources and extension resources +- extension controllers must not mutate core tables directly +``` + +Forbidden: + +```text +SchoolCore controllers importing IslamicSundaySchool classes +Core student resource always exposing Qur'an/Arabic/halaqa fields +Core attendance controller hardcoding halaqa/session behavior +``` + +--- + +## 15. Exception Handling + +Centralize API exceptions. + +Map exceptions: + +```text +ValidationException -> 422 +AuthenticationException -> 401 +AuthorizationException -> 403 +ModelNotFoundException / domain not found -> 404 +DomainConflictException -> 409 +IdempotencyConflictException -> 409 +RateLimitException -> 429 +Unhandled exception -> 500 +``` + +Domain exceptions from prior phases: + +```text +FinanceException +AttendanceException +StudentException +CommunicationException +ReportingException +UnauthorizedFinanceAccessException +UnauthorizedAttendanceAccessException +UnauthorizedStudentAccessException +UnauthorizedCommunicationAccessException +UnauthorizedReportAccessException +``` + +Rules: + +```text +- exception messages must be safe for clients +- internal details logged server-side +- response envelope is consistent +- no stack traces in API output +``` + +--- + +## 16. OpenAPI Documentation + +Phase 8 should produce or update OpenAPI docs. + +Required for each endpoint: + +```text +method +path +summary +tags +auth requirements +permissions +request schema +response schema +error responses +pagination metadata, if any +deprecation status +``` + +OpenAPI grouping: + +```text +Auth +School Context +Students +Guardians +Enrollment +Attendance +Finance +Communication +Reporting +Islamic Sunday School +System/Admin +``` + +Rules: + +```text +- docs generated from route inventory where possible +- deprecated endpoints marked +- v1/v2 differences documented +- sensitive fields documented by permission +``` + +--- + +## 17. Compatibility Strategy + +Not all controllers should be broken at once. Apparently users dislike when APIs evaporate for architectural purity. Strange but documented. + +Compatibility options: + +```text +adapter controller +legacy resource +v1 route preservation +feature flag +deprecation header +API changelog +``` + +Deprecation headers: + +```text +Deprecation: true +Sunset: 2027-01-01 +Link: ; rel="successor-version" +``` + +Each deprecated endpoint must document: + +```text +replacement route +behavior differences +removal target +migration notes +``` + +--- + +## 18. Route Naming Standard + +Use consistent route names. + +Examples: + +```text +api.v2.students.index +api.v2.students.store +api.v2.students.show +api.v2.students.update +api.v2.finance.payments.store +api.v2.finance.payments.update +api.v2.finance.payments.file +api.v2.attendance.scans.store +api.v2.communication.recipient-previews.store +api.v2.communication.bulk-sends.store +api.v2.reporting.reports.run +api.v2.islamic-sunday-school.quran-progress.index +``` + +Rules: + +```text +- names are stable +- extension routes are clearly namespaced +- route names map to OpenAPI operation IDs +``` + +--- + +## 19. Middleware Standard + +Recommended middleware layers: + +```text +auth:sanctum or equivalent +resolve.school.context +ensure.school.membership +ensure.domain.profile, for extension routes +permission or policy middleware +request.id +rate.limit +audit.context +``` + +High-risk routes require stricter middleware: + +```text +finance mutations +file downloads +bulk communication sends +report exports +role/permission mutations +sensitive Islamic Sunday School profile fields +``` + +--- + +## 20. Testing Strategy + +### 20.1 Controller feature tests + +Every canonical controller action must test: + +```text +success path +validation failure +unauthenticated +unauthorized role +wrong school +wrong owner/scope +resource not found +standard response envelope +``` + +### 20.2 Adapter tests + +Every adapter route must test: + +```text +legacy route still works +response shape remains compatible +adapter delegates to contract +no domain logic in adapter +deprecation header if applicable +``` + +### 20.3 Architecture tests + +Required assertions: + +```text +controllers do not call DB directly for domain mutations +controllers do not call Mail/SMS/WhatsApp provider directly +controllers do not calculate invoice balance +controllers do not generate student identifiers +controllers do not write attendance records directly +controllers do not build report SQL +controllers use FormRequests for mutable endpoints +controllers return Resources or standardized response helpers +SchoolCore controllers do not import IslamicSundaySchool +extension controllers are under IslamicSundaySchool namespace or extension route group +``` + +--- + +## 21. Static Analysis Rules + +Add static checks where practical: + +```text +No DB::table in Http\Controllers +No Validator::make in canonical controllers +No Mail:: in Http\Controllers +No response()->download for payment files outside FinanceFileService +No max('id') identifier generation in controllers +No private methods over a set complexity threshold in controllers +No controller over a line-count threshold without review +``` + +Suggested thresholds: + +```text +controller class max 250 lines +controller action max 40 lines +private methods in controllers discouraged +``` + +These are not moral laws. They are smoke alarms. When they go off, investigate before the kitchen is structural ash. + +--- + +## 22. Migration Strategy + +### 22.1 Recommended order + +```text +1. Route inventory +2. Controller classification +3. Response envelope standard +4. Exception handler standard +5. FormRequest standard +6. Resource standard +7. Finance controller cleanup +8. Attendance/scanner controller cleanup +9. Student lifecycle controller cleanup +10. Communication controller cleanup +11. Reporting controller cleanup +12. Islamic Sunday School extension controller cleanup +13. OpenAPI docs +14. Static/architecture tests +15. Deprecation cleanup +``` + +Why this order: + +```text +- inventory prevents blind refactor +- classification prevents duplicate chaos +- response/exception standards stabilize API contract +- high-risk module controllers are cleaned before cosmetic ones +- documentation follows actual route decisions +- architecture tests prevent regression +``` + +--- + +## 23. First Implementation Slice: Route Inventory + +### Objective + +Create a complete inventory of active API routes. + +### Tasks + +```text +API-001. Generate route list from framework. +API-002. Parse method, URI, controller, action, middleware. +API-003. Add manual fields: module, risk, canonical status. +API-004. Identify duplicate controller/action mappings. +API-005. Export inventory to JSON and Markdown. +``` + +### Required tests/checks + +```text +- inventory generation succeeds in CI +- every route has controller/action +- every route has module classification +- every mutation route has auth middleware or documented exception +``` + +### Done when + +```text +- route inventory exists +- unknown routes are visible +- duplicate routes/controllers are listed +``` + +--- + +## 24. Second Implementation Slice: Controller Classification + +### Objective + +Mark every controller as canonical, adapter, deprecated, remove, or extension. + +### Tasks + +```text +API-006. Create controller classification document. +API-007. Add docblocks or config map for classification. +API-008. Identify canonical controller per module. +API-009. Identify adapter/deprecated duplicates. +API-010. Create removal tickets for unused controllers. +``` + +### Required checks + +```text +- every controller is classified +- every duplicate has canonical target +- no route points to remove candidate +``` + +### Done when + +```text +- controller ownership is explicit +- duplicate cleanup has a queue +``` + +--- + +## 25. Third Implementation Slice: Response and Exception Standard + +### Objective + +Standardize API success/error responses. + +### Tasks + +```text +API-011. Create ApiResponse helper or base resource response. +API-012. Update exception handler mapping. +API-013. Add validation error envelope. +API-014. Add authorization error envelope. +API-015. Add not-found/conflict envelope. +API-016. Add response snapshot tests. +``` + +### Required tests + +```text +- validation error returns 422 envelope +- unauthorized returns 403 envelope +- not found returns 404 envelope +- conflict returns 409 envelope +- success response uses standard shape +``` + +### Done when + +```text +- new canonical endpoints use consistent envelope +- legacy exceptions are safely mapped +``` + +--- + +## 26. Fourth Implementation Slice: FormRequest and Resource Standard + +### Objective + +Force input/output boundaries. + +### Tasks + +```text +API-017. Create request/resource naming standard. +API-018. Add DTO conversion convention. +API-019. Replace manual validation in high-risk controllers. +API-020. Add Resources for high-risk module responses. +API-021. Add architecture tests requiring FormRequest for mutations. +``` + +### Required tests + +```text +- mutable routes use FormRequest +- invalid input fails before service call +- resource hides sensitive fields without permission +``` + +### Done when + +```text +- finance, attendance, student, communication, and reporting mutations use FormRequests +- responses use Resources or standard response helper +``` + +--- + +## 27. Fifth Implementation Slice: Finance Controller Cleanup + +### Objective + +Make finance controllers thin and safe. + +### Tasks + +```text +API-022. Replace payment file filename route with payment-id route. +API-023. Update payment record/edit routes to use PaymentServiceContract. +API-024. Update refund/reversal routes to use RefundServiceContract. +API-025. Add finance Resources. +API-026. Add finance controller feature tests. +``` + +### Required tests + +```text +- payment file wrong-school access fails +- payment edit unauthorized fails +- payment file route uses payment ID +- controller does not calculate balance +``` + +### Done when + +```text +- finance controllers delegate only +- unsafe payment file route is deprecated or removed +``` + +--- + +## 28. Sixth Implementation Slice: Attendance Controller Cleanup + +### Objective + +Make scanner and attendance controllers thin. + +### Tasks + +```text +API-027. Update ScannerController to use ScannerServiceContract. +API-028. Move badge lookup out of controller. +API-029. Move late/status logic out of controller. +API-030. Update manual override routes to require reason. +API-031. Add ScanResultResource and AttendanceRecordResource. +``` + +### Required tests + +```text +- duplicate scan behavior preserved/fixed +- scanner controller has no domain private methods +- manual override requires reason +- wrong-school scan fails +``` + +### Done when + +```text +- scanner is HTTP adapter only +- attendance mutation belongs to services +``` + +--- + +## 29. Seventh Implementation Slice: Student Controller Cleanup + +### Objective + +Make student lifecycle controllers delegate to services. + +### Tasks + +```text +API-032. Update StudentController create/update to use StudentServiceContract. +API-033. Remove identifier generation from controller. +API-034. Update guardian routes to use GuardianServiceContract. +API-035. Update enrollment/assignment routes to use contracts. +API-036. Update promotion routes to use PromotionServiceContract. +API-037. Add student/guardian/enrollment Resources. +``` + +### Required tests + +```text +- no controller path uses max(id) + 1 +- student creation delegates to service +- wrong-school guardian link fails +- promotion unauthorized fails +``` + +### Done when + +```text +- student lifecycle is no longer controller-orchestrated +``` + +--- + +## 30. Eighth Implementation Slice: Communication Controller Cleanup + +### Objective + +Make communication controllers safe and preview-driven. + +### Tasks + +```text +API-038. Add recipient preview endpoint. +API-039. Update bulk send endpoint to require preview_id. +API-040. Remove direct send loops from controllers. +API-041. Move Mail/SMS/WhatsApp calls behind channels. +API-042. Add communication Resources. +``` + +### Required tests + +```text +- bulk send without preview fails +- controller does not call Mail directly +- wrong-school recipients excluded +- opt-outs enforced +``` + +### Done when + +```text +- communication controllers cannot fire blind bulk sends +``` + +--- + +## 31. Ninth Implementation Slice: Reporting Controller Cleanup + +### Objective + +Make reporting controllers consume reporting contracts. + +### Tasks + +```text +API-043. Add report list endpoint via ReportRegistryContract. +API-044. Add run report endpoint via ReportRunnerContract. +API-045. Add export endpoint via ReportExportServiceContract. +API-046. Add snapshot endpoint via ReportSnapshotServiceContract. +API-047. Add scheduled report endpoints through ScheduledReportServiceContract. +``` + +### Required tests + +```text +- view-only user cannot export +- Islamic reports hidden outside islamic_sunday_school profile +- export audit is written +- report controller does not build SQL +``` + +### Done when + +```text +- reporting controllers delegate only +``` + +--- + +## 32. Tenth Implementation Slice: Islamic Sunday School Extension Routes + +### Objective + +Make extension endpoints explicit and isolated. + +### Tasks + +```text +API-048. Create Islamic Sunday School route group. +API-049. Add ensure.domain.profile middleware. +API-050. Add extension FormRequests/Resources. +API-051. Move Qur'an/Arabic/halaqa/masjid-family endpoints under extension namespace. +API-052. Add extension route tests. +``` + +### Required tests + +```text +- extension route fails for non-islamic_sunday_school profile +- Qur'an profile fields require permission +- halaqa attendance uses extension controller/resource +- SchoolCore controllers do not import IslamicSundaySchool +``` + +### Done when + +```text +- Islamic Sunday School endpoints are explicitly extension-owned +``` + +--- + +## 33. Eleventh Implementation Slice: OpenAPI and Deprecation + +### Objective + +Document the cleaned API and migration path. + +### Tasks + +```text +API-053. Generate OpenAPI baseline. +API-054. Add operation IDs from route names. +API-055. Mark deprecated endpoints. +API-056. Add successor route links. +API-057. Create API changelog. +API-058. Add docs test to ensure route docs coverage. +``` + +### Required checks + +```text +- every canonical route appears in OpenAPI +- every deprecated route has replacement +- high-risk routes document permissions +``` + +### Done when + +```text +- API consumers can migrate without guessing +``` + +--- + +## 34. Twelfth Implementation Slice: Architecture Enforcement + +### Objective + +Prevent controller bloat from returning. + +### Tasks + +```text +API-059. Add controller architecture tests. +API-060. Add static analysis rules. +API-061. Add CI route inventory diff. +API-062. Add CI check for undocumented routes. +API-063. Add controller complexity/line threshold warnings. +``` + +### Required checks + +```text +- controller DB mutation rule enforced +- no provider SDK calls in controllers +- no SchoolCore -> IslamicSundaySchool imports +- mutation routes use FormRequests +- canonical routes use Resources/standard responses +``` + +### Done when + +```text +- bad controller patterns fail CI or produce review blockers +``` + +--- + +## 35. Database and Schema Requirements + +Phase 8 should avoid schema changes unless required for API support. + +Possible supporting tables: + +```text +api_route_inventory_snapshots +api_deprecation_notices +api_client_usage_logs, optional +``` + +More likely, store documentation artifacts in: + +```text +docs/ +storage/app/generated/ +``` + +Do not create database tables just to feel productive. Databases are not filing cabinets for indecision. + +--- + +## 36. Implementation Tickets + +## API-001: Generate route inventory + +### Description + +Generate complete API route inventory with controller/action/middleware/module classification. + +### Acceptance Criteria + +```text +- Inventory exists in JSON and Markdown. +- Every route has method, URI, controller, action, middleware. +- Mutation routes missing auth are flagged. +- Duplicate controller usage is listed. +``` + +--- + +## API-002: Classify controllers + +### Description + +Mark each controller as canonical, adapter, deprecated, remove, or extension. + +### Acceptance Criteria + +```text +- Every controller is classified. +- Every duplicate has canonical target. +- No active route points to remove candidate. +``` + +--- + +## API-003: Standardize API responses and exceptions + +### Description + +Create response envelope and exception mapping. + +### Acceptance Criteria + +```text +- Success, validation, auth, not-found, and conflict responses are standardized. +- Legacy compatibility exceptions documented. +- Snapshot tests exist. +``` + +--- + +## API-004: Enforce FormRequest and Resource standard + +### Description + +Require FormRequests and Resources for canonical endpoints. + +### Acceptance Criteria + +```text +- Mutable canonical endpoints use FormRequests. +- Domain data responses use Resources. +- Sensitive fields hidden unless authorized. +``` + +--- + +## API-005: Clean finance controllers + +### Description + +Move finance controllers to contract delegation and remove unsafe file/balance behavior. + +### Acceptance Criteria + +```text +- Payment file route uses payment ID. +- Payment mutations use finance contracts. +- Controllers do not calculate balances. +- Finance authorization tests pass. +``` + +--- + +## API-006: Clean attendance/scanner controllers + +### Description + +Move scanner and attendance controllers to contract delegation. + +### Acceptance Criteria + +```text +- ScannerController delegates to ScannerServiceContract. +- Badge lookup/status/idempotency outside controller. +- Manual override requires reason. +``` + +--- + +## API-007: Clean student lifecycle controllers + +### Description + +Move student, guardian, enrollment, assignment, and promotion controllers to contracts. + +### Acceptance Criteria + +```text +- No controller generates student identifiers. +- Student creation delegates to StudentServiceContract. +- Guardian/enrollment/promotion routes use contracts. +``` + +--- + +## API-008: Clean communication controllers + +### Description + +Move communication controllers to preview-driven communication contracts. + +### Acceptance Criteria + +```text +- Bulk send requires preview_id. +- Controllers do not call Mail/SMS/WhatsApp providers. +- Recipient dedupe/preferences enforced. +``` + +--- + +## API-009: Clean reporting controllers + +### Description + +Move reporting controllers to report contracts. + +### Acceptance Criteria + +```text +- Report list uses registry. +- Report run uses runner. +- Export uses export service. +- No report SQL in controller. +``` + +--- + +## API-010: Add Islamic Sunday School extension routes + +### Description + +Create explicit extension route group and controllers. + +### Acceptance Criteria + +```text +- Extension routes require islamic_sunday_school profile. +- Extension controllers use IslamicSundaySchool contracts. +- SchoolCore controllers do not import extension classes. +``` + +--- + +## API-011: Generate OpenAPI documentation + +### Description + +Generate or update OpenAPI docs from cleaned route inventory. + +### Acceptance Criteria + +```text +- Canonical routes documented. +- Deprecated routes marked. +- Permissions documented. +- Request/response schemas documented. +``` + +--- + +## API-012: Add controller architecture tests + +### Description + +Enforce controller boundary rules in CI. + +### Acceptance Criteria + +```text +- Controllers cannot perform domain DB mutations. +- Controllers cannot call provider SDKs. +- Mutation routes require FormRequests. +- Canonical routes use Resources/standard response. +- CI fails on boundary violation. +``` + +--- + +## 37. Definition of Done for Phase 8 + +Phase 8 is done when: + +```text +- Complete route inventory exists. +- Every controller is classified. +- Duplicate controllers have canonical targets or removal tickets. +- API response envelope is standardized for canonical endpoints. +- Exception handling is centralized. +- Mutable canonical endpoints use FormRequests. +- Domain data responses use Resources. +- Finance controllers delegate to finance contracts. +- Scanner/attendance controllers delegate to attendance contracts. +- Student lifecycle controllers delegate to student contracts. +- Communication controllers require preview for bulk sends. +- Reporting controllers delegate to reporting contracts. +- Islamic Sunday School extension routes are explicit and profile-gated. +- OpenAPI docs cover canonical routes. +- Deprecated routes have replacement paths and removal targets. +- Controller architecture tests exist. +- Legacy controller methods are labeled canonical, adapter, deprecated, or remove. +``` + +--- + +## 38. Non-Goals + +Do not include in Phase 8: + +```text +- full frontend migration +- complete API v2 rewrite unless required +- new business features +- new reporting calculations +- payment gateway implementation +- scanner hardware integration +- full permissions redesign +- public developer portal +``` + +Phase 8 is about making the HTTP layer boring. Boring APIs are underrated. Exciting APIs usually mean someone is debugging production at night. + +--- + +## 39. Main Risks + +### Risk: breaking existing clients + +Mitigation: + +```text +- v1 adapters +- response snapshot tests +- deprecation headers +- OpenAPI changelog +``` + +### Risk: controller logic moves sideways, not downward + +Mitigation: + +```text +- architecture tests +- contract delegation checks +- controller size thresholds +``` + +### Risk: duplicate controllers remain active + +Mitigation: + +```text +- route inventory +- controller classification +- canonical target mapping +``` + +### Risk: authorization gaps + +Mitigation: + +```text +- route authorization matrix +- negative tests +- school context middleware +``` + +### Risk: Islamic Sunday School concepts leak into core routes + +Mitigation: + +```text +- extension route group +- domain profile middleware +- import boundary tests +``` + +### Risk: documentation lies + +Mitigation: + +```text +- docs generated from route inventory +- docs coverage check +- deprecated route metadata +``` + +--- + +## 40. Immediate Next Work Queue + +Start here: + +```text +1. API-001 Generate route inventory +2. API-002 Classify controllers +3. API-003 Standardize API responses and exceptions +4. API-004 Enforce FormRequest and Resource standard +5. API-005 Clean finance controllers +6. API-006 Clean attendance/scanner controllers +7. API-007 Clean student lifecycle controllers +8. API-008 Clean communication controllers +9. API-009 Clean reporting controllers +10. API-010 Add Islamic Sunday School extension routes +11. API-011 Generate OpenAPI documentation +12. API-012 Add controller architecture tests +``` + +Do not begin by renaming files into prettier folders. Start with the route inventory. The routes are the actual public contract. Everything else is backstage clutter wearing a namespace. diff --git a/docs/modular_plans/phase_9_boundary_enforcement_execution_plan_islamic.md b/docs/modular_plans/phase_9_boundary_enforcement_execution_plan_islamic.md new file mode 100644 index 00000000..52aecb93 --- /dev/null +++ b/docs/modular_plans/phase_9_boundary_enforcement_execution_plan_islamic.md @@ -0,0 +1,1602 @@ +# Phase 9 Execution Plan: Boundary Enforcement and Modular Governance + +## 1. Purpose + +Phase 9 makes the modular architecture enforceable. + +By this point, the project has plans for: + +```text +Phase 1: SchoolContext +Phase 2: Core contracts +Phase 3: Finance +Phase 4: Attendance +Phase 5: Student lifecycle +Phase 6: Communication +Phase 7: Reporting +Phase 8: Controller/API cleanup +``` + +Phase 9 exists because architecture that is not enforced is just a poster. A very pretty poster, probably next to a codebase cheerfully importing whatever it wants. + +The goal is to prevent regression into: + +```text +SchoolCore importing IslamicSundaySchool +controllers doing business logic +services reading Request/auth directly +finance calculations duplicated in reports +attendance scans double-counting +communication sends bypassing preview +student identifiers generated unsafely +extension concepts leaking into global contracts +``` + +This phase adds architecture tests, static analysis rules, CI checks, module ownership, dependency maps, deprecation enforcement, documentation checks, and review gates. + +--- + +## 2. Core Rule + +The architecture must be mechanically enforced. + +Humans reviewing pull requests are useful, but tired humans miss things. CI should catch forbidden dependencies and unsafe patterns before they enter the main branch. + +Core boundary: + +```text +App\Domain\IslamicSundaySchool + ↓ may depend on +App\Domain\SchoolCore + ↓ may depend on +App\Domain\SchoolCore\Shared +``` + +Forbidden: + +```text +App\Domain\SchoolCore -> App\Domain\IslamicSundaySchool +App\Domain\SchoolCore -> Http\Request +App\Domain\SchoolCore -> controllers +App\Domain\SchoolCore -> auth() +App\Domain\SchoolCore -> request() +App\Domain\SchoolCore -> tenant-specific vocabulary +``` + +--- + +## 3. Boundary Enforcement Layers + +Use multiple layers. One check will miss things. Software is sneaky, mostly because humans write it. + +Required enforcement layers: + +```text +1. Architecture tests +2. Static analysis +3. CI dependency checks +4. Route inventory diff +5. Controller complexity checks +6. Forbidden vocabulary checks +7. Module ownership review +8. Deprecation tracking +9. Documentation coverage checks +10. Release readiness gates +``` + +--- + +## 4. Target Tooling + +Recommended Laravel/PHP-compatible tooling: + +```text +PHPUnit or Pest for architecture tests +PHPStan or Psalm for static analysis +Laravel Pint or PHP-CS-Fixer for formatting +Rector for automated refactors, optional +Composer scripts for local checks +GitHub Actions/GitLab CI for pipeline enforcement +OpenAPI validation tool +Custom PHP scripts for route/controller/module scans +``` + +Do not rely on one magic tool. That is how teams discover “coverage” covered everything except the bug. + +--- + +## 5. Architecture Test Categories + +Create an architecture test suite: + +```text +tests/Architecture/ + ModuleBoundaryTest.php + ControllerBoundaryTest.php + ServiceBoundaryTest.php + FinanceBoundaryTest.php + AttendanceBoundaryTest.php + StudentBoundaryTest.php + CommunicationBoundaryTest.php + ReportingBoundaryTest.php + IslamicSundaySchoolBoundaryTest.php + RouteInventoryTest.php + DeprecationPolicyTest.php +``` + +These tests should run in CI on every pull request. + +--- + +## 6. Module Boundary Rules + +### 6.1 SchoolCore must not depend on IslamicSundaySchool + +Rule: + +```text +No file under app/Domain/SchoolCore may import or reference: +App\Domain\IslamicSundaySchool +``` + +Test: + +```php +public function test_school_core_does_not_depend_on_islamic_sunday_school(): void +{ + $this->assertNamespaceDoesNotReference( + base_path('app/Domain/SchoolCore'), + 'App\\Domain\\IslamicSundaySchool' + ); +} +``` + +### 6.2 IslamicSundaySchool may depend on SchoolCore + +Allowed: + +```text +IslamicSundaySchool -> SchoolCore contracts +IslamicSundaySchool -> Shared value objects +IslamicSundaySchool -> extension service providers +``` + +Forbidden: + +```text +IslamicSundaySchool -> SchoolCore concrete services when contract exists +IslamicSundaySchool -> direct mutation of core tables outside contracts +``` + +### 6.3 SchoolCore contracts must be neutral + +Forbidden vocabulary in `SchoolCore` contracts: + +```text +Islamic +Quran +Qur'an +ArabicLevel +Halaqa +Masjid +Imam +Sadaqah +Zakat +SundaySchool +IslamicSundaySchool +``` + +Allowed in Islamic extension only: + +```text +App\Domain\IslamicSundaySchool\* +``` + +Exception: + +```text +Documentation comments explaining forbidden examples may exist only in docs, not executable contracts. +``` + +--- + +## 7. Controller Boundary Rules + +Controllers must be HTTP adapters. + +Forbidden in canonical controllers: + +```text +DB::table +Model::query()->update for domain mutation +Validator::make +Mail:: +Notification::send directly +SMS provider calls +WhatsApp provider calls +response()->download for finance files +Student::max('id') +Invoice balance calculation +Attendance status calculation +Report SQL building +Recipient send loops +``` + +Allowed: + +```text +FormRequest +Resource +SchoolContext +domain contract injection +policy authorization +response helper +``` + +Architecture checks: + +```text +- mutation routes use FormRequest +- controllers do not call provider SDKs +- controllers do not perform domain DB writes +- controllers do not exceed size threshold without exception +- ScannerController has no domain private methods +``` + +Suggested thresholds: + +```text +controller class max 250 lines +controller action max 40 lines +private methods in controllers discouraged +``` + +Threshold violations should warn first, then fail after migration deadline. + +--- + +## 8. Service Boundary Rules + +Domain services must not read HTTP/global state directly. + +Forbidden in `App\Domain\SchoolCore` and `App\Domain\IslamicSundaySchool` services: + +```text +request() +auth() +session() +$_GET +$_POST +$_SERVER +Illuminate\Http\Request +``` + +Allowed: + +```text +SchoolContext +DTOs +Contracts +Repositories +Policies +Domain exceptions +``` + +Reason: + +```text +Services should be callable from controllers, jobs, CLI commands, scheduled tasks, and tests. +``` + +If a service requires HTTP state, it is not a domain service. It is a controller wearing a fake badge. + +--- + +## 9. SchoolContext Enforcement + +Every modular service method must accept `SchoolContext` or receive it through a clearly defined application-layer context object. + +Required checks: + +```text +Finance services require SchoolContext +Attendance services require SchoolContext +Student services require SchoolContext +Communication services require SchoolContext +Reporting services require SchoolContext +IslamicSundaySchool extension services require SchoolContext +``` + +Forbidden: + +```text +Service resolving school_id from auth()->user() +Service reading school_id from request() +Service querying without school_id scope +``` + +Architecture test examples: + +```text +- public methods in SchoolCore\Services must include SchoolContext parameter unless explicitly exempted +- repositories must have school-scoped query methods +- report read models must require SchoolContext +``` + +--- + +## 10. Finance Enforcement Rules + +Finance is high-risk. The rules should be strict. + +Forbidden: + +```text +float casts in SchoolCore\Finance +payment file download by filename only +invoice balance calculation outside BalanceCalculator/read model +payment mutation outside PaymentServiceContract +refund mutation outside RefundServiceContract +finance file access outside FinanceFileServiceContract +silent payment deletion +finance mutation without audit log +finance mutation without transaction +``` + +Required checks: + +```text +- no `(float)` in finance module +- no `floatval()` in finance module +- no `response()->download()` in finance controllers for payment files +- payment routes use payment ID for files +- payment edit path uses transaction/lock +- finance reports do not calculate balances directly +``` + +Suggested static scan patterns: + +```text +app/Domain/SchoolCore/Finance contains "(float)" +app/Domain/SchoolCore/Finance contains "floatval(" +app/Http/Controllers contains "serveCheckFile($filename" +app/Http/Controllers contains "response()->download" near payment +``` + +--- + +## 11. Attendance Enforcement Rules + +Forbidden: + +```text +attendance mutation in controllers +badge lookup in controllers +late/present status calculation in controllers +scanner duplicate detection outside ScannerService/Idempotency service +raw scan log counting as attendance result in reports +manual override without reason +``` + +Required checks: + +```text +- ScannerController delegates to ScannerServiceContract +- StudentAttendanceService/StaffAttendanceService own mutation +- AttendanceStatusPolicyContract owns status rules +- AttendanceSessionResolverContract owns session resolution +- duplicate scan tests exist +``` + +Forbidden vocabulary in core attendance contracts: + +```text +halaqa +masjid +Quran +Islamic studies +``` + +Those belong in `IslamicSundaySchool\Attendance`. + +--- + +## 12. Student Lifecycle Enforcement Rules + +Forbidden: + +```text +max(id) + 1 student identifier generation +student identifier generation in controller +guardian link mutation outside GuardianService +enrollment mutation outside EnrollmentService +assignment mutation outside AssignmentService +promotion logic outside PromotionService/PromotionPolicy +Islamic-specific fields in core Student model/service contracts +``` + +Required checks: + +```text +- no `max('id')` near student creation +- unique student identifier constraint exists +- StudentServiceContract owns student creation +- status transitions use lifecycle state machine +- Islamic profile lives in extension namespace/table +``` + +Forbidden vocabulary in SchoolCore student contracts/models: + +```text +quran_level +arabic_level +halaqa +masjid_family +imam_note +sadaqah +zakat +``` + +Allowed in Islamic extension. + +--- + +## 13. Communication Enforcement Rules + +Forbidden: + +```text +bulk send without preview +recipient loops in controllers +Mail:: in controllers +SMS/WhatsApp provider SDK calls outside channel adapters +recipient query logic in controllers +send without idempotency for canonical endpoint +preference bypass without audit +``` + +Required checks: + +```text +- bulk send route requires preview_id +- CommunicationServiceContract owns send orchestration +- RecipientPreviewServiceContract owns preview +- RecipientResolverContract owns audience resolution +- channel adapters isolate providers +- delivery attempts logged +``` + +Islamic extension communication resolvers may exist only under: + +```text +App\Domain\IslamicSundaySchool\Communication +``` + +--- + +## 14. Reporting Enforcement Rules + +Forbidden: + +```text +report SQL in controllers +finance balance calculation in reports +attendance duplicate scan counting in reports +export without audit +scheduled report without recipient authorization +cache key without school_id and permission scope +Islamic report classes registered from SchoolCore provider +``` + +Required checks: + +```text +- ReportRunnerContract owns report execution +- ReportExportServiceContract owns export +- reports use read models +- exports log audit event +- Islamic reports register through IslamicSundaySchool provider +``` + +Reporting must not become a second implementation of the app. It is a mirror, not an alternate universe. + +--- + +## 15. Islamic Sunday School Extension Enforcement + +The extension must remain isolated. + +Allowed extension concepts: + +```text +Qur'an level +Arabic level +Islamic studies track +halaqa/group +masjid family/community profile +volunteer/program team +imam/admin sensitive notes +Sunday program session +sadaqah/zakat-supported scholarship classification, if applicable +``` + +Allowed locations: + +```text +app/Domain/IslamicSundaySchool +app/Http/Controllers/Api/IslamicSundaySchool +app/Http/Resources/IslamicSundaySchool +app/Http/Requests/IslamicSundaySchool +database migrations for extension tables +docs/islamic-sunday-school +``` + +Forbidden locations: + +```text +app/Domain/SchoolCore contracts +app/Domain/SchoolCore services +core controller routes +core Resources always returned for every school +core database fields unless neutralized as generic metadata/profile extension +``` + +Required checks: + +```text +- Islamic extension routes require islamic_sunday_school domain profile +- Islamic extension providers register extension policies/resolvers/reports +- SchoolCore does not import IslamicSundaySchool +- extension sensitive fields require explicit permission +``` + +--- + +## 16. Route Inventory Enforcement + +The route inventory created in Phase 8 must become a CI artifact. + +Required route inventory fields: + +```text +method +uri +name +controller +action +middleware +module +canonical_status +permission +request_class +resource_class +domain_contract +risk_level +deprecated +replacement_route +``` + +CI checks: + +```text +- every new route appears in inventory +- every mutation route has auth/context middleware or documented exception +- every high-risk route has permission +- deprecated routes have replacement route +- extension routes have domain profile middleware +``` + +Recommended command: + +```bash +php artisan app:route-inventory --format=json > storage/app/generated/api-route-inventory.json +``` + +Compare diff in CI. + +--- + +## 17. Deprecation Enforcement + +Legacy methods/routes/classes must not linger forever, which is apparently shocking news to legacy code. + +Deprecation metadata required: + +```text +deprecated: true +replacement_route +replacement_class +sunset_date +owner +removal_ticket +compatibility_notes +``` + +Deprecation checks: + +```text +- deprecated route without replacement fails +- deprecated route past sunset date fails or warns loudly +- adapter controller with domain logic fails +- remove-candidate controller with active route fails +``` + +Deprecation header standard: + +```text +Deprecation: true +Sunset: 2027-01-01 +Link: ; rel="successor-version" +``` + +--- + +## 18. Module Ownership + +Each module must have an owner and review requirements. + +Suggested ownership map: + +```text +SchoolContext: Platform owner +Core Contracts: Platform owner +Finance: Finance domain owner + platform reviewer +Attendance: Attendance domain owner + platform reviewer +Students: Student lifecycle owner + platform reviewer +Communication: Communication owner + platform reviewer +Reporting: Reporting owner + data/security reviewer +IslamicSundaySchool: Islamic Sunday School domain owner + platform reviewer +API/Controllers: API owner + module owner +Security/Auth: Security owner +``` + +Every pull request touching a module should require review from: + +```text +module owner +platform owner if boundary changed +security owner if permissions/sensitive data changed +finance owner if money changed +data/reporting owner if exports changed +``` + +--- + +## 19. Pull Request Checklist + +Every PR must answer: + +```text +Does this change touch SchoolCore? +Does this change touch IslamicSundaySchool? +Does this introduce a new route? +Does this introduce a new dependency direction? +Does this add a new report/export? +Does this mutate finance, attendance, student lifecycle, communication, or permissions? +Does this expose sensitive data? +Does this require SchoolContext? +Does this need an architecture test update? +Does this deprecate or replace anything? +``` + +Required PR checklist: + +```md +- [ ] SchoolContext is used where required. +- [ ] No SchoolCore dependency on IslamicSundaySchool. +- [ ] Controllers remain thin. +- [ ] FormRequest validates input. +- [ ] Resource controls output. +- [ ] Authorization is tested. +- [ ] Wrong-school access is tested. +- [ ] Sensitive fields are protected. +- [ ] New route appears in route inventory. +- [ ] New report/export is audited. +- [ ] Module owner reviewed. +``` + +--- + +## 20. CI Pipeline + +Recommended CI stages: + +```text +1. composer validate +2. install dependencies +3. lint/format check +4. static analysis +5. unit tests +6. feature tests +7. architecture tests +8. route inventory check +9. OpenAPI/docs coverage check +10. migration check +11. security-sensitive pattern scan +``` + +Example composer scripts: + +```json +{ + "scripts": { + "test": "php artisan test", + "test:architecture": "php artisan test tests/Architecture", + "analyse": "phpstan analyse", + "route:inventory": "php artisan app:route-inventory --format=json", + "openapi:validate": "php artisan app:openapi-validate", + "architecture:scan": "php artisan app:architecture-scan" + } +} +``` + +--- + +## 21. Static Pattern Scanner + +Create a custom scanner for project-specific rules. + +Command: + +```bash +php artisan app:architecture-scan +``` + +Scanner should detect: + +```text +forbidden imports +forbidden function calls +forbidden vocabulary in core +controller DB mutations +provider SDK calls in controllers +finance float usage +bulk send without preview +report export without audit +extension route missing domain middleware +``` + +Output format: + +```text +file +line +rule_id +severity +message +suggested_fix +``` + +Severity levels: + +```text +error +warning +info +``` + +--- + +## 22. Architecture Rule IDs + +Use stable rule IDs so violations are searchable. + +Suggested rule IDs: + +```text +ARCH-001 SchoolCore must not import IslamicSundaySchool +ARCH-002 Domain services must not use Request/auth/request() +ARCH-003 Controllers must not perform domain DB mutation +ARCH-004 Controllers must not call external providers directly +ARCH-005 Core contracts must not contain extension vocabulary +CTX-001 Modular service methods must require SchoolContext +FIN-001 Finance module must not use float math +FIN-002 Payment files must not be served by filename-only route +FIN-003 Finance mutation must be transactional/audited +ATT-001 Scanner writes must go through ScannerService +ATT-002 Attendance status must go through policy +ATT-003 Duplicate scans must use idempotency +STU-001 Student identifier must not use max(id)+1 +STU-002 Student lifecycle mutations must use services +COM-001 Bulk sends must require preview +COM-002 Provider SDK calls must stay in channel adapters +REP-001 Report exports must be audited +REP-002 Reports must use read models/domain truth +API-001 Mutation routes must use FormRequest +API-002 Canonical responses must use Resource/response helper +EXT-001 Islamic extension routes require domain profile middleware +DEP-001 Deprecated routes require replacement and sunset +``` + +--- + +## 23. Documentation Enforcement + +Documentation must stay connected to code. + +Required docs: + +```text +docs/architecture/module-boundaries.md +docs/architecture/dependency-map.md +docs/api/api-route-inventory.md +docs/api/openapi.yaml or openapi.json +docs/deprecations.md +docs/modules/finance.md +docs/modules/attendance.md +docs/modules/students.md +docs/modules/communication.md +docs/modules/reporting.md +docs/modules/islamic-sunday-school.md +``` + +Docs checks: + +```text +- every canonical route appears in OpenAPI +- every deprecated route appears in docs/deprecations.md +- every module has owner listed +- every extension report/route documents required domain profile +``` + +Docs are not sacred scrolls. They are executable expectations where possible. Otherwise they become fiction with headings. + +--- + +## 24. Dependency Map + +Generate a module dependency map. + +Suggested output: + +```text +docs/architecture/dependency-map.md +storage/app/generated/dependency-map.json +``` + +Required sections: + +```text +SchoolCore modules +IslamicSundaySchool modules +HTTP controllers +Providers +Contracts +Forbidden dependency violations +``` + +Example acceptable graph: + +```text +Http\Controllers\Api\Finance + -> SchoolCore\Finance\Contracts + +IslamicSundaySchool\Reporting + -> SchoolCore\Reporting\Contracts + -> SchoolCore\Students\Contracts + -> SchoolCore\Attendance\Contracts +``` + +Unacceptable graph: + +```text +SchoolCore\Students + -> IslamicSundaySchool\Students +``` + +--- + +## 25. Release Gates + +Before each release, require: + +```text +architecture tests passing +route inventory updated +OpenAPI updated +deprecation list updated +no critical scanner violations +no high-risk route without permission +no finance mutation without tests +no report export without audit +no bulk send without preview +no extension route missing domain profile +``` + +Release checklist: + +```md +- [ ] Architecture tests pass. +- [ ] Static analysis passes. +- [ ] Route inventory diff reviewed. +- [ ] OpenAPI updated. +- [ ] Deprecated endpoints documented. +- [ ] Security-sensitive changes reviewed. +- [ ] Cross-school access tests pass. +- [ ] Extension boundary tests pass. +``` + +--- + +## 26. Migration Strategy + +### 26.1 Start in warning mode + +Initial rollout should produce warnings for some rules, because the existing app may violate them. + +Recommended modes: + +```text +Phase 9A: Scan-only mode +Phase 9B: Warning mode in CI +Phase 9C: Error mode for new/changed files +Phase 9D: Error mode globally +``` + +Do not instantly fail the whole project if legacy code already violates everything. That produces noise, rebellion, and creative disabling of CI. Humans are predictable like that. + +### 26.2 New code must be clean + +Even while legacy violations exist: + +```text +new files must satisfy all rules +changed files must not introduce new violations +high-risk modules must fail immediately on critical rules +``` + +Critical rules fail immediately: + +```text +SchoolCore importing IslamicSundaySchool +bulk send without preview +payment file filename route +student max(id)+1 identifier generation +controller calling payment provider/SMS/WhatsApp directly +report export without authorization/audit +wrong-school access test missing for high-risk route +``` + +--- + +## 27. First Implementation Slice: Architecture Test Scaffold + +### Objective + +Create architecture test infrastructure. + +### Tasks + +```text +GOV-001. Create tests/Architecture directory. +GOV-002. Add helper to scan PHP files for imports/references. +GOV-003. Add ModuleBoundaryTest. +GOV-004. Add ControllerBoundaryTest. +GOV-005. Add initial CI job for architecture tests. +``` + +### Required tests + +```text +- SchoolCore does not import IslamicSundaySchool +- domain services do not import Request +- controllers do not call provider SDKs +``` + +### Done when + +```text +- architecture tests run in CI +- violations produce readable messages +``` + +--- + +## 28. Second Implementation Slice: Static Pattern Scanner + +### Objective + +Create custom scanner for project-specific architectural rules. + +### Tasks + +```text +GOV-006. Create artisan command app:architecture-scan. +GOV-007. Add rule registry. +GOV-008. Add file scanner. +GOV-009. Add JSON and console output. +GOV-010. Add CI integration in warning mode. +``` + +### Required checks + +```text +- detects forbidden imports +- detects forbidden vocabulary in SchoolCore +- detects controller DB/provider calls +- detects finance float usage +``` + +### Done when + +```text +- scanner can run locally and in CI +- warnings are actionable +``` + +--- + +## 29. Third Implementation Slice: SchoolContext Rule Enforcement + +### Objective + +Ensure modular services require context. + +### Tasks + +```text +GOV-011. Add CTX-001 rule. +GOV-012. Scan public service methods. +GOV-013. Add allowed exemption list. +GOV-014. Fail new services without SchoolContext. +``` + +### Required tests/checks + +```text +- Finance service method without context is flagged +- Attendance service method without context is flagged +- Reporting read model without context is flagged +``` + +### Done when + +```text +- new modular services cannot ignore SchoolContext +``` + +--- + +## 30. Fourth Implementation Slice: Route Inventory CI + +### Objective + +Make route inventory a controlled artifact. + +### Tasks + +```text +GOV-015. Add route inventory command if not already present. +GOV-016. Store baseline inventory. +GOV-017. Add CI diff check. +GOV-018. Require metadata for new routes. +GOV-019. Flag mutation routes without auth/context. +``` + +### Required checks + +```text +- new route without module fails +- new mutation route without auth fails +- extension route without domain profile middleware fails +``` + +### Done when + +```text +- route changes are reviewed intentionally +``` + +--- + +## 31. Fifth Implementation Slice: Module Ownership and PR Checks + +### Objective + +Create human governance that complements CI. + +### Tasks + +```text +GOV-020. Create module ownership file. +GOV-021. Add PR checklist template. +GOV-022. Add CODEOWNERS entries. +GOV-023. Define high-risk review requirements. +``` + +Suggested file: + +```text +.github/CODEOWNERS +docs/architecture/module-ownership.md +.github/pull_request_template.md +``` + +### Required checks + +```text +- finance changes request finance owner +- extension changes request Islamic Sunday School owner +- security-sensitive changes request security owner +``` + +### Done when + +```text +- ownership is explicit +- PRs include architecture questions +``` + +--- + +## 32. Sixth Implementation Slice: Deprecation Enforcement + +### Objective + +Prevent legacy adapters from becoming permanent architecture furniture. + +### Tasks + +```text +GOV-024. Create docs/deprecations.md. +GOV-025. Add deprecation metadata schema. +GOV-026. Add deprecation scan. +GOV-027. Add CI warning for expired sunset dates. +GOV-028. Add route response deprecation headers for deprecated routes. +``` + +### Required checks + +```text +- deprecated route has replacement +- deprecated route has sunset date +- remove-candidate route is not active +``` + +### Done when + +```text +- deprecation is visible and time-bound +``` + +--- + +## 33. Seventh Implementation Slice: Documentation Coverage + +### Objective + +Keep docs aligned with routes/modules. + +### Tasks + +```text +GOV-029. Add OpenAPI coverage check. +GOV-030. Add module docs coverage check. +GOV-031. Add route-to-doc mapping check. +GOV-032. Add docs generation step to CI. +``` + +### Required checks + +```text +- every canonical route is documented +- every module has owner docs +- every extension route documents domain profile +``` + +### Done when + +```text +- docs cannot drift silently from API behavior +``` + +--- + +## 34. Eighth Implementation Slice: Release Gate + +### Objective + +Create release readiness checklist and automated gate. + +### Tasks + +```text +GOV-033. Create release checklist. +GOV-034. Add release CI workflow. +GOV-035. Add high-risk violation summary. +GOV-036. Require route inventory and OpenAPI artifacts. +``` + +### Required checks + +```text +- architecture tests pass +- static analysis passes +- no critical architecture scan violations +- route inventory current +- OpenAPI current +``` + +### Done when + +```text +- release cannot ignore boundary violations +``` + +--- + +## 35. Implementation Tickets + +## GOV-001: Create architecture test scaffold + +### Description + +Create architecture test directory, helpers, and initial boundary tests. + +### Acceptance Criteria + +```text +- tests/Architecture exists. +- ModuleBoundaryTest exists. +- ControllerBoundaryTest exists. +- CI runs architecture tests. +``` + +--- + +## GOV-002: Enforce SchoolCore to IslamicSundaySchool dependency rule + +### Description + +Add rule preventing SchoolCore from importing IslamicSundaySchool. + +### Acceptance Criteria + +```text +- SchoolCore import violation fails test. +- IslamicSundaySchool may import SchoolCore contracts. +- Error message includes file path and import. +``` + +--- + +## GOV-003: Enforce domain service HTTP isolation + +### Description + +Prevent domain services from using Request/auth/request/session directly. + +### Acceptance Criteria + +```text +- Request imports in domain services fail. +- auth() in domain services fails. +- request() in domain services fails. +- Exemptions require explicit allowlist entry. +``` + +--- + +## GOV-004: Enforce controller boundary rules + +### Description + +Prevent controllers from performing domain mutations or calling external providers directly. + +### Acceptance Criteria + +```text +- DB mutation in controller is flagged. +- Mail/SMS/WhatsApp provider call in controller is flagged. +- payment file response()->download in controller is flagged unless allowlisted. +``` + +--- + +## GOV-005: Enforce SchoolContext usage + +### Description + +Add static/architecture checks requiring context in modular services. + +### Acceptance Criteria + +```text +- Finance/Attendance/Students/Communication/Reporting service methods without SchoolContext are flagged. +- Allowed exemptions are documented. +``` + +--- + +## GOV-006: Add finance boundary rules + +### Description + +Add finance-specific architecture/static checks. + +### Acceptance Criteria + +```text +- Float usage in finance is flagged. +- Filename-only payment file routes are flagged. +- Balance calculation outside allowed services is flagged where detectable. +``` + +--- + +## GOV-007: Add attendance boundary rules + +### Description + +Add attendance/scanner-specific checks. + +### Acceptance Criteria + +```text +- Scanner controller domain logic is flagged. +- Attendance mutation in controller is flagged. +- Manual override route without reason validation is flagged where detectable. +``` + +--- + +## GOV-008: Add student lifecycle boundary rules + +### Description + +Add student lifecycle-specific checks. + +### Acceptance Criteria + +```text +- max(id)+1 student identifier generation is flagged. +- Controller identifier generation is flagged. +- Core student vocabulary leak is flagged. +``` + +--- + +## GOV-009: Add communication boundary rules + +### Description + +Add communication-specific checks. + +### Acceptance Criteria + +```text +- Bulk send without preview_id is flagged where detectable. +- Provider SDK calls outside channel adapters are flagged. +- Recipient loops in controllers are flagged. +``` + +--- + +## GOV-010: Add reporting boundary rules + +### Description + +Add reporting-specific checks. + +### Acceptance Criteria + +```text +- Report export without audit path is flagged where detectable. +- Report SQL in controller is flagged. +- Islamic reports registered from SchoolCore provider are flagged. +``` + +--- + +## GOV-011: Add route inventory CI check + +### Description + +Ensure routes are inventoried and reviewed. + +### Acceptance Criteria + +```text +- Route inventory generated in CI. +- New route without metadata fails or warns based on rollout mode. +- Extension route without domain middleware fails. +``` + +--- + +## GOV-012: Add module ownership and PR checklist + +### Description + +Create CODEOWNERS, module ownership doc, and pull request template. + +### Acceptance Criteria + +```text +- Module ownership document exists. +- CODEOWNERS maps high-risk modules. +- PR template includes boundary questions. +``` + +--- + +## GOV-013: Add deprecation enforcement + +### Description + +Track and enforce deprecated routes/controllers/classes. + +### Acceptance Criteria + +```text +- Deprecated route requires replacement. +- Deprecated route requires sunset date. +- Expired deprecations produce CI warning/failure. +``` + +--- + +## GOV-014: Add documentation coverage checks + +### Description + +Ensure API and module docs stay current. + +### Acceptance Criteria + +```text +- Canonical routes must appear in OpenAPI. +- Deprecated routes must appear in deprecation docs. +- Extension routes must document domain profile requirement. +``` + +--- + +## GOV-015: Add release gate workflow + +### Description + +Create release gate workflow with architecture, route, docs, and static checks. + +### Acceptance Criteria + +```text +- Release workflow runs architecture tests. +- Release workflow checks route inventory. +- Release workflow checks OpenAPI. +- Critical violations block release. +``` + +--- + +## 36. Definition of Done for Phase 9 + +Phase 9 is done when: + +```text +- Architecture test scaffold exists. +- SchoolCore cannot import IslamicSundaySchool. +- Domain services cannot use Request/auth/request directly. +- Controllers cannot perform forbidden domain/provider behavior without failing checks. +- Modular services require SchoolContext or documented exemption. +- Finance boundary rules exist. +- Attendance boundary rules exist. +- Student lifecycle boundary rules exist. +- Communication boundary rules exist. +- Reporting boundary rules exist. +- Islamic Sunday School extension boundary rules exist. +- Route inventory is checked in CI. +- Module ownership is documented. +- PR checklist exists. +- Deprecation enforcement exists. +- Documentation coverage checks exist. +- Release gate exists. +- Critical violations fail CI. +``` + +--- + +## 37. Non-Goals + +Do not include in Phase 9: + +```text +- rewriting all legacy code immediately +- replacing the entire framework +- building a full internal developer portal +- enforcing perfect architecture on untouched legacy files on day one +- moving every module into separate Composer packages immediately +- adding Kubernetes because someone got bored +``` + +Phase 9 is about enforceable boundaries and governance. Not architecture cosplay with extra YAML. + +--- + +## 38. Main Risks + +### Risk: too many legacy violations create noise + +Mitigation: + +```text +- start in scan/warning mode +- fail only new/changed files initially +- immediately fail critical security/data rules +``` + +### Risk: developers bypass checks + +Mitigation: + +```text +- make violations readable +- provide suggested fixes +- document exemptions +- require owner approval for exemptions +``` + +### Risk: false positives + +Mitigation: + +```text +- allowlist with expiration +- rule IDs +- test scanner against known safe files +``` + +### Risk: boundary rules block urgent fixes + +Mitigation: + +```text +- emergency bypass process +- required follow-up cleanup ticket +- security owner approval +``` + +### Risk: architecture docs drift again + +Mitigation: + +```text +- docs generated from route/dependency inventory where possible +- docs coverage CI +``` + +### Risk: Islamic extension becomes coupled to core internals + +Mitigation: + +```text +- extension uses contracts +- import boundary tests +- provider binding review +``` + +--- + +## 39. Immediate Next Work Queue + +Start here: + +```text +1. GOV-001 Create architecture test scaffold +2. GOV-002 Enforce SchoolCore to IslamicSundaySchool dependency rule +3. GOV-003 Enforce domain service HTTP isolation +4. GOV-004 Enforce controller boundary rules +5. GOV-005 Enforce SchoolContext usage +6. GOV-011 Add route inventory CI check +7. GOV-012 Add module ownership and PR checklist +8. GOV-006 Add finance boundary rules +9. GOV-007 Add attendance boundary rules +10. GOV-008 Add student lifecycle boundary rules +11. GOV-009 Add communication boundary rules +12. GOV-010 Add reporting boundary rules +13. GOV-013 Add deprecation enforcement +14. GOV-014 Add documentation coverage checks +15. GOV-015 Add release gate workflow +``` + +Start with import boundaries and HTTP isolation. They are cheap to detect and stop architectural rot early. The rest can tighten progressively as legacy code is cleaned. diff --git a/docs/school_tuition_payment_management_plan.md b/docs/school_tuition_payment_management_plan.md new file mode 100644 index 00000000..78b392f4 --- /dev/null +++ b/docs/school_tuition_payment_management_plan.md @@ -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 student’s 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 student’s 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? diff --git a/docs/student_promotion_enrollment_plan.md b/docs/student_promotion_enrollment_plan.md new file mode 100644 index 00000000..97b02585 --- /dev/null +++ b/docs/student_promotion_enrollment_plan.md @@ -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 student’s 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 student’s 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 student’s 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** diff --git a/docs/tuition_income_prediction_plan.md b/docs/tuition_income_prediction_plan.md new file mode 100644 index 00000000..a886853a --- /dev/null +++ b/docs/tuition_income_prediction_plan.md @@ -0,0 +1,1094 @@ +# Tuition Income Prediction Plan: One Service With Old/New Calculation Flag + +## Goal + +Create one dedicated tuition income prediction service that supports two calculation methods: + +1. **Old calculation** +2. **New calculation** + +The selected calculation method is controlled by a flag. + +The service must: + +- Pull students from the `students` table. +- Group students by `parent_id`. +- Filter by `school_id`. +- Filter by `school_year`. +- Calculate tuition per family. +- Support editable tuition settings. +- Return family-level details. +- Return family-size summary. +- Return grand totals. +- Allow the prediction page to select which calculation method to use. + +The existing `FeeCalculationService` should remain for refund logic only. + +--- + +## 1. Service Design + +Create one new service: + +```text +App\Services\TuitionIncomePredictionService +``` + +This service contains both calculation methods: + +```php +calculateOldModel(int $studentCount, array $config): float + +calculateNewModel(int $studentCount, array $config): float +``` + +The service chooses which method to use based on: + +```text +calculation_method +``` + +Allowed values: + +```text +old +new +comparison +``` + +Recommended default: + +```text +comparison +``` + +--- + +## 2. Calculation Method Flag + +Use this request parameter: + +```text +calculation_method +``` + +Allowed values: + +| Value | Meaning | +|---|---| +| `old` | Use old calculation only | +| `new` | Use new calculation only | +| `comparison` | Calculate both old and new side by side | + +Example request: + +```text +GET /reports/tuition-income-prediction?school_id=ABC&school_year=2025-2026&calculation_method=comparison +``` + +Do not use `selected_service`. + +This is not selecting a different service anymore. It is selecting a calculation method inside the same service. + +--- + +## 3. Pricing Configuration + +All pricing inputs must be editable. + +Do not hardcode these values inside the calculation logic. + +Configuration keys: + +| Setting Key | Default | Description | +|---|---:|---| +| `tuition_prediction_base_tuition` | `350.00` | Full tuition amount | +| `tuition_prediction_old_model_increase_percent` | `20.00` | Percent increase applied to old model | +| `tuition_prediction_old_model_additional_student_discount` | `150.00` | Old model discount for 2nd student and above | +| `tuition_prediction_new_model_second_third_discount` | `50.00` | New model discount for 2nd and 3rd students | +| `tuition_prediction_new_model_fourth_plus_discount` | `100.00` | New model discount for 4th student and above | + +Use scoped config names. Generic names like `base_tuition` and `increase_percent` are future bug bait. + +--- + +## 4. Calculation Rules + +### 4.1 Old Calculation + +The old model applies a percentage increase to the old pricing structure. + +Formula: + +```text +1st student = +base_tuition × (1 + old_model_increase_percent / 100) + +2nd student and above = +(base_tuition - old_model_additional_student_discount) +× (1 + old_model_increase_percent / 100) +``` + +Default example: + +```text +base_tuition = 350 +old_model_additional_student_discount = 150 +old_model_increase_percent = 20 +``` + +| Student Position | Fee | +|---:|---:| +| 1st | 420 | +| 2nd+ | 240 | + +### 4.2 New Calculation + +The new model uses the base tuition without the old model increase. + +Formula: + +```text +1st student = base_tuition +2nd student = base_tuition - new_model_second_third_discount +3rd student = base_tuition - new_model_second_third_discount +4th student and above = base_tuition - new_model_fourth_plus_discount +``` + +Default example: + +```text +base_tuition = 350 +new_model_second_third_discount = 50 +new_model_fourth_plus_discount = 100 +``` + +| Student Position | Fee | +|---:|---:| +| 1st | 350 | +| 2nd | 300 | +| 3rd | 300 | +| 4th+ | 250 | + +--- + +## 5. Required Student Filters + +All prediction queries must filter students with: + +```sql +WHERE is_active = 1 + AND school_id = :school_id + AND school_year = :school_year + AND parent_id IS NOT NULL + AND parent_id > 0 +``` + +Optional filters: + +```sql +AND semester = :semester +AND year_of_registration = :year_of_registration +``` + +Use optional filters only when provided. + +`school_year` is required. + +Do not replace `school_year` with `year_of_registration`. + +--- + +## 6. Student Model Methods + +Add these methods to the student model. + +### 6.1 Get Student Counts by Family + +```php +public function getStudentCountsByFamily( + string $schoolId, + string $schoolYear, + ?string $semester = null, + ?string $yearOfRegistration = null +): array +``` + +SQL: + +```sql +SELECT + parent_id, + COUNT(*) AS student_count, + SUM(CASE WHEN tuition_paid = 1 THEN 1 ELSE 0 END) AS paid_student_count, + SUM(CASE WHEN tuition_paid = 0 THEN 1 ELSE 0 END) AS unpaid_student_count +FROM students +WHERE is_active = 1 + AND school_id = :school_id + AND school_year = :school_year + AND parent_id IS NOT NULL + AND parent_id > 0 +GROUP BY parent_id +ORDER BY student_count DESC, parent_id ASC; +``` + +Append optional filters safely using bound parameters. + +### 6.2 Count Eligible Students + +```php +public function countEligibleStudents( + string $schoolId, + string $schoolYear, + ?string $semester = null, + ?string $yearOfRegistration = null +): int +``` + +SQL: + +```sql +SELECT COUNT(*) AS raw_student_count +FROM students +WHERE is_active = 1 + AND school_id = :school_id + AND school_year = :school_year + AND parent_id IS NOT NULL + AND parent_id > 0; +``` + +Apply the same optional filters. + +### 6.3 Count Students Missing Parent ID + +```php +public function countStudentsMissingParentId( + string $schoolId, + string $schoolYear, + ?string $semester = null, + ?string $yearOfRegistration = null +): int +``` + +SQL: + +```sql +SELECT COUNT(*) AS missing_parent_count +FROM students +WHERE is_active = 1 + AND school_id = :school_id + AND school_year = :school_year + AND (parent_id IS NULL OR parent_id <= 0); +``` + +Apply optional filters. + +### 6.4 Count Students Missing School Year + +```php +public function countStudentsMissingSchoolYear(string $schoolId): int +``` + +SQL: + +```sql +SELECT COUNT(*) AS missing_school_year_count +FROM students +WHERE is_active = 1 + AND school_id = :school_id + AND (school_year IS NULL OR school_year = ''); +``` + +--- + +## 7. Prediction Service Structure + +Create: + +```text +app/Services/TuitionIncomePredictionService.php +``` + +Suggested methods: + +```php +public function generateReport( + string $schoolId, + string $schoolYear, + string $calculationMethod = 'comparison', + ?string $semester = null, + ?string $yearOfRegistration = null +): array; + +private function getPredictionConfig(): array; + +private function validatePredictionConfig(array $config): void; + +private function validateCalculationMethod(string $calculationMethod): void; + +private function calculateOldModel(int $studentCount, array $config): float; + +private function calculateNewModel(int $studentCount, array $config): float; + +private function buildFamilyReport(array $familyCounts, array $config, string $calculationMethod): array; + +private function buildFamilySizeSummary(array $familyReport, string $calculationMethod): array; + +private function buildTotals(array $familyReport, string $calculationMethod): array; + +private function determineBetterMethod(float $difference): string; + +private function buildWarnings( + int $rawStudentCount, + int $groupedStudentCount, + int $missingParentCount, + int $missingSchoolYearCount +): array; +``` + +--- + +## 8. Configuration Loading + +```php +private function getPredictionConfig(): array +{ + return [ + 'base_tuition' => (float) ($this->configModel->getConfig('tuition_prediction_base_tuition') ?? 350), + 'old_model_increase_percent' => (float) ($this->configModel->getConfig('tuition_prediction_old_model_increase_percent') ?? 20), + 'old_model_additional_student_discount' => (float) ($this->configModel->getConfig('tuition_prediction_old_model_additional_student_discount') ?? 150), + 'new_model_second_third_discount' => (float) ($this->configModel->getConfig('tuition_prediction_new_model_second_third_discount') ?? 50), + 'new_model_fourth_plus_discount' => (float) ($this->configModel->getConfig('tuition_prediction_new_model_fourth_plus_discount') ?? 100), + ]; +} +``` + +--- + +## 9. Configuration Validation + +```php +private function validatePredictionConfig(array $config): void +{ + $baseTuition = (float) $config['base_tuition']; + + if ($baseTuition <= 0) { + throw new \InvalidArgumentException('Base tuition must be greater than zero.'); + } + + if ((float) $config['old_model_increase_percent'] < 0) { + throw new \InvalidArgumentException('Old model increase percent cannot be negative.'); + } + + $discountKeys = [ + 'old_model_additional_student_discount', + 'new_model_second_third_discount', + 'new_model_fourth_plus_discount', + ]; + + foreach ($discountKeys as $key) { + $discount = (float) $config[$key]; + + if ($discount < 0) { + throw new \InvalidArgumentException("{$key} cannot be negative."); + } + + if ($discount > $baseTuition) { + throw new \InvalidArgumentException("{$key} cannot exceed base tuition."); + } + } +} +``` + +--- + +## 10. Calculation Method Validation + +```php +private function validateCalculationMethod(string $calculationMethod): void +{ + $allowed = ['old', 'new', 'comparison']; + + if (!in_array($calculationMethod, $allowed, true)) { + throw new \InvalidArgumentException('Invalid calculation_method.'); + } +} +``` + +--- + +## 11. Old Calculation Method + +```php +private function calculateOldModel(int $studentCount, array $config): float +{ + $baseTuition = (float) $config['base_tuition']; + $increasePercent = (float) $config['old_model_increase_percent']; + $oldModelDiscount = (float) $config['old_model_additional_student_discount']; + + $multiplier = 1 + ($increasePercent / 100); + + $firstStudentFee = $baseTuition * $multiplier; + $additionalStudentFee = ($baseTuition - $oldModelDiscount) * $multiplier; + + $total = 0.0; + + for ($position = 1; $position <= $studentCount; $position++) { + $fee = ($position === 1) + ? $firstStudentFee + : $additionalStudentFee; + + $total += max(0, $fee); + } + + return round($total, 2); +} +``` + +--- + +## 12. New Calculation Method + +```php +private function calculateNewModel(int $studentCount, array $config): float +{ + $baseTuition = (float) $config['base_tuition']; + $secondThirdDiscount = (float) $config['new_model_second_third_discount']; + $fourthPlusDiscount = (float) $config['new_model_fourth_plus_discount']; + + $total = 0.0; + + for ($position = 1; $position <= $studentCount; $position++) { + if ($position === 1) { + $fee = $baseTuition; + } elseif ($position === 2 || $position === 3) { + $fee = $baseTuition - $secondThirdDiscount; + } else { + $fee = $baseTuition - $fourthPlusDiscount; + } + + $total += max(0, $fee); + } + + return round($total, 2); +} +``` + +--- + +## 13. Family Report Logic + +```php +private function buildFamilyReport(array $familyCounts, array $config, string $calculationMethod): array +{ + $report = []; + + foreach ($familyCounts as $family) { + $studentCount = (int) $family['student_count']; + + $row = [ + 'parent_id' => (int) $family['parent_id'], + 'student_count' => $studentCount, + 'paid_student_count' => (int) $family['paid_student_count'], + 'unpaid_student_count' => (int) $family['unpaid_student_count'], + ]; + + if ($calculationMethod === 'old' || $calculationMethod === 'comparison') { + $row['old_model_total'] = $this->calculateOldModel($studentCount, $config); + } + + if ($calculationMethod === 'new' || $calculationMethod === 'comparison') { + $row['new_model_total'] = $this->calculateNewModel($studentCount, $config); + } + + if ($calculationMethod === 'comparison') { + $row['difference'] = round($row['new_model_total'] - $row['old_model_total'], 2); + $row['better_method'] = $this->determineBetterMethod($row['difference']); + } + + $report[] = $row; + } + + return $report; +} +``` + +--- + +## 14. Better Method Logic + +```php +private function determineBetterMethod(float $difference): string +{ + if ($difference > 0) { + return 'new'; + } + + if ($difference < 0) { + return 'old'; + } + + return 'equal'; +} +``` + +--- + +## 15. Family-Size Summary Logic + +Group rows by `student_count`. + +```php +private function buildFamilySizeSummary(array $familyReport, string $calculationMethod): array +{ + $summary = []; + + foreach ($familyReport as $row) { + $size = (int) $row['student_count']; + + if (!isset($summary[$size])) { + $summary[$size] = [ + 'family_size' => $size, + 'family_count' => 0, + 'total_students' => 0, + 'paid_student_count' => 0, + 'unpaid_student_count' => 0, + ]; + + if ($calculationMethod === 'old' || $calculationMethod === 'comparison') { + $summary[$size]['old_model_total'] = 0.0; + } + + if ($calculationMethod === 'new' || $calculationMethod === 'comparison') { + $summary[$size]['new_model_total'] = 0.0; + } + + if ($calculationMethod === 'comparison') { + $summary[$size]['difference'] = 0.0; + $summary[$size]['better_method'] = 'equal'; + } + } + + $summary[$size]['family_count']++; + $summary[$size]['total_students'] += $size; + $summary[$size]['paid_student_count'] += (int) $row['paid_student_count']; + $summary[$size]['unpaid_student_count'] += (int) $row['unpaid_student_count']; + + if (isset($row['old_model_total'])) { + $summary[$size]['old_model_total'] += $row['old_model_total']; + } + + if (isset($row['new_model_total'])) { + $summary[$size]['new_model_total'] += $row['new_model_total']; + } + + if ($calculationMethod === 'comparison') { + $summary[$size]['difference'] = round( + $summary[$size]['new_model_total'] - $summary[$size]['old_model_total'], + 2 + ); + + $summary[$size]['better_method'] = $this->determineBetterMethod($summary[$size]['difference']); + } + } + + ksort($summary); + + return array_values($summary); +} +``` + +--- + +## 16. Totals Logic + +```php +private function buildTotals(array $familyReport, string $calculationMethod): array +{ + $totals = [ + 'total_families' => count($familyReport), + 'total_students' => 0, + 'total_paid_students' => 0, + 'total_unpaid_students' => 0, + ]; + + if ($calculationMethod === 'old' || $calculationMethod === 'comparison') { + $totals['old_model_total'] = 0.0; + } + + if ($calculationMethod === 'new' || $calculationMethod === 'comparison') { + $totals['new_model_total'] = 0.0; + } + + foreach ($familyReport as $row) { + $totals['total_students'] += (int) $row['student_count']; + $totals['total_paid_students'] += (int) $row['paid_student_count']; + $totals['total_unpaid_students'] += (int) $row['unpaid_student_count']; + + if (isset($row['old_model_total'])) { + $totals['old_model_total'] += $row['old_model_total']; + } + + if (isset($row['new_model_total'])) { + $totals['new_model_total'] += $row['new_model_total']; + } + } + + if (isset($totals['old_model_total'])) { + $totals['old_model_total'] = round($totals['old_model_total'], 2); + } + + if (isset($totals['new_model_total'])) { + $totals['new_model_total'] = round($totals['new_model_total'], 2); + } + + if ($calculationMethod === 'comparison') { + $totals['difference'] = round($totals['new_model_total'] - $totals['old_model_total'], 2); + $totals['better_method'] = $this->determineBetterMethod($totals['difference']); + } + + return $totals; +} +``` + +--- + +## 17. Generate Report Flow + +```php +public function generateReport( + string $schoolId, + string $schoolYear, + string $calculationMethod = 'comparison', + ?string $semester = null, + ?string $yearOfRegistration = null +): array { + $this->validateCalculationMethod($calculationMethod); + + $config = $this->getPredictionConfig(); + + $this->validatePredictionConfig($config); + + $familyCounts = $this->studentModel->getStudentCountsByFamily( + $schoolId, + $schoolYear, + $semester, + $yearOfRegistration + ); + + $familyReport = $this->buildFamilyReport($familyCounts, $config, $calculationMethod); + + $familySizeSummary = $this->buildFamilySizeSummary($familyReport, $calculationMethod); + + $totals = $this->buildTotals($familyReport, $calculationMethod); + + $rawStudentCount = $this->studentModel->countEligibleStudents( + $schoolId, + $schoolYear, + $semester, + $yearOfRegistration + ); + + $groupedStudentCount = (int) array_sum(array_column($familyCounts, 'student_count')); + + $missingParentCount = $this->studentModel->countStudentsMissingParentId( + $schoolId, + $schoolYear, + $semester, + $yearOfRegistration + ); + + $missingSchoolYearCount = $this->studentModel->countStudentsMissingSchoolYear($schoolId); + + $warnings = $this->buildWarnings( + $rawStudentCount, + $groupedStudentCount, + $missingParentCount, + $missingSchoolYearCount + ); + + return [ + 'filters' => [ + 'school_id' => $schoolId, + 'school_year' => $schoolYear, + 'semester' => $semester, + 'year_of_registration' => $yearOfRegistration, + 'calculation_method' => $calculationMethod, + ], + 'config' => $config, + 'family_report' => $familyReport, + 'family_size_summary' => $familySizeSummary, + 'totals' => $totals, + 'validation' => [ + 'raw_student_count' => $rawStudentCount, + 'grouped_student_count' => $groupedStudentCount, + 'missing_parent_count' => $missingParentCount, + 'missing_school_year_count' => $missingSchoolYearCount, + ], + 'warnings' => $warnings, + ]; +} +``` + +--- + +## 18. Controller Endpoint + +Suggested route: + +```text +GET /reports/tuition-income-prediction +``` + +Required query parameters: + +```text +school_id +school_year +``` + +Optional query parameters: + +```text +calculation_method +semester +year_of_registration +``` + +Default: + +```text +calculation_method = comparison +``` + +Controller example: + +```php +public function tuitionIncomePrediction() +{ + $schoolId = $this->request->getGet('school_id'); + $schoolYear = $this->request->getGet('school_year'); + $calculationMethod = $this->request->getGet('calculation_method') ?? 'comparison'; + $semester = $this->request->getGet('semester'); + $yearOfRegistration = $this->request->getGet('year_of_registration'); + + if (!$schoolId) { + return $this->response->setStatusCode(400)->setJSON([ + 'error' => 'school_id is required', + ]); + } + + if (!$schoolYear) { + return $this->response->setStatusCode(400)->setJSON([ + 'error' => 'school_year is required', + ]); + } + + try { + $report = $this->tuitionIncomePredictionService->generateReport( + $schoolId, + $schoolYear, + $calculationMethod, + $semester, + $yearOfRegistration + ); + + return $this->response->setJSON($report); + } catch (\InvalidArgumentException $e) { + return $this->response->setStatusCode(400)->setJSON([ + 'error' => $e->getMessage(), + ]); + } catch (\Throwable $e) { + log_message('error', 'Tuition prediction failed: ' . $e->getMessage()); + + return $this->response->setStatusCode(500)->setJSON([ + 'error' => 'Unable to generate tuition prediction report.', + ]); + } +} +``` + +--- + +## 19. Prediction Page UI + +Add a dropdown: + +```text +Calculation Method +``` + +Options: + +| Label | Value | +|---|---| +| Compare Old vs New | `comparison` | +| Old Calculation | `old` | +| New Calculation | `new` | + +Default: + +```text +comparison +``` + +Required filters: + +```text +School +School Year +``` + +Optional filters: + +```text +Semester +Year of Registration +``` + +Display: + +1. Configuration values used. +2. Selected calculation method. +3. Family-size summary. +4. Family-level report. +5. Totals. +6. Validation warnings. + +--- + +## 20. Response Shape + +### Comparison Mode + +```php +[ + 'filters' => [ + 'school_id' => 'ABC', + 'school_year' => '2025-2026', + 'semester' => null, + 'year_of_registration' => null, + 'calculation_method' => 'comparison', + ], + 'config' => [ + 'base_tuition' => 350.00, + 'old_model_increase_percent' => 20.00, + 'old_model_additional_student_discount' => 150.00, + 'new_model_second_third_discount' => 50.00, + 'new_model_fourth_plus_discount' => 100.00, + ], + 'family_report' => [ + [ + 'parent_id' => 101, + 'student_count' => 3, + 'paid_student_count' => 2, + 'unpaid_student_count' => 1, + 'old_model_total' => 900.00, + 'new_model_total' => 950.00, + 'difference' => 50.00, + 'better_method' => 'new', + ], + ], + 'family_size_summary' => [...], + 'totals' => [ + 'total_families' => 102, + 'total_students' => 200, + 'total_paid_students' => 150, + 'total_unpaid_students' => 50, + 'old_model_total' => 66360.00, + 'new_model_total' => 64650.00, + 'difference' => -1710.00, + 'better_method' => 'old', + ], + 'validation' => [ + 'raw_student_count' => 200, + 'grouped_student_count' => 200, + 'missing_parent_count' => 0, + 'missing_school_year_count' => 0, + ], + 'warnings' => [], +] +``` + +### Old Mode + +```php +[ + 'filters' => [ + 'calculation_method' => 'old', + ], + 'family_report' => [ + [ + 'parent_id' => 101, + 'student_count' => 3, + 'paid_student_count' => 2, + 'unpaid_student_count' => 1, + 'old_model_total' => 900.00, + ], + ], + 'totals' => [ + 'total_families' => 102, + 'total_students' => 200, + 'old_model_total' => 66360.00, + ], +] +``` + +### New Mode + +```php +[ + 'filters' => [ + 'calculation_method' => 'new', + ], + 'family_report' => [ + [ + 'parent_id' => 101, + 'student_count' => 3, + 'paid_student_count' => 2, + 'unpaid_student_count' => 1, + 'new_model_total' => 950.00, + ], + ], + 'totals' => [ + 'total_families' => 102, + 'total_students' => 200, + 'new_model_total' => 64650.00, + ], +] +``` + +--- + +## 21. Old Refund Service Fix + +Keep the existing old refund service: + +```text +App\Services\FeeCalculationService +``` + +Do not delete it. + +Do not use it for prediction. + +Fix its known issues separately. + +Critical issue in the old refund service: + +- It calculates `$studentFee` while assigning fees. +- It does not store the fee on the student. +- Later it uses `$studentFee` in the refund loop. +- That can use a stale value from a previous loop. + +Fix by storing: + +```php +$student['tuition_fee'] = $studentFee; +``` + +Then in the refund loop use: + +```php +$studentFee = (float) ($student['tuition_fee'] ?? 0); +``` + +Also fix the array-copy issue: + +- Do not assign `tuition_fee` to `$allStudents` and then calculate refunds from the older `$withdrawnStudents` array. +- Either calculate refunds from `$allStudents`, or map fees back by student ID. + +Recommended: + +```php +foreach ($allStudents as $student) { + if (!$this->isWithdrawnStatus($student['enrollment_status'] ?? '')) { + continue; + } + + $studentFee = (float) ($student['tuition_fee'] ?? 0); + + // refund calculation here +} +``` + +Also fix: + +1. Remove unused `InvoiceModel` unless needed. +2. Validate `refund_deadline`. +3. Validate `last_school_day`. +4. Validate `weeks_study > 0`. +5. Normalize enrollment/admission statuses to lowercase. +6. Handle missing `class_section_id`. +7. Use `student_id ?? id ?? 'unknown'` in logs. + +Do not change the public method signature: + +```php +public function calculateRefund(array $students, int $parentId): float +``` + +Existing callers should continue working. + +--- + +## 22. Test Cases + +Using defaults: + +```text +base_tuition = 350 +old_model_increase_percent = 20 +old_model_additional_student_discount = 150 +new_model_second_third_discount = 50 +new_model_fourth_plus_discount = 100 +``` + +| Students | Old Model | New Model | Difference | Better | +|---:|---:|---:|---:|---| +| 1 | 420 | 350 | -70 | old | +| 2 | 660 | 650 | -10 | old | +| 3 | 900 | 950 | 50 | new | +| 4 | 1140 | 1200 | 60 | new | +| 5 | 1380 | 1450 | 70 | new | + +Also test: + +1. `calculation_method = old` +2. `calculation_method = new` +3. `calculation_method = comparison` +4. Invalid `calculation_method` +5. Missing `school_id` +6. Missing `school_year` +7. Missing `parent_id` +8. Inactive students excluded +9. Different school year excluded +10. Discount greater than base tuition rejected +11. Negative increase percent rejected + +--- + +## 23. Acceptance Criteria + +The task is complete when: + +1. One new prediction service exists. +2. The prediction service contains both old and new calculation methods. +3. A `calculation_method` flag controls which method is used. +4. Valid calculation methods are `old`, `new`, and `comparison`. +5. Prediction page exposes this flag as a dropdown. +6. Report requires `school_id`. +7. Report requires `school_year`. +8. Student counts are grouped by `parent_id`. +9. Base tuition is editable. +10. Increase percent is editable. +11. Old discount is editable. +12. New 2nd/3rd student discount is editable. +13. New 4th+ discount is editable. +14. Report includes config values used. +15. Report includes filters used. +16. Report includes validation counts. +17. Report includes warnings. +18. Existing `FeeCalculationService` remains for refund logic. +19. Existing `FeeCalculationService` is fixed internally. +20. Prediction logic does not call refund logic. diff --git a/routes/api.php b/routes/api.php index 30066186..156b5405 100644 --- a/routes/api.php +++ b/routes/api.php @@ -1,4 +1,3 @@ - group(function () { Route::get('profile', [InfoIconController::class, 'profileIcon']); }); - Route::middleware('auth:api')->prefix('administrator')->group(function () { + Route::middleware(['auth:api', 'admin.access'])->prefix('administrator')->group(function () { Route::get('attendance-templates', [AttendanceCommentTemplateController::class, 'bootstrapUrls']) ->name('api.v1.administrator.attendance-templates.bootstrap'); @@ -312,6 +314,29 @@ Route::prefix('v1')->group(function () { Route::get('enroll-withdrawal/new-students', [AdministratorEnrollmentController::class, 'newStudents']); Route::post('enroll-withdrawal/update-statuses', [AdministratorEnrollmentController::class, 'updateStatuses']); + Route::prefix('promotions')->group(function () { + Route::get('/', [AdministratorPromotionController::class, 'index']); + Route::get('summary', [AdministratorPromotionController::class, 'summary']); + Route::post('evaluate', [AdministratorPromotionController::class, 'evaluate']); + Route::post('reminders/dispatch', [AdministratorPromotionController::class, 'dispatchScheduledReminders']); + Route::post('deadlines/expire', [AdministratorPromotionController::class, 'expireDeadlines']); + Route::post('deadlines', [AdministratorPromotionController::class, 'setDeadline']); + + Route::prefix('levels')->group(function () { + Route::get('/', [AdministratorPromotionController::class, 'levelProgressions']); + Route::post('/', [AdministratorPromotionController::class, 'upsertLevelProgression']); + Route::post('seed', [AdministratorPromotionController::class, 'seedLevelProgressions']); + }); + + Route::get('{promotionId}', [AdministratorPromotionController::class, 'show'])->whereNumber('promotionId'); + Route::patch('{promotionId}/status', [AdministratorPromotionController::class, 'updateStatus'])->whereNumber('promotionId'); + Route::post('{promotionId}/deadline', [AdministratorPromotionController::class, 'setDeadline'])->whereNumber('promotionId'); + Route::post('{promotionId}/reminders', [AdministratorPromotionController::class, 'sendReminder'])->whereNumber('promotionId'); + Route::get('{promotionId}/reminders', [AdministratorPromotionController::class, 'reminders'])->whereNumber('promotionId'); + Route::get('{promotionId}/audit', [AdministratorPromotionController::class, 'audit'])->whereNumber('promotionId'); + Route::patch('{promotionId}/enrollment-steps', [AdministratorPromotionController::class, 'adminCompleteEnrollment'])->whereNumber('promotionId'); + }); + Route::get('emergency-contacts', [AdministratorEmergencyContactController::class, 'index']); Route::get('emergency-contacts/data', [AdministratorEmergencyContactController::class, 'index']); Route::get('emergency-contacts/{contactId}', [AdministratorEmergencyContactController::class, 'show']); @@ -396,7 +421,7 @@ Route::prefix('v1')->group(function () { Route::post('generate', [CertificateController::class, 'generate']); }); - Route::prefix('role-permission')->group(function () { + Route::middleware('admin.access')->prefix('role-permission')->group(function () { Route::get('roles', [RolePermissionController::class, 'roles']); Route::post('roles', [RolePermissionController::class, 'storeRole']); Route::get('roles/{roleId}', [RolePermissionController::class, 'showRole']); @@ -625,7 +650,7 @@ Route::prefix('v1')->group(function () { Route::post('{notificationId}/restore', [ApiNotificationController::class, 'restore']); Route::post('{notificationId}/read', [ApiNotificationController::class, 'markRead']); }); - Route::prefix('role-permissions')->group(function () { + Route::middleware('admin.access')->prefix('role-permissions')->group(function () { Route::get('roles', [RolePermissionController::class, 'roles']); Route::post('roles', [RolePermissionController::class, 'storeRole']); Route::get('roles/{roleId}', [RolePermissionController::class, 'showRole']); @@ -645,25 +670,6 @@ Route::prefix('v1')->group(function () { Route::put('roles/{roleId}/permissions', [RolePermissionController::class, 'updateRolePermissions']); }); - Route::prefix('role-permission')->group(function () { - Route::get('roles', [RolePermissionController::class, 'roles']); - Route::post('roles', [RolePermissionController::class, 'storeRole']); - Route::get('roles/{roleId}', [RolePermissionController::class, 'showRole']); - Route::patch('roles/{roleId}', [RolePermissionController::class, 'updateRole']); - Route::delete('roles/{roleId}', [RolePermissionController::class, 'deleteRole']); - - Route::get('users', [RolePermissionController::class, 'users']); - Route::post('users/{userId}/roles', [RolePermissionController::class, 'assignRoles']); - - Route::get('permissions', [RolePermissionController::class, 'permissions']); - Route::post('permissions', [RolePermissionController::class, 'storePermission']); - Route::get('permissions/{permissionId}', [RolePermissionController::class, 'showPermission']); - Route::patch('permissions/{permissionId}', [RolePermissionController::class, 'updatePermission']); - Route::delete('permissions/{permissionId}', [RolePermissionController::class, 'deletePermission']); - - Route::get('roles/{roleId}/permissions', [RolePermissionController::class, 'rolePermissions']); - Route::put('roles/{roleId}/permissions', [RolePermissionController::class, 'updateRolePermissions']); - }); Route::prefix('role-switcher')->group(function () { Route::get('/', [RoleSwitcherController::class, 'index']); Route::post('switch', [RoleSwitcherController::class, 'switch']); @@ -741,11 +747,19 @@ Route::prefix('v1')->group(function () { Route::get('{studentId}/score-card', [StudentApiController::class, 'scoreCard']); }); - Route::prefix('parents')->group(function () { + Route::middleware('parent.access')->prefix('parents')->group(function () { Route::get('attendance', [ParentApiController::class, 'attendance']); Route::get('invoices', [ParentApiController::class, 'invoices']); Route::get('enrollments', [ParentApiController::class, 'enrollments']); Route::post('enrollments', [ParentApiController::class, 'updateEnrollments']); + + Route::prefix('promotions')->group(function () { + Route::get('/', [ParentPromotionController::class, 'overview']); + Route::get('{promotionId}', [ParentPromotionController::class, 'show'])->whereNumber('promotionId'); + Route::post('{promotionId}/start', [ParentPromotionController::class, 'start'])->whereNumber('promotionId'); + Route::patch('{promotionId}/steps', [ParentPromotionController::class, 'updateSteps'])->whereNumber('promotionId'); + Route::post('{promotionId}/submit', [ParentPromotionController::class, 'submit'])->whereNumber('promotionId'); + }); Route::get('registration', [ParentApiController::class, 'registration']); Route::post('registration', [ParentApiController::class, 'storeRegistration']); Route::patch('students/{studentId}', [ParentApiController::class, 'updateStudent']); @@ -770,7 +784,7 @@ Route::prefix('v1')->group(function () { Route::delete('authorized-users/{id}', [AuthorizedUsersController::class, 'destroy'])->whereNumber('id'); }); - Route::prefix('users')->group(function () { + Route::middleware('admin.access')->prefix('users')->group(function () { Route::get('/', [UserController::class, 'index']); Route::post('/', [UserController::class, 'store']); Route::get('login-activity', [UserController::class, 'loginActivity']); @@ -896,6 +910,18 @@ Route::prefix('v1')->group(function () { Route::get('unpaid-parents', [FinancialController::class, 'unpaidParents']); Route::post('fees/refund', [FeeCalculationController::class, 'refund']); Route::post('fees/tuition-total', [FeeCalculationController::class, 'tuitionTotal']); + Route::post('fees/tuition-breakdown', [FeeCalculationController::class, 'tuitionBreakdown']); + Route::post('fees/family-balance', [FeeCalculationController::class, 'familyBalance']); + + Route::prefix('fees/charges')->group(function () { + Route::get('/', [ChargeController::class, 'index']); + Route::post('extra', [ChargeController::class, 'storeExtra']); + Route::post('event', [ChargeController::class, 'storeEvent']); + Route::post('extra/{id}/cancel', [ChargeController::class, 'cancelExtra']); + Route::post('extra/{id}/apply', [ChargeController::class, 'applyExtra']); + Route::post('event/{id}/cancel', [ChargeController::class, 'cancelEvent']); + Route::post('event/{id}/mark-paid', [ChargeController::class, 'markEventPaid']); + }); Route::prefix('refunds')->group(function () { Route::post('recalculate-overpayments', [RefundController::class, 'recalculateOverpayments']); diff --git a/tests/Feature/Api/V1/Finance/ChargeControllerTest.php b/tests/Feature/Api/V1/Finance/ChargeControllerTest.php new file mode 100644 index 00000000..e613a0e7 --- /dev/null +++ b/tests/Feature/Api/V1/Finance/ChargeControllerTest.php @@ -0,0 +1,166 @@ +mock(InvoiceGenerationService::class, function ($mock) { + $mock->shouldReceive('generateInvoice')->andReturn(['ok' => true]); + }); + } + + public function test_list_charges_for_parent(): void + { + $user = $this->createUser(); + Sanctum::actingAs($user); + $this->seedConfig(); + + DB::table('additional_charges')->insert([ + 'parent_id' => 10, + 'school_year' => '2025-2026', + 'semester' => 'Fall', + 'charge_type' => 'add', + 'title' => 'Books', + 'amount' => 20.00, + 'status' => 'pending', + ]); + + $response = $this->getJson('/api/v1/finance/fees/charges?parent_id=10'); + + $response->assertOk(); + $response->assertJsonPath('ok', true); + $this->assertCount(1, $response->json('charges.extra_charges')); + } + + public function test_store_extra_charge_returns_409_on_duplicate(): void + { + $user = $this->createUser(); + Sanctum::actingAs($user); + $this->seedConfig(); + + DB::table('additional_charges')->insert([ + 'parent_id' => 10, + 'school_year' => '2025-2026', + 'semester' => 'Fall', + 'charge_type' => 'add', + 'title' => 'Books', + 'amount' => 20.00, + 'status' => 'pending', + ]); + + $response = $this->postJson('/api/v1/finance/fees/charges/extra', [ + 'parent_id' => 10, + 'title' => 'Books', + 'amount' => 20, + 'charge_type' => 'add', + ]); + + $response->assertStatus(409); + $response->assertJsonPath('ok', false); + $response->assertJsonPath('charge.error', 'duplicate_charge'); + } + + public function test_store_event_charge_creates_row(): void + { + $user = $this->createUser(); + Sanctum::actingAs($user); + $this->seedConfig(); + + $response = $this->postJson('/api/v1/finance/fees/charges/event', [ + 'parent_id' => 10, + 'student_id' => 1, + 'event_name' => 'Science Fair', + 'amount' => 15, + ]); + + $response->assertStatus(201); + $response->assertJsonPath('ok', true); + $this->assertDatabaseHas('event_charges', [ + 'parent_id' => 10, + 'student_id' => 1, + 'event_name' => 'Science Fair', + ]); + } + + public function test_cancel_extra_charge_voids_row(): void + { + $user = $this->createUser(); + Sanctum::actingAs($user); + $this->seedConfig(); + + $id = DB::table('additional_charges')->insertGetId([ + 'parent_id' => 10, + 'school_year' => '2025-2026', + 'semester' => 'Fall', + 'charge_type' => 'add', + 'title' => 'Fee', + 'amount' => 10.00, + 'status' => 'pending', + ]); + + $response = $this->postJson("/api/v1/finance/fees/charges/extra/{$id}/cancel"); + + $response->assertOk(); + $this->assertDatabaseHas('additional_charges', ['id' => $id, 'status' => 'void']); + } + + public function test_charges_endpoints_require_authentication(): void + { + $response = $this->getJson('/api/v1/finance/fees/charges?parent_id=10'); + $response->assertStatus(401); + } + + private function seedConfig(): void + { + DB::table('configuration')->insert([ + ['config_key' => 'school_year', 'config_value' => '2025-2026'], + ['config_key' => 'semester', 'config_value' => 'Fall'], + ]); + } + + private function createUser(): User + { + DB::table('users')->insert([ + 'id' => 1, + 'school_id' => 1, + 'firstname' => 'Admin', + 'lastname' => 'User', + 'cellphone' => '5555555555', + 'email' => 'admin@example.com', + 'address_street' => '123 Main', + 'city' => 'City', + 'state' => 'ST', + 'zip' => '12345', + 'accept_school_policy' => 1, + 'is_verified' => 1, + 'status' => 'Active', + 'is_suspended' => 0, + 'failed_attempts' => 0, + 'password' => bcrypt('secret'), + 'semester' => 'Fall', + 'school_year' => '2025-2026', + ]); + + return User::query()->findOrFail(1); + } +} diff --git a/tests/Feature/Api/V1/Finance/FeeCalculationControllerTest.php b/tests/Feature/Api/V1/Finance/FeeCalculationControllerTest.php index d8859ef7..caa8a480 100644 --- a/tests/Feature/Api/V1/Finance/FeeCalculationControllerTest.php +++ b/tests/Feature/Api/V1/Finance/FeeCalculationControllerTest.php @@ -74,9 +74,188 @@ class FeeCalculationControllerTest extends TestCase 'school_end_date', 'weeks_of_study', 'withdrawn_students', + 'students' => [ + '*' => [ + 'student_id', + 'grade', + 'grade_level', + 'fee_category', + 'tuition_fee', + 'withdrawal_date', + 'weeks_remaining', + 'refund_amount', + 'eligible', + 'reason', + ], + ], ], ]); $this->assertGreaterThan(0, (float) $response->json('refund.refund_amount')); + $this->assertGreaterThan(0, count($response->json('refund.students') ?? [])); + } + + public function test_tuition_breakdown_endpoint_returns_per_student_detail(): void + { + $user = $this->createUser(); + Sanctum::actingAs($user); + + $this->seedConfig(); + $this->seedClassSection(); + + $response = $this->postJson('/api/v1/finance/fees/tuition-breakdown', [ + 'students' => [ + [ + 'student_id' => 1, + 'firstname' => 'Alpha', + 'lastname' => 'Last', + 'class_section_id' => 101, + 'enrollment_status' => 'enrolled', + 'admission_status' => 'accepted', + ], + [ + 'student_id' => 2, + 'firstname' => 'Beta', + 'lastname' => 'Last', + 'class_section_id' => 101, + 'enrollment_status' => 'enrolled', + 'admission_status' => 'accepted', + ], + ], + ]); + + $response->assertOk(); + $response->assertJsonPath('ok', true); + $response->assertJsonStructure([ + 'ok', + 'tuition' => [ + 'school_year', + 'family_total', + 'billable_count', + 'non_billable_count', + 'fees' => ['first_student_fee', 'second_student_fee', 'youth_fee'], + 'students' => [ + '*' => [ + 'student_id', + 'student_name', + 'grade', + 'grade_level', + 'enrollment_status', + 'admission_status', + 'fee_category', + 'tuition_fee', + 'discount_amount', + 'final_tuition_amount', + ], + ], + ], + ]); + + $this->assertSame(550.0, (float) $response->json('tuition.family_total')); + $this->assertSame(2, (int) $response->json('tuition.billable_count')); + } + + public function test_tuition_breakdown_endpoint_requires_authentication(): void + { + $response = $this->postJson('/api/v1/finance/fees/tuition-breakdown', [ + 'students' => [ + ['class_section_id' => 101], + ], + ]); + + $response->assertStatus(401); + } + + public function test_tuition_breakdown_endpoint_validates_payload(): void + { + $user = $this->createUser(); + Sanctum::actingAs($user); + + $response = $this->postJson('/api/v1/finance/fees/tuition-breakdown', [ + 'students' => [], + ]); + + $response->assertStatus(422); + $response->assertJsonStructure(['message', 'errors']); + } + + public function test_family_balance_endpoint_returns_account_summary(): void + { + $user = $this->createUser(); + Sanctum::actingAs($user); + + $this->seedConfig(); + $this->seedClassSection(); + + DB::table('invoices')->insert([ + 'id' => 1, + 'parent_id' => 10, + 'invoice_number' => 'INV-1', + 'total_amount' => 500.00, + 'balance' => 0.00, + 'paid_amount' => 200.00, + 'has_discount' => 0, + 'issue_date' => '2025-01-05', + 'status' => 'paid', + 'school_year' => '2025-2026', + 'semester' => 'Fall', + ]); + + DB::table('payments')->insert([ + 'id' => 1, + 'parent_id' => 10, + 'invoice_id' => 1, + 'paid_amount' => 200.00, + 'total_amount' => 200.00, + 'balance' => 0.00, + 'number_of_installments' => 1, + 'payment_date' => '2025-01-10', + 'payment_method' => 'manual', + 'school_year' => '2025-2026', + 'status' => 'Paid', + ]); + + $response = $this->postJson('/api/v1/finance/fees/family-balance', [ + 'parent_id' => 10, + 'students' => [ + [ + 'student_id' => 1, + 'class_section_id' => 101, + 'enrollment_status' => 'enrolled', + 'admission_status' => 'accepted', + ], + ], + ]); + + $response->assertOk(); + $response->assertJsonPath('ok', true); + $response->assertJsonStructure([ + 'ok', + 'account' => [ + 'parent_id', + 'school_year', + 'charges' => ['tuition', 'extra_charges', 'event_charges', 'late_fees', 'total'], + 'credits' => ['discounts', 'total'], + 'payments', + 'refunds_applied', + 'remaining_balance', + 'account_credit', + 'students', + ], + ]); + $this->assertSame(350.0, (float) $response->json('account.charges.tuition')); + $this->assertSame(150.0, (float) $response->json('account.remaining_balance')); + } + + public function test_family_balance_endpoint_requires_authentication(): void + { + $response = $this->postJson('/api/v1/finance/fees/family-balance', [ + 'parent_id' => 10, + 'students' => [ + ['class_section_id' => 101], + ], + ]); + + $response->assertStatus(401); } public function test_tuition_total_endpoint_returns_total(): void diff --git a/tests/Feature/Api/V1/Promotions/ParentPromotionControllerTest.php b/tests/Feature/Api/V1/Promotions/ParentPromotionControllerTest.php new file mode 100644 index 00000000..6fbf199b --- /dev/null +++ b/tests/Feature/Api/V1/Promotions/ParentPromotionControllerTest.php @@ -0,0 +1,145 @@ +seedParent(); + $studentId = $this->seedStudent($parent->id); + $this->seedPromotionRecord($studentId, $parent->id); + + Sanctum::actingAs($parent); + $response = $this->getJson('/api/v1/parents/promotions'); + + $response->assertOk(); + $response->assertJsonPath('ok', true); + $response->assertJsonPath('records.0.parent_action_required', true); + $response->assertJsonPath('records.0.promoted_level', 'Grade 4'); + } + + public function test_parent_completes_full_enrollment_flow(): void + { + $parent = $this->seedParent(); + $studentId = $this->seedStudent($parent->id); + $promotionId = $this->seedPromotionRecord($studentId, $parent->id); + + Sanctum::actingAs($parent); + + // Start enrollment + $start = $this->postJson('/api/v1/parents/promotions/' . $promotionId . '/start'); + $start->assertOk(); + $start->assertJsonPath('data.promotion_status', StudentPromotionRecord::STATUS_ENROLLMENT_STARTED); + + // Complete the checklist incrementally + $update = $this->patchJson('/api/v1/parents/promotions/' . $promotionId . '/steps', [ + 'info_confirmed' => true, + 'documents_uploaded' => true, + 'agreement_accepted' => true, + ]); + $update->assertOk(); + $update->assertJsonPath('data.promotion_status', StudentPromotionRecord::STATUS_ENROLLMENT_STARTED); + + // Final step finalises promotion + $finish = $this->patchJson('/api/v1/parents/promotions/' . $promotionId . '/steps', [ + 'payment_completed' => true, + ]); + $finish->assertOk(); + $finish->assertJsonPath('data.promotion_status', StudentPromotionRecord::STATUS_PROMOTED_AND_ENROLLED); + + $this->assertDatabaseHas('enrollments', [ + 'student_id' => $studentId, + 'school_year' => '2026-2027', + 'enrollment_status' => 'enrolled', + ]); + } + + public function test_parent_cannot_view_other_parents_promotion(): void + { + $parentA = $this->seedParent('A', 'a@example.com'); + $parentB = $this->seedParent('B', 'b@example.com'); + $studentId = $this->seedStudent($parentB->id); + $promotionId = $this->seedPromotionRecord($studentId, $parentB->id); + + Sanctum::actingAs($parentA); + $response = $this->getJson('/api/v1/parents/promotions/' . $promotionId); + $response->assertStatus(403); + } + + public function test_unauthenticated_request_is_unauthorized(): void + { + $response = $this->getJson('/api/v1/parents/promotions'); + $response->assertStatus(401); + } + + private function seedParent(string $name = 'Parent', string $email = 'parent@example.com'): User + { + DB::table('roles')->insertOrIgnore([ + ['id' => 2, 'name' => 'parent', 'slug' => 'parent', 'is_active' => 1], + ]); + + $userId = DB::table('users')->insertGetId([ + 'firstname' => $name, + 'lastname' => 'User', + 'cellphone' => '5550000000', + 'email' => $email, + 'address_street' => '1 Way', + 'city' => 'C', + 'state' => 'S', + 'zip' => '00000', + 'accept_school_policy' => 1, + 'password' => bcrypt('secret'), + 'user_type' => 'primary', + 'school_year' => '2025-2026', + 'semester' => 'Fall', + 'status' => 'Active', + ]); + + DB::table('user_roles')->insert([ + 'user_id' => $userId, + 'role_id' => 2, + ]); + + return User::query()->findOrFail($userId); + } + + private function seedStudent(int $parentId): int + { + return (int) DB::table('students')->insertGetId([ + 'school_id' => 'S-1', + 'firstname' => 'Sara', + 'lastname' => 'Test', + 'parent_id' => $parentId, + 'year_of_registration' => '2025', + 'school_year' => '2025-2026', + 'semester' => 'Fall', + ]); + } + + private function seedPromotionRecord(int $studentId, int $parentId): int + { + $record = StudentPromotionRecord::query()->create([ + 'student_id' => $studentId, + 'parent_id' => $parentId, + 'current_school_year' => '2025-2026', + 'next_school_year' => '2026-2027', + 'promotion_status' => StudentPromotionRecord::STATUS_AWAITING_PARENT, + 'enrollment_required' => true, + 'enrollment_status' => StudentPromotionRecord::ENROLLMENT_NOT_STARTED, + 'current_level_name' => 'Grade 3', + 'promoted_level_name' => 'Grade 4', + 'enrollment_deadline' => now()->addDays(30)->toDateString(), + ]); + return (int) $record->getKey(); + } +} diff --git a/tests/Unit/Services/Billing/BalanceCalculationServiceTest.php b/tests/Unit/Services/Billing/BalanceCalculationServiceTest.php new file mode 100644 index 00000000..11422592 --- /dev/null +++ b/tests/Unit/Services/Billing/BalanceCalculationServiceTest.php @@ -0,0 +1,238 @@ +seedFeeConfig(); + $this->seedClassSection(101, '2'); + $this->seedClassSection(102, '5'); + + $parentId = 10; + $invoiceId = $this->seedInvoice($parentId, 1000.00); + + DB::table('payments')->insert([ + 'parent_id' => $parentId, + 'invoice_id' => $invoiceId, + 'paid_amount' => 500.00, + 'total_amount' => 500.00, + 'balance' => 0.00, + 'number_of_installments' => 1, + 'payment_date' => '2025-01-10', + 'school_year' => '2025-2026', + 'status' => 'Paid', + ]); + + DB::table('discount_usages')->insert([ + 'voucher_id' => 1, + 'invoice_id' => $invoiceId, + 'parent_id' => $parentId, + 'discount_amount' => 80.00, + 'school_year' => '2025-2026', + 'semester' => 'Fall', + ]); + + DB::table('additional_charges')->insert([ + 'parent_id' => $parentId, + 'invoice_id' => $invoiceId, + 'school_year' => '2025-2026', + 'semester' => 'Fall', + 'charge_type' => 'add', + 'title' => 'Books', + 'amount' => 100.00, + 'status' => 'applied', + ]); + + DB::table('events')->insert([ + 'id' => 1, + 'event_name' => 'Field Trip', + 'amount' => 25.00, + 'expiration_date' => '2025-06-01', + ]); + + DB::table('event_charges')->insert([ + 'event_id' => 1, + 'parent_id' => $parentId, + 'student_id' => 2, + 'charged' => 50.00, + 'school_year' => '2025-2026', + 'semester' => 'Fall', + ]); + + DB::table('refunds')->insert([ + 'parent_id' => $parentId, + 'school_year' => '2025-2026', + 'invoice_id' => $invoiceId, + 'refund_amount' => 30.00, + 'refund_paid_amount' => 30.00, + 'status' => 'Paid', + 'semester' => 'Fall', + ]); + + $service = $this->makeService(); + + $result = $service->calculateFamilyAccount([ + [ + 'student_id' => 1, + 'class_section_id' => 101, + 'enrollment_status' => 'enrolled', + 'admission_status' => 'accepted', + ], + [ + 'student_id' => 2, + 'class_section_id' => 102, + 'enrollment_status' => 'enrolled', + 'admission_status' => 'accepted', + ], + ], $parentId); + + $this->assertSame($parentId, $result['parent_id']); + $this->assertSame(550.0, $result['charges']['tuition']); + $this->assertSame(100.0, $result['charges']['extra_charges']); + $this->assertSame(50.0, $result['charges']['event_charges']); + $this->assertSame(700.0, $result['charges']['total']); + $this->assertSame(500.0, $result['payments']); + $this->assertSame(80.0, $result['credits']['total']); + $this->assertSame(30.0, $result['refunds_applied']); + $this->assertSame(90.0, $result['remaining_balance']); + $this->assertSame(0.0, $result['account_credit']); + $this->assertCount(2, $result['students']); + } + + public function test_overpayment_records_account_credit(): void + { + $this->seedFeeConfig(); + $this->seedClassSection(201, '3'); + + $parentId = 20; + $invoiceId = $this->seedInvoice($parentId, 500.00); + + DB::table('payments')->insert([ + 'parent_id' => $parentId, + 'invoice_id' => $invoiceId, + 'paid_amount' => 500.00, + 'total_amount' => 500.00, + 'balance' => 0.00, + 'number_of_installments' => 1, + 'payment_date' => '2025-01-10', + 'school_year' => '2025-2026', + 'status' => 'Paid', + ]); + + $service = $this->makeService(); + + $result = $service->calculateFamilyAccount([ + [ + 'student_id' => 5, + 'class_section_id' => 201, + 'enrollment_status' => 'enrolled', + 'admission_status' => 'accepted', + ], + ], $parentId); + + $this->assertSame(350.0, $result['charges']['tuition']); + $this->assertSame(0.0, $result['remaining_balance']); + $this->assertSame(150.0, $result['account_credit']); + } + + public function test_student_rows_include_event_charges(): void + { + $this->seedFeeConfig(); + $this->seedClassSection(301, '4'); + + $parentId = 30; + + DB::table('events')->insert([ + 'id' => 2, + 'event_name' => 'Camp', + 'amount' => 40.00, + 'expiration_date' => '2025-06-01', + ]); + DB::table('event_charges')->insert([ + 'event_id' => 2, + 'parent_id' => $parentId, + 'student_id' => 7, + 'charged' => 40.00, + 'school_year' => '2025-2026', + ]); + + $service = $this->makeService(); + + $result = $service->calculateFamilyAccount([ + [ + 'student_id' => 7, + 'firstname' => 'Sam', + 'lastname' => 'Lee', + 'class_section_id' => 301, + 'enrollment_status' => 'enrolled', + 'admission_status' => 'accepted', + ], + ], $parentId); + + $student = $result['students'][0]; + $this->assertSame(7, $student['student_id']); + $this->assertSame(350.0, $student['tuition']); + $this->assertSame(40.0, $student['event_charges']); + $this->assertSame(390.0, $student['total_charges']); + } + + private function makeService(): BalanceCalculationService + { + $config = new FeeConfigService(); + $tuition = new TuitionCalculationService( + $config, + new FeeStudentFeeService(new FeeGradeService(), $config) + ); + + return new BalanceCalculationService($config, $tuition, new BillingTotalsService()); + } + + private function seedFeeConfig(): void + { + DB::table('configuration')->insert([ + ['config_key' => 'school_year', 'config_value' => '2025-2026'], + ['config_key' => 'first_student_fee', 'config_value' => '350'], + ['config_key' => 'second_student_fee', 'config_value' => '200'], + ['config_key' => 'youth_fee', 'config_value' => '180'], + ]); + } + + private function seedClassSection(int $sectionId, string $gradeName): void + { + DB::table('classSection')->insert([ + 'class_id' => $sectionId, + 'class_section_id' => $sectionId, + 'class_section_name' => $gradeName, + 'semester' => 'Fall', + 'school_year' => '2025-2026', + ]); + } + + private function seedInvoice(int $parentId, float $total): int + { + return (int) DB::table('invoices')->insertGetId([ + 'parent_id' => $parentId, + 'invoice_number' => 'INV-' . $parentId, + 'total_amount' => $total, + 'balance' => 0.00, + 'paid_amount' => 0.00, + 'issue_date' => '2025-01-01', + 'school_year' => '2025-2026', + 'semester' => 'Fall', + ]); + } +} diff --git a/tests/Unit/Services/Billing/BillingTotalsServiceTest.php b/tests/Unit/Services/Billing/BillingTotalsServiceTest.php index 5b55939e..3fefc0ed 100644 --- a/tests/Unit/Services/Billing/BillingTotalsServiceTest.php +++ b/tests/Unit/Services/Billing/BillingTotalsServiceTest.php @@ -17,6 +17,7 @@ class BillingTotalsServiceTest extends TestCase 'id' => 1, 'event_name' => 'Field Trip', 'amount' => 25.00, + 'expiration_date' => '2025-06-01', ]); DB::table('event_charges')->insert([ @@ -58,4 +59,78 @@ class BillingTotalsServiceTest extends TestCase $this->assertSame(25.0, $eventSubtotal); $this->assertSame(10.0, $additionalSubtotal); } + + public function test_additional_subtotal_by_parent_sums_applied_charges(): void + { + DB::table('additional_charges')->insert([ + [ + 'parent_id' => 10, + 'invoice_id' => 99, + 'school_year' => '2025-2026', + 'semester' => 'Fall', + 'charge_type' => 'add', + 'title' => 'Uniform', + 'amount' => 40.00, + 'status' => 'applied', + ], + [ + 'parent_id' => 10, + 'invoice_id' => 99, + 'school_year' => '2025-2026', + 'semester' => 'Fall', + 'charge_type' => 'deduct', + 'title' => 'Credit', + 'amount' => 10.00, + 'status' => 'applied', + ], + [ + 'parent_id' => 10, + 'invoice_id' => 99, + 'school_year' => '2025-2026', + 'semester' => 'Fall', + 'charge_type' => 'add', + 'title' => 'Pending', + 'amount' => 99.00, + 'status' => 'pending', + ], + ]); + + $service = new BillingTotalsService(); + + $this->assertSame(30.0, $service->additionalSubtotalByParent(10, '2025-2026')); + } + + public function test_event_subtotals_by_student_groups_amounts(): void + { + DB::table('events')->insert([ + 'id' => 1, + 'event_name' => 'Trip', + 'amount' => 20.00, + 'expiration_date' => '2025-06-01', + ]); + + DB::table('event_charges')->insert([ + [ + 'event_id' => 1, + 'parent_id' => 5, + 'student_id' => 1, + 'charged' => 20.00, + 'school_year' => '2025-2026', + ], + [ + 'event_id' => 1, + 'parent_id' => 5, + 'student_id' => 2, + 'charged' => 15.00, + 'school_year' => '2025-2026', + ], + ]); + + $service = new BillingTotalsService(); + $byStudent = $service->eventSubtotalsByStudent(5, '2025-2026'); + + $this->assertSame(20.0, $byStudent[1]); + $this->assertSame(15.0, $byStudent[2]); + $this->assertSame(35.0, $service->eventSubtotal(5, '2025-2026')); + } } diff --git a/tests/Unit/Services/Billing/ChargeServiceTest.php b/tests/Unit/Services/Billing/ChargeServiceTest.php new file mode 100644 index 00000000..6cb0c8f7 --- /dev/null +++ b/tests/Unit/Services/Billing/ChargeServiceTest.php @@ -0,0 +1,228 @@ +seedConfig(); + + DB::table('additional_charges')->insert([ + 'parent_id' => 10, + 'school_year' => '2025-2026', + 'semester' => 'Fall', + 'charge_type' => 'add', + 'title' => 'Books', + 'amount' => 25.00, + 'status' => 'pending', + ]); + + $service = $this->makeService(); + $result = $service->createExtraCharge([ + 'parent_id' => 10, + 'title' => 'Books', + 'amount' => 25, + 'charge_type' => 'add', + ]); + + $this->assertFalse($result['ok']); + $this->assertSame('duplicate_charge', $result['error']); + } + + public function test_create_event_charge_blocks_duplicate(): void + { + $this->seedConfig(); + + DB::table('event_charges')->insert([ + 'parent_id' => 10, + 'student_id' => 1, + 'event_name' => 'Field Trip', + 'participation' => 'yes', + 'charged' => 25.00, + 'amount' => 25.00, + 'school_year' => '2025-2026', + 'semester' => 'Fall', + ]); + + $service = $this->makeService(); + $result = $service->createEventCharge([ + 'parent_id' => 10, + 'student_id' => 1, + 'event_name' => 'Field Trip', + 'amount' => 25, + ]); + + $this->assertFalse($result['ok']); + $this->assertSame('duplicate_charge', $result['error']); + } + + public function test_create_event_charge_succeeds(): void + { + $this->seedConfig(); + + $service = $this->makeService(); + $result = $service->createEventCharge([ + 'parent_id' => 10, + 'student_id' => 1, + 'event_name' => 'Camp', + 'amount' => 40, + 'created_by' => 1, + ]); + + $this->assertTrue($result['ok']); + $this->assertDatabaseHas('event_charges', [ + 'parent_id' => 10, + 'student_id' => 1, + 'event_name' => 'Camp', + 'participation' => 'yes', + ]); + } + + public function test_cancel_event_charge_zeros_charge_and_creates_credit(): void + { + $this->seedConfig(); + + $chargeId = DB::table('event_charges')->insertGetId([ + 'parent_id' => 10, + 'student_id' => 1, + 'event_name' => 'Trip', + 'participation' => 'yes', + 'charged' => 30.00, + 'amount' => 30.00, + 'school_year' => '2025-2026', + 'semester' => 'Fall', + ]); + + $service = $this->makeService(); + $result = $service->cancelEventCharge((int) $chargeId, true); + + $this->assertTrue($result['ok']); + $this->assertSame('canceled', $result['cancellation_status']); + + $charge = EventCharges::query()->find($chargeId); + $this->assertSame('no', $charge->participation); + $this->assertSame(0.0, (float) $charge->charged); + + $this->assertDatabaseHas('additional_charges', [ + 'parent_id' => 10, + 'charge_type' => 'deduct', + 'status' => 'pending', + ]); + } + + public function test_apply_extra_charge_marks_applied(): void + { + $this->seedConfig(); + + $invoiceId = DB::table('invoices')->insertGetId([ + 'parent_id' => 10, + 'invoice_number' => 'INV-10', + 'total_amount' => 100.00, + 'balance' => 100.00, + 'paid_amount' => 0.00, + 'issue_date' => '2025-01-01', + 'school_year' => '2025-2026', + 'semester' => 'Fall', + ]); + + $chargeId = DB::table('additional_charges')->insertGetId([ + 'parent_id' => 10, + 'school_year' => '2025-2026', + 'semester' => 'Fall', + 'charge_type' => 'add', + 'title' => 'Lab fee', + 'amount' => 15.00, + 'status' => 'pending', + ]); + + $service = $this->makeService(); + $result = $service->applyExtraCharge((int) $chargeId, (int) $invoiceId); + + $this->assertTrue($result['ok']); + $charge = AdditionalCharge::query()->find($chargeId); + $this->assertSame('applied', $charge->status); + $this->assertSame($invoiceId, (int) $charge->invoice_id); + } + + public function test_list_for_parent_returns_both_charge_types(): void + { + $this->seedConfig(); + + DB::table('additional_charges')->insert([ + 'parent_id' => 10, + 'school_year' => '2025-2026', + 'semester' => 'Fall', + 'charge_type' => 'add', + 'title' => 'Uniform', + 'amount' => 20.00, + 'status' => 'pending', + ]); + + DB::table('events')->insert([ + 'id' => 1, + 'event_name' => 'Trip', + 'amount' => 25.00, + 'expiration_date' => '2025-06-01', + ]); + + DB::table('event_charges')->insert([ + 'event_id' => 1, + 'parent_id' => 10, + 'student_id' => 1, + 'participation' => 'yes', + 'charged' => 25.00, + 'school_year' => '2025-2026', + 'semester' => 'Fall', + ]); + + $service = $this->makeService(); + $list = $service->listForParent(10, '2025-2026'); + + $this->assertCount(1, $list['extra_charges']); + $this->assertCount(1, $list['event_charges']); + $this->assertSame('extra', $list['extra_charges'][0]['charge_type']); + $this->assertSame('event', $list['event_charges'][0]['charge_type']); + } + + private function makeService(): ChargeService + { + $invoiceGen = Mockery::mock(InvoiceGenerationService::class); + $invoiceGen->shouldReceive('generateInvoice')->andReturn(['ok' => true]); + + return new ChargeService( + app(ExtraChargesChargeService::class), + app(ExtraChargesInvoiceService::class), + app(ExtraChargesContextService::class), + $invoiceGen + ); + } + + private function seedConfig(): void + { + DB::table('configuration')->insert([ + ['config_key' => 'school_year', 'config_value' => '2025-2026'], + ['config_key' => 'semester', 'config_value' => 'Fall'], + ]); + } +} diff --git a/tests/Unit/Services/Fees/FeeRefundCalculatorServiceTest.php b/tests/Unit/Services/Fees/FeeRefundCalculatorServiceTest.php index 9c8c804e..3c7469a7 100644 --- a/tests/Unit/Services/Fees/FeeRefundCalculatorServiceTest.php +++ b/tests/Unit/Services/Fees/FeeRefundCalculatorServiceTest.php @@ -16,54 +16,12 @@ class FeeRefundCalculatorServiceTest extends TestCase public function test_calculate_refund_caps_at_total_paid(): void { - DB::table('configuration')->insert([ - ['config_key' => 'school_year', 'config_value' => '2025-2026'], - ['config_key' => 'refund_deadline', 'config_value' => '2025-02-01'], - ['config_key' => 'weeks_study', 'config_value' => '8'], - ['config_key' => 'last_school_day', 'config_value' => '2025-06-01'], - ['config_key' => 'first_student_fee', 'config_value' => '350'], - ['config_key' => 'second_student_fee', 'config_value' => '200'], - ['config_key' => 'youth_fee', 'config_value' => '180'], - ]); + $this->seedFeeConfig(); + $this->seedClassSection(101, '3'); - DB::table('classSection')->insert([ - 'id' => 1, - 'class_id' => 1, - 'class_section_id' => 101, - 'class_section_name' => '3', - 'semester' => 'Fall', - 'school_year' => '2025-2026', - ]); + $this->seedInvoiceAndPayment(99, 100.00); - DB::table('invoices')->insert([ - 'id' => 1, - 'parent_id' => 99, - 'invoice_number' => 'INV-1', - 'total_amount' => 100.00, - 'balance' => 0.00, - 'paid_amount' => 100.00, - 'issue_date' => '2025-01-01', - 'school_year' => '2025-2026', - 'semester' => 'Fall', - ]); - - DB::table('payments')->insert([ - 'id' => 1, - 'parent_id' => 99, - 'invoice_id' => 1, - 'paid_amount' => 100.00, - 'total_amount' => 100.00, - 'balance' => 0.00, - 'number_of_installments' => 1, - 'payment_date' => '2025-01-10', - 'school_year' => '2025-2026', - 'status' => 'Paid', - ]); - - $service = new FeeRefundCalculatorService( - new FeeConfigService(), - new FeeStudentFeeService(new FeeGradeService(), new FeeConfigService()) - ); + $service = $this->makeService(); $result = $service->calculateRefund([ [ @@ -76,5 +34,249 @@ class FeeRefundCalculatorServiceTest extends TestCase ], 99); $this->assertSame(100.0, $result['refund_amount']); + $this->assertSame(1, $result['withdrawn_students']); + $this->assertCount(1, $result['students']); + $this->assertTrue($result['students'][0]['eligible']); + $this->assertSame(350.0, $result['students'][0]['tuition_fee']); + } + + public function test_returns_zero_when_parent_has_no_payments(): void + { + $this->seedFeeConfig(); + $this->seedClassSection(101, '3'); + + $service = $this->makeService(); + + $result = $service->calculateRefund([ + [ + 'student_id' => 1, + 'class_section_id' => 101, + 'enrollment_status' => 'withdrawn', + 'admission_status' => 'accepted', + 'withdrawal_date' => '2025-01-15', + ], + ], 12345); + + $this->assertSame(0.0, $result['refund_amount']); + $this->assertSame(0, $result['withdrawn_students']); + $this->assertSame([], $result['students']); + } + + public function test_returns_zero_when_no_withdrawn_students(): void + { + $this->seedFeeConfig(); + $this->seedClassSection(101, '3'); + $this->seedInvoiceAndPayment(50, 500.00); + + $service = $this->makeService(); + + $result = $service->calculateRefund([ + [ + 'student_id' => 1, + 'class_section_id' => 101, + 'enrollment_status' => 'enrolled', + 'admission_status' => 'accepted', + ], + ], 50); + + $this->assertSame(0.0, $result['refund_amount']); + $this->assertSame(0, $result['withdrawn_students']); + } + + /** + * Plan section 8 regression: each withdrawn student must carry the + * tuition fee that was assigned during fee calculation, otherwise the + * refund loop silently produces $0 for any student whose id does not + * round-trip through the lookup map. + */ + public function test_withdrawn_student_breakdown_carries_assigned_tuition_fee(): void + { + $this->seedFeeConfig(); + $this->seedClassSection(201, '4'); + $this->seedClassSection(202, '5'); + $this->seedInvoiceAndPayment(77, 1000.00); + + $service = $this->makeService(); + + $result = $service->calculateRefund([ + [ + 'student_id' => 10, + 'class_section_id' => 201, + 'enrollment_status' => 'enrolled', + 'admission_status' => 'accepted', + ], + [ + 'student_id' => 11, + 'class_section_id' => 202, + 'enrollment_status' => 'withdrawn', + 'admission_status' => 'accepted', + 'withdrawal_date' => '2025-01-15', + ], + ], 77); + + $this->assertCount(1, $result['students']); + $withdrawn = $result['students'][0]; + $this->assertSame(11, $withdrawn['student_id']); + // First regular = $350 (grade 4), additional = $200 (grade 5) -> the + // withdrawn 5th-grader is the additional regular student and must + // refund using $200, not $0 or a stale lookup value. + $this->assertSame(200.0, $withdrawn['tuition_fee']); + $this->assertSame('additional_regular', $withdrawn['fee_category']); + $this->assertGreaterThan(0, $withdrawn['refund_amount']); + $this->assertTrue($withdrawn['eligible']); + } + + public function test_multi_student_family_only_refunds_withdrawn_students(): void + { + $this->seedFeeConfig(); + $this->seedClassSection(301, '2'); + $this->seedClassSection(302, '6'); + $this->seedClassSection(303, '10'); + $this->seedInvoiceAndPayment(88, 1000.00); + + $service = $this->makeService(); + + $result = $service->calculateRefund([ + [ + 'student_id' => 21, + 'class_section_id' => 301, + 'enrollment_status' => 'enrolled', + 'admission_status' => 'accepted', + ], + [ + 'student_id' => 22, + 'class_section_id' => 302, + 'enrollment_status' => 'withdrawn', + 'admission_status' => 'accepted', + 'withdrawal_date' => '2025-01-15', + ], + [ + 'student_id' => 23, + 'class_section_id' => 303, + 'enrollment_status' => 'withdrawn', + 'admission_status' => 'accepted', + 'withdrawal_date' => '2025-01-15', + ], + ], 88); + + $this->assertCount(2, $result['students']); + $this->assertSame(2, $result['withdrawn_students']); + + $byStudent = []; + foreach ($result['students'] as $row) { + $byStudent[$row['student_id']] = $row; + } + + $this->assertSame('additional_regular', $byStudent[22]['fee_category']); + $this->assertSame(200.0, $byStudent[22]['tuition_fee']); + + $this->assertSame('youth', $byStudent[23]['fee_category']); + $this->assertSame(180.0, $byStudent[23]['tuition_fee']); + + $sumPerStudent = round(array_sum(array_column($result['students'], 'refund_amount')), 2); + $this->assertSame($result['refund_amount'], $sumPerStudent); + } + + public function test_withdrawal_after_deadline_is_not_eligible(): void + { + $this->seedFeeConfig(); + $this->seedClassSection(401, '3'); + $this->seedInvoiceAndPayment(33, 500.00); + + $service = $this->makeService(); + + $result = $service->calculateRefund([ + [ + 'student_id' => 1, + 'class_section_id' => 401, + 'enrollment_status' => 'withdrawn', + 'admission_status' => 'accepted', + 'withdrawal_date' => '2025-03-01', + ], + ], 33); + + $this->assertSame(0.0, $result['refund_amount']); + $this->assertCount(1, $result['students']); + $this->assertFalse($result['students'][0]['eligible']); + $this->assertSame('after_refund_deadline', $result['students'][0]['reason']); + } + + public function test_missing_withdrawal_date_is_skipped_with_reason(): void + { + $this->seedFeeConfig(); + $this->seedClassSection(501, '3'); + $this->seedInvoiceAndPayment(44, 500.00); + + $service = $this->makeService(); + + $result = $service->calculateRefund([ + [ + 'student_id' => 1, + 'class_section_id' => 501, + 'enrollment_status' => 'withdrawn', + 'admission_status' => 'accepted', + ], + ], 44); + + $this->assertSame(0.0, $result['refund_amount']); + $this->assertSame('missing_withdrawal_date', $result['students'][0]['reason']); + } + + private function makeService(): FeeRefundCalculatorService + { + return new FeeRefundCalculatorService( + new FeeConfigService(), + new FeeStudentFeeService(new FeeGradeService(), new FeeConfigService()) + ); + } + + private function seedFeeConfig(): void + { + DB::table('configuration')->insert([ + ['config_key' => 'school_year', 'config_value' => '2025-2026'], + ['config_key' => 'refund_deadline', 'config_value' => '2025-02-01'], + ['config_key' => 'weeks_study', 'config_value' => '8'], + ['config_key' => 'last_school_day', 'config_value' => '2025-06-01'], + ['config_key' => 'first_student_fee', 'config_value' => '350'], + ['config_key' => 'second_student_fee', 'config_value' => '200'], + ['config_key' => 'youth_fee', 'config_value' => '180'], + ]); + } + + private function seedClassSection(int $sectionId, string $gradeName): void + { + DB::table('classSection')->insert([ + 'class_id' => $sectionId, + 'class_section_id' => $sectionId, + 'class_section_name' => $gradeName, + 'semester' => 'Fall', + 'school_year' => '2025-2026', + ]); + } + + private function seedInvoiceAndPayment(int $parentId, float $amount): void + { + $invoiceId = DB::table('invoices')->insertGetId([ + 'parent_id' => $parentId, + 'invoice_number' => 'INV-' . $parentId, + 'total_amount' => $amount, + 'balance' => 0.00, + 'paid_amount' => $amount, + 'issue_date' => '2025-01-01', + 'school_year' => '2025-2026', + 'semester' => 'Fall', + ]); + + DB::table('payments')->insert([ + 'parent_id' => $parentId, + 'invoice_id' => $invoiceId, + 'paid_amount' => $amount, + 'total_amount' => $amount, + 'balance' => 0.00, + 'number_of_installments' => 1, + 'payment_date' => '2025-01-10', + 'school_year' => '2025-2026', + 'status' => 'Paid', + ]); } } diff --git a/tests/Unit/Services/Fees/FeeStudentFeeServiceTest.php b/tests/Unit/Services/Fees/FeeStudentFeeServiceTest.php index 6b4a669e..ff59b8b6 100644 --- a/tests/Unit/Services/Fees/FeeStudentFeeServiceTest.php +++ b/tests/Unit/Services/Fees/FeeStudentFeeServiceTest.php @@ -15,32 +15,11 @@ class FeeStudentFeeServiceTest extends TestCase public function test_total_tuition_fee_applies_tiers_and_youth_fee(): void { - DB::table('configuration')->insert([ - ['config_key' => 'first_student_fee', 'config_value' => '350'], - ['config_key' => 'second_student_fee', 'config_value' => '200'], - ['config_key' => 'youth_fee', 'config_value' => '180'], - ]); + $this->seedFeeConfig(); + $this->seedClassSection(101, '3'); + $this->seedClassSection(102, '10'); - DB::table('classSection')->insert([ - [ - 'id' => 1, - 'class_id' => 1, - 'class_section_id' => 101, - 'class_section_name' => '3', - 'semester' => 'Fall', - 'school_year' => '2025-2026', - ], - [ - 'id' => 2, - 'class_id' => 2, - 'class_section_id' => 102, - 'class_section_name' => '10', - 'semester' => 'Fall', - 'school_year' => '2025-2026', - ], - ]); - - $service = new FeeStudentFeeService(new FeeGradeService(), new FeeConfigService()); + $service = $this->makeService(); $total = $service->totalTuitionFee([ ['class_section_id' => 101], @@ -49,4 +28,136 @@ class FeeStudentFeeServiceTest extends TestCase $this->assertSame(530.0, $total); } + + public function test_assign_fees_stores_tuition_fee_and_fee_category_on_each_student(): void + { + $this->seedFeeConfig(); + $this->seedClassSection(101, '2'); + $this->seedClassSection(102, '5'); + $this->seedClassSection(103, '10'); + + $service = $this->makeService(); + + $assigned = $service->assignFees([ + ['student_id' => 1, 'class_section_id' => 101], + ['student_id' => 2, 'class_section_id' => 102], + ['student_id' => 3, 'class_section_id' => 103], + ]); + + $byId = []; + foreach ($assigned as $row) { + $byId[$row['student_id']] = $row; + } + + $this->assertSame(350.0, $byId[1]['tuition_fee']); + $this->assertSame('first_regular', $byId[1]['fee_category']); + + $this->assertSame(200.0, $byId[2]['tuition_fee']); + $this->assertSame('additional_regular', $byId[2]['fee_category']); + + $this->assertSame(180.0, $byId[3]['tuition_fee']); + $this->assertSame('youth', $byId[3]['fee_category']); + } + + public function test_calculate_breakdown_includes_billable_and_non_billable_students(): void + { + $this->seedFeeConfig(); + $this->seedClassSection(201, '3'); + $this->seedClassSection(202, '4'); + + $service = $this->makeService(); + + $breakdown = $service->calculateBreakdown([ + [ + 'student_id' => 1, + 'firstname' => 'Ada', + 'lastname' => 'L', + 'class_section_id' => 201, + 'enrollment_status' => 'enrolled', + 'admission_status' => 'accepted', + ], + [ + 'student_id' => 2, + 'firstname' => 'Bo', + 'lastname' => 'L', + 'class_section_id' => 202, + 'enrollment_status' => 'withdrawn', + 'admission_status' => 'accepted', + ], + [ + 'student_id' => 3, + 'firstname' => 'Cy', + 'lastname' => 'L', + 'class_section_id' => 201, + 'enrollment_status' => 'enrolled', + 'admission_status' => 'pending', + ], + ]); + + $this->assertSame(350.0, $breakdown['family_total']); + $this->assertSame(1, $breakdown['billable_count']); + $this->assertSame(2, $breakdown['non_billable_count']); + $this->assertCount(3, $breakdown['students']); + + $byId = []; + foreach ($breakdown['students'] as $row) { + $byId[$row['student_id']] = $row; + } + + $this->assertSame('first_regular', $byId[1]['fee_category']); + $this->assertSame(350.0, $byId[1]['tuition_fee']); + + $this->assertSame('not_billable', $byId[2]['fee_category']); + $this->assertSame(0.0, $byId[2]['tuition_fee']); + + $this->assertSame('not_billable', $byId[3]['fee_category']); + } + + public function test_is_billable_and_is_withdrawn_classifiers(): void + { + $service = $this->makeService(); + + $this->assertTrue($service->isBillable([ + 'enrollment_status' => 'enrolled', + 'admission_status' => 'accepted', + ])); + $this->assertTrue($service->isBillable([ + 'enrollment_status' => 'payment pending', + 'admission_status' => 'accepted', + ])); + $this->assertFalse($service->isBillable([ + 'enrollment_status' => 'enrolled', + 'admission_status' => 'pending', + ])); + + $this->assertTrue($service->isWithdrawn(['enrollment_status' => 'withdrawn'])); + $this->assertTrue($service->isWithdrawn(['enrollment_status' => 'refund pending'])); + $this->assertTrue($service->isWithdrawn(['enrollment_status' => 'withdraw under review'])); + $this->assertFalse($service->isWithdrawn(['enrollment_status' => 'enrolled'])); + } + + private function makeService(): FeeStudentFeeService + { + return new FeeStudentFeeService(new FeeGradeService(), new FeeConfigService()); + } + + private function seedFeeConfig(): void + { + DB::table('configuration')->insert([ + ['config_key' => 'first_student_fee', 'config_value' => '350'], + ['config_key' => 'second_student_fee', 'config_value' => '200'], + ['config_key' => 'youth_fee', 'config_value' => '180'], + ]); + } + + private function seedClassSection(int $sectionId, string $gradeName): void + { + DB::table('classSection')->insert([ + 'class_id' => $sectionId, + 'class_section_id' => $sectionId, + 'class_section_name' => $gradeName, + 'semester' => 'Fall', + 'school_year' => '2025-2026', + ]); + } } diff --git a/tests/Unit/Services/Fees/TuitionCalculationServiceTest.php b/tests/Unit/Services/Fees/TuitionCalculationServiceTest.php new file mode 100644 index 00000000..f9ecaf8d --- /dev/null +++ b/tests/Unit/Services/Fees/TuitionCalculationServiceTest.php @@ -0,0 +1,178 @@ +seedConfig(); + $this->seedClassSection(101, '2'); + $this->seedClassSection(102, '5'); + $this->seedClassSection(103, '11'); + + $service = $this->makeService(); + + $result = $service->calculate([ + [ + 'student_id' => 1, + 'firstname' => 'Anna', + 'lastname' => 'Doe', + 'class_section_id' => 101, + 'enrollment_status' => 'enrolled', + 'admission_status' => 'accepted', + ], + [ + 'student_id' => 2, + 'firstname' => 'Ben', + 'lastname' => 'Doe', + 'class_section_id' => 102, + 'enrollment_status' => 'enrolled', + 'admission_status' => 'accepted', + ], + [ + 'student_id' => 3, + 'firstname' => 'Cara', + 'lastname' => 'Doe', + 'class_section_id' => 103, + 'enrollment_status' => 'enrolled', + 'admission_status' => 'accepted', + ], + ]); + + $this->assertSame('2025-2026', $result['school_year']); + $this->assertSame(3, $result['billable_count']); + $this->assertSame(0, $result['non_billable_count']); + $this->assertSame(730.0, $result['family_total']); + + $byStudent = []; + foreach ($result['students'] as $row) { + $byStudent[$row['student_id']] = $row; + } + + $this->assertSame('first_regular', $byStudent[1]['fee_category']); + $this->assertSame(350.0, $byStudent[1]['tuition_fee']); + $this->assertSame('Anna Doe', $byStudent[1]['student_name']); + + $this->assertSame('additional_regular', $byStudent[2]['fee_category']); + $this->assertSame(200.0, $byStudent[2]['tuition_fee']); + + $this->assertSame('youth', $byStudent[3]['fee_category']); + $this->assertSame(180.0, $byStudent[3]['tuition_fee']); + } + + public function test_calculate_excludes_non_billable_students_from_family_total(): void + { + $this->seedConfig(); + $this->seedClassSection(201, '3'); + $this->seedClassSection(202, '4'); + + $service = $this->makeService(); + + $result = $service->calculate([ + [ + 'student_id' => 10, + 'class_section_id' => 201, + 'enrollment_status' => 'enrolled', + 'admission_status' => 'accepted', + ], + [ + 'student_id' => 11, + 'class_section_id' => 202, + 'enrollment_status' => 'withdrawn', + 'admission_status' => 'accepted', + ], + ]); + + $this->assertSame(1, $result['billable_count']); + $this->assertSame(1, $result['non_billable_count']); + $this->assertSame(350.0, $result['family_total']); + + $byStudent = []; + foreach ($result['students'] as $row) { + $byStudent[$row['student_id']] = $row; + } + + $this->assertSame('not_billable', $byStudent[11]['fee_category']); + $this->assertSame(0.0, $byStudent[11]['tuition_fee']); + } + + public function test_calculate_applies_per_student_discount(): void + { + $this->seedConfig(); + $this->seedClassSection(301, '2'); + + $service = $this->makeService(); + + $result = $service->calculate([ + [ + 'student_id' => 99, + 'class_section_id' => 301, + 'enrollment_status' => 'enrolled', + 'admission_status' => 'accepted', + 'discount_amount' => 100.0, + ], + ]); + + $this->assertSame(250.0, $result['family_total']); + $this->assertSame(100.0, $result['students'][0]['discount_amount']); + $this->assertSame(250.0, $result['students'][0]['final_tuition_amount']); + } + + public function test_family_total_helper(): void + { + $this->seedConfig(); + $this->seedClassSection(401, '3'); + + $service = $this->makeService(); + + $this->assertSame(350.0, $service->familyTotal([ + [ + 'student_id' => 1, + 'class_section_id' => 401, + 'enrollment_status' => 'enrolled', + 'admission_status' => 'accepted', + ], + ])); + } + + private function makeService(): TuitionCalculationService + { + $config = new FeeConfigService(); + return new TuitionCalculationService( + $config, + new FeeStudentFeeService(new FeeGradeService(), $config) + ); + } + + private function seedConfig(): void + { + DB::table('configuration')->insert([ + ['config_key' => 'school_year', 'config_value' => '2025-2026'], + ['config_key' => 'first_student_fee', 'config_value' => '350'], + ['config_key' => 'second_student_fee', 'config_value' => '200'], + ['config_key' => 'youth_fee', 'config_value' => '180'], + ]); + } + + private function seedClassSection(int $sectionId, string $gradeName): void + { + DB::table('classSection')->insert([ + 'class_id' => $sectionId, + 'class_section_id' => $sectionId, + 'class_section_name' => $gradeName, + 'semester' => 'Fall', + 'school_year' => '2025-2026', + ]); + } +} diff --git a/tests/Unit/Services/Payments/PaymentEventChargesServiceTest.php b/tests/Unit/Services/Payments/PaymentEventChargesServiceTest.php index c11f82ca..98cddfd6 100644 --- a/tests/Unit/Services/Payments/PaymentEventChargesServiceTest.php +++ b/tests/Unit/Services/Payments/PaymentEventChargesServiceTest.php @@ -72,7 +72,7 @@ class PaymentEventChargesServiceTest extends TestCase 'updated_by' => null, ]); - $service = new PaymentEventChargesService(); + $service = app(PaymentEventChargesService::class); $students = $service->getEnrolledStudents(10, '2025-2026'); $this->assertCount(1, $students); diff --git a/tests/Unit/Services/Promotions/LevelProgressionServiceTest.php b/tests/Unit/Services/Promotions/LevelProgressionServiceTest.php new file mode 100644 index 00000000..f20daddd --- /dev/null +++ b/tests/Unit/Services/Promotions/LevelProgressionServiceTest.php @@ -0,0 +1,86 @@ +seedDefaults(); + $this->assertGreaterThan(0, $created); + $this->assertSame($created, LevelProgression::query()->count()); + $this->assertDatabaseHas('level_progressions', [ + 'current_level_name' => 'Grade 3', + 'next_level_name' => 'Grade 4', + ]); + $this->assertDatabaseHas('level_progressions', [ + 'current_level_name' => 'Youth', + 'is_terminal' => 1, + ]); + } + + public function test_resolve_by_class_id_uses_mapping_when_present(): void + { + DB::table('classes')->insert([ + ['id' => 7, 'class_name' => 'Grade 3', 'school_year' => '2025-2026', 'semester' => 'Fall', 'capacity' => 0], + ['id' => 8, 'class_name' => 'Grade 4', 'school_year' => '2025-2026', 'semester' => 'Fall', 'capacity' => 0], + ]); + + $service = new LevelProgressionService(); + $service->seedDefaults(); + + $resolved = $service->resolveByCurrentClassId(7); + $this->assertNotNull($resolved); + $this->assertSame('Grade 3', $resolved['current_level_name']); + $this->assertSame('Grade 4', $resolved['next_level_name']); + $this->assertSame(8, $resolved['next_level_id']); + $this->assertFalse($resolved['is_terminal']); + } + + public function test_resolve_falls_back_to_class_id_plus_one_when_no_mapping(): void + { + DB::table('classes')->insert([ + ['id' => 100, 'class_name' => 'Custom A', 'school_year' => '2025-2026', 'semester' => 'Fall', 'capacity' => 0], + ['id' => 101, 'class_name' => 'Custom B', 'school_year' => '2025-2026', 'semester' => 'Fall', 'capacity' => 0], + ]); + + $service = new LevelProgressionService(); + $resolved = $service->resolveByCurrentClassId(100); + + $this->assertNotNull($resolved); + $this->assertSame('Custom A', $resolved['current_level_name']); + $this->assertSame('Custom B', $resolved['next_level_name']); + $this->assertSame(101, $resolved['next_level_id']); + } + + public function test_upsert_mapping_updates_existing_row(): void + { + $service = new LevelProgressionService(); + $service->upsertMapping([ + 'current_level_name' => 'Grade 5', + 'next_level_name' => 'Grade 6', + 'order_index' => 70, + ]); + $service->upsertMapping([ + 'current_level_name' => 'Grade 5', + 'next_level_name' => 'Grade 6 (Updated)', + 'order_index' => 75, + ]); + + $this->assertDatabaseHas('level_progressions', [ + 'current_level_name' => 'Grade 5', + 'next_level_name' => 'Grade 6 (Updated)', + 'order_index' => 75, + ]); + $this->assertSame(1, LevelProgression::query()->where('current_level_name', 'Grade 5')->count()); + } +} diff --git a/tests/Unit/Services/Promotions/PromotionEligibilityServiceTest.php b/tests/Unit/Services/Promotions/PromotionEligibilityServiceTest.php new file mode 100644 index 00000000..ea0c1e05 --- /dev/null +++ b/tests/Unit/Services/Promotions/PromotionEligibilityServiceTest.php @@ -0,0 +1,169 @@ +seedConfig(); + $studentId = $this->seedStudent(); + $this->seedClassChain(); + $this->seedAssignment($studentId, 701, '2025-2026'); + $this->seedScores($studentId, '2025-2026', 80.0, 75.0); + + $service = $this->makeService(); + $record = $service->evaluateStudent($studentId, '2025-2026', 1); + + $this->assertNotNull($record); + $this->assertSame(StudentPromotionRecord::STATUS_AWAITING_PARENT, $record->promotion_status); + $this->assertTrue((bool) $record->passed_current_level); + $this->assertSame(77.5, (float) $record->final_average); + $this->assertSame('Grade 3', $record->current_level_name); + $this->assertSame('Grade 4', $record->promoted_level_name); + $this->assertSame('2026-2027', $record->next_school_year); + } + + public function test_failing_student_becomes_repeated(): void + { + $this->seedConfig(); + $studentId = $this->seedStudent(); + $this->seedClassChain(); + $this->seedAssignment($studentId, 701, '2025-2026'); + $this->seedScores($studentId, '2025-2026', 50.0, 55.0); + + $service = $this->makeService(); + $record = $service->evaluateStudent($studentId, '2025-2026', 1); + + $this->assertNotNull($record); + $this->assertSame(StudentPromotionRecord::STATUS_REPEATED, $record->promotion_status); + $this->assertFalse((bool) $record->passed_current_level); + } + + public function test_missing_scores_marks_on_hold(): void + { + $this->seedConfig(); + $studentId = $this->seedStudent(); + $this->seedClassChain(); + $this->seedAssignment($studentId, 701, '2025-2026'); + + $service = $this->makeService(); + $record = $service->evaluateStudent($studentId, '2025-2026', 1); + + $this->assertNotNull($record); + $this->assertSame(StudentPromotionRecord::STATUS_ON_HOLD, $record->promotion_status); + $this->assertNull($record->passed_current_level); + } + + public function test_terminal_level_is_graduated(): void + { + $this->seedConfig(); + $studentId = $this->seedStudent(); + $this->seedClassChain(); + $this->seedAssignment($studentId, 901, '2025-2026'); + $this->seedScores($studentId, '2025-2026', 90.0, 90.0); + + // Mark Youth as terminal + \App\Models\LevelProgression::query()->where('current_level_name', 'Youth')->update(['is_terminal' => 1]); + + $service = $this->makeService(); + $record = $service->evaluateStudent($studentId, '2025-2026', 1); + + $this->assertNotNull($record); + $this->assertSame(StudentPromotionRecord::STATUS_GRADUATED, $record->promotion_status); + } + + public function test_audit_log_records_status_changes(): void + { + $this->seedConfig(); + $studentId = $this->seedStudent(); + $this->seedClassChain(); + $this->seedAssignment($studentId, 701, '2025-2026'); + $this->seedScores($studentId, '2025-2026', 80.0, 80.0); + + $service = $this->makeService(); + $service->evaluateStudent($studentId, '2025-2026', 99); + + $this->assertDatabaseHas('promotion_audit_log', [ + 'student_id' => $studentId, + 'action_type' => 'eligibility_evaluated', + 'user_id' => 99, + ]); + $this->assertDatabaseHas('promotion_audit_log', [ + 'student_id' => $studentId, + 'action_type' => 'record_created', + ]); + } + + private function makeService(): PromotionEligibilityService + { + $progression = new LevelProgressionService(); + $audit = new PromotionAuditService(); + $status = new PromotionStatusService($audit); + return new PromotionEligibilityService($progression, $status, $audit); + } + + private function seedConfig(): void + { + DB::table('configuration')->insert([ + ['config_key' => 'school_year', 'config_value' => '2025-2026'], + ['config_key' => 'semester', 'config_value' => 'Fall'], + ]); + } + + private function seedStudent(int $parentId = 5): int + { + return DB::table('students')->insertGetId([ + 'school_id' => 'S-100', + 'firstname' => 'Sara', + 'lastname' => 'Test', + 'parent_id' => $parentId, + 'year_of_registration' => '2025', + 'school_year' => '2025-2026', + 'semester' => 'Fall', + ]); + } + + private function seedClassChain(): void + { + DB::table('classes')->insert([ + ['id' => 7, 'class_name' => 'Grade 3', 'school_year' => '2025-2026', 'semester' => 'Fall', 'capacity' => 30], + ['id' => 8, 'class_name' => 'Grade 4', 'school_year' => '2025-2026', 'semester' => 'Fall', 'capacity' => 30], + ['id' => 13, 'class_name' => 'Youth', 'school_year' => '2025-2026', 'semester' => 'Fall', 'capacity' => 30], + ]); + DB::table('classSection')->insert([ + ['class_id' => 7, 'class_section_id' => 701, 'class_section_name' => '3A'], + ['class_id' => 13, 'class_section_id' => 901, 'class_section_name' => 'Youth A'], + ]); + } + + private function seedAssignment(int $studentId, int $classSectionId, string $schoolYear): void + { + DB::table('student_class')->insert([ + 'student_id' => $studentId, + 'class_section_id' => $classSectionId, + 'school_year' => $schoolYear, + 'semester' => 'Fall', + 'is_event_only' => 0, + ]); + } + + private function seedScores(int $studentId, string $schoolYear, float $fall, float $spring): void + { + DB::table('semester_scores')->insert([ + ['student_id' => $studentId, 'class_section_id' => 0, 'school_year' => $schoolYear, 'semester' => 'fall', 'semester_score' => $fall], + ['student_id' => $studentId, 'class_section_id' => 0, 'school_year' => $schoolYear, 'semester' => 'spring', 'semester_score' => $spring], + ]); + } +} diff --git a/tests/Unit/Services/Promotions/PromotionEnrollmentServiceTest.php b/tests/Unit/Services/Promotions/PromotionEnrollmentServiceTest.php new file mode 100644 index 00000000..efa98d9c --- /dev/null +++ b/tests/Unit/Services/Promotions/PromotionEnrollmentServiceTest.php @@ -0,0 +1,146 @@ +bootstrap(); + + $service->updateSteps($record, $parentId, [ + 'info_confirmed' => true, + 'documents_uploaded' => true, + 'agreement_accepted' => true, + ], $parentId); + + $service->updateSteps($record, $parentId, [ + 'payment_completed' => true, + ], $parentId); + + $record->refresh(); + $this->assertSame(StudentPromotionRecord::STATUS_PROMOTED_AND_ENROLLED, $record->promotion_status); + $this->assertSame(StudentPromotionRecord::ENROLLMENT_COMPLETED, $record->enrollment_status); + $this->assertNotNull($record->promotion_finalized_at); + $this->assertDatabaseHas('enrollments', [ + 'student_id' => $record->student_id, + 'school_year' => $record->next_school_year, + 'enrollment_status' => 'enrolled', + 'admission_status' => 'accepted', + ]); + $this->assertDatabaseHas('promotion_audit_log', [ + 'promotion_id' => $record->getKey(), + 'action_type' => 'promotion_finalized', + ]); + } + + public function test_partial_checklist_keeps_status_in_progress(): void + { + [$record, $service, $parentId] = $this->bootstrap(); + + $service->updateSteps($record, $parentId, [ + 'info_confirmed' => true, + 'documents_uploaded' => true, + ], $parentId); + + $record->refresh(); + $this->assertSame(StudentPromotionRecord::STATUS_ENROLLMENT_STARTED, $record->promotion_status); + $this->assertSame(StudentPromotionRecord::ENROLLMENT_IN_PROGRESS, $record->enrollment_status); + $this->assertNull($record->promotion_finalized_at); + } + + public function test_submit_requires_complete_checklist(): void + { + [$record, $service, $parentId] = $this->bootstrap(); + + $service->updateSteps($record, $parentId, [ + 'info_confirmed' => true, + ], $parentId); + + $this->expectException(RuntimeException::class); + $service->submitEnrollment($record->refresh(), $parentId, $parentId); + } + + public function test_other_parent_cannot_act_on_record(): void + { + [$record, $service] = $this->bootstrap(); + + $this->expectException(RuntimeException::class); + $service->startEnrollment($record, 9999, 9999); + } + + public function test_expired_records_become_not_enrolled(): void + { + [$record, $service] = $this->bootstrap(); + $record->enrollment_deadline = now()->subDays(2)->toDateString(); + $record->save(); + + $result = $service->markExpiredDeadlines(CarbonImmutable::now(), 99); + + $this->assertSame(1, $result['expired']); + $record->refresh(); + $this->assertSame(StudentPromotionRecord::STATUS_NOT_ENROLLED, $record->promotion_status); + $this->assertSame(StudentPromotionRecord::ENROLLMENT_EXPIRED, $record->enrollment_status); + } + + /** + * @return array{0:StudentPromotionRecord, 1:PromotionEnrollmentService, 2:int} + */ + private function bootstrap(): array + { + $audit = new PromotionAuditService(); + $service = new PromotionEnrollmentService(new PromotionStatusService($audit), $audit); + + $parentId = (int) DB::table('users')->insertGetId([ + 'firstname' => 'Parent', + 'lastname' => 'One', + 'email' => 'parent@example.com', + 'cellphone' => '5551234567', + 'address_street' => '1 Way', + 'city' => 'C', + 'state' => 'S', + 'zip' => '00000', + 'accept_school_policy' => 1, + 'password' => bcrypt('x'), + 'school_year' => '2025-2026', + 'semester' => 'Fall', + 'status' => 'Active', + ]); + + $studentId = (int) DB::table('students')->insertGetId([ + 'school_id' => 'S-1', + 'firstname' => 'Sara', + 'lastname' => 'Test', + 'parent_id' => $parentId, + 'year_of_registration' => '2025', + 'school_year' => '2025-2026', + 'semester' => 'Fall', + ]); + + $record = StudentPromotionRecord::query()->create([ + 'student_id' => $studentId, + 'parent_id' => $parentId, + 'current_school_year' => '2025-2026', + 'next_school_year' => '2026-2027', + 'promotion_status' => StudentPromotionRecord::STATUS_AWAITING_PARENT, + 'enrollment_required' => true, + 'enrollment_status' => StudentPromotionRecord::ENROLLMENT_NOT_STARTED, + 'promoted_level_name' => 'Grade 4', + 'enrollment_deadline' => now()->addDays(30)->toDateString(), + ]); + + return [$record, $service, $parentId]; + } +} diff --git a/tests/Unit/Services/Promotions/PromotionStatusServiceTest.php b/tests/Unit/Services/Promotions/PromotionStatusServiceTest.php new file mode 100644 index 00000000..c4e157f6 --- /dev/null +++ b/tests/Unit/Services/Promotions/PromotionStatusServiceTest.php @@ -0,0 +1,110 @@ +makeRecord(StudentPromotionRecord::STATUS_AWAITING_PARENT); + $service = $this->makeService(); + + $updated = $service->transition( + $record, + StudentPromotionRecord::STATUS_ENROLLMENT_STARTED, + 42, + 'parent began enrollment' + ); + + $this->assertSame(StudentPromotionRecord::STATUS_ENROLLMENT_STARTED, $updated->promotion_status); + $this->assertDatabaseHas('promotion_audit_log', [ + 'promotion_id' => $record->getKey(), + 'action_type' => 'status_changed', + 'old_value' => StudentPromotionRecord::STATUS_AWAITING_PARENT, + 'new_value' => StudentPromotionRecord::STATUS_ENROLLMENT_STARTED, + 'user_id' => 42, + ]); + } + + public function test_disallowed_transition_throws(): void + { + $record = $this->makeRecord(StudentPromotionRecord::STATUS_NOT_REVIEWED); + $service = $this->makeService(); + + $this->expectException(RuntimeException::class); + $service->transition($record, StudentPromotionRecord::STATUS_PROMOTED_AND_ENROLLED, 1); + } + + public function test_force_status_bypasses_transition_map(): void + { + $record = $this->makeRecord(StudentPromotionRecord::STATUS_NOT_REVIEWED); + $service = $this->makeService(); + + $updated = $service->forceStatus($record, StudentPromotionRecord::STATUS_PROMOTED_AND_ENROLLED, 7, 'manual fix'); + + $this->assertSame(StudentPromotionRecord::STATUS_PROMOTED_AND_ENROLLED, $updated->promotion_status); + $this->assertDatabaseHas('promotion_audit_log', [ + 'promotion_id' => $record->getKey(), + 'action_type' => 'manual_override', + 'field' => 'promotion_status', + 'old_value' => StudentPromotionRecord::STATUS_NOT_REVIEWED, + 'new_value' => StudentPromotionRecord::STATUS_PROMOTED_AND_ENROLLED, + ]); + } + + public function test_no_op_transition_returns_record_unchanged(): void + { + $record = $this->makeRecord(StudentPromotionRecord::STATUS_AWAITING_PARENT); + $service = $this->makeService(); + + $updated = $service->transition( + $record, + StudentPromotionRecord::STATUS_AWAITING_PARENT, + 1 + ); + + $this->assertSame(StudentPromotionRecord::STATUS_AWAITING_PARENT, $updated->promotion_status); + $this->assertDatabaseMissing('promotion_audit_log', [ + 'promotion_id' => $record->getKey(), + 'action_type' => 'status_changed', + ]); + } + + private function makeService(): PromotionStatusService + { + return new PromotionStatusService(new PromotionAuditService()); + } + + private function makeRecord(string $status): StudentPromotionRecord + { + $studentId = DB::table('students')->insertGetId([ + 'school_id' => 'S-1', + 'firstname' => 'Test', + 'lastname' => 'Student', + 'parent_id' => 1, + 'year_of_registration' => '2025', + 'school_year' => '2025-2026', + 'semester' => 'Fall', + ]); + + return StudentPromotionRecord::query()->create([ + 'student_id' => $studentId, + 'parent_id' => 1, + 'current_school_year' => '2025-2026', + 'next_school_year' => '2026-2027', + 'promotion_status' => $status, + 'enrollment_required' => true, + 'enrollment_status' => StudentPromotionRecord::ENROLLMENT_NOT_STARTED, + ]); + } +}