fix unit tests as well as missing code
API CI/CD / Validate (composer + pint) (push) Successful in 2m7s
API CI/CD / Test (PHPUnit) (push) Failing after 2m23s
API CI/CD / Build frontend assets (push) Successful in 2m18s
API CI/CD / Security audit (push) Successful in 31s
API CI/CD / Deploy to shared hosting (PHP) (push) Has been skipped

This commit is contained in:
root
2026-06-25 14:26:32 -04:00
parent fdfcd1f0e2
commit 940afe9319
115 changed files with 4554 additions and 290 deletions
+86 -69
View File
@@ -181,17 +181,19 @@ Route::post('set_authorized_user_password/{authorizedUserId}', [AuthorizedUserIn
/*
| Legacy paths without /v1: POST cannot use 301 reliably; alias to same handlers as v1 (auth:api).
| GET uses 301 to canonical /api/v1/...
| GET aliases return JSON behind auth so browser cookies do not grant API access.
*/
Route::middleware('auth:api')->prefix('attendance-templates')->group(function () {
Route::post('save', [AttendanceCommentTemplateController::class, 'legacySave']);
Route::post('delete', [AttendanceCommentTemplateController::class, 'legacyDelete']);
});
Route::permanentRedirect('attendance-templates', '/api/v1/attendance-templates');
Route::permanentRedirect('attendance-comment-templates', '/api/v1/attendance-comment-templates');
Route::permanentRedirect('attendance-comment-templates/list-data', '/api/v1/attendance-comment-templates/list-data');
Route::permanentRedirect('administrator/attendance-templates', '/api/v1/administrator/attendance-templates');
Route::middleware(['auth:api', 'throttle:60,1'])->group(function () {
Route::get('attendance-templates', [AttendanceCommentTemplateController::class, 'listData']);
Route::get('attendance-comment-templates', [AttendanceCommentTemplateController::class, 'index']);
Route::get('attendance-comment-templates/list-data', [AttendanceCommentTemplateController::class, 'listData']);
Route::get('administrator/attendance-templates', [AttendanceCommentTemplateController::class, 'bootstrapUrls']);
});
Route::prefix('v1')->group(function () {
// Public auth aliases without the `/auth` prefix.
@@ -246,16 +248,16 @@ Route::prefix('v1')->group(function () {
Route::get('/', [PreferencesController::class, 'show']);
Route::post('/', [PreferencesController::class, 'store']);
Route::get('list', [PreferencesController::class, 'index']);
Route::get('{userId}', [PreferencesController::class, 'showForUser']);
Route::put('{userId}', [PreferencesController::class, 'updateForUser']);
Route::delete('{userId}', [PreferencesController::class, 'destroy']);
Route::get('{userId}', [PreferencesController::class, 'showForUser'])->whereNumber('userId');
Route::put('{userId}', [PreferencesController::class, 'updateForUser'])->whereNumber('userId');
Route::delete('{userId}', [PreferencesController::class, 'destroy'])->whereNumber('userId');
});
Route::middleware('multi.auth')->prefix('nav-builder')->group(function () {
Route::get('menu', [NavBuilderController::class, 'menu']);
Route::get('data', [NavBuilderController::class, 'data']);
Route::post('/', [NavBuilderController::class, 'store']);
Route::delete('{id}', [NavBuilderController::class, 'destroy']);
Route::delete('{id}', [NavBuilderController::class, 'destroy'])->whereNumber('id');
Route::post('reorder', [NavBuilderController::class, 'reorder']);
});
@@ -452,16 +454,16 @@ Route::prefix('v1')->group(function () {
Route::middleware('admin.access')->prefix('role-permission')->group(function () {
Route::get('roles', [RolePermissionController::class, 'roles']);
Route::post('roles', [RolePermissionController::class, 'storeRole']);
Route::get('roles/{roleId}', [RolePermissionController::class, 'showRole']);
Route::patch('roles/{roleId}', [RolePermissionController::class, 'updateRole']);
Route::delete('roles/{roleId}', [RolePermissionController::class, 'deleteRole']);
Route::get('roles/{roleId}', [RolePermissionController::class, 'showRole'])->whereNumber('roleId');
Route::patch('roles/{roleId}', [RolePermissionController::class, 'updateRole'])->whereNumber('roleId');
Route::delete('roles/{roleId}', [RolePermissionController::class, 'deleteRole'])->whereNumber('roleId');
Route::get('users', [RolePermissionController::class, 'users']);
Route::post('users/{userId}/roles', [RolePermissionController::class, 'assignRoles']);
Route::post('users/{userId}/roles', [RolePermissionController::class, 'assignRoles'])->whereNumber('userId');
Route::get('permissions', [RolePermissionController::class, 'permissions']);
Route::post('permissions', [RolePermissionController::class, 'storePermission']);
Route::get('permissions/{permissionId}', [RolePermissionController::class, 'showPermission']);
Route::get('permissions/{permissionId}', [RolePermissionController::class, 'showPermission'])->whereNumber('permissionId');
Route::patch('permissions/{permissionId}', [RolePermissionController::class, 'updatePermission']);
Route::delete('permissions/{permissionId}', [RolePermissionController::class, 'deletePermission']);
@@ -485,7 +487,7 @@ Route::prefix('v1')->group(function () {
Route::get('{schoolYear}/promotion-preview', [SchoolYearController::class, 'promotionPreview'])->whereNumber('schoolYear');
});
Route::middleware(['auth:api', 'school_year.editable'])->prefix('attendance')->group(function () {
Route::middleware(['multi.auth', 'school_year.editable'])->prefix('attendance')->group(function () {
// Teacher
Route::get('/teacher/grid', [TeacherAttendanceApiController::class, 'grid']);
Route::get('/teacher/form', [TeacherAttendanceApiController::class, 'form']);
@@ -498,8 +500,8 @@ Route::prefix('v1')->group(function () {
// Staff/admin monthly/admin attendance
Route::get('/staff/month', [StaffAttendanceApiController::class, 'monthData']);
Route::get('/staff/admins', [StaffAttendanceApiController::class, 'admins']);
Route::post('/staff/admins/save', [StaffAttendanceApiController::class, 'saveAdmins']);
Route::get('/staff/admins', [StaffAttendanceApiController::class, 'admins'])->middleware('admin.access');
Route::post('/staff/admins/save', [StaffAttendanceApiController::class, 'saveAdmins'])->middleware('admin.access');
Route::post('/staff/cell', [StaffAttendanceApiController::class, 'saveCell']);
Route::get('/staff/month-csv', [StaffAttendanceApiController::class, 'monthCsv']);
@@ -527,7 +529,7 @@ Route::prefix('v1')->group(function () {
});
});
Route::middleware(app()->runningUnitTests() ? ['school_year.editable'] : ['multi.auth', 'school_year.editable'])->prefix('attendance-tracking')->group(function () {
Route::middleware(app()->runningUnitTests() ? ['throttle:120,1', 'school_year.editable'] : ['multi.auth', 'school_year.editable'])->prefix('attendance-tracking')->group(function () {
Route::get('/pending-violations', [AttendanceTrackingController::class, 'pendingViolations']);
Route::get('/notified-violations', [AttendanceTrackingController::class, 'notifiedViolations']);
Route::get('/student-case/{studentId}', [AttendanceTrackingController::class, 'studentCase']);
@@ -706,12 +708,12 @@ Route::prefix('v1')->group(function () {
Route::middleware('admin.access')->prefix('role-permissions')->group(function () {
Route::get('roles', [RolePermissionController::class, 'roles']);
Route::post('roles', [RolePermissionController::class, 'storeRole']);
Route::get('roles/{roleId}', [RolePermissionController::class, 'showRole']);
Route::patch('roles/{roleId}', [RolePermissionController::class, 'updateRole']);
Route::delete('roles/{roleId}', [RolePermissionController::class, 'deleteRole']);
Route::get('roles/{roleId}', [RolePermissionController::class, 'showRole'])->whereNumber('roleId');
Route::patch('roles/{roleId}', [RolePermissionController::class, 'updateRole'])->whereNumber('roleId');
Route::delete('roles/{roleId}', [RolePermissionController::class, 'deleteRole'])->whereNumber('roleId');
Route::get('users', [RolePermissionController::class, 'users']);
Route::post('users/{userId}/roles', [RolePermissionController::class, 'assignRoles']);
Route::post('users/{userId}/roles', [RolePermissionController::class, 'assignRoles'])->whereNumber('userId');
Route::get('permissions', [RolePermissionController::class, 'permissions']);
Route::post('permissions', [RolePermissionController::class, 'storePermission']);
@@ -727,7 +729,7 @@ Route::prefix('v1')->group(function () {
Route::get('/', [RoleSwitcherController::class, 'index']);
Route::post('switch', [RoleSwitcherController::class, 'switch']);
});
Route::prefix('settings/school-calendar')->group(function () {
Route::middleware('admin.access')->prefix('settings/school-calendar')->group(function () {
Route::get('options', [SchoolCalendarController::class, 'options']);
Route::get('events', [SchoolCalendarController::class, 'index']);
Route::get('events/{eventId}', [SchoolCalendarController::class, 'show']);
@@ -736,7 +738,7 @@ Route::prefix('v1')->group(function () {
Route::delete('events/{eventId}', [SchoolCalendarController::class, 'destroy']);
});
Route::prefix('settings/events')->group(function () {
Route::middleware('admin.access')->prefix('settings/events')->group(function () {
Route::get('/', [EventController::class, 'index']);
Route::post('/', [EventController::class, 'store']);
Route::get('charges/list', [EventController::class, 'charges']);
@@ -768,32 +770,38 @@ Route::prefix('v1')->group(function () {
Route::prefix('students')->group(function () {
Route::get('/', [StudentApiController::class, 'index']);
Route::post('/', [StudentApiController::class, 'store']);
Route::get('score-card/selectable', [StudentApiController::class, 'scoreCardSelectable']);
Route::get('assignments', [StudentApiController::class, 'assignments']);
Route::post('assign-class', [StudentApiController::class, 'assignClass']);
Route::post('remove-class', [StudentApiController::class, 'removeClass']);
Route::get('removed', [StudentApiController::class, 'removed']);
Route::post('set-active', [StudentApiController::class, 'setActive']);
Route::post('auto-distribute', [StudentApiController::class, 'autoDistribute']);
Route::get('promotion-totals', [StudentApiController::class, 'promotionTotals']);
Route::patch('{studentId}', [StudentApiController::class, 'update']);
Route::delete('{studentId}', [StudentApiController::class, 'destroy']);
Route::middleware('admin.access')->group(function () {
Route::post('/', [StudentApiController::class, 'store']);
Route::post('assign-class', [StudentApiController::class, 'assignClass']);
Route::post('remove-class', [StudentApiController::class, 'removeClass']);
Route::post('set-active', [StudentApiController::class, 'setActive']);
Route::post('auto-distribute', [StudentApiController::class, 'autoDistribute']);
Route::patch('{studentId}', [StudentApiController::class, 'update']);
Route::delete('{studentId}', [StudentApiController::class, 'destroy']);
});
Route::get('{studentId}', [StudentApiController::class, 'show']);
Route::get('{studentId}/classes', [StudentApiController::class, 'classes']);
Route::post('{studentId}/classes', [StudentApiController::class, 'assignClassForStudent']);
Route::delete('{studentId}/classes/{classSectionId}', [StudentApiController::class, 'removeClassForStudent']);
Route::post('{studentId}/promote', [StudentApiController::class, 'promote']);
Route::middleware('admin.access')->group(function () {
Route::post('{studentId}/classes', [StudentApiController::class, 'assignClassForStudent']);
Route::delete('{studentId}/classes/{classSectionId}', [StudentApiController::class, 'removeClassForStudent']);
Route::post('{studentId}/promote', [StudentApiController::class, 'promote']);
});
Route::get('{studentId}/attendance', [StudentApiController::class, 'attendance']);
Route::get('{studentId}/incidents', [StudentApiController::class, 'incidents']);
Route::get('{studentId}/scores', [StudentApiController::class, 'scores']);
Route::get('{studentId}/notes', [StudentApiController::class, 'notes']);
Route::get('{studentId}/parents', [StudentApiController::class, 'parents']);
Route::get('{studentId}/emergency-contacts', [StudentApiController::class, 'emergencyContacts']);
Route::post('{studentId}/emergency-contacts', [StudentApiController::class, 'addEmergencyContact']);
Route::patch('{studentId}/emergency-contacts/{contactId}', [StudentApiController::class, 'updateEmergencyContact']);
Route::post('{studentId}/badge_scan', [StudentApiController::class, 'updateBadgeScan']);
Route::post('{studentId}/photo', [StudentApiController::class, 'uploadPhoto']);
Route::middleware('admin.access')->group(function () {
Route::post('{studentId}/emergency-contacts', [StudentApiController::class, 'addEmergencyContact']);
Route::patch('{studentId}/emergency-contacts/{contactId}', [StudentApiController::class, 'updateEmergencyContact']);
Route::post('{studentId}/badge_scan', [StudentApiController::class, 'updateBadgeScan']);
Route::post('{studentId}/photo', [StudentApiController::class, 'uploadPhoto']);
});
Route::get('{studentId}/photo', [StudentApiController::class, 'photo']);
Route::get('{studentId}/report-card', [ReportCardsController::class, 'studentReport'])
->name('api.v1.students.report-card');
@@ -841,12 +849,12 @@ Route::prefix('v1')->group(function () {
Route::get('/', [UserController::class, 'index']);
Route::post('/', [UserController::class, 'store']);
Route::get('login-activity', [UserController::class, 'loginActivity']);
Route::get('{userId}', [UserController::class, 'show']);
Route::put('{userId}', [UserController::class, 'update']);
Route::delete('{userId}', [UserController::class, 'destroy']);
Route::get('{userId}', [UserController::class, 'show'])->whereNumber('userId');
Route::put('{userId}', [UserController::class, 'update'])->whereNumber('userId');
Route::delete('{userId}', [UserController::class, 'destroy'])->whereNumber('userId');
});
Route::prefix('administrator')->group(function () {
Route::middleware('admin.access')->prefix('administrator')->group(function () {
Route::get('login-activity', [UserController::class, 'loginActivity']);
});
@@ -874,7 +882,7 @@ Route::prefix('v1')->group(function () {
Route::post('send', [CommunicationController::class, 'send']);
});
Route::prefix('inventory')->group(function () {
Route::middleware('admin.access')->prefix('inventory')->group(function () {
Route::get('items', [InventoryController::class, 'index']);
Route::get('items/{id}', [InventoryController::class, 'show']);
Route::post('items', [InventoryController::class, 'store']);
@@ -962,7 +970,7 @@ Route::prefix('v1')->group(function () {
Route::post('{id}/export-quiz', [CompetitionWinnersController::class, 'exportQuiz']);
});
Route::prefix('discounts')->group(function () {
Route::middleware('admin.access')->prefix('discounts')->group(function () {
Route::get('/', [DiscountController::class, 'options']);
Route::get('options', [DiscountController::class, 'options']);
Route::post('apply', [DiscountController::class, 'apply']);
@@ -989,10 +997,12 @@ Route::prefix('v1')->group(function () {
Route::get('stakeholder-analysis/csv', [FinancialController::class, 'stakeholderAnalysisCsv']);
Route::get('parent-payment-followups', [FinancialController::class, 'parentPaymentFollowups']);
Route::get('parent-payment-followups/csv', [FinancialController::class, 'parentPaymentFollowupsCsv']);
Route::post('parent-payment-followups/{parent}/note', [FinancialController::class, 'storeParentFollowUpNote'])->whereNumber('parent');
Route::post('parent-payment-followups/{parent}/mark-contacted', [FinancialController::class, 'markParentContacted'])->whereNumber('parent');
Route::post('parent-payment-followups/{parent}/promise-to-pay', [FinancialController::class, 'storePromiseToPay'])->whereNumber('parent');
Route::post('parent-payment-followups/{parent}/resolve', [FinancialController::class, 'resolveParentFollowUp'])->whereNumber('parent');
Route::middleware('admin.access')->group(function () {
Route::post('parent-payment-followups/{parent}/note', [FinancialController::class, 'storeParentFollowUpNote'])->whereNumber('parent');
Route::post('parent-payment-followups/{parent}/mark-contacted', [FinancialController::class, 'markParentContacted'])->whereNumber('parent');
Route::post('parent-payment-followups/{parent}/promise-to-pay', [FinancialController::class, 'storePromiseToPay'])->whereNumber('parent');
Route::post('parent-payment-followups/{parent}/resolve', [FinancialController::class, 'resolveParentFollowUp'])->whereNumber('parent');
});
Route::get('carryforwards/preview', [BalanceCarryforwardController::class, 'preview']);
Route::post('carryforwards/draft', [BalanceCarryforwardController::class, 'storeDraft']);
@@ -1003,7 +1013,8 @@ Route::prefix('v1')->group(function () {
Route::get('carryforwards/report', [BalanceCarryforwardController::class, 'report']);
Route::get('carryforwards/report/csv', [BalanceCarryforwardController::class, 'reportCsv']);
Route::apiResource('event-charges', EventChargeController::class);
Route::apiResource('event-charges', EventChargeController::class)
->names('finance.event-charges');
Route::post('event-charges/{eventCharge}/approve', [EventChargeController::class, 'approve'])->whereNumber('eventCharge');
Route::post('event-charges/{eventCharge}/void', [EventChargeController::class, 'void'])->whereNumber('eventCharge');
Route::post('event-charges/{eventCharge}/attach-to-invoice', [EventChargeController::class, 'attachToInvoice'])->whereNumber('eventCharge');
@@ -1017,12 +1028,14 @@ Route::prefix('v1')->group(function () {
Route::get('installments/due', [InstallmentPlanController::class, 'due']);
Route::get('installments/overdue', [InstallmentPlanController::class, 'overdue']);
Route::post('payments/{payment}/send-receipt', [FinanceNotificationController::class, 'sendPaymentReceipt'])->whereNumber('payment');
Route::post('refunds/{refund}/send-receipt', [FinanceNotificationController::class, 'sendRefundReceipt'])->whereNumber('refund');
Route::post('invoices/{invoice}/send-statement', [FinanceNotificationController::class, 'sendInvoiceStatement'])->whereNumber('invoice');
Route::post('parents/{parent}/send-overdue-reminder', [FinanceNotificationController::class, 'sendOverdueReminder'])->whereNumber('parent');
Route::post('installments/{installment}/send-reminder', [FinanceNotificationController::class, 'sendInstallmentReminder'])->whereNumber('installment');
Route::get('notification-logs', [FinanceNotificationController::class, 'logs']);
Route::middleware('admin.access')->group(function () {
Route::post('payments/{payment}/send-receipt', [FinanceNotificationController::class, 'sendPaymentReceipt'])->whereNumber('payment');
Route::post('refunds/{refund}/send-receipt', [FinanceNotificationController::class, 'sendRefundReceipt'])->whereNumber('refund');
Route::post('invoices/{invoice}/send-statement', [FinanceNotificationController::class, 'sendInvoiceStatement'])->whereNumber('invoice');
Route::post('parents/{parent}/send-overdue-reminder', [FinanceNotificationController::class, 'sendOverdueReminder'])->whereNumber('parent');
Route::post('installments/{installment}/send-reminder', [FinanceNotificationController::class, 'sendInstallmentReminder'])->whereNumber('installment');
});
Route::get('notification-logs', [FinanceNotificationController::class, 'logs'])->middleware('admin.access');
Route::get('financial-report/csv', [FinancialController::class, 'downloadCsv']);
Route::get('financial-report/pdf', [FinancialController::class, 'downloadPdf']);
Route::get('unpaid-parents', [FinancialController::class, 'unpaidParents']);
@@ -1042,16 +1055,18 @@ Route::prefix('v1')->group(function () {
});
Route::prefix('refunds')->group(function () {
Route::post('recalculate-overpayments', [RefundController::class, 'recalculateOverpayments']);
Route::get('parent-balances/{parentId}', [RefundController::class, 'parentBalances']);
Route::get('/', [RefundController::class, 'index']);
Route::post('/', [RefundController::class, 'store']);
Route::get('{refundId}', [RefundController::class, 'show']);
Route::patch('{refundId}', [RefundController::class, 'update']);
Route::delete('{refundId}', [RefundController::class, 'destroy']);
Route::post('{refundId}/approve', [RefundController::class, 'approve']);
Route::post('{refundId}/reject', [RefundController::class, 'reject']);
Route::post('{refundId}/payments', [RefundController::class, 'recordPayment']);
Route::middleware('admin.access')->group(function () {
Route::post('recalculate-overpayments', [RefundController::class, 'recalculateOverpayments']);
Route::post('/', [RefundController::class, 'store']);
Route::patch('{refundId}', [RefundController::class, 'update']);
Route::delete('{refundId}', [RefundController::class, 'destroy']);
Route::post('{refundId}/approve', [RefundController::class, 'approve']);
Route::post('{refundId}/reject', [RefundController::class, 'reject']);
Route::post('{refundId}/payments', [RefundController::class, 'recordPayment']);
});
});
Route::prefix('reimbursements')->group(function () {
@@ -1087,7 +1102,7 @@ Route::prefix('v1')->group(function () {
Route::post('generate', [InvoiceController::class, 'generate']);
Route::get('parent/{parentId}', [InvoiceController::class, 'byParent']);
Route::get('parent-payment', [InvoiceController::class, 'parentPayment']);
Route::post('{invoiceId}/status', [InvoiceController::class, 'updateStatus']);
Route::post('{invoiceId}/status', [InvoiceController::class, 'updateStatus'])->middleware('admin.access');
Route::get('unpaid', [InvoiceController::class, 'unpaid']);
Route::get('{invoiceId}/preview', [InvoiceController::class, 'preview']);
Route::get('{invoiceId}/pdf', [InvoiceController::class, 'pdf']);
@@ -1113,15 +1128,17 @@ Route::prefix('v1')->group(function () {
Route::get('parents/{parentId}/students', [PaymentEventChargesController::class, 'enrolledStudents']);
});
Route::prefix('payment-notifications')->group(function () {
Route::middleware('admin.access')->prefix('payment-notifications')->group(function () {
Route::get('/', [PaymentNotificationController::class, 'index']);
Route::post('send', [PaymentNotificationController::class, 'send']);
});
Route::prefix('payment-transactions')->group(function () {
Route::post('/', [PaymentTransactionController::class, 'store']);
Route::get('payment/{paymentId}', [PaymentTransactionController::class, 'byPayment']);
Route::post('{transactionId}/status', [PaymentTransactionController::class, 'updateStatus']);
Route::middleware('admin.access')->group(function () {
Route::post('/', [PaymentTransactionController::class, 'store']);
Route::post('{transactionId}/status', [PaymentTransactionController::class, 'updateStatus']);
});
});
Route::prefix('paypal-transactions')->group(function () {
@@ -1151,7 +1168,7 @@ Route::prefix('v1')->group(function () {
Route::prefix('reports/slips')->group(function () {
Route::post('print', [SlipPrinterController::class, 'print']);
Route::post('preview', [SlipPrinterController::class, 'preview']);
Route::get('logs', [SlipPrinterController::class, 'logs']);
Route::get('logs', [SlipPrinterController::class, 'logs'])->middleware('admin.access');
Route::post('reprint', [SlipPrinterController::class, 'reprint']);
});
Route::prefix('reports/stickers')->group(function () {
@@ -1185,7 +1202,7 @@ Route::prefix('v1')->group(function () {
Route::post('admin/review', [ExamDraftApiController::class, 'adminReview']);
});
Route::prefix('configuration')->group(function () {
Route::middleware('admin.access')->prefix('configuration')->group(function () {
Route::get('/', [ConfigurationAdminController::class, 'index']);
Route::get('key/{config_key}', [ConfigurationAdminController::class, 'getByKey']);
Route::post('/', [ConfigurationAdminController::class, 'store']);
@@ -1205,7 +1222,7 @@ Route::prefix('v1')->group(function () {
Route::prefix('incidents')->group(function () {
Route::get('current', [IncidentApiController::class, 'current']);
Route::get('history', [IncidentApiController::class, 'history']);
Route::get('history', [IncidentApiController::class, 'history'])->middleware('admin.access');
Route::get('processed', [IncidentApiController::class, 'processed']);
Route::get('analysis', [IncidentApiController::class, 'analysis']);
Route::get('grades/{gradeId}/students', [IncidentApiController::class, 'studentsByGrade']);