From 60ecacb7f8133cece499e6db145b269d7a4dbf59 Mon Sep 17 00:00:00 2001 From: root Date: Mon, 8 Jun 2026 22:06:30 -0400 Subject: [PATCH] update tests --- .../Api/Auth/AuthSessionController.php | 8 +- app/Models/Notification.php | 37 +- app/Models/PaymentTransaction.php | 4 +- app/Models/StudentAllergy.php | 2 +- app/Models/StudentMedicalCondition.php | 2 +- .../TeacherAttendanceSubmissionService.php | 25 +- app/Services/Auth/ApiLoginSecurityService.php | 11 +- .../ParentAttendanceReportCalendarService.php | 23 +- .../Parents/ParentRegistrationService.php | 1 + .../PaymentNotificationDispatchService.php | 2 +- app/Services/SemesterRangeService.php | 4 +- .../Teachers/TeacherDashboardService.php | 2 +- bootstrap/cache/packages.php | 0 bootstrap/cache/services.php | 0 docker-compose.test.yml | 21 + docs/alrahma_end_to_end_use_case_test_plan.md | 2075 +++ failed-tests.txt | Bin 0 -> 122 bytes gitlab-ci.yml | 124 +- phpunit.xml | 10 +- resources/docs/controllers.md | 2 +- resources/docs/openapi.json | 14293 ++++++++++------ .../wa_439ca9047bd5e69a212f1371da361efe.png | Bin 0 -> 1423 bytes .../wa_cdc39b246c6ed974bf58c5f610ab924c.png | Bin 0 -> 1427 bytes test-output.txt | Bin 0 -> 117880 bytes tests/Concerns/SeedsE2ETestFixtures.php | 279 + .../ApiAuthenticationAndAuthorizationTest.php | 48 + .../AdministratorDashboardControllerTest.php | 70 + .../V1/Administrator/TrophyControllerTest.php | 89 + .../V1/BadgeScan/BadgeScanControllerTest.php | 53 + .../V1/Finance/EventChargeControllerTest.php | 118 + .../Parents/AuthorizedUsersControllerTest.php | 106 + .../V1/Public/PublicWinnersControllerTest.php | 82 + .../Feature/Api/V1/ScannerControllerTest.php | 42 + .../Api/V1/System/StatsControllerTest.php | 50 + .../V1/Utilities/ProofreadControllerTest.php | 67 + .../ScenarioAFamilyEnrollmentTest.php | 104 + .../ScenarioBSundaySchoolDayTest.php | 107 + .../Workflows/ScenarioCAcademicTermTest.php | 95 + .../Workflows/ScenarioDFamilyBillingTest.php | 92 + .../V1/Workflows/ScenarioEFinanceEdgeTest.php | 122 + .../ScenarioFTeacherSubmissionTest.php | 93 + .../V1/Workflows/ScenarioGPrintBadgeTest.php | 75 + ...cenarioHSecurityUnauthorizedAccessTest.php | 74 + .../StudentEnrollmentWorkflowTest.php | 282 + tests/Unit/ApiLoginSecurityServiceTest.php | 4 + tests/Unit/AuthSessionServiceTest.php | 6 +- ...ttendanceCommentTemplateControllerTest.php | 28 +- .../NotificationUserListServiceTest.php | 4 +- .../ParentAttendanceReportServiceTest.php | 3 + .../PaymentEnrollmentEventServiceTest.php | 10 +- .../PaymentMissedCheckServiceTest.php | 9 + ...PaymentNotificationDispatchServiceTest.php | 2 +- .../PaymentTestNotificationServiceTest.php | 34 +- .../Payments/PaypalPaymentServiceTest.php | 10 +- 54 files changed, 13243 insertions(+), 5561 deletions(-) mode change 100755 => 100644 bootstrap/cache/packages.php mode change 100755 => 100644 bootstrap/cache/services.php create mode 100644 docker-compose.test.yml create mode 100644 docs/alrahma_end_to_end_use_case_test_plan.md create mode 100644 failed-tests.txt create mode 100644 storage/app/qrcodes/wa_439ca9047bd5e69a212f1371da361efe.png create mode 100644 storage/app/qrcodes/wa_cdc39b246c6ed974bf58c5f610ab924c.png create mode 100644 test-output.txt create mode 100644 tests/Concerns/SeedsE2ETestFixtures.php create mode 100644 tests/Feature/Api/V1/Administrator/AdministratorDashboardControllerTest.php create mode 100644 tests/Feature/Api/V1/Administrator/TrophyControllerTest.php create mode 100644 tests/Feature/Api/V1/BadgeScan/BadgeScanControllerTest.php create mode 100644 tests/Feature/Api/V1/Finance/EventChargeControllerTest.php create mode 100644 tests/Feature/Api/V1/Parents/AuthorizedUsersControllerTest.php create mode 100644 tests/Feature/Api/V1/Public/PublicWinnersControllerTest.php create mode 100644 tests/Feature/Api/V1/ScannerControllerTest.php create mode 100644 tests/Feature/Api/V1/System/StatsControllerTest.php create mode 100644 tests/Feature/Api/V1/Utilities/ProofreadControllerTest.php create mode 100644 tests/Feature/Api/V1/Workflows/ScenarioAFamilyEnrollmentTest.php create mode 100644 tests/Feature/Api/V1/Workflows/ScenarioBSundaySchoolDayTest.php create mode 100644 tests/Feature/Api/V1/Workflows/ScenarioCAcademicTermTest.php create mode 100644 tests/Feature/Api/V1/Workflows/ScenarioDFamilyBillingTest.php create mode 100644 tests/Feature/Api/V1/Workflows/ScenarioEFinanceEdgeTest.php create mode 100644 tests/Feature/Api/V1/Workflows/ScenarioFTeacherSubmissionTest.php create mode 100644 tests/Feature/Api/V1/Workflows/ScenarioGPrintBadgeTest.php create mode 100644 tests/Feature/Api/V1/Workflows/ScenarioHSecurityUnauthorizedAccessTest.php create mode 100644 tests/Feature/Api/V1/Workflows/StudentEnrollmentWorkflowTest.php diff --git a/app/Http/Controllers/Api/Auth/AuthSessionController.php b/app/Http/Controllers/Api/Auth/AuthSessionController.php index f7c83949..449acf5b 100644 --- a/app/Http/Controllers/Api/Auth/AuthSessionController.php +++ b/app/Http/Controllers/Api/Auth/AuthSessionController.php @@ -12,6 +12,7 @@ use App\Services\Auth\AuthSessionService; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; +use PHPOpenSourceSaver\JWTAuth\Facades\JWTAuth; /** * Session (cookie) endpoints for SPA — JSON alongside JWT on /api/login. @@ -141,12 +142,13 @@ class AuthSessionController extends Controller ], 403); } - return response()->json([ - 'status' => true, + $loginPayload = $this->security->buildLoginResponse($fresh, JWTAuth::fromUser($fresh)); + + return response()->json(array_merge($loginPayload, [ 'requires_role_selection' => ($dest['kind'] ?? '') === 'select_role', 'roles' => $dest['roles'] ?? null, 'next_url' => $dest['redirect_url'] ?? $this->urls->docsHomeUrl(false), - ]); + ])); } /** Closes the current web session. */ diff --git a/app/Models/Notification.php b/app/Models/Notification.php index 15340ecc..94b895b9 100644 --- a/app/Models/Notification.php +++ b/app/Models/Notification.php @@ -3,6 +3,7 @@ namespace App\Models; use App\Models\BaseModel; +use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\SoftDeletes; class Notification extends BaseModel @@ -28,12 +29,40 @@ class Notification extends BaseModel ]; protected $casts = [ - 'scheduled_at' => 'datetime', - 'expires_at' => 'datetime', - // If delivery_channels is JSON in DB: - 'delivery_channels' => 'array', + 'scheduled_at' => 'datetime', + 'expires_at' => 'datetime', ]; + /** + * MySQL stores channels as SET('in_app','email','sms'); expose as array in PHP. + */ + protected function deliveryChannels(): Attribute + { + return Attribute::make( + get: static function (?string $value): array { + if ($value === null || $value === '') { + return []; + } + + $trimmed = trim($value); + if (str_starts_with($trimmed, '[')) { + $decoded = json_decode($trimmed, true); + + return is_array($decoded) ? array_values($decoded) : []; + } + + return array_values(array_filter(array_map('trim', explode(',', $value)))); + }, + set: static function ($value): string { + if (is_array($value)) { + return implode(',', array_values(array_filter(array_map('strval', $value)))); + } + + return (string) $value; + } + ); + } + /** * Equivalent of legacy getActiveNotifications() * Active = scheduled_at <= now AND (expires_at is null OR expires_at > now) diff --git a/app/Models/PaymentTransaction.php b/app/Models/PaymentTransaction.php index 198369e9..506323b2 100644 --- a/app/Models/PaymentTransaction.php +++ b/app/Models/PaymentTransaction.php @@ -9,8 +9,8 @@ class PaymentTransaction extends BaseModel { protected $table = 'payment_transactions'; - // ✅ legacy: useTimestamps = true (created_at/updated_at) - public $timestamps = true; + // Legacy table has no created_at/updated_at columns. + public $timestamps = false; protected $fillable = [ 'transaction_id', diff --git a/app/Models/StudentAllergy.php b/app/Models/StudentAllergy.php index 3f8c9765..83c1ea7b 100644 --- a/app/Models/StudentAllergy.php +++ b/app/Models/StudentAllergy.php @@ -13,7 +13,7 @@ class StudentAllergy extends BaseModel /** * legacy: timestamps disabled */ - public $timestamps = true; + public $timestamps = false; protected $fillable = [ 'student_id', diff --git a/app/Models/StudentMedicalCondition.php b/app/Models/StudentMedicalCondition.php index 262cb527..2f1209ff 100644 --- a/app/Models/StudentMedicalCondition.php +++ b/app/Models/StudentMedicalCondition.php @@ -13,7 +13,7 @@ class StudentMedicalCondition extends BaseModel /** * legacy: timestamps disabled */ - public $timestamps = true; + public $timestamps = false; protected $fillable = [ 'student_id', diff --git a/app/Services/Attendance/TeacherAttendanceSubmissionService.php b/app/Services/Attendance/TeacherAttendanceSubmissionService.php index 19617a6a..48798821 100644 --- a/app/Services/Attendance/TeacherAttendanceSubmissionService.php +++ b/app/Services/Attendance/TeacherAttendanceSubmissionService.php @@ -9,6 +9,7 @@ use App\Models\TeacherClass; use Carbon\Carbon; use Illuminate\Contracts\Auth\Authenticatable; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Schema; use RuntimeException; class TeacherAttendanceSubmissionService @@ -171,6 +172,20 @@ class TeacherAttendanceSubmissionService $position = strtolower((string)($teacher['position'] ?? 'main')); $position = $position === 'ta' ? 'ta' : 'main'; + $staffPayload = [ + 'role_name' => 'Teacher', + 'status' => strtolower((string)($teachersData[$teacherId]['status'] ?? 'present')), + 'reason' => ($teachersData[$teacherId]['reason'] ?? null) ?: null, + 'updated_at' => now(), + 'created_at' => now(), + ]; + if (Schema::hasColumn('staff_attendance', 'class_section_id')) { + $staffPayload['class_section_id'] = $classSectionId; + } + if (Schema::hasColumn('staff_attendance', 'position')) { + $staffPayload['position'] = $position; + } + DB::table('staff_attendance')->updateOrInsert( [ 'user_id' => $teacherId, @@ -178,15 +193,7 @@ class TeacherAttendanceSubmissionService 'semester' => $semester, 'school_year' => $schoolYear, ], - [ - 'role_name' => 'Teacher', - 'class_section_id' => $classSectionId, - 'position' => $position, - 'status' => strtolower((string)($teachersData[$teacherId]['status'] ?? 'present')), - 'reason' => ($teachersData[$teacherId]['reason'] ?? null) ?: null, - 'updated_at' => now(), - 'created_at' => now(), - ] + $staffPayload ); } diff --git a/app/Services/Auth/ApiLoginSecurityService.php b/app/Services/Auth/ApiLoginSecurityService.php index f73f8de5..1c3b18b5 100644 --- a/app/Services/Auth/ApiLoginSecurityService.php +++ b/app/Services/Auth/ApiLoginSecurityService.php @@ -129,7 +129,7 @@ class ApiLoginSecurityService /** * Top-level JSON body for API login success responses. * - * @return array{status: true, token: string, user: array{id: int, name: string, roles: object, class_section_id: ?int, class_section_name: ?string}} + * @return array{status: true, token: string, access_token: string, token_type: string, expires_in: int, user: array{id: int, name: string, firstname: ?string, lastname: ?string, email: ?string, roles: object, class_section_id: ?int, class_section_name: ?string}} */ public function buildLoginResponse(User $user, string $jwtToken): array { @@ -139,10 +139,19 @@ class ApiLoginSecurityService return [ 'status' => true, + // Keep both names. Older clients read `token`; JWT-aware clients often read + // `access_token`. Removing either one just makes login fail in a new and + // irritatingly avoidable way. 'token' => $jwtToken, + 'access_token' => $jwtToken, + 'token_type' => 'bearer', + 'expires_in' => (int) config('jwt.ttl') * 60, 'user' => [ 'id' => (int) $user->id, 'name' => $fullName, + 'firstname' => $user->firstname, + 'lastname' => $user->lastname, + 'email' => $user->email, 'roles' => $this->rolesToObjectMap($roleNames), 'class_section_id' => $teacherContext['class_section_id'], 'class_section_name' => $teacherContext['class_section_name'], diff --git a/app/Services/Parents/ParentAttendanceReportCalendarService.php b/app/Services/Parents/ParentAttendanceReportCalendarService.php index 1bccf3d5..f7f03c12 100644 --- a/app/Services/Parents/ParentAttendanceReportCalendarService.php +++ b/app/Services/Parents/ParentAttendanceReportCalendarService.php @@ -15,17 +15,17 @@ class ParentAttendanceReportCalendarService */ public function computeSundays(int $weeksBefore = 4, int $weeksAfter = 26): array { - $today = new \DateTime('today'); + $today = now()->startOfDay(); $dow = (int) $today->format('w'); - $thisSunday = clone $today; + $thisSunday = $today->copy(); if ($dow !== 0) { $thisSunday->modify('last sunday'); } - $default = clone $today; + $default = $today->copy(); if ($dow !== 0) { - $default->modify('next sunday'); + $default = $default->copy()->modify('next sunday'); } $schoolYear = (string) ($this->configService->context()['school_year'] ?? ''); @@ -33,13 +33,13 @@ class ParentAttendanceReportCalendarService $dates = []; for ($i = $weeksBefore; $i >= 1; $i--) { - $dates[] = (clone $thisSunday)->modify('-' . $i . ' week'); + $dates[] = $thisSunday->copy()->modify('-' . $i . ' week'); } - $cursor = clone $thisSunday; + $cursor = $thisSunday->copy(); while ($cursor <= $cap) { - $dates[] = clone $cursor; - $cursor->modify('+1 week'); + $dates[] = $cursor->copy(); + $cursor = $cursor->copy()->modify('+1 week'); } $rangeStart = reset($dates); @@ -108,7 +108,7 @@ class ParentAttendanceReportCalendarService public function firstSundayOfJune(string $schoolYear): \DateTime { - $today = new \DateTime('today'); + $today = now()->startOfDay(); $endYear = null; if (preg_match('/^(\\d{4})\\D(\\d{4})$/', $schoolYear, $m)) { $endYear = (int) $m[2]; @@ -118,10 +118,11 @@ class ParentAttendanceReportCalendarService $endYear = ((int) $today->format('n') > 6) ? ($y + 1) : $y; } - $juneFirst = new \DateTime(sprintf('%04d-06-01', $endYear)); + $juneFirst = now()->parse(sprintf('%04d-06-01', $endYear))->startOfDay(); if ((int) $juneFirst->format('w') !== 0) { - $juneFirst->modify('next sunday'); + $juneFirst = $juneFirst->copy()->modify('next sunday'); } + return $juneFirst; } } diff --git a/app/Services/Parents/ParentRegistrationService.php b/app/Services/Parents/ParentRegistrationService.php index fc4c4e31..ba0a7a7f 100644 --- a/app/Services/Parents/ParentRegistrationService.php +++ b/app/Services/Parents/ParentRegistrationService.php @@ -82,6 +82,7 @@ class ParentRegistrationService $emergencyContacts, $schoolYear, $semester, + $ageReference, &$createdStudentIds, &$skippedStudents ) { diff --git a/app/Services/Payments/PaymentNotificationDispatchService.php b/app/Services/Payments/PaymentNotificationDispatchService.php index a18d1a05..7bfa3d85 100644 --- a/app/Services/Payments/PaymentNotificationDispatchService.php +++ b/app/Services/Payments/PaymentNotificationDispatchService.php @@ -16,7 +16,7 @@ class PaymentNotificationDispatchService $notification = Notification::query()->create([ 'title' => $title, 'message' => $message, - 'target_group' => 'user', + 'target_group' => 'everyone', 'delivery_channels' => $channels, 'priority' => 'normal', 'status' => 'sent', diff --git a/app/Services/SemesterRangeService.php b/app/Services/SemesterRangeService.php index 6247ccf1..f13d68a3 100644 --- a/app/Services/SemesterRangeService.php +++ b/app/Services/SemesterRangeService.php @@ -67,7 +67,9 @@ class SemesterRangeService $lastDay = $this->configService->getLastSchoolDay(); try { - $target = new DateTimeImmutable($date ?: 'now'); + $target = ($date !== null && $date !== '') + ? new DateTimeImmutable($date) + : DateTimeImmutable::createFromInterface(now()); } catch (\Throwable) { return ''; } diff --git a/app/Services/Teachers/TeacherDashboardService.php b/app/Services/Teachers/TeacherDashboardService.php index 3723e007..ee297c0d 100644 --- a/app/Services/Teachers/TeacherDashboardService.php +++ b/app/Services/Teachers/TeacherDashboardService.php @@ -96,7 +96,7 @@ class TeacherDashboardService ->whereIn('tc.class_section_id', $classSectionIds) ->where('tc.school_year', $schoolYear) ->orderBy('tc.class_section_id') - ->orderByRaw("FIELD(tc.position,'main','ta')") + ->orderByRaw("CASE tc.position WHEN 'main' THEN 0 WHEN 'ta' THEN 1 ELSE 2 END") ->orderBy('u.firstname') ->get(); diff --git a/bootstrap/cache/packages.php b/bootstrap/cache/packages.php old mode 100755 new mode 100644 diff --git a/bootstrap/cache/services.php b/bootstrap/cache/services.php old mode 100755 new mode 100644 diff --git a/docker-compose.test.yml b/docker-compose.test.yml new file mode 100644 index 00000000..32454f4b --- /dev/null +++ b/docker-compose.test.yml @@ -0,0 +1,21 @@ +services: + test-db: + image: mysql:8.0 + container_name: alrahma_test_db + command: + - --default-authentication-plugin=mysql_native_password + environment: + MYSQL_DATABASE: ${TEST_DB_DATABASE:-school_api_test} + MYSQL_USER: ${TEST_DB_USERNAME:-school} + MYSQL_PASSWORD: ${TEST_DB_PASSWORD:-school} + MYSQL_ROOT_PASSWORD: ${TEST_DB_ROOT_PASSWORD:-root} + ports: + - "${TEST_DB_PORT:-33106}:3306" + # Store data in RAM so the test database is fast and ephemeral. + tmpfs: + - /var/lib/mysql + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-p${TEST_DB_ROOT_PASSWORD:-root}"] + interval: 3s + timeout: 3s + retries: 30 diff --git a/docs/alrahma_end_to_end_use_case_test_plan.md b/docs/alrahma_end_to_end_use_case_test_plan.md new file mode 100644 index 00000000..e57957d3 --- /dev/null +++ b/docs/alrahma_end_to_end_use_case_test_plan.md @@ -0,0 +1,2075 @@ +# Alrahma Sunday School API: End-to-End Use Case, Negative Test, and Penetration Test Plan + +## 1. Goal + +The goal is to cover every major end-to-end workflow in the Alrahma Sunday School API from public access through authenticated school operations, including administration, registration, attendance, grading, finance, reporting, communications, inventory, security, and system health. + +This plan should validate that the API works as a complete school management system, not as a loose collection of endpoints pretending to be architecture. + +## 2. Scope + +The project is a Laravel 12 API with role-based access, authentication, school-year controls, finance workflows, grading, attendance, parent/student/staff/teacher modules, reporting, notifications, and operational tools. + +The plan covers: + +- Authentication and authorization +- Public website and frontend APIs +- User, role, permission, and navigation management +- Student, parent, family, staff, and teacher management +- Enrollment and withdrawal +- Class sections and teacher assignments +- Attendance, lateness, absence, early dismissal, and attendance consequences +- Assignments and class progress +- Scores, exams, quizzes, homework, participation, projects, midterms, finals, and grading locks +- Strong grading feature gates +- Report cards, certificates, slips, stickers, badges, and printable outputs +- Finance, invoices, payments, PayPal, manual payments, refunds, reimbursements, discounts, event charges, installment plans, carryforwards, and notifications +- Inventory, supplies, suppliers, purchase orders, and class preparation +- Messaging, broadcast email, WhatsApp groups, notifications, and support +- Incidents, flags, and emergency contacts +- Configuration, school year, calendar, settings, UI, policies, and system health +- Background commands and scheduled jobs +- Security, auditability, validation, and failure handling + +## 3. User Roles to Cover + +The minimum role set should include: + +- Guest/Public User +- Parent +- Student +- Teacher +- Staff +- Administrator +- Finance/Admin user +- Super Admin or system-level user + +Each use case must prove three things: + +1. The correct role can perform the workflow. +2. The wrong role is blocked. +3. The system leaves correct data behind after the workflow completes. + +Testing only the happy path is decorative QA. It makes dashboards green and production angry. + +--- + +# Part I: End-to-End Workflow Coverage + +## 4. Public and Guest Workflows + +### Use Cases + +- View public school information +- View landing page content +- View facility, team, testimonial, call-to-action, help, privacy, and terms pages +- Submit contact/support form +- Register a new account +- Login with valid credentials +- Fail login with invalid credentials +- Access public certificate verification by token +- Access public time-off notification link by token +- Use public badge scan/scanner routes where allowed + +### Acceptance Criteria + +- Public routes return correct status codes and payloads. +- Contact form creates a valid record or notification. +- Registration validates required fields, captcha rules, duplicate emails, phone formatting, and role defaults. +- Login returns token/session data. +- Invalid login attempts are tracked. +- Protected routes reject unauthenticated users. + +## 5. Authentication, Session, and Security Workflows + +### Use Cases + +- Register user +- Login +- Refresh token/session +- Logout +- Fetch current user profile +- Switch active role +- Retrieve allowed permissions +- Attempt unauthorized route access +- Trigger IP attempt tracking +- Ban and unban IP addresses +- Handle inactive users +- Delete unverified users +- Reset password +- Clean up expired password resets + +### Acceptance Criteria + +- Tokens are issued, refreshed, and revoked correctly. +- Role switching does not grant permissions the user does not own. +- Permission middleware blocks forbidden access. +- Security logs are created for suspicious behavior. +- Unverified or inactive cleanup does not remove valid active users. +- Password reset tokens expire and cannot be reused. + +## 6. User, Role, Permission, and Navigation Workflows + +### Use Cases + +- Admin lists users +- Admin creates user +- Admin updates user +- Admin deletes or deactivates user +- Admin assigns roles +- Admin removes roles +- Admin creates roles +- Admin updates role permissions +- Admin builds navigation/menu by role +- User receives correct navigation items after login +- Forbidden roles cannot access admin-only menus + +### Acceptance Criteria + +- Role-permission mappings are enforced at route and UI-navigation levels. +- Users with multiple roles only receive permitted actions for their active role. +- Deleted/deactivated users cannot authenticate. +- Navigation output matches actual backend permissions. + +## 7. Parent, Family, Guardian, and Student Lifecycle + +### Use Cases + +- Create family +- Add guardians +- Add students to family +- Assign emergency contacts +- Add allergies and medical conditions +- Update student profile +- Assign student school ID +- Assign student to class section +- View parent dashboard +- Parent views student data +- Parent cannot view unrelated student data +- Admin searches families/students +- Withdraw student +- Re-enroll student +- Transfer or update family relationships + +### Acceptance Criteria + +- Family, guardian, student, and emergency contact links persist correctly. +- Sensitive student medical data is protected. +- Parents only access their own children. +- Enrollment status changes cascade to finance, class assignment, and reporting where appropriate. +- School ID generation is unique and stable. + +## 8. Administrator Enrollment and School Operations + +### Use Cases + +- View admin dashboard metrics +- Search users/students/families +- Manage enrollment withdrawal queue +- List new students +- Update enrollment statuses +- Assign teachers to classes +- Manage emergency contacts +- View teacher submissions +- Notify teachers of missing submissions +- Manage promotions and placement +- Finalize promotion batches +- Audit promotion changes + +### Acceptance Criteria + +- Admin dashboards reflect current database state. +- Enrollment updates are transactional. +- Teacher-class assignments are visible to both admin and teacher workflows. +- Promotion workflows record audit logs. +- Placement finalization cannot corrupt current class assignments. + +## 9. Staff and Teacher Workflows + +### Use Cases + +- Teacher logs in +- Teacher views assigned classes +- Teacher views class roster +- Teacher submits attendance +- Teacher submits class progress +- Teacher submits assignments +- Teacher submits scores +- Teacher requests print materials +- Staff logs attendance +- Staff manages absence/vacation/time-off where applicable +- Admin views staff/teacher activity + +### Acceptance Criteria + +- Teachers only see assigned classes. +- Teachers cannot submit scores or attendance for unrelated classes. +- Teacher submissions are timestamped. +- Admin notification logic catches missing teacher submissions. +- Print request workflow moves through teacher request and admin processing states. + +## 10. Attendance Workflows + +### Use Cases + +- Teacher submits student attendance +- Staff submits attendance +- Admin manages attendance +- Attendance auto-publish runs +- Attendance records sync +- Parent views attendance report +- Absence is recorded +- Late arrival is recorded +- Early dismissal is recorded with signature +- Attendance comment templates are used +- Attendance consequences are calculated +- Absentee and late summaries are sent +- Attendance is recalculated by command + +### Acceptance Criteria + +- Attendance status is stored correctly per student, class, and date. +- Late slips and early dismissals generate correct logs. +- Parents see only their own child’s attendance. +- Attendance summary calculations match source records. +- Scheduled attendance commands are idempotent. +- Attendance consequence rules trigger only when thresholds are met. + +## 11. Attendance Tracking and Violation Workflow + +### Use Cases + +- System identifies pending attendance violations +- Admin/staff reviews attendance tracking cases +- System composes attendance communication +- Notification logs are created +- Parent contact is recorded +- Violation status is resolved or escalated + +### Acceptance Criteria + +- Violation rules are deterministic and testable. +- No duplicate notifications are created for the same unresolved case. +- Parent communication history is preserved. +- Resolved cases are excluded from active violation queues. + +## 12. Assignments and Class Progress + +### Use Cases + +- Teacher creates assignment +- Teacher updates assignment +- Teacher lists assignments by class +- Teacher submits class progress report +- Teacher attaches progress files +- Admin reviews class progress +- Parent/student views relevant class progress where allowed + +### Acceptance Criteria + +- Assignments are scoped to the correct class/section. +- Class progress supports attachments safely. +- Unauthorized users cannot access class progress for unrelated students/classes. +- Progress reports can be audited by date, teacher, class, and school year. + +## 13. Scores, Exams, and Grading + +### Use Cases + +- Teacher enters quiz score +- Teacher enters homework score +- Teacher enters project score +- Teacher enters participation score +- Teacher enters midterm score +- Teacher enters final score +- Teacher enters exam draft +- Admin reviews score dashboard +- System calculates semester score +- System predicts score risk +- Teacher/admin adds score comments +- Admin locks grading period +- Admin unlocks or prevents changes depending on policy +- Strong grading is disabled by default +- Strong grading enabled for selected sections +- Strong grading blocks invalid or pending scores +- Legacy scores remain unchanged + +### Acceptance Criteria + +- Score values are validated against max points. +- Invalid scores are rejected. +- Pending, excused, missing, and scored statuses behave correctly. +- Legacy semester scores are displayed without recalculation. +- Strong grading only applies when configured. +- Grading locks prevent mutation after finalization. +- Score snapshots preserve calculation inputs and outputs. + +## 14. Report Cards, Certificates, Slips, Stickers, and Badges + +### Use Cases + +- Generate report card +- Parent acknowledges report card +- Generate certificate +- Verify certificate publicly by token +- Generate slips +- Generate stickers +- Generate badge PDF +- Print badge +- Scan badge +- Log badge print +- Admin views print logs + +### Acceptance Criteria + +- Generated files match student/class/school-year data. +- Private files require authorization. +- Public verification tokens do not expose unrelated private data. +- Badge scan creates valid logs. +- PDF generation works under expected production dependencies. + +## 15. Finance: Core Billing and Invoices + +### Use Cases + +- Configure fees +- Calculate tuition +- Create invoice +- Attach students to invoice +- View invoice list +- View invoice details +- Generate invoice PDF +- Apply charges +- Apply additional charges +- Apply event charges +- Apply discounts +- Void charge +- Approve charge +- Attach approved charge to invoice + +### Acceptance Criteria + +- Invoice totals match charges, discounts, refunds, and payments. +- Fee calculations are repeatable and explainable. +- Event charges do not mutate invoice totals until approved/attached. +- Discount vouchers track usage and eligibility. +- Invoice PDFs match database totals. + +## 16. Finance: Payments, Manual Payments, PayPal, and Transactions + +### Use Cases + +- Record manual payment +- Record PayPal payment +- Sync PayPal payments +- View payment transactions +- Handle payment error +- Allocate payment to invoice +- Allocate payment to installment +- Send receipt notification log +- View parent payment history +- Admin views unpaid parents + +### Acceptance Criteria + +- Payments update balances correctly. +- Duplicate PayPal transactions are not double-counted. +- Manual payments require admin/finance permission. +- Payment allocation rules are consistent. +- Receipts are logged even if external sending is disabled. +- Payment errors are visible and actionable. + +## 17. Finance: Refunds, Reimbursements, Carryforwards, and Installments + +### Use Cases + +- Request refund +- Approve refund +- Reject refund +- Process refund payout +- Request reimbursement +- Assign reimbursement batch +- Upload reimbursement files +- Export reimbursement data +- Create installment plan +- Activate installment plan +- Cancel installment plan +- Detect overdue installments +- Preview prior-year balance carryforward +- Draft carryforward +- Approve carryforward +- Post carryforward to new-year invoice +- Waive or adjust carryforward +- Export carryforward report +- Add finance follow-up note +- Mark parent contacted +- Record promise-to-pay +- Resolve follow-up + +### Acceptance Criteria + +- Refunds do not create negative or inconsistent balances. +- Reimbursements preserve attachment access rules. +- Installment plan totals reconcile with invoice balances. +- Carryforwards create separate new-year invoices and do not mutate prior-year invoices. +- Follow-up notes do not change balances. +- Finance lifecycle actions are auditable. + +## 18. Communication and Notification Workflows + +### Use Cases + +- Send direct message +- List message inbox +- Mark message read +- Send notification +- Read notification +- Delete notification +- Broadcast email +- Attach broadcast email image/file +- Send communication to family/student groups +- Log communication +- Send WhatsApp invite +- Manage WhatsApp group links +- Track WhatsApp membership +- Send payment notification logs +- Send teacher-submission reminders +- Send attendance summaries + +### Acceptance Criteria + +- Messages are scoped to sender and recipient. +- Broadcast recipients are correctly selected. +- Notification logs exist even when external channels are mocked/log-only. +- WhatsApp invite links are valid and not duplicated unnecessarily. +- Attachments are validated and protected. + +## 19. Inventory, Supplies, Purchase Orders, and Class Preparation + +### Use Cases + +- Create inventory category +- Create inventory item +- Record inventory movement +- Assign supply category +- Create supplier +- Create purchase order +- Receive purchase order +- Update purchase order status +- Teacher requests class preparation materials +- Class preparation adjusts inventory +- Generate class preparation print output + +### Acceptance Criteria + +- Inventory quantities update correctly after movements and receiving. +- Class preparation cannot consume unavailable stock unless explicitly allowed. +- Purchase order receiving is idempotent. +- Supplier and item references remain consistent. +- Inventory summary matches raw movements. + +## 20. Incidents, Flags, and Emergency Management + +### Use Cases + +- Create incident +- Update incident +- List incident history +- Create current flag +- Resolve current flag +- Analyze incidents +- View emergency contact directory +- Update emergency contact +- Delete emergency contact + +### Acceptance Criteria + +- Incident data is visible only to authorized roles. +- Current flags reflect active operational issues. +- Emergency contacts remain linked to correct students/families. +- Incident history is not silently lost when active state changes. + +## 21. Settings, School Year, Calendar, UI, and Configuration + +### Use Cases + +- View settings +- Update settings +- View school calendar +- Create calendar event +- Update event +- Delete event +- Switch school year context +- Close school year +- Block edits to locked school year +- Update configuration +- Update UI style +- View frontend page configuration +- Manage policies + +### Acceptance Criteria + +- School-year edit guards block unsafe historical edits. +- Calendar events are visible to intended audiences. +- Configuration changes are validated. +- Closed school years are immutable unless a privileged override exists. +- UI and frontend settings do not expose admin-only configuration. + +## 22. System, Health, Documentation, and Maintenance + +### Use Cases + +- Call health endpoint +- Call database health endpoint +- Call system dashboard endpoint +- Export API documentation +- Run cleanup expired notifications +- Run cleanup password resets +- Run delete unverified users +- Run delete inactive users +- Run attendance publish command +- Run payment notification command +- Run PayPal sync command +- Run config update command + +### Acceptance Criteria + +- Health endpoints return useful operational status. +- Scheduled commands can run repeatedly without corrupting data. +- Cleanup commands remove only eligible records. +- Documentation route reflects active API routes. +- CI/CD pipelines run tests and static checks before deployment. + +--- + +# Part II: Required End-to-End Scenario Set + +## Scenario A: New Family Registration to Active Enrollment + +1. Guest registers. +2. Admin approves or manages account. +3. Family is created. +4. Parent/guardian is linked. +5. Student is added. +6. Emergency contacts and medical data are added. +7. Student is assigned school ID. +8. Student is assigned class section. +9. Teacher sees student in roster. +10. Parent sees student profile. + +## Scenario B: Normal Sunday School Day + +1. Teacher logs in. +2. Teacher opens assigned class. +3. Teacher takes attendance. +4. Late student is logged. +5. Early dismissal is recorded. +6. Attendance auto-publish runs. +7. Parent views attendance report. +8. Admin views attendance summary. +9. Late/absentee summary command runs. + +## Scenario C: Academic Term Completion + +1. Teacher enters homework, quiz, project, participation, midterm, and final scores. +2. Invalid score is rejected. +3. Teacher adds score comments. +4. Admin reviews grading dashboard. +5. Admin locks grading. +6. Semester score is calculated. +7. Report card is generated. +8. Parent acknowledges report card. +9. Certificate is generated where eligible. + +## Scenario D: Family Billing Lifecycle + +1. Admin configures tuition/fees. +2. Invoice is generated for family. +3. Discount is applied. +4. Event charge is created and approved. +5. Manual payment is recorded. +6. PayPal sync is run. +7. Receipt notification is logged. +8. Balance is recalculated. +9. Parent payment history is viewed. +10. Unpaid parent report is generated. + +## Scenario E: Finance Edge Lifecycle + +1. Parent has unpaid prior-year balance. +2. Admin previews carryforward. +3. Admin drafts carryforward. +4. Admin approves carryforward. +5. System posts new-year invoice. +6. Admin creates installment plan. +7. Payment is allocated to installment. +8. Overdue installment report runs. +9. Follow-up note is created. +10. Promise-to-pay is recorded. +11. Case is resolved. + +## Scenario F: Teacher Submission Follow-Up + +1. Teacher misses required submission. +2. Admin dashboard detects missing submission. +3. Admin sends reminder. +4. Notification history is created. +5. Teacher submits missing item. +6. Admin report updates. +7. Duplicate reminder behavior is controlled. + +## Scenario G: Print and Badge Workflow + +1. Teacher requests print materials. +2. Admin reviews print request. +3. Admin generates slips/stickers. +4. Badge PDF is generated. +5. Badge is printed. +6. Badge print log is created. +7. Badge is scanned. +8. Scan log is created. + +## Scenario H: Security and Unauthorized Access + +1. Parent attempts to access unrelated student. +2. Teacher attempts to access unrelated class. +3. Staff attempts admin finance action. +4. Inactive user attempts login. +5. Banned IP attempts login. +6. Expired token accesses route. +7. All are rejected with correct status codes and no data leakage. + +--- + +# Part III: Data Setup Strategy + +Create reusable seed fixtures for: + +- One administrator +- One finance administrator +- One staff member +- Two teachers +- Two parents +- Two families +- At least four students +- Two school years, one current and one closed +- Two class sections +- One active event +- One invoice with charges +- One invoice with payments +- One student with medical/allergy records +- One student with full academic scores +- One student with attendance violations +- One inventory item with supplier and purchase order + +Avoid brittle tests that depend on random seed data. Randomness belongs in chaos engineering, not basic confidence. + +--- + +# Part IV: Test Layers + +## Unit Tests + +Use for: + +- Services +- Calculators +- Validators +- Permission helpers +- Score formulas +- Finance math +- Notification recipient selection +- Attendance rule engine + +## Feature/API Tests + +Use for: + +- Routes +- Auth +- Validation +- Role access +- CRUD flows +- State transitions +- File generation +- Command behavior + +## End-to-End Scenario Tests + +Use for: + +- Multi-module workflows +- Parent/student/admin/teacher handoffs +- Finance reconciliation +- Academic lifecycle +- Attendance lifecycle +- Deployment smoke checks + +## Contract Tests + +Use for: + +- Response shape +- Status codes +- Pagination +- Error format +- Required fields +- Backward compatibility + +--- + +# Part V: Negative Test Coverage Plan + +## 1. Negative Testing Principle + +Every end-to-end use case must include failure-path testing. A workflow is not covered unless the system proves that it rejects invalid users, invalid input, invalid ownership, invalid state transitions, duplicate actions, expired data, and unsafe edge cases. + +For each workflow, test: + +- Unauthenticated access +- Wrong-role access +- Wrong-owner access +- Missing required fields +- Invalid field format +- Invalid enum/status value +- Boundary values +- Duplicate request +- Out-of-order workflow step +- Closed school year mutation +- Deleted/inactive record usage +- External provider failure +- File upload failure +- Race condition or repeated command execution + +A passing happy-path test proves the system works when everyone behaves. That is adorable and useless by itself. + +## 2. Authentication and Authorization Negative Tests + +### Test Cases + +- Unauthenticated user calls protected endpoint +- Expired token calls protected endpoint +- Malformed token calls protected endpoint +- Logged-out token is reused +- Inactive user attempts login +- Deleted user attempts login +- Unverified user attempts restricted access +- Banned IP attempts login +- User switches to role they do not own +- User accesses route outside active role permissions +- Parent accesses admin endpoint +- Teacher accesses finance endpoint +- Staff accesses super-admin endpoint +- Student accesses another student’s data +- Parent accesses unrelated child +- Teacher accesses unassigned class +- Finance user modifies grading data +- Admin route called without required permission + +### Expected Result + +- Return 401 for unauthenticated requests. +- Return 403 for authenticated but unauthorized requests. +- Do not leak protected records in the response. +- Do not mutate database state. +- Log security-relevant failures where appropriate. + +## 3. Registration and Login Negative Tests + +### Test Cases + +- Register with missing name +- Register with missing email +- Register with invalid email format +- Register with duplicate email +- Register with weak password +- Register with password confirmation mismatch +- Register with invalid phone format +- Register with missing required guardian/student fields +- Submit invalid captcha if captcha is enabled +- Login with wrong password +- Login with unknown email +- Login with inactive account +- Login repeatedly from same IP until rate-limit/attempt tracking triggers +- Password reset with invalid email +- Password reset with expired token +- Password reset with reused token +- Password reset with mismatched password confirmation + +### Expected Result + +- Validation errors are specific and consistent. +- No duplicate users are created. +- No token is issued on failed login. +- Login attempt tracking works. +- Expired or reused reset tokens fail safely. + +## 4. User, Role, Permission, and Navigation Negative Tests + +### Test Cases + +- Create user with duplicate email +- Create user with invalid role +- Assign nonexistent role to user +- Remove last required admin role from only admin user +- Assign permission that does not exist +- Create role with duplicate name +- Delete role still assigned to users +- Update role without permission +- Fetch navigation as user with no active role +- Fetch navigation for role with no menu permissions +- Force menu item access through direct API route + +### Expected Result + +- Invalid role and permission operations fail. +- Navigation never exposes unauthorized menu items. +- Direct API calls remain protected even if a user guesses the route. + +## 5. Student, Parent, Family, and Guardian Negative Tests + +### Test Cases + +- Create student without required family +- Create student with invalid date of birth +- Create student with future date of birth +- Create duplicate student school ID +- Assign student to nonexistent family +- Assign student to deleted family +- Parent attempts to update unrelated student +- Parent attempts to view unrelated emergency contact +- Remove last guardian from active student +- Create emergency contact with invalid phone/email +- Create allergy/medical record for unrelated student +- Withdraw student twice +- Re-enroll withdrawn student into invalid class +- Assign student to closed school year +- Delete student with active invoice, scores, or attendance records + +### Expected Result + +- Invalid family/student relationships are rejected. +- Ownership boundaries are enforced. +- Critical linked records are protected from destructive deletion. +- Withdraw/re-enroll transitions are controlled. + +## 6. Class Section and Teacher Assignment Negative Tests + +### Test Cases + +- Create class section with duplicate name for same school year +- Create class section with invalid grade level +- Assign teacher to nonexistent section +- Assign inactive teacher to section +- Assign student to nonexistent section +- Assign student to section from wrong school year +- Assign duplicate teacher-section relationship +- Teacher fetches roster for unassigned section +- Teacher submits data for unassigned section +- Delete class section with active students +- Delete section with scores, attendance, or assignments + +### Expected Result + +- Class assignments remain consistent. +- Teachers cannot operate outside assigned classes. +- Historical academic records are not orphaned. + +## 7. Attendance Negative Tests + +### Test Cases + +- Submit attendance without class section +- Submit attendance for unrelated class +- Submit attendance for nonexistent student +- Submit attendance for student not in class +- Submit invalid attendance status +- Submit future attendance date if not allowed +- Submit attendance for closed school year +- Submit duplicate attendance for same student/date/class +- Submit early dismissal without signature or required reason +- Submit late slip with invalid timestamp +- Submit absence without required fields +- Teacher changes attendance after lock/publish +- Attendance publish command runs twice +- Attendance recalculation command runs twice +- Parent fetches unrelated attendance report + +### Expected Result + +- Invalid attendance records are rejected. +- Duplicate records are not created. +- Locked or published attendance cannot be mutated unless allowed by privileged override. +- Commands are idempotent. +- Parent access is scoped correctly. + +## 8. Assignment and Class Progress Negative Tests + +### Test Cases + +- Teacher creates assignment for unassigned class +- Create assignment without title +- Create assignment with due date before assigned date +- Create assignment with invalid attachment type +- Upload oversized attachment +- Update another teacher’s assignment +- Delete assignment after scores depend on it +- Submit class progress for wrong class +- Submit class progress for closed school year +- Parent/student views unrelated class progress +- Admin reviews nonexistent progress report + +### Expected Result + +- Assignments stay scoped to valid classes. +- Files are validated and protected. +- Academic records are not broken by unsafe deletes. + +## 9. Scores, Exams, and Grading Negative Tests + +### Test Cases + +- Submit score for unrelated class +- Submit score for nonexistent student +- Submit score for student not in class +- Submit score below zero +- Submit score above max points +- Submit score with invalid decimal precision +- Submit score with invalid status +- Submit score without required assessment type +- Submit final score before required prerequisite scores +- Submit score into locked grading period +- Modify score after report card finalization +- Submit score for closed school year +- Submit duplicate exam draft +- Approve invalid exam draft +- Enable strong grading with incomplete config +- Strong grading rejects missing required categories +- Strong grading rejects invalid score composition +- Legacy score is recalculated unexpectedly +- Finance/admin role attempts grading endpoint +- Parent attempts to mutate score +- Student attempts to mutate score + +### Expected Result + +- Invalid scores are rejected. +- Locked/finalized grading records remain immutable. +- Strong grading gates only apply when enabled and configured. +- Legacy records remain stable. +- Unauthorized users cannot mutate academic records. + +## 10. Report Card, Certificate, Badge, and PDF Negative Tests + +### Test Cases + +- Generate report card for nonexistent student +- Generate report card for unrelated student +- Generate report card before required scores exist +- Generate report card for unlocked/unfinalized grading period +- Parent acknowledges unrelated report card +- Acknowledge report card twice +- Generate certificate for ineligible student +- Verify certificate with invalid token +- Verify certificate with expired/revoked token +- Download private PDF without auth +- Download PDF for unrelated student +- Generate badge for nonexistent student +- Scan invalid badge token +- Scan revoked badge +- Print badge without permission +- Generate sticker/slip for closed or invalid data + +### Expected Result + +- Private files are protected. +- Public token routes expose only intentionally public data. +- Duplicate acknowledgements/scans are handled safely. +- Eligibility rules are enforced. + +## 11. Finance Negative Tests + +### 11.1 Invoice and Charge Negative Tests + +#### Test Cases + +- Create invoice for nonexistent family +- Create invoice for inactive/withdrawn student if not allowed +- Create duplicate invoice for same period/student +- Create charge with negative amount +- Create charge with zero amount if not allowed +- Create charge with invalid category +- Approve nonexistent charge +- Approve same charge twice +- Attach unapproved charge to invoice +- Attach charge to unrelated invoice +- Void charge after payment allocation if not allowed +- Apply discount exceeding invoice total +- Apply expired discount voucher +- Apply voucher beyond usage limit +- Apply voucher to ineligible family/student +- Generate invoice PDF for unrelated family + +#### Expected Result + +- Invoice totals remain correct. +- Charges and discounts obey lifecycle rules. +- No negative balance is created unless explicitly modeled as credit. +- Unauthorized access is blocked. + +### 11.2 Payment Negative Tests + +#### Test Cases + +- Record payment for nonexistent invoice +- Record payment for unrelated family +- Record payment with negative amount +- Record payment with zero amount +- Record overpayment if not allowed +- Record duplicate manual payment reference +- Record duplicate PayPal transaction ID +- PayPal webhook/sync sends same transaction twice +- PayPal payment has invalid status +- PayPal payment belongs to wrong account/config +- Payment allocation exceeds invoice balance +- Payment allocation to cancelled installment +- Parent attempts manual payment admin route +- Receipt notification fails externally +- Payment recorded but notification fails + +#### Expected Result + +- No duplicate payments. +- Balances reconcile after every payment attempt. +- External notification failure does not corrupt payment records. +- Manual payment remains admin/finance-only. + +### 11.3 Refund, Reimbursement, Installment, and Carryforward Negative Tests + +#### Test Cases + +- Request refund above paid amount +- Approve refund twice +- Process refund before approval +- Reject already processed refund +- Create reimbursement without required attachment +- Upload invalid reimbursement file type +- Assign reimbursement to invalid batch +- Create installment plan where total does not equal invoice balance +- Activate installment plan twice +- Cancel installment plan after completed payments if not allowed +- Pay cancelled installment +- Carryforward already carried balance +- Carryforward negative or zero balance +- Approve carryforward twice +- Post carryforward to closed school year +- Waive carryforward without permission +- Promise-to-pay date in past if not allowed +- Resolve finance follow-up without required resolution note + +#### Expected Result + +- Financial state transitions are strict. +- Refunds, installments, reimbursements, and carryforwards do not corrupt ledger totals. +- Repeated actions are idempotent or rejected clearly. + +## 12. Communication and Notification Negative Tests + +### Test Cases + +- Send message without recipient +- Send message to nonexistent user +- Send message to unauthorized recipient group +- Parent messages unrelated teacher/class group +- Upload invalid broadcast attachment +- Upload oversized broadcast attachment +- Send broadcast without permission +- Send duplicate broadcast by retry +- Read notification belonging to another user +- Delete notification belonging to another user +- Send WhatsApp invite to invalid group +- Create duplicate WhatsApp group link +- Send notification when channel config is missing +- External email/SMS provider fails +- Notification command runs twice + +### Expected Result + +- Recipient targeting is correct. +- Private messages and notifications remain private. +- External provider failures are logged and recoverable. +- Duplicate command runs do not spam recipients. + +## 13. Inventory and Purchase Order Negative Tests + +### Test Cases + +- Create inventory item without category +- Create duplicate SKU/item code +- Record negative inventory movement without permission +- Consume more stock than available +- Receive purchase order twice +- Receive more quantity than ordered +- Create purchase order for nonexistent supplier +- Delete supplier with active purchase orders +- Teacher requests unavailable supplies +- Class preparation consumes stock twice +- Upload invalid attachment to purchase order +- Update purchase order after final/received status + +### Expected Result + +- Inventory quantities never become accidentally inconsistent. +- Purchase order lifecycle is enforced. +- Stock consumption is idempotent and traceable. + +## 14. Incidents, Flags, and Emergency Negative Tests + +### Test Cases + +- Create incident for nonexistent student +- Create incident for unrelated student +- Create incident without required severity/type +- Use invalid incident severity +- Resolve nonexistent incident +- Resolve already resolved incident +- Create duplicate active flag +- Resolve flag without permission +- Parent views unrelated incident +- Teacher views incident outside assigned class if restricted +- Delete emergency contact used by active student +- Create emergency contact with invalid details + +### Expected Result + +- Incidents and flags remain scoped and auditable. +- Sensitive disciplinary or safety data is protected. +- Duplicate active flags are prevented where appropriate. + +## 15. Settings, Calendar, School Year, and Configuration Negative Tests + +### Test Cases + +- Update settings without admin permission +- Update config with invalid value/type +- Set school year date range where end is before start +- Create overlapping active school years +- Close school year with pending required tasks if blocked +- Modify closed school year records +- Create calendar event with invalid date range +- Delete calendar event without permission +- Create frontend setting with unsafe HTML/script if applicable +- Update policy with missing content +- Switch to nonexistent school year context + +### Expected Result + +- Configuration remains valid. +- Closed school years are protected. +- Unsafe content is rejected or sanitized. +- Only authorized users can change system behavior. + +## 16. File Upload and Storage Negative Tests + +### Test Cases + +- Upload unsupported file type +- Upload oversized file +- Upload empty file +- Upload file with misleading extension +- Upload executable/script file +- Download file without permission +- Download file for unrelated student/family +- Delete file owned by another user +- Generate PDF when storage path is unavailable +- Generate PDF when template data is missing +- External storage failure +- Repeated PDF generation request + +### Expected Result + +- Unsafe files are rejected. +- Private files are protected. +- Storage failures fail cleanly. +- Generated files are deterministic or versioned intentionally. + +## 17. Scheduled Command and Job Negative Tests + +### Test Cases + +- Run attendance auto-publish twice +- Run attendance sync twice +- Run late summary command twice +- Run absentee summary command twice +- Run payment notification command twice +- Run PayPal sync twice +- Run cleanup expired notifications twice +- Run cleanup expired password resets twice +- Run delete inactive users with no eligible users +- Run delete unverified users with recently registered users +- Run config update command with invalid config +- Job fails halfway through database mutation +- Notification job fails after log creation +- External API unavailable during sync + +### Expected Result + +- Commands are idempotent. +- No duplicate records or duplicate notifications are created. +- Partial failures roll back or leave recoverable state. +- Cleanup commands do not delete ineligible records. + +## 18. API Contract and Validation Negative Tests + +### Test Cases + +- Send malformed JSON +- Send wrong content type +- Send invalid pagination parameters +- Send invalid sort field +- Send invalid filter field +- Send SQL-like input in search fields +- Send script tags in text fields +- Send extremely long strings +- Send null where object expected +- Send array where scalar expected +- Send unknown fields if strict validation is expected +- Call invalid route +- Call valid route with unsupported HTTP method + +### Expected Result + +- Return consistent validation errors. +- No SQL errors leak. +- No stack traces leak in production mode. +- Unknown or malformed requests fail safely. +- Error response format remains consistent. + +## 19. Concurrency and Race Condition Negative Tests + +### Test Cases + +- Two admins approve same charge simultaneously +- Two payments submit for same invoice simultaneously +- Two teachers submit attendance for same student/date simultaneously +- Two report card generations run simultaneously +- Two carryforward approvals run simultaneously +- Two PayPal sync jobs process same transaction +- Two inventory consumption actions reduce same item below zero +- Two users update same student profile simultaneously +- Retry same request after timeout +- Client double-clicks submit button + +### Expected Result + +- Database constraints and transactions protect state. +- Duplicate writes are prevented. +- Final balances, scores, attendance, and inventory quantities remain correct. +- The API returns predictable results under retries. + +## 20. Required Negative Test Matrix Per Use Case + +Each end-to-end scenario must include at least these negative tests: + +- Unauthenticated request returns 401 +- Wrong role returns 403 +- Wrong owner returns 403 or 404 according to API policy +- Missing required field returns 422 +- Invalid field type returns 422 +- Invalid state transition returns 409 or 422 +- Duplicate action is rejected or idempotently accepted +- Closed school year mutation is blocked +- Deleted/inactive related record is rejected +- Database state remains unchanged after failed request +- Audit/security/notification log is created where expected + +## 21. Negative Test Naming Convention + +Use this pattern: + +```text +test_[role]_[action]_fails_when_[invalid_condition] +``` + +Examples: + +```text +test_parent_cannot_view_unrelated_student_attendance +test_teacher_cannot_submit_scores_for_unassigned_class +test_invoice_balance_does_not_change_when_invalid_payment_fails +test_paypal_sync_does_not_duplicate_existing_transaction +test_attendance_publish_command_is_idempotent +test_report_card_cannot_be_generated_before_grading_is_finalized +``` + +Vague names like `testFinanceValidation` should be treated as little bug incubators wearing a trench coat. + +## 22. Definition of Done for Negative Testing + +A module is not complete unless: + +- Every protected route has auth and role negative tests. +- Every ownership-sensitive route has wrong-owner tests. +- Every create/update endpoint has validation failure tests. +- Every stateful workflow has invalid transition tests. +- Every finance mutation has reconciliation tests after failure. +- Every grading mutation has locked/finalized-period tests. +- Every scheduled command has idempotency tests. +- Every file route has unauthorized and invalid-file tests. +- Every public token route has invalid-token tests. +- Every failed request proves the database did not change incorrectly. + +Negative tests are not extra. They are where the real system is tested. + +--- + +# Part VI: Penetration Testing Coverage Plan + +## 1. Objective + +The objective of penetration testing is to verify that the Alrahma Sunday School API resists real-world attack patterns across authentication, authorization, student privacy, finance workflows, file handling, public token routes, admin functions, and infrastructure exposure. + +This testing must focus on the API as a complete school management system containing sensitive student, parent, finance, medical, attendance, and academic data. + +Penetration testing should validate: + +- Unauthorized data access is blocked. +- Role boundaries cannot be bypassed. +- Family/student ownership rules cannot be bypassed. +- Financial records cannot be manipulated. +- Grades and attendance cannot be tampered with. +- Files and PDFs cannot be accessed without permission. +- Public token routes do not leak private data. +- Inputs cannot be abused for injection or stored attacks. +- Authentication tokens cannot be reused, forged, or escalated. +- Infrastructure and deployment settings do not expose secrets. + +## 2. Testing Rules and Constraints + +Penetration tests must be performed only against approved environments. + +Use: + +- Local development environment +- Dedicated staging environment +- Seeded test database +- Mocked payment providers +- Mocked email/SMS/WhatsApp providers +- Non-production file storage +- Non-production secrets +- Synthetic student, parent, and finance data + +Do not: + +- Use production data +- Test against production without written approval +- Run destructive tests against shared environments +- Use real parent, student, staff, or financial information +- Trigger real payment transactions or real communications + +Apparently this needs saying, because “testing security” has historically been a popular way to accidentally become the incident. + +## 3. Penetration Testing Phases + +## Phase 1: Reconnaissance and Attack Surface Mapping + +### Test Areas + +- Enumerate all public routes +- Enumerate all protected API routes +- Identify admin-only endpoints +- Identify finance endpoints +- Identify grading endpoints +- Identify attendance endpoints +- Identify public token routes +- Identify upload/download routes +- Identify PDF generation routes +- Identify scheduled-command-triggerable endpoints if any +- Identify debug, docs, health, and system endpoints +- Check exposed response headers +- Check CORS behavior +- Check error responses +- Check route naming leaks +- Check API documentation exposure + +### Expected Findings to Validate + +- No private route is accidentally public. +- No sensitive route appears in public documentation without auth. +- Health endpoints expose only safe operational data. +- Error responses do not reveal stack traces, SQL, file paths, secrets, or internal class names in production mode. +- CORS does not allow unsafe origins with credentials. + +## Phase 2: Authentication Penetration Tests + +### Test Cases + +- Attempt login with valid email and wrong password +- Attempt brute-force login with repeated password guesses +- Attempt credential stuffing pattern with many emails +- Attempt login from same IP repeatedly +- Attempt login from rotating IP headers such as `X-Forwarded-For` +- Attempt login with inactive user +- Attempt login with deleted user +- Attempt login with unverified user +- Attempt login after logout using old token +- Attempt token replay +- Attempt malformed JWT/token +- Attempt unsigned or tampered token +- Attempt token with modified role/user ID claims +- Attempt expired token +- Attempt refresh token reuse +- Attempt password reset token reuse +- Attempt password reset token guessing +- Attempt password reset for unknown email +- Check whether user enumeration is possible through login/reset responses +- Check session fixation behavior if sessions are used + +### Expected Result + +- Brute-force attempts are rate-limited or tracked. +- Tokens cannot be forged, replayed after logout, or reused after expiration. +- Password reset tokens are single-use and expire correctly. +- Responses do not disclose whether an email exists unless intentionally designed. +- Inactive, deleted, and unverified users cannot access protected workflows. + +## Phase 3: Authorization and Role Escalation Penetration Tests + +### Test Cases + +- Parent calls admin endpoints +- Parent calls finance endpoints +- Parent calls teacher endpoints +- Teacher calls finance endpoints +- Teacher calls admin user-management endpoints +- Staff calls super-admin endpoints +- Student calls parent-only endpoints +- Finance user calls grading mutation endpoints +- Authenticated user changes request role ID manually +- Authenticated user changes active role to unauthorized role +- Authenticated user edits role/permission payload in request +- Authenticated user accesses hidden menu endpoint directly +- Authenticated user guesses privileged route URL +- User with multiple roles attempts privilege escalation during role switch +- User modifies `user_id`, `role_id`, `parent_id`, `student_id`, `teacher_id`, or `family_id` in payload + +### Expected Result + +- Every forbidden action returns 403 or safe 404. +- Direct route access is blocked even when the UI hides the route. +- Role switching cannot grant permissions the user does not own. +- Changing IDs in the request does not bypass server-side authorization. + +## Phase 4: Object-Level Authorization / IDOR Tests + +This is one of the highest-risk areas. Parent/student systems are basically IDOR playgrounds if backend checks are lazy. + +### Test Cases + +- Parent accesses unrelated student profile by changing student ID +- Parent accesses unrelated attendance report +- Parent accesses unrelated report card +- Parent accesses unrelated invoice +- Parent accesses unrelated payment history +- Parent downloads unrelated student PDF +- Parent acknowledges unrelated report card +- Teacher accesses roster for unassigned class +- Teacher submits attendance for unassigned class +- Teacher submits scores for unassigned student +- Teacher views incident for student outside assigned class +- Student views another student’s record +- Staff accesses records outside allowed scope +- Finance user accesses family not in permitted scope if scoping exists +- User changes ID in URL path +- User changes ID in query string +- User changes ID in JSON body +- User changes nested relationship IDs +- User uses deleted or historical IDs +- User enumerates sequential IDs + +### Expected Result + +- Ownership is checked server-side for every object. +- Unauthorized object access returns 403 or safe 404. +- No response contains partial private data. +- Sequential ID enumeration does not expose records. + +## Phase 5: Input Validation and Injection Tests + +### Test Cases + +- Submit SQL injection strings in search fields +- Submit SQL injection strings in filters +- Submit SQL injection strings in login fields +- Submit SQL injection strings in finance note fields +- Submit SQL injection strings in student/family names +- Submit NoSQL-style operators if any JSON filtering exists +- Submit command injection strings to file/PDF/export endpoints +- Submit path traversal strings such as `../` in file paths +- Submit null bytes in filenames +- Submit extremely long strings +- Submit deeply nested JSON +- Submit oversized request bodies +- Submit invalid UTF-8 +- Submit arrays where scalars are expected +- Submit objects where strings are expected +- Submit script tags in comments, messages, notes, incident descriptions, frontend page content, policy content, broadcast email content, and support forms +- Submit HTML event handlers such as image error handlers +- Submit markdown/HTML injection in rich text fields +- Submit formula injection strings in CSV/exportable fields, such as values starting with `=`, `+`, `-`, or `@` + +### Expected Result + +- Inputs are validated, rejected, escaped, or sanitized according to field purpose. +- No SQL errors leak. +- No command execution occurs. +- Exports protect against spreadsheet formula injection. +- Stored text does not execute script when rendered by frontend/admin tools. + +## Phase 6: File Upload, Download, and PDF Security Tests + +### Test Cases + +- Upload executable file with allowed extension +- Upload script file disguised as image +- Upload oversized file +- Upload empty file +- Upload file with double extension +- Upload file with path traversal filename +- Upload file with MIME mismatch +- Upload SVG containing script +- Upload HTML file if not allowed +- Upload malicious PDF if PDF upload exists +- Upload image with metadata payload +- Attempt unauthorized download of private file +- Attempt download by guessing file ID +- Attempt download using path traversal +- Attempt access to storage path directly +- Attempt access to generated report card PDF without auth +- Attempt access to unrelated invoice PDF +- Attempt access to unrelated badge PDF +- Attempt public certificate token enumeration +- Attempt public badge token enumeration +- Attempt repeated PDF generation to exhaust resources + +### Expected Result + +- File type validation checks both extension and MIME/content where practical. +- Private files require authorization. +- Generated PDFs are not exposed by predictable public paths. +- Public token routes are unguessable and scoped. +- PDF generation is rate-limited or protected from abuse. + +## Phase 7: Finance Penetration Tests + +Finance needs special treatment because attackers love money almost as much as developers love forgetting server-side validation. + +### Test Cases + +- Parent modifies invoice amount in payment request +- Parent pays another family’s invoice +- Parent submits negative payment amount +- Parent submits zero payment amount +- Parent submits overpayment if not allowed +- Parent replays PayPal callback/webhook payload +- Parent modifies PayPal transaction ID +- Parent reuses existing PayPal transaction ID +- Parent changes invoice ID during payment allocation +- Parent applies expired discount voucher +- Parent applies someone else’s voucher +- Parent applies voucher beyond usage limit +- Parent applies discount exceeding balance +- Parent manipulates installment plan ID +- Parent pays cancelled installment +- User creates refund request above paid amount +- User requests refund for unrelated payment +- Finance user approves same refund twice +- Finance user posts carryforward twice +- User modifies prior-year carryforward amount +- User creates event charge for unrelated student +- User attaches unapproved charge to invoice +- User voids paid charge without permission +- Race two payments against same invoice +- Race two approvals against same charge/refund/carryforward + +### Expected Result + +- All financial calculations happen server-side. +- Client-provided totals are ignored or verified. +- Duplicate payment references are rejected. +- PayPal/webhook data is verified. +- Balances cannot be corrupted by replay, race, or tampering. +- Every finance mutation is auditable. + +## Phase 8: Grading and Academic Integrity Penetration Tests + +### Test Cases + +- Teacher changes score for unassigned student +- Teacher changes score in locked grading period +- Teacher changes finalized report card data +- Teacher submits score above max value +- Teacher bypasses strong grading by changing payload flags +- Teacher submits legacy score into strong grading flow +- Parent attempts score mutation +- Student attempts score mutation +- Finance/admin role attempts score mutation +- User modifies semester score directly if endpoint exists +- User changes assessment category ID to manipulate weighting +- User submits duplicate score records +- Race two score updates against same student/category +- Generate report card before grading finalization +- Acknowledge another student’s report card + +### Expected Result + +- Academic records are protected by role, ownership, class assignment, school year, and grading lock. +- Server-side calculation cannot be bypassed by client payloads. +- Historical and finalized records remain stable. + +## Phase 9: Attendance and Student Safety Penetration Tests + +### Test Cases + +- Teacher submits attendance for unassigned class +- Teacher changes attendance after publish/lock +- Parent submits attendance if not allowed +- User submits early dismissal for unrelated student +- User omits required dismissal signature/reason +- User modifies dismissal timestamp +- User downloads unrelated late slip +- User accesses attendance violation records for unrelated student +- User manipulates attendance consequence status +- Run attendance publish command repeatedly +- Race duplicate attendance submissions +- Submit attendance for closed school year +- Submit attendance for withdrawn student + +### Expected Result + +- Attendance and dismissal workflows are protected from unauthorized mutation. +- Published/locked attendance cannot be changed without privileged workflow. +- Safety-sensitive records are private and auditable. + +## Phase 10: Public Token Route Penetration Tests + +### Test Cases + +- Guess certificate verification token +- Guess badge scan token +- Guess time-off notification token +- Use expired token +- Use revoked token +- Use token for wrong resource type +- Use token with modified characters +- Use token repeatedly +- Use token to infer private student data +- Use valid token and inspect whether excessive data is returned +- Check token length and entropy +- Check whether tokens are sequential or predictable + +### Expected Result + +- Tokens are long, random, unguessable, scoped, and revocable where needed. +- Public routes return only minimum required data. +- Invalid tokens return safe errors without revealing whether nearby tokens exist. + +## Phase 11: Messaging, Notifications, and Broadcast Abuse Tests + +### Test Cases + +- Parent messages unrelated family/student group +- Student messages restricted admin group +- User sends message as another sender by modifying sender ID +- User reads another user’s notification +- User deletes another user’s notification +- User sends broadcast without permission +- User injects script into broadcast email content +- User uploads malicious broadcast attachment +- User triggers duplicate broadcast by retry +- User abuses notification endpoint for spam +- External provider failure leaks secrets or stack trace +- User manipulates recipient group filters + +### Expected Result + +- Sender and recipient are enforced server-side. +- Broadcast routes are privileged. +- Stored content is sanitized or safely rendered. +- Duplicate sends are controlled. +- Notification data remains private. + +## Phase 12: Admin, Settings, and School-Year Abuse Tests + +### Test Cases + +- Non-admin updates settings +- Non-admin changes school year +- Admin creates overlapping school years +- User mutates closed school year records +- User reopens closed school year without permission +- User changes system config through mass assignment +- User injects unsafe HTML into frontend settings +- User changes policy content without permission +- User changes fee config without finance/admin permission +- User accesses system dashboard without admin role +- User accesses API docs if they expose private routes + +### Expected Result + +- System configuration is admin-only and validated. +- Closed school-year protections work. +- Mass assignment cannot modify sensitive fields. +- Administrative dashboards do not leak data. + +## Phase 13: Mass Assignment and Parameter Tampering Tests + +### Test Cases + +- Add `is_admin: true` to profile update +- Add `role_id` to profile update +- Add `permissions` array to user update +- Add `balance` to invoice update +- Add `paid: true` to invoice update +- Add `finalized: true` to grading payload +- Add `school_year_locked: false` to school year payload +- Add `created_by` or `updated_by` fields +- Add `deleted_at` field +- Add nested unauthorized objects +- Modify server-calculated fields +- Modify audit fields +- Modify status fields outside allowed transition endpoint + +### Expected Result + +- Sensitive attributes cannot be mass-assigned. +- Server-calculated and audit fields are ignored or rejected. +- State changes happen only through explicit authorized workflows. + +## Phase 14: Rate Limiting and Abuse Tests + +### Test Cases + +- Rapid login attempts +- Rapid registration attempts +- Rapid password reset requests +- Rapid contact form submissions +- Rapid support ticket submissions +- Rapid PDF generation requests +- Rapid public token verification attempts +- Rapid badge scan attempts +- Rapid search requests +- Large pagination limits +- Expensive report-generation loops +- Repeated file uploads +- Repeated notification sends + +### Expected Result + +- High-risk and expensive endpoints are rate-limited. +- Abuse does not exhaust server resources. +- Rate-limit responses do not leak sensitive state. + +## Phase 15: API Error Handling and Information Disclosure Tests + +### Test Cases + +- Trigger validation error +- Trigger 401 +- Trigger 403 +- Trigger 404 +- Trigger 405 +- Trigger 409 +- Trigger 422 +- Trigger 500 in staging +- Trigger database constraint violation +- Trigger missing file error +- Trigger external provider error +- Trigger PDF generation error +- Check production debug mode +- Check stack trace exposure +- Check SQL query exposure +- Check environment variable exposure +- Check Laravel version/header exposure if avoidable + +### Expected Result + +- Errors are consistent and safe. +- Production does not expose debug traces. +- Sensitive internals are not returned to clients. + +## Phase 16: Infrastructure and Deployment Penetration Tests + +### Test Areas + +- TLS configuration +- HTTP to HTTPS redirect +- Security headers +- CORS configuration +- Cookie flags if cookies are used +- Environment file exposure +- Git directory exposure +- Storage directory exposure +- Public upload directory exposure +- Debug routes +- Telescope/Horizon/admin tooling exposure if installed +- Queue dashboard exposure +- API documentation exposure +- Database port exposure +- Redis/cache exposure +- Default credentials +- Container image secrets +- CI/CD secret leakage +- Backup file exposure +- Old build artifacts +- Directory listing +- Server banner disclosure + +### Expected Result + +- No secrets are exposed. +- Admin tooling is protected. +- Only required ports are reachable. +- Deployment artifacts are not publicly accessible. +- Security headers and TLS are correctly configured. + +## 4. Recommended Tooling + +Use a mix of automated and manual testing. + +Recommended automated tools: + +- OWASP ZAP for dynamic API scanning +- Burp Suite Community or Professional for manual API testing +- Postman or Insomnia for authenticated request collections +- k6 for abuse and rate-limit testing +- Nuclei for infrastructure template checks +- Nikto for basic web server checks where applicable +- SQLMap only against approved test environments +- Trivy or Grype for container and dependency scanning +- Composer audit for PHP dependency vulnerabilities +- npm audit or equivalent if frontend assets exist +- GitLeaks for secret scanning +- Semgrep for static code security rules +- PHPStan or Larastan for static correctness +- OWASP Dependency-Check if dependency reporting is needed + +Do not blindly trust scanner output. Automated scanners produce false positives with the confidence of a mediocre consultant. + +## 5. OWASP API Security Coverage + +Map testing to OWASP API Security Top 10: + +- API1 Broken Object Level Authorization +- API2 Broken Authentication +- API3 Broken Object Property Level Authorization +- API4 Unrestricted Resource Consumption +- API5 Broken Function Level Authorization +- API6 Unrestricted Access to Sensitive Business Flows +- API7 Server-Side Request Forgery, if URL fetching exists +- API8 Security Misconfiguration +- API9 Improper Inventory Management +- API10 Unsafe Consumption of APIs + +For this project, the highest-priority OWASP risks are: + +- Broken object-level authorization +- Broken function-level authorization +- Broken authentication +- Unrestricted access to sensitive finance and grading workflows +- Security misconfiguration +- Unrestricted resource consumption through PDFs, reports, uploads, and public token endpoints + +## 6. Required Penetration Test Scenarios + +### Scenario A: Parent Attempts Data Theft + +1. Login as Parent A. +2. Attempt to access Parent B’s student profile, attendance, report card, invoice, payment history, PDFs, emergency contacts, and messages by changing IDs. + +Expected result: all access blocked. + +### Scenario B: Teacher Attempts Class Escalation + +1. Login as Teacher A. +2. Attempt to view Teacher B’s roster, submit attendance, submit scores, create assignments, view incidents, and generate class reports for Teacher B’s class. + +Expected result: all access blocked. + +### Scenario C: Finance Tampering + +1. Login as parent or low-privilege user. +2. Attempt to modify invoice totals, apply invalid discounts, replay payment transactions, access another family’s invoices, and manipulate installment/carryforward IDs. + +Expected result: all financial tampering blocked and balances unchanged. + +### Scenario D: Grading Tampering + +1. Login as teacher, parent, and student. +2. Attempt to alter locked scores, finalized report cards, unrelated student scores, grading weights, and strong grading flags. + +Expected result: academic records remain unchanged. + +### Scenario E: Public Token Enumeration + +1. Attempt to guess certificate, badge, and notification tokens. +2. Try expired, revoked, malformed, short, sequential, and random tokens. + +Expected result: invalid tokens fail safely and reveal no private data. + +### Scenario F: Malicious Upload + +1. Attempt to upload executable, script, oversized, double-extension, SVG-script, MIME-mismatched, and path-traversal filenames across all upload endpoints. + +Expected result: unsafe uploads rejected and no public execution path exists. + +### Scenario G: Stored XSS Attempt + +1. Inject scripts into messages, notes, incidents, broadcast emails, frontend page content, policies, student names, family names, and finance notes. + +Expected result: content is rejected, sanitized, or safely escaped when rendered. + +### Scenario H: Command and Job Idempotency Abuse + +1. Run attendance publish, PayPal sync, payment notification, cleanup, absentee summary, late summary, and carryforward-related commands repeatedly. + +Expected result: no duplicate records, duplicate notifications, duplicate payments, or corrupted state. + +## 7. Penetration Testing Deliverables + +Each penetration test cycle should produce: + +- Executive summary +- Scope and environment +- Test accounts used +- Tools used +- Attack surface map +- Findings by severity +- Reproduction steps +- Affected endpoints +- Evidence screenshots or request/response samples +- Business impact +- Technical impact +- Recommended fix +- Retest result +- Residual risk +- Final sign-off + +Severity levels: + +- Critical: unauthorized access to student/finance/medical data, account takeover, payment tampering, remote code execution +- High: privilege escalation, IDOR, stored XSS, unauthorized grading/finance mutation +- Medium: sensitive metadata leakage, weak rate limiting, unsafe file handling without direct exploit +- Low: minor headers, verbose non-sensitive errors, low-impact misconfiguration +- Informational: hardening suggestions + +## 8. Release Gate + +A release should not proceed if any of these remain open: + +- Critical finding +- High-risk IDOR +- Authentication bypass +- Role escalation +- Finance balance manipulation +- Unauthorized score mutation +- Unauthorized report card/certificate/PDF access +- Public token predictability +- Stored XSS in admin/parent/teacher views +- Production debug exposure +- Exposed secrets +- Duplicate payment processing vulnerability + +Medium findings may be accepted only with documented risk acceptance and a remediation deadline. + +Low and informational findings should be tracked, not ignored. That is how small cracks become incident reports with meetings. + +## 9. Definition of Done for Penetration Testing + +Penetration testing is complete only when: + +- The full route inventory has been reviewed. +- All critical workflows have role and ownership attack tests. +- All public token routes have enumeration tests. +- All upload and download routes have abuse tests. +- Finance mutation routes have tampering and replay tests. +- Grading mutation routes have lock-bypass tests. +- Attendance and dismissal routes have safety-boundary tests. +- Scanner findings have been manually verified. +- False positives have been documented. +- Valid findings have fixes or approved risk acceptance. +- Retesting confirms fixes. +- No critical or high-severity finding remains unresolved before release. + +--- + +# Part VII: Overall Release Quality Gates + +Before release, require: + +- All migrations pass from empty database. +- All seeders pass. +- All feature tests pass. +- All unit tests pass. +- Route contract tests pass. +- No public route exposes protected data. +- No role can access unrelated records. +- Finance totals reconcile. +- Grading calculations are snapshot/audit-safe. +- PDF/file routes enforce authorization. +- Commands are idempotent. +- Docker build succeeds. +- CI pipeline succeeds. +- Production environment variables are documented. +- No critical or high penetration-test finding remains open. + +--- + +# Part VIII: High-Risk Areas + +## Finance + +Finance is the highest-risk area because invoices, payments, PayPal sync, refunds, carryforwards, discounts, installments, and event charges all touch balances. Treat every finance workflow as a reconciliation problem, not a CRUD problem. + +## Grading + +Grading is high-risk because legacy and strong grading modes coexist. The system must never silently recalculate historical scores. + +## Authorization + +Authorization is high-risk because the app has many roles and many ownership boundaries. Parent/student/teacher data leakage would be a serious failure. + +## Attendance + +Attendance is high-risk because it drives reports, parent communications, consequences, and administrative summaries. + +## File Generation + +File generation is high-risk because PDFs, badges, certificates, receipts, and reports can expose private student or family data. + +## Scheduled Commands + +Scheduled commands are high-risk because repeated execution can create duplicate records if not designed idempotently. + +--- + +# Part IX: Final Definition of Done + +A use case is done only when: + +- The happy path passes. +- The wrong-role path fails. +- The unauthenticated path fails. +- The wrong-owner path fails. +- Invalid input fails cleanly. +- Duplicate submission is handled safely. +- Invalid state transition is blocked. +- Database records are correct. +- Related modules see the updated state. +- Audit/log/notification records are created where expected. +- The workflow works across current school year rules. +- Closed school year mutation is blocked. +- Security and penetration-test risks are accounted for. +- The test is automated or has a documented manual test script. + +Anything less is not done. It is merely optimistic. diff --git a/failed-tests.txt b/failed-tests.txt new file mode 100644 index 0000000000000000000000000000000000000000..5ef5a8ee0dae4dd095354be5d8b4b5d9658e36a8 GIT binary patch literal 122 zcmezWkC%aqA%G!~p@^ZFA(cS^h>L(QABe4hyikTbhGd2uARi=~0u)ha&;ViuhD@N0 k0)sP9CKm`3fjp3G4pT0Fkj34FCWD literal 0 HcmV?d00001 diff --git a/gitlab-ci.yml b/gitlab-ci.yml index 0a93c318..a15d0b76 100644 --- a/gitlab-ci.yml +++ b/gitlab-ci.yml @@ -1,9 +1,5 @@ -# GitLab CI for Laravel 12 -# Save this file as: .gitlab-ci.yml - -default: - tags: - - runner1 +# GitLab CI for Laravel +# GitLab reads this file automatically when it is committed at the repository root as `.gitlab-ci.yml`. workflow: rules: @@ -15,150 +11,78 @@ workflow: when: always - if: '$CI_COMMIT_TAG' when: always - - when: always + - when: never stages: - - validate - test - - build - - security variables: APP_ENV: testing APP_DEBUG: "false" LOG_CHANNEL: stderr - CACHE_DRIVER: array - SESSION_DRIVER: array - QUEUE_CONNECTION: sync DB_CONNECTION: sqlite DB_DATABASE: "$CI_PROJECT_DIR/database/database.sqlite" + CACHE_DRIVER: array + CACHE_STORE: array + SESSION_DRIVER: array + QUEUE_CONNECTION: sync + MAIL_MAILER: array + BROADCAST_CONNECTION: log + FILESYSTEM_DISK: local COMPOSER_CACHE_DIR: "$CI_PROJECT_DIR/.composer-cache" - NPM_CONFIG_CACHE: "$CI_PROJECT_DIR/.npm-cache" + XDEBUG_MODE: coverage cache: key: files: - composer.lock - - package-lock.json paths: - .composer-cache/ - - .npm-cache/ - vendor/ - - node_modules/ -.php_laravel_job: +phpunit: image: php:8.3-cli + stage: test before_script: - apt-get update - - apt-get install -y --no-install-recommends git unzip curl libzip-dev libpng-dev libonig-dev libxml2-dev libsqlite3-dev - - docker-php-ext-install pdo pdo_sqlite mbstring zip gd + - apt-get install -y --no-install-recommends git unzip curl libzip-dev libpng-dev libonig-dev libxml2-dev libsqlite3-dev libcurl4-openssl-dev + - docker-php-ext-install curl dom gd mbstring pdo pdo_sqlite xml xmlwriter zip - php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" - php composer-setup.php --install-dir=/usr/local/bin --filename=composer - rm composer-setup.php - composer install --prefer-dist --no-interaction --no-progress - - cp .env.example .env || true - | - cat > .env <<'EOF' + cat > .env < + + - + + + + + @@ -33,4 +39,4 @@ - + \ No newline at end of file diff --git a/resources/docs/controllers.md b/resources/docs/controllers.md index 8a72a97d..7923d9a6 100644 --- a/resources/docs/controllers.md +++ b/resources/docs/controllers.md @@ -76,7 +76,7 @@ This document lists every API controller alphabetically with any inline route co **File:** `app/Http/Controllers/Api/Auth/AuthController.php` **Purpose:** JWT-backed authentication endpoints. -- `POST /api/v1/auth/login` (`login`) – Validates `email` and `password`, looks up the user case-insensitively, uses `verify_stored_password`, and returns `{access_token, token_type, expires_in}` or `401`. +- `POST /api/v1/auth/login` (`login`) – Validates `email` and `password`, looks up the user case-insensitively, uses `verify_stored_password`, and returns a top-level `{token, access_token, token_type, expires_in, user}` payload or `401`. - `GET /api/v1/auth/me` (`me`) – Requires a valid bearer token, returns the decoded `User` JSON via `JWTAuth::parseToken()->authenticate()`. - `POST /api/v1/auth/logout` (`logout`) – Invalidates the caller’s JWT token (best-effort) and returns `{message: "Logged out"}` even if the token was already invalid. diff --git a/resources/docs/openapi.json b/resources/docs/openapi.json index f07ce948..c0e59ccf 100644 --- a/resources/docs/openapi.json +++ b/resources/docs/openapi.json @@ -1,5418 +1,8915 @@ { - "openapi": "3.0.3", - "info": { - "title": "Alrahma Unified API", - "version": "1.0.0", - "description": "HTTP endpoints that power the Alrahma platform. Routes are namespaced under /api/v1 unless noted otherwise." + "openapi": "3.0.3", + "info": { + "title": "Alrahma Unified API", + "version": "1.0.0", + "description": "HTTP endpoints that power the Alrahma platform. Routes are namespaced under /api/v1 unless noted otherwise." + }, + "servers": [ + { + "url": "http://localhost:8000", + "description": "Local development server" }, - "servers": [ - { - "url": "http://localhost:8000", - "description": "Local development server" + { + "url": "https://localhost", + "description": "Local HTTPS server" + }, + { + "url": "{scheme}://{host}", + "description": "Custom environment", + "variables": { + "scheme": { + "default": "https", + "enum": [ + "http", + "https" + ] }, - { - "url": "https://localhost", - "description": "Local HTTPS server" - }, - { - "url": "{scheme}://{host}", - "description": "Custom environment", - "variables": { - "scheme": { - "default": "https", - "enum": ["http", "https"] - }, - "host": { - "default": "localhost:8000" - } - } + "host": { + "default": "localhost:8000" } - ], - "components": { - "securitySchemes": { - "bearerAuth": { - "type": "http", - "scheme": "bearer", - "bearerFormat": "JWT" + } + } + ], + "components": { + "securitySchemes": { + "bearerAuth": { + "type": "http", + "scheme": "bearer", + "bearerFormat": "JWT" + } + }, + "schemas": { + "ErrorResponse": { + "type": "object", + "properties": { + "status": { + "type": "boolean", + "example": false + }, + "message": { + "type": "string", + "example": "Validation failed" + }, + "errors": { + "type": "object", + "additionalProperties": true + } + } + }, + "Pagination": { + "type": "object", + "properties": { + "current_page": { + "type": "integer", + "example": 1 + }, + "per_page": { + "type": "integer", + "example": 20 + }, + "total": { + "type": "integer", + "example": 250 + }, + "total_pages": { + "type": "integer", + "example": 13 + } + } + }, + "PaginatedResponse": { + "type": "object", + "properties": { + "status": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "Students retrieved successfully" + }, + "data": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Student" + } + }, + "pagination": { + "$ref": "#/components/schemas/Pagination" + } } - }, - "schemas": { - "ErrorResponse": { + } + } + }, + "Student": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "example": 42 + }, + "school_year": { + "type": "string" + }, + "school_id": { + "type": "string" + }, + "firstname": { + "type": "string", + "example": "Ahmed" + }, + "lastname": { + "type": "string", + "example": "Rahman" + }, + "dob": { + "type": "string", + "format": "date", + "example": "2013-04-22" + }, + "age": { + "type": "integer" + }, + "gender": { + "type": "string", + "enum": [ + "male", + "female" + ] + }, + "is_active": { + "type": "boolean" + }, + "registration_grade": { + "type": "string" + }, + "is_new": { + "type": "boolean" + }, + "photo_consent": { + "type": "boolean" + }, + "parent_id": { + "type": "integer" + }, + "registration_date": { + "type": "string", + "format": "date" + }, + "tuition_paid": { + "type": "boolean" + }, + "semester": { + "type": "string" + }, + "year_of_registration": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "cellphone": { + "type": "string" + }, + "email": { + "type": "string" + }, + "relation": { + "type": "string" + }, + "current_class": { + "type": "object", + "nullable": true + } + } + }, + "AttendanceRecord": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "example": 321 + }, + "student_id": { + "type": "integer", + "example": 42 + }, + "class_section_id": { + "type": "integer", + "example": 8 + }, + "date": { + "type": "string", + "format": "date", + "example": "2024-11-01" + }, + "status": { + "type": "string", + "enum": [ + "present", + "absent", + "late" + ] + }, + "notes": { + "type": "string", + "nullable": true + } + } + }, + "Invoice": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "example": 101 + }, + "parent_id": { + "type": "integer", + "example": 55 + }, + "status": { + "type": "string", + "example": "pending" + }, + "balance": { + "type": "number", + "format": "float", + "example": 125.5 + }, + "description": { + "type": "string" + }, + "school_year": { + "type": "string", + "example": "2024-2025" + }, + "semester": { + "type": "string", + "example": "Fall" + } + } + }, + "LoginRequest": { + "type": "object", + "required": [ + "email", + "password" + ], + "properties": { + "email": { + "type": "string", + "format": "email", + "example": "admin@example.com" + }, + "password": { + "type": "string", + "example": "secret" + } + } + }, + "LoginResponse": { + "type": "object", + "required": [ + "status", + "token", + "access_token", + "token_type", + "expires_in", + "user" + ], + "properties": { + "status": { + "type": "boolean", + "example": true + }, + "token": { + "type": "string", + "example": "eyJ0eXAiOiJKV1QiLCJh..." + }, + "access_token": { + "type": "string", + "example": "eyJ0eXAiOiJKV1QiLCJh..." + }, + "token_type": { + "type": "string", + "example": "bearer" + }, + "expires_in": { + "type": "integer", + "example": 216000 + }, + "user": { + "type": "object", + "required": [ + "id", + "name", + "roles" + ], + "properties": { + "id": { + "type": "integer", + "example": 2 + }, + "name": { + "type": "string", + "example": "Amina Teacher" + }, + "firstname": { + "type": "string", + "nullable": true, + "example": "Amina" + }, + "lastname": { + "type": "string", + "nullable": true, + "example": "Teacher" + }, + "email": { + "type": "string", + "format": "email", + "nullable": true, + "example": "amina.teacher@example.test" + }, + "roles": { "type": "object", - "properties": { - "status": { - "type": "boolean", - "example": false - }, - "message": { - "type": "string", - "example": "Validation failed" - }, - "errors": { - "type": "object", - "additionalProperties": true - } - } - }, - "Pagination": { - "type": "object", - "properties": { - "current_page": { - "type": "integer", - "example": 1 - }, - "per_page": { - "type": "integer", - "example": 20 - }, - "total": { - "type": "integer", - "example": 250 - }, - "total_pages": { - "type": "integer", - "example": 13 - } - } - }, - "PaginatedResponse": { - "type": "object", - "properties": { - "status": { - "type": "boolean", - "example": true - }, - "message": { - "type": "string", - "example": "Students retrieved successfully" - }, - "data": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Student" - } - }, - "pagination": { - "$ref": "#/components/schemas/Pagination" - } - } - } - } - }, - "Student": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "example": 42 - }, - "school_year": { - "type": "string" - }, - "school_id": { - "type": "string" - }, - "firstname": { - "type": "string", - "example": "Ahmed" - }, - "lastname": { - "type": "string", - "example": "Rahman" - }, - "dob": { - "type": "string", - "format": "date", - "example": "2013-04-22" - }, - "age": { - "type": "integer" - }, - "gender": { - "type": "string", - "enum": ["male", "female"] - }, - "is_active": { - "type": "boolean" - }, - "registration_grade": { - "type": "string" - }, - "is_new": { - "type": "boolean" - }, - "photo_consent": { - "type": "boolean" - }, - "parent_id": { - "type": "integer" - }, - "registration_date": { - "type": "string", - "format": "date" - }, - "tuition_paid": { - "type": "boolean" - }, - "semester": { - "type": "string" - }, - "year_of_registration": { - "type": "integer" - }, - "name": { - "type": "string" - }, - "cellphone": { - "type": "string" - }, - "email": { - "type": "string" - }, - "relation": { - "type": "string" - }, - "current_class": { - "type": "object", - "nullable": true - } - } - }, - "AttendanceRecord": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "example": 321 - }, - "student_id": { - "type": "integer", - "example": 42 - }, - "class_section_id": { - "type": "integer", - "example": 8 - }, - "date": { - "type": "string", - "format": "date", - "example": "2024-11-01" - }, - "status": { - "type": "string", - "enum": ["present", "absent", "late"] - }, - "notes": { - "type": "string", - "nullable": true - } - } - }, - "Invoice": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "example": 101 - }, - "parent_id": { - "type": "integer", - "example": 55 - }, - "status": { - "type": "string", - "example": "pending" - }, - "balance": { - "type": "number", - "format": "float", - "example": 125.50 - }, - "description": { - "type": "string" - }, - "school_year": { - "type": "string", - "example": "2024-2025" - }, - "semester": { - "type": "string", - "example": "Fall" - } - } - }, - "LoginRequest": { - "type": "object", - "required": ["email", "password"], - "properties": { - "email": { - "type": "string", - "format": "email", - "example": "admin@example.com" - }, - "password": { - "type": "string", - "example": "secret" - } - } - }, - "LoginResponse": { - "type": "object", - "properties": { - "status": { - "type": "boolean", - "example": true - }, - "message": { - "type": "string", - "example": "Authenticated." - }, - "data": { - "type": "object", - "properties": { - "user": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "example": 2 - }, - "firstname": { - "type": "string", - "example": "makamak" - }, - "lastname": { - "type": "string", - "example": "mkmkmkm" - }, - "email": { - "type": "string", - "format": "email", - "example": "mkdfso2123@gmail.com" - }, - "roles": { - "type": "array", - "items": { - "type": "string", - "example": "parent" - } - } - } - }, - "jwt": { - "type": "object", - "properties": { - "access_token": { - "type": "string", - "example": "eyJ0eXAiOiJKV1QiLCJh..." - }, - "token_type": { - "type": "string", - "example": "bearer" - }, - "expires_in": { - "type": "integer", - "example": 216000 - } - } - }, - "sanctum": { - "type": "object", - "properties": { - "access_token": { - "type": "string", - "example": "2|5NJcqQ8YixX8NIhvKZpGoW1x4BUzhEsshSiIbCFFce48f27f" - }, - "token_type": { - "type": "string", - "example": "bearer" - } - } - } - } - } - } - }, - "RegisterRequest": { - "type": "object", - "required": [ - "firstname", - "lastname", - "gender", - "email", - "confirm_email", - "cellphone", - "city", - "state", - "zip", - "accept_school_policy", - "captcha" - ], - "properties": { - "firstname": { - "type": "string", - "example": "Yara" - }, - "lastname": { - "type": "string", - "example": "Khalil" - }, - "gender": { - "type": "string", - "example": "Female" - }, - "email": { - "type": "string", - "format": "email", - "example": "parent@example.com" - }, - "confirm_email": { - "type": "string", - "format": "email", - "example": "parent@example.com" - }, - "cellphone": { - "type": "string", - "example": "203-555-1234" - }, - "address_street": { - "type": "string", - "example": "123 Main St" - }, - "apt": { - "type": "string", - "example": "Apt 4B" - }, - "city": { - "type": "string", - "example": "New Haven" - }, - "state": { - "type": "string", - "example": "CT" - }, - "zip": { - "type": "string", - "example": "06510" - }, - "accept_school_policy": { - "type": "boolean", - "example": true - }, - "captcha": { - "type": "string", - "example": "A1B2" - }, - "is_parent": { - "type": "boolean", - "example": true - }, - "no_second_parent_info": { - "type": "boolean", - "example": false - }, - "second_firstname": { - "type": "string", - "example": "Omar" - }, - "second_lastname": { - "type": "string", - "example": "Khalil" - }, - "second_gender": { - "type": "string", - "example": "Male" - }, - "second_email": { - "type": "string", - "format": "email", - "example": "second.parent@example.com" - }, - "second_cellphone": { - "type": "string", - "example": "203-555-4567" - } - } - }, - "UserSummary": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "example": 4321 - }, - "firstname": { - "type": "string", - "example": "Yara" - }, - "lastname": { - "type": "string", - "example": "Khalil" - }, - "email": { - "type": "string", - "format": "email", - "example": "parent@example.com" - }, - "school_id": { - "type": "string", - "example": "SID-AB12CD34" - }, - "status": { - "type": "string", - "example": "active" - } - } - }, - "RegisterResponse": { - "type": "object", - "properties": { - "ok": { - "type": "boolean", - "example": true - }, - "registration": { - "type": "object", - "properties": { - "user_id": { - "type": "integer", - "example": 4321 - }, - "email": { - "type": "string", - "format": "email", - "example": "parent@example.com" - }, - "firstname": { - "type": "string", - "example": "Yara" - }, - "lastname": { - "type": "string", - "example": "Khalil" - }, - "role": { - "type": "string", - "example": "parent" - }, - "activation_sent": { - "type": "boolean", - "example": true - } - } - } - } - }, - "ForgotPasswordRequest": { - "type": "object", - "required": ["email"], - "properties": { - "email": { - "type": "string", - "format": "email", - "example": "parent@example.com" - } - } - }, - "PasswordResetRequest": { - "type": "object", - "required": ["token", "password", "pass_confirm"], - "properties": { - "token": { - "type": "string", - "example": "abc123reset" - }, - "password": { - "type": "string", - "example": "Str0ngReset!" - }, - "pass_confirm": { - "type": "string", - "example": "Str0ngReset!" - } - } - }, - "SetPasswordRequest": { - "type": "object", - "required": ["user_id", "token", "password", "password_confirm"], - "properties": { - "user_id": { - "type": "integer", - "example": 4321 - }, - "token": { - "type": "string", - "example": "def456confirm" - }, - "password": { - "type": "string", - "example": "NewPass123!" - }, - "password_confirm": { - "type": "string", - "example": "NewPass123!" - } - } - }, - "MessageResponse": { - "type": "object", - "properties": { - "status": { - "type": "boolean", - "example": true - }, - "message": { - "type": "string", - "example": "Check your inbox for the next steps." - } - } - }, - "LoginActivityRecord": { - "type": "object", - "properties": { - "user_id": { - "type": "integer", - "example": 7 - }, - "email": { - "type": "string", - "format": "email", - "example": "parent@example.com" - }, - "login_time": { - "type": "string", - "format": "date-time", - "example": "2025-08-15T08:22:41" - }, - "logout_time": { - "type": [ - "string", - "null" - ], - "format": "date-time", - "example": null - }, - "ip_address": { - "type": "string", - "example": "192.0.2.55" - }, - "user_agent": { - "type": "string", - "example": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)" - } - } - }, - "LoginActivityResponse": { - "type": "object", - "properties": { - "status": { - "type": "boolean", - "example": true - }, - "data": { - "type": "object", - "properties": { - "activities": { - "type": "array", - "items": { - "$ref": "#/components/schemas/LoginActivityRecord" - } - }, - "pagination": { - "$ref": "#/components/schemas/Pagination" - } - } - }, - "message": { - "type": "string", - "example": "Login activity retrieved" - } - } - }, - "WebRegisterForm": { - "type": "object", - "required": [ - "firstname", - "lastname", - "gender", - "email", - "confirm_email", - "cellphone", - "address_street", - "city", - "state", - "zip", - "accept_school_policy", - "captcha" - ], - "properties": { - "firstname": { - "type": "string", - "example": "Amina" - }, - "lastname": { - "type": "string", - "example": "El-Sayed" - }, - "gender": { - "type": "string", - "enum": ["Male", "Female"], - "example": "Female" - }, - "email": { - "type": "string", - "format": "email", - "example": "parent@alrahma.org" - }, - "confirm_email": { - "type": "string", - "format": "email", - "example": "parent@alrahma.org" - }, - "cellphone": { - "type": "string", - "example": "203-555-0147" - }, - "address_street": { - "type": "string", - "example": "123 Elm Street" - }, - "apt": { - "type": "string", - "example": "Apt 5" - }, - "city": { - "type": "string", - "example": "Hartford" - }, - "state": { - "type": "string", - "enum": ["CT", "ME", "MA", "NH", "NY", "RI", "VT"], - "example": "CT" - }, - "zip": { - "type": "string", - "example": "06106" - }, - "accept_school_policy": { - "type": "boolean", - "example": true - }, - "captcha": { - "type": "string", - "example": "7F3Z" - }, - "is_parent": { - "type": "boolean", - "example": true - }, - "no_second_parent_info": { - "type": "boolean", - "example": false - }, - "second_firstname": { - "type": "string", - "example": "Maya" - }, - "second_lastname": { - "type": "string", - "example": "El-Sayed" - }, - "second_gender": { - "type": "string", - "enum": ["Male", "Female"], - "example": "Female" - }, - "second_email": { - "type": "string", - "format": "email", - "example": "co-parent@alrahma.org" - }, - "second_cellphone": { - "type": "string", - "example": "203-555-0189" - } - } - }, - "WebLoginForm": { - "type": "object", - "required": ["email", "password"], - "properties": { - "email": { - "type": "string", - "format": "email", - "example": "parent@alrahma.org" - }, - "password": { - "type": "string", - "example": "SecurePass123!" - } - } - }, - "WebForgotPasswordForm": { - "type": "object", - "required": ["email"], - "properties": { - "email": { - "type": "string", - "format": "email", - "example": "parent@alrahma.org" - } - } - }, - "WebResetPasswordForm": { - "type": "object", - "required": ["token", "password", "pass_confirm"], - "properties": { - "token": { - "type": "string", - "example": "abc123reset" - }, - "password": { - "type": "string", - "example": "Str0ngReset!" - }, - "pass_confirm": { - "type": "string", - "example": "Str0ngReset!" - } - } - }, - "WebSetPasswordForm": { - "type": "object", - "required": ["user_id", "token", "password", "password_confirm"], - "properties": { - "user_id": { - "type": "integer", - "example": 4321 - }, - "token": { - "type": "string", - "example": "def456confirm" - }, - "password": { - "type": "string", - "example": "NewPass123!" - }, - "password_confirm": { - "type": "string", - "example": "NewPass123!" - } + "additionalProperties": { + "type": "boolean" + }, + "example": { + "teacher": true } + }, + "class_section_id": { + "type": "integer", + "nullable": true, + "example": 42 + }, + "class_section_name": { + "type": "string", + "nullable": true, + "example": "Grade 5 - A" + } } + } + } + }, + "RegisterRequest": { + "type": "object", + "required": [ + "firstname", + "lastname", + "gender", + "email", + "confirm_email", + "cellphone", + "city", + "state", + "zip", + "accept_school_policy", + "captcha" + ], + "properties": { + "firstname": { + "type": "string", + "example": "Yara" + }, + "lastname": { + "type": "string", + "example": "Khalil" + }, + "gender": { + "type": "string", + "example": "Female" + }, + "email": { + "type": "string", + "format": "email", + "example": "parent@example.com" + }, + "confirm_email": { + "type": "string", + "format": "email", + "example": "parent@example.com" + }, + "cellphone": { + "type": "string", + "example": "203-555-1234" + }, + "address_street": { + "type": "string", + "example": "123 Main St" + }, + "apt": { + "type": "string", + "example": "Apt 4B" + }, + "city": { + "type": "string", + "example": "New Haven" + }, + "state": { + "type": "string", + "example": "CT" + }, + "zip": { + "type": "string", + "example": "06510" + }, + "accept_school_policy": { + "type": "boolean", + "example": true + }, + "captcha": { + "type": "string", + "example": "A1B2" + }, + "is_parent": { + "type": "boolean", + "example": true + }, + "no_second_parent_info": { + "type": "boolean", + "example": false + }, + "second_firstname": { + "type": "string", + "example": "Omar" + }, + "second_lastname": { + "type": "string", + "example": "Khalil" + }, + "second_gender": { + "type": "string", + "example": "Male" + }, + "second_email": { + "type": "string", + "format": "email", + "example": "second.parent@example.com" + }, + "second_cellphone": { + "type": "string", + "example": "203-555-4567" + } + } + }, + "UserSummary": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "example": 4321 + }, + "firstname": { + "type": "string", + "example": "Yara" + }, + "lastname": { + "type": "string", + "example": "Khalil" + }, + "email": { + "type": "string", + "format": "email", + "example": "parent@example.com" + }, + "school_id": { + "type": "string", + "example": "SID-AB12CD34" + }, + "status": { + "type": "string", + "example": "active" + } + } + }, + "RegisterResponse": { + "type": "object", + "properties": { + "ok": { + "type": "boolean", + "example": true + }, + "registration": { + "type": "object", + "properties": { + "user_id": { + "type": "integer", + "example": 4321 + }, + "email": { + "type": "string", + "format": "email", + "example": "parent@example.com" + }, + "firstname": { + "type": "string", + "example": "Yara" + }, + "lastname": { + "type": "string", + "example": "Khalil" + }, + "role": { + "type": "string", + "example": "parent" + }, + "activation_sent": { + "type": "boolean", + "example": true + } + } + } + } + }, + "ForgotPasswordRequest": { + "type": "object", + "required": [ + "email" + ], + "properties": { + "email": { + "type": "string", + "format": "email", + "example": "parent@example.com" + } + } + }, + "PasswordResetRequest": { + "type": "object", + "required": [ + "token", + "password", + "pass_confirm" + ], + "properties": { + "token": { + "type": "string", + "example": "abc123reset" + }, + "password": { + "type": "string", + "example": "Str0ngReset!" + }, + "pass_confirm": { + "type": "string", + "example": "Str0ngReset!" + } + } + }, + "SetPasswordRequest": { + "type": "object", + "required": [ + "user_id", + "token", + "password", + "password_confirm" + ], + "properties": { + "user_id": { + "type": "integer", + "example": 4321 + }, + "token": { + "type": "string", + "example": "def456confirm" + }, + "password": { + "type": "string", + "example": "NewPass123!" + }, + "password_confirm": { + "type": "string", + "example": "NewPass123!" + } + } + }, + "MessageResponse": { + "type": "object", + "properties": { + "status": { + "type": "boolean", + "example": true + }, + "message": { + "type": "string", + "example": "Check your inbox for the next steps." + } + } + }, + "LoginActivityRecord": { + "type": "object", + "properties": { + "user_id": { + "type": "integer", + "example": 7 + }, + "email": { + "type": "string", + "format": "email", + "example": "parent@example.com" + }, + "login_time": { + "type": "string", + "format": "date-time", + "example": "2025-08-15T08:22:41" + }, + "logout_time": { + "type": [ + "string", + "null" + ], + "format": "date-time", + "example": null + }, + "ip_address": { + "type": "string", + "example": "192.0.2.55" + }, + "user_agent": { + "type": "string", + "example": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)" + } + } + }, + "LoginActivityResponse": { + "type": "object", + "properties": { + "status": { + "type": "boolean", + "example": true + }, + "data": { + "type": "object", + "properties": { + "activities": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LoginActivityRecord" + } + }, + "pagination": { + "$ref": "#/components/schemas/Pagination" + } + } + }, + "message": { + "type": "string", + "example": "Login activity retrieved" + } + } + }, + "WebRegisterForm": { + "type": "object", + "required": [ + "firstname", + "lastname", + "gender", + "email", + "confirm_email", + "cellphone", + "address_street", + "city", + "state", + "zip", + "accept_school_policy", + "captcha" + ], + "properties": { + "firstname": { + "type": "string", + "example": "Amina" + }, + "lastname": { + "type": "string", + "example": "El-Sayed" + }, + "gender": { + "type": "string", + "enum": [ + "Male", + "Female" + ], + "example": "Female" + }, + "email": { + "type": "string", + "format": "email", + "example": "parent@alrahma.org" + }, + "confirm_email": { + "type": "string", + "format": "email", + "example": "parent@alrahma.org" + }, + "cellphone": { + "type": "string", + "example": "203-555-0147" + }, + "address_street": { + "type": "string", + "example": "123 Elm Street" + }, + "apt": { + "type": "string", + "example": "Apt 5" + }, + "city": { + "type": "string", + "example": "Hartford" + }, + "state": { + "type": "string", + "enum": [ + "CT", + "ME", + "MA", + "NH", + "NY", + "RI", + "VT" + ], + "example": "CT" + }, + "zip": { + "type": "string", + "example": "06106" + }, + "accept_school_policy": { + "type": "boolean", + "example": true + }, + "captcha": { + "type": "string", + "example": "7F3Z" + }, + "is_parent": { + "type": "boolean", + "example": true + }, + "no_second_parent_info": { + "type": "boolean", + "example": false + }, + "second_firstname": { + "type": "string", + "example": "Maya" + }, + "second_lastname": { + "type": "string", + "example": "El-Sayed" + }, + "second_gender": { + "type": "string", + "enum": [ + "Male", + "Female" + ], + "example": "Female" + }, + "second_email": { + "type": "string", + "format": "email", + "example": "co-parent@alrahma.org" + }, + "second_cellphone": { + "type": "string", + "example": "203-555-0189" + } + } + }, + "WebLoginForm": { + "type": "object", + "required": [ + "email", + "password" + ], + "properties": { + "email": { + "type": "string", + "format": "email", + "example": "parent@alrahma.org" + }, + "password": { + "type": "string", + "example": "SecurePass123!" + } + } + }, + "WebForgotPasswordForm": { + "type": "object", + "required": [ + "email" + ], + "properties": { + "email": { + "type": "string", + "format": "email", + "example": "parent@alrahma.org" + } + } + }, + "WebResetPasswordForm": { + "type": "object", + "required": [ + "token", + "password", + "pass_confirm" + ], + "properties": { + "token": { + "type": "string", + "example": "abc123reset" + }, + "password": { + "type": "string", + "example": "Str0ngReset!" + }, + "pass_confirm": { + "type": "string", + "example": "Str0ngReset!" + } + } + }, + "WebSetPasswordForm": { + "type": "object", + "required": [ + "user_id", + "token", + "password", + "password_confirm" + ], + "properties": { + "user_id": { + "type": "integer", + "example": 4321 + }, + "token": { + "type": "string", + "example": "def456confirm" + }, + "password": { + "type": "string", + "example": "NewPass123!" + }, + "password_confirm": { + "type": "string", + "example": "NewPass123!" + } + } + } + }, + "responses": { + "UnauthorizedError": { + "description": "Missing or invalid bearer token", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ], + "x-controllers": [ + { + "name": "AdministratorController", + "class": "App\\Http\\Controllers\\Api\\AdministratorController", + "path": "app/Http/Controllers/Api/AdministratorController.php" + }, + { + "name": "AssignmentController", + "class": "App\\Http\\Controllers\\Api\\AssignmentController", + "path": "app/Http/Controllers/Api/AssignmentController.php" + }, + { + "name": "AttendanceController", + "class": "App\\Http\\Controllers\\Api\\AttendanceController", + "path": "app/Http/Controllers/Api/AttendanceController.php" + }, + { + "name": "AttendanceTrackingController", + "class": "App\\Http\\Controllers\\Api\\AttendanceTrackingController", + "path": "app/Http/Controllers/Api/AttendanceTrackingController.php" + }, + { + "name": "AuthorizedUsersController", + "class": "App\\Http\\Controllers\\Api\\AuthorizedUsersController", + "path": "app/Http/Controllers/Api/AuthorizedUsersController.php" + }, + { + "name": "BaseApiController", + "class": "App\\Http\\Controllers\\Api\\Core\\BaseApiController", + "path": "app/Http/Controllers/Api/Core/BaseApiController.php" + }, + { + "name": "BroadcastEmailController", + "class": "App\\Http\\Controllers\\Api\\BroadcastEmailController", + "path": "app/Http/Controllers/Api/BroadcastEmailController.php" + }, + { + "name": "CalendarController", + "class": "App\\Http\\Controllers\\Api\\CalendarController", + "path": "app/Http/Controllers/Api/CalendarController.php" + }, + { + "name": "request adapter", + "class": "App\\Http\\Controllers\\Api\\Core\\request adapter", + "path": "app/Http/Controllers/Api/Core/request adapter.php" + }, + { + "name": "ClassController", + "class": "App\\Http\\Controllers\\Api\\ClassController", + "path": "app/Http/Controllers/Api/ClassController.php" + }, + { + "name": "ClassPreparationController", + "class": "App\\Http\\Controllers\\Api\\ClassPreparationController", + "path": "app/Http/Controllers/Api/ClassPreparationController.php" + }, + { + "name": "CommunicationController", + "class": "App\\Http\\Controllers\\Api\\CommunicationController", + "path": "app/Http/Controllers/Api/CommunicationController.php" + }, + { + "name": "ConfigurationController", + "class": "App\\Http\\Controllers\\Api\\ConfigurationController", + "path": "app/Http/Controllers/Api/ConfigurationController.php" + }, + { + "name": "ContactController", + "class": "App\\Http\\Controllers\\Api\\ContactController", + "path": "app/Http/Controllers/Api/ContactController.php" + }, + { + "name": "DiscountController", + "class": "App\\Http\\Controllers\\Api\\DiscountController", + "path": "app/Http/Controllers/Api/DiscountController.php" + }, + { + "name": "EmailController", + "class": "App\\Http\\Controllers\\Api\\EmailController", + "path": "app/Http/Controllers/Api/EmailController.php" + }, + { + "name": "EmailExtractorController", + "class": "App\\Http\\Controllers\\Api\\EmailExtractorController", + "path": "app/Http/Controllers/Api/EmailExtractorController.php" + }, + { + "name": "EmergencyContactController", + "class": "App\\Http\\Controllers\\Api\\EmergencyContactController", + "path": "app/Http/Controllers/Api/EmergencyContactController.php" + }, + { + "name": "EventController", + "class": "App\\Http\\Controllers\\Api\\EventController", + "path": "app/Http/Controllers/Api/EventController.php" + }, + { + "name": "ExpenseController", + "class": "App\\Http\\Controllers\\Api\\ExpenseController", + "path": "app/Http/Controllers/Api/ExpenseController.php" + }, + { + "name": "ExtraChargesController", + "class": "App\\Http\\Controllers\\Api\\ExtraChargesController", + "path": "app/Http/Controllers/Api/ExtraChargesController.php" + }, + { + "name": "FamilyAdminController", + "class": "App\\Http\\Controllers\\Api\\FamilyAdminController", + "path": "app/Http/Controllers/Api/FamilyAdminController.php" + }, + { + "name": "FamilyController", + "class": "App\\Http\\Controllers\\Api\\FamilyController", + "path": "app/Http/Controllers/Api/FamilyController.php" + }, + { + "name": "FilesController", + "class": "App\\Http\\Controllers\\Api\\FilesController", + "path": "app/Http/Controllers/Api/FilesController.php" + }, + { + "name": "FinalController", + "class": "App\\Http\\Controllers\\Api\\FinalController", + "path": "app/Http/Controllers/Api/FinalController.php" + }, + { + "name": "FinancialController", + "class": "App\\Http\\Controllers\\Api\\FinancialController", + "path": "app/Http/Controllers/Api/FinancialController.php" + }, + { + "name": "FlagController", + "class": "App\\Http\\Controllers\\Api\\FlagController", + "path": "app/Http/Controllers/Api/FlagController.php" + }, + { + "name": "FrontendController", + "class": "App\\Http\\Controllers\\Api\\FrontendController", + "path": "app/Http/Controllers/Api/FrontendController.php" + }, + { + "name": "GradingController", + "class": "App\\Http\\Controllers\\Api\\GradingController", + "path": "app/Http/Controllers/Api/GradingController.php" + }, + { + "name": "HealthController", + "class": "App\\Http\\Controllers\\Api\\HealthController", + "path": "app/Http/Controllers/Api/HealthController.php" + }, + { + "name": "HomeworkController", + "class": "App\\Http\\Controllers\\Api\\HomeworkController", + "path": "app/Http/Controllers/Api/HomeworkController.php" + }, + { + "name": "HomeworkTrackingController", + "class": "App\\Http\\Controllers\\Api\\HomeworkTrackingController", + "path": "app/Http/Controllers/Api/HomeworkTrackingController.php" + }, + { + "name": "InventoryController", + "class": "App\\Http\\Controllers\\Api\\InventoryController", + "path": "app/Http/Controllers/Api/InventoryController.php" + }, + { + "name": "InvoiceController", + "class": "App\\Http\\Controllers\\Api\\InvoiceController", + "path": "app/Http/Controllers/Api/InvoiceController.php" + }, + { + "name": "IpBanController", + "class": "App\\Http\\Controllers\\Api\\IpBanController", + "path": "app/Http/Controllers/Api/IpBanController.php" + }, + { + "name": "LandingPageController", + "class": "App\\Http\\Controllers\\Api\\LandingPageController", + "path": "app/Http/Controllers/Api/LandingPageController.php" + }, + { + "name": "LateSlipLogsController", + "class": "App\\Http\\Controllers\\Api\\LateSlipLogsController", + "path": "app/Http/Controllers/Api/LateSlipLogsController.php" + }, + { + "name": "MessageController", + "class": "App\\Http\\Controllers\\Api\\MessageController", + "path": "app/Http/Controllers/Api/MessageController.php" + }, + { + "name": "MessagesController", + "class": "App\\Http\\Controllers\\Api\\MessagesController", + "path": "app/Http/Controllers/Api/MessagesController.php" + }, + { + "name": "MessagingController", + "class": "App\\Http\\Controllers\\Api\\MessagingController", + "path": "app/Http/Controllers/Api/MessagingController.php" + }, + { + "name": "NavBuilderController", + "class": "App\\Http\\Controllers\\Api\\NavBuilderController", + "path": "app/Http/Controllers/Api/NavBuilderController.php" + }, + { + "name": "NotificationController", + "class": "App\\Http\\Controllers\\Api\\NotificationController", + "path": "app/Http/Controllers/Api/NotificationController.php" + }, + { + "name": "NotificationsController", + "class": "App\\Http\\Controllers\\Api\\NotificationsController", + "path": "app/Http/Controllers/Api/NotificationsController.php" + }, + { + "name": "PageController", + "class": "App\\Http\\Controllers\\Api\\PageController", + "path": "app/Http/Controllers/Api/PageController.php" + }, + { + "name": "ParentAttendanceReportController", + "class": "App\\Http\\Controllers\\Api\\ParentAttendanceReportController", + "path": "app/Http/Controllers/Api/ParentAttendanceReportController.php" + }, + { + "name": "ParentController", + "class": "App\\Http\\Controllers\\Api\\ParentController", + "path": "app/Http/Controllers/Api/ParentController.php" + }, + { + "name": "ParticipationController", + "class": "App\\Http\\Controllers\\Api\\ParticipationController", + "path": "app/Http/Controllers/Api/ParticipationController.php" + }, + { + "name": "PaymentController", + "class": "App\\Http\\Controllers\\Api\\PaymentController", + "path": "app/Http/Controllers/Api/PaymentController.php" + }, + { + "name": "PaymentNotificationController", + "class": "App\\Http\\Controllers\\Api\\PaymentNotificationController", + "path": "app/Http/Controllers/Api/PaymentNotificationController.php" + }, + { + "name": "PaymentTransactionController", + "class": "App\\Http\\Controllers\\Api\\PaymentTransactionController", + "path": "app/Http/Controllers/Api/PaymentTransactionController.php" + }, + { + "name": "PaypalTransactionsController", + "class": "App\\Http\\Controllers\\Api\\PaypalTransactionsController", + "path": "app/Http/Controllers/Api/PaypalTransactionsController.php" + }, + { + "name": "PaypalWebhook", + "class": "App\\Http\\Controllers\\Api\\PaypalWebhook", + "path": "app/Http/Controllers/Api/PaypalWebhook.php" + }, + { + "name": "PolicyController", + "class": "App\\Http\\Controllers\\Api\\PolicyController", + "path": "app/Http/Controllers/Api/PolicyController.php" + }, + { + "name": "PreferencesController", + "class": "App\\Http\\Controllers\\Api\\PreferencesController", + "path": "app/Http/Controllers/Api/PreferencesController.php" + }, + { + "name": "PrintablesReportsController", + "class": "App\\Http\\Controllers\\Api\\PrintablesReportsController", + "path": "app/Http/Controllers/Api/PrintablesReportsController.php" + }, + { + "name": "ProjectController", + "class": "App\\Http\\Controllers\\Api\\ProjectController", + "path": "app/Http/Controllers/Api/ProjectController.php" + }, + { + "name": "PurchaseOrderController", + "class": "App\\Http\\Controllers\\Api\\PurchaseOrderController", + "path": "app/Http/Controllers/Api/PurchaseOrderController.php" + }, + { + "name": "QuizController", + "class": "App\\Http\\Controllers\\Api\\QuizController", + "path": "app/Http/Controllers/Api/QuizController.php" + }, + { + "name": "BadgeScanController", + "class": "App\\Http\\Controllers\\Api\\BadgeScan\\BadgeScanController", + "path": "app/Http/Controllers/Api/BadgeScan/BadgeScanController.php" + }, + { + "name": "RefundController", + "class": "App\\Http\\Controllers\\Api\\RefundController", + "path": "app/Http/Controllers/Api/RefundController.php" + }, + { + "name": "RegisterController", + "class": "App\\Http\\Controllers\\Api\\RegisterController", + "path": "app/Http/Controllers/Api/RegisterController.php" + }, + { + "name": "ReimbursementController", + "class": "App\\Http\\Controllers\\Api\\ReimbursementController", + "path": "app/Http/Controllers/Api/ReimbursementController.php" + }, + { + "name": "RolePermissionController", + "class": "App\\Http\\Controllers\\Api\\Auth\\RolePermissionController", + "path": "app/Http/Controllers/Api/Auth/RolePermissionController.php" + }, + { + "name": "RoleSwitcherController", + "class": "App\\Http\\Controllers\\Api\\Auth\\RoleSwitcherController", + "path": "app/Http/Controllers/Api/Auth/RoleSwitcherController.php" + }, + { + "name": "SchoolCalendarController", + "class": "App\\Http\\Controllers\\Api\\SchoolCalendarController", + "path": "app/Http/Controllers/Api/SchoolCalendarController.php" + }, + { + "name": "ScoreCommentController", + "class": "App\\Http\\Controllers\\Api\\ScoreCommentController", + "path": "app/Http/Controllers/Api/ScoreCommentController.php" + }, + { + "name": "ScoreController", + "class": "App\\Http\\Controllers\\Api\\ScoreController", + "path": "app/Http/Controllers/Api/ScoreController.php" + }, + { + "name": "ScorePredictorController", + "class": "App\\Http\\Controllers\\Api\\ScorePredictorController", + "path": "app/Http/Controllers/Api/ScorePredictorController.php" + }, + { + "name": "SendSMSController", + "class": "App\\Http\\Controllers\\Api\\SendSMSController", + "path": "app/Http/Controllers/Api/SendSMSController.php" + }, + { + "name": "SessionController", + "class": "App\\Http\\Controllers\\Api\\SessionController", + "path": "app/Http/Controllers/Api/SessionController.php" + }, + { + "name": "SessionTimeoutController", + "class": "App\\Http\\Controllers\\Api\\SessionTimeoutController", + "path": "app/Http/Controllers/Api/SessionTimeoutController.php" + }, + { + "name": "SettingsController", + "class": "App\\Http\\Controllers\\Api\\SettingsController", + "path": "app/Http/Controllers/Api/SettingsController.php" + }, + { + "name": "SlipPrinterController", + "class": "App\\Http\\Controllers\\Api\\SlipPrinterController", + "path": "app/Http/Controllers/Api/SlipPrinterController.php" + }, + { + "name": "StaffController", + "class": "App\\Http\\Controllers\\Api\\StaffController", + "path": "app/Http/Controllers/Api/StaffController.php" + }, + { + "name": "StatsController", + "class": "App\\Http\\Controllers\\Api\\System\\StatsController", + "path": "app/Http/Controllers/Api/System/StatsController.php" + }, + { + "name": "StudentController", + "class": "App\\Http\\Controllers\\Api\\StudentController", + "path": "app/Http/Controllers/Api/StudentController.php" + }, + { + "name": "SupplierController", + "class": "App\\Http\\Controllers\\Api\\SupplierController", + "path": "app/Http/Controllers/Api/SupplierController.php" + }, + { + "name": "SupplyCategoryController", + "class": "App\\Http\\Controllers\\Api\\SupplyCategoryController", + "path": "app/Http/Controllers/Api/SupplyCategoryController.php" + }, + { + "name": "SupportController", + "class": "App\\Http\\Controllers\\Api\\SupportController", + "path": "app/Http/Controllers/Api/SupportController.php" + }, + { + "name": "TeacherController", + "class": "App\\Http\\Controllers\\Api\\TeacherController", + "path": "app/Http/Controllers/Api/TeacherController.php" + }, + { + "name": "TestDBController", + "class": "App\\Http\\Controllers\\Api\\TestDBController", + "path": "app/Http/Controllers/Api/TestDBController.php" + }, + { + "name": "TimeController", + "class": "App\\Http\\Controllers\\Api\\TimeController", + "path": "app/Http/Controllers/Api/TimeController.php" + }, + { + "name": "UiController", + "class": "App\\Http\\Controllers\\Api\\UiController", + "path": "app/Http/Controllers/Api/UiController.php" + }, + { + "name": "UserController", + "class": "App\\Http\\Controllers\\Api\\Users\\UserController", + "path": "app/Http/Controllers/Api/Users/UserController.php" + }, + { + "name": "WhatsappController", + "class": "App\\Http\\Controllers\\Api\\WhatsappController", + "path": "app/Http/Controllers/Api/WhatsappController.php" + }, + { + "name": "AuthController", + "class": "App\\Http\\Controllers\\Api\\Auth\\AuthController", + "path": "app/Http/Controllers/Api/Auth/AuthController.php" + } + ], + "paths": { + "/api/v1/auth/register": { + "post": { + "operationId": "register", + "tags": [ + "Auth" + ], + "summary": "Create a parent account and trigger email verification", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RegisterRequest" + } + } + } }, "responses": { - "UnauthorizedError": { - "description": "Missing or invalid bearer token", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } + "201": { + "description": "Parent account created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RegisterResponse" } + } } + }, + "422": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } } + } }, - "security": [ - { + "/api/v1/auth/forgot-password": { + "post": { + "operationId": "forgotPassword", + "tags": [ + "Auth" + ], + "summary": "Request a password reset link", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForgotPasswordRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Email sent (or queued)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MessageResponse" + } + } + } + }, + "422": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/auth/reset-password": { + "post": { + "operationId": "resetPassword", + "tags": [ + "Auth" + ], + "summary": "Submit a new password using the reset token received via email", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PasswordResetRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Password changed successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MessageResponse" + } + } + } + }, + "422": { + "description": "Validation or token error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/auth/set-password": { + "post": { + "operationId": "setPassword", + "tags": [ + "Auth" + ], + "summary": "Complete account setup by choosing a new password after email confirmation", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SetPasswordRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Password saved", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MessageResponse" + } + } + } + }, + "422": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/auth/login": { + "post": { + "operationId": "login", + "tags": [ + "Auth" + ], + "summary": "Issue a JWT for a valid user", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LoginRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Authentication successful", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LoginResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [] + } + }, + "/api/v1/auth/me": { + "get": { + "operationId": "getCurrentUser", + "tags": [ + "Auth" + ], + "summary": "Return the authenticated user's profile", + "security": [ + { "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Profile data" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + } } - ], - "x-controllers": [ - { - "name": "AdministratorController", - "class": "App\\Http\\Controllers\\Api\\AdministratorController", - "path": "app/Http/Controllers/Api/AdministratorController.php" - }, - { - "name": "AssignmentController", - "class": "App\\Http\\Controllers\\Api\\AssignmentController", - "path": "app/Http/Controllers/Api/AssignmentController.php" - }, - { - "name": "AttendanceController", - "class": "App\\Http\\Controllers\\Api\\AttendanceController", - "path": "app/Http/Controllers/Api/AttendanceController.php" - }, - { - "name": "AttendanceTrackingController", - "class": "App\\Http\\Controllers\\Api\\AttendanceTrackingController", - "path": "app/Http/Controllers/Api/AttendanceTrackingController.php" - }, - { - "name": "AuthorizedUsersController", - "class": "App\\Http\\Controllers\\Api\\AuthorizedUsersController", - "path": "app/Http/Controllers/Api/AuthorizedUsersController.php" - }, - { - "name": "BaseApiController", - "class": "App\\Http\\Controllers\\Api\\Core\\BaseApiController", - "path": "app/Http/Controllers/Api/Core/BaseApiController.php" - }, - { - "name": "BroadcastEmailController", - "class": "App\\Http\\Controllers\\Api\\BroadcastEmailController", - "path": "app/Http/Controllers/Api/BroadcastEmailController.php" - }, - { - "name": "CalendarController", - "class": "App\\Http\\Controllers\\Api\\CalendarController", - "path": "app/Http/Controllers/Api/CalendarController.php" - }, - { - "name": "request adapter", - "class": "App\\Http\\Controllers\\Api\\Core\\request adapter", - "path": "app/Http/Controllers/Api/Core/request adapter.php" - }, - { - "name": "ClassController", - "class": "App\\Http\\Controllers\\Api\\ClassController", - "path": "app/Http/Controllers/Api/ClassController.php" - }, - { - "name": "ClassPreparationController", - "class": "App\\Http\\Controllers\\Api\\ClassPreparationController", - "path": "app/Http/Controllers/Api/ClassPreparationController.php" - }, - { - "name": "CommunicationController", - "class": "App\\Http\\Controllers\\Api\\CommunicationController", - "path": "app/Http/Controllers/Api/CommunicationController.php" - }, - { - "name": "ConfigurationController", - "class": "App\\Http\\Controllers\\Api\\ConfigurationController", - "path": "app/Http/Controllers/Api/ConfigurationController.php" - }, - { - "name": "ContactController", - "class": "App\\Http\\Controllers\\Api\\ContactController", - "path": "app/Http/Controllers/Api/ContactController.php" - }, - { - "name": "DiscountController", - "class": "App\\Http\\Controllers\\Api\\DiscountController", - "path": "app/Http/Controllers/Api/DiscountController.php" - }, - { - "name": "EmailController", - "class": "App\\Http\\Controllers\\Api\\EmailController", - "path": "app/Http/Controllers/Api/EmailController.php" - }, - { - "name": "EmailExtractorController", - "class": "App\\Http\\Controllers\\Api\\EmailExtractorController", - "path": "app/Http/Controllers/Api/EmailExtractorController.php" - }, - { - "name": "EmergencyContactController", - "class": "App\\Http\\Controllers\\Api\\EmergencyContactController", - "path": "app/Http/Controllers/Api/EmergencyContactController.php" - }, - { - "name": "EventController", - "class": "App\\Http\\Controllers\\Api\\EventController", - "path": "app/Http/Controllers/Api/EventController.php" - }, - { - "name": "ExpenseController", - "class": "App\\Http\\Controllers\\Api\\ExpenseController", - "path": "app/Http/Controllers/Api/ExpenseController.php" - }, - { - "name": "ExtraChargesController", - "class": "App\\Http\\Controllers\\Api\\ExtraChargesController", - "path": "app/Http/Controllers/Api/ExtraChargesController.php" - }, - { - "name": "FamilyAdminController", - "class": "App\\Http\\Controllers\\Api\\FamilyAdminController", - "path": "app/Http/Controllers/Api/FamilyAdminController.php" - }, - { - "name": "FamilyController", - "class": "App\\Http\\Controllers\\Api\\FamilyController", - "path": "app/Http/Controllers/Api/FamilyController.php" - }, - { - "name": "FilesController", - "class": "App\\Http\\Controllers\\Api\\FilesController", - "path": "app/Http/Controllers/Api/FilesController.php" - }, - { - "name": "FinalController", - "class": "App\\Http\\Controllers\\Api\\FinalController", - "path": "app/Http/Controllers/Api/FinalController.php" - }, - { - "name": "FinancialController", - "class": "App\\Http\\Controllers\\Api\\FinancialController", - "path": "app/Http/Controllers/Api/FinancialController.php" - }, - { - "name": "FlagController", - "class": "App\\Http\\Controllers\\Api\\FlagController", - "path": "app/Http/Controllers/Api/FlagController.php" - }, - { - "name": "FrontendController", - "class": "App\\Http\\Controllers\\Api\\FrontendController", - "path": "app/Http/Controllers/Api/FrontendController.php" - }, - { - "name": "GradingController", - "class": "App\\Http\\Controllers\\Api\\GradingController", - "path": "app/Http/Controllers/Api/GradingController.php" - }, - { - "name": "HealthController", - "class": "App\\Http\\Controllers\\Api\\HealthController", - "path": "app/Http/Controllers/Api/HealthController.php" - }, - { - "name": "HomeworkController", - "class": "App\\Http\\Controllers\\Api\\HomeworkController", - "path": "app/Http/Controllers/Api/HomeworkController.php" - }, - { - "name": "HomeworkTrackingController", - "class": "App\\Http\\Controllers\\Api\\HomeworkTrackingController", - "path": "app/Http/Controllers/Api/HomeworkTrackingController.php" - }, - { - "name": "InventoryController", - "class": "App\\Http\\Controllers\\Api\\InventoryController", - "path": "app/Http/Controllers/Api/InventoryController.php" - }, - { - "name": "InvoiceController", - "class": "App\\Http\\Controllers\\Api\\InvoiceController", - "path": "app/Http/Controllers/Api/InvoiceController.php" - }, - { - "name": "IpBanController", - "class": "App\\Http\\Controllers\\Api\\IpBanController", - "path": "app/Http/Controllers/Api/IpBanController.php" - }, - { - "name": "LandingPageController", - "class": "App\\Http\\Controllers\\Api\\LandingPageController", - "path": "app/Http/Controllers/Api/LandingPageController.php" - }, - { - "name": "LateSlipLogsController", - "class": "App\\Http\\Controllers\\Api\\LateSlipLogsController", - "path": "app/Http/Controllers/Api/LateSlipLogsController.php" - }, - { - "name": "MessageController", - "class": "App\\Http\\Controllers\\Api\\MessageController", - "path": "app/Http/Controllers/Api/MessageController.php" - }, - { - "name": "MessagesController", - "class": "App\\Http\\Controllers\\Api\\MessagesController", - "path": "app/Http/Controllers/Api/MessagesController.php" - }, - { - "name": "MessagingController", - "class": "App\\Http\\Controllers\\Api\\MessagingController", - "path": "app/Http/Controllers/Api/MessagingController.php" - }, - { - "name": "NavBuilderController", - "class": "App\\Http\\Controllers\\Api\\NavBuilderController", - "path": "app/Http/Controllers/Api/NavBuilderController.php" - }, - { - "name": "NotificationController", - "class": "App\\Http\\Controllers\\Api\\NotificationController", - "path": "app/Http/Controllers/Api/NotificationController.php" - }, - { - "name": "NotificationsController", - "class": "App\\Http\\Controllers\\Api\\NotificationsController", - "path": "app/Http/Controllers/Api/NotificationsController.php" - }, - { - "name": "PageController", - "class": "App\\Http\\Controllers\\Api\\PageController", - "path": "app/Http/Controllers/Api/PageController.php" - }, - { - "name": "ParentAttendanceReportController", - "class": "App\\Http\\Controllers\\Api\\ParentAttendanceReportController", - "path": "app/Http/Controllers/Api/ParentAttendanceReportController.php" - }, - { - "name": "ParentController", - "class": "App\\Http\\Controllers\\Api\\ParentController", - "path": "app/Http/Controllers/Api/ParentController.php" - }, - { - "name": "ParticipationController", - "class": "App\\Http\\Controllers\\Api\\ParticipationController", - "path": "app/Http/Controllers/Api/ParticipationController.php" - }, - { - "name": "PaymentController", - "class": "App\\Http\\Controllers\\Api\\PaymentController", - "path": "app/Http/Controllers/Api/PaymentController.php" - }, - { - "name": "PaymentNotificationController", - "class": "App\\Http\\Controllers\\Api\\PaymentNotificationController", - "path": "app/Http/Controllers/Api/PaymentNotificationController.php" - }, - { - "name": "PaymentTransactionController", - "class": "App\\Http\\Controllers\\Api\\PaymentTransactionController", - "path": "app/Http/Controllers/Api/PaymentTransactionController.php" - }, - { - "name": "PaypalTransactionsController", - "class": "App\\Http\\Controllers\\Api\\PaypalTransactionsController", - "path": "app/Http/Controllers/Api/PaypalTransactionsController.php" - }, - { - "name": "PaypalWebhook", - "class": "App\\Http\\Controllers\\Api\\PaypalWebhook", - "path": "app/Http/Controllers/Api/PaypalWebhook.php" - }, - { - "name": "PolicyController", - "class": "App\\Http\\Controllers\\Api\\PolicyController", - "path": "app/Http/Controllers/Api/PolicyController.php" - }, - { - "name": "PreferencesController", - "class": "App\\Http\\Controllers\\Api\\PreferencesController", - "path": "app/Http/Controllers/Api/PreferencesController.php" - }, - { - "name": "PrintablesReportsController", - "class": "App\\Http\\Controllers\\Api\\PrintablesReportsController", - "path": "app/Http/Controllers/Api/PrintablesReportsController.php" - }, - { - "name": "ProjectController", - "class": "App\\Http\\Controllers\\Api\\ProjectController", - "path": "app/Http/Controllers/Api/ProjectController.php" - }, - { - "name": "PurchaseOrderController", - "class": "App\\Http\\Controllers\\Api\\PurchaseOrderController", - "path": "app/Http/Controllers/Api/PurchaseOrderController.php" - }, - { - "name": "QuizController", - "class": "App\\Http\\Controllers\\Api\\QuizController", - "path": "app/Http/Controllers/Api/QuizController.php" - }, - { - "name": "BadgeScanController", - "class": "App\\Http\\Controllers\\Api\\BadgeScan\\BadgeScanController", - "path": "app/Http/Controllers/Api/BadgeScan/BadgeScanController.php" - }, - { - "name": "RefundController", - "class": "App\\Http\\Controllers\\Api\\RefundController", - "path": "app/Http/Controllers/Api/RefundController.php" - }, - { - "name": "RegisterController", - "class": "App\\Http\\Controllers\\Api\\RegisterController", - "path": "app/Http/Controllers/Api/RegisterController.php" - }, - { - "name": "ReimbursementController", - "class": "App\\Http\\Controllers\\Api\\ReimbursementController", - "path": "app/Http/Controllers/Api/ReimbursementController.php" - }, - { - "name": "RolePermissionController", - "class": "App\\Http\\Controllers\\Api\\Auth\\RolePermissionController", - "path": "app/Http/Controllers/Api/Auth/RolePermissionController.php" - }, - { - "name": "RoleSwitcherController", - "class": "App\\Http\\Controllers\\Api\\Auth\\RoleSwitcherController", - "path": "app/Http/Controllers/Api/Auth/RoleSwitcherController.php" - }, - { - "name": "SchoolCalendarController", - "class": "App\\Http\\Controllers\\Api\\SchoolCalendarController", - "path": "app/Http/Controllers/Api/SchoolCalendarController.php" - }, - { - "name": "ScoreCommentController", - "class": "App\\Http\\Controllers\\Api\\ScoreCommentController", - "path": "app/Http/Controllers/Api/ScoreCommentController.php" - }, - { - "name": "ScoreController", - "class": "App\\Http\\Controllers\\Api\\ScoreController", - "path": "app/Http/Controllers/Api/ScoreController.php" - }, - { - "name": "ScorePredictorController", - "class": "App\\Http\\Controllers\\Api\\ScorePredictorController", - "path": "app/Http/Controllers/Api/ScorePredictorController.php" - }, - { - "name": "SendSMSController", - "class": "App\\Http\\Controllers\\Api\\SendSMSController", - "path": "app/Http/Controllers/Api/SendSMSController.php" - }, - { - "name": "SessionController", - "class": "App\\Http\\Controllers\\Api\\SessionController", - "path": "app/Http/Controllers/Api/SessionController.php" - }, - { - "name": "SessionTimeoutController", - "class": "App\\Http\\Controllers\\Api\\SessionTimeoutController", - "path": "app/Http/Controllers/Api/SessionTimeoutController.php" - }, - { - "name": "SettingsController", - "class": "App\\Http\\Controllers\\Api\\SettingsController", - "path": "app/Http/Controllers/Api/SettingsController.php" - }, - { - "name": "SlipPrinterController", - "class": "App\\Http\\Controllers\\Api\\SlipPrinterController", - "path": "app/Http/Controllers/Api/SlipPrinterController.php" - }, - { - "name": "StaffController", - "class": "App\\Http\\Controllers\\Api\\StaffController", - "path": "app/Http/Controllers/Api/StaffController.php" - }, - { - "name": "StatsController", - "class": "App\\Http\\Controllers\\Api\\System\\StatsController", - "path": "app/Http/Controllers/Api/System/StatsController.php" - }, - { - "name": "StudentController", - "class": "App\\Http\\Controllers\\Api\\StudentController", - "path": "app/Http/Controllers/Api/StudentController.php" - }, - { - "name": "SupplierController", - "class": "App\\Http\\Controllers\\Api\\SupplierController", - "path": "app/Http/Controllers/Api/SupplierController.php" - }, - { - "name": "SupplyCategoryController", - "class": "App\\Http\\Controllers\\Api\\SupplyCategoryController", - "path": "app/Http/Controllers/Api/SupplyCategoryController.php" - }, - { - "name": "SupportController", - "class": "App\\Http\\Controllers\\Api\\SupportController", - "path": "app/Http/Controllers/Api/SupportController.php" - }, - { - "name": "TeacherController", - "class": "App\\Http\\Controllers\\Api\\TeacherController", - "path": "app/Http/Controllers/Api/TeacherController.php" - }, - { - "name": "TestDBController", - "class": "App\\Http\\Controllers\\Api\\TestDBController", - "path": "app/Http/Controllers/Api/TestDBController.php" - }, - { - "name": "TimeController", - "class": "App\\Http\\Controllers\\Api\\TimeController", - "path": "app/Http/Controllers/Api/TimeController.php" - }, - { - "name": "UiController", - "class": "App\\Http\\Controllers\\Api\\UiController", - "path": "app/Http/Controllers/Api/UiController.php" - }, - { - "name": "UserController", - "class": "App\\Http\\Controllers\\Api\\Users\\UserController", - "path": "app/Http/Controllers/Api/Users/UserController.php" - }, - { - "name": "WhatsappController", - "class": "App\\Http\\Controllers\\Api\\WhatsappController", - "path": "app/Http/Controllers/Api/WhatsappController.php" - }, - { - "name": "AuthController", - "class": "App\\Http\\Controllers\\Api\\Auth\\AuthController", - "path": "app/Http/Controllers/Api/Auth/AuthController.php" + } + }, + "/api/v1/auth/logout": { + "post": { + "operationId": "logout", + "tags": [ + "Auth" + ], + "summary": "Revoke the active token", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "204": { + "description": "Token revoked" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + } } - ], - "paths": { - "/api/v1/auth/register": { - "post": { - "operationId": "register", - "tags": ["Auth"], - "summary": "Create a parent account and trigger email verification", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RegisterRequest" - } - } - } - }, - "responses": { - "201": { - "description": "Parent account created", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RegisterResponse" - } - } - } - }, - "422": { - "description": "Validation error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - } - } - } - }, - "/api/v1/auth/forgot-password": { - "post": { - "operationId": "forgotPassword", - "tags": ["Auth"], - "summary": "Request a password reset link", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ForgotPasswordRequest" - } - } - } - }, - "responses": { - "200": { - "description": "Email sent (or queued)", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MessageResponse" - } - } - } - }, - "422": { - "description": "Validation error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - } - } - } - }, - "/api/v1/auth/reset-password": { - "post": { - "operationId": "resetPassword", - "tags": ["Auth"], - "summary": "Submit a new password using the reset token received via email", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PasswordResetRequest" - } - } - } - }, - "responses": { - "200": { - "description": "Password changed successfully", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MessageResponse" - } - } - } - }, - "422": { - "description": "Validation or token error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - } - } - } - }, - "/api/v1/auth/set-password": { - "post": { - "operationId": "setPassword", - "tags": ["Auth"], - "summary": "Complete account setup by choosing a new password after email confirmation", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SetPasswordRequest" - } - } - } - }, - "responses": { - "200": { - "description": "Password saved", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MessageResponse" - } - } - } - }, - "422": { - "description": "Validation error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - } - } - } - }, - "/api/v1/auth/login": { - "post": { - "operationId": "login", - "tags": ["Auth"], - "summary": "Issue a JWT for a valid user", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/LoginRequest" - } - } - } - }, - "responses": { - "200": { - "description": "Authentication successful", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/LoginResponse" - } - } - } - }, - "401": { - "description": "Invalid credentials", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - } - }, - "security": [] - } - }, - "/api/v1/auth/me": { - "get": { - "operationId": "getCurrentUser", - "tags": ["Auth"], - "summary": "Return the authenticated user's profile", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "200": { - "description": "Profile data" - }, - "401": { - "$ref": "#/components/responses/UnauthorizedError" - } - } - } - }, - "/api/v1/auth/logout": { - "post": { - "operationId": "logout", - "tags": ["Auth"], - "summary": "Revoke the active token", - "security": [ - { - "bearerAuth": [] - } - ], - "responses": { - "204": { - "description": "Token revoked" - }, - "401": { - "$ref": "#/components/responses/UnauthorizedError" - } - } - } - }, - "/api/v1/users/login-activity": { - "get": { - "operationId": "getLoginActivity", - "tags": ["Users"], - "summary": "Retrieve parent login activities for the management portal", - "parameters": [ - { - "name": "page", - "in": "query", - "schema": { - "type": "integer", - "default": 1 - } - }, - { - "name": "per_page", - "in": "query", - "schema": { - "type": "integer", - "default": 25 - } - } - ], - "responses": { - "200": { - "description": "Paginated login activity recorded for parents", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/LoginActivityResponse" - } - } - } - }, - "401": { - "$ref": "#/components/responses/UnauthorizedError" - } - } - } - }, - "/api/v1/users": { - "get": { - "operationId": "listUsers", - "tags": ["Users"], - "summary": "List users", - "security": [ - { - "bearerAuth": [] - } - ], - "parameters": [ - { - "name": "page", - "in": "query", - "schema": { - "type": "integer", - "default": 1 - } - }, - { - "name": "per_page", - "in": "query", - "schema": { - "type": "integer", - "default": 20 - } - }, - { - "name": "sort", - "in": "query", - "schema": { - "type": "string", - "enum": ["id", "firstname", "lastname", "email", "role"] - } - }, - { - "name": "order", - "in": "query", - "schema": { - "type": "string", - "enum": ["asc", "desc"] - } - } - ], - "responses": { - "200": { - "description": "Paginated user list" - }, - "401": { - "$ref": "#/components/responses/UnauthorizedError" - } - } - }, - "post": { - "operationId": "createUser", - "tags": ["Users"], - "summary": "Create a new user", - "security": [{"bearerAuth": []}], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": ["firstname", "lastname", "email", "password", "cellphone", "address_street", "city", "state", "zip", "accept_school_policy", "role_id"], - "properties": { - "firstname": {"type": "string"}, - "lastname": {"type": "string"}, - "email": {"type": "string", "format": "email"}, - "password": {"type": "string"}, - "cellphone": {"type": "string"}, - "address_street": {"type": "string"}, - "city": {"type": "string"}, - "state": {"type": "string"}, - "zip": {"type": "string"}, - "accept_school_policy": {"type": "boolean"}, - "role_id": {"type": "integer"} - } - } - } - } - }, - "responses": { - "201": {"description": "User created"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "422": {"description": "Validation error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorResponse"}}}} - } - } - }, - "/api/v1/students": { - "get": { - "operationId": "listStudents", - "tags": ["Students"], - "summary": "List students for the current school year", - "security": [ - { - "bearerAuth": [] - } - ], - "parameters": [ - { - "name": "page", - "in": "query", - "schema": { - "type": "integer", - "default": 1 - } - }, - { - "name": "per_page", - "in": "query", - "schema": { - "type": "integer", - "default": 20 - } - }, - { - "name": "parent_id", - "in": "query", - "schema": { - "type": "integer" - } - }, - { - "name": "parent_ids[]", - "in": "query", - "schema": { - "type": "array", - "items": { - "type": "integer" - } - } - }, - { - "name": "class_id", - "in": "query", - "schema": { - "type": "integer" - } - } - ], - "responses": { - "200": { - "description": "Paginated students", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PaginatedResponse" - } - } - } - }, - "401": { - "$ref": "#/components/responses/UnauthorizedError" - } - } - }, - "post": { - "operationId": "createStudent", - "tags": ["Students"], - "summary": "Register a new student", - "security": [ - { - "bearerAuth": [] - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Student" - } - } - } - }, - "responses": { - "201": { - "description": "Student created" - }, - "401": { - "$ref": "#/components/responses/UnauthorizedError" - }, - "422": { - "description": "Validation error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - } - } - } - }, - "/api/v1/students/{id}": { - "get": { - "operationId": "getStudent", - "tags": ["Students"], - "summary": "Retrieve a single student", - "security": [ - { - "bearerAuth": [] - } - ], - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "integer" - } - } - ], - "responses": { - "200": { - "description": "Student details", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Student" - } - } - } - }, - "401": { - "$ref": "#/components/responses/UnauthorizedError" - }, - "404": { - "description": "Not found" - } - } - }, - "patch": { - "operationId": "updateStudent", - "tags": ["Students"], - "summary": "Update a student", - "security": [{"bearerAuth": []}], - "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "integer"}}], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "school_id": {"type": "string"}, - "school_year": {"type": "string"}, - "firstname": {"type": "string"}, - "lastname": {"type": "string"}, - "dob": {"type": "string", "format": "date"}, - "age": {"type": "integer"}, - "gender": {"type": "string"}, - "is_active": {"type": "boolean"}, - "registration_grade": {"type": "string"}, - "is_new": {"type": "boolean"}, - "photo_consent": {"type": "boolean"}, - "parent_id": {"type": "integer"}, - "registration_date": {"type": "string", "format": "date"}, - "tuition_paid": {"type": "boolean"}, - "semester": {"type": "string"}, - "year_of_registration": {"type": "integer"}, - "name": {"type": "string"}, - "cellphone": {"type": "string"}, - "email": {"type": "string"}, - "relation": {"type": "string"} - } - } - } - } - }, - "responses": { - "200": {"description": "Student updated"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "422": {"description": "Validation error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorResponse"}}}} - } - }, - "delete": { - "operationId": "deleteStudent", - "tags": ["Students"], - "summary": "Delete a student", - "security": [{"bearerAuth": []}], - "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "integer"}}], - "responses": { - "200": {"description": "Student deleted"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "404": {"description": "Not found"} - } - } - }, - "/api/v1/attendance": { - "get": { - "operationId": "listAttendance", - "tags": ["Attendance"], - "summary": "List attendance records", - "security": [ - { - "bearerAuth": [] - } - ], - "parameters": [ - { - "name": "page", - "in": "query", - "schema": { - "type": "integer", - "default": 1 - } - }, - { - "name": "per_page", - "in": "query", - "schema": { - "type": "integer", - "default": 20 - } - }, - { - "name": "student_id", - "in": "query", - "schema": { - "type": "integer" - } - }, - { - "name": "date", - "in": "query", - "schema": { - "type": "string", - "format": "date" - } - } - ], - "responses": { - "200": { - "description": "Paginated attendance records", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PaginatedResponse" - } - } - } - }, - "401": { - "$ref": "#/components/responses/UnauthorizedError" - } - } - }, - "post": { - "operationId": "createAttendance", - "tags": ["Attendance"], - "summary": "Create an attendance record", - "security": [ - { - "bearerAuth": [] - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AttendanceRecord" - } - } - } - }, - "responses": { - "201": { - "description": "Record created" - }, - "401": { - "$ref": "#/components/responses/UnauthorizedError" - }, - "409": { - "description": "Duplicate for student/date" - } - } - } - }, - "/api/v1/invoices": { - "get": { - "operationId": "listInvoices", - "tags": ["Invoices"], - "summary": "List invoices", - "security": [ - { - "bearerAuth": [] - } - ], - "parameters": [ - { - "name": "page", - "in": "query", - "schema": { - "type": "integer", - "default": 1 - } - }, - { - "name": "per_page", - "in": "query", - "schema": { - "type": "integer", - "default": 20 - } - }, - { - "name": "parent_id", - "in": "query", - "schema": { - "type": "integer" - } - }, - { - "name": "status", - "in": "query", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Paginated invoices", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PaginatedResponse" - } - } - } - }, - "401": { - "$ref": "#/components/responses/UnauthorizedError" - } - } - }, - "post": { - "operationId": "createInvoice", - "tags": ["Invoices"], - "summary": "Create an invoice", - "security": [ - { - "bearerAuth": [] - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Invoice" - } - } - } - }, - "responses": { - "201": { - "description": "Invoice created" - }, - "401": { - "$ref": "#/components/responses/UnauthorizedError" - } - } - } - }, - "/api/v1/invoices/{id}": { - "get": { - "operationId": "getInvoice", - "tags": ["Invoices"], - "summary": "Show a single invoice", - "security": [ - { - "bearerAuth": [] - } - ], - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "integer" - } - } - ], - "responses": { - "200": { - "description": "Invoice details", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Invoice" - } - } - } - }, - "401": { - "$ref": "#/components/responses/UnauthorizedError" - }, - "404": { - "description": "Not found" - } - } - }, - "patch": { - "operationId": "updateInvoice", - "tags": ["Invoices"], - "summary": "Update invoice attributes", - "security": [ - { - "bearerAuth": [] - } - ], - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "integer" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Invoice" - } - } - } - }, - "responses": { - "200": { - "description": "Invoice updated" - }, - "401": { - "$ref": "#/components/responses/UnauthorizedError" - } - } - }, - "delete": { - "operationId": "deleteInvoice", - "tags": ["Invoices"], - "summary": "Delete an invoice", - "security": [ - { - "bearerAuth": [] - } - ], - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "integer" - } - } - ], - "responses": { - "200": { - "description": "Invoice deleted" - }, - "401": { - "$ref": "#/components/responses/UnauthorizedError" - }, - "404": { - "description": "Not found" - } - } - } - }, - "/register": { - "post": { - "operationId": "webRegister", - "tags": ["Web"], - "summary": "Submit the legacy parent registration form", - "requestBody": { - "required": true, - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/WebRegisterForm" - } - } - } - }, - "responses": { - "302": { - "description": "Redirects to /register/success when registration completes." - }, - "422": { - "description": "Validation issues cause a redirect back to the form with flash errors." - } - } - } - }, - "/user/login": { - "post": { - "operationId": "webLogin", - "tags": ["Web"], - "summary": "Submit the legacy login form", - "requestBody": { - "required": true, - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/WebLoginForm" - } - } - } - }, - "responses": { - "302": { - "description": "Redirects to the dashboard when authentication succeeds." - }, - "401": { - "description": "Invalid credentials or suspended account results in a redirect back with flash information." - } - } - } - }, - "/processForgotPassword": { - "post": { - "operationId": "webForgotPassword", - "tags": ["Web"], - "summary": "Request a password reset link via the legacy form", - "requestBody": { - "required": true, - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/WebForgotPasswordForm" - } - } - } - }, - "responses": { - "302": { - "description": "Always redirects back to the form; success is indicated via flash data." - } - } - } - }, - "/user/processResetPassword": { - "post": { - "operationId": "webResetPassword", - "tags": ["Web"], - "summary": "Submit a new password using the reset token from the email", - "requestBody": { - "required": true, - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/WebResetPasswordForm" - } - } - } - }, - "responses": { - "200": { - "description": "Renders the 'password_set_success' view when the token is valid." - }, - "302": { - "description": "Invalid token or validation errors redirect to the home page or form." - } - } - } - }, - "/user/save_password": { - "post": { - "operationId": "webSetPassword", - "tags": ["Web"], - "summary": "Save the password after confirming the email address", - "requestBody": { - "required": true, - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/WebSetPasswordForm" - } - } - } - }, - "responses": { - "302": { - "description": "Redirects to /user/password_set_success on success or back on failure." - } - } - } - }, - "/api/v1/administrator/dashboard": { - "get": { - "operationId": "getAdministratorDashboard", - "tags": ["Administrator"], - "summary": "Returns counts for students, staff roles, and parents plus the four most recent login activities", - "security": [{"bearerAuth": []}], - "responses": { - "200": {"description": "Dashboard data"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"} - } - } - }, - "/api/v1/administrator/absence": { - "get": { - "operationId": "getAdministratorAbsence", - "tags": ["Administrator"], - "summary": "Get absence form data", - "security": [{"bearerAuth": []}], - "responses": { - "200": {"description": "Absence form data"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"} - } - }, - "post": { - "operationId": "submitAdministratorAbsence", - "tags": ["Administrator"], - "summary": "Submit absence request", - "security": [{"bearerAuth": []}], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": ["dates", "reason"], - "properties": { - "dates": {"type": "array", "items": {"type": "string", "format": "date"}}, - "reason_type": {"type": "string"}, - "reason": {"type": "string"} - } - } - } - } - }, - "responses": { - "200": {"description": "Absence submitted"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "422": {"description": "Validation error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorResponse"}}}} - } - } - }, - "/api/v1/administrator/search": { - "get": { - "operationId": "searchAdministrator", - "tags": ["Administrator"], - "summary": "Search across users, students, parents, staff, and emergency contacts", - "security": [{"bearerAuth": []}], - "parameters": [{ - "name": "q", - "in": "query", - "required": true, - "schema": {"type": "string"} - }], - "responses": { - "200": {"description": "Search results"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"} - } - } - }, - "/api/v1/assignments": { - "get": { - "operationId": "listAssignments", - "tags": ["Assignments"], - "summary": "List assignments", - "security": [{"bearerAuth": []}], - "parameters": [ - {"name": "class_id", "in": "query", "schema": {"type": "integer"}}, - {"name": "teacher_id", "in": "query", "schema": {"type": "integer"}}, - {"name": "student_id", "in": "query", "schema": {"type": "integer"}}, - {"name": "page", "in": "query", "schema": {"type": "integer", "default": 1}}, - {"name": "per_page", "in": "query", "schema": {"type": "integer", "default": 20}} - ], - "responses": { - "200": {"description": "Paginated assignments"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"} - } - }, - "post": { - "operationId": "createAssignment", - "tags": ["Assignments"], - "summary": "Create an assignment", - "security": [{"bearerAuth": []}], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": ["title", "due_date", "class_id", "teacher_id"], - "properties": { - "title": {"type": "string"}, - "due_date": {"type": "string", "format": "date"}, - "class_id": {"type": "integer"}, - "teacher_id": {"type": "integer"} - } - } - } - } - }, - "responses": { - "201": {"description": "Assignment created"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "422": {"description": "Validation error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorResponse"}}}} - } - } - }, - "/api/v1/assignments/{id}": { - "get": { - "operationId": "getAssignment", - "tags": ["Assignments"], - "summary": "Get a single assignment", - "security": [{"bearerAuth": []}], - "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "integer"}}], - "responses": { - "200": {"description": "Assignment details"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "404": {"description": "Not found"} - } - }, - "patch": { - "operationId": "updateAssignment", - "tags": ["Assignments"], - "summary": "Update an assignment", - "security": [{"bearerAuth": []}], - "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "integer"}}], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "title": {"type": "string"}, - "due_date": {"type": "string", "format": "date"}, - "class_id": {"type": "integer"}, - "teacher_id": {"type": "integer"} - } - } - } - } - }, - "responses": { - "200": {"description": "Assignment updated"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"} - } - }, - "delete": { - "operationId": "deleteAssignment", - "tags": ["Assignments"], - "summary": "Delete an assignment", - "security": [{"bearerAuth": []}], - "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "integer"}}], - "responses": { - "200": {"description": "Assignment deleted"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "404": {"description": "Not found"} - } - } - }, - "/api/v1/attendance/{id}": { - "get": { - "operationId": "getAttendance", - "tags": ["Attendance"], - "summary": "Get a single attendance record", - "security": [{"bearerAuth": []}], - "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "integer"}}], - "responses": { - "200": {"description": "Attendance record"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "404": {"description": "Not found"} - } - }, - "patch": { - "operationId": "updateAttendance", - "tags": ["Attendance"], - "summary": "Update an attendance record", - "security": [{"bearerAuth": []}], - "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "integer"}}], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "status": {"type": "string", "enum": ["present", "absent", "late"]}, - "notes": {"type": "string"} - } - } - } - } - }, - "responses": { - "200": {"description": "Attendance record updated"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"} - } - }, - "delete": { - "operationId": "deleteAttendance", - "tags": ["Attendance"], - "summary": "Delete an attendance record", - "security": [{"bearerAuth": []}], - "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "integer"}}], - "responses": { - "200": {"description": "Attendance record deleted"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "404": {"description": "Not found"} - } - } - }, - "/api/v1/attendance/class/{class_section_id}": { - "get": { - "operationId": "getClassAttendance", - "tags": ["Attendance"], - "summary": "Get class roster with attendance status", - "security": [{"bearerAuth": []}], - "parameters": [ - {"name": "class_section_id", "in": "path", "required": true, "schema": {"type": "integer"}}, - {"name": "date", "in": "query", "schema": {"type": "string", "format": "date"}} - ], - "responses": { - "200": {"description": "Class roster with attendance"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"} - } - } - }, - "/api/v1/attendance/class/{class_section_id}/record": { - "post": { - "operationId": "recordClassAttendance", - "tags": ["Attendance"], - "summary": "Record attendance for multiple students in a class", - "security": [{"bearerAuth": []}], - "parameters": [{"name": "class_section_id", "in": "path", "required": true, "schema": {"type": "integer"}}], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": ["date", "records"], - "properties": { - "date": {"type": "string", "format": "date"}, - "records": { - "type": "array", - "items": { - "type": "object", - "required": ["student_id", "status"], - "properties": { - "student_id": {"type": "integer"}, - "status": {"type": "string", "enum": ["present", "absent", "late"]}, - "reason": {"type": "string"} - } - } - } - } - } - } - } - }, - "responses": { - "200": {"description": "Attendance recorded"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "422": {"description": "Validation error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorResponse"}}}} - } - } - }, - "/api/v1/classes": { - "get": { - "operationId": "listClasses", - "tags": ["Classes"], - "summary": "List classes", - "security": [{"bearerAuth": []}], - "parameters": [ - {"name": "grade", "in": "query", "schema": {"type": "string"}}, - {"name": "teacher_id", "in": "query", "schema": {"type": "integer"}}, - {"name": "page", "in": "query", "schema": {"type": "integer", "default": 1}}, - {"name": "per_page", "in": "query", "schema": {"type": "integer", "default": 20}} - ], - "responses": { - "200": {"description": "Paginated classes"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"} - } - } - }, - "/api/v1/classes/{id}": { - "get": { - "operationId": "getClass", - "tags": ["Classes"], - "summary": "Get a single class", - "security": [{"bearerAuth": []}], - "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "integer"}}], - "responses": { - "200": {"description": "Class details"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "404": {"description": "Not found"} - } - } - }, - "/api/v1/classes/{id}/students": { - "get": { - "operationId": "getClassStudents", - "tags": ["Classes"], - "summary": "Get students in a class", - "security": [{"bearerAuth": []}], - "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "integer"}}], - "responses": { - "200": {"description": "List of students"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"} - } - } - }, - "/api/v1/calendar": { - "get": { - "operationId": "listCalendarEvents", - "tags": ["Calendar"], - "summary": "List calendar events", - "security": [{"bearerAuth": []}], - "parameters": [ - {"name": "page", "in": "query", "schema": {"type": "integer", "default": 1}}, - {"name": "per_page", "in": "query", "schema": {"type": "integer", "default": 20}}, - {"name": "month", "in": "query", "schema": {"type": "integer"}}, - {"name": "year", "in": "query", "schema": {"type": "integer"}} - ], - "responses": { - "200": {"description": "Paginated calendar events"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"} - } - }, - "post": { - "operationId": "createCalendarEvent", - "tags": ["Calendar"], - "summary": "Create a calendar event", - "security": [{"bearerAuth": []}], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": ["title", "start_date"], - "properties": { - "title": {"type": "string"}, - "start_date": {"type": "string", "format": "date"}, - "description": {"type": "string"}, - "end_date": {"type": "string", "format": "date"}, - "location": {"type": "string"} - } - } - } - } - }, - "responses": { - "201": {"description": "Event created"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "422": {"description": "Validation error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorResponse"}}}} - } - } - }, - "/api/v1/calendar/{id}": { - "get": { - "operationId": "getCalendarEvent", - "tags": ["Calendar"], - "summary": "Get a single calendar event", - "security": [{"bearerAuth": []}], - "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "integer"}}], - "responses": { - "200": {"description": "Event details"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "404": {"description": "Not found"} - } - }, - "patch": { - "operationId": "updateCalendarEvent", - "tags": ["Calendar"], - "summary": "Update a calendar event", - "security": [{"bearerAuth": []}], - "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "integer"}}], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "title": {"type": "string"}, - "start_date": {"type": "string", "format": "date"}, - "description": {"type": "string"}, - "end_date": {"type": "string", "format": "date"}, - "location": {"type": "string"} - } - } - } - } - }, - "responses": { - "200": {"description": "Event updated"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"} - } - }, - "delete": { - "operationId": "deleteCalendarEvent", - "tags": ["Calendar"], - "summary": "Delete a calendar event", - "security": [{"bearerAuth": []}], - "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "integer"}}], - "responses": { - "200": {"description": "Event deleted"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "404": {"description": "Not found"} - } - } - }, - "/api/v1/contact": { - "post": { - "operationId": "submitContact", - "tags": ["Contact"], - "summary": "Submit contact form", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": ["name", "email", "subject", "message"], - "properties": { - "name": {"type": "string"}, - "email": {"type": "string", "format": "email"}, - "subject": {"type": "string"}, - "message": {"type": "string"} - } - } - } - } - }, - "responses": { - "200": {"description": "Contact form submitted"}, - "422": {"description": "Validation error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorResponse"}}}}, - "500": {"description": "Server error"} - }, - "security": [] - } - }, - "/api/v1/payments": { - "get": { - "operationId": "listPayments", - "tags": ["Payments"], - "summary": "List payments with optional filters", - "security": [{"bearerAuth": []}], - "parameters": [ - {"name": "parent_id", "in": "query", "schema": {"type": "integer"}}, - {"name": "status", "in": "query", "schema": {"type": "string"}}, - {"name": "page", "in": "query", "schema": {"type": "integer", "default": 1}}, - {"name": "per_page", "in": "query", "schema": {"type": "integer", "default": 20}} - ], - "responses": { - "200": {"description": "Paginated payments"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"} - } - }, - "post": { - "operationId": "createPayment", - "tags": ["Payments"], - "summary": "Create a new payment plan", - "security": [{"bearerAuth": []}], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": ["parent_id", "total_amount", "payment_date"], - "properties": { - "parent_id": {"type": "integer"}, - "total_amount": {"type": "number"}, - "payment_date": {"type": "string", "format": "date"}, - "number_of_installments": {"type": "integer"}, - "payment_type": {"type": "string"}, - "semester": {"type": "string"}, - "school_year": {"type": "string"} - } - } - } - } - }, - "responses": { - "201": {"description": "Payment created"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "422": {"description": "Validation error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorResponse"}}}} - } - } - }, - "/api/v1/payments/{id}": { - "get": { - "operationId": "getPayment", - "tags": ["Payments"], - "summary": "Get a single payment", - "security": [{"bearerAuth": []}], - "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "integer"}}], - "responses": { - "200": {"description": "Payment details"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "404": {"description": "Not found"} - } - }, - "patch": { - "operationId": "updatePayment", - "tags": ["Payments"], - "summary": "Update payment fields", - "security": [{"bearerAuth": []}], - "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "integer"}}], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "paid_amount": {"type": "number"}, - "balance": {"type": "number"}, - "status": {"type": "string"}, - "payment_method": {"type": "string"}, - "payment_date": {"type": "string", "format": "date"}, - "check_number": {"type": "string"} - } - } - } - } - }, - "responses": { - "200": {"description": "Payment updated"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"} - } - }, - "delete": { - "operationId": "deletePayment", - "tags": ["Payments"], - "summary": "Delete a payment", - "security": [{"bearerAuth": []}], - "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "integer"}}], - "responses": { - "200": {"description": "Payment deleted"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "404": {"description": "Not found"} - } - } - }, - "/api/v1/payments/parent/{id}": { - "get": { - "operationId": "getPaymentsByParent", - "tags": ["Payments"], - "summary": "Get all payments for a specific parent", - "security": [{"bearerAuth": []}], - "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "integer"}}], - "responses": { - "200": {"description": "Parent payments"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"} - } - } - }, - "/api/v1/payments/manual-pay-search": { - "get": { - "operationId": "searchManualPayment", - "tags": ["Payments"], - "summary": "Search for parent by email or phone and retrieve payment data", - "security": [{"bearerAuth": []}], - "parameters": [{"name": "search_term", "in": "query", "required": true, "schema": {"type": "string"}}], - "responses": { - "200": {"description": "Payment search results"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"} - } - } - }, - "/api/v1/payments/manual-pay-update": { - "post": { - "operationId": "updateManualPayment", - "tags": ["Payments"], - "summary": "Record a manual payment", - "security": [{"bearerAuth": []}], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": ["invoice_id", "amount", "payment_method"], - "properties": { - "invoice_id": {"type": "integer"}, - "amount": {"type": "number"}, - "payment_method": {"type": "string", "enum": ["cash", "check", "card"]}, - "check_number": {"type": "string"}, - "payment_date": {"type": "string", "format": "date"} - } - } - } - } - }, - "responses": { - "200": {"description": "Payment recorded"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "422": {"description": "Validation error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorResponse"}}}} - } - } - }, - "/api/v1/users/with-roles": { - "get": { - "operationId": "listUsersWithRoles", - "tags": ["Users"], - "summary": "List users with their roles", - "security": [{"bearerAuth": []}], - "responses": { - "200": {"description": "Users with roles"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"} - } - } - }, - "/api/v1/users/{id}": { - "patch": { - "operationId": "updateUser", - "tags": ["Users"], - "summary": "Update user account/profile", - "security": [{"bearerAuth": []}], - "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "integer"}}], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "firstname": {"type": "string"}, - "lastname": {"type": "string"}, - "email": {"type": "string", "format": "email"}, - "cellphone": {"type": "string"}, - "address_street": {"type": "string"}, - "city": {"type": "string"}, - "state": {"type": "string"}, - "zip": {"type": "string"} - } - } - } - } - }, - "responses": { - "200": {"description": "User updated"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"} - } - }, - "delete": { - "operationId": "deleteUser", - "tags": ["Users"], - "summary": "Delete a user", - "security": [{"bearerAuth": []}], - "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "integer"}}], - "responses": { - "200": {"description": "User deleted"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "404": {"description": "Not found"} - } - } - }, - "/api/v1/users/login-activity": { - "get": { - "operationId": "getUserLoginActivity", - "tags": ["Users"], - "summary": "Get paginated login history", - "security": [{"bearerAuth": []}], - "parameters": [ - {"name": "page", "in": "query", "schema": {"type": "integer", "default": 1}}, - {"name": "per_page", "in": "query", "schema": {"type": "integer", "default": 25}} - ], - "responses": { - "200": {"description": "Login activity"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"} - } - } - }, - "/api/v1/users/select-role": { - "post": { - "operationId": "selectUserRole", - "tags": ["Users"], - "summary": "Select and update the logged-in user's role", - "security": [{"bearerAuth": []}], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": ["role"], - "properties": { - "role": {"type": "string"} - } - } - } - } - }, - "responses": { - "200": {"description": "Role selected"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"} - } - } - }, - "/api/v1/user/profile": { - "get": { - "operationId": "getUserProfile", - "tags": ["Users"], - "summary": "Get the signed-in user's profile", - "security": [{"bearerAuth": []}], - "responses": { - "200": {"description": "User profile"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"} - } - }, - "patch": { - "operationId": "updateUserProfile", - "tags": ["Users"], - "summary": "Update user profile", - "security": [{"bearerAuth": []}], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "firstname": {"type": "string"}, - "lastname": {"type": "string"}, - "cellphone": {"type": "string"}, - "address_street": {"type": "string"}, - "apt": {"type": "string"}, - "city": {"type": "string"}, - "state": {"type": "string"}, - "zip": {"type": "string"}, - "gender": {"type": "string"}, - "email": {"type": "string", "format": "email"}, - "password": {"type": "string"}, - "current_password": {"type": "string"} - } - } - } - } - }, - "responses": { - "200": {"description": "Profile updated"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "422": {"description": "Validation error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorResponse"}}}} - } - } - }, - "/api/v1/teachers": { - "get": { - "operationId": "listTeachers", - "tags": ["Teachers"], - "summary": "List teachers and teacher assistants", - "security": [{"bearerAuth": []}], - "parameters": [ - {"name": "page", "in": "query", "schema": {"type": "integer", "default": 1}}, - {"name": "per_page", "in": "query", "schema": {"type": "integer", "default": 20}} - ], - "responses": { - "200": {"description": "Paginated teachers"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"} - } - } - }, - "/api/v1/teachers/{id}": { - "get": { - "operationId": "getTeacher", - "tags": ["Teachers"], - "summary": "Get teacher details with class assignments", - "security": [{"bearerAuth": []}], - "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "integer"}}], - "responses": { - "200": {"description": "Teacher details"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "404": {"description": "Not found"} - } - } - }, - "/api/v1/teachers/{id}/classes": { - "get": { - "operationId": "getTeacherClasses", - "tags": ["Teachers"], - "summary": "Get class sections assigned to a teacher", - "security": [{"bearerAuth": []}], - "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "integer"}}], - "responses": { - "200": {"description": "Teacher classes"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"} - } - } - }, - "/api/v1/teachers/class-view": { - "get": { - "operationId": "getTeacherClassView", - "tags": ["Teachers"], - "summary": "Get comprehensive class view data for logged-in teacher", - "security": [{"bearerAuth": []}], - "responses": { - "200": {"description": "Class view data"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"} - } - } - }, - "/api/v1/teachers/absence-form": { - "get": { - "operationId": "getTeacherAbsenceForm", - "tags": ["Teachers"], - "summary": "Get absence form data for logged-in teacher", - "security": [{"bearerAuth": []}], - "responses": { - "200": {"description": "Absence form data"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"} - } - } - }, - "/api/v1/teachers/absence": { - "post": { - "operationId": "submitTeacherAbsence", - "tags": ["Teachers"], - "summary": "Submit an absence/vacation request", - "security": [{"bearerAuth": []}], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": ["dates", "reason"], - "properties": { - "dates": {"type": "array", "items": {"type": "string", "format": "date"}}, - "reason": {"type": "string"}, - "reason_type": {"type": "string"} - } - } - } - } - }, - "responses": { - "200": {"description": "Absence submitted"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "422": {"description": "Validation error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorResponse"}}}} - } - } - }, - "/api/v1/whatsapp": { - "get": { - "operationId": "getWhatsAppData", - "tags": ["WhatsApp"], - "summary": "Get initial data for WhatsApp management page", - "security": [{"bearerAuth": []}], - "responses": { - "200": {"description": "WhatsApp management data"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"} - } - } - }, - "/api/v1/whatsapp/links": { - "get": { - "operationId": "listWhatsAppLinks", - "tags": ["WhatsApp"], - "summary": "List WhatsApp invite links", - "security": [{"bearerAuth": []}], - "responses": { - "200": {"description": "WhatsApp links"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"} - } - }, - "post": { - "operationId": "saveWhatsAppLink", - "tags": ["WhatsApp"], - "summary": "Create or update invite link for a class section", - "security": [{"bearerAuth": []}], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": ["class_section_id", "invite_link"], - "properties": { - "class_section_id": {"type": "integer"}, - "invite_link": {"type": "string"}, - "active": {"type": "integer", "enum": [0, 1]}, - "class_section_name": {"type": "string"} - } - } - } - } - }, - "responses": { - "200": {"description": "Link saved"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "422": {"description": "Validation error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorResponse"}}}} - } - } - }, - "/api/v1/whatsapp/send-invites": { - "post": { - "operationId": "sendWhatsAppInvites", - "tags": ["WhatsApp"], - "summary": "Send WhatsApp invites in three modes", - "security": [{"bearerAuth": []}], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": ["mode"], - "properties": { - "mode": {"type": "string", "enum": ["parents", "class", "all"]}, - "parent_ids": {"type": "array", "items": {"type": "integer"}}, - "class_section_id": {"type": "integer"} - } - } - } - } - }, - "responses": { - "200": {"description": "Invites sent"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "422": {"description": "Validation error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorResponse"}}}} - } - } - }, - "/api/v1/whatsapp/parent-contacts": { - "get": { - "operationId": "getWhatsAppParentContacts", - "tags": ["WhatsApp"], - "summary": "Get flat list of all parent contacts", - "security": [{"bearerAuth": []}], - "parameters": [ - {"name": "school_year", "in": "query", "schema": {"type": "string"}}, - {"name": "semester", "in": "query", "schema": {"type": "string"}} - ], - "responses": { - "200": {"description": "Parent contacts"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"} - } - } - }, - "/api/v1/whatsapp/parent-contacts-by-class": { - "get": { - "operationId": "getWhatsAppParentContactsByClass", - "tags": ["WhatsApp"], - "summary": "Get parent contacts grouped by class section", - "security": [{"bearerAuth": []}], - "parameters": [ - {"name": "school_year", "in": "query", "schema": {"type": "string"}}, - {"name": "semester", "in": "query", "schema": {"type": "string"}} - ], - "responses": { - "200": {"description": "Parent contacts by class"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"} - } - } - }, - "/api/v1/whatsapp/memberships": { - "post": { - "operationId": "updateWhatsAppMembership", - "tags": ["WhatsApp"], - "summary": "Update WhatsApp group membership flags", - "security": [{"bearerAuth": []}], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": ["class_section_id"], - "properties": { - "class_section_id": {"type": "integer"}, - "primary_id": {"type": "integer"}, - "second_id": {"type": "integer"}, - "primary_member": {"type": ["string", "integer"]}, - "second_member": {"type": ["string", "integer"]}, - "school_year": {"type": "string"}, - "semester": {"type": "string"} - } - } - } - } - }, - "responses": { - "200": {"description": "Membership updated"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "422": {"description": "Validation error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorResponse"}}}} - } - } - }, - "/api/v1/health": { - "get": { - "operationId": "healthCheck", - "tags": ["Health"], - "summary": "Health check endpoint", - "responses": { - "200": {"description": "Health status"}, - "503": {"description": "Service unavailable"} - }, - "security": [] - } - }, - "/api/v1/landing": { - "get": { - "operationId": "getLandingPage", - "tags": ["Landing"], - "summary": "Get role-based dashboard data", - "security": [{"bearerAuth": []}], - "responses": { - "200": {"description": "Landing page data"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"} - } - } - }, - "/api/v1/landing/data": { - "get": { - "operationId": "getLandingPageData", - "tags": ["Landing"], - "summary": "Get basic landing page bootstrap data", - "security": [{"bearerAuth": []}], - "responses": { - "200": {"description": "Landing page data"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"} - } - } - }, - "/api/v1/staff": { - "get": { - "operationId": "listStaff", - "tags": ["Staff"], - "summary": "List staff members", - "security": [{"bearerAuth": []}], - "parameters": [ - {"name": "page", "in": "query", "schema": {"type": "integer", "default": 1}}, - {"name": "per_page", "in": "query", "schema": {"type": "integer", "default": 20}}, - {"name": "role", "in": "query", "schema": {"type": "string"}} - ], - "responses": { - "200": {"description": "Paginated staff"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"} - } - }, - "post": { - "operationId": "createStaff", - "tags": ["Staff"], - "summary": "Create a new staff member", - "security": [{"bearerAuth": []}], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": ["firstname", "lastname", "email", "role_name"], - "properties": { - "firstname": {"type": "string"}, - "lastname": {"type": "string"}, - "email": {"type": "string", "format": "email"}, - "phone": {"type": "string"}, - "role_name": {"type": "string"}, - "school_year": {"type": "string"}, - "status": {"type": "string"} - } - } - } - } - }, - "responses": { - "201": {"description": "Staff created"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "422": {"description": "Validation error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorResponse"}}}} - } - } - }, - "/api/v1/staff/{id}": { - "get": { - "operationId": "getStaff", - "tags": ["Staff"], - "summary": "Get a single staff member", - "security": [{"bearerAuth": []}], - "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "integer"}}], - "responses": { - "200": {"description": "Staff details"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "404": {"description": "Not found"} - } - }, - "patch": { - "operationId": "updateStaff", - "tags": ["Staff"], - "summary": "Update a staff member", - "security": [{"bearerAuth": []}], - "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "integer"}}], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "firstname": {"type": "string"}, - "lastname": {"type": "string"}, - "email": {"type": "string", "format": "email"}, - "phone": {"type": "string"}, - "role_name": {"type": "string"}, - "school_year": {"type": "string"}, - "status": {"type": "string"} - } - } - } - } - }, - "responses": { - "200": {"description": "Staff updated"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"} - } - }, - "delete": { - "operationId": "deleteStaff", - "tags": ["Staff"], - "summary": "Delete a staff member", - "security": [{"bearerAuth": []}], - "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "integer"}}], - "responses": { - "200": {"description": "Staff deleted"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "404": {"description": "Not found"} - } - } - }, - "/api/v1/suppliers": { - "get": { - "operationId": "listSuppliers", - "tags": ["Suppliers"], - "summary": "List suppliers", - "security": [{"bearerAuth": []}], - "parameters": [ - {"name": "page", "in": "query", "schema": {"type": "integer", "default": 1}}, - {"name": "per_page", "in": "query", "schema": {"type": "integer", "default": 20}}, - {"name": "q", "in": "query", "schema": {"type": "string"}} - ], - "responses": { - "200": {"description": "Paginated suppliers"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"} - } - }, - "post": { - "operationId": "createSupplier", - "tags": ["Suppliers"], - "summary": "Create a new supplier", - "security": [{"bearerAuth": []}], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": ["name"], - "properties": { - "name": {"type": "string"}, - "email": {"type": "string", "format": "email"}, - "phone": {"type": "string"}, - "address": {"type": "string"}, - "notes": {"type": "string"} - } - } - } - } - }, - "responses": { - "201": {"description": "Supplier created"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "422": {"description": "Validation error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorResponse"}}}} - } - } - }, - "/api/v1/suppliers/{id}": { - "get": { - "operationId": "getSupplier", - "tags": ["Suppliers"], - "summary": "Get a single supplier", - "security": [{"bearerAuth": []}], - "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "integer"}}], - "responses": { - "200": {"description": "Supplier details"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "404": {"description": "Not found"} - } - }, - "patch": { - "operationId": "updateSupplier", - "tags": ["Suppliers"], - "summary": "Update a supplier", - "security": [{"bearerAuth": []}], - "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "integer"}}], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "name": {"type": "string"}, - "email": {"type": "string", "format": "email"}, - "phone": {"type": "string"}, - "address": {"type": "string"}, - "notes": {"type": "string"} - } - } - } - } - }, - "responses": { - "200": {"description": "Supplier updated"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"} - } - }, - "delete": { - "operationId": "deleteSupplier", - "tags": ["Suppliers"], - "summary": "Delete a supplier", - "security": [{"bearerAuth": []}], - "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "integer"}}], - "responses": { - "200": {"description": "Supplier deleted"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "404": {"description": "Not found"} - } - } - }, - "/api/v1/communications": { - "get": { - "operationId": "listCommunications", - "tags": ["Communications"], - "summary": "List communications", - "security": [{"bearerAuth": []}], - "parameters": [ - {"name": "sender_id", "in": "query", "schema": {"type": "integer"}}, - {"name": "receiver_id", "in": "query", "schema": {"type": "integer"}}, - {"name": "page", "in": "query", "schema": {"type": "integer", "default": 1}}, - {"name": "per_page", "in": "query", "schema": {"type": "integer", "default": 20}} - ], - "responses": { - "200": {"description": "Paginated communications"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"} - } - }, - "post": { - "operationId": "sendCommunication", - "tags": ["Communications"], - "summary": "Send a communication", - "security": [{"bearerAuth": []}], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": ["sender_id", "receiver_id", "subject", "message"], - "properties": { - "sender_id": {"type": "integer"}, - "receiver_id": {"type": "integer"}, - "subject": {"type": "string"}, - "message": {"type": "string"} - } - } - } - } - }, - "responses": { - "201": {"description": "Communication sent"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "422": {"description": "Validation error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorResponse"}}}} - } - } - }, - "/api/v1/communications/{id}": { - "delete": { - "operationId": "deleteCommunication", - "tags": ["Communications"], - "summary": "Delete a communication", - "security": [{"bearerAuth": []}], - "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "integer"}}], - "responses": { - "200": {"description": "Communication deleted"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "404": {"description": "Not found"} - } - } - }, - "/api/v1/configuration": { - "get": { - "operationId": "listConfiguration", - "tags": ["Configuration"], - "summary": "List configuration entries", - "security": [{"bearerAuth": []}], - "parameters": [ - {"name": "key", "in": "query", "schema": {"type": "string"}}, - {"name": "page", "in": "query", "schema": {"type": "integer", "default": 1}}, - {"name": "per_page", "in": "query", "schema": {"type": "integer", "default": 20}} - ], - "responses": { - "200": {"description": "Paginated configuration"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"} - } - }, - "post": { - "operationId": "createConfiguration", - "tags": ["Configuration"], - "summary": "Create a configuration entry", - "security": [{"bearerAuth": []}], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": ["config_key", "config_value"], - "properties": { - "config_key": {"type": "string"}, - "config_value": {"type": "string"} - } - } - } - } - }, - "responses": { - "201": {"description": "Configuration created"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "422": {"description": "Validation error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorResponse"}}}} - } - } - }, - "/api/v1/configuration/{id}": { - "get": { - "operationId": "getConfiguration", - "tags": ["Configuration"], - "summary": "Get a configuration entry", - "security": [{"bearerAuth": []}], - "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "integer"}}], - "responses": { - "200": {"description": "Configuration details"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "404": {"description": "Not found"} - } - }, - "patch": { - "operationId": "updateConfiguration", - "tags": ["Configuration"], - "summary": "Update a configuration entry", - "security": [{"bearerAuth": []}], - "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "integer"}}], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "config_key": {"type": "string"}, - "config_value": {"type": "string"} - } - } - } - } - }, - "responses": { - "200": {"description": "Configuration updated"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"} - } - }, - "delete": { - "operationId": "deleteConfiguration", - "tags": ["Configuration"], - "summary": "Delete a configuration entry", - "security": [{"bearerAuth": []}], - "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "integer"}}], - "responses": { - "200": {"description": "Configuration deleted"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "404": {"description": "Not found"} - } - } - }, - "/api/v1/configuration/key/{config_key}": { - "get": { - "operationId": "getConfigurationByKey", - "tags": ["Configuration"], - "summary": "Get configuration by key", - "security": [{"bearerAuth": []}], - "parameters": [{"name": "config_key", "in": "path", "required": true, "schema": {"type": "string"}}], - "responses": { - "200": {"description": "Configuration value"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "404": {"description": "Not found"} - } - } - }, - "/api/v1/discounts": { - "get": { - "operationId": "listDiscounts", - "tags": ["Discounts"], - "summary": "List discount vouchers", - "security": [{"bearerAuth": []}], - "parameters": [ - {"name": "active", "in": "query", "schema": {"type": "boolean"}} - ], - "responses": { - "200": {"description": "List of discounts"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"} - } - } - }, - "/api/v1/discounts/{code}": { - "get": { - "operationId": "getDiscountByCode", - "tags": ["Discounts"], - "summary": "Get discount by code", - "security": [{"bearerAuth": []}], - "parameters": [{"name": "code", "in": "path", "required": true, "schema": {"type": "string"}}], - "responses": { - "200": {"description": "Discount details"}, - "400": {"description": "Max usage reached"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "404": {"description": "Not found or inactive"} - } - } - }, - "/api/v1/discounts/apply": { - "post": { - "operationId": "applyDiscount", - "tags": ["Discounts"], - "summary": "Apply a discount code to an invoice", - "security": [{"bearerAuth": []}], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": ["code", "invoice_id"], - "properties": { - "code": {"type": "string"}, - "invoice_id": {"type": "integer"} - } - } - } - } - }, - "responses": { - "200": {"description": "Discount applied"}, - "400": {"description": "Invalid discount or already applied"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "500": {"description": "Server error"} - } - } - }, - "/api/v1/emergency-contacts": { - "get": { - "operationId": "listEmergencyContacts", - "tags": ["Emergency Contacts"], - "summary": "List emergency contacts", - "security": [{"bearerAuth": []}], - "parameters": [ - {"name": "parent_id", "in": "query", "schema": {"type": "integer"}}, - {"name": "page", "in": "query", "schema": {"type": "integer", "default": 1}}, - {"name": "per_page", "in": "query", "schema": {"type": "integer", "default": 20}} - ], - "responses": { - "200": {"description": "Paginated emergency contacts"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"} - } - }, - "post": { - "operationId": "createEmergencyContact", - "tags": ["Emergency Contacts"], - "summary": "Create an emergency contact", - "security": [{"bearerAuth": []}], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": ["parent_id", "emergency_contact_name", "cellphone", "relation"], - "properties": { - "parent_id": {"type": "integer"}, - "emergency_contact_name": {"type": "string"}, - "cellphone": {"type": "string"}, - "relation": {"type": "string"}, - "email": {"type": "string", "format": "email"} - } - } - } - } - }, - "responses": { - "201": {"description": "Emergency contact created"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "422": {"description": "Validation error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorResponse"}}}} - } - } - }, - "/api/v1/emergency-contacts/{id}": { - "get": { - "operationId": "getEmergencyContact", - "tags": ["Emergency Contacts"], - "summary": "Get a single emergency contact", - "security": [{"bearerAuth": []}], - "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "integer"}}], - "responses": { - "200": {"description": "Emergency contact details"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "404": {"description": "Not found"} - } - }, - "patch": { - "operationId": "updateEmergencyContact", - "tags": ["Emergency Contacts"], - "summary": "Update an emergency contact", - "security": [{"bearerAuth": []}], - "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "integer"}}], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "emergency_contact_name": {"type": "string"}, - "cellphone": {"type": "string"}, - "email": {"type": "string", "format": "email"}, - "relation": {"type": "string"} - } - } - } - } - }, - "responses": { - "200": {"description": "Emergency contact updated"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"} - } - }, - "delete": { - "operationId": "deleteEmergencyContact", - "tags": ["Emergency Contacts"], - "summary": "Delete an emergency contact", - "security": [{"bearerAuth": []}], - "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "integer"}}], - "responses": { - "200": {"description": "Emergency contact deleted"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "404": {"description": "Not found"} - } - } - }, - "/api/v1/events": { - "get": { - "operationId": "listEvents", - "tags": ["Events"], - "summary": "List events", - "security": [{"bearerAuth": []}], - "parameters": [ - {"name": "school_year", "in": "query", "schema": {"type": "string"}}, - {"name": "upcoming", "in": "query", "schema": {"type": "boolean"}} - ], - "responses": { - "200": {"description": "List of events"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"} - } - }, - "post": { - "operationId": "createEvent", - "tags": ["Events"], - "summary": "Create a new event", - "security": [{"bearerAuth": []}], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": ["event_name", "amount", "expiration_date"], - "properties": { - "event_name": {"type": "string"}, - "amount": {"type": "number"}, - "expiration_date": {"type": "string", "format": "date"}, - "description": {"type": "string"}, - "semester": {"type": "string"}, - "school_year": {"type": "string"} - } - } - } - } - }, - "responses": { - "201": {"description": "Event created"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "422": {"description": "Validation error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorResponse"}}}} - } - } - }, - "/api/v1/events/{id}": { - "get": { - "operationId": "getEvent", - "tags": ["Events"], - "summary": "Get a single event", - "security": [{"bearerAuth": []}], - "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "integer"}}], - "responses": { - "200": {"description": "Event details"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "404": {"description": "Not found"} - } - }, - "patch": { - "operationId": "updateEvent", - "tags": ["Events"], - "summary": "Update an event", - "security": [{"bearerAuth": []}], - "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "integer"}}], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "event_name": {"type": "string"}, - "description": {"type": "string"}, - "amount": {"type": "number"}, - "expiration_date": {"type": "string", "format": "date"}, - "semester": {"type": "string"}, - "school_year": {"type": "string"} - } - } - } - } - }, - "responses": { - "200": {"description": "Event updated"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"} - } - }, - "delete": { - "operationId": "deleteEvent", - "tags": ["Events"], - "summary": "Delete an event", - "security": [{"bearerAuth": []}], - "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "integer"}}], - "responses": { - "200": {"description": "Event deleted"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "404": {"description": "Not found"} - } - } - }, - "/api/v1/expenses": { - "get": { - "operationId": "listExpenses", - "tags": ["Expenses"], - "summary": "List expenses", - "security": [{"bearerAuth": []}], - "parameters": [ - {"name": "status", "in": "query", "schema": {"type": "string"}}, - {"name": "category", "in": "query", "schema": {"type": "string"}}, - {"name": "page", "in": "query", "schema": {"type": "integer", "default": 1}}, - {"name": "per_page", "in": "query", "schema": {"type": "integer", "default": 20}} - ], - "responses": { - "200": {"description": "Paginated expenses"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"} - } - }, - "post": { - "operationId": "createExpense", - "tags": ["Expenses"], - "summary": "Create an expense", - "security": [{"bearerAuth": []}], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": ["category", "amount", "description", "date_of_purchase"], - "properties": { - "category": {"type": "string"}, - "amount": {"type": "number"}, - "description": {"type": "string"}, - "date_of_purchase": {"type": "string", "format": "date"} - } - } - } - } - }, - "responses": { - "201": {"description": "Expense created"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "422": {"description": "Validation error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorResponse"}}}} - } - } - }, - "/api/v1/expenses/{id}": { - "get": { - "operationId": "getExpense", - "tags": ["Expenses"], - "summary": "Get a single expense", - "security": [{"bearerAuth": []}], - "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "integer"}}], - "responses": { - "200": {"description": "Expense details"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "404": {"description": "Not found"} - } - }, - "patch": { - "operationId": "updateExpense", - "tags": ["Expenses"], - "summary": "Update an expense", - "security": [{"bearerAuth": []}], - "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "integer"}}], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "category": {"type": "string"}, - "amount": {"type": "number"}, - "description": {"type": "string"}, - "date_of_purchase": {"type": "string", "format": "date"}, - "status": {"type": "string"}, - "status_reason": {"type": "string"} - } - } - } - } - }, - "responses": { - "200": {"description": "Expense updated"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"} - } - }, - "delete": { - "operationId": "deleteExpense", - "tags": ["Expenses"], - "summary": "Delete an expense", - "security": [{"bearerAuth": []}], - "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "integer"}}], - "responses": { - "200": {"description": "Expense deleted"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "404": {"description": "Not found"} - } - } - }, - "/api/v1/extra-charges": { - "get": { - "operationId": "listExtraCharges", - "tags": ["Extra Charges"], - "summary": "List extra charges", - "security": [{"bearerAuth": []}], - "parameters": [ - {"name": "school_year", "in": "query", "schema": {"type": "string"}}, - {"name": "semester", "in": "query", "schema": {"type": "string"}}, - {"name": "status", "in": "query", "schema": {"type": "string"}}, - {"name": "q", "in": "query", "schema": {"type": "string"}}, - {"name": "page", "in": "query", "schema": {"type": "integer", "default": 1}}, - {"name": "per_page", "in": "query", "schema": {"type": "integer", "default": 20}} - ], - "responses": { - "200": {"description": "Paginated extra charges"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"} - } - }, - "post": { - "operationId": "createExtraCharge", - "tags": ["Extra Charges"], - "summary": "Create an extra charge", - "security": [{"bearerAuth": []}], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": ["parent_id", "title", "amount", "charge_type"], - "properties": { - "parent_id": {"type": "integer"}, - "title": {"type": "string"}, - "amount": {"type": "number"}, - "charge_type": {"type": "string", "enum": ["add", "deduct"]}, - "due_date": {"type": "string", "format": "date"}, - "invoice_id": {"type": "integer"}, - "description": {"type": "string"}, - "school_year": {"type": "string"}, - "semester": {"type": "string"} - } - } - } - } - }, - "responses": { - "201": {"description": "Extra charge created"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "422": {"description": "Validation error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorResponse"}}}} - } - } - }, - "/api/v1/extra-charges/{id}": { - "get": { - "operationId": "getExtraCharge", - "tags": ["Extra Charges"], - "summary": "Get a single extra charge", - "security": [{"bearerAuth": []}], - "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "integer"}}], - "responses": { - "200": {"description": "Extra charge details"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "404": {"description": "Not found"} - } - }, - "patch": { - "operationId": "updateExtraCharge", - "tags": ["Extra Charges"], - "summary": "Update an extra charge", - "security": [{"bearerAuth": []}], - "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "integer"}}], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "title": {"type": "string"}, - "description": {"type": "string"}, - "amount": {"type": "number"}, - "due_date": {"type": "string", "format": "date"}, - "charge_type": {"type": "string", "enum": ["add", "deduct"]} - } - } - } - } - }, - "responses": { - "200": {"description": "Extra charge updated"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"} - } - }, - "delete": { - "operationId": "deleteExtraCharge", - "tags": ["Extra Charges"], - "summary": "Delete an extra charge", - "security": [{"bearerAuth": []}], - "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "integer"}}], - "responses": { - "200": {"description": "Extra charge deleted"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "404": {"description": "Not found"} - } - } - }, - "/api/v1/messages": { - "get": { - "operationId": "listMessages", - "tags": ["Messages"], - "summary": "List messages", - "security": [{"bearerAuth": []}], - "parameters": [ - {"name": "type", "in": "query", "schema": {"type": "string", "enum": ["sent", "inbox"]}}, - {"name": "read", "in": "query", "schema": {"type": "boolean"}} - ], - "responses": { - "200": {"description": "Messages"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"} - } - }, - "post": { - "operationId": "sendMessage", - "tags": ["Messages"], - "summary": "Send a message", - "security": [{"bearerAuth": []}], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": ["subject", "message"], - "properties": { - "recipient_id": {"type": "integer"}, - "recipient_role": {"type": "string"}, - "subject": {"type": "string"}, - "message": {"type": "string"}, - "priority": {"type": "string", "enum": ["normal", "high", "low"]}, - "status": {"type": "string", "enum": ["sent", "draft"]} - } - } - } - } - }, - "responses": { - "201": {"description": "Message sent"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "422": {"description": "Validation error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ErrorResponse"}}}} - } - } - }, - "/api/v1/messages/{id}": { - "get": { - "operationId": "getMessage", - "tags": ["Messages"], - "summary": "Get a single message", - "security": [{"bearerAuth": []}], - "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "integer"}}], - "responses": { - "200": {"description": "Message details"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "404": {"description": "Not found"} - } - } - }, - "/api/v1/messages/inbox": { - "get": { - "operationId": "getInboxMessages", - "tags": ["Messages"], - "summary": "Get inbox messages", - "security": [{"bearerAuth": []}], - "responses": { - "200": {"description": "Inbox messages"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"} - } - } - }, - "/api/v1/messages/sent": { - "get": { - "operationId": "getSentMessages", - "tags": ["Messages"], - "summary": "Get sent messages", - "security": [{"bearerAuth": []}], - "responses": { - "200": {"description": "Sent messages"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"} - } - } - }, - "/api/v1/notifications": { - "get": { - "operationId": "listNotifications", - "tags": ["Notifications"], - "summary": "List notifications", - "security": [{"bearerAuth": []}], - "parameters": [ - {"name": "read", "in": "query", "schema": {"type": "boolean"}}, - {"name": "page", "in": "query", "schema": {"type": "integer", "default": 1}}, - {"name": "per_page", "in": "query", "schema": {"type": "integer", "default": 20}} - ], - "responses": { - "200": {"description": "Paginated notifications"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"} - } - } - }, - "/api/v1/notifications/{id}": { - "get": { - "operationId": "getNotification", - "tags": ["Notifications"], - "summary": "Get a single notification", - "security": [{"bearerAuth": []}], - "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "integer"}}], - "responses": { - "200": {"description": "Notification details"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "404": {"description": "Not found"} - } - } - }, - "/api/v1/notifications/{id}/read": { - "post": { - "operationId": "markNotificationRead", - "tags": ["Notifications"], - "summary": "Mark a notification as read", - "security": [{"bearerAuth": []}], - "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "integer"}}], - "responses": { - "200": {"description": "Notification marked as read"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"} - } - } - }, - "/api/v1/settings": { - "get": { - "operationId": "getSettings", - "tags": ["Settings"], - "summary": "Get grouped configuration values", - "security": [{"bearerAuth": []}], - "responses": { - "200": {"description": "Settings"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"} - } - }, - "patch": { - "operationId": "updateSettings", - "tags": ["Settings"], - "summary": "Update settings", - "security": [{"bearerAuth": []}], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "school_id": {"type": "string"}, - "school_year": {"type": "string"}, - "firstname": {"type": "string"}, - "lastname": {"type": "string"}, - "dob": {"type": "string", "format": "date"}, - "age": {"type": "integer"}, - "gender": {"type": "string"}, - "is_active": {"type": "boolean"}, - "registration_grade": {"type": "string"}, - "is_new": {"type": "boolean"}, - "photo_consent": {"type": "boolean"}, - "parent_id": {"type": "integer"}, - "registration_date": {"type": "string", "format": "date"}, - "tuition_paid": {"type": "boolean"}, - "semester": {"type": "string"}, - "year_of_registration": {"type": "integer"} - } - } - } - } - }, - "responses": { - "200": {"description": "Settings updated"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"} - } - } - }, - "/api/v1/scores/homework": { - "get": { - "operationId": "getHomeworkScores", - "tags": ["Scores"], - "summary": "HomeworkController::index", - "security": [{"bearerAuth": []}], - "parameters": [ - { - "name": "class_section_id", - "in": "query", - "required": true, - "schema": { - "type": "integer", - "minimum": 1 - } - }, - { - "name": "semester", - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "school_year", - "in": "query", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": {"description": "OK"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "422": { - "description": "Validation error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - } - } - } - }, - "/api/v1/scores/comments": { - "get": { - "operationId": "getScoreComments", - "tags": ["Scores"], - "summary": "ScoreCommentController::index", - "security": [{"bearerAuth": []}], - "parameters": [ - { - "name": "class_section_id", - "in": "query", - "required": true, - "schema": { - "type": "integer", - "minimum": 1 - } - }, - { - "name": "semester", - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "school_year", - "in": "query", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": {"description": "OK"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "422": { - "description": "Validation error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - } - } - } - }, - "/api/v1/scores/projects": { - "get": { - "operationId": "getProjectScores", - "tags": ["Scores"], - "summary": "ProjectController::index", - "security": [{"bearerAuth": []}], - "parameters": [ - { - "name": "class_section_id", - "in": "query", - "required": true, - "schema": { - "type": "integer", - "minimum": 1 - } - }, - { - "name": "semester", - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "school_year", - "in": "query", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": {"description": "OK"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "422": { - "description": "Validation error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - } - } - } - }, - "/api/v1/scores/quizzes": { - "get": { - "operationId": "getQuizScores", - "tags": ["Scores"], - "summary": "QuizController::index", - "security": [{"bearerAuth": []}], - "parameters": [ - { - "name": "class_section_id", - "in": "query", - "required": true, - "schema": { - "type": "integer", - "minimum": 1 - } - }, - { - "name": "semester", - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "school_year", - "in": "query", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": {"description": "OK"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "422": { - "description": "Validation error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - } - } - } - }, - "/api/v1/scores/midterm": { - "get": { - "operationId": "getMidtermScores", - "tags": ["Scores"], - "summary": "MidtermController::index", - "security": [{"bearerAuth": []}], - "parameters": [ - { - "name": "class_section_id", - "in": "query", - "required": true, - "schema": { - "type": "integer", - "minimum": 1 - } - }, - { - "name": "semester", - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "school_year", - "in": "query", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": {"description": "OK"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "422": { - "description": "Validation error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - } - } - } - }, - "/api/v1/scores/final": { - "get": { - "operationId": "getFinalScores", - "tags": ["Scores"], - "summary": "FinalController::index", - "security": [{"bearerAuth": []}], - "parameters": [ - { - "name": "class_section_id", - "in": "query", - "required": true, - "schema": { - "type": "integer", - "minimum": 1 - } - }, - { - "name": "semester", - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "school_year", - "in": "query", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": {"description": "OK"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "422": { - "description": "Validation error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - } - } - } - }, - "/api/v1/scores/participation": { - "get": { - "operationId": "getParticipationScores", - "tags": ["Scores"], - "summary": "ParticipationController::index", - "security": [{"bearerAuth": []}], - "parameters": [ - { - "name": "class_section_id", - "in": "query", - "required": true, - "schema": { - "type": "integer", - "minimum": 1 - } - }, - { - "name": "semester", - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "school_year", - "in": "query", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": {"description": "OK"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "422": { - "description": "Validation error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - } - } - } - }, - "/api/v1/scores/projects/by-year": { - "get": { - "operationId": "getProjectScoresByYear", - "tags": ["Scores"], - "summary": "ProjectController::bySchoolYear", - "security": [{"bearerAuth": []}], - "parameters": [ - { - "name": "school_year", - "in": "query", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": {"description": "OK"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "422": { - "description": "Validation error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - } - } - } - }, - "/api/v1/scores/quizzes/by-year": { - "get": { - "operationId": "getQuizScoresByYear", - "tags": ["Scores"], - "summary": "QuizController::bySchoolYear", - "security": [{"bearerAuth": []}], - "parameters": [ - { - "name": "school_year", - "in": "query", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": {"description": "OK"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "422": { - "description": "Validation error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - } - } - } - }, - "/api/v1/scores/homework/by-year": { - "get": { - "operationId": "getHomeworkScoresByYear", - "tags": ["Scores"], - "summary": "HomeworkController::bySchoolYear", - "security": [{"bearerAuth": []}], - "parameters": [ - { - "name": "school_year", - "in": "query", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": {"description": "OK"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "422": { - "description": "Validation error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - } - } - } - }, - "/api/v1/scores/midterm/by-year": { - "get": { - "operationId": "getMidtermScoresByYear", - "tags": ["Scores"], - "summary": "MidtermController::bySchoolYear", - "security": [{"bearerAuth": []}], - "parameters": [ - { - "name": "school_year", - "in": "query", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": {"description": "OK"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "422": { - "description": "Validation error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - } - } - } - }, - "/api/v1/scores/final/by-year": { - "get": { - "operationId": "getFinalScoresByYear", - "tags": ["Scores"], - "summary": "FinalController::bySchoolYear", - "security": [{"bearerAuth": []}], - "parameters": [ - { - "name": "school_year", - "in": "query", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": {"description": "OK"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "422": { - "description": "Validation error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - } - } - } - }, - "/api/v1/scores/participation/by-year": { - "get": { - "operationId": "getParticipationScoresByYear", - "tags": ["Scores"], - "summary": "ParticipationController::bySchoolYear", - "security": [{"bearerAuth": []}], - "parameters": [ - { - "name": "school_year", - "in": "query", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": {"description": "OK"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "422": { - "description": "Validation error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - } - } - } - }, - "/api/v1/students": { - "get": { - "operationId": "listStudentsByYear", - "tags": ["Students"], - "summary": "StudentController::index", - "security": [{"bearerAuth": []}], - "parameters": [ - { - "name": "school_year", - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "parent_id", - "in": "query", - "schema": { - "type": "integer" - } - }, - { - "name": "parent_ids[]", - "in": "query", - "schema": { - "type": "array", - "items": { - "type": "integer" - } - } - } - ], - "responses": { - "200": {"description": "OK"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "422": { - "description": "Validation error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - } - } - }, - "post": { - "operationId": "createStudent", - "tags": ["Students"], - "summary": "StudentController::store", - "security": [{"bearerAuth": []}], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": ["school_year", "firstname", "lastname"], - "properties": { - "school_year": {"type": "string"}, - "firstname": {"type": "string"}, - "lastname": {"type": "string"}, - "dob": {"type": "string", "format": "date"}, - "age": {"type": "integer"}, - "gender": {"type": "string"}, - "is_active": {"type": "boolean"}, - "registration_grade": {"type": "string"}, - "is_new": {"type": "boolean"}, - "photo_consent": {"type": "boolean"}, - "parent_id": {"type": "integer"}, - "registration_date": {"type": "string", "format": "date"}, - "tuition_paid": {"type": "boolean"}, - "semester": {"type": "string"}, - "year_of_registration": {"type": "integer"} - } - } - } - } - }, - "responses": { - "201": {"description": "Created"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "422": { - "description": "Validation error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - } - } - } - }, - "/api/v1/students/{studentId}": { - "get": { - "operationId": "getStudent", - "tags": ["Students"], - "summary": "StudentController::show", - "security": [{"bearerAuth": []}], - "parameters": [ - { - "name": "studentId", - "in": "path", - "required": true, - "schema": {"type": "integer"} - } - ], - "responses": { - "200": {"description": "OK"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "404": {"description": "Not found"} - } - }, - "patch": { - "operationId": "updateStudent", - "tags": ["Students"], - "summary": "StudentController::update", - "security": [{"bearerAuth": []}], - "parameters": [ - { - "name": "studentId", - "in": "path", - "required": true, - "schema": {"type": "integer"} - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true - } - } - } - }, - "responses": { - "200": {"description": "OK"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "422": { - "description": "Validation error", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/ErrorResponse"} - } - } - } - } - }, - "delete": { - "operationId": "deactivateStudent", - "tags": ["Students"], - "summary": "StudentController::destroy", - "security": [{"bearerAuth": []}], - "parameters": [ - { - "name": "studentId", - "in": "path", - "required": true, - "schema": {"type": "integer"} - } - ], - "responses": { - "200": {"description": "OK"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "404": {"description": "Not found"} - } - } - }, - "/api/v1/students/{studentId}/classes": { - "get": { - "operationId": "getStudentClasses", - "tags": ["Students"], - "summary": "StudentController::classes", - "security": [{"bearerAuth": []}], - "parameters": [ - {"name": "studentId", "in": "path", "required": true, "schema": {"type": "integer"}}, - {"name": "school_year", "in": "query", "schema": {"type": "string"}} - ], - "responses": { - "200": {"description": "OK"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"} - } - }, - "post": { - "operationId": "assignStudentClasses", - "tags": ["Students"], - "summary": "StudentController::assignClassForStudent", - "security": [{"bearerAuth": []}], - "parameters": [ - {"name": "studentId", "in": "path", "required": true, "schema": {"type": "integer"}} - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": ["student_id", "class_section_id"], - "properties": { - "student_id": {"type": "integer"}, - "class_section_id": {"type": "integer"}, - "class_section_ids": {"type": "array", "items": {"type": "integer"}}, - "is_event_only": {"type": "boolean"} - } - } - } - } - }, - "responses": { - "200": {"description": "OK"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "422": {"description": "Validation error"} - } - } - }, - "/api/v1/students/{studentId}/classes/{classSectionId}": { - "delete": { - "operationId": "removeStudentClass", - "tags": ["Students"], - "summary": "StudentController::removeClassForStudent", - "security": [{"bearerAuth": []}], - "parameters": [ - {"name": "studentId", "in": "path", "required": true, "schema": {"type": "integer"}}, - {"name": "classSectionId", "in": "path", "required": true, "schema": {"type": "integer"}} - ], - "responses": { - "200": {"description": "OK"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "422": {"description": "Validation error"} - } - } - }, - "/api/v1/students/{studentId}/promote": { - "post": { - "operationId": "promoteStudent", - "tags": ["Students"], - "summary": "StudentController::promote", - "security": [{"bearerAuth": []}], - "parameters": [ - {"name": "studentId", "in": "path", "required": true, "schema": {"type": "integer"}} - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": ["name", "cellphone"], - "properties": { - "name": {"type": "string"}, - "cellphone": {"type": "string"}, - "email": {"type": "string", "format": "email"}, - "relation": {"type": "string"}, - "semester": {"type": "string"}, - "school_year": {"type": "string"} - } - } - } - } - }, - "responses": { - "200": {"description": "OK"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "422": {"description": "Validation error"} - } - } - }, - "/api/v1/students/{studentId}/attendance": { - "get": { - "operationId": "getStudentAttendance", - "tags": ["Students"], - "summary": "StudentController::attendance", - "security": [{"bearerAuth": []}], - "parameters": [ - {"name": "studentId", "in": "path", "required": true, "schema": {"type": "integer"}}, - {"name": "school_year", "in": "query", "schema": {"type": "string"}}, - {"name": "semester", "in": "query", "schema": {"type": "string"}} - ], - "responses": { - "200": {"description": "OK"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"} - } - } - }, - "/api/v1/students/{studentId}/incidents": { - "get": { - "operationId": "getStudentIncidents", - "tags": ["Students"], - "summary": "StudentController::incidents", - "security": [{"bearerAuth": []}], - "parameters": [ - {"name": "studentId", "in": "path", "required": true, "schema": {"type": "integer"}}, - {"name": "school_year", "in": "query", "schema": {"type": "string"}}, - {"name": "semester", "in": "query", "schema": {"type": "string"}} - ], - "responses": { - "200": {"description": "OK"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"} - } - } - }, - "/api/v1/students/{studentId}/scores": { - "get": { - "operationId": "getStudentScores", - "tags": ["Students"], - "summary": "StudentController::scores", - "security": [{"bearerAuth": []}], - "parameters": [ - {"name": "studentId", "in": "path", "required": true, "schema": {"type": "integer"}}, - {"name": "school_year", "in": "query", "schema": {"type": "string"}}, - {"name": "semester", "in": "query", "schema": {"type": "string"}} - ], - "responses": { - "200": {"description": "OK"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"} - } - } - }, - "/api/v1/students/{studentId}/notes": { - "get": { - "operationId": "getStudentNotes", - "tags": ["Students"], - "summary": "StudentController::notes", - "security": [{"bearerAuth": []}], - "parameters": [ - {"name": "studentId", "in": "path", "required": true, "schema": {"type": "integer"}} - ], - "responses": { - "200": {"description": "OK"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"} - } - } - }, - "/api/v1/students/{studentId}/parents": { - "get": { - "operationId": "getStudentParents", - "tags": ["Students"], - "summary": "StudentController::parents", - "security": [{"bearerAuth": []}], - "parameters": [ - {"name": "studentId", "in": "path", "required": true, "schema": {"type": "integer"}} - ], - "responses": { - "200": {"description": "OK"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "404": {"description": "Not found"} - } - } - }, - "/api/v1/students/{studentId}/emergency-contacts": { - "get": { - "operationId": "getStudentEmergencyContacts", - "tags": ["Students"], - "summary": "StudentController::emergencyContacts", - "security": [{"bearerAuth": []}], - "parameters": [ - {"name": "studentId", "in": "path", "required": true, "schema": {"type": "integer"}} - ], - "responses": { - "200": {"description": "OK"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"} - } - }, - "post": { - "operationId": "addStudentEmergencyContact", - "tags": ["Students"], - "summary": "StudentController::addEmergencyContact", - "security": [{"bearerAuth": []}], - "parameters": [ - {"name": "studentId", "in": "path", "required": true, "schema": {"type": "integer"}} - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": {"type": "object", "additionalProperties": true} - } - } - }, - "responses": { - "201": {"description": "Created"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "422": {"description": "Validation error"} - } - } - }, - "/api/v1/students/{studentId}/emergency-contacts/{contactId}": { - "patch": { - "operationId": "updateStudentEmergencyContact", - "tags": ["Students"], - "summary": "StudentController::updateEmergencyContact", - "security": [{"bearerAuth": []}], - "parameters": [ - {"name": "studentId", "in": "path", "required": true, "schema": {"type": "integer"}}, - {"name": "contactId", "in": "path", "required": true, "schema": {"type": "integer"}} - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": {"type": "object", "additionalProperties": true} - } - } - }, - "responses": { - "200": {"description": "OK"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "422": {"description": "Validation error"} - } - } - }, - "/api/v1/students/{studentId}/badge_scan": { - "post": { - "operationId": "updateStudentBadgeScan", - "tags": ["Students"], - "summary": "StudentController::updateBadgeScan", - "security": [{"bearerAuth": []}], - "parameters": [ - {"name": "studentId", "in": "path", "required": true, "schema": {"type": "integer"}} - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": {"type": "object", "additionalProperties": true} - } - } - }, - "responses": { - "200": {"description": "OK"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "422": {"description": "Validation error"} - } - } - }, - "/api/v1/students/{studentId}/photo": { - "get": { - "operationId": "getStudentPhoto", - "tags": ["Students"], - "summary": "StudentController::photo", - "security": [{"bearerAuth": []}], - "parameters": [ - {"name": "studentId", "in": "path", "required": true, "schema": {"type": "integer"}} - ], - "responses": { - "200": {"description": "OK"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "404": {"description": "Not found"} - } - }, - "post": { - "operationId": "uploadStudentPhoto", - "tags": ["Students"], - "summary": "StudentController::uploadPhoto", - "security": [{"bearerAuth": []}], - "parameters": [ - {"name": "studentId", "in": "path", "required": true, "schema": {"type": "integer"}} - ], - "requestBody": { - "required": true, - "content": { - "multipart/form-data": { - "schema": { - "type": "object", - "properties": { - "photo": {"type": "string", "format": "binary"} - } - } - } - } - }, - "responses": { - "200": {"description": "OK"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "422": {"description": "Validation error"} - } - } - }, - "/api/v1/students/{studentId}/report-card": { - "get": { - "operationId": "getStudentReportCard", - "tags": ["Students"], - "summary": "ReportCardsController::studentReport", - "security": [{"bearerAuth": []}], - "parameters": [ - {"name": "studentId", "in": "path", "required": true, "schema": {"type": "integer"}} - ], - "responses": { - "200": {"description": "OK"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "422": {"description": "Validation error"} - } - } - }, - "/api/v1/students/{studentId}/score-card": { - "get": { - "operationId": "getStudentScoreCard", - "tags": ["Students"], - "summary": "StudentController::scoreCard", - "security": [{"bearerAuth": []}], - "parameters": [ - {"name": "studentId", "in": "path", "required": true, "schema": {"type": "integer"}} - ], - "responses": { - "200": {"description": "OK"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"} - } - } - }, - "/api/v1/students/assign-class": { - "post": { - "operationId": "assignClass", - "tags": ["Students"], - "summary": "StudentController::assignClass", - "security": [{"bearerAuth": []}], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": ["student_id", "class_section_id"], - "properties": { - "student_id": {"type": "integer"}, - "class_section_id": {"type": "array", "items": {"type": "integer"}}, - "is_event_only": {"type": "boolean"} - } - } - } - } - }, - "responses": { - "200": {"description": "OK"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "422": {"description": "Validation error"} - } - } - }, - "/api/v1/students/remove-class": { - "post": { - "operationId": "removeClass", - "tags": ["Students"], - "summary": "StudentController::removeClass", - "security": [{"bearerAuth": []}], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": ["student_id", "class_section_id"], - "properties": { - "student_id": {"type": "integer"}, - "class_section_id": {"type": "integer"} - } - } - } - } - }, - "responses": { - "200": {"description": "OK"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "422": {"description": "Validation error"} - } - } - }, - "/api/v1/students/set-active": { - "post": { - "operationId": "setActive", - "tags": ["Students"], - "summary": "StudentController::setActive", - "security": [{"bearerAuth": []}], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": ["student_id", "is_active"], - "properties": { - "student_id": {"type": "integer"}, - "is_active": {"type": "boolean"} - } - } - } - } - }, - "responses": { - "200": {"description": "OK"}, - "401": {"$ref": "#/components/responses/UnauthorizedError"}, - "422": {"description": "Validation error"} - } - } - }, - "/api/v1/time": { - "get": { - "operationId": "getServerTime", - "tags": ["Time"], - "summary": "Get server timestamp and timezone", - "responses": { - "200": {"description": "Server time information"} - }, - "security": [] - } + } + }, + "/api/v1/users/login-activity": { + "get": { + "operationId": "getUserLoginActivity", + "tags": [ + "Users" + ], + "summary": "Get paginated login history", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "page", + "in": "query", + "schema": { + "type": "integer", + "default": 1 + } + }, + { + "name": "per_page", + "in": "query", + "schema": { + "type": "integer", + "default": 25 + } + } + ], + "responses": { + "200": { + "description": "Login activity" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + } } + } + }, + "/api/v1/users": { + "get": { + "operationId": "listUsers", + "tags": [ + "Users" + ], + "summary": "List users", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "page", + "in": "query", + "schema": { + "type": "integer", + "default": 1 + } + }, + { + "name": "per_page", + "in": "query", + "schema": { + "type": "integer", + "default": 20 + } + }, + { + "name": "sort", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "id", + "firstname", + "lastname", + "email", + "role" + ] + } + }, + { + "name": "order", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "asc", + "desc" + ] + } + } + ], + "responses": { + "200": { + "description": "Paginated user list" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + } + } + }, + "post": { + "operationId": "createUser", + "tags": [ + "Users" + ], + "summary": "Create a new user", + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "firstname", + "lastname", + "email", + "password", + "cellphone", + "address_street", + "city", + "state", + "zip", + "accept_school_policy", + "role_id" + ], + "properties": { + "firstname": { + "type": "string" + }, + "lastname": { + "type": "string" + }, + "email": { + "type": "string", + "format": "email" + }, + "password": { + "type": "string" + }, + "cellphone": { + "type": "string" + }, + "address_street": { + "type": "string" + }, + "city": { + "type": "string" + }, + "state": { + "type": "string" + }, + "zip": { + "type": "string" + }, + "accept_school_policy": { + "type": "boolean" + }, + "role_id": { + "type": "integer" + } + } + } + } + } + }, + "responses": { + "201": { + "description": "User created" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "422": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/students": { + "get": { + "operationId": "listStudentsByYear", + "tags": [ + "Students" + ], + "summary": "StudentController::index", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "school_year", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "parent_id", + "in": "query", + "schema": { + "type": "integer" + } + }, + { + "name": "parent_ids[]", + "in": "query", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "422": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + }, + "post": { + "operationId": "createStudent", + "tags": [ + "Students" + ], + "summary": "StudentController::store", + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "school_year", + "firstname", + "lastname" + ], + "properties": { + "school_year": { + "type": "string" + }, + "firstname": { + "type": "string" + }, + "lastname": { + "type": "string" + }, + "dob": { + "type": "string", + "format": "date" + }, + "age": { + "type": "integer" + }, + "gender": { + "type": "string" + }, + "is_active": { + "type": "boolean" + }, + "registration_grade": { + "type": "string" + }, + "is_new": { + "type": "boolean" + }, + "photo_consent": { + "type": "boolean" + }, + "parent_id": { + "type": "integer" + }, + "registration_date": { + "type": "string", + "format": "date" + }, + "tuition_paid": { + "type": "boolean" + }, + "semester": { + "type": "string" + }, + "year_of_registration": { + "type": "integer" + } + } + } + } + } + }, + "responses": { + "201": { + "description": "Created" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "422": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/students/{id}": { + "get": { + "operationId": "getStudent", + "tags": [ + "Students" + ], + "summary": "Retrieve a single student", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Student details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Student" + } + } + } + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "404": { + "description": "Not found" + } + } + }, + "patch": { + "operationId": "updateStudent", + "tags": [ + "Students" + ], + "summary": "Update a student", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "school_id": { + "type": "string" + }, + "school_year": { + "type": "string" + }, + "firstname": { + "type": "string" + }, + "lastname": { + "type": "string" + }, + "dob": { + "type": "string", + "format": "date" + }, + "age": { + "type": "integer" + }, + "gender": { + "type": "string" + }, + "is_active": { + "type": "boolean" + }, + "registration_grade": { + "type": "string" + }, + "is_new": { + "type": "boolean" + }, + "photo_consent": { + "type": "boolean" + }, + "parent_id": { + "type": "integer" + }, + "registration_date": { + "type": "string", + "format": "date" + }, + "tuition_paid": { + "type": "boolean" + }, + "semester": { + "type": "string" + }, + "year_of_registration": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "cellphone": { + "type": "string" + }, + "email": { + "type": "string" + }, + "relation": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Student updated" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "422": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + }, + "delete": { + "operationId": "deleteStudent", + "tags": [ + "Students" + ], + "summary": "Delete a student", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Student deleted" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "404": { + "description": "Not found" + } + } + } + }, + "/api/v1/attendance": { + "get": { + "operationId": "listAttendance", + "tags": [ + "Attendance" + ], + "summary": "List attendance records", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "page", + "in": "query", + "schema": { + "type": "integer", + "default": 1 + } + }, + { + "name": "per_page", + "in": "query", + "schema": { + "type": "integer", + "default": 20 + } + }, + { + "name": "student_id", + "in": "query", + "schema": { + "type": "integer" + } + }, + { + "name": "date", + "in": "query", + "schema": { + "type": "string", + "format": "date" + } + } + ], + "responses": { + "200": { + "description": "Paginated attendance records", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaginatedResponse" + } + } + } + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + } + } + }, + "post": { + "operationId": "createAttendance", + "tags": [ + "Attendance" + ], + "summary": "Create an attendance record", + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AttendanceRecord" + } + } + } + }, + "responses": { + "201": { + "description": "Record created" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "409": { + "description": "Duplicate for student/date" + } + } + } + }, + "/api/v1/invoices": { + "get": { + "operationId": "listInvoices", + "tags": [ + "Invoices" + ], + "summary": "List invoices", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "page", + "in": "query", + "schema": { + "type": "integer", + "default": 1 + } + }, + { + "name": "per_page", + "in": "query", + "schema": { + "type": "integer", + "default": 20 + } + }, + { + "name": "parent_id", + "in": "query", + "schema": { + "type": "integer" + } + }, + { + "name": "status", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Paginated invoices", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaginatedResponse" + } + } + } + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + } + } + }, + "post": { + "operationId": "createInvoice", + "tags": [ + "Invoices" + ], + "summary": "Create an invoice", + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Invoice" + } + } + } + }, + "responses": { + "201": { + "description": "Invoice created" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + } + } + } + }, + "/api/v1/invoices/{id}": { + "get": { + "operationId": "getInvoice", + "tags": [ + "Invoices" + ], + "summary": "Show a single invoice", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Invoice details", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Invoice" + } + } + } + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "404": { + "description": "Not found" + } + } + }, + "patch": { + "operationId": "updateInvoice", + "tags": [ + "Invoices" + ], + "summary": "Update invoice attributes", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Invoice" + } + } + } + }, + "responses": { + "200": { + "description": "Invoice updated" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + } + } + }, + "delete": { + "operationId": "deleteInvoice", + "tags": [ + "Invoices" + ], + "summary": "Delete an invoice", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Invoice deleted" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "404": { + "description": "Not found" + } + } + } + }, + "/register": { + "post": { + "operationId": "webRegister", + "tags": [ + "Web" + ], + "summary": "Submit the legacy parent registration form", + "requestBody": { + "required": true, + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/WebRegisterForm" + } + } + } + }, + "responses": { + "302": { + "description": "Redirects to /register/success when registration completes." + }, + "422": { + "description": "Validation issues cause a redirect back to the form with flash errors." + } + } + } + }, + "/user/login": { + "post": { + "operationId": "webLogin", + "tags": [ + "Web" + ], + "summary": "Submit the legacy login form", + "requestBody": { + "required": true, + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/WebLoginForm" + } + } + } + }, + "responses": { + "302": { + "description": "Redirects to the dashboard when authentication succeeds." + }, + "401": { + "description": "Invalid credentials or suspended account results in a redirect back with flash information." + } + } + } + }, + "/processForgotPassword": { + "post": { + "operationId": "webForgotPassword", + "tags": [ + "Web" + ], + "summary": "Request a password reset link via the legacy form", + "requestBody": { + "required": true, + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/WebForgotPasswordForm" + } + } + } + }, + "responses": { + "302": { + "description": "Always redirects back to the form; success is indicated via flash data." + } + } + } + }, + "/user/processResetPassword": { + "post": { + "operationId": "webResetPassword", + "tags": [ + "Web" + ], + "summary": "Submit a new password using the reset token from the email", + "requestBody": { + "required": true, + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/WebResetPasswordForm" + } + } + } + }, + "responses": { + "200": { + "description": "Renders the 'password_set_success' view when the token is valid." + }, + "302": { + "description": "Invalid token or validation errors redirect to the home page or form." + } + } + } + }, + "/user/save_password": { + "post": { + "operationId": "webSetPassword", + "tags": [ + "Web" + ], + "summary": "Save the password after confirming the email address", + "requestBody": { + "required": true, + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/WebSetPasswordForm" + } + } + } + }, + "responses": { + "302": { + "description": "Redirects to /user/password_set_success on success or back on failure." + } + } + } + }, + "/api/v1/administrator/dashboard": { + "get": { + "operationId": "getAdministratorDashboard", + "tags": [ + "Administrator" + ], + "summary": "Returns counts for students, staff roles, and parents plus the four most recent login activities", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Dashboard data" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + } + } + } + }, + "/api/v1/administrator/absence": { + "get": { + "operationId": "getAdministratorAbsence", + "tags": [ + "Administrator" + ], + "summary": "Get absence form data", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Absence form data" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + } + } + }, + "post": { + "operationId": "submitAdministratorAbsence", + "tags": [ + "Administrator" + ], + "summary": "Submit absence request", + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "dates", + "reason" + ], + "properties": { + "dates": { + "type": "array", + "items": { + "type": "string", + "format": "date" + } + }, + "reason_type": { + "type": "string" + }, + "reason": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Absence submitted" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "422": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/administrator/search": { + "get": { + "operationId": "searchAdministrator", + "tags": [ + "Administrator" + ], + "summary": "Search across users, students, parents, staff, and emergency contacts", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "q", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Search results" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + } + } + } + }, + "/api/v1/assignments": { + "get": { + "operationId": "listAssignments", + "tags": [ + "Assignments" + ], + "summary": "List assignments", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "class_id", + "in": "query", + "schema": { + "type": "integer" + } + }, + { + "name": "teacher_id", + "in": "query", + "schema": { + "type": "integer" + } + }, + { + "name": "student_id", + "in": "query", + "schema": { + "type": "integer" + } + }, + { + "name": "page", + "in": "query", + "schema": { + "type": "integer", + "default": 1 + } + }, + { + "name": "per_page", + "in": "query", + "schema": { + "type": "integer", + "default": 20 + } + } + ], + "responses": { + "200": { + "description": "Paginated assignments" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + } + } + }, + "post": { + "operationId": "createAssignment", + "tags": [ + "Assignments" + ], + "summary": "Create an assignment", + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "title", + "due_date", + "class_id", + "teacher_id" + ], + "properties": { + "title": { + "type": "string" + }, + "due_date": { + "type": "string", + "format": "date" + }, + "class_id": { + "type": "integer" + }, + "teacher_id": { + "type": "integer" + } + } + } + } + } + }, + "responses": { + "201": { + "description": "Assignment created" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "422": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/assignments/{id}": { + "get": { + "operationId": "getAssignment", + "tags": [ + "Assignments" + ], + "summary": "Get a single assignment", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Assignment details" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "404": { + "description": "Not found" + } + } + }, + "patch": { + "operationId": "updateAssignment", + "tags": [ + "Assignments" + ], + "summary": "Update an assignment", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "due_date": { + "type": "string", + "format": "date" + }, + "class_id": { + "type": "integer" + }, + "teacher_id": { + "type": "integer" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Assignment updated" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + } + } + }, + "delete": { + "operationId": "deleteAssignment", + "tags": [ + "Assignments" + ], + "summary": "Delete an assignment", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Assignment deleted" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "404": { + "description": "Not found" + } + } + } + }, + "/api/v1/attendance/{id}": { + "get": { + "operationId": "getAttendance", + "tags": [ + "Attendance" + ], + "summary": "Get a single attendance record", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Attendance record" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "404": { + "description": "Not found" + } + } + }, + "patch": { + "operationId": "updateAttendance", + "tags": [ + "Attendance" + ], + "summary": "Update an attendance record", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "present", + "absent", + "late" + ] + }, + "notes": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Attendance record updated" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + } + } + }, + "delete": { + "operationId": "deleteAttendance", + "tags": [ + "Attendance" + ], + "summary": "Delete an attendance record", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Attendance record deleted" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "404": { + "description": "Not found" + } + } + } + }, + "/api/v1/attendance/class/{class_section_id}": { + "get": { + "operationId": "getClassAttendance", + "tags": [ + "Attendance" + ], + "summary": "Get class roster with attendance status", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "class_section_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "name": "date", + "in": "query", + "schema": { + "type": "string", + "format": "date" + } + } + ], + "responses": { + "200": { + "description": "Class roster with attendance" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + } + } + } + }, + "/api/v1/attendance/class/{class_section_id}/record": { + "post": { + "operationId": "recordClassAttendance", + "tags": [ + "Attendance" + ], + "summary": "Record attendance for multiple students in a class", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "class_section_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "date", + "records" + ], + "properties": { + "date": { + "type": "string", + "format": "date" + }, + "records": { + "type": "array", + "items": { + "type": "object", + "required": [ + "student_id", + "status" + ], + "properties": { + "student_id": { + "type": "integer" + }, + "status": { + "type": "string", + "enum": [ + "present", + "absent", + "late" + ] + }, + "reason": { + "type": "string" + } + } + } + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Attendance recorded" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "422": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/classes": { + "get": { + "operationId": "listClasses", + "tags": [ + "Classes" + ], + "summary": "List classes", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "grade", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "teacher_id", + "in": "query", + "schema": { + "type": "integer" + } + }, + { + "name": "page", + "in": "query", + "schema": { + "type": "integer", + "default": 1 + } + }, + { + "name": "per_page", + "in": "query", + "schema": { + "type": "integer", + "default": 20 + } + } + ], + "responses": { + "200": { + "description": "Paginated classes" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + } + } + } + }, + "/api/v1/classes/{id}": { + "get": { + "operationId": "getClass", + "tags": [ + "Classes" + ], + "summary": "Get a single class", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Class details" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "404": { + "description": "Not found" + } + } + } + }, + "/api/v1/classes/{id}/students": { + "get": { + "operationId": "getClassStudents", + "tags": [ + "Classes" + ], + "summary": "Get students in a class", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "List of students" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + } + } + } + }, + "/api/v1/calendar": { + "get": { + "operationId": "listCalendarEvents", + "tags": [ + "Calendar" + ], + "summary": "List calendar events", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "page", + "in": "query", + "schema": { + "type": "integer", + "default": 1 + } + }, + { + "name": "per_page", + "in": "query", + "schema": { + "type": "integer", + "default": 20 + } + }, + { + "name": "month", + "in": "query", + "schema": { + "type": "integer" + } + }, + { + "name": "year", + "in": "query", + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Paginated calendar events" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + } + } + }, + "post": { + "operationId": "createCalendarEvent", + "tags": [ + "Calendar" + ], + "summary": "Create a calendar event", + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "title", + "start_date" + ], + "properties": { + "title": { + "type": "string" + }, + "start_date": { + "type": "string", + "format": "date" + }, + "description": { + "type": "string" + }, + "end_date": { + "type": "string", + "format": "date" + }, + "location": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "201": { + "description": "Event created" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "422": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/calendar/{id}": { + "get": { + "operationId": "getCalendarEvent", + "tags": [ + "Calendar" + ], + "summary": "Get a single calendar event", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Event details" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "404": { + "description": "Not found" + } + } + }, + "patch": { + "operationId": "updateCalendarEvent", + "tags": [ + "Calendar" + ], + "summary": "Update a calendar event", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "start_date": { + "type": "string", + "format": "date" + }, + "description": { + "type": "string" + }, + "end_date": { + "type": "string", + "format": "date" + }, + "location": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Event updated" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + } + } + }, + "delete": { + "operationId": "deleteCalendarEvent", + "tags": [ + "Calendar" + ], + "summary": "Delete a calendar event", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Event deleted" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "404": { + "description": "Not found" + } + } + } + }, + "/api/v1/contact": { + "post": { + "operationId": "submitContact", + "tags": [ + "Contact" + ], + "summary": "Submit contact form", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "name", + "email", + "subject", + "message" + ], + "properties": { + "name": { + "type": "string" + }, + "email": { + "type": "string", + "format": "email" + }, + "subject": { + "type": "string" + }, + "message": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Contact form submitted" + }, + "422": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Server error" + } + }, + "security": [] + } + }, + "/api/v1/payments": { + "get": { + "operationId": "listPayments", + "tags": [ + "Payments" + ], + "summary": "List payments with optional filters", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "parent_id", + "in": "query", + "schema": { + "type": "integer" + } + }, + { + "name": "status", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "page", + "in": "query", + "schema": { + "type": "integer", + "default": 1 + } + }, + { + "name": "per_page", + "in": "query", + "schema": { + "type": "integer", + "default": 20 + } + } + ], + "responses": { + "200": { + "description": "Paginated payments" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + } + } + }, + "post": { + "operationId": "createPayment", + "tags": [ + "Payments" + ], + "summary": "Create a new payment plan", + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "parent_id", + "total_amount", + "payment_date" + ], + "properties": { + "parent_id": { + "type": "integer" + }, + "total_amount": { + "type": "number" + }, + "payment_date": { + "type": "string", + "format": "date" + }, + "number_of_installments": { + "type": "integer" + }, + "payment_type": { + "type": "string" + }, + "semester": { + "type": "string" + }, + "school_year": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "201": { + "description": "Payment created" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "422": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/payments/{id}": { + "get": { + "operationId": "getPayment", + "tags": [ + "Payments" + ], + "summary": "Get a single payment", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Payment details" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "404": { + "description": "Not found" + } + } + }, + "patch": { + "operationId": "updatePayment", + "tags": [ + "Payments" + ], + "summary": "Update payment fields", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "paid_amount": { + "type": "number" + }, + "balance": { + "type": "number" + }, + "status": { + "type": "string" + }, + "payment_method": { + "type": "string" + }, + "payment_date": { + "type": "string", + "format": "date" + }, + "check_number": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Payment updated" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + } + } + }, + "delete": { + "operationId": "deletePayment", + "tags": [ + "Payments" + ], + "summary": "Delete a payment", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Payment deleted" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "404": { + "description": "Not found" + } + } + } + }, + "/api/v1/payments/parent/{id}": { + "get": { + "operationId": "getPaymentsByParent", + "tags": [ + "Payments" + ], + "summary": "Get all payments for a specific parent", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Parent payments" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + } + } + } + }, + "/api/v1/payments/manual-pay-search": { + "get": { + "operationId": "searchManualPayment", + "tags": [ + "Payments" + ], + "summary": "Search for parent by email or phone and retrieve payment data", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "search_term", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Payment search results" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + } + } + } + }, + "/api/v1/payments/manual-pay-update": { + "post": { + "operationId": "updateManualPayment", + "tags": [ + "Payments" + ], + "summary": "Record a manual payment", + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "invoice_id", + "amount", + "payment_method" + ], + "properties": { + "invoice_id": { + "type": "integer" + }, + "amount": { + "type": "number" + }, + "payment_method": { + "type": "string", + "enum": [ + "cash", + "check", + "card" + ] + }, + "check_number": { + "type": "string" + }, + "payment_date": { + "type": "string", + "format": "date" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Payment recorded" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "422": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/users/with-roles": { + "get": { + "operationId": "listUsersWithRoles", + "tags": [ + "Users" + ], + "summary": "List users with their roles", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Users with roles" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + } + } + } + }, + "/api/v1/users/{id}": { + "patch": { + "operationId": "updateUser", + "tags": [ + "Users" + ], + "summary": "Update user account/profile", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "firstname": { + "type": "string" + }, + "lastname": { + "type": "string" + }, + "email": { + "type": "string", + "format": "email" + }, + "cellphone": { + "type": "string" + }, + "address_street": { + "type": "string" + }, + "city": { + "type": "string" + }, + "state": { + "type": "string" + }, + "zip": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "User updated" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + } + } + }, + "delete": { + "operationId": "deleteUser", + "tags": [ + "Users" + ], + "summary": "Delete a user", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "User deleted" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "404": { + "description": "Not found" + } + } + } + }, + "/api/v1/users/select-role": { + "post": { + "operationId": "selectUserRole", + "tags": [ + "Users" + ], + "summary": "Select and update the logged-in user's role", + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "role" + ], + "properties": { + "role": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Role selected" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + } + } + } + }, + "/api/v1/user/profile": { + "get": { + "operationId": "getUserProfile", + "tags": [ + "Users" + ], + "summary": "Get the signed-in user's profile", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "User profile" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + } + } + }, + "patch": { + "operationId": "updateUserProfile", + "tags": [ + "Users" + ], + "summary": "Update user profile", + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "firstname": { + "type": "string" + }, + "lastname": { + "type": "string" + }, + "cellphone": { + "type": "string" + }, + "address_street": { + "type": "string" + }, + "apt": { + "type": "string" + }, + "city": { + "type": "string" + }, + "state": { + "type": "string" + }, + "zip": { + "type": "string" + }, + "gender": { + "type": "string" + }, + "email": { + "type": "string", + "format": "email" + }, + "password": { + "type": "string" + }, + "current_password": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Profile updated" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "422": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/teachers": { + "get": { + "operationId": "listTeachers", + "tags": [ + "Teachers" + ], + "summary": "List teachers and teacher assistants", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "page", + "in": "query", + "schema": { + "type": "integer", + "default": 1 + } + }, + { + "name": "per_page", + "in": "query", + "schema": { + "type": "integer", + "default": 20 + } + } + ], + "responses": { + "200": { + "description": "Paginated teachers" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + } + } + } + }, + "/api/v1/teachers/{id}": { + "get": { + "operationId": "getTeacher", + "tags": [ + "Teachers" + ], + "summary": "Get teacher details with class assignments", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Teacher details" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "404": { + "description": "Not found" + } + } + } + }, + "/api/v1/teachers/{id}/classes": { + "get": { + "operationId": "getTeacherClasses", + "tags": [ + "Teachers" + ], + "summary": "Get class sections assigned to a teacher", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Teacher classes" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + } + } + } + }, + "/api/v1/teachers/class-view": { + "get": { + "operationId": "getTeacherClassView", + "tags": [ + "Teachers" + ], + "summary": "Get comprehensive class view data for logged-in teacher", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Class view data" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + } + } + } + }, + "/api/v1/teachers/absence-form": { + "get": { + "operationId": "getTeacherAbsenceForm", + "tags": [ + "Teachers" + ], + "summary": "Get absence form data for logged-in teacher", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Absence form data" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + } + } + } + }, + "/api/v1/teachers/absence": { + "post": { + "operationId": "submitTeacherAbsence", + "tags": [ + "Teachers" + ], + "summary": "Submit an absence/vacation request", + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "dates", + "reason" + ], + "properties": { + "dates": { + "type": "array", + "items": { + "type": "string", + "format": "date" + } + }, + "reason": { + "type": "string" + }, + "reason_type": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Absence submitted" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "422": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/whatsapp": { + "get": { + "operationId": "getWhatsAppData", + "tags": [ + "WhatsApp" + ], + "summary": "Get initial data for WhatsApp management page", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "WhatsApp management data" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + } + } + } + }, + "/api/v1/whatsapp/links": { + "get": { + "operationId": "listWhatsAppLinks", + "tags": [ + "WhatsApp" + ], + "summary": "List WhatsApp invite links", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "WhatsApp links" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + } + } + }, + "post": { + "operationId": "saveWhatsAppLink", + "tags": [ + "WhatsApp" + ], + "summary": "Create or update invite link for a class section", + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "class_section_id", + "invite_link" + ], + "properties": { + "class_section_id": { + "type": "integer" + }, + "invite_link": { + "type": "string" + }, + "active": { + "type": "integer", + "enum": [ + 0, + 1 + ] + }, + "class_section_name": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Link saved" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "422": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/whatsapp/send-invites": { + "post": { + "operationId": "sendWhatsAppInvites", + "tags": [ + "WhatsApp" + ], + "summary": "Send WhatsApp invites in three modes", + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "mode" + ], + "properties": { + "mode": { + "type": "string", + "enum": [ + "parents", + "class", + "all" + ] + }, + "parent_ids": { + "type": "array", + "items": { + "type": "integer" + } + }, + "class_section_id": { + "type": "integer" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Invites sent" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "422": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/whatsapp/parent-contacts": { + "get": { + "operationId": "getWhatsAppParentContacts", + "tags": [ + "WhatsApp" + ], + "summary": "Get flat list of all parent contacts", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "school_year", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "semester", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Parent contacts" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + } + } + } + }, + "/api/v1/whatsapp/parent-contacts-by-class": { + "get": { + "operationId": "getWhatsAppParentContactsByClass", + "tags": [ + "WhatsApp" + ], + "summary": "Get parent contacts grouped by class section", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "school_year", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "semester", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Parent contacts by class" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + } + } + } + }, + "/api/v1/whatsapp/memberships": { + "post": { + "operationId": "updateWhatsAppMembership", + "tags": [ + "WhatsApp" + ], + "summary": "Update WhatsApp group membership flags", + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "class_section_id" + ], + "properties": { + "class_section_id": { + "type": "integer" + }, + "primary_id": { + "type": "integer" + }, + "second_id": { + "type": "integer" + }, + "primary_member": { + "type": [ + "string", + "integer" + ] + }, + "second_member": { + "type": [ + "string", + "integer" + ] + }, + "school_year": { + "type": "string" + }, + "semester": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Membership updated" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "422": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/health": { + "get": { + "operationId": "healthCheck", + "tags": [ + "Health" + ], + "summary": "Health check endpoint", + "responses": { + "200": { + "description": "Health status" + }, + "503": { + "description": "Service unavailable" + } + }, + "security": [] + } + }, + "/api/v1/landing": { + "get": { + "operationId": "getLandingPage", + "tags": [ + "Landing" + ], + "summary": "Get role-based dashboard data", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Landing page data" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + } + } + } + }, + "/api/v1/landing/data": { + "get": { + "operationId": "getLandingPageData", + "tags": [ + "Landing" + ], + "summary": "Get basic landing page bootstrap data", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Landing page data" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + } + } + } + }, + "/api/v1/staff": { + "get": { + "operationId": "listStaff", + "tags": [ + "Staff" + ], + "summary": "List staff members", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "page", + "in": "query", + "schema": { + "type": "integer", + "default": 1 + } + }, + { + "name": "per_page", + "in": "query", + "schema": { + "type": "integer", + "default": 20 + } + }, + { + "name": "role", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Paginated staff" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + } + } + }, + "post": { + "operationId": "createStaff", + "tags": [ + "Staff" + ], + "summary": "Create a new staff member", + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "firstname", + "lastname", + "email", + "role_name" + ], + "properties": { + "firstname": { + "type": "string" + }, + "lastname": { + "type": "string" + }, + "email": { + "type": "string", + "format": "email" + }, + "phone": { + "type": "string" + }, + "role_name": { + "type": "string" + }, + "school_year": { + "type": "string" + }, + "status": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "201": { + "description": "Staff created" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "422": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/staff/{id}": { + "get": { + "operationId": "getStaff", + "tags": [ + "Staff" + ], + "summary": "Get a single staff member", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Staff details" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "404": { + "description": "Not found" + } + } + }, + "patch": { + "operationId": "updateStaff", + "tags": [ + "Staff" + ], + "summary": "Update a staff member", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "firstname": { + "type": "string" + }, + "lastname": { + "type": "string" + }, + "email": { + "type": "string", + "format": "email" + }, + "phone": { + "type": "string" + }, + "role_name": { + "type": "string" + }, + "school_year": { + "type": "string" + }, + "status": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Staff updated" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + } + } + }, + "delete": { + "operationId": "deleteStaff", + "tags": [ + "Staff" + ], + "summary": "Delete a staff member", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Staff deleted" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "404": { + "description": "Not found" + } + } + } + }, + "/api/v1/suppliers": { + "get": { + "operationId": "listSuppliers", + "tags": [ + "Suppliers" + ], + "summary": "List suppliers", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "page", + "in": "query", + "schema": { + "type": "integer", + "default": 1 + } + }, + { + "name": "per_page", + "in": "query", + "schema": { + "type": "integer", + "default": 20 + } + }, + { + "name": "q", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Paginated suppliers" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + } + } + }, + "post": { + "operationId": "createSupplier", + "tags": [ + "Suppliers" + ], + "summary": "Create a new supplier", + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + }, + "email": { + "type": "string", + "format": "email" + }, + "phone": { + "type": "string" + }, + "address": { + "type": "string" + }, + "notes": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "201": { + "description": "Supplier created" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "422": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/suppliers/{id}": { + "get": { + "operationId": "getSupplier", + "tags": [ + "Suppliers" + ], + "summary": "Get a single supplier", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Supplier details" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "404": { + "description": "Not found" + } + } + }, + "patch": { + "operationId": "updateSupplier", + "tags": [ + "Suppliers" + ], + "summary": "Update a supplier", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "email": { + "type": "string", + "format": "email" + }, + "phone": { + "type": "string" + }, + "address": { + "type": "string" + }, + "notes": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Supplier updated" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + } + } + }, + "delete": { + "operationId": "deleteSupplier", + "tags": [ + "Suppliers" + ], + "summary": "Delete a supplier", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Supplier deleted" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "404": { + "description": "Not found" + } + } + } + }, + "/api/v1/communications": { + "get": { + "operationId": "listCommunications", + "tags": [ + "Communications" + ], + "summary": "List communications", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "sender_id", + "in": "query", + "schema": { + "type": "integer" + } + }, + { + "name": "receiver_id", + "in": "query", + "schema": { + "type": "integer" + } + }, + { + "name": "page", + "in": "query", + "schema": { + "type": "integer", + "default": 1 + } + }, + { + "name": "per_page", + "in": "query", + "schema": { + "type": "integer", + "default": 20 + } + } + ], + "responses": { + "200": { + "description": "Paginated communications" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + } + } + }, + "post": { + "operationId": "sendCommunication", + "tags": [ + "Communications" + ], + "summary": "Send a communication", + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "sender_id", + "receiver_id", + "subject", + "message" + ], + "properties": { + "sender_id": { + "type": "integer" + }, + "receiver_id": { + "type": "integer" + }, + "subject": { + "type": "string" + }, + "message": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "201": { + "description": "Communication sent" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "422": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/communications/{id}": { + "delete": { + "operationId": "deleteCommunication", + "tags": [ + "Communications" + ], + "summary": "Delete a communication", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Communication deleted" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "404": { + "description": "Not found" + } + } + } + }, + "/api/v1/configuration": { + "get": { + "operationId": "listConfiguration", + "tags": [ + "Configuration" + ], + "summary": "List configuration entries", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "key", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "page", + "in": "query", + "schema": { + "type": "integer", + "default": 1 + } + }, + { + "name": "per_page", + "in": "query", + "schema": { + "type": "integer", + "default": 20 + } + } + ], + "responses": { + "200": { + "description": "Paginated configuration" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + } + } + }, + "post": { + "operationId": "createConfiguration", + "tags": [ + "Configuration" + ], + "summary": "Create a configuration entry", + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "config_key", + "config_value" + ], + "properties": { + "config_key": { + "type": "string" + }, + "config_value": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "201": { + "description": "Configuration created" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "422": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/configuration/{id}": { + "get": { + "operationId": "getConfiguration", + "tags": [ + "Configuration" + ], + "summary": "Get a configuration entry", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Configuration details" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "404": { + "description": "Not found" + } + } + }, + "patch": { + "operationId": "updateConfiguration", + "tags": [ + "Configuration" + ], + "summary": "Update a configuration entry", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "config_key": { + "type": "string" + }, + "config_value": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Configuration updated" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + } + } + }, + "delete": { + "operationId": "deleteConfiguration", + "tags": [ + "Configuration" + ], + "summary": "Delete a configuration entry", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Configuration deleted" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "404": { + "description": "Not found" + } + } + } + }, + "/api/v1/configuration/key/{config_key}": { + "get": { + "operationId": "getConfigurationByKey", + "tags": [ + "Configuration" + ], + "summary": "Get configuration by key", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "config_key", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Configuration value" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "404": { + "description": "Not found" + } + } + } + }, + "/api/v1/discounts": { + "get": { + "operationId": "listDiscounts", + "tags": [ + "Discounts" + ], + "summary": "List discount vouchers", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "active", + "in": "query", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "List of discounts" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + } + } + } + }, + "/api/v1/discounts/{code}": { + "get": { + "operationId": "getDiscountByCode", + "tags": [ + "Discounts" + ], + "summary": "Get discount by code", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "code", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Discount details" + }, + "400": { + "description": "Max usage reached" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "404": { + "description": "Not found or inactive" + } + } + } + }, + "/api/v1/discounts/apply": { + "post": { + "operationId": "applyDiscount", + "tags": [ + "Discounts" + ], + "summary": "Apply a discount code to an invoice", + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "code", + "invoice_id" + ], + "properties": { + "code": { + "type": "string" + }, + "invoice_id": { + "type": "integer" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Discount applied" + }, + "400": { + "description": "Invalid discount or already applied" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "500": { + "description": "Server error" + } + } + } + }, + "/api/v1/emergency-contacts": { + "get": { + "operationId": "listEmergencyContacts", + "tags": [ + "Emergency Contacts" + ], + "summary": "List emergency contacts", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "parent_id", + "in": "query", + "schema": { + "type": "integer" + } + }, + { + "name": "page", + "in": "query", + "schema": { + "type": "integer", + "default": 1 + } + }, + { + "name": "per_page", + "in": "query", + "schema": { + "type": "integer", + "default": 20 + } + } + ], + "responses": { + "200": { + "description": "Paginated emergency contacts" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + } + } + }, + "post": { + "operationId": "createEmergencyContact", + "tags": [ + "Emergency Contacts" + ], + "summary": "Create an emergency contact", + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "parent_id", + "emergency_contact_name", + "cellphone", + "relation" + ], + "properties": { + "parent_id": { + "type": "integer" + }, + "emergency_contact_name": { + "type": "string" + }, + "cellphone": { + "type": "string" + }, + "relation": { + "type": "string" + }, + "email": { + "type": "string", + "format": "email" + } + } + } + } + } + }, + "responses": { + "201": { + "description": "Emergency contact created" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "422": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/emergency-contacts/{id}": { + "get": { + "operationId": "getEmergencyContact", + "tags": [ + "Emergency Contacts" + ], + "summary": "Get a single emergency contact", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Emergency contact details" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "404": { + "description": "Not found" + } + } + }, + "patch": { + "operationId": "updateEmergencyContact", + "tags": [ + "Emergency Contacts" + ], + "summary": "Update an emergency contact", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "emergency_contact_name": { + "type": "string" + }, + "cellphone": { + "type": "string" + }, + "email": { + "type": "string", + "format": "email" + }, + "relation": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Emergency contact updated" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + } + } + }, + "delete": { + "operationId": "deleteEmergencyContact", + "tags": [ + "Emergency Contacts" + ], + "summary": "Delete an emergency contact", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Emergency contact deleted" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "404": { + "description": "Not found" + } + } + } + }, + "/api/v1/events": { + "get": { + "operationId": "listEvents", + "tags": [ + "Events" + ], + "summary": "List events", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "school_year", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "upcoming", + "in": "query", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "List of events" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + } + } + }, + "post": { + "operationId": "createEvent", + "tags": [ + "Events" + ], + "summary": "Create a new event", + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "event_name", + "amount", + "expiration_date" + ], + "properties": { + "event_name": { + "type": "string" + }, + "amount": { + "type": "number" + }, + "expiration_date": { + "type": "string", + "format": "date" + }, + "description": { + "type": "string" + }, + "semester": { + "type": "string" + }, + "school_year": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "201": { + "description": "Event created" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "422": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/events/{id}": { + "get": { + "operationId": "getEvent", + "tags": [ + "Events" + ], + "summary": "Get a single event", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Event details" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "404": { + "description": "Not found" + } + } + }, + "patch": { + "operationId": "updateEvent", + "tags": [ + "Events" + ], + "summary": "Update an event", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "event_name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "amount": { + "type": "number" + }, + "expiration_date": { + "type": "string", + "format": "date" + }, + "semester": { + "type": "string" + }, + "school_year": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Event updated" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + } + } + }, + "delete": { + "operationId": "deleteEvent", + "tags": [ + "Events" + ], + "summary": "Delete an event", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Event deleted" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "404": { + "description": "Not found" + } + } + } + }, + "/api/v1/expenses": { + "get": { + "operationId": "listExpenses", + "tags": [ + "Expenses" + ], + "summary": "List expenses", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "status", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "category", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "page", + "in": "query", + "schema": { + "type": "integer", + "default": 1 + } + }, + { + "name": "per_page", + "in": "query", + "schema": { + "type": "integer", + "default": 20 + } + } + ], + "responses": { + "200": { + "description": "Paginated expenses" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + } + } + }, + "post": { + "operationId": "createExpense", + "tags": [ + "Expenses" + ], + "summary": "Create an expense", + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "category", + "amount", + "description", + "date_of_purchase" + ], + "properties": { + "category": { + "type": "string" + }, + "amount": { + "type": "number" + }, + "description": { + "type": "string" + }, + "date_of_purchase": { + "type": "string", + "format": "date" + } + } + } + } + } + }, + "responses": { + "201": { + "description": "Expense created" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "422": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/expenses/{id}": { + "get": { + "operationId": "getExpense", + "tags": [ + "Expenses" + ], + "summary": "Get a single expense", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Expense details" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "404": { + "description": "Not found" + } + } + }, + "patch": { + "operationId": "updateExpense", + "tags": [ + "Expenses" + ], + "summary": "Update an expense", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "category": { + "type": "string" + }, + "amount": { + "type": "number" + }, + "description": { + "type": "string" + }, + "date_of_purchase": { + "type": "string", + "format": "date" + }, + "status": { + "type": "string" + }, + "status_reason": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Expense updated" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + } + } + }, + "delete": { + "operationId": "deleteExpense", + "tags": [ + "Expenses" + ], + "summary": "Delete an expense", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Expense deleted" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "404": { + "description": "Not found" + } + } + } + }, + "/api/v1/extra-charges": { + "get": { + "operationId": "listExtraCharges", + "tags": [ + "Extra Charges" + ], + "summary": "List extra charges", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "school_year", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "semester", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "status", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "q", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "page", + "in": "query", + "schema": { + "type": "integer", + "default": 1 + } + }, + { + "name": "per_page", + "in": "query", + "schema": { + "type": "integer", + "default": 20 + } + } + ], + "responses": { + "200": { + "description": "Paginated extra charges" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + } + } + }, + "post": { + "operationId": "createExtraCharge", + "tags": [ + "Extra Charges" + ], + "summary": "Create an extra charge", + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "parent_id", + "title", + "amount", + "charge_type" + ], + "properties": { + "parent_id": { + "type": "integer" + }, + "title": { + "type": "string" + }, + "amount": { + "type": "number" + }, + "charge_type": { + "type": "string", + "enum": [ + "add", + "deduct" + ] + }, + "due_date": { + "type": "string", + "format": "date" + }, + "invoice_id": { + "type": "integer" + }, + "description": { + "type": "string" + }, + "school_year": { + "type": "string" + }, + "semester": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "201": { + "description": "Extra charge created" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "422": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/extra-charges/{id}": { + "get": { + "operationId": "getExtraCharge", + "tags": [ + "Extra Charges" + ], + "summary": "Get a single extra charge", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Extra charge details" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "404": { + "description": "Not found" + } + } + }, + "patch": { + "operationId": "updateExtraCharge", + "tags": [ + "Extra Charges" + ], + "summary": "Update an extra charge", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "amount": { + "type": "number" + }, + "due_date": { + "type": "string", + "format": "date" + }, + "charge_type": { + "type": "string", + "enum": [ + "add", + "deduct" + ] + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Extra charge updated" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + } + } + }, + "delete": { + "operationId": "deleteExtraCharge", + "tags": [ + "Extra Charges" + ], + "summary": "Delete an extra charge", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Extra charge deleted" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "404": { + "description": "Not found" + } + } + } + }, + "/api/v1/messages": { + "get": { + "operationId": "listMessages", + "tags": [ + "Messages" + ], + "summary": "List messages", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "type", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "sent", + "inbox" + ] + } + }, + { + "name": "read", + "in": "query", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "Messages" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + } + } + }, + "post": { + "operationId": "sendMessage", + "tags": [ + "Messages" + ], + "summary": "Send a message", + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "subject", + "message" + ], + "properties": { + "recipient_id": { + "type": "integer" + }, + "recipient_role": { + "type": "string" + }, + "subject": { + "type": "string" + }, + "message": { + "type": "string" + }, + "priority": { + "type": "string", + "enum": [ + "normal", + "high", + "low" + ] + }, + "status": { + "type": "string", + "enum": [ + "sent", + "draft" + ] + } + } + } + } + } + }, + "responses": { + "201": { + "description": "Message sent" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "422": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/messages/{id}": { + "get": { + "operationId": "getMessage", + "tags": [ + "Messages" + ], + "summary": "Get a single message", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Message details" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "404": { + "description": "Not found" + } + } + } + }, + "/api/v1/messages/inbox": { + "get": { + "operationId": "getInboxMessages", + "tags": [ + "Messages" + ], + "summary": "Get inbox messages", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Inbox messages" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + } + } + } + }, + "/api/v1/messages/sent": { + "get": { + "operationId": "getSentMessages", + "tags": [ + "Messages" + ], + "summary": "Get sent messages", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Sent messages" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + } + } + } + }, + "/api/v1/notifications": { + "get": { + "operationId": "listNotifications", + "tags": [ + "Notifications" + ], + "summary": "List notifications", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "read", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "page", + "in": "query", + "schema": { + "type": "integer", + "default": 1 + } + }, + { + "name": "per_page", + "in": "query", + "schema": { + "type": "integer", + "default": 20 + } + } + ], + "responses": { + "200": { + "description": "Paginated notifications" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + } + } + } + }, + "/api/v1/notifications/{id}": { + "get": { + "operationId": "getNotification", + "tags": [ + "Notifications" + ], + "summary": "Get a single notification", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Notification details" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "404": { + "description": "Not found" + } + } + } + }, + "/api/v1/notifications/{id}/read": { + "post": { + "operationId": "markNotificationRead", + "tags": [ + "Notifications" + ], + "summary": "Mark a notification as read", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Notification marked as read" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + } + } + } + }, + "/api/v1/settings": { + "get": { + "operationId": "getSettings", + "tags": [ + "Settings" + ], + "summary": "Get grouped configuration values", + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Settings" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + } + } + }, + "patch": { + "operationId": "updateSettings", + "tags": [ + "Settings" + ], + "summary": "Update settings", + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "school_id": { + "type": "string" + }, + "school_year": { + "type": "string" + }, + "firstname": { + "type": "string" + }, + "lastname": { + "type": "string" + }, + "dob": { + "type": "string", + "format": "date" + }, + "age": { + "type": "integer" + }, + "gender": { + "type": "string" + }, + "is_active": { + "type": "boolean" + }, + "registration_grade": { + "type": "string" + }, + "is_new": { + "type": "boolean" + }, + "photo_consent": { + "type": "boolean" + }, + "parent_id": { + "type": "integer" + }, + "registration_date": { + "type": "string", + "format": "date" + }, + "tuition_paid": { + "type": "boolean" + }, + "semester": { + "type": "string" + }, + "year_of_registration": { + "type": "integer" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Settings updated" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + } + } + } + }, + "/api/v1/scores/homework": { + "get": { + "operationId": "getHomeworkScores", + "tags": [ + "Scores" + ], + "summary": "HomeworkController::index", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "class_section_id", + "in": "query", + "required": true, + "schema": { + "type": "integer", + "minimum": 1 + } + }, + { + "name": "semester", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "school_year", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "422": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/scores/comments": { + "get": { + "operationId": "getScoreComments", + "tags": [ + "Scores" + ], + "summary": "ScoreCommentController::index", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "class_section_id", + "in": "query", + "required": true, + "schema": { + "type": "integer", + "minimum": 1 + } + }, + { + "name": "semester", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "school_year", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "422": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/scores/projects": { + "get": { + "operationId": "getProjectScores", + "tags": [ + "Scores" + ], + "summary": "ProjectController::index", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "class_section_id", + "in": "query", + "required": true, + "schema": { + "type": "integer", + "minimum": 1 + } + }, + { + "name": "semester", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "school_year", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "422": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/scores/quizzes": { + "get": { + "operationId": "getQuizScores", + "tags": [ + "Scores" + ], + "summary": "QuizController::index", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "class_section_id", + "in": "query", + "required": true, + "schema": { + "type": "integer", + "minimum": 1 + } + }, + { + "name": "semester", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "school_year", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "422": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/scores/midterm": { + "get": { + "operationId": "getMidtermScores", + "tags": [ + "Scores" + ], + "summary": "MidtermController::index", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "class_section_id", + "in": "query", + "required": true, + "schema": { + "type": "integer", + "minimum": 1 + } + }, + { + "name": "semester", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "school_year", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "422": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/scores/final": { + "get": { + "operationId": "getFinalScores", + "tags": [ + "Scores" + ], + "summary": "FinalController::index", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "class_section_id", + "in": "query", + "required": true, + "schema": { + "type": "integer", + "minimum": 1 + } + }, + { + "name": "semester", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "school_year", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "422": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/scores/participation": { + "get": { + "operationId": "getParticipationScores", + "tags": [ + "Scores" + ], + "summary": "ParticipationController::index", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "class_section_id", + "in": "query", + "required": true, + "schema": { + "type": "integer", + "minimum": 1 + } + }, + { + "name": "semester", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "school_year", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "422": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/scores/projects/by-year": { + "get": { + "operationId": "getProjectScoresByYear", + "tags": [ + "Scores" + ], + "summary": "ProjectController::bySchoolYear", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "school_year", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "422": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/scores/quizzes/by-year": { + "get": { + "operationId": "getQuizScoresByYear", + "tags": [ + "Scores" + ], + "summary": "QuizController::bySchoolYear", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "school_year", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "422": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/scores/homework/by-year": { + "get": { + "operationId": "getHomeworkScoresByYear", + "tags": [ + "Scores" + ], + "summary": "HomeworkController::bySchoolYear", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "school_year", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "422": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/scores/midterm/by-year": { + "get": { + "operationId": "getMidtermScoresByYear", + "tags": [ + "Scores" + ], + "summary": "MidtermController::bySchoolYear", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "school_year", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "422": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/scores/final/by-year": { + "get": { + "operationId": "getFinalScoresByYear", + "tags": [ + "Scores" + ], + "summary": "FinalController::bySchoolYear", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "school_year", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "422": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/scores/participation/by-year": { + "get": { + "operationId": "getParticipationScoresByYear", + "tags": [ + "Scores" + ], + "summary": "ParticipationController::bySchoolYear", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "school_year", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "422": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/api/v1/students/{studentId}": { + "get": { + "operationId": "getStudent", + "tags": [ + "Students" + ], + "summary": "StudentController::show", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "studentId", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "404": { + "description": "Not found" + } + } + }, + "patch": { + "operationId": "updateStudent", + "tags": [ + "Students" + ], + "summary": "StudentController::update", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "studentId", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "responses": { + "200": { + "description": "OK" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "422": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + }, + "delete": { + "operationId": "deactivateStudent", + "tags": [ + "Students" + ], + "summary": "StudentController::destroy", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "studentId", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "404": { + "description": "Not found" + } + } + } + }, + "/api/v1/students/{studentId}/classes": { + "get": { + "operationId": "getStudentClasses", + "tags": [ + "Students" + ], + "summary": "StudentController::classes", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "studentId", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "name": "school_year", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + } + } + }, + "post": { + "operationId": "assignStudentClasses", + "tags": [ + "Students" + ], + "summary": "StudentController::assignClassForStudent", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "studentId", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "student_id", + "class_section_id" + ], + "properties": { + "student_id": { + "type": "integer" + }, + "class_section_id": { + "type": "integer" + }, + "class_section_ids": { + "type": "array", + "items": { + "type": "integer" + } + }, + "is_event_only": { + "type": "boolean" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "OK" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "422": { + "description": "Validation error" + } + } + } + }, + "/api/v1/students/{studentId}/classes/{classSectionId}": { + "delete": { + "operationId": "removeStudentClass", + "tags": [ + "Students" + ], + "summary": "StudentController::removeClassForStudent", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "studentId", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "name": "classSectionId", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "422": { + "description": "Validation error" + } + } + } + }, + "/api/v1/students/{studentId}/promote": { + "post": { + "operationId": "promoteStudent", + "tags": [ + "Students" + ], + "summary": "StudentController::promote", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "studentId", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "name", + "cellphone" + ], + "properties": { + "name": { + "type": "string" + }, + "cellphone": { + "type": "string" + }, + "email": { + "type": "string", + "format": "email" + }, + "relation": { + "type": "string" + }, + "semester": { + "type": "string" + }, + "school_year": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "OK" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "422": { + "description": "Validation error" + } + } + } + }, + "/api/v1/students/{studentId}/attendance": { + "get": { + "operationId": "getStudentAttendance", + "tags": [ + "Students" + ], + "summary": "StudentController::attendance", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "studentId", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "name": "school_year", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "semester", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + } + } + } + }, + "/api/v1/students/{studentId}/incidents": { + "get": { + "operationId": "getStudentIncidents", + "tags": [ + "Students" + ], + "summary": "StudentController::incidents", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "studentId", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "name": "school_year", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "semester", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + } + } + } + }, + "/api/v1/students/{studentId}/scores": { + "get": { + "operationId": "getStudentScores", + "tags": [ + "Students" + ], + "summary": "StudentController::scores", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "studentId", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "name": "school_year", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "semester", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + } + } + } + }, + "/api/v1/students/{studentId}/notes": { + "get": { + "operationId": "getStudentNotes", + "tags": [ + "Students" + ], + "summary": "StudentController::notes", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "studentId", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + } + } + } + }, + "/api/v1/students/{studentId}/parents": { + "get": { + "operationId": "getStudentParents", + "tags": [ + "Students" + ], + "summary": "StudentController::parents", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "studentId", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "404": { + "description": "Not found" + } + } + } + }, + "/api/v1/students/{studentId}/emergency-contacts": { + "get": { + "operationId": "getStudentEmergencyContacts", + "tags": [ + "Students" + ], + "summary": "StudentController::emergencyContacts", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "studentId", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + } + } + }, + "post": { + "operationId": "addStudentEmergencyContact", + "tags": [ + "Students" + ], + "summary": "StudentController::addEmergencyContact", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "studentId", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "responses": { + "201": { + "description": "Created" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "422": { + "description": "Validation error" + } + } + } + }, + "/api/v1/students/{studentId}/emergency-contacts/{contactId}": { + "patch": { + "operationId": "updateStudentEmergencyContact", + "tags": [ + "Students" + ], + "summary": "StudentController::updateEmergencyContact", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "studentId", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "name": "contactId", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "responses": { + "200": { + "description": "OK" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "422": { + "description": "Validation error" + } + } + } + }, + "/api/v1/students/{studentId}/badge_scan": { + "post": { + "operationId": "updateStudentBadgeScan", + "tags": [ + "Students" + ], + "summary": "StudentController::updateBadgeScan", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "studentId", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + }, + "responses": { + "200": { + "description": "OK" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "422": { + "description": "Validation error" + } + } + } + }, + "/api/v1/students/{studentId}/photo": { + "get": { + "operationId": "getStudentPhoto", + "tags": [ + "Students" + ], + "summary": "StudentController::photo", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "studentId", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "404": { + "description": "Not found" + } + } + }, + "post": { + "operationId": "uploadStudentPhoto", + "tags": [ + "Students" + ], + "summary": "StudentController::uploadPhoto", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "studentId", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "photo": { + "type": "string", + "format": "binary" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "OK" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "422": { + "description": "Validation error" + } + } + } + }, + "/api/v1/students/{studentId}/report-card": { + "get": { + "operationId": "getStudentReportCard", + "tags": [ + "Students" + ], + "summary": "ReportCardsController::studentReport", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "studentId", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "422": { + "description": "Validation error" + } + } + } + }, + "/api/v1/students/{studentId}/score-card": { + "get": { + "operationId": "getStudentScoreCard", + "tags": [ + "Students" + ], + "summary": "StudentController::scoreCard", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "studentId", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + } + } + } + }, + "/api/v1/students/assign-class": { + "post": { + "operationId": "assignClass", + "tags": [ + "Students" + ], + "summary": "StudentController::assignClass", + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "student_id", + "class_section_id" + ], + "properties": { + "student_id": { + "type": "integer" + }, + "class_section_id": { + "type": "array", + "items": { + "type": "integer" + } + }, + "is_event_only": { + "type": "boolean" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "OK" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "422": { + "description": "Validation error" + } + } + } + }, + "/api/v1/students/remove-class": { + "post": { + "operationId": "removeClass", + "tags": [ + "Students" + ], + "summary": "StudentController::removeClass", + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "student_id", + "class_section_id" + ], + "properties": { + "student_id": { + "type": "integer" + }, + "class_section_id": { + "type": "integer" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "OK" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "422": { + "description": "Validation error" + } + } + } + }, + "/api/v1/students/set-active": { + "post": { + "operationId": "setActive", + "tags": [ + "Students" + ], + "summary": "StudentController::setActive", + "security": [ + { + "bearerAuth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "student_id", + "is_active" + ], + "properties": { + "student_id": { + "type": "integer" + }, + "is_active": { + "type": "boolean" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "OK" + }, + "401": { + "$ref": "#/components/responses/UnauthorizedError" + }, + "422": { + "description": "Validation error" + } + } + } + }, + "/api/v1/time": { + "get": { + "operationId": "getServerTime", + "tags": [ + "Time" + ], + "summary": "Get server timestamp and timezone", + "responses": { + "200": { + "description": "Server time information" + } + }, + "security": [] + } } + } } diff --git a/storage/app/qrcodes/wa_439ca9047bd5e69a212f1371da361efe.png b/storage/app/qrcodes/wa_439ca9047bd5e69a212f1371da361efe.png new file mode 100644 index 0000000000000000000000000000000000000000..673c88278bf713817a281f52230c5cf9c246f8ca GIT binary patch literal 1423 zcmZ9MeN0+c7{-}Nx3wMZs?kiSYZe+!YnxV0hbt|c%0$+U)+rc2YudG_{h)ZGsQ8g= ztC?T4XmnLCn(3kpkWA4kk$_t%T5O>$r3OV=M7>)TNM*uA!CkvWx&CqPNzS?Vx$pCy z-+8JoP|4^$m^~;I3Qb8MeGSjf&GD`u{LH&$ZHK4bw-QnT6l$MpbG+3Uz7LB+?TVq0 zVv>tM>-29o=R;ASJyQrbHpc(zI7h~PF`~vpV#nC{6VBXMHrw zKa4u7qJYs=0b#7!!&FdJ)v&o}Yv&zumlB7SqlYrs&%|z`DBgS4lEHteZcO!k8Jm_U zn{-K@N_AfKs!Tt5vc^}ZA^d2{8a+0iSm3cvv^S^IPHJ`2#vKyr_dAxwdT0Ln0*iV% z$hVvx%B9rM^b+vX12?^obP6T(AFtp~}5+ z@8N%@-Z&l7Va-1p*|VcUSC!Br=OOJ(cIJEQ)Hu48|EAPR#O~A(d4Am!*j@Gj&jp~b z;s9JLLiq|LFs;5Qg)rk|t_ZDe)X08bgAiynb@tiOJO(=kEc9Q>wba(is|U-ofB3)Y zaDB%KtrU0td&F z?2DRd?!!jr+o!!hb7}T~x|~|6eLY|>>_Ox_A+%P=y?H1!yjn>SZh$wxP)uS`vAv`ZpIr0)p25N2N^0pbAP6>)6aZ& qD-?!wxtw#^#}O%kh&vcp&)(PX-8rT&*gpa%EQ<0ul_ZJH{`OxS8r6RQ literal 0 HcmV?d00001 diff --git a/storage/app/qrcodes/wa_cdc39b246c6ed974bf58c5f610ab924c.png b/storage/app/qrcodes/wa_cdc39b246c6ed974bf58c5f610ab924c.png new file mode 100644 index 0000000000000000000000000000000000000000..482e136109e115cd6a94367e235e8b1a9b27ab2c GIT binary patch literal 1427 zcmeAS@N?(olHy`uVBq!ia0vp^-$0my2}ouYyqyoEI14-?iy0XBj({-ZRBb+K1_oAF zPZ!6KiaBrZdiSZQGdMU-H~qf9s5)EIU97qBOpWNqb<48fs4sD=v9q_guXntDzr3FD zhWDc*o^vcZh1Clmsrbz~(mC0pP)OYmB)B79S^igz-R}Lz>vz0cw6kvh#JOh5|G6Ij zd1>LF{rHp)vTEM7D`MVSYnyx*|F!BW15k;7Z_nmacfK2TBO5$fTPs}s|F3zs3qNkT z+G*Zr{QTZj$<1&Jfv(t|(`k2os<-C*pO{k}pH=tR;`_TTkw;OC z0eYtL-(9uv>-j&n*ed!swgs*+|G21Q0kN13JOF)VK@-M*f{I=Kj%dybD+Q)BhKY#o+CBhBi z+cv+yF?Xx`fZ-`#y}xqK^Zko!Q&GGD4A35npU*zs@i#4-v_>lZezeE;cg;p9sqpl> zz@6W7A6e$tT&%cnQM~{4t4#A7s1BX|eMQfoD;>|*|Fo%}GkLz`&$iX53Qk_1Jip%j z&8qwVY#-aaJ-cA%#Pz6=a%^_ydbfW{^Q0^9^R?%f6_sY+_f$pk>-LXV&R^a4S>fG6 zTjO(ktMlIkHr}5A&kH*iicam_|F3m5*VRJ5d+EFLzn`8OiJVWRt-fx~cRzSJ?(yGe zh2Pe0oU_ODDQcd)yT?8C+{yB+uv+=YcTD?GbVqMcJ}wT$(6UHZPkxUO8&f8 zxDM417q{2Bt*Lrme)adc>xE%+8>?u;4`b}A_ y$K^yPSd_v_wDXt0O*;^Ul7+!V9a8!Cn|Y+TyEPwn~(`}cRl#aqL~2gA-w`+i7& zg8Q2Ux5Iw2cxXIwW`E4rIbw{5?i*yUjn3}}E_!abI=6417MClz>OU6$$HqRl=igrO z%Ll{b-|h2rdtTfGSADd5z^!;xSoWt*2JC~`L*ggVSlInGVVCRw{FC5*W|(?oki#Qy zzu*vj0Lw?a@_vB%WWeonyXUjrD>y&dzfZ5^?A)H`*Qa(jIDpgM8~$D{{ur>he|~GZ zYFBUz<~IiSr$OG}nrU3X+w1Z7XB+pIfseuMn*p<{bfu_!G0X=!c(eG2(L!{6Vc(#y z_?u7AjeCBx&ptQjke)GKnpXK}BV2Yq?a~FOAXjK`YydK-Io31sS@mdyA8fQwHtv(f z>3}ON10y5f=Y~PIJN(O?=*|}drm#Ca^KQ_R@Gm+C*fbyg%Jn=V_dt7}qhk(jgsl8= z1=FWNDz*?;Lkq!-9Cw;wO$%i$*GK+m6CQT^!% z?6st$>wh);fTzR#YvFcV)?;tKHx77eIKke%HtTveSaP`HuXY`e(LcSp!W`P-v0WXr zn#6WQEx#JRF20N;E+ewzEnxM%jf+k9nYz`@&%CpHM6qY~Hu`*4;9tD9=bjJL{y5z2SRrbAY4+ji5Z?kn@AUWfVEMky16Y`Y z`!Toe6K~}zQ8oS>anCFJ4}JTyJ;9aw*(uyRhx;c|36vt=CK zlc#tw@Hf5-UK(6|I#Ba`;GY(s%2kLL;RNW94!|E0AE8H}+qroLCj-UaUVRsyGY5JU z58uGMc(G_9X!86E7kP#RDLEhHhkOXWtGMCB_&F@r^jqL}Xs$er^Qe#RJVI_k`G{A8 z^)I2(Z@lA~-S>Q$4PL^DVS%Vo{127Z8<$vBSku_x6dq?pGCn7Ze>W(>kYW*^{mG(_ z%I^&7_lqBGrqCKWdTwJo-Op_7$PGNOyP(0_5nU$wqZKux^p{69imHFH(a9{-_7klG z1U`4i5VKH@0p7r$1P9_LVF8{}6p{Td#__TG@UU)Fv>Fp=S!P#808ldf#lQp3GmXc1 ze-4Ol4|IJxNxPVFiw&3N9Oe5wmp$iHaEg95UW5ANSnsHYHbub*}3R@ zxstlzooo&Bc2E9=eS2zr2*2iEzyQ)k)DHfHEuNIbUVM=2&n) z&fX;5!aJE`>+iUYK_{VQm@jW28_uV=4oAUR%m;n-!hWr3sJWuIJy(Rk0i0YRiib1N z;CbYFl*rB#4d?lkzpS1Qc>Q3KfiIHBDX(XUf+qM$?!VPDET89F&PK`h31%?1Y$V~Q z#+xMg+^`RqxNRJRUUV(J#&)!IWg&F2GF`3qx?|jeA1%9;_3vYwGZ5D}b4^m~>(+LW z?UIg%8adnZn|;SOMQ-5K-;F;+TYO^8$1529?|)@a;)TGSwWopn@H=rr`9`QNKS3Tf zG^^)_L@D1&jL<)$H+DC=j(0FCII6@%#!>xN*D9`2YAlv8_0NjgVuKZLAHlM<0jDjCD$Az*;QhySN2BU zaDWXRtD5L>!*yzKVIlFNupfs7&FYx4)Oe=`i|Qdz3BUiA!S(mSfBa-t88}sQ8neP# zI{NkAww$;u+tTtX-L(Bu3i+zUzSP!U>R*BPJIHA2M1<{d;Od(Ji@5VkxJO*mmXW$n&Y-J zcMK-)?48cWT&GvMT8hg|#hrl*urIk^Wy~MgCjWByne`J}S;Oj@e>JVmY+VBOm3a<2 z9J_>2-+O2F&uip|1<(5OA$VFSwvmVZbJrhNm2&F;EiBimvDWpm?hgD#by~KCwHV5l zzq&z?eQ!Q7z9jvj_y#wJW@m$kL!BC{b93N2EJ)VXb9ar;)VF$P=+JmKLYZUIcmdB% z$sd+o^klZFx*s>!{a{ewNB?gB(vjRkb4)yU&m@So|*$ZPt~k`&XL%%%UpRoJ`ftk4X;-hBs!vZkbi4ZqBMS_8@3^@0j3v zFvNpIhi@(JeQNOB488uqEEN6JhgjoUSJyo-PJ3azMkf5-;$}otz!d%N5mo&(_!d-O z<@p}Bbp2@j_nYlX1?`UA6P#M5c-$01=Xs_7meC1Hsry$euWnA)VTKg#s17hzn;?DE z%U@WQknZQ3L(!Yhvxh~ibuF8C5^su3$48Sz??`Onx=xRN6S%N$%0#_bphdJ+l25S~ z?509_{BT_k-tcOl_&T|A-M9>C3;+I(**&@sj=f8&glbCTA7Tu;iB<{v9hQu3$BSC` z$4)!y>(Ra^EriDuS&J{RX{>yvf8nOVr+C!kI99+OJH=Qp$9@Z*17V$`_V&GLkli;Z z$>NXgCUaa;R?{qU3CB(|;?i;_u(3|Mcf|PJ&vF2sG@849CZ5s zu&e-k&K!SUfK?r2I#?6^a`BUWqqcs2L-KPEjYi|}9TrEf>-EBa?4HmOu5FH3t3Aiyc=wb!Gj3b9E5|YU9l%0118N{cL9xT) zymj-3z(utdAA@t1jvX3S&%d(2u^RMY!M47RaM)Tnj!Tk&gWXPBIF8AGFS|i+nSI5= zvc~qw3Z&nq~jgBn-Kv!G%ZcGk&%6`=>OYL6@e>th6}4hbq)iog*Y9lh3}YnZW=!P zDRSA!8)pd6g?O`Iy=U~(T9ubax)LW&*d#k0-IG%%4z#(byi*bg`fvAezU+4g$7nHs4@zXh{f_Kqb2w4cwWaB~6%=dxayzl?v#SsmgJ_yV5Lj$NvmmG@mQ z7TS}{n2+ycw2IytUhE0yVtg=~vo{$Kv#;0n!Z-EymfmiYdv-CKTVwrfTEtK98Ospv zLuaAKI5mnLWRU9P(P1qur+7H)YCkXLnQ^SNc5pLNo8h10qU*iBo$OA*jX#)SN1M@u z=NYK$g_*ba(>rVwc{D$NGHf$APz6%@`TAj7HiMSlp|e(J0Pt)$6=+hfn~ojlJC@CkJ@@nij&}?{PvT~s>UuNZe4)VRZ*WT^0gk0pOW30<69Xf)+X0*sQ#bm{}Q{P9*`N94!t?Opf2_}La~U`e9Y2NXxV~GQJO01oJ}@4i_JjMcJic2zyI$ukA95+q z^A}r0RbREsC)3z9=c2{y-bk*{r=eU%ea2<%d03D2)!W(L-yfqz*}3`^>(V{nj7C|T z<*lgjsh?KWK7C$|>6;$U&adU^kostURJG7Yp109H4msNG$tUz?&~9QhwJE2U_%G=J z$49;VEC0{W|38~YL)`ji=qk7zs=BN3{FTq|4;eN0FckZ9-YPl-n|W?ECVFL+Gl=!1 z>ao<>iRhds*2CdzO{cO3>f-#njYSQi9NlN&dA)!BNj88Q5VN2sD)(rRvwLp8eYUqU zw-d87{{Ffmv09?bxqyX$W)n!=&o0NEf|0!?w7W@$f|J6u`k&qm`pl!U_t_v;rXZEoJal@|R|H+veJ`06CBYs%OLTNy(B3gjQ(I*n4)cVQBPQ#kDl0%PhPtB5GiSuB zgHttA?e-(d%$;a-KeFTarC}+a*`M^*JUxaDc7Q%-V zpDu0so3I(w#9tm1hdi?R_vVDae3D?O6Kf<9*2hY2DE?bSr*VA16=K8Tegu2bHJ1`0K($j!oR>>VrJ{ zC|Ot6vJAxXVB!3?#vQws>D9E|PPQ+Mvhq}cwe-}{+BzBe5a4{2c(9MECD+!pgo2#K z35=IJ_Pkb85vh$VXCHh(phydUOFP<@)|NE@PwgI}7I7+dPtQQGzZUB!SS1nqLhB;P zlOPrI@VDmo+BnVQk3I-p1GA!;y`&Yp$fht7o&^?6GFe;Q_rux`R(YtK7#|j>><-q*CFeFqjE3IhOT9z#b@>}r#7D0 z|FI+FYf5}_cbs=GUkDEl&-SVPR`S&p_WS2ZUcSr;;b*88z==t8`@t2;o<*K$jOP7s zC0pT{x*G2%h9&%ZW&!-$!w9amX9o0DbTGz^{DRG9)qu1(xOm;n0DSnGpfQ-CF0vIl zjraCVJov%>tsz=HUiSZ)B~()!t{KAJV z873cXexA8hM1vj*A1t_GSNfAYZi^SMje+mpJapi^)Und6?~Hn(J@*R@Svs@`JL{uU z(DZ$F!{bP@b0uAZ+M*l$Sn8Adi7B~`ca-`DX>#t>JJ6c=ip(Ers5KVyjiGv zezEITFIrw0=a2_B3vk+pN9PSRjCXfLyaO!LHp3xuZ)5dXwInO&NNoTb5sA4TMhffk z!7}*eKKyi6+ho7K9zEY59KKIlK!eaw>rx~?oAnJgm%jHoAZbsl(rJELUE=-=oYr?| zAB=b%FU*d~mQx>smUQw%U)d3OZ=5S5KD`*|j|@uo@N2MxS`W+Uk+XJ-(%Nr)89KqD zRv@cOh68_tnV|Wlq40N}+ZFjsycbOzZ}#fJ^TB7371Rh``5_9xx{T?L-OyBLN8snl zI*-$nt}ZaTZmztRJ7o=o5$#r-J;+*@w*I5b{id3-<;`{O z7x^&YRNj6Web<;^Ex*HF>r~~YusTsm_53T`<$Pa`d7a#JXV6mEMSPaPM6ZR1tB@E) zf5=(hfkF60j*v`A5OK zkylXlyu34FLzmNi>v~p7pj~7S|7!mdk%xBe!!x+dIVt25*ziCiXM!Y-=Pa%Z`r01$ zK??_j{!x^Jy^$tGqoSqfi6A$%;bWA{xuQqI452li-8#02$V5+N$r+29q+>E)lva6v z7;3Tc$g~I^>K)^Lt=WCqsZ*wWarU@)`^9&6j&}cr{fdw?v^6rpnX$hbj|2{}XQk}P zx5o1KY2dqdC%mT)3w&Fw{kV_IRqwUCWzWE|%nu&MkI=JtEO-aVvhQs>Wp8;FFNX}F zJTkJPwPqlb|9p4@aeN(q9D@SZ;r|wxan%guqw4xRI6o~j<4cvh)7mxV9 zLr|GJ4*5@A%6}dtt=(g58QgBU>28w8B(8?ahyT~Y?HXx54`W=;&>2LJ@JerH7T$|z z(1~aqGO}n}{0Oif8920683noTK9$4ld&4r%jkR{@jM{_Qrw#+RM0LIrVZClumtJCI zw7F~%-t@ZB0Q~QkjoDy1WXbAr<#Wr@VMmBkJ^K>*s~n^4&T|^)*V{HH*)6ggQ71n$ zxWdb8HK%&p1A3~@`UZE(DxgVY#4;Au9XTG@Cnr4|c!2CqZzm{F<+=~Cv@*}yGaaiB zeX$mwm% z_X=d^HlBPJq})egF4;>fA=xLz*3CwiGgU{jcFAmgGOT~((}@4PlOkl4s=R0B9ILyw zC6A1{(*Ja_T^hxnclv5MhXV|)7ghRd-1EZDoy5kd5{<7&OgGj~!o6rbuO9b|Uss-Y z8T*fGIy4$~83@sBj|@8P8`-DpJez`+%j1AaAIUkxB|mrGlzyc=a59fAKZgHV`_P-! z3|s0?V`#O`@8PYiU)&c`HAZwtr$lB1TZ~;CtM+aFHuj_Py3KWPTef{ja_~YQzrh#r zjOSV4T<;ahZ-9^RXm-aj_YI#&&e>pjK3?qrjZY>Uj~CPj88JQC2SyYBb4sk!N}20j z*_LrA*0nD9Ey|k+CwPBl?O(k@9y9F<8t|QUQNK=8H);cZp;8fPD z)rNXo=H_v2*oUX}GCgO;XIiy?%Ra$#+huy-Rbo4K-xhuQGCetJ2tN)@xXvg}jX8af z0Yd4Ukljb6?_SBaJfV61&q-vIK~NoGf7Z4?56o(Jd1dTqW9>gk@uNn`+T?4 z)jbEdUSyldJ(YGae*=qc-<{+2;>vI2E-Z@sle>_#$2NY%NMkr&Kd*|s#+ccUmmE`B zCmx#eq{?h-mce>t)y~PftUI#PNZm!y8LQxZ+S1S39kVx=y?)MjHSUS;>n`#>*+y3{ zTj>k5U^DC0&}x00wj5$|*S}aU|Dj=)%=zzwRJ^#_vj|V(bnqzPdSs&yF8cT2E*UF3C-=y>Si2bIPWbz8lod88vyO_}VWFPql?9M?;5#h1iV(%wjEs zp>o{d9Ict-UUcxBvOdK(eX@0Kl>x?P5H{HXn)D9(wmERs37ryGWu*AHj-9ZavtI9ff zokjyM;gzaV6`ZHM2AmbErulZgf6KXSHQeM1eAf>wJUR~^b9{uQQ2$f-$fbNDyUKg+ z2Olk$@m`OOvQ%fW9mCVw?|QLZzP-gczt?cLalSA}Oi4UR zT;OX79Ddj4^7`Z*Q!~GQ`wfJxPS0>>uQ>cxor*+kM+^ZRKu*Nxr?PpDB}*EZ0SE6b zdS=gn694=Dwh7$K`MJ&fH~TkZhU=bb%vjntOg-;g2ksovw2}|hzZx~sv-Ffi-sHMz zJBA0r{p}Us)p`x9n7gw*LX*FZkAQUmuDl|Y8nHT{==wkw- z_#?+9%DTBXbe&MF|L*1VhKhLB_&i#BS^Gt-A*GLsOego!!3dYL!c(!vA#%Cv^W3%T zyuMl0?>mD#d}8HX$aAQFl058N(}`pcB4bs0`mHaa{y%x!@hrF|6G0AIz8n@Kd!yuh z^SW31jAJgLlQkQM+v76dJp~($1ya^g7M-`ysSHi)x2pR{eRlPjNJy^Qp*QHGP!=U} zqSyzr$Er0cYpq?A=E;iHG|Jx}+nl@`tB*2yuI|OCZwpC~4-MwA`&4&U_j|2+F8UDh zj2uslE>9s--ZPBcNzB(~r}O;V8Tz|w+Qr^E;sT;SB%KNYba&d%BS)u0B&BRIWlz^R+EZjLOZpqu%l;Bo?~5YsqGJTMtvgX8`&cl zU%TeXO;*T!XLHxCDc~4+xMT7rj|NRY{p+bcx6XQOWh#i&wI0cBD^Me6-oM3~1#&;c z>Jg&{&B5~_{w?=5ur7fuS)6j+xv#y*byf9bB!>s`8b*;GqT^W1Wdd}N%o z$*$|Uk_UIWw};fbEO5QfZbYSAmAkmxG2r?Y9xrFcc2WEuZ_&xb8;GZm>#vn}r}}1me>&h8-wHlM8xxt_GTg&w{d=>b z#mil2O0TB$dEi60jjDHY1ucy}#?ONG@UHJ1sV8(7-VuA&<8yqlc@s_19fH+zY`6?C z0(PEzd4g%lD?5kc|j@V`oe z6{CgMQe((9SmcPnnd5C!Deww<%>HcP6m|HY5BtE#Ctr8|stJh4a*iX%F7os|YICl; zG^~wtbzsMRy1<6kSRuXYFoM!Zz+C$Q2UQ(cc%l)5XS$Eyfp+m#J01=9VKw{4#D(=J z$a39f8gcw?_I1Id;8YDTRgYN9(3a`JG zG9cfb$W0#4g~1vt=3HO6oxt+tx1+y5UTHFNDvl}7^&n+rT+nFh?}ZL+bTp4HU6;p* zlE@z@e(b}d_gdixyRHss!Jc}8rNwulbJvCP-r&J0X9KtNu8PfFNM$CS>=m>o_6~ml zZ~PCt5_N_{fd0PGX?2H!tf|vBI9jp2Rua0u1!a*CbOk=gEBkBZ1bZScQ8J+JLG@d@ zBq}G1G|bUmxxK}~4q;V$_cj1ex<6}y+TW8uR%2-VStyX}mhQ`ucZTb<|H~op)%w1g zQ$6e7QrqI0mig-1DiejdF81-JpDFcHsnwkxb9sX6YNbAH8y;N9WbN0NIFtpKoMqq9 z;dShJW>B0fE|$F?=im9QKYB@CC%ucM^yc4g8Bb6jKo_m&2f`U>e5^FOs2&Aewf+zH z!*{B;l(M!-7IPZ1Wl%q`Suo#KeBzl)>Kxv=jT~h!1~@a<5aVL`iT7S#=@hhkST3G8 zjKBFa899c_e72k#L|75MtfGon5W%`zK|a$v>ua1_&SPIz;xF>cs@AJIO&$l8rv@4hi8(-R#TLuMCGdnyia(t^WDH(Pz1IejJ zzhtWL;d~^WqB?+TE?7_TSo_;! zn`z$Dtq;P`+4z1~MY8_tiOQn^A>Qmq(=PICh?Cw88fp$osy2#kM2iaAGnc=Q#WfIyx-g~$Y!XW0aLFAW!Q*;NK9$n(F_)cIhHJhy$iW%qGlU1Vr zNd9IuzstX>d6MWq#>n~(zNf1l8Aksd`k&&t-2W*p6@DevcJ|96yBc-)8Uu$8XFb2R z6D7lqc5XY5B;Qy(d}r^us7F5R?=9RRW1IMj$Qb$wJBJ^CIrtsqY^5F0m84>pM}?+l&>vk)%uJNQQB z42%2P-qlkDkwnVJ^QZhoAK=UuBQ)({b7P*}+?l0mypz=z$8H|A)ELKx)B6d?q!@3kknGW zncf?x#RGdQmH~aZUUt0A9cGEfYetXJIf55D$9eWs5^^@^65?3tso;=xDFPKstsDK4 z^GnAchIjfRusHgB+vHsPBop((5Aw15;9&NOD&hT7F3KI4x0UNZ%m)VB$feIet~|1< zWXRO#y#Cyoc|7077w!v(N297IStj;s2D;rs2*+vP#6IXk=An_E^xD7H-O)o4-T>Ob zqYPQ%d0^6tic{k%w&&1EQOz|n6c%Ry4MB~-aPOrJ_KJ{;jew?c;~M9Yh8bqDhXWU8B3s{ zX9|mvy^r87akW#;ISgDucQh3o_SvY5&Vgfn4M00;^yk&%#rxnSVji6fQ_lx0xcR+Gg1~ z;Tn6XPv0A!)Qx#&_*$kFg=wOH-A_lGynFgarSYk3DjRxz7~AyCEv6aC_ZF#hrhx}^ z58Q(k%L`(+QCW5@44Mi57imXE=@eA`chi~9``p~QshYzc8SE~HWM68jTg7+YMN&T5 zN;nd&>K8Ua_5iEz+{r2~mmSxjwN_v+0dmYa{ma7r@`$AgaL|^!Whq z8^aG;tc8sGLz|7ik(wCZ2X#kAzc4HlEfPcG<@r;B8Z3hc-sSJx)c=ASp~D~9H^=F1 z`wZ9P_0J_)Xf_`cIBTxXR&##n&I-g}ea=VL?=CA@|1memEH+bgnUljF7kaL?PJJHea{C0G&JA;n`_)T zctaIx&nThWBArHrs-e;&+xp6ulib@5i@-S^@1JZ5j@MKxdW=-vnMKe4)2 zBh{EB8lfvl-BDOLWeDcUFa|W^&^@%7#Xe4ba#DKL*hlgjz?JhorFNrlpL0T>BNhqV zc&2tARMcM6k8FjvOaG`mSP z5KZQ?G|qizCS{(*|DQHXq@<57XeRjR#Bi_Yu;4XiWVP@ak!SA4$43&S!Q6m0q=^i`=BE0=g)g?%13Ca2=$~h(32j` z6Gfs$WW~1SRAc}Mn8gdT1E*KtRV{~dbDZ2$*C$kQ00wajTg1R{qfXeqr5UnL=kg%4K;U-;iA;xyMVUrF~2uh zy53#6vLC*h3(&I4kCS_@eWH16>Dl0W;9EZbqR&@xw(O{9Ct5q|{(yX;eOSJE-?6+# z7jTz)ti=O-k1j#NkKu1_|KWRs9L*zdfjt|^p^@7%e3-j71zmcxUeyz*8cBi zU({djdGM%Yk%?$)aq&F$2I4RBM5?F~f3R~<*%MRC-6kGKjfwL67+(6Y5%D8@rroA% zEs1>$tbiv@x)B|SB%Ko5>&g|;kN{>X8R(-b6r9d<5Z#4cmFF|hiqm^0S>y|m48_4u z7WK7T_6xY>jAgvIK8T1K^py6|e{FbCwg$R@oASNzD0SSDcem%z8XEHN^|c=cxZuB@ zl^fDZS}{eRAAYT}GT@NvH@P9@H-Or^S`{d3p}@B|0V!JRc^W6&a%AB;cAob}N% zLs9SL>h(3`Pm-oAd`U;ahDCI*HuuM8EcoiXc_{B){i3WWUtXg&hkSWWv=1X$JGFJy zT1RDA$M5zhHj7`NdI{ASr1u6m-*wWwh7b)UOi6yh88tlTYTv`evs@0*7`;mw$m8vc z>JLl;z?jc=6RM9v>`?ELj=PAf%HGX=KvBkux34rX+LMUMEp{JW6j#BG#D`H!EPcf> zE9&`!teHIb9&;gcNLHsj5q^FwLD}dU>8&V~lFB^u7(UB!IUKv&pO}w_11{E+-V}XH zRZ#ZdwXI)Kc2t?nJ`OH(pBJW?qGMK3CSC-Za^AeCEQinAy#-Y~f$e+yFHWyrr*DNU zaXjgD`4|~HSlSfs{@oyaGgvd96WlllNBVeUFuIS{N2}}&DVH1leALT{RnaR@3%XJ{ zQ;!qE878%du9UN1c27P!{kxxdNrLWENDhct6kCwC9` zL!0>~odR5VYUr{?1sL_ zBEU&-jK8n#4bS#a62GjcjBJW~ua5Q2Jhlw+I2P7jhsG$Za%AlpUJ$!(dd9WH!5le{ z)$@O3ZztRI(f*ST?vs=kt< z_$Sy_c$RL*sN)92Pi-`+ZHzwv=U+LA%}+5B5jH>ez3$jL*4FZljOh&cUsu zDj!QUtvye`L}YR2pwc)D%iud5bRKqxD7Ozn>@1Gy!x{S0{Ua0iEhY&|Ka- zRGuf#K1Hu(u->*=Fmq$0%CG@79s6S(RBPl-YR;QMOKkzYsSfN_v{gk~ zxte&&PeVWw^SW;LR4z=G`R(xksLcM`;xAXTy&h@XJi;q8-o(F93SU5yHN}0dG2M=q z_1aQ$*LC zN*Z~i*6vEPIV6tlu_rghxsSbHEN$ZDxs?af@_96ctf1R_pxQrV(482fkN(Jm`XyQq zZk+c*4x&G8N&;&8ut{&kCidZM=(nejBpbi@P~_Udzo z*R|o)cv!q;IVOMe_Bebv_$11`5cA~kw~SK6IM^)wbm~rcJl+r0hqI{~aE^h-dQu!$ zoB1gjjvl!BU!WrnKX*)Ko$EbgshNtj`}jb`?#I>m~_!@DT5ZH zvH%vTF_Ukh65IzFwD70#*xHJDNuBWGcfoy8TXa1aSH3gYs0m4T_d!=i_GD#a1R#m? zbfF|Pi}#UbC6ZC}KMxIK^cKD`35(r!`{0c%F_|gv)~l=DWyey@srasL)BDEDRw9q) z7^PPlT<81=-a_4nJBJ_ju%O`>mK`g;a{-Y`Z@;leeE1Qr4L!dL7RdYLI%dX9bad>V zmp19XRTV;BZb`*j!;S9!Pt; zrd6$?V()Od&fq?c2KITDZ`c+_^{g|Aj4iJ#bR3mAsO8X?A>$u9#Jp z+yF77dGwfkm(5Pun9D^ZB*5Zx);?6a7C?08A~i8 zQGq<4$Uk2g4|7+`t=V!Cr(>P`?6vyMSX|FFWRG!rbk`R1TGnyQUK;N_?~JW3%-i5@ zbYFDKgV7ph=OwR=agRR@mUqoR<5JP<6^C|)7ul<8TsP}PFs)&n2PQa?j&5>&{`sz5 zJ2xKmtk&u(gesbz+vB;`pNoCSyxOBo01%I@2QEj;-=Hm-exh#opSI;-y7cguE(c7+ zx6=VX%I)=Lz&*wxGwP$Z_`p^Z=_uZrgFo>L*h7?m730gx9lH)(>vl|{r|~t|->S`W z@D^Zd_ZHeDo6lYj{m}emGxMxXugtaH0{j?VuHJzJRIaHdfAjc~>%g~eXQy+ZOPWU| zEf=;}2y3+53Za43kBhw1jWMpX;@vLg+sZ1ojb4?YIQsQ{)@;XF(+q5( zIkI<9ziF8t(!L+RnmK6CVP+cqKxA3Z%W3JcYpEk@7_z6wJMd(bcXcS`x6GprTP_iA zWZt`kr-n=Rw1aQ;1S3E%tvBIc&xv_< zpvEuMq>I%@SoKauCz_Ltwpo}`#_^e?!!_gVO(8`(UxpsqdR}M%dGkyJ6X}FN%2DXc&ljUR=r=etbXWAos~PEv?Q59IT4-#SzFEQM}Vpdl3*L zKwDsxJ{2~(B8njUC0|Egpw>`5{3H%!9-`XVJg|Cj5i~5Z&+PHVnNw)onyO^_w7a4E zg=PM{#0Wa4okcw++Mn2-dP^xqnBTMkAS<$mLQFOX+x_Rd2z zSn$DIaYjvH_u@*rpBY>(2YsBU*n(bZ*|><+?r~NtsWpX=V{!zv-+*lhK;;t!rvRZGS_lL$;^jGHC9ZhKNpwTgG93H%>#J zx_8$ntDgI!%S@*dk|UJvQFja+i}pf_wFZ=Gu{%xEVn zVD2kLE<7sqyM^YbfwRtSCb|CEM?L8p@K45;cu&!C*1+f-@A4_%AuHtEyIUw94$x6S z>dkZswyElHsb%0h_cM15N95i5Xe197?MOTqm0MrkEa@rrNKL=|>u%v&Hycz&6nT}e zDt_6++wIIB8HVxw!Sgyi7SBw}$@S5LeRE=Gi$0DcCW>d!=Cy`^5AiWw`?DsC3{lyG zH4nWwL6%M>{7n2g^euKo)_MF_y2;d^am((+V&~l!`mo^n6P^rDOHjv{f?4kh-|P?| z?}IXPW(iA%A9{m*@w)Q&ffF?6C$k=N)a8-FahfitIHw=Mj~*fQLGM<8noaq9>vt;# zhx@=pKwopj9P!4cSm3#G{aGI#&YS1ZWw~x4FD_Rgq?tJ}G;qP{Tg?ECF&8Ghi{ClG zzn>bti9x5X_rZ=vAzRVL)UgvB6!&Ug&S%%zA#&}q(qOdggULD-{gB5|1s>1x_d$+c z3^_yBb?wYtqe<)fJnK=(J2E5n4J$_>KNVTksE*6_9ol7QyD^o4h`p?qWpqoOxyhPw zxZmM{C(aC)?ASqO9k`VTzH3-suY&CB*r+hPFk8g`-BZRRmSvJnb z;6}T`m7do`E@T_XZg~uJ=_;o zi3R)#@D9eaPDbmJEf*K!mHD0`4)dY@D_VRzN{dZu^sO$)n3s0QS3*auqJ;WMTHOj| zvc~sa%CHe-F9kI4n&j$n;2rWuR1@lYCpI|urI$K%A9&k5-mB-1_en-2*L`c8f(PGC zC1Ww7$aYlkGeCA^1@KqY$%IcA@`KdGK4jt5{A$*C4$KnyBfVtHp=ww|IONo>K+8V< zEB$t_eB|t+XU@yF%nQdOqX#g}6=gwR;Rrr}`m(c7M%@QULB7i&hwhxR{c2`PIfV05 z$D!B_MQ#sP;1`oAR_EX)_wf~#L~tn|r#^#-IeTZP%hi~S>Ym*|HYPiLu*JS)j{9*L zf!l~<@av-L#51mYfJ~NjGac((P7Xt#H`k*M3$w0ifp;2xP}mTkx!a%6oGxFC6RSg( zjUK=4hSFzO^Prm0M`5ffyz8)puF&3d_^xAkniGE_Rvs)i&#{G6mD@p-oOAM&UbEJ7pYd z+f3&dyv7MHbVuQ5{c61FP`S3@dF;EYOk~ceisF}nVT~OaT$MdwM!c2$Y^*k$BkNS+ z^Nzhi8X{K5uZvZmt1`#AgOGr+t~}sQr<#5h9;cdc{$-#}C0C&yBAij1S;>Ki_X#y4ToaPi^q8(=ckR zIp*e|d=1Iw98hMi<(Jo*0m+9Z$jeyuO&+PZpOqo+?7p_!41ZR8NAXVRD^Gq-ykM=> zL8R(yu0y*P*j7fc>ErLn6mNa*Qd06ad6)Gnmy4vg=9UDq>>s}D7k9jJP{Ogf6x5Q8 zm4vI^7Djf{xo8!AYfYG`*{=cHw;YbFktut#_{Y}Pv&Q}Yw5UVym2bQwcpo}Cuzfn% zV~?%2)yqgG-E#PLSShdnqm^@}sg=RRly|xSM?O77nMEH{*;4VIV{HykFn9rPpIT<$%rETjGX4v!M~<92ES3BJH}`^H L*Qzb9YZ?0gVn9Q8 literal 0 HcmV?d00001 diff --git a/tests/Concerns/SeedsE2ETestFixtures.php b/tests/Concerns/SeedsE2ETestFixtures.php new file mode 100644 index 00000000..d3376cb6 --- /dev/null +++ b/tests/Concerns/SeedsE2ETestFixtures.php @@ -0,0 +1,279 @@ +travelTo('2025-10-05 10:00:00'); + + $this->seedApiTestConfig(self::E2E_SCHOOL_YEAR, self::E2E_SEMESTER); + + DB::table('configuration')->updateOrInsert( + ['config_key' => 'fall_semester_start'], + ['config_value' => '2025-09-01'] + ); + DB::table('configuration')->updateOrInsert( + ['config_key' => 'spring_semester_start'], + ['config_value' => '2026-01-15'] + ); + DB::table('configuration')->updateOrInsert( + ['config_key' => 'last_school_day'], + ['config_value' => '2026-06-15'] + ); + } + + protected function seedClosedSchoolYear(): void + { + SchoolYear::query()->updateOrCreate( + ['name' => self::E2E_PREV_SCHOOL_YEAR], + [ + 'start_date' => '2024-09-01', + 'end_date' => '2025-06-30', + 'status' => SchoolYear::STATUS_CLOSED, + 'is_current' => false, + ] + ); + } + + protected function seedClassSection( + int $classSectionId = 701, + string $name = '5A', + int $classId = 5 + ): int { + DB::table('classSection')->insert([ + 'class_id' => $classId, + 'class_section_id' => $classSectionId, + 'class_section_name' => $name, + 'semester' => self::E2E_SEMESTER, + 'school_year' => self::E2E_SCHOOL_YEAR, + ]); + + return $classSectionId; + } + + protected function seedTeacherClassAssignment(int $teacherId, int $classSectionId, string $position = 'main'): void + { + DB::table('teacher_class')->insert([ + 'teacher_id' => $teacherId, + 'class_section_id' => $classSectionId, + 'position' => $position, + 'school_year' => self::E2E_SCHOOL_YEAR, + 'semester' => self::E2E_SEMESTER, + 'created_at' => now(), + 'updated_at' => now(), + ]); + } + + protected function seedStudentClassAssignment(int $studentId, int $classSectionId): void + { + DB::table('student_class')->insert([ + 'student_id' => $studentId, + 'class_section_id' => $classSectionId, + 'school_year' => self::E2E_SCHOOL_YEAR, + 'semester' => self::E2E_SEMESTER, + 'is_event_only' => 0, + 'created_at' => now(), + 'updated_at' => now(), + ]); + } + + /** + * @return array{teacher: User, class_section_id: int, student_id: int, parent_id: int} + */ + protected function seedTeacherClassWithStudent(?int $classSectionId = null): array + { + $this->seedE2EConfiguration(); + $classSectionId ??= $this->seedClassSection(801, '3A', 3); + + $teacher = $this->createApiUserWithRole('teacher', [ + 'firstname' => 'Tara', + 'lastname' => 'Teacher', + 'email' => 'e2e.teacher.' . uniqid('', true) . '@example.test', + ]); + $this->seedTeacherClassAssignment($teacher->id, $classSectionId); + + $parent = $this->createApiUserWithRole('parent', [ + 'firstname' => 'Parent', + 'lastname' => 'Fixture', + 'email' => 'e2e.parent.' . uniqid('', true) . '@example.test', + ]); + + $studentId = DB::table('students')->insertGetId([ + 'school_id' => 'S-E2E-' . random_int(1000, 9999), + 'firstname' => 'Kai', + 'lastname' => 'Student', + 'dob' => '2014-09-01', + 'age' => 11, + 'gender' => 'Male', + 'photo_consent' => 1, + 'parent_id' => $parent->id, + 'year_of_registration' => '2025', + 'school_year' => self::E2E_SCHOOL_YEAR, + 'semester' => self::E2E_SEMESTER, + 'is_active' => 1, + ]); + $this->seedStudentClassAssignment($studentId, $classSectionId); + + return [ + 'teacher' => $teacher, + 'class_section_id' => $classSectionId, + 'student_id' => $studentId, + 'parent_id' => $parent->id, + 'parent' => $parent, + ]; + } + + protected function createParentAccountViaApi(array $overrides = []): int + { + $parentRoleId = (int) Role::query()->where('name', 'parent')->value('id'); + $unique = str_replace('.', '', uniqid('', true)); + + $payload = array_merge([ + 'firstname' => 'E2E', + 'lastname' => 'Parent', + 'gender' => 'Female', + 'cellphone' => '5551234567', + 'email' => "e2e.parent.$unique@example.test", + 'address_street' => '42 Cedar Lane', + 'city' => 'Brooklyn', + 'state' => 'NY', + 'zip' => '11201', + 'accept_school_policy' => true, + 'role_id' => $parentRoleId, + 'password' => 'secret123', + 'semester' => self::E2E_SEMESTER, + 'school_year' => self::E2E_SCHOOL_YEAR, + 'user_type' => 'primary', + 'status' => 'Active', + 'is_verified' => true, + ], $overrides); + + $response = $this->postJson('/api/v1/users', $payload); + $response->assertStatus(201)->assertJsonPath('ok', true); + + return (int) $response->json('user_id'); + } + + protected function createStudentViaApi(int $parentId, array $overrides = [], bool $firstChild = true): int + { + $unique = str_replace('.', '', uniqid('', true)); + $payload = array_merge([ + 'firstname' => 'E2E', + 'lastname' => 'Student', + 'dob' => '2015-05-10', + 'age' => 10, + 'gender' => 'Male', + 'is_active' => true, + 'registration_grade' => '5', + 'is_new' => true, + 'photo_consent' => true, + 'parent_id' => $parentId, + 'registration_date' => '2025-09-01', + 'tuition_paid' => false, + 'semester' => self::E2E_SEMESTER, + 'year_of_registration' => 2025, + 'school_year' => self::E2E_SCHOOL_YEAR, + ], $overrides); + + if ($firstChild) { + $payload = array_merge($payload, [ + 'name' => $overrides['name'] ?? 'E2E Parent', + 'cellphone' => $overrides['cellphone'] ?? '5551234567', + 'email' => $overrides['email'] ?? "e2e.contact.$unique@example.test", + 'relation' => $overrides['relation'] ?? 'Mother', + ]); + } + + $response = $this->postJson('/api/v1/students', $payload); + $response->assertStatus(201)->assertJsonPath('ok', true); + + return (int) $response->json('student.id'); + } + + protected function assignStudentToClassViaApi(int $studentId, int $classSectionId): TestResponse + { + return $this->postJson('/api/v1/students/assign-class', [ + 'student_id' => $studentId, + 'class_section_id' => [$classSectionId], + ]); + } + + protected function postEnrollmentStatuses(array $studentStatusMap): TestResponse + { + return $this->post( + '/api/v1/administrator/enrollment-withdrawal/update-statuses?' . + http_build_query(['enrollment_status' => $studentStatusMap]), + [] + ); + } + + protected function seedInvoiceForParent(int $parentId, array $overrides = []): int + { + return DB::table('invoices')->insertGetId(array_merge([ + 'parent_id' => $parentId, + 'invoice_number' => 'INV-E2E-' . random_int(1000, 9999), + 'total_amount' => 500.00, + 'balance' => 500.00, + 'paid_amount' => 0, + 'has_discount' => 0, + 'issue_date' => '2025-09-01', + 'due_date' => '2025-10-01', + 'status' => 'unpaid', + 'school_year' => self::E2E_SCHOOL_YEAR, + 'semester' => self::E2E_SEMESTER, + 'created_at' => now(), + 'updated_at' => now(), + ], $overrides)); + } + + protected function seedPriorYearUnpaidInvoice(int $parentId): int + { + return DB::table('invoices')->insertGetId([ + 'parent_id' => $parentId, + 'invoice_number' => 'INV-PRIOR-' . random_int(1000, 9999), + 'total_amount' => 200.00, + 'balance' => 150.00, + 'paid_amount' => 50.00, + 'has_discount' => 0, + 'issue_date' => '2024-09-01', + 'due_date' => '2024-10-01', + 'status' => 'unpaid', + 'school_year' => self::E2E_PREV_SCHOOL_YEAR, + 'semester' => self::E2E_SEMESTER, + 'created_at' => now(), + 'updated_at' => now(), + ]); + } + + protected function seedDiscountVoucher(): int + { + return DB::table('discount_vouchers')->insertGetId([ + 'code' => 'E2E10-' . random_int(100, 999), + 'discount_type' => 'fixed', + 'discount_value' => 50, + 'max_uses' => 100, + 'times_used' => 0, + 'is_active' => 1, + ]); + } +} diff --git a/tests/Feature/Api/ApiAuthenticationAndAuthorizationTest.php b/tests/Feature/Api/ApiAuthenticationAndAuthorizationTest.php index baa29634..ebe6a27a 100644 --- a/tests/Feature/Api/ApiAuthenticationAndAuthorizationTest.php +++ b/tests/Feature/Api/ApiAuthenticationAndAuthorizationTest.php @@ -13,6 +13,54 @@ class ApiAuthenticationAndAuthorizationTest extends TestCase use CreatesApiTestUsers; use RefreshDatabase; + + public function test_api_login_returns_token_and_user_object(): void + { + $user = $this->createApiUserWithRole('teacher', [ + 'firstname' => 'Login', + 'lastname' => 'Tester', + 'email' => 'api.login@example.test', + ]); + + $this->postJson('/api/v1/auth/login', [ + 'email' => strtoupper($user->email), + 'password' => 'password', + ]) + ->assertOk() + ->assertJsonPath('status', true) + ->assertJsonStructure([ + 'token', + 'access_token', + 'token_type', + 'expires_in', + 'user' => ['id', 'name', 'firstname', 'lastname', 'email', 'roles'], + ]) + ->assertJsonPath('user.email', 'api.login@example.test'); + } + + public function test_session_login_also_returns_token_and_user_object_for_spa_clients(): void + { + $user = $this->createApiUserWithRole('teacher', [ + 'firstname' => 'Session', + 'lastname' => 'Tester', + 'email' => 'session.login@example.test', + ]); + + $this->postJson('/user/login', [ + 'email' => $user->email, + 'password' => 'password', + ]) + ->assertOk() + ->assertJsonPath('status', true) + ->assertJsonStructure([ + 'token', + 'access_token', + 'user' => ['id', 'name', 'firstname', 'lastname', 'email', 'roles'], + 'next_url', + ]) + ->assertJsonPath('user.email', 'session.login@example.test'); + } + public function test_auth_me_requires_authentication(): void { $this->getJson('/api/v1/auth/me')->assertUnauthorized(); diff --git a/tests/Feature/Api/V1/Administrator/AdministratorDashboardControllerTest.php b/tests/Feature/Api/V1/Administrator/AdministratorDashboardControllerTest.php new file mode 100644 index 00000000..b8fd8a7b --- /dev/null +++ b/tests/Feature/Api/V1/Administrator/AdministratorDashboardControllerTest.php @@ -0,0 +1,70 @@ +getJson('/api/v1/administrator/dashboard/metrics') + ->assertStatus(401); + } + + public function test_metrics_are_forbidden_for_non_admin_users(): void + { + $teacher = $this->createApiUserWithRole('teacher'); + + $this->actingAs($teacher, 'api') + ->getJson('/api/v1/administrator/dashboard/metrics') + ->assertStatus(403); + } + + public function test_metrics_return_payload_for_admin(): void + { + $admin = $this->createApiUserWithRole('administrator'); + + $mock = Mockery::mock(AdministratorDashboardService::class); + $mock->shouldReceive('metrics')->once()->andReturn([ + 'students' => 42, + 'teachers' => 5, + ]); + $this->app->instance(AdministratorDashboardService::class, $mock); + + $this->actingAs($admin, 'api') + ->getJson('/api/v1/administrator/dashboard/metrics') + ->assertOk() + ->assertJson(['students' => 42, 'teachers' => 5]); + } + + public function test_user_search_passes_query_to_service(): void + { + $admin = $this->createApiUserWithRole('administrator'); + + $mock = Mockery::mock(AdministratorDashboardService::class); + $mock->shouldReceive('userSearch') + ->once() + ->with('smith') + ->andReturn(['results' => [['id' => 1, 'name' => 'Smith']]]); + $this->app->instance(AdministratorDashboardService::class, $mock); + + $this->actingAs($admin, 'api') + ->getJson('/api/v1/administrator/dashboard/user-search?query=smith') + ->assertOk() + ->assertJsonPath('results.0.name', 'Smith'); + } +} diff --git a/tests/Feature/Api/V1/Administrator/TrophyControllerTest.php b/tests/Feature/Api/V1/Administrator/TrophyControllerTest.php new file mode 100644 index 00000000..d07dedba --- /dev/null +++ b/tests/Feature/Api/V1/Administrator/TrophyControllerTest.php @@ -0,0 +1,89 @@ +getJson('/api/v1/administrator/trophy') + ->assertStatus(401); + } + + public function test_index_is_forbidden_for_non_admin_users(): void + { + $teacher = $this->createApiUserWithRole('teacher'); + + $this->actingAs($teacher, 'api') + ->getJson('/api/v1/administrator/trophy') + ->assertStatus(403); + } + + public function test_index_returns_projection_for_admin(): void + { + $admin = $this->createApiUserWithRole('administrator'); + + $mock = Mockery::mock(TrophyReportService::class); + $mock->shouldReceive('projection')->once()->andReturn(['rows' => []]); + $this->app->instance(TrophyReportService::class, $mock); + + $this->actingAs($admin, 'api') + ->getJson('/api/v1/administrator/trophy') + ->assertOk() + ->assertJsonPath('ok', true) + ->assertJsonPath('data.rows', []); + } + + public function test_winners_returns_data_for_admin(): void + { + $admin = $this->createApiUserWithRole('administrator'); + + $mock = Mockery::mock(TrophyReportService::class); + $mock->shouldReceive('winners')->once()->andReturn(['winners' => []]); + $this->app->instance(TrophyReportService::class, $mock); + + $this->actingAs($admin, 'api') + ->getJson('/api/v1/administrator/trophy/winners') + ->assertOk() + ->assertJsonPath('ok', true); + } + + public function test_final_returns_data_for_admin(): void + { + $admin = $this->createApiUserWithRole('administrator'); + + $mock = Mockery::mock(TrophyReportService::class); + $mock->shouldReceive('final')->once()->andReturn(['final' => []]); + $this->app->instance(TrophyReportService::class, $mock); + + $this->actingAs($admin, 'api') + ->getJson('/api/v1/administrator/trophy/final') + ->assertOk() + ->assertJsonPath('ok', true); + } + + public function test_index_validates_percentile_range(): void + { + $admin = $this->createApiUserWithRole('administrator'); + + $this->actingAs($admin, 'api') + ->getJson('/api/v1/administrator/trophy?percentile=150') + ->assertStatus(422) + ->assertJsonValidationErrors(['percentile']); + } +} diff --git a/tests/Feature/Api/V1/BadgeScan/BadgeScanControllerTest.php b/tests/Feature/Api/V1/BadgeScan/BadgeScanControllerTest.php new file mode 100644 index 00000000..583ddcdf --- /dev/null +++ b/tests/Feature/Api/V1/BadgeScan/BadgeScanControllerTest.php @@ -0,0 +1,53 @@ +postJson('/api/v1/badge_scan/scan', []) + ->assertStatus(422) + ->assertJsonPath('status', false); + } + + public function test_scan_returns_404_for_unrecognized_badge(): void + { + $this->postJson('/api/v1/badge_scan/scan', ['badge_scan' => 'UNKNOWN-BADGE-123']) + ->assertNotFound() + ->assertJsonPath('status', false); + } + + public function test_logs_require_authentication(): void + { + $this->getJson('/api/v1/badge_scan/logs') + ->assertStatus(401); + } + + public function test_logs_are_forbidden_for_non_staff_roles(): void + { + $parent = $this->createApiUserWithRole('parent'); + + $this->actingAs($parent, 'api') + ->getJson('/api/v1/badge_scan/logs') + ->assertStatus(403); + } + + public function test_logs_return_data_for_staff(): void + { + $admin = $this->createApiUserWithRole('administrator'); + + $this->actingAs($admin, 'api') + ->getJson('/api/v1/badge_scan/logs') + ->assertOk() + ->assertJsonPath('status', true) + ->assertJsonStructure(['status', 'data' => ['logs', 'meta']]); + } +} diff --git a/tests/Feature/Api/V1/Finance/EventChargeControllerTest.php b/tests/Feature/Api/V1/Finance/EventChargeControllerTest.php new file mode 100644 index 00000000..7d8300a6 --- /dev/null +++ b/tests/Feature/Api/V1/Finance/EventChargeControllerTest.php @@ -0,0 +1,118 @@ +create(array_merge([ + 'event_name' => 'Field Trip', + 'description' => 'Annual field trip charge', + 'amount' => 25.00, + 'charged' => 0, + 'semester' => 'Fall', + 'school_year' => '2025-2026', + ], $overrides)); + } + + public function test_show_requires_authentication(): void + { + $charge = $this->makeCharge(); + + $this->getJson('/api/v1/finance/event-charges/' . $charge->id) + ->assertStatus(401); + } + + public function test_show_returns_event_charge(): void + { + $this->actingAsApiAdministrator(); + $charge = $this->makeCharge(); + + $this->getJson('/api/v1/finance/event-charges/' . $charge->id) + ->assertOk() + ->assertJsonPath('ok', true) + ->assertJsonPath('event_charge.id', $charge->id) + ->assertJsonPath('event_charge.event_name', 'Field Trip'); + } + + public function test_show_returns_404_for_unknown_charge(): void + { + $this->actingAsApiAdministrator(); + + $this->getJson('/api/v1/finance/event-charges/999999') + ->assertNotFound(); + } + + public function test_update_modifies_event_charge(): void + { + $this->actingAsApiAdministrator(); + $charge = $this->makeCharge(); + + $this->putJson('/api/v1/finance/event-charges/' . $charge->id, [ + 'event_name' => 'Museum Visit', + 'amount' => 40.50, + ]) + ->assertOk() + ->assertJsonPath('ok', true) + ->assertJsonPath('event_charge.event_name', 'Museum Visit'); + + $this->assertDatabaseHas('event_charges', [ + 'id' => $charge->id, + 'event_name' => 'Museum Visit', + ]); + } + + public function test_approve_marks_charge_as_charged(): void + { + $this->actingAsApiAdministrator(); + $charge = $this->makeCharge(['charged' => 0]); + + $this->postJson('/api/v1/finance/event-charges/' . $charge->id . '/approve') + ->assertOk() + ->assertJsonPath('ok', true); + + $this->assertDatabaseHas('event_charges', [ + 'id' => $charge->id, + 'charged' => 1, + ]); + } + + public function test_void_clears_charged_flag(): void + { + $this->actingAsApiAdministrator(); + $charge = $this->makeCharge(['charged' => 1]); + + $this->postJson('/api/v1/finance/event-charges/' . $charge->id . '/void') + ->assertOk() + ->assertJsonPath('ok', true); + + $this->assertDatabaseHas('event_charges', [ + 'id' => $charge->id, + 'charged' => 0, + ]); + } + + public function test_destroy_voids_the_charge(): void + { + $this->actingAsApiAdministrator(); + $charge = $this->makeCharge(['charged' => 1]); + + $this->deleteJson('/api/v1/finance/event-charges/' . $charge->id) + ->assertOk() + ->assertJsonPath('ok', true); + + $this->assertDatabaseHas('event_charges', [ + 'id' => $charge->id, + 'charged' => 0, + ]); + } +} diff --git a/tests/Feature/Api/V1/Parents/AuthorizedUsersControllerTest.php b/tests/Feature/Api/V1/Parents/AuthorizedUsersControllerTest.php new file mode 100644 index 00000000..fcdb2cb7 --- /dev/null +++ b/tests/Feature/Api/V1/Parents/AuthorizedUsersControllerTest.php @@ -0,0 +1,106 @@ +create(array_merge([ + 'user_id' => $parentId, + 'firstname' => 'Aunt', + 'lastname' => 'Carer', + 'email' => 'carer-' . uniqid() . '@example.test', + 'phone_number' => '5551112222', + 'relation_to_user' => 'Aunt', + 'status' => 'pending', + ], $overrides)); + } + + public function test_index_requires_authentication(): void + { + $this->getJson('/api/v1/parents/authorized-users') + ->assertStatus(401); + } + + public function test_index_is_forbidden_for_non_parent_users(): void + { + $teacher = $this->createApiUserWithRole('teacher'); + + $this->actingAs($teacher, 'api') + ->getJson('/api/v1/parents/authorized-users') + ->assertStatus(403); + } + + public function test_index_returns_only_the_parents_own_authorized_users(): void + { + $parent = $this->createApiUserWithRole('parent'); + $otherParent = $this->createApiUserWithRole('parent'); + + $own = $this->makeAuthorizedUser($parent->id); + $this->makeAuthorizedUser($otherParent->id); + + $response = $this->actingAs($parent, 'api') + ->getJson('/api/v1/parents/authorized-users') + ->assertOk() + ->assertJsonPath('status', true); + + $this->assertCount(1, $response->json('data')); + $this->assertSame($own->id, $response->json('data.0.id')); + } + + public function test_show_returns_404_for_unknown_authorized_user(): void + { + $parent = $this->createApiUserWithRole('parent'); + + $this->actingAs($parent, 'api') + ->getJson('/api/v1/parents/authorized-users/999999') + ->assertNotFound() + ->assertJsonPath('status', false); + } + + public function test_show_returns_404_for_another_parents_authorized_user(): void + { + $parent = $this->createApiUserWithRole('parent'); + $otherParent = $this->createApiUserWithRole('parent'); + $foreign = $this->makeAuthorizedUser($otherParent->id); + + $this->actingAs($parent, 'api') + ->getJson('/api/v1/parents/authorized-users/' . $foreign->id) + ->assertNotFound(); + } + + public function test_destroy_deletes_the_parents_authorized_user(): void + { + $parent = $this->createApiUserWithRole('parent'); + $record = $this->makeAuthorizedUser($parent->id); + + $this->actingAs($parent, 'api') + ->deleteJson('/api/v1/parents/authorized-users/' . $record->id) + ->assertOk() + ->assertJsonPath('status', true); + + $this->assertDatabaseMissing('authorized_users', ['id' => $record->id]); + } + + public function test_parent_cannot_delete_another_parents_authorized_user(): void + { + $parent = $this->createApiUserWithRole('parent'); + $otherParent = $this->createApiUserWithRole('parent'); + $foreign = $this->makeAuthorizedUser($otherParent->id); + + $this->actingAs($parent, 'api') + ->deleteJson('/api/v1/parents/authorized-users/' . $foreign->id) + ->assertNotFound(); + + $this->assertDatabaseHas('authorized_users', ['id' => $foreign->id]); + } +} diff --git a/tests/Feature/Api/V1/Public/PublicWinnersControllerTest.php b/tests/Feature/Api/V1/Public/PublicWinnersControllerTest.php new file mode 100644 index 00000000..8503ec7e --- /dev/null +++ b/tests/Feature/Api/V1/Public/PublicWinnersControllerTest.php @@ -0,0 +1,82 @@ +create([ + 'title' => 'Published Quiz Bowl', + 'school_year' => '2025-2026', + 'is_published' => 1, + 'published_at' => now(), + ]); + + Competition::query()->create([ + 'title' => 'Hidden Draft', + 'school_year' => '2025-2026', + 'is_published' => 0, + ]); + + $response = $this->getJson('/api/winners/competitions'); + + $response->assertOk() + ->assertJsonPath('status', true) + ->assertJsonStructure(['status', 'data' => ['competitions']]); + + $titles = array_column($response->json('data.competitions'), 'title'); + $this->assertContains('Published Quiz Bowl', $titles); + $this->assertNotContains('Hidden Draft', $titles); + } + + public function test_competition_index_returns_empty_collection_when_none_published(): void + { + $this->getJson('/api/winners/competitions') + ->assertOk() + ->assertJsonPath('status', true) + ->assertJsonPath('data.competitions', []); + } + + public function test_competition_show_returns_payload_for_published_competition(): void + { + $competition = Competition::query()->create([ + 'title' => 'Spelling Bee', + 'school_year' => '2025-2026', + 'is_published' => 1, + 'published_at' => now(), + ]); + + $this->getJson('/api/winners/competitions/' . $competition->id) + ->assertOk() + ->assertJsonPath('status', true) + ->assertJsonPath('data.competition.id', $competition->id) + ->assertJsonStructure(['data' => ['competition', 'winners_by_class', 'section_map', 'question_counts']]); + } + + public function test_competition_show_returns_404_for_unknown_competition(): void + { + $this->getJson('/api/winners/competitions/999999') + ->assertNotFound() + ->assertJsonPath('status', false); + } + + public function test_competition_show_returns_404_for_unpublished_competition(): void + { + $competition = Competition::query()->create([ + 'title' => 'Unpublished', + 'school_year' => '2025-2026', + 'is_published' => 0, + ]); + + $this->getJson('/api/winners/competitions/' . $competition->id) + ->assertNotFound() + ->assertJsonPath('status', false); + } +} diff --git a/tests/Feature/Api/V1/ScannerControllerTest.php b/tests/Feature/Api/V1/ScannerControllerTest.php new file mode 100644 index 00000000..8507c18d --- /dev/null +++ b/tests/Feature/Api/V1/ScannerControllerTest.php @@ -0,0 +1,42 @@ +seedApiTestConfig(); + } + + public function test_process_requires_a_badge_code(): void + { + $this->postJson('/api/v1/scanner/process', ['badgeCode' => '']) + ->assertStatus(400) + ->assertJsonPath('status', false) + ->assertJsonPath('ok', false); + } + + public function test_process_returns_404_for_unknown_badge(): void + { + $this->postJson('/api/v1/scanner/process', ['badgeCode' => 'NO-SUCH-BADGE']) + ->assertNotFound() + ->assertJsonPath('status', false) + ->assertJsonPath('ok', false); + } + + public function test_process_accepts_alternative_badge_field_names(): void + { + $this->postJson('/api/v1/scanner/process', ['barcode' => 'STILL-UNKNOWN']) + ->assertNotFound() + ->assertJsonPath('status', false); + } +} diff --git a/tests/Feature/Api/V1/System/StatsControllerTest.php b/tests/Feature/Api/V1/System/StatsControllerTest.php new file mode 100644 index 00000000..18adb068 --- /dev/null +++ b/tests/Feature/Api/V1/System/StatsControllerTest.php @@ -0,0 +1,50 @@ +getJson('/api/v1/stats') + ->assertStatus(401); + } + + public function test_stats_index_returns_rows_for_authenticated_user(): void + { + Stats::query()->create([ + 'students' => 12, + 'teachers' => 3, + 'admins' => 2, + 'users' => 17, + ]); + + Sanctum::actingAs(User::factory()->create()); + + $response = $this->getJson('/api/v1/stats'); + + $response->assertOk() + ->assertJsonPath('status', true) + ->assertJsonStructure(['status', 'message', 'data']); + + $this->assertSame(12, $response->json('data.0.students')); + } + + public function test_stats_index_returns_empty_array_when_no_stats(): void + { + Sanctum::actingAs(User::factory()->create()); + + $this->getJson('/api/v1/stats') + ->assertOk() + ->assertJsonPath('status', true) + ->assertJsonPath('data', []); + } +} diff --git a/tests/Feature/Api/V1/Utilities/ProofreadControllerTest.php b/tests/Feature/Api/V1/Utilities/ProofreadControllerTest.php new file mode 100644 index 00000000..1935f36d --- /dev/null +++ b/tests/Feature/Api/V1/Utilities/ProofreadControllerTest.php @@ -0,0 +1,67 @@ +postJson('/api/proofread', ['text' => 'hello']) + ->assertStatus(401); + } + + public function test_proofread_rejects_empty_text(): void + { + Sanctum::actingAs(User::factory()->create()); + + $this->postJson('/api/proofread', ['text' => '']) + ->assertStatus(422) + ->assertJsonPath('ok', false); + } + + public function test_proofread_rejects_text_that_is_too_long(): void + { + Sanctum::actingAs(User::factory()->create()); + + $this->postJson('/api/proofread', ['text' => str_repeat('a', 20001)]) + ->assertStatus(422) + ->assertJsonPath('ok', false); + } + + public function test_proofread_proxies_languagetool_and_returns_result(): void + { + Http::fake([ + 'api.languagetool.org/*' => Http::response(['matches' => []], 200), + ]); + + Sanctum::actingAs(User::factory()->create()); + + $this->postJson('/api/proofread', ['text' => 'This are a test.']) + ->assertOk() + ->assertJsonPath('ok', true) + ->assertJsonStructure(['ok', 'result', 'csrfHash']); + + Http::assertSent(fn ($request) => str_contains($request->url(), 'languagetool.org')); + } + + public function test_proofread_returns_502_when_upstream_fails(): void + { + Http::fake([ + 'api.languagetool.org/*' => Http::response('error', 500), + ]); + + Sanctum::actingAs(User::factory()->create()); + + $this->postJson('/api/proofread', ['text' => 'Some text to check.']) + ->assertStatus(502) + ->assertJsonPath('ok', false); + } +} diff --git a/tests/Feature/Api/V1/Workflows/ScenarioAFamilyEnrollmentTest.php b/tests/Feature/Api/V1/Workflows/ScenarioAFamilyEnrollmentTest.php new file mode 100644 index 00000000..f4a8214b --- /dev/null +++ b/tests/Feature/Api/V1/Workflows/ScenarioAFamilyEnrollmentTest.php @@ -0,0 +1,104 @@ +seedE2EConfiguration(); + + $admin = $this->actingAsApiAdministrator(); + $classSectionId = $this->seedClassSection(701, '5A', 5); + $teacher = $this->createApiUserWithRole('teacher'); + $this->seedTeacherClassAssignment($teacher->id, $classSectionId); + + $parentId = $this->createParentAccountViaApi([ + 'firstname' => 'Khadija', + 'lastname' => 'Saleh', + 'email' => 'khadija.saleh@example.test', + ]); + $studentId = $this->createStudentViaApi($parentId, [ + 'firstname' => 'Yusuf', + 'lastname' => 'Saleh', + 'name' => 'Khadija Saleh', + 'email' => 'khadija.saleh@example.test', + ]); + + $this->assignStudentToClassViaApi($studentId, $classSectionId) + ->assertOk() + ->assertJsonPath('ok', true); + + $this->postEnrollmentStatuses([$studentId => 'enrolled'])->assertOk(); + + $this->assertDatabaseHas('enrollments', [ + 'student_id' => $studentId, + 'parent_id' => $parentId, + 'enrollment_status' => 'enrolled', + ]); + + // Teacher sees student in roster. + $this->actingAs($teacher, 'api'); + $this->getJson('/api/v1/teachers/classes?class_section_id=' . $classSectionId) + ->assertOk() + ->assertJsonPath('ok', true); + + $studentIds = array_column($this->getJson('/api/v1/teachers/classes?class_section_id=' . $classSectionId)->json('students'), 'student_id'); + $this->assertContains($studentId, $studentIds); + + // Parent sees own profile. + $parent = User::query()->findOrFail($parentId); + $this->actingAs($parent, 'api'); + $this->getJson('/api/v1/parents/profile') + ->assertOk() + ->assertJsonPath('ok', true); + + // Admin can search the new user. + $this->actingAs($admin, 'api'); + $this->getJson('/api/v1/administrator/dashboard/user-search?query=Saleh') + ->assertOk(); + } + + public function test_parent_cannot_update_unrelated_student(): void + { + Event::fake(); + $this->seedE2EConfiguration(); + + $this->actingAsApiAdministrator(); + $parentA = $this->createApiUserWithRole('parent'); + $parentB = $this->createApiUserWithRole('parent'); + + $studentB = $this->createStudentViaApi($parentB->id, [ + 'firstname' => 'Other', + 'lastname' => 'Child', + ]); + + $this->actingAs($parentA, 'api'); + $this->patchJson('/api/v1/parents/students/' . $studentB, [ + 'firstname' => 'Hacked', + 'lastname' => 'Child', + 'dob' => '2015-01-01', + 'gender' => 'Male', + ])->assertNotFound(); + + $this->assertDatabaseHas('students', [ + 'id' => $studentB, + 'firstname' => 'Other', + ]); + } +} diff --git a/tests/Feature/Api/V1/Workflows/ScenarioBSundaySchoolDayTest.php b/tests/Feature/Api/V1/Workflows/ScenarioBSundaySchoolDayTest.php new file mode 100644 index 00000000..2601f88b --- /dev/null +++ b/tests/Feature/Api/V1/Workflows/ScenarioBSundaySchoolDayTest.php @@ -0,0 +1,107 @@ +seedTeacherClassWithStudent(); + $teacher = $world['teacher']; + $classSectionId = $world['class_section_id']; + $studentId = $world['student_id']; + $parent = $world['parent']; + + $this->actingAs($teacher, 'api'); + + $form = $this->getJson('/api/v1/attendance/teacher/form?class_section_id=' . $classSectionId) + ->assertOk(); + + $date = (string) ($form->json('data.current_sunday') ?? '2025-10-05'); + $submit = $this->postJson('/api/v1/attendance/teacher/submit', [ + 'class_section_id' => $classSectionId, + 'date' => $date, + 'attendance' => [ + ['student_id' => $studentId, 'status' => 'present'], + ], + 'teachers' => [ + (string) $teacher->id => ['status' => 'present'], + ], + ]); + + $submit->assertOk(); + $this->assertDatabaseHas('attendance_data', [ + 'student_id' => $studentId, + 'class_section_id' => $classSectionId, + 'status' => 'present', + 'school_year' => self::E2E_SCHOOL_YEAR, + ]); + + // Parent views attendance for their child. + $this->actingAs($parent, 'api'); + $this->getJson('/api/v1/parents/attendance?school_year=' . self::E2E_SCHOOL_YEAR) + ->assertOk() + ->assertJsonPath('ok', true); + } + + public function test_teacher_cannot_submit_attendance_for_unassigned_class(): void + { + $world = $this->seedTeacherClassWithStudent(); + $otherSectionId = $this->seedClassSection(999, '9Z', 9); + $otherTeacher = $this->createApiUserWithRole('teacher'); + $this->seedTeacherClassAssignment($otherTeacher->id, $otherSectionId); + + $this->actingAs($world['teacher'], 'api'); + $this->postJson('/api/v1/attendance/teacher/submit', [ + 'class_section_id' => $otherSectionId, + 'date' => '2025-10-05', + 'attendance' => [ + ['student_id' => $world['student_id'], 'status' => 'present'], + ], + 'teachers' => [ + (string) $world['teacher']->id => ['status' => 'present'], + ], + ])->assertStatus(422); + + $this->assertDatabaseMissing('attendance_data', [ + 'student_id' => $world['student_id'], + 'class_section_id' => $otherSectionId, + ]); + } + + public function test_admin_views_daily_attendance_summary(): void + { + $world = $this->seedTeacherClassWithStudent(); + $admin = $this->actingAsApiAdministrator(); + + DB::table('attendance_data')->insert([ + 'class_id' => 3, + 'class_section_id' => $world['class_section_id'], + 'student_id' => $world['student_id'], + 'school_id' => '1', + 'date' => '2025-10-05', + 'status' => 'late', + 'semester' => self::E2E_SEMESTER, + 'school_year' => self::E2E_SCHOOL_YEAR, + 'created_at' => now(), + 'updated_at' => now(), + ]); + + $this->actingAs($admin, 'api'); + $this->getJson('/api/v1/attendance/admin/daily?date=2025-10-05') + ->assertOk(); + } +} diff --git a/tests/Feature/Api/V1/Workflows/ScenarioCAcademicTermTest.php b/tests/Feature/Api/V1/Workflows/ScenarioCAcademicTermTest.php new file mode 100644 index 00000000..b7fa6038 --- /dev/null +++ b/tests/Feature/Api/V1/Workflows/ScenarioCAcademicTermTest.php @@ -0,0 +1,95 @@ +seedTeacherClassWithStudent(); + $teacher = $world['teacher']; + $classSectionId = $world['class_section_id']; + $studentId = $world['student_id']; + + DB::table('homework')->insert([ + 'student_id' => $studentId, + 'school_id' => 1, + 'class_section_id' => $classSectionId, + 'updated_by' => $teacher->id, + 'homework_index' => 1, + 'score' => 85, + 'semester' => self::E2E_SEMESTER, + 'school_year' => self::E2E_SCHOOL_YEAR, + 'created_at' => now(), + 'updated_at' => now(), + ]); + + DB::table('semester_scores')->insert([ + 'student_id' => $studentId, + 'class_section_id' => $classSectionId, + 'school_id' => 'E2E-SCORE-1', + 'school_year' => self::E2E_SCHOOL_YEAR, + 'semester' => self::E2E_SEMESTER, + 'midterm_exam_score' => '85', + 'participation_score' => '10', + 'created_at' => now(), + 'updated_at' => now(), + ]); + + $this->actingAs($teacher, 'api'); + + $this->getJson('/api/v1/scores/overview?class_section_id=' . $classSectionId . '&semester=' . self::E2E_SEMESTER . '&school_year=' . self::E2E_SCHOOL_YEAR) + ->assertOk() + ->assertJsonPath('ok', true); + + $this->postJson('/api/v1/scores/lock', [ + 'class_section_id' => $classSectionId, + 'semester' => self::E2E_SEMESTER, + 'school_year' => self::E2E_SCHOOL_YEAR, + 'missing_ok' => [], + ])->assertOk()->assertJsonPath('ok', true); + + $this->assertDatabaseHas('grading_locks', [ + 'class_section_id' => $classSectionId, + 'semester' => self::E2E_SEMESTER, + 'school_year' => self::E2E_SCHOOL_YEAR, + 'is_locked' => 1, + ]); + + $this->getJson('/api/v1/reports/report-cards/students/' . $studentId . '?school_year=' . self::E2E_SCHOOL_YEAR . '&semester=' . self::E2E_SEMESTER) + ->assertOk(); + } + + public function test_teacher_invalid_homework_score_is_rejected(): void + { + $world = $this->seedTeacherClassWithStudent(); + + $this->actingAs($world['teacher'], 'api'); + $response = $this->postJson('/api/v1/scores/homework', [ + 'class_section_id' => $world['class_section_id'], + 'semester' => self::E2E_SEMESTER, + 'school_year' => self::E2E_SCHOOL_YEAR, + 'scores' => [ + $world['student_id'] => [ + 1 => 999, + ], + ], + ]); + + $this->assertGreaterThanOrEqual(400, $response->getStatusCode()); + $this->assertNotSame(true, $response->json('ok')); + } +} diff --git a/tests/Feature/Api/V1/Workflows/ScenarioDFamilyBillingTest.php b/tests/Feature/Api/V1/Workflows/ScenarioDFamilyBillingTest.php new file mode 100644 index 00000000..4d5d7fa0 --- /dev/null +++ b/tests/Feature/Api/V1/Workflows/ScenarioDFamilyBillingTest.php @@ -0,0 +1,92 @@ +seedTeacherClassWithStudent(); + $admin = $this->actingAsApiAdministrator(); + $parentId = $world['parent_id']; + $studentId = $world['student_id']; + + DB::table('enrollments')->insert([ + 'parent_id' => $parentId, + 'student_id' => $studentId, + 'school_year' => self::E2E_SCHOOL_YEAR, + 'semester' => self::E2E_SEMESTER, + 'enrollment_status' => 'payment pending', + 'enrollment_date' => '2025-09-01', + ]); + + $invoiceId = $this->seedInvoiceForParent($parentId); + $voucherId = $this->seedDiscountVoucher(); + + $this->actingAs($admin, 'api'); + $this->postJson('/api/v1/discounts/apply', [ + 'voucher_id' => $voucherId, + 'parent_ids' => [$parentId], + 'allow_additional' => true, + ])->assertOk()->assertJsonPath('ok', true); + + $chargeResponse = $this->postJson('/api/v1/finance/event-charges', [ + 'event_name' => 'Science Fair', + 'description' => 'Materials fee', + 'amount' => 15.00, + 'parent_id' => $parentId, + 'student_id' => $studentId, + 'school_year' => self::E2E_SCHOOL_YEAR, + 'semester' => self::E2E_SEMESTER, + ]); + $chargeResponse->assertOk()->assertJsonPath('ok', true); + $chargeId = (int) DB::table('event_charges') + ->where('parent_id', $parentId) + ->where('student_id', $studentId) + ->where('event_name', 'Science Fair') + ->orderByDesc('id') + ->value('id'); + $this->assertGreaterThan(0, $chargeId); + + $this->postJson('/api/v1/finance/event-charges/' . $chargeId . '/approve') + ->assertOk() + ->assertJsonPath('ok', true); + + $this->postJson('/api/v1/finance/payments', [ + 'parent_id' => $parentId, + 'invoice_id' => $invoiceId, + 'total_amount' => 100, + 'number_of_installments' => 1, + 'payment_date' => '2025-10-01', + 'payment_method' => 'cash', + 'semester' => self::E2E_SEMESTER, + 'school_year' => self::E2E_SCHOOL_YEAR, + ])->assertStatus(201)->assertJsonPath('ok', true); + + $this->assertDatabaseHas('payments', [ + 'parent_id' => $parentId, + 'invoice_id' => $invoiceId, + ]); + + $this->getJson('/api/v1/finance/payments/parent/' . $parentId . '?school_year=' . self::E2E_SCHOOL_YEAR) + ->assertOk() + ->assertJsonPath('ok', true); + + $this->getJson('/api/v1/finance/unpaid-parents') + ->assertOk() + ->assertJsonPath('ok', true); + } +} diff --git a/tests/Feature/Api/V1/Workflows/ScenarioEFinanceEdgeTest.php b/tests/Feature/Api/V1/Workflows/ScenarioEFinanceEdgeTest.php new file mode 100644 index 00000000..115544aa --- /dev/null +++ b/tests/Feature/Api/V1/Workflows/ScenarioEFinanceEdgeTest.php @@ -0,0 +1,122 @@ +seedE2EConfiguration(); + $this->seedClosedSchoolYear(); + $admin = $this->actingAsApiAdministrator(); + + $parent = $this->createApiUserWithRole('parent'); + $priorInvoiceId = $this->seedPriorYearUnpaidInvoice($parent->id); + + $preview = $this->getJson('/api/v1/finance/carryforwards/preview?' . http_build_query([ + 'from_school_year' => self::E2E_PREV_SCHOOL_YEAR, + 'to_school_year' => self::E2E_SCHOOL_YEAR, + 'parent_id' => $parent->id, + ])); + $preview->assertOk()->assertJsonPath('ok', true); + $this->assertNotEmpty($preview->json('preview.rows')); + + $draft = $this->postJson('/api/v1/finance/carryforwards/draft', [ + 'from_school_year' => self::E2E_PREV_SCHOOL_YEAR, + 'to_school_year' => self::E2E_SCHOOL_YEAR, + 'parent_id' => $parent->id, + 'reason' => 'E2E carryforward', + ]); + $draft->assertOk()->assertJsonPath('ok', true); + $this->assertNotEmpty($draft->json('result.created')); + + $carryforwardId = (int) $draft->json('result.created.0.id'); + $this->postJson('/api/v1/finance/carryforwards/' . $carryforwardId . '/approve') + ->assertOk() + ->assertJsonPath('ok', true); + + $posted = $this->postJson('/api/v1/finance/carryforwards/' . $carryforwardId . '/post') + ->assertOk() + ->assertJsonPath('ok', true); + $newInvoiceId = (int) ($posted->json('carryforward.posted_invoice_id') ?? 0); + $this->assertGreaterThan(0, $newInvoiceId); + + $plan = $this->postJson('/api/v1/finance/invoices/' . $newInvoiceId . '/installment-plans', [ + 'number_of_installments' => 2, + 'first_due_date' => '2025-09-15', + 'interval_months' => 1, + ])->assertCreated()->assertJsonPath('ok', true); + + $planId = (int) $plan->json('plan.id'); + $this->postJson('/api/v1/finance/installment-plans/' . $planId . '/activate') + ->assertOk() + ->assertJsonPath('ok', true); + + $paymentId = DB::table('payments')->insertGetId([ + 'parent_id' => $parent->id, + 'invoice_id' => $newInvoiceId, + 'total_amount' => 75.00, + 'paid_amount' => 75.00, + 'balance' => 0, + 'number_of_installments' => 1, + 'payment_method' => 'cash', + 'payment_date' => '2025-09-20', + 'school_year' => self::E2E_SCHOOL_YEAR, + 'semester' => self::E2E_SEMESTER, + 'status' => 'Paid', + 'created_at' => now(), + 'updated_at' => now(), + ]); + + $this->postJson('/api/v1/finance/payments/' . $paymentId . '/allocate-installments', []) + ->assertOk() + ->assertJsonPath('ok', true); + + $this->getJson('/api/v1/finance/installments/overdue') + ->assertOk() + ->assertJsonPath('ok', true); + + $this->postJson('/api/v1/finance/parent-payment-followups/' . $parent->id . '/note', [ + 'invoice_id' => $newInvoiceId, + 'school_year' => self::E2E_SCHOOL_YEAR, + 'note' => 'Called parent about prior-year balance.', + 'contact_method' => 'phone', + ])->assertCreated()->assertJsonPath('ok', true); + + $this->postJson('/api/v1/finance/parent-payment-followups/' . $parent->id . '/promise-to-pay', [ + 'invoice_id' => $newInvoiceId, + 'promise_to_pay_date' => '2025-10-15', + 'promise_to_pay_amount' => 50, + ])->assertCreated()->assertJsonPath('ok', true); + + $this->postJson('/api/v1/finance/parent-payment-followups/' . $parent->id . '/resolve', [ + 'invoice_id' => $newInvoiceId, + 'note' => 'Balance plan agreed.', + ])->assertCreated()->assertJsonPath('ok', true); + + $this->assertDatabaseHas('finance_balance_carryforwards', [ + 'id' => $carryforwardId, + 'parent_id' => $parent->id, + 'status' => 'posted_to_new_year', + ]); + + $this->assertDatabaseHas('invoices', [ + 'id' => $priorInvoiceId, + 'parent_id' => $parent->id, + 'school_year' => self::E2E_PREV_SCHOOL_YEAR, + ]); + } +} diff --git a/tests/Feature/Api/V1/Workflows/ScenarioFTeacherSubmissionTest.php b/tests/Feature/Api/V1/Workflows/ScenarioFTeacherSubmissionTest.php new file mode 100644 index 00000000..045b90a9 --- /dev/null +++ b/tests/Feature/Api/V1/Workflows/ScenarioFTeacherSubmissionTest.php @@ -0,0 +1,93 @@ +seedTeacherClassWithStudent(); + $admin = $this->actingAsApiAdministrator(); + $teacher = $world['teacher']; + $classSectionId = $world['class_section_id']; + + $this->actingAs($admin, 'api'); + $report = $this->getJson('/api/v1/administrator/teacher-submissions'); + $report->assertOk(); + $this->assertNotEmpty($report->json('rows')); + + $row = collect($report->json('rows')) + ->firstWhere('class_section_id', $classSectionId); + $this->assertNotNull($row); + $this->assertNotEmpty($row['missing_items'] ?? []); + + $notify = $this->postJson('/api/v1/administrator/teacher-submissions/notify', [ + 'notify' => [ + $classSectionId => [ + $teacher->id => 1, + ], + ], + 'missing_items' => [ + $classSectionId => [ + $teacher->id => base64_encode(json_encode($row['missing_items'])), + ], + ], + ]); + + $notify->assertOk()->assertJsonPath('sent', 1); + + $this->assertDatabaseHas('teacher_submission_notification_history', [ + 'teacher_id' => $teacher->id, + 'class_section_id' => $classSectionId, + 'admin_id' => $admin->id, + 'notification_category' => 'teacher_submissions', + ]); + + // Teacher submits missing homework after reminder. + $this->actingAs($teacher, 'api'); + $this->postJson('/api/v1/scores/homework', [ + 'class_section_id' => $classSectionId, + 'semester' => self::E2E_SEMESTER, + 'school_year' => self::E2E_SCHOOL_YEAR, + 'scores' => [ + $world['student_id'] => [ + 1 => 88, + ], + ], + ])->assertOk()->assertJsonPath('ok', true); + + $this->assertDatabaseHas('homework', [ + 'student_id' => $world['student_id'], + 'class_section_id' => $classSectionId, + 'homework_index' => 1, + 'score' => 88, + ]); + } + + public function test_duplicate_notify_requires_valid_payload(): void + { + $admin = $this->actingAsApiAdministrator(); + + $this->actingAs($admin, 'api'); + $this->postJson('/api/v1/administrator/teacher-submissions/notify', [ + 'missing_items' => [], + ])->assertStatus(422) + ->assertJsonValidationErrors(['notify']); + } +} diff --git a/tests/Feature/Api/V1/Workflows/ScenarioGPrintBadgeTest.php b/tests/Feature/Api/V1/Workflows/ScenarioGPrintBadgeTest.php new file mode 100644 index 00000000..29f93bc8 --- /dev/null +++ b/tests/Feature/Api/V1/Workflows/ScenarioGPrintBadgeTest.php @@ -0,0 +1,75 @@ +seedTeacherClassWithStudent(); + $teacher = $world['teacher']; + $classSectionId = $world['class_section_id']; + $admin = $this->actingAsApiAdministrator(); + + $this->actingAs($teacher, 'api'); + $create = $this->postJson('/api/v1/print-requests/hand-copy', [ + 'class_id' => $classSectionId, + 'num_copies' => 2, + 'required_by' => '2026-09-13 11:00:00', + 'pickup_method' => 'Self Pickup', + ]); + $create->assertCreated(); + $requestId = (int) $create->json('data.print_request.id'); + + $this->actingAs($admin, 'api'); + foreach (['assigned', 'done', 'delivered'] as $status) { + $this->patchJson('/api/v1/print-requests/' . $requestId . '/status', [ + 'status' => $status, + ])->assertOk(); + } + + $this->assertDatabaseHas('print_requests', [ + 'id' => $requestId, + 'status' => 'delivered', + ]); + + $badgeTag = 'E2E-BADGE-' . random_int(10000, 99999); + DB::table('users')->where('id', $teacher->id)->update(['rfid_tag' => $badgeTag]); + + $this->postJson('/api/v1/badges/log-print', [ + 'user_ids' => [$teacher->id], + 'school_year' => self::E2E_SCHOOL_YEAR, + 'roles' => [(string) $teacher->id => 'Teacher'], + 'classes' => [(string) $teacher->id => '3A'], + ])->assertOk()->assertJsonPath('ok', true); + + $this->postJson('/api/v1/badge_scan/scan', [ + 'badge_scan' => $badgeTag, + 'school_year' => self::E2E_SCHOOL_YEAR, + 'semester' => self::E2E_SEMESTER, + ])->assertOk(); + + $this->assertDatabaseHas('scan_log', [ + 'user_id' => $teacher->id, + 'card_id' => $badgeTag, + ]); + + $this->actingAs($admin, 'api'); + $this->getJson('/api/v1/badge_scan/logs') + ->assertOk() + ->assertJsonPath('status', true); + } +} diff --git a/tests/Feature/Api/V1/Workflows/ScenarioHSecurityUnauthorizedAccessTest.php b/tests/Feature/Api/V1/Workflows/ScenarioHSecurityUnauthorizedAccessTest.php new file mode 100644 index 00000000..d34d18df --- /dev/null +++ b/tests/Feature/Api/V1/Workflows/ScenarioHSecurityUnauthorizedAccessTest.php @@ -0,0 +1,74 @@ +seedE2EConfiguration(); + $world = $this->seedTeacherClassWithStudent(); + $parentA = $this->createApiUserWithRole('parent'); + $teacher = $world['teacher']; + $otherSectionId = $this->seedClassSection(902, '8B', 8); + $otherTeacher = $this->createApiUserWithRole('teacher'); + $this->seedTeacherClassAssignment($otherTeacher->id, $otherSectionId); + + // Parent cannot update another family's student. + $this->actingAs($parentA, 'api'); + $this->patchJson('/api/v1/parents/students/' . $world['student_id'], [ + 'firstname' => 'Blocked', + 'lastname' => 'Update', + 'dob' => '2014-01-01', + 'gender' => 'Male', + ])->assertNotFound(); + + // Teacher cannot submit attendance for a class they are not assigned to. + $this->actingAs($teacher, 'api'); + $this->postJson('/api/v1/attendance/teacher/submit', [ + 'class_section_id' => $otherSectionId, + 'date' => '2025-10-05', + 'attendance' => [ + ['student_id' => $world['student_id'], 'status' => 'present'], + ], + 'teachers' => [ + (string) $teacher->id => ['status' => 'present'], + ], + ])->assertStatus(422); + + // Teacher cannot access admin-only dashboard routes. + $this->getJson('/api/v1/administrator/dashboard/metrics')->assertForbidden(); + $this->getJson('/api/v1/print-requests/admin')->assertForbidden(); + + // Suspended user cannot log in after valid credentials. + $suspended = $this->createApiUserWithRole('parent', [ + 'email' => 'suspended.user@example.test', + 'password' => Hash::make('secret123'), + 'is_suspended' => 1, + ]); + + $this->postJson('/api/v1/auth/login', [ + 'email' => $suspended->email, + 'password' => 'secret123', + ])->assertStatus(403); + + // Anonymous requests are rejected before business logic. + $this->actingAsGuest('api'); + $this->getJson('/api/v1/finance/invoices/management')->assertUnauthorized(); + $this->getJson('/api/v1/administrator/dashboard/metrics')->assertUnauthorized(); + $this->getJson('/api/v1/parents/profile')->assertUnauthorized(); + } +} diff --git a/tests/Feature/Api/V1/Workflows/StudentEnrollmentWorkflowTest.php b/tests/Feature/Api/V1/Workflows/StudentEnrollmentWorkflowTest.php new file mode 100644 index 00000000..aafc30fc --- /dev/null +++ b/tests/Feature/Api/V1/Workflows/StudentEnrollmentWorkflowTest.php @@ -0,0 +1,282 @@ +actingAsApiAdministrator(); + $parentRoleId = (int) Role::query()->where('name', 'parent')->value('id'); + $this->assertGreaterThan(0, $parentRoleId); + + // 2. Create a brand-new parent account. + $createParent = $this->postJson('/api/v1/users', [ + 'firstname' => 'Khadija', + 'lastname' => 'Saleh', + 'gender' => 'Female', + 'cellphone' => '5551234567', + 'email' => 'khadija.saleh@example.test', + 'address_street' => '42 Cedar Lane', + 'city' => 'Brooklyn', + 'state' => 'NY', + 'zip' => '11201', + 'accept_school_policy' => true, + 'role_id' => $parentRoleId, + 'password' => 'secret123', + 'semester' => self::SEMESTER, + 'school_year' => self::SCHOOL_YEAR, + 'user_type' => 'primary', + 'status' => 'Active', + 'is_verified' => true, + ]); + + $createParent->assertStatus(201)->assertJsonPath('ok', true); + $parentId = (int) $createParent->json('user_id'); + $this->assertGreaterThan(0, $parentId); + + $this->assertDatabaseHas('users', [ + 'id' => $parentId, + 'email' => 'khadija.saleh@example.test', + ]); + $this->assertDatabaseHas('user_roles', [ + 'user_id' => $parentId, + 'role_id' => $parentRoleId, + ]); + + // 3. Register the parent's first student (triggers the first-child + // emergency-contact branch in StudentController@store). + $createStudent = $this->postJson('/api/v1/students', [ + 'firstname' => 'Yusuf', + 'lastname' => 'Saleh', + 'dob' => '2015-05-10', + 'age' => 10, + 'gender' => 'Male', + 'is_active' => true, + 'registration_grade' => '5', + 'is_new' => true, + 'photo_consent' => true, + 'parent_id' => $parentId, + 'registration_date' => '2025-09-01', + 'tuition_paid' => false, + 'semester' => self::SEMESTER, + 'year_of_registration' => 2025, + 'school_year' => self::SCHOOL_YEAR, + // first-child contact details: + 'name' => 'Khadija Saleh', + 'cellphone' => '5551234567', + 'email' => 'khadija.saleh@example.test', + 'relation' => 'Mother', + ]); + + $createStudent->assertStatus(201)->assertJsonPath('ok', true); + $studentId = (int) $createStudent->json('student.id'); + $this->assertGreaterThan(0, $studentId); + + $this->assertDatabaseHas('students', [ + 'id' => $studentId, + 'parent_id' => $parentId, + 'firstname' => 'Yusuf', + 'lastname' => 'Saleh', + ]); + $this->assertDatabaseHas('emergency_contacts', [ + 'parent_id' => $parentId, + 'emergency_contact_name' => 'Khadija Saleh', + 'relation' => 'Mother', + ]); + + // 4. Enrol the student into a class section. + $classSectionId = $this->seedClassSection(); + $enroll = $this->postJson('/api/v1/students/assign-class', [ + 'student_id' => $studentId, + 'class_section_id' => [$classSectionId], + ]); + + $enroll->assertOk()->assertJsonPath('ok', true); + $this->assertDatabaseHas('student_class', [ + 'student_id' => $studentId, + 'class_section_id' => $classSectionId, + 'school_year' => self::SCHOOL_YEAR, + ]); + + // The class roster endpoint reflects the assignment. + $classes = $this->getJson('/api/v1/students/' . $studentId . '/classes'); + $classes->assertOk()->assertJsonPath('ok', true); + $assignedIds = array_column($classes->json('classes'), 'class_section_id'); + $this->assertContains($classSectionId, $assignedIds); + + // 5. Move the student to "enrolled" via the enrollment workflow. + $statusResponse = $this->post( + '/api/v1/administrator/enrollment-withdrawal/update-statuses?' . + http_build_query(['enrollment_status' => [$studentId => 'enrolled']]), + [] + ); + + $statusResponse->assertOk(); + $this->assertDatabaseHas('enrollments', [ + 'student_id' => $studentId, + 'parent_id' => $parentId, + 'school_year' => self::SCHOOL_YEAR, + 'semester' => self::SEMESTER, + 'enrollment_status' => 'enrolled', + 'admission_status' => 'accepted', + ]); + } + + public function test_admin_can_withdraw_an_enrolled_student(): void + { + Event::fake(); + + $this->actingAsApiAdministrator(); + $parentRoleId = (int) Role::query()->where('name', 'parent')->value('id'); + + $parentId = (int) $this->postJson('/api/v1/users', [ + 'firstname' => 'Omar', + 'lastname' => 'Hadid', + 'gender' => 'Male', + 'cellphone' => '5559876543', + 'email' => 'omar.hadid@example.test', + 'address_street' => '7 Maple Ct', + 'city' => 'Queens', + 'state' => 'NY', + 'zip' => '11367', + 'accept_school_policy' => true, + 'role_id' => $parentRoleId, + 'password' => 'secret123', + 'semester' => self::SEMESTER, + 'school_year' => self::SCHOOL_YEAR, + 'user_type' => 'primary', + 'status' => 'Active', + 'is_verified' => true, + ])->json('user_id'); + + $studentId = (int) $this->postJson('/api/v1/students', [ + 'firstname' => 'Layla', + 'lastname' => 'Hadid', + 'gender' => 'Female', + 'is_active' => true, + 'registration_grade' => '2', + 'parent_id' => $parentId, + 'semester' => self::SEMESTER, + 'school_year' => self::SCHOOL_YEAR, + 'name' => 'Omar Hadid', + 'cellphone' => '5559876543', + 'email' => 'omar.hadid@example.test', + 'relation' => 'Father', + ])->json('student.id'); + + // First enrol... + $this->post( + '/api/v1/administrator/enrollment-withdrawal/update-statuses?' . + http_build_query(['enrollment_status' => [$studentId => 'enrolled']]), + [] + )->assertOk(); + + // ...then withdraw. + $this->post( + '/api/v1/administrator/enrollment-withdrawal/update-statuses?' . + http_build_query(['enrollment_status' => [$studentId => 'withdrawn']]), + [] + )->assertOk(); + + $this->assertDatabaseHas('enrollments', [ + 'student_id' => $studentId, + 'parent_id' => $parentId, + 'school_year' => self::SCHOOL_YEAR, + 'enrollment_status' => 'withdrawn', + ]); + } + + public function test_creating_a_user_requires_a_valid_role(): void + { + $this->actingAsApiAdministrator(); + + $this->postJson('/api/v1/users', [ + 'firstname' => 'No', + 'lastname' => 'Role', + 'cellphone' => '5550001111', + 'email' => 'no.role@example.test', + 'address_street' => '1 Test St', + 'city' => 'Town', + 'state' => 'NY', + 'zip' => '10001', + 'accept_school_policy' => true, + 'role_id' => 999999, + 'password' => 'secret123', + 'semester' => self::SEMESTER, + ])->assertStatus(422) + ->assertJsonValidationErrors(['role_id']); + } + + public function test_student_registration_requires_first_child_contact_details(): void + { + $this->actingAsApiAdministrator(); + $parentRoleId = (int) Role::query()->where('name', 'parent')->value('id'); + + $parentId = (int) $this->postJson('/api/v1/users', [ + 'firstname' => 'First', + 'lastname' => 'Parent', + 'gender' => 'Female', + 'cellphone' => '5552223333', + 'email' => 'first.parent@example.test', + 'address_street' => '9 Birch Rd', + 'city' => 'Bronx', + 'state' => 'NY', + 'zip' => '10451', + 'accept_school_policy' => true, + 'role_id' => $parentRoleId, + 'password' => 'secret123', + 'semester' => self::SEMESTER, + 'school_year' => self::SCHOOL_YEAR, + ])->json('user_id'); + + // First student for a parent must include name/cellphone/email. + $this->postJson('/api/v1/students', [ + 'firstname' => 'Solo', + 'lastname' => 'Child', + 'parent_id' => $parentId, + 'school_year' => self::SCHOOL_YEAR, + ])->assertStatus(422) + ->assertJsonValidationErrors(['name', 'cellphone', 'email']); + } + + private function seedClassSection(int $classSectionId = 701, int $classId = 1): int + { + DB::table('classSection')->insert([ + 'class_id' => $classId, + 'class_section_id' => $classSectionId, + 'class_section_name' => '5A', + 'semester' => self::SEMESTER, + 'school_year' => self::SCHOOL_YEAR, + ]); + + return $classSectionId; + } +} diff --git a/tests/Unit/ApiLoginSecurityServiceTest.php b/tests/Unit/ApiLoginSecurityServiceTest.php index 3b589e43..ec4680e8 100644 --- a/tests/Unit/ApiLoginSecurityServiceTest.php +++ b/tests/Unit/ApiLoginSecurityServiceTest.php @@ -44,8 +44,12 @@ class ApiLoginSecurityServiceTest extends TestCase $this->assertTrue($payload['status']); $this->assertSame('jwt-token', $payload['token']); + $this->assertSame('jwt-token', $payload['access_token']); + $this->assertSame('bearer', $payload['token_type']); $this->assertSame(99, $payload['user']['id']); $this->assertSame('Amina Teacher', $payload['user']['name']); + $this->assertSame('Amina', $payload['user']['firstname']); + $this->assertSame('Teacher', $payload['user']['lastname']); $this->assertSame(42, $payload['user']['class_section_id']); $this->assertSame('Grade 5 - A', $payload['user']['class_section_name']); $this->assertTrue($payload['user']['roles']->Teacher); diff --git a/tests/Unit/AuthSessionServiceTest.php b/tests/Unit/AuthSessionServiceTest.php index d7dcf328..87e20fc5 100644 --- a/tests/Unit/AuthSessionServiceTest.php +++ b/tests/Unit/AuthSessionServiceTest.php @@ -13,9 +13,9 @@ class AuthSessionServiceTest extends TestCase protected function setUp(): void { parent::setUp(); - $urls = $this->createMock(ApplicationUrlService::class); - $urls->method('docsHomeUrl')->willReturn('http://example.org'); - $this->svc = new AuthSessionService($urls); + // ApplicationUrlService is final (cannot be mocked); these tests only exercise + // relative-path sanitization and never call docsHomeUrl(). + $this->svc = new AuthSessionService(new ApplicationUrlService()); } public function test_sanitize_redirect_relative(): void diff --git a/tests/Unit/Controllers/AttendanceCommentTemplateControllerTest.php b/tests/Unit/Controllers/AttendanceCommentTemplateControllerTest.php index dd76bc2e..26cf43d9 100644 --- a/tests/Unit/Controllers/AttendanceCommentTemplateControllerTest.php +++ b/tests/Unit/Controllers/AttendanceCommentTemplateControllerTest.php @@ -3,8 +3,7 @@ namespace Tests\Unit\Http\Controllers\Api; use App\Http\Controllers\Api\Attendance\AttendanceCommentTemplateController; -use App\Http\Requests\AttendanceCommentTemplate\StoreAttendanceCommentTemplateRequest; -use App\Http\Requests\AttendanceCommentTemplate\UpdateAttendanceCommentTemplateRequest; +use App\Services\ApplicationUrlService; use App\Services\AttendanceCommentTemplateService; use Illuminate\Http\Request; use Mockery; @@ -12,6 +11,11 @@ use Tests\TestCase; class AttendanceCommentTemplateControllerTest extends TestCase { + private function makeController(AttendanceCommentTemplateService $service): AttendanceCommentTemplateController + { + return new AttendanceCommentTemplateController($service, new ApplicationUrlService()); + } + protected function tearDown(): void { Mockery::close(); @@ -28,7 +32,7 @@ class AttendanceCommentTemplateControllerTest extends TestCase ['id' => 1, 'min_score' => 0, 'max_score' => 59, 'template_text' => 'Needs improvement', 'is_active' => true], ]); - $controller = new AttendanceCommentTemplateController($service); + $controller = $this->makeController($service); $request = Request::create('/api/attendance-comment-templates', 'GET'); @@ -50,7 +54,7 @@ class AttendanceCommentTemplateControllerTest extends TestCase ->with(true) ->andReturn([]); - $controller = new AttendanceCommentTemplateController($service); + $controller = $this->makeController($service); $request = Request::create('/api/attendance-comment-templates?active_only=1', 'GET'); @@ -75,7 +79,7 @@ class AttendanceCommentTemplateControllerTest extends TestCase 'is_active' => true, ]); - $controller = new AttendanceCommentTemplateController($service); + $controller = $this->makeController($service); $response = $controller->show(5); @@ -101,10 +105,9 @@ class AttendanceCommentTemplateControllerTest extends TestCase ->with($payload) ->andReturn(array_merge(['id' => 10], $payload)); - $controller = new AttendanceCommentTemplateController($service); + $controller = $this->makeController($service); - $request = Mockery::mock(StoreAttendanceCommentTemplateRequest::class); - $request->shouldReceive('validated')->once()->andReturn($payload); + $request = Request::create('/api/attendance-comment-templates', 'POST', $payload); $response = $controller->store($request); @@ -135,10 +138,9 @@ class AttendanceCommentTemplateControllerTest extends TestCase 'is_active' => false, ]); - $controller = new AttendanceCommentTemplateController($service); + $controller = $this->makeController($service); - $request = Mockery::mock(UpdateAttendanceCommentTemplateRequest::class); - $request->shouldReceive('validated')->once()->andReturn($payload); + $request = Request::create('/api/attendance-comment-templates/7', 'PUT', $payload); $response = $controller->update($request, 7); @@ -155,7 +157,7 @@ class AttendanceCommentTemplateControllerTest extends TestCase $service = Mockery::mock(AttendanceCommentTemplateService::class); $service->shouldReceive('delete')->once()->with(12); - $controller = new AttendanceCommentTemplateController($service); + $controller = $this->makeController($service); $response = $controller->destroy(12); @@ -176,7 +178,7 @@ class AttendanceCommentTemplateControllerTest extends TestCase ['id' => 1, 'min_score' => 0, 'max_score' => 49, 'template_text' => 'Low', 'is_active' => true], ]); - $controller = new AttendanceCommentTemplateController($service); + $controller = $this->makeController($service); $request = Request::create('/api/attendance-comment-templates/list-data', 'GET'); diff --git a/tests/Unit/Services/Notifications/NotificationUserListServiceTest.php b/tests/Unit/Services/Notifications/NotificationUserListServiceTest.php index 209e788a..b3709f3b 100644 --- a/tests/Unit/Services/Notifications/NotificationUserListServiceTest.php +++ b/tests/Unit/Services/Notifications/NotificationUserListServiceTest.php @@ -19,7 +19,7 @@ class NotificationUserListServiceTest extends TestCase 'title' => 'Unread', 'message' => 'Message 1', 'target_group' => 'parent', - 'delivery_channels' => json_encode(['in_app']), + 'delivery_channels' => 'in_app', 'scheduled_at' => now()->subHour(), 'created_at' => now(), 'updated_at' => now(), @@ -29,7 +29,7 @@ class NotificationUserListServiceTest extends TestCase 'title' => 'Read', 'message' => 'Message 2', 'target_group' => 'parent', - 'delivery_channels' => json_encode(['in_app']), + 'delivery_channels' => 'in_app', 'scheduled_at' => now()->subHours(2), 'created_at' => now(), 'updated_at' => now(), diff --git a/tests/Unit/Services/Parents/ParentAttendanceReportServiceTest.php b/tests/Unit/Services/Parents/ParentAttendanceReportServiceTest.php index e230f997..3321c784 100644 --- a/tests/Unit/Services/Parents/ParentAttendanceReportServiceTest.php +++ b/tests/Unit/Services/Parents/ParentAttendanceReportServiceTest.php @@ -17,6 +17,9 @@ class ParentAttendanceReportServiceTest extends TestCase public function test_form_data_returns_students(): void { + // Pin calendar so upcoming Sundays exist within the 2025-2026 school year. + $this->travelTo('2025-10-05 10:00:00'); + $this->seedConfig(); $parentId = $this->seedParent(); $this->seedStudent($parentId); diff --git a/tests/Unit/Services/Payments/PaymentEnrollmentEventServiceTest.php b/tests/Unit/Services/Payments/PaymentEnrollmentEventServiceTest.php index 6917bbba..4225103d 100644 --- a/tests/Unit/Services/Payments/PaymentEnrollmentEventServiceTest.php +++ b/tests/Unit/Services/Payments/PaymentEnrollmentEventServiceTest.php @@ -2,6 +2,7 @@ namespace Tests\Unit\Services\Payments; +use App\Services\ApplicationUrlService; use App\Services\Payments\PaymentEnrollmentEventService; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\DB; @@ -11,9 +12,14 @@ class PaymentEnrollmentEventServiceTest extends TestCase { use RefreshDatabase; + private function makeService(): PaymentEnrollmentEventService + { + return new PaymentEnrollmentEventService(new ApplicationUrlService()); + } + public function test_build_event_data_requires_parent(): void { - $service = new PaymentEnrollmentEventService(); + $service = $this->makeService(); $this->expectException(\RuntimeException::class); @@ -43,7 +49,7 @@ class PaymentEnrollmentEventServiceTest extends TestCase 'school_year' => '2025-2026', ]); - $service = new PaymentEnrollmentEventService(); + $service = $this->makeService(); [$parent, $students] = $service->buildEventData(10, [], '2025-2026', 'Fall', 'https://example.test/login'); $this->assertSame(10, $parent['user_id']); diff --git a/tests/Unit/Services/Payments/PaymentMissedCheckServiceTest.php b/tests/Unit/Services/Payments/PaymentMissedCheckServiceTest.php index 8faa9d38..8a41a5d9 100644 --- a/tests/Unit/Services/Payments/PaymentMissedCheckServiceTest.php +++ b/tests/Unit/Services/Payments/PaymentMissedCheckServiceTest.php @@ -23,7 +23,16 @@ class PaymentMissedCheckServiceTest extends TestCase 'id' => 1, 'firstname' => 'Parent', 'lastname' => 'Late', + 'cellphone' => '5551112222', 'email' => 'late@example.com', + 'address_street' => '123 Main', + 'city' => 'City', + 'state' => 'ST', + 'zip' => '12345', + 'accept_school_policy' => 1, + 'password' => bcrypt('secret'), + 'semester' => 'Spring', + 'school_year' => '2024-2025', 'status' => 'Active', ]); diff --git a/tests/Unit/Services/Payments/PaymentNotificationDispatchServiceTest.php b/tests/Unit/Services/Payments/PaymentNotificationDispatchServiceTest.php index d27d7dc6..74b9c2ba 100644 --- a/tests/Unit/Services/Payments/PaymentNotificationDispatchServiceTest.php +++ b/tests/Unit/Services/Payments/PaymentNotificationDispatchServiceTest.php @@ -24,7 +24,7 @@ class PaymentNotificationDispatchServiceTest extends TestCase $this->assertDatabaseHas('notifications', [ 'title' => 'Payment Reminder', 'message' => 'Reminder body', - 'target_group' => 'user', + 'target_group' => 'everyone', 'status' => 'sent', 'school_year' => '2025-2026', 'semester' => 'Fall', diff --git a/tests/Unit/Services/Payments/PaymentTestNotificationServiceTest.php b/tests/Unit/Services/Payments/PaymentTestNotificationServiceTest.php index f4c57ccf..6570cfb4 100644 --- a/tests/Unit/Services/Payments/PaymentTestNotificationServiceTest.php +++ b/tests/Unit/Services/Payments/PaymentTestNotificationServiceTest.php @@ -21,8 +21,38 @@ class PaymentTestNotificationServiceTest extends TestCase ]); DB::table('users')->insert([ - ['id' => 10, 'firstname' => 'Primary', 'lastname' => 'Parent', 'email' => 'primary@example.com', 'status' => 'Active'], - ['id' => 11, 'firstname' => 'Secondary', 'lastname' => 'Parent', 'email' => 'secondary@example.com', 'status' => 'Active'], + [ + 'id' => 10, + 'firstname' => 'Primary', + 'lastname' => 'Parent', + 'cellphone' => '5551112222', + 'email' => 'primary@example.com', + 'address_street' => '123 Main', + 'city' => 'City', + 'state' => 'ST', + 'zip' => '12345', + 'accept_school_policy' => 1, + 'password' => bcrypt('secret'), + 'semester' => 'Spring', + 'school_year' => '2024-2025', + 'status' => 'Active', + ], + [ + 'id' => 11, + 'firstname' => 'Secondary', + 'lastname' => 'Parent', + 'cellphone' => '5553334444', + 'email' => 'secondary@example.com', + 'address_street' => '456 Oak', + 'city' => 'City', + 'state' => 'ST', + 'zip' => '12345', + 'accept_school_policy' => 1, + 'password' => bcrypt('secret'), + 'semester' => 'Spring', + 'school_year' => '2024-2025', + 'status' => 'Active', + ], ]); DB::table('families')->insert([ diff --git a/tests/Unit/Services/Payments/PaypalPaymentServiceTest.php b/tests/Unit/Services/Payments/PaypalPaymentServiceTest.php index 9bb032cc..a7a93778 100644 --- a/tests/Unit/Services/Payments/PaypalPaymentServiceTest.php +++ b/tests/Unit/Services/Payments/PaypalPaymentServiceTest.php @@ -2,6 +2,7 @@ namespace Tests\Unit\Services\Payments; +use App\Services\ApplicationUrlService; use App\Services\Payments\PaypalPaymentService; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\DB; @@ -11,6 +12,11 @@ class PaypalPaymentServiceTest extends TestCase { use RefreshDatabase; + private function makeService(): PaypalPaymentService + { + return new PaypalPaymentService(new ApplicationUrlService()); + } + public function test_create_payment_falls_back_to_hosted_when_sdk_missing(): void { DB::table('payments')->insert([ @@ -27,7 +33,7 @@ class PaypalPaymentServiceTest extends TestCase 'status' => 'Pending', ]); - $service = new PaypalPaymentService(); + $service = $this->makeService(); $result = $service->createPayment(1); $this->assertSame('hosted', $result['mode']); @@ -36,7 +42,7 @@ class PaypalPaymentServiceTest extends TestCase public function test_execute_payment_throws_when_sdk_missing(): void { - $service = new PaypalPaymentService(); + $service = $this->makeService(); $this->expectException(\RuntimeException::class);