From 8d4d610b82508c1f87646955005201ebbe40ca1c Mon Sep 17 00:00:00 2001 From: root Date: Mon, 8 Jun 2026 23:45:55 -0400 Subject: [PATCH] add test batches --- .../Commands/AttendanceAutoPublishCommand.php | 1 - .../Commands/CheckMissedPaymentsCommand.php | 2 - .../CleanupExpiredNotificationsCommand.php | 2 - .../Commands/CleanupPasswordResetsCommand.php | 1 - app/Console/Commands/ConfigUpdateCommand.php | 11 +- .../Commands/DeleteInactiveUsersCommand.php | 1 - .../Commands/DeleteUnverifiedUsersCommand.php | 1 - .../Commands/RecalculateAttendanceCommand.php | 3 +- .../Commands/SendAbsenteesSummaryCommand.php | 1 - .../Commands/SendLatesSummaryCommand.php | 1 - ...SendMonthlyPaymentNotificationsCommand.php | 10 +- .../SendTestPaymentNotificationCommand.php | 11 +- .../Commands/SyncPaypalPaymentsCommand.php | 3 +- app/Events/DeleteUnverifiedUser.php | 4 +- app/Events/WhatsappInvitesSend.php | 4 +- app/Helpers/app_helpers.php | 18 +- .../AdministratorAbsenceController.php | 5 +- .../AdministratorDashboardController.php | 5 +- .../AdministratorEnrollmentController.php | 7 +- .../AdministratorNotificationController.php | 7 +- .../AdministratorPromotionController.php | 30 +- ...ministratorTeacherSubmissionController.php | 3 +- .../EmergencyContactController.php | 7 +- .../TeacherClassAssignmentController.php | 3 + .../AdminAttendanceApiController.php | 6 +- .../AttendanceCommentTemplateController.php | 5 +- .../Attendance/EarlyDismissalsController.php | 83 +- .../Api/Attendance/LateSlipLogsController.php | 9 +- .../StaffAttendanceApiController.php | 18 +- .../TeacherAttendanceApiController.php | 15 +- .../AttendanceManagementController.php | 24 +- .../AttendanceTrackingController.php | 3 +- .../Controllers/Api/Auth/AuthController.php | 6 +- .../Controllers/Api/Auth/IpBanController.php | 15 +- .../Api/Auth/RegisterController.php | 3 +- .../Api/Auth/RolePermissionController.php | 41 +- .../Api/Auth/RoleSwitcherController.php | 5 +- .../Api/Auth/SessionTimeoutController.php | 3 +- .../Api/Badges/BadgeController.php | 10 +- .../Controllers/Api/BaseApiController.php | 4 +- .../ClassPreparationController.php | 8 +- .../ClassProgress/ClassProgressController.php | 22 +- .../Api/Classes/ClassController.php | 7 +- .../Communication/CommunicationController.php | 19 +- .../CompetitionScoresController.php | 16 +- .../Api/Core/BaseApiController.php | 82 +- .../Api/Discounts/DiscountController.php | 11 +- .../Api/Email/BroadcastEmailController.php | 9 +- .../Controllers/Api/Email/EmailController.php | 2 +- .../Api/Email/EmailExtractorController.php | 1 + .../Api/Exams/ExamDraftController.php | 9 +- .../Api/Expenses/ExpenseController.php | 12 +- .../ExtraCharges/ExtraChargesController.php | 9 +- .../Api/Family/FamilyAdminController.php | 5 +- .../Api/Family/FamilyController.php | 15 +- .../Finance/BalanceCarryforwardController.php | 9 +- .../Api/Finance/ChargeController.php | 3 + .../Api/Finance/EventChargeController.php | 69 +- .../Finance/FinanceNotificationController.php | 11 +- .../Api/Finance/FinancialController.php | 22 +- .../Api/Finance/InstallmentPlanController.php | 51 +- .../Api/Finance/InvoiceController.php | 7 +- .../Api/Finance/PaymentController.php | 2 +- .../Finance/PaymentEventChargesController.php | 3 + .../Api/Finance/PaymentManualController.php | 4 +- .../Finance/PaymentTransactionController.php | 2 +- .../Finance/PaypalTransactionsController.php | 2 +- .../Api/Finance/PurchaseOrderController.php | 10 +- .../Api/Finance/RefundController.php | 6 +- .../Api/Finance/ReimbursementController.php | 21 +- .../Api/Frontend/FrontendController.php | 2 +- .../Api/Frontend/InfoIconController.php | 3 + .../Api/Frontend/LandingPageController.php | 6 +- .../Api/Frontend/PageController.php | 2 +- .../Api/Grading/GradingController.php | 9 +- .../Api/Incidents/IncidentController.php | 4 + .../Inventory/InventoryCategoryController.php | 6 +- .../Api/Inventory/InventoryController.php | 24 +- .../Inventory/InventoryMovementController.php | 11 +- .../Api/Inventory/SupplierController.php | 6 +- .../Inventory/SupplyCategoryController.php | 6 +- .../Api/Messaging/MessagesController.php | 20 +- .../Api/Messaging/WhatsappController.php | 13 +- .../Notifications/NotificationController.php | 12 +- .../ParentAttendanceReportController.php | 7 +- .../Api/Parents/ParentController.php | 4 +- .../Api/Parents/ParentPromotionController.php | 8 +- .../PrintRequests/PrintRequestsController.php | 3 +- .../Api/Reports/ReportCardsController.php | 18 +- .../Api/Reports/SlipPrinterController.php | 11 +- .../Api/Reports/StickersController.php | 7 +- .../Api/RolePermissionController.php | 38 +- .../Api/RoleSwitcherController.php | 2 +- .../Controllers/Api/ScannerController.php | 82 +- .../Api/Scores/FinalController.php | 3 + .../Api/Scores/HomeworkController.php | 3 + .../Api/Scores/MidtermController.php | 3 + .../Api/Scores/ParticipationController.php | 3 + .../Api/Scores/ProjectController.php | 3 + .../Controllers/Api/Scores/QuizController.php | 3 + .../Api/Scores/ScoreCommentController.php | 7 +- .../Api/Scores/ScoreController.php | 3 + .../Settings/ConfigurationAdminController.php | 6 +- .../Api/Settings/EventController.php | 6 +- .../Api/Settings/PreferencesController.php | 18 +- .../Api/Settings/SchoolCalendarController.php | 24 +- .../Api/Settings/SettingsController.php | 6 +- .../Controllers/Api/Staff/StaffController.php | 20 +- .../Api/Staff/TeacherController.php | 7 +- app/Http/Controllers/Api/StatsController.php | 3 +- .../Api/Students/StudentController.php | 52 +- .../Subjects/SubjectCurriculumController.php | 6 +- .../Api/Support/SupportController.php | 8 +- .../Api/System/DashboardController.php | 2 +- .../System/DashboardRedirectController.php | 5 +- .../Api/System/DatabaseHealthController.php | 2 +- .../Api/System/NavBuilderController.php | 7 +- .../Api/System/StatsController.php | 2 - app/Http/Controllers/Api/Ui/UiController.php | 3 + .../Controllers/Api/Users/UserController.php | 3 + .../Web/SchoolYearAdminPageController.php | 3 +- .../Web/TeacherCalendarPageController.php | 3 +- app/Http/Middleware/ApiDocsAuth.php | 6 +- app/Http/Middleware/ApiJwtAuth.php | 2 +- app/Http/Middleware/CleanupScheduler.php | 4 +- .../EnsurePrintRequestsAdminAccess.php | 3 +- .../Middleware/EnsureSchoolYearEditable.php | 8 +- app/Http/Middleware/MultiAuth.php | 2 - app/Http/Middleware/PrimeTimezone.php | 4 +- app/Http/Middleware/RequirePermission.php | 7 +- .../Admin/SaveCompetitionScoresRequest.php | 3 +- .../Admin/UpdateCompetitionWinnerRequest.php | 4 +- .../SubmitAdministratorAbsenceRequest.php | 2 +- .../Api/Admin/IndexAdminProgressRequest.php | 3 +- .../SaveAdminNotificationSubjectsRequest.php | 4 +- ...SavePrintNotificationRecipientsRequest.php | 4 +- ...dTeacherSubmissionNotificationsRequest.php | 4 +- .../StoreStudentAssignmentRequest.php | 12 +- .../AdminAddAttendanceEntryRequest.php | 2 +- .../Attendance/SaveAdminAttendanceRequest.php | 2 +- .../SaveStaffAttendanceCellRequest.php | 2 +- .../TeacherSubmitAttendanceRequest.php | 2 +- .../UpdateAttendanceManagementRequest.php | 2 +- .../ComposeAttendanceEmailRequest.php | 6 +- .../RecordAttendanceTrackingRequest.php | 10 +- .../SaveAttendanceNotificationNoteRequest.php | 12 +- .../SendAttendanceManualEmailRequest.php | 12 +- app/Http/Requests/Auth/RegisterRequest.php | 2 +- .../Badges/BadgePrintStatusRequest.php | 4 +- .../Badges/GenerateBadgePdfRequest.php | 4 +- .../Requests/Badges/LogBadgePrintRequest.php | 4 +- .../ClassProgressIndexRequest.php | 4 +- .../ClassProgressMetaRequest.php | 2 + .../ClassProgressStoreRequest.php | 6 +- .../ClassProgressUpdateRequest.php | 8 +- .../Families/FamilyComposeEmailRequest.php | 2 +- .../Finance/CarryforwardAdjustmentRequest.php | 10 +- .../Finance/CarryforwardFinalizeRequest.php | 14 +- .../Finance/CarryforwardPreviewRequest.php | 14 +- .../Finance/EventChargeLifecycleRequest.php | 26 +- .../Finance/FinanceNotificationLogRequest.php | 16 +- .../Finance/FinancialSummaryRequest.php | 4 +- .../Requests/Finance/FollowUpNoteRequest.php | 24 +- .../Finance/InstallmentPlanRequest.php | 16 +- .../Finance/ParentPaymentFollowUpRequest.php | 32 +- .../Finance/PaymentAllocationRequest.php | 12 +- .../Incidents/IncidentStoreRequest.php | 2 +- .../Promotions/AdminListPromotionsRequest.php | 2 +- .../ParentEnrollmentStepRequest.php | 1 - .../PurchaseOrderStoreRequest.php | 2 +- .../Refunds/RefundDecisionRequest.php | 2 +- .../Requests/Refunds/RefundIndexRequest.php | 2 +- .../Stickers/StickerFormDataRequest.php | 2 +- .../Reports/Stickers/StickerPrintRequest.php | 4 +- .../StickerStudentsByClassRequest.php | 2 +- .../Requests/Roles/PermissionStoreRequest.php | 1 + app/Http/Requests/Security/IpBanRequest.php | 2 +- app/Http/Requests/Security/IpUnbanRequest.php | 2 +- .../Admin/AdminNotificationAlertsResource.php | 2 +- .../Admin/PrintRecipientsResource.php | 2 +- .../Admin/Progress/ProgressGroupResource.php | 2 +- .../Progress/ProgressReportDetailResource.php | 2 +- .../Admin/TeacherSubmissionReportResource.php | 2 +- .../Attendance/AdminAttendanceResource.php | 2 +- .../Attendance/DailyAttendanceResource.php | 2 +- .../TeacherAttendanceFormResource.php | 4 +- .../Attendance/TeacherGridResource.php | 4 +- .../ClassProgressGroupCollection.php | 4 +- .../Resources/Events/EventChargeResource.php | 4 +- .../Resources/Messaging/MessageCollection.php | 2 +- .../Resources/Messaging/MessageResource.php | 4 +- .../PaymentNotificationLogResource.php | 2 +- .../Promotions/StudentPromotionResource.php | 1 - .../ReimbursementRecipientResource.php | 2 +- app/Http/Resources/Staff/StaffCollection.php | 2 +- .../Support/SupportRequestCollection.php | 2 +- app/Http/Resources/System/NavItemResource.php | 2 +- .../TeacherSubmissionRowResource.php | 2 +- app/Libraries/StaffTimeOffLinkService.php | 4 +- .../AttendanceConsequenceListener.php | 4 +- app/Listeners/BelowSixtyEmailListener.php | 4 +- .../DeleteUnverifiedUserListener.php | 4 +- app/Listeners/SchoolEventListener.php | 3 +- app/Listeners/WhatsappInviteListener.php | 4 +- app/Mail/TeacherSubmissionReminderMail.php | 3 +- app/Models/AdditionalCharge.php | 21 +- app/Models/AdminNotificationSubject.php | 7 +- app/Models/AttendanceCommentTemplate.php | 4 +- app/Models/AttendanceData.php | 190 +- app/Models/AttendanceDay.php | 87 +- app/Models/AttendanceEmailTemplate.php | 4 +- app/Models/AttendanceRecord.php | 34 +- app/Models/AttendanceTracking.php | 158 +- app/Models/AuthorizedUser.php | 6 +- app/Models/BadgePrintLog.php | 70 +- app/Models/BaseModel.php | 20 +- app/Models/Calendar.php | 4 +- app/Models/CalendarEvent.php | 21 +- app/Models/ClassModel.php | 4 +- app/Models/ClassPrepAdjustment.php | 6 +- app/Models/ClassPreparationLog.php | 6 +- app/Models/ClassProgressAttachment.php | 6 +- app/Models/ClassProgressReport.php | 10 +- app/Models/ClassSection.php | 4 +- app/Models/Communication.php | 4 +- app/Models/CommunicationLog.php | 18 +- app/Models/Competition.php | 23 +- app/Models/CompetitionClassWinner.php | 12 +- app/Models/CompetitionScore.php | 10 +- app/Models/CompetitionWinner.php | 16 +- app/Models/Configuration.php | 10 +- app/Models/ContactUs.php | 6 +- app/Models/CurrentFlag.php | 14 +- app/Models/CurrentIncident.php | 3 +- app/Models/DiscountUsage.php | 15 +- app/Models/DiscountVoucher.php | 26 +- app/Models/EarlyDismissalSignature.php | 6 +- app/Models/EmailTemplate.php | 6 +- app/Models/EmergencyContact.php | 8 +- app/Models/Enrollment.php | 21 +- app/Models/Event.php | 29 +- app/Models/EventCharge.php | 4 +- app/Models/EventCharges.php | 19 +- app/Models/Exam.php | 9 +- app/Models/ExamDraft.php | 16 +- app/Models/Expense.php | 29 +- app/Models/Family.php | 4 +- app/Models/FamilyCommPref.php | 12 +- app/Models/FamilyGuardian.php | 14 +- app/Models/FamilyStudent.php | 9 +- app/Models/FinalExam.php | 22 +- app/Models/FinalScore.php | 12 +- app/Models/FinanceBalanceCarryforward.php | 2 - app/Models/FinanceFollowUpNote.php | 1 - app/Models/FinanceNotificationLog.php | 1 - app/Models/FinanceNotificationPreference.php | 1 - app/Models/FinanceReceiptSequence.php | 1 - app/Models/GradingLock.php | 11 +- app/Models/Homework.php | 39 +- app/Models/Incident.php | 10 +- app/Models/InventoryCategory.php | 4 +- app/Models/InventoryItem.php | 18 +- app/Models/InventoryMovement.php | 16 +- app/Models/Invoice.php | 52 +- app/Models/InvoiceEvent.php | 10 +- app/Models/InvoiceInstallment.php | 1 - app/Models/InvoiceInstallmentPlan.php | 1 - app/Models/InvoiceStudentList.php | 8 +- app/Models/IpAttempt.php | 9 +- app/Models/LateSlipLog.php | 36 +- app/Models/LoginActivity.php | 8 +- app/Models/ManualPayment.php | 6 +- app/Models/Message.php | 20 +- app/Models/MidtermExam.php | 22 +- app/Models/MissingScoreOverride.php | 39 +- app/Models/NavItem.php | 8 +- app/Models/Notification.php | 13 +- app/Models/ParentAttendanceReport.php | 25 +- app/Models/ParentMeetingSchedule.php | 12 +- app/Models/ParentModel.php | 4 +- app/Models/ParentNotification.php | 8 +- app/Models/Participation.php | 42 +- app/Models/PasswordReset.php | 5 +- app/Models/PasswordResetRequest.php | 4 +- app/Models/PayPalPayment.php | 15 +- app/Models/Payment.php | 47 +- app/Models/PaymentCarryforwardAllocation.php | 1 - app/Models/PaymentError.php | 16 +- app/Models/PaymentInstallmentAllocation.php | 1 - app/Models/PaymentNotificationLog.php | 26 +- app/Models/PaymentTransaction.php | 13 +- app/Models/PaypalTransaction.php | 12 +- app/Models/Permission.php | 9 +- app/Models/PlacementBatch.php | 4 +- app/Models/PlacementLevel.php | 4 +- app/Models/PlacementScore.php | 8 +- app/Models/Preferences.php | 16 +- app/Models/PrintRequest.php | 14 +- app/Models/Project.php | 27 +- app/Models/PromotionAuditLog.php | 11 - app/Models/PromotionQueue.php | 22 +- app/Models/PromotionReminderLog.php | 4 - app/Models/PurchaseOrder.php | 52 +- app/Models/PurchaseOrderItem.php | 30 +- app/Models/Quiz.php | 22 +- app/Models/Refund.php | 61 +- app/Models/Reimbursement.php | 53 +- app/Models/ReimbursementBatch.php | 35 +- app/Models/ReimbursementBatchAdminFile.php | 23 +- app/Models/ReimbursementBatchItem.php | 35 +- app/Models/ReportCardAcknowledgement.php | 34 +- app/Models/Role.php | 88 +- app/Models/RoleNavItem.php | 21 +- app/Models/RolePermission.php | 39 +- app/Models/SchoolClass.php | 10 +- app/Models/SchoolYear.php | 3 - app/Models/ScoreComment.php | 35 +- app/Models/Section.php | 24 +- app/Models/SectionPlacementBatch.php | 3 - app/Models/SemesterScore.php | 73 +- app/Models/SemesterScoreSnapshot.php | 1 + app/Models/Setting.php | 13 +- app/Models/Staff.php | 39 +- app/Models/StaffAttendance.php | 147 +- app/Models/Stats.php | 11 +- app/Models/Student.php | 74 +- app/Models/StudentAllergy.php | 12 +- app/Models/StudentClass.php | 57 +- app/Models/StudentMedicalCondition.php | 12 +- app/Models/StudentPromotionRecord.php | 14 - app/Models/SubjectCurriculum.php | 28 +- app/Models/Supplier.php | 3 +- app/Models/Supply.php | 2 + app/Models/SupplyCategory.php | 3 +- app/Models/SupplyTransaction.php | 2 + app/Models/SupportRequest.php | 3 +- app/Models/Teacher.php | 4 +- app/Models/TeacherClass.php | 74 +- .../TeacherSubmissionNotificationHistory.php | 29 +- app/Models/User.php | 114 +- app/Models/UserNotification.php | 19 +- app/Models/UserRole.php | 35 +- app/Models/WhatsappGroupLink.php | 49 +- app/Models/WhatsappGroupMembership.php | 71 +- app/Models/WhatsappInviteLog.php | 48 +- app/Policies/NavItemPolicy.php | 2 +- app/Providers/AppServiceProvider.php | 48 +- app/Providers/EventServiceProvider.php | 60 +- .../CompetitionWinnersAdminService.php | 7 +- .../CompetitionWinnersDomain.php | 2 - .../AdminNotificationSubjectService.php | 15 +- .../AdminNotificationUserService.php | 4 +- .../AdminPrintRecipientService.php | 15 +- .../AdministratorAbsenceService.php | 62 +- .../AdministratorDashboardService.php | 5 +- .../AdministratorEnrollmentEventService.php | 7 +- .../AdministratorEnrollmentQueryService.php | 21 +- .../AdministratorEnrollmentRefundService.php | 6 +- .../AdministratorEnrollmentService.php | 3 +- .../AdministratorEnrollmentStatusService.php | 22 +- .../AdministratorMetricsService.php | 5 +- .../AdministratorNotificationService.php | 5 +- .../AdministratorSharedService.php | 8 +- .../AdministratorTeacherSubmissionService.php | 3 +- .../AdministratorUserSearchService.php | 34 +- .../TeacherSubmissionNotificationService.php | 37 +- .../TeacherSubmissionReportService.php | 15 +- .../TeacherSubmissionSupportService.php | 11 +- .../Trophy/TrophyReportService.php | 4 +- app/Services/ApplicationUrlService.php | 9 +- .../AssignmentDataLoaderService.php | 2 +- .../Assignment/AssignmentSectionService.php | 1 - app/Services/Assignment/AssignmentService.php | 20 +- app/Services/AssignmentService.php | 4 +- .../AttendanceAutoPublishJobService.php | 5 +- .../AttendanceAutoPublishService.php | 5 +- .../Attendance/AttendanceCommentService.php | 4 +- .../AttendanceConsequenceService.php | 33 +- .../AttendanceDailySummaryService.php | 13 +- .../Attendance/AttendancePolicyService.php | 13 +- .../Attendance/AttendanceQueryService.php | 162 +- .../AttendanceRecordSyncService.php | 25 +- app/Services/Attendance/AttendanceService.php | 57 +- .../AttendanceSummaryRebuildService.php | 4 +- .../Attendance/LateSlipLogQueryService.php | 9 +- .../Attendance/SemesterRangeService.php | 6 +- .../Attendance/StaffAttendanceService.php | 134 +- .../StudentAttendanceWriterService.php | 28 +- .../TeacherAttendanceSubmissionService.php | 44 +- .../AttendanceManagementService.php | 122 +- .../AttendanceCaseQueryService.php | 79 +- .../AttendanceCommunicationSupportService.php | 13 +- .../AttendanceEmailComposerService.php | 98 +- .../AttendanceMailerService.php | 2 +- .../AttendanceNotificationLogService.php | 44 +- .../AttendanceNotificationWorkflowService.php | 162 +- .../AttendanceParentLookupService.php | 68 +- .../AttendancePendingViolationService.php | 115 +- .../AttendanceTrackingService.php | 9 +- ...endanceViolationStudentResolverService.php | 103 +- .../DefaultAttendanceMailerService.php | 25 +- .../ViolationRuleEngineService.php | 259 +- app/Services/Auth/ApiLoginSecurityService.php | 4 +- app/Services/Auth/AuthSessionService.php | 6 +- app/Services/Auth/PermissionCheckService.php | 2 +- .../Auth/RegistrationCaptchaService.php | 3 +- .../Auth/RegistrationFormatterService.php | 10 +- app/Services/Auth/RegistrationService.php | 25 +- app/Services/Auth/UserRoleService.php | 8 +- app/Services/BadgeScan/BadgeScanService.php | 3 +- app/Services/Badges/BadgeFormDataService.php | 35 +- app/Services/Badges/BadgePdfService.php | 123 +- app/Services/Badges/BadgePrintLogService.php | 7 +- app/Services/Badges/BadgeService.php | 62 +- .../Badges/BadgeStudentLookupService.php | 13 +- app/Services/Badges/BadgeTextFormatter.php | 13 +- .../Badges/BadgeUserLookupService.php | 51 +- .../Billing/BalanceCalculationService.php | 11 +- app/Services/Billing/BillingTotalsService.php | 2 +- app/Services/Billing/ChargeService.php | 25 +- .../BroadcastEmailComposerService.php | 3 +- .../BroadcastEmailDispatchService.php | 3 +- .../BroadcastEmailImageService.php | 8 +- .../BroadcastEmailRecipientService.php | 1 - .../BroadcastEmailSenderOptionsService.php | 7 +- .../Certificates/CertificateAdminService.php | 2 +- .../Certificates/CertificatePdfService.php | 27 +- ...lassPreparationAdjustmentWriterService.php | 3 +- .../ClassPreparationCalculatorService.php | 3 - .../ClassPreparationContextService.php | 1 - .../ClassPreparationInventoryService.php | 3 +- .../ClassPreparationLogService.php | 1 - .../ClassPreparationPrintService.php | 6 +- .../ClassPreparationRosterService.php | 1 - .../ClassPreparationService.php | 22 +- .../ClassProgressAttachmentService.php | 15 +- .../ClassProgressMetaService.php | 2 +- .../ClassProgressMutationService.php | 15 +- .../ClassProgressQueryService.php | 20 +- .../ClassProgressRuleService.php | 2 - .../ClassSections/ClassAttendanceService.php | 2 +- .../ClassSectionQueryService.php | 14 +- .../CommunicationFamilyService.php | 2 +- .../Communication/CommunicationLogService.php | 2 +- .../CommunicationPreviewService.php | 10 +- .../CommunicationSendService.php | 4 +- .../CommunicationStudentService.php | 4 +- .../CommunicationTemplateService.php | 4 +- .../CompetitionScoresQueryService.php | 4 +- .../CompetitionScoresRosterService.php | 3 +- .../CompetitionScoresSaveService.php | 3 +- .../Dashboard/DashboardRouteService.php | 5 +- .../Discounts/DiscountApplyService.php | 13 +- .../Discounts/DiscountInvoiceService.php | 22 +- .../Discounts/DiscountVoucherService.php | 1 - app/Services/Docs/ApiDocsService.php | 3 +- app/Services/Docs/DocsCatalogService.php | 6 +- app/Services/Docs/OpenApiRouteExporter.php | 18 +- app/Services/Email/EmailAttachmentService.php | 6 +- app/Services/Email/EmailDispatchService.php | 20 +- app/Services/Email/EmailExtractorService.php | 4 +- app/Services/Email/EmailProfileService.php | 2 - .../Email/EmailSenderOptionsService.php | 6 +- app/Services/EmailService.php | 12 +- .../EmergencyContactCrudService.php | 3 +- .../EmergencyContactDirectoryService.php | 11 +- .../Events/EventChargeQueryService.php | 9 +- app/Services/Events/EventChargeService.php | 7 +- app/Services/Events/EventListService.php | 12 +- .../Events/EventManagementService.php | 15 +- .../Events/EventStudentChargeService.php | 2 +- app/Services/Exams/ExamDraftAdminService.php | 27 +- app/Services/Exams/ExamDraftConfigService.php | 4 - app/Services/Exams/ExamDraftFileService.php | 39 +- .../Exams/ExamDraftTeacherService.php | 10 +- .../Expenses/ExpenseReceiptService.php | 5 +- app/Services/Expenses/ExpenseStaffService.php | 7 +- app/Services/Expenses/ExpenseWriteService.php | 1 - .../ExtraChargesChargeService.php | 29 +- .../ExtraChargesInvoiceService.php | 6 +- .../ExtraCharges/ExtraChargesMetaService.php | 6 +- .../ExtraChargesParentService.php | 9 +- .../Families/FamilyMutationService.php | 21 +- .../Families/FamilyNotificationService.php | 4 +- app/Services/Families/FamilyQueryService.php | 23 +- app/Services/FeeCalculationService.php | 4 +- app/Services/Fees/FeeGradeService.php | 1 - .../Fees/FeeRefundCalculatorService.php | 21 +- app/Services/Fees/FeeRefundDetailService.php | 23 +- app/Services/Fees/FeeStudentFeeService.php | 12 +- .../Fees/TuitionCalculationService.php | 10 +- .../Files/ExamDraftDownloadNameService.php | 7 +- app/Services/Files/FileServeService.php | 14 +- .../Finance/FinanceNotificationLogService.php | 21 +- .../Finance/FinancialChartService.php | 7 +- .../Finance/FinancialPaymentService.php | 8 +- .../Finance/FinancialPdfReportService.php | 14 +- .../Finance/FinancialReportService.php | 9 +- .../Finance/FinancialSchoolYearService.php | 2 +- .../Finance/FinancialSummaryService.php | 88 +- .../Finance/FinancialUnpaidParentsService.php | 20 +- .../Finance/InstallmentPlanService.php | 33 +- .../Finance/ParentPaymentFollowUpService.php | 140 +- .../PriorYearBalanceCarryforwardService.php | 76 +- .../StakeholderFinancialAnalysisService.php | 83 +- .../Frontend/ContactSubmissionService.php | 15 +- .../LandingPageParentDashboardService.php | 3 +- .../Frontend/LandingPageRoleService.php | 4 +- app/Services/Frontend/LandingPageService.php | 7 +- .../LandingPageTeacherSummaryService.php | 60 +- app/Services/Frontend/ProfileIconService.php | 4 +- app/Services/Frontend/StaticPageService.php | 4 +- .../Grading/BelowSixtyEmailService.php | 14 +- .../Grading/GradingBelowSixtyService.php | 118 +- app/Services/Grading/GradingLockService.php | 16 +- .../Grading/GradingOverviewService.php | 19 +- .../Grading/GradingPlacementService.php | 23 +- .../Grading/GradingRefreshService.php | 6 +- app/Services/Grading/GradingScoreService.php | 32 +- .../HomeworkTrackingCalendarService.php | 4 +- .../Grading/HomeworkTrackingService.php | 26 +- .../Grading/Policy/GradingPolicyResolver.php | 6 +- .../StrongCategoryAverageCalculator.php | 3 - .../GradebookFinalizationValidator.php | 2 +- .../Validation/ScoreValueValidator.php | 3 +- .../Incidents/CurrentIncidentService.php | 20 +- .../Incidents/IncidentAnalysisService.php | 9 +- .../Incidents/IncidentHistoryService.php | 8 +- .../Incidents/IncidentLookupService.php | 7 +- .../Inventory/InventoryCategoryService.php | 16 +- .../Inventory/InventoryItemService.php | 39 +- .../Inventory/InventoryMovementService.php | 37 +- .../Inventory/InventorySummaryService.php | 14 +- .../InventoryTeacherDistributionService.php | 61 +- app/Services/Inventory/SupplierService.php | 19 +- .../Inventory/SupplyCategoryService.php | 13 +- .../Invoices/InvoiceConfigService.php | 3 +- .../Invoices/InvoiceGenerationService.php | 21 +- app/Services/Invoices/InvoiceGradeService.php | 10 +- .../Invoices/InvoiceManagementService.php | 17 +- .../Invoices/InvoicePaymentService.php | 12 +- app/Services/Invoices/InvoicePdfService.php | 55 +- app/Services/Invoices/InvoiceService.php | 3 +- .../Invoices/InvoiceTuitionService.php | 6 +- .../Messaging/MessageCommandService.php | 17 +- .../Messaging/MessageQueryService.php | 11 +- .../Messaging/MessageRecipientService.php | 4 +- app/Services/Navigation/NavBuilderService.php | 16 +- app/Services/Navigation/NavbarService.php | 4 +- .../NotificationManagementService.php | 4 +- .../Notifications/NotificationReadService.php | 2 +- .../NotificationRecipientService.php | 1 - .../Notifications/NotificationSendService.php | 7 +- .../NotificationUserListService.php | 4 +- .../AuthorizedUsersManagementService.php | 3 +- .../ParentAttendanceReportCalendarService.php | 14 +- .../Parents/ParentAttendanceReportService.php | 65 +- .../Parents/ParentAttendanceService.php | 4 +- .../Parents/ParentEmergencyContactService.php | 3 +- .../Parents/ParentEnrollmentService.php | 14 +- .../ParentEventParticipationService.php | 8 +- app/Services/Parents/ParentProfileService.php | 1 - .../Parents/ParentProgressQueryService.php | 11 +- .../Parents/ParentRegistrationService.php | 10 +- .../Parents/ParentReportCardService.php | 9 +- .../Parents/PrimaryParentUserResolver.php | 8 +- .../PaymentEnrollmentEventService.php | 9 +- .../Payments/PaymentEventChargesService.php | 8 +- .../Payments/PaymentManualService.php | 67 +- .../Payments/PaymentMissedCheckService.php | 9 +- .../PaymentNotificationDispatchService.php | 2 +- .../Payments/PaymentNotificationService.php | 34 +- .../PaymentTestNotificationService.php | 24 +- .../Payments/PaymentTransactionService.php | 2 +- .../Payments/PaypalPaymentService.php | 44 +- .../Payments/PaypalPaymentSyncService.php | 27 +- .../Payments/PaypalTransactionsService.php | 20 +- app/Services/Policy/PolicyContentService.php | 7 +- .../Preferences/PreferencesCommandService.php | 5 +- .../Preferences/PreferencesOptionsService.php | 6 +- .../Preferences/PreferencesQueryService.php | 14 +- .../PrintRequestsPortalService.php | 7 +- .../Promotions/LevelProgressionService.php | 27 +- .../BalancedSectionPlacementService.php | 3 - .../Placement/PlacementPoolBuilder.php | 18 +- .../PromotionSectionCapacityService.php | 3 +- .../SectionPlacementPreviewService.php | 17 +- .../PromotionEligibilityService.php | 27 +- .../Promotions/PromotionEnrollmentService.php | 31 +- .../Promotions/PromotionQueryService.php | 34 +- .../Promotions/PromotionReminderService.php | 18 +- .../Promotions/PromotionStatusService.php | 13 +- .../PublicWinnersPortalService.php | 6 +- .../PurchaseOrderCreateService.php | 2 +- .../PurchaseOrderQueryService.php | 2 +- .../PurchaseOrderReceiveService.php | 10 +- .../PurchaseOrderStatusService.php | 2 +- .../Refunds/RefundDecisionService.php | 6 +- .../RefundInvoiceAdjustmentService.php | 6 +- .../Refunds/RefundOverpaymentService.php | 10 +- app/Services/Refunds/RefundPayoutService.php | 5 +- app/Services/Refunds/RefundPolicyService.php | 13 +- app/Services/Refunds/RefundQueryService.php | 18 +- app/Services/Refunds/RefundRequestService.php | 12 +- .../ReimbursementBatchAssignmentService.php | 16 +- .../ReimbursementBatchService.php | 16 +- .../ReimbursementDonationService.php | 6 +- .../ReimbursementEmailService.php | 90 +- .../ReimbursementExportService.php | 43 +- .../ReimbursementFileService.php | 27 +- .../ReimbursementQueryService.php | 61 +- .../ReimbursementRecipientService.php | 14 +- .../Reports/ReportCards/ReportCardService.php | 152 +- .../Reports/SlipPrinterConfigService.php | 3 +- .../Reports/SlipPrinterFormatterService.php | 22 +- .../Reports/SlipPrinterPdfService.php | 8 +- app/Services/Reports/SlipPrinterService.php | 8 +- .../Reports/Stickers/StickerPresetService.php | 3 +- .../Reports/Stickers/StickerPrintService.php | 19 +- .../Reports/Stickers/StickerQueryService.php | 7 +- app/Services/Roles/PermissionCrudService.php | 7 +- app/Services/Roles/RoleAssignmentService.php | 9 +- app/Services/Roles/RoleCrudService.php | 13 +- app/Services/Roles/RoleDashboardService.php | 2 +- app/Services/Roles/RolePermissionService.php | 14 +- app/Services/Roles/RoleQueryService.php | 24 +- app/Services/Roles/RoleStaffService.php | 16 +- app/Services/Roles/RoleSwitchService.php | 5 +- app/Services/School/AccountEventService.php | 44 +- .../School/EnrollmentEventService.php | 42 +- app/Services/School/PaymentEventService.php | 35 +- app/Services/SchoolIdService.php | 6 +- .../SchoolIds/SchoolIdAssignmentService.php | 4 +- .../SchoolIds/SchoolIdGenerationService.php | 4 +- .../SchoolYears/SchoolYearClosureService.php | 52 +- .../SchoolYears/SchoolYearContextService.php | 12 +- .../SchoolYears/SchoolYearResolver.php | 2 +- .../SchoolYears/SchoolYearWriteGuard.php | 2 +- app/Services/Scores/AttendanceCalculator.php | 24 +- app/Services/Scores/ExamScoreService.php | 18 +- app/Services/Scores/HomeworkCalculator.php | 5 +- app/Services/Scores/HomeworkScoreService.php | 28 +- .../Scores/ParticipationScoreService.php | 18 +- app/Services/Scores/ProjectCalculator.php | 5 +- app/Services/Scores/ProjectScoreService.php | 25 +- app/Services/Scores/QuizCalculator.php | 2 +- app/Services/Scores/QuizScoreService.php | 26 +- app/Services/Scores/ScoreCommentService.php | 37 +- app/Services/Scores/ScoreDashboardService.php | 17 +- .../Scores/ScorePredictorDataService.php | 4 +- .../Scores/ScorePredictorRiskService.php | 25 +- app/Services/Scores/ScorePredictorService.php | 3 +- app/Services/Scores/ScoreTermService.php | 1 - app/Services/Scores/SemesterScoreService.php | 46 +- app/Services/Security/IpBanCommandService.php | 8 +- app/Services/Security/IpBanQueryService.php | 5 +- app/Services/SemesterRangeService.php | 15 +- .../Settings/ConfigurationService.php | 4 +- .../SchoolCalendarFormatterService.php | 20 +- .../SchoolCalendarMeetingService.php | 6 +- .../SchoolCalendarMutationService.php | 19 +- .../SchoolCalendarNotificationService.php | 26 +- .../SchoolCalendarQueryService.php | 5 +- app/Services/Settings/SettingsService.php | 2 +- app/Services/Staff/StaffCommandService.php | 14 +- app/Services/Staff/StaffQueryService.php | 13 +- .../Staff/StaffTimeOffLinkService.php | 13 +- .../Students/StudentAssignmentService.php | 21 +- .../StudentAutoDistributionService.php | 6 +- .../Students/StudentDirectoryService.php | 13 +- .../Students/StudentProfileService.php | 14 +- .../Students/StudentScoreCardService.php | 9 +- .../Students/StudentStatusService.php | 10 +- .../Subjects/SubjectCurriculumService.php | 4 +- .../Support/ContactMessageService.php | 12 +- .../Support/SupportRequestService.php | 15 +- .../System/CleanupSchedulerService.php | 2 +- app/Services/System/ConfigUpdateService.php | 24 +- app/Services/System/HealthCheckService.php | 12 +- app/Services/System/TimeService.php | 6 +- .../Teachers/TeacherAbsenceService.php | 63 +- .../Teachers/TeacherAssignmentService.php | 16 +- .../Teachers/TeacherDashboardService.php | 20 +- app/Services/Ui/UiStyleService.php | 10 +- .../Users/DeleteUnverifiedUserService.php | 8 +- .../Users/InactiveUserCleanupService.php | 1 - app/Services/Users/UserEventService.php | 11 +- app/Services/Users/UserListService.php | 8 +- app/Services/Users/UserManagementService.php | 22 +- .../Whatsapp/WhatsappContactService.php | 40 +- .../Whatsapp/WhatsappContextService.php | 4 +- .../Whatsapp/WhatsappInviteBundleService.php | 47 +- .../Whatsapp/WhatsappInviteEmailService.php | 59 +- .../WhatsappInviteNotificationService.php | 23 +- .../Whatsapp/WhatsappInviteService.php | 13 +- app/Services/Whatsapp/WhatsappLinkService.php | 23 +- .../Whatsapp/WhatsappMembershipService.php | 7 +- app/Support/MailHtml.php | 10 +- app/Support/SessionTimeoutConfig.php | 2 - app/ThirdParty/fpdf/font/courier.php | 9 +- app/ThirdParty/fpdf/font/courierb.php | 9 +- app/ThirdParty/fpdf/font/courierbi.php | 9 +- app/ThirdParty/fpdf/font/courieri.php | 9 +- app/ThirdParty/fpdf/font/helvetica.php | 30 +- app/ThirdParty/fpdf/font/helveticab.php | 30 +- app/ThirdParty/fpdf/font/helveticabi.php | 30 +- app/ThirdParty/fpdf/font/helveticai.php | 30 +- app/ThirdParty/fpdf/font/symbol.php | 30 +- app/ThirdParty/fpdf/font/times.php | 30 +- app/ThirdParty/fpdf/font/timesb.php | 30 +- app/ThirdParty/fpdf/font/timesbi.php | 30 +- app/ThirdParty/fpdf/font/timesi.php | 30 +- app/ThirdParty/fpdf/font/zapfdingbats.php | 30 +- app/ThirdParty/fpdf/fpdf.php | 3869 ++++++++--------- bootstrap/app.php | 62 +- bootstrap/cache/packages.php | 0 bootstrap/cache/services.php | 0 bootstrap/providers.php | 7 +- config/auth.php | 4 +- config/jwt.php | 7 +- config/sanctum.php | 9 +- database/database.sqlite | Bin 1265664 -> 0 bytes database/database.sqlite-journal | Bin 4616 -> 0 bytes database/factories/ClassSectionFactory.php | 4 +- database/factories/StudentFactory.php | 6 +- database/factories/UserFactory.php | 4 +- .../0001_01_01_000000_create_users_table.php | 1 + .../0001_01_01_000001_create_cache_table.php | 1 + .../0001_01_01_000002_create_jobs_table.php | 2 + .../2026_02_23_204001_create_users_table.php | 7 +- ...026_02_23_204002_create_students_table.php | 10 +- ...026_02_23_204030_create_expenses_table.php | 10 +- ...3_204035_create_current_incident_table.php | 7 +- ...026_02_23_204036_create_incident_table.php | 10 +- ...2_23_204037_create_grading_locks_table.php | 10 +- ...026_02_23_204038_create_homework_table.php | 8 +- ...4039_create_inventory_categories_table.php | 10 +- ...23_204040_create_inventory_items_table.php | 12 +- ...04041_create_inventory_movements_table.php | 12 +- ...2_23_204042_create_invoice_event_table.php | 10 +- ...026_02_23_204043_create_invoices_table.php | 10 +- ...044_create_invoice_students_list_table.php | 10 +- ..._02_23_204045_create_ip_attempts_table.php | 10 +- ..._23_204046_create_late_slip_logs_table.php | 8 +- ..._23_204047_create_login_activity_table.php | 10 +- ...23_204048_create_manual_payments_table.php | 10 +- ...026_02_23_204049_create_messages_table.php | 10 +- ...02_23_204050_create_midterm_exam_table.php | 8 +- ...1_create_missing_score_overrides_table.php | 10 +- ...26_02_23_204052_create_nav_items_table.php | 10 +- ...2_23_204053_create_notifications_table.php | 10 +- ...create_parent_attendance_reports_table.php | 10 +- ..._create_parent_meeting_schedules_table.php | 10 +- ...2026_02_23_204056_create_parents_table.php | 10 +- ...4057_create_parent_notifications_table.php | 10 +- ...2_23_204058_create_participation_table.php | 10 +- ...23_204059_create_password_resets_table.php | 11 +- ...0_create_password_reset_requests_table.php | 10 +- ...100_create_reimbursement_batches_table.php | 12 +- ...23_204101_create_paypal_payments_table.php | 10 +- ..._23_204101_create_reimbursements_table.php | 10 +- ...2_23_204102_create_payment_error_table.php | 10 +- ...026_02_23_204103_create_payments_table.php | 10 +- ...create_payment_notification_logs_table.php | 10 +- ...4105_create_payment_transactions_table.php | 10 +- ...04106_create_paypal_transactions_table.php | 11 +- ..._02_23_204107_create_permissions_table.php | 10 +- ..._204108_create_placement_batches_table.php | 10 +- ...3_204109_create_placement_levels_table.php | 12 +- ...3_204110_create_placement_scores_table.php | 10 +- ...3_204111_create_user_preferences_table.php | 10 +- ..._23_204112_create_print_requests_table.php | 13 +- ...2026_02_23_204113_create_project_table.php | 10 +- ...23_204114_create_promotion_queue_table.php | 10 +- .../2026_02_23_204117_create_quiz_table.php | 10 +- ...2026_02_23_204118_create_refunds_table.php | 10 +- ..._reimbursement_batch_admin_files_table.php | 10 +- .../2026_02_23_204123_create_roles_table.php | 10 +- ..._23_204124_create_role_nav_items_table.php | 10 +- ...3_204125_create_role_permissions_table.php | 10 +- ..._23_204126_create_score_comments_table.php | 10 +- ...026_02_23_204127_create_sections_table.php | 10 +- ...23_204128_create_semester_scores_table.php | 10 +- ...026_02_23_204129_create_settings_table.php | 10 +- ...3_204130_create_staff_attendance_table.php | 12 +- .../2026_02_23_204131_create_staff_table.php | 10 +- ..._204133_create_student_allergies_table.php | 12 +- ...2_23_204134_create_student_class_table.php | 10 +- ...reate_student_medical_conditions_table.php | 12 +- ..._create_subject_curriculum_items_table.php | 10 +- ...2_23_204138_create_teacher_class_table.php | 10 +- ..._submission_notification_history_table.php | 10 +- ...204142_create_user_notifications_table.php | 10 +- ...6_02_23_204143_create_user_roles_table.php | 10 +- ...4144_create_whatsapp_group_links_table.php | 10 +- ...reate_whatsapp_group_memberships_table.php | 8 +- ...4146_create_whatsapp_invites_log_table.php | 10 +- ...50_create_class_progress_reports_table.php | 10 +- ...ate_report_card_acknowledgements_table.php | 7 +- ...reate_class_progress_attachments_table.php | 12 +- ...create_reimbursement_batch_items_table.php | 12 +- ...900001_create_additional_charges_table.php | 10 +- ...eate_admin_notification_subjects_table.php | 10 +- ...eate_attendance_comment_template_table.php | 10 +- ...23_900004_create_attendance_data_table.php | 10 +- ..._23_900005_create_attendance_day_table.php | 10 +- ..._900006_create_attendance_record_table.php | 10 +- ...00007_create_attendance_tracking_table.php | 12 +- ...3_900008_create_authorized_users_table.php | 10 +- ...3_900009_create_badge_print_logs_table.php | 10 +- ...23_900010_create_calendar_events_table.php | 11 +- ...026_02_23_900011_create_chapters_table.php | 10 +- ...02_23_900012_create_classSection_table.php | 10 +- ...13_create_class_prep_adjustments_table.php | 10 +- ...014_create_class_preparation_log_table.php | 10 +- ...2026_02_23_900017_create_classes_table.php | 11 +- ...create_competition_class_winners_table.php | 10 +- ...900019_create_competition_scores_table.php | 10 +- ...00020_create_competition_winners_table.php | 10 +- ...02_23_900021_create_competitions_table.php | 10 +- ...2_23_900022_create_configuration_table.php | 10 +- ...26_02_23_900023_create_contactus_table.php | 10 +- ...02_23_900024_create_current_flag_table.php | 10 +- ...23_900025_create_discount_usages_table.php | 10 +- ..._900026_create_discount_vouchers_table.php | 10 +- ...reate_early_dismissal_signatures_table.php | 10 +- ...23_900028_create_email_templates_table.php | 10 +- ...900029_create_emergency_contacts_table.php | 10 +- ..._02_23_900030_create_enrollments_table.php | 10 +- ...2_23_900031_create_event_charges_table.php | 8 +- .../2026_02_23_900032_create_events_table.php | 10 +- ..._02_23_900033_create_exam_drafts_table.php | 12 +- ..._create_examquestiondistribution_table.php | 10 +- ...2_23_900035_create_examquestions_table.php | 10 +- .../2026_02_23_900036_create_exams_table.php | 10 +- ...026_02_23_900038_create_families_table.php | 11 +- ..._900039_create_family_comm_prefs_table.php | 12 +- ...3_900040_create_family_guardians_table.php | 12 +- ...23_900041_create_family_students_table.php | 12 +- ...6_02_23_900042_create_final_exam_table.php | 10 +- ..._02_23_900043_create_final_score_table.php | 10 +- ...00044_create_longanswerquestions_table.php | 10 +- ...6_create_notification_recipients_table.php | 10 +- ..._02_23_900047_create_preferences_table.php | 10 +- ...02_23_900048_create_qcmquestions_table.php | 10 +- ...26_02_23_900049_create_questions_table.php | 10 +- ...900050_create_questionstruefalse_table.php | 10 +- ...2_23_900051_create_questiontypes_table.php | 10 +- ...026_02_23_900052_create_scan_log_table.php | 10 +- ...0053_create_shortanswerquestions_table.php | 10 +- ...3_900054_create_support_requests_table.php | 10 +- ...5_create_teacher_attendance_data_table.php | 10 +- ...900056_create_communication_logs_table.php | 8 +- ...9_070060_add_fields_to_suppliers_table.php | 8 +- ...20000_create_certificate_records_table.php | 7 +- .../2026_06_04_120010_create_flag_table.php | 7 +- ...000_create_below_sixty_decisions_table.php | 7 +- ..._130010_create_student_decisions_table.php | 7 +- ...lifecycle_and_section_placement_tables.php | 12 +- ...00000_add_badge_scanning_list_nav_item.php | 8 +- ..._strong_grading_backward_compatibility.php | 22 +- ...04_230000_add_finance_lifecycle_tables.php | 18 +- ...0000_create_school_year_closure_tables.php | 13 +- ...5_000000_sort_nav_items_alphabetically.php | 2 +- docs/alrahma_end_to_end_use_case_test_plan.md | 380 ++ routes/api.php | 213 +- tests/Concerns/AssertsE2EApiResponses.php | 34 + tests/Concerns/CreatesApiTestUsers.php | 2 +- .../Concerns/SeedsAdministratorApiConfig.php | 2 +- tests/Concerns/SeedsE2ETestFixtures.php | 16 +- .../ApiAuthenticationAndAuthorizationTest.php | 5 +- .../Feature/Api/ApiPreferencesFeatureTest.php | 1 + tests/Feature/Api/ApiPublicEndpointsTest.php | 2 +- .../Api/ApiRolePermissionFeatureTest.php | 2 + tests/Feature/Api/ApiRouteContractTest.php | 10 +- .../Api/ApiUseCaseCoverageMatrixTest.php | 169 + .../Api/ApiUserManagementFeatureTest.php | 1 + .../AdministratorAbsenceControllerTest.php | 2 +- .../AdministratorEnrollmentControllerTest.php | 2 +- ...stratorEnrollmentUpdateStatusesApiTest.php | 3 +- ...dministratorNotificationControllerTest.php | 2 +- ...stratorTeacherSubmissionControllerTest.php | 2 +- ...istratorTeacherSubmissionReportApiTest.php | 2 +- .../EmergencyContactControllerTest.php | 10 +- .../AssignmentApiControllerTest.php | 8 +- tests/Feature/Api/V1/AssignmentApiTest.php | 98 +- .../Attendance/LateSlipLogsControllerTest.php | 2 +- .../V1/AttendanceCommentTemplateApiTest.php | 12 +- .../AttendanceTrackingControllerTest.php | 3 +- .../Api/V1/Auth/IpBanControllerTest.php | 2 +- tests/Feature/Api/V1/BadgeControllerTest.php | 2 +- .../BroadcastEmailControllerTest.php | 7 +- .../ClassProgressControllerTest.php | 10 +- .../V1/Classes/ClassSectionControllerTest.php | 2 +- .../CommunicationControllerTest.php | 4 +- .../V1/Family/FamilyAdminControllerTest.php | 8 +- .../Api/V1/Family/FamilyControllerTest.php | 6 +- .../V1/Finance/EventChargeControllerTest.php | 12 +- .../V1/Frontend/FrontendControllerTest.php | 2 +- .../V1/Frontend/InfoIconControllerTest.php | 1 + .../V1/Frontend/LandingPageControllerTest.php | 2 +- .../Api/V1/Frontend/PageControllerTest.php | 4 +- ...demicAttendanceFullSurfaceContractTest.php | 173 + .../ApiAccountStatusLifecycleContractTest.php | 31 + ...AdminOperationsFullSurfaceContractTest.php | 163 + ...achmentUploadContentSafetyContractTest.php | 55 + ...AdministrativeTraceabilityContractTest.php | 46 + ...thenticationLifecycleDepthContractTest.php | 90 + ...orizationCacheInvalidationContractTest.php | 29 + .../ApiBatch11RouteRegressionNetTest.php | 59 + ...tch12AcademicGradeBoundaryContractTest.php | 49 + ...Batch12AttachmentLifecycleContractTest.php | 56 + ...2AttendanceCorrectionAuditContractTest.php | 51 + ...atch12BulkMutationBoundaryContractTest.php | 47 + .../ApiBatch12CombinedRegressionSweepTest.php | 51 + ...istencyUnderPartialPayloadContractTest.php | 46 + ...tch12CrossActorDataLeakageContractTest.php | 53 + ...12FinancialLedgerIntegrityContractTest.php | 51 + ...ardianDelegationExpirationContractTest.php | 48 + ...atch12LongRunningExportJobContractTest.php | 41 + ...eModeAndOperationalControlContractTest.php | 47 + ...piBatch12RouteMetadataRiskRegisterTest.php | 39 + ...earchEnumerationResistanceContractTest.php | 27 + ...itiveConfigurationExposureContractTest.php | 43 + ...tch12WebhookProviderMatrixContractTest.php | 39 + ...ntRecoveryCredentialChangeContractTest.php | 55 + ...PaginationPerformanceGuardContractTest.php | 48 + ...ctMessagingAbuseModerationContractTest.php | 56 + ...untVoucherScholarshipAbuseContractTest.php | 58 + ...h13FullSurfaceRegressionSuperSweepTest.php | 61 + ...curementReceivingIntegrityContractTest.php | 53 + ...ocaleCalendarReligiousDateContractTest.php | 50 + ...3MobileOfflineSyncConflictContractTest.php | 57 + ...ObservableErrorCorrelationContractTest.php | 47 + ...ParentPortalInvoicePrivacyContractTest.php | 57 + ...oviderLedgerReconciliationContractTest.php | 68 + ...anscriptCertificateForgeryContractTest.php | 51 + ...RolePermissionMutationRaceContractTest.php | 55 + ...ch13StaffHrPayrollBoundaryContractTest.php | 49 + ...rollmentLifecycleIntegrityContractTest.php | 58 + ...osterAndAssignmentBoundaryContractTest.php | 54 + ...PromotionRollbackIntegrityContractTest.php | 32 + ...merationAndIdentityPrivacyContractTest.php | 38 + ...DelegationApprovalWorkflowContractTest.php | 30 + ...14ApiConsumerContractCompatibilityTest.php | 36 + ...endanceDeviceAndKioskAbuseContractTest.php | 32 + ...AuditTrailTamperResistanceContractTest.php | 32 + ...h14BulkAssignmentCollisionContractTest.php | 37 + ...14CrossModuleInvariantRiskRegisterTest.php | 38 + ...4DataLifecycleArchivePurgeContractTest.php | 31 + ...conciliationAndRefundAbuseContractTest.php | 31 + ...ificationTemplateInjectionContractTest.php | 34 + ...atch14PrintExportTamperingContractTest.php | 33 + .../ApiBatch14RegressionMegaSweepTest.php | 33 + ...lationshipOwnershipMatrixExpansionTest.php | 31 + ...4SessionCookieCsrfBoundaryContractTest.php | 33 + ...h14StoragePathAndSignedUrlContractTest.php | 29 + ...personationSessionBoundaryContractTest.php | 32 + ...TokenRotationAndRevocationContractTest.php | 50 + ...kgroundCommandAndSchedulerContractTest.php | 34 + ...rivacyAndDataSubjectRightsContractTest.php | 48 + ...ossSchoolIdentityCollisionContractTest.php | 35 + ...aResidencyBackupAndRestoreContractTest.php | 36 + ...AndNotificationConsistencyContractTest.php | 32 + ...reFlagExperimentAndRolloutContractTest.php | 32 + ...questAuthorizationCoverageContractTest.php | 33 + ...rchAndDirectoryEnumerationContractTest.php | 31 + ...ontentDirectionAndEncodingContractTest.php | 35 + ...gAndSparseFieldsetSecurityContractTest.php | 35 + ...15RateLimitAndAbusePatternContractTest.php | 35 + .../ApiBatch15RegressionUltraSweepTest.php | 47 + ...illdownAndAggregatePrivacyContractTest.php | 38 + ...15RouteParameterPoisoningExpansionTest.php | 30 + ...ntSafetyIncidentEscalationContractTest.php | 33 + ...rtExportAndPartialFailureExpansionTest.php | 81 + .../ApiBusinessRuleEdgeCaseContractTest.php | 85 + ...hnessAndConditionalRequestContractTest.php | 42 + ...piCalendarScheduleConflictContractTest.php | 56 + ...entCompatibilityVersioningContractTest.php | 71 + ...piConcurrencyAndStaleWriteContractTest.php | 49 + ...entNegotiationAndMediaTypeContractTest.php | 61 + ...piContractCoverageHeatmapExpansionTest.php | 74 + .../ApiContractSnapshotSeedTest.php | 58 + ...ModuleReferentialWorkflowExpansionTest.php | 32 + ...ossVersionRouteCompatibilityMatrixTest.php | 46 + ...piCsvExcelFormulaInjectionContractTest.php | 35 + ...piDataExportLeastPrivilegeContractTest.php | 61 + .../ApiDataMinimizationByRoleContractTest.php | 33 + ...ShapeBackwardCompatibilityContractTest.php | 61 + ...atabaseTransactionRollbackContractTest.php | 62 + ...teMoneyAndTimezoneBoundaryContractTest.php | 68 + ...piDeepAuthorizationIsolationMatrixTest.php | 70 + ...ocumentationAndRouteParityContractTest.php | 76 + ...leImportExportAndPrintableContractTest.php | 82 + ...encyAndDuplicateSubmissionContractTest.php | 95 + ...pPaginationSearchAndFilterContractTest.php | 98 + ...epResponseShapeAndEnvelopeContractTest.php | 99 + ...epStateTransitionInvariantContractTest.php | 77 + .../ApiDeepValidationFailureContractTest.php | 98 + ...DestructiveActionSafeguardContractTest.php | 87 + ...piEmptyDatasetAndNullStateContractTest.php | 92 + .../ApiEnumAndStateMachineContractTest.php | 44 + ...piErrorEnvelopeConsistencyContractTest.php | 89 + ...ApiEtagConditionalMutationContractTest.php | 28 + .../ApiFailureModeResilienceExpansionTest.php | 51 + ...ApiFamilyPortalFullSurfaceContractTest.php | 98 + ...eFlagAndConfigurationDriftContractTest.php | 55 + ...ApiFileMediaMetadataSafetyContractTest.php | 57 + ...yCommunicationsFullSurfaceContractTest.php | 134 + .../ApiFrontendBackendContractParityTest.php | 66 + .../ApiFullSurfaceEndToEndContractTest.php | 367 ++ .../ApiHtmlJavaScriptEscapingContractTest.php | 34 + ...tpSemanticsAndMethodSafetyContractTest.php | 55 + ...iIdempotencyScopeAndReplayContractTest.php | 42 + ...ationDelegationAndActingAsContractTest.php | 32 + .../ApiImportTemplateAndCsvContractTest.php | 47 + ...iLegacyCurrentAliasParityExpansionTest.php | 82 + ...alizationAndLocaleFallbackContractTest.php | 49 + ...ssAssignmentAndOverpostingContractTest.php | 75 + .../ApiMigrationSafeDefaultContractTest.php | 50 + .../ApiMobileClientToleranceContractTest.php | 49 + ...piModelLifecycleInvariantExpansionTest.php | 61 + ...nReadAfterWriteConsistencyContractTest.php | 62 + ...NestedPayloadAndArrayAbuseContractTest.php | 47 + ...ificationMessagingDeliveryContractTest.php | 58 + ...icationPreferenceAndOptOutContractTest.php | 60 + ...NullBooleanAndTypeCoercionContractTest.php | 32 + ...abilityRequestIdAndLoggingContractTest.php | 49 + ...iOpenApiDocumentationDriftContractTest.php | 60 + ...piOpenRedirectAndReturnUrlContractTest.php | 49 + ...perationalEndpointExposureContractTest.php | 25 + ...ionalResiliencePayloadSizeContractTest.php | 51 + ...piOwnerBoundaryAndPrivacyExpansionTest.php | 127 + ...nationCursorAndOffsetAbuseContractTest.php | 30 + ...PermissionMatrixExhaustiveContractTest.php | 81 + ...licyAuthorizationDiscoveryContractTest.php | 49 + ...lymorphicOwnershipBoundaryContractTest.php | 54 + ...PrivacyRetentionAndErasureContractTest.php | 49 + ...piPublicAndAuthFullSurfaceContractTest.php | 100 + ...blicPrivateCacheSeparationContractTest.php | 54 + .../ApiQueryParameterFuzzingContractTest.php | 98 + ...ApiQueueJobAndAsyncTriggerContractTest.php | 52 + ...itTokenAndSessionHardeningContractTest.php | 86 + ...tialIntegrityAndForeignKeyContractTest.php | 62 + ...ApiRelationshipCardinalityContractTest.php | 30 + ...ConsistencyAndAggregationExpansionTest.php | 51 + ...xportPrivacyAndAggregationContractTest.php | 51 + ...tMathAndFinancialInvariantContractTest.php | 53 + .../ApiRequestHeaderSpoofingContractTest.php | 31 + ...RoleWorkflowNegativeMatrixContractTest.php | 83 + ...ApiRouteNamingVersioningAndHygieneTest.php | 60 + .../ApiRouteParameterAbuseContractTest.php | 43 + ...ApiRouteUseCaseCatalogCompletenessTest.php | 160 + ...chemaKeyStabilityExpansionContractTest.php | 83 + ...lCalendarAcademicBoundaryExpansionTest.php | 49 + ...SchoolOperationsRegressionBackstopTest.php | 52 + ...SearchIndexAndAutocompleteContractTest.php | 51 + .../ApiSecurityAndTransportContractTest.php | 88 + ...tionSensitiveDataRedactionContractTest.php | 70 + ...eteRestoreArchiveLifecycleContractTest.php | 47 + ...ngFieldSelectionAndIncludeContractTest.php | 34 + ...olYearAndSemesterIsolationContractTest.php | 56 + ...oneCalendarRecurrenceDepthContractTest.php | 34 + ...icodeNormalizationAndInputContractTest.php | 54 + ...FetchSsrfAndCallbackSafetyContractTest.php | 43 + ...xternalProviderAndCallbackContractTest.php | 49 + ...ookReplayAndSignatureDepthContractTest.php | 40 + .../ApiWriteAuditAndTimestampContractTest.php | 116 + .../Support/FullSurfaceE2EContractCase.php | 344 ++ .../V1/Incidents/IncidentControllerTest.php | 2 +- .../InventoryCategoriesControllerTest.php | 6 +- .../InventoryItemsControllerTest.php | 16 +- .../InventoryMovementsControllerTest.php | 4 +- .../V1/Inventory/SuppliersControllerTest.php | 4 +- .../SupplyCategoriesControllerTest.php | 4 +- .../V1/Messaging/MessagesControllerTest.php | 8 +- .../Parents/AuthorizedUsersControllerTest.php | 8 +- .../ParentAttendanceReportControllerTest.php | 3 +- .../PrintRequestsWorkflowTest.php | 4 +- .../ParentPromotionControllerTest.php | 9 +- .../V1/Public/PublicWinnersControllerTest.php | 4 +- .../Api/V1/Reports/FilesControllerTest.php | 8 +- .../V1/Reports/ReportCardsControllerTest.php | 10 +- .../V1/Roles/RolePermissionControllerTest.php | 20 +- .../V1/Roles/RoleSwitcherControllerTest.php | 1 + .../V1/Settings/PreferencesControllerTest.php | 6 +- .../Settings/SchoolCalendarControllerTest.php | 12 +- .../V1/Settings/SettingsControllerTest.php | 3 +- .../Api/V1/Staff/StaffControllerTest.php | 6 +- .../Api/V1/Students/StudentControllerTest.php | 2 +- .../Api/V1/Support/SupportControllerTest.php | 2 +- .../Api/V1/Teachers/TeacherControllerTest.php | 2 +- tests/Feature/Api/V1/Ui/UiControllerTest.php | 2 +- .../Whatsapp/WhatsappInviteControllerTest.php | 3 +- .../Whatsapp/WhatsappLinkControllerTest.php | 6 +- .../ScenarioAFamilyEnrollmentTest.php | 6 +- .../ScenarioBSundaySchoolDayTest.php | 4 +- .../Workflows/ScenarioCAcademicTermTest.php | 4 +- .../Workflows/ScenarioDFamilyBillingTest.php | 4 +- .../V1/Workflows/ScenarioEFinanceEdgeTest.php | 18 +- .../ScenarioFTeacherSubmissionTest.php | 1 + .../V1/Workflows/ScenarioGPrintBadgeTest.php | 4 +- ...cenarioHSecurityUnauthorizedAccessTest.php | 2 +- ...cenarioIPublicAuthAndSystemUseCaseTest.php | 130 + ...cenarioJCrossModuleApiUseCaseSmokeTest.php | 127 + ...enarioKParentSelfServiceAndPrivacyTest.php | 149 + ...ScenarioLTeacherAcademicOperationsTest.php | 130 + ...tratorEnrollmentAndClassOperationsTest.php | 134 + ...NCommunicationsAndSupportLifecycleTest.php | 99 + ...oOInventoryFinanceAndAuditControlsTest.php | 115 + ...ConfigurationAndCalendarGovernanceTest.php | 139 + ...choolYearPromotionClosureLifecycleTest.php | 151 + ...anceOperationsAndExceptionHandlingTest.php | 175 + ...oringGradingAndReportCardLifecycleTest.php | 131 + ...rintBadgesAndCertificatesLifecycleTest.php | 132 + ...ymentRefundAndInstallmentLifecycleTest.php | 157 + ...entReimbursementAndSupplyLifecycleTest.php | 153 + ...ntrolRolesNavigationAndPreferencesTest.php | 134 + ...oskScannerAndVerificationLifecycleTest.php | 107 + ...AuthorizedUserAndWhatsappLifecycleTest.php | 143 + ...yAliasesValidationAndCompatibilityTest.php | 155 + .../StudentEnrollmentWorkflowTest.php | 9 +- tests/Feature/BulkUserRoleCreationTest.php | 2 +- .../RequirePermissionMiddlewareTest.php | 2 +- tests/Feature/Web/SchoolYearAdminPageTest.php | 4 +- tests/Unit/ApiLoginSecurityServiceTest.php | 6 +- tests/Unit/AuthSessionServiceTest.php | 2 +- ...ttendanceCommentTemplateControllerTest.php | 4 +- .../Unit/Grading/ScoreValueValidatorTest.php | 6 +- tests/Unit/Models/AdditionalChargeTest.php | 12 +- .../AttendanceCommentTemplateModelTest.php | 2 +- tests/Unit/Models/AttendanceDataTest.php | 8 +- .../Models/AttendanceEmailTemplateTest.php | 8 +- tests/Unit/Models/AttendanceRecordTest.php | 6 +- tests/Unit/Models/AttendanceTrackingTest.php | 6 +- tests/Unit/Models/AuthorizedUserTest.php | 6 +- tests/Unit/Models/BadgePrintLogTest.php | 6 +- tests/Unit/Models/CalendarTest.php | 6 +- tests/Unit/Models/ClassPrepAdjustmentTest.php | 6 +- tests/Unit/Models/ClassPreparationLogTest.php | 6 +- .../Models/ClassProgressAttachmentTest.php | 6 +- tests/Unit/Models/ClassProgressReportTest.php | 6 +- tests/Unit/Models/ClassSectionTest.php | 6 +- tests/Unit/Models/ClassTest.php | 6 +- tests/Unit/Models/CommunicationLogTest.php | 6 +- .../Models/CompetitionClassWinnerTest.php | 6 +- tests/Unit/Models/CompetitionScoreTest.php | 6 +- tests/Unit/Models/CompetitionTest.php | 8 +- tests/Unit/Models/CompetitionWinnerTest.php | 6 +- tests/Unit/Models/ConfigurationTest.php | 6 +- tests/Unit/Models/ContactUsTest.php | 6 +- tests/Unit/Models/CurrentFlagTest.php | 6 +- tests/Unit/Models/DiscountUsageTest.php | 6 +- tests/Unit/Models/DiscountVoucherTest.php | 6 +- .../Models/EarlyDismissalSignatureTest.php | 6 +- tests/Unit/Models/EmergencyContactTest.php | 6 +- tests/Unit/Models/EnrollmentTest.php | 6 +- tests/Unit/Models/EventChargesTest.php | 6 +- tests/Unit/Models/EventTest.php | 6 +- tests/Unit/Models/ExamDraftTest.php | 6 +- tests/Unit/Models/ExamTest.php | 6 +- tests/Unit/Models/ExpenseTest.php | 6 +- tests/Unit/Models/FamilyCommPrefTest.php | 6 +- tests/Unit/Models/FamilyGuardianTest.php | 6 +- tests/Unit/Models/FamilyStudentTest.php | 6 +- tests/Unit/Models/FamilyTest.php | 6 +- tests/Unit/Models/FinalExamTest.php | 6 +- tests/Unit/Models/FinalScoreTest.php | 6 +- tests/Unit/Models/GradingLockTest.php | 6 +- tests/Unit/Models/HomeworkTest.php | 6 +- tests/Unit/Models/IncidentTest.php | 6 +- tests/Unit/Models/InventoryCategoryTest.php | 6 +- tests/Unit/Models/InventoryItemTest.php | 6 +- tests/Unit/Models/InventoryMovementTest.php | 6 +- tests/Unit/Models/InvoiceEventTest.php | 6 +- tests/Unit/Models/InvoiceStudentListTest.php | 6 +- tests/Unit/Models/InvoiceTest.php | 6 +- tests/Unit/Models/IpAttemptTest.php | 6 +- tests/Unit/Models/LateSlipLogTest.php | 6 +- tests/Unit/Models/LoginActivityTest.php | 6 +- tests/Unit/Models/ManualPaymentTest.php | 6 +- tests/Unit/Models/MessageTest.php | 6 +- tests/Unit/Models/MidtermExamTest.php | 6 +- .../Unit/Models/MissingScoreOverrideTest.php | 6 +- tests/Unit/Models/NavItemTest.php | 8 +- tests/Unit/Models/NotificationTest.php | 8 +- .../Models/ParentAttendanceReportTest.php | 6 +- .../Unit/Models/ParentMeetingScheduleTest.php | 6 +- tests/Unit/Models/ParentNotificationTest.php | 6 +- tests/Unit/Models/ParentTest.php | 6 +- tests/Unit/Models/ParticipationTest.php | 6 +- .../Unit/Models/PasswordResetRequestTest.php | 6 +- tests/Unit/Models/PasswordResetTest.php | 6 +- tests/Unit/Models/PayPalPaymentTest.php | 6 +- tests/Unit/Models/PaymentErrorTest.php | 6 +- .../Models/PaymentNotificationLogTest.php | 6 +- tests/Unit/Models/PaymentTest.php | 6 +- tests/Unit/Models/PaymentTransactionTest.php | 6 +- tests/Unit/Models/PaypalTransactionTest.php | 6 +- tests/Unit/Models/PermissionTest.php | 6 +- tests/Unit/Models/PlacementBatchTest.php | 6 +- tests/Unit/Models/PlacementLevelTest.php | 6 +- tests/Unit/Models/PlacementScoreTest.php | 6 +- tests/Unit/Models/PreferencesTest.php | 6 +- tests/Unit/Models/PrintRequestTest.php | 6 +- tests/Unit/Models/ProjectTest.php | 6 +- tests/Unit/Models/PromotionQueueTest.php | 6 +- tests/Unit/Models/PurchaseOrderItemTest.php | 6 +- tests/Unit/Models/PurchaseOrderTest.php | 8 +- tests/Unit/Models/QuizTest.php | 6 +- tests/Unit/Models/RefundTest.php | 6 +- .../ReimbursementBatchAdminFileTest.php | 6 +- .../Models/ReimbursementBatchItemTest.php | 6 +- tests/Unit/Models/ReimbursementBatchTest.php | 6 +- tests/Unit/Models/ReimbursementTest.php | 6 +- tests/Unit/Models/RoleNavItemTest.php | 6 +- tests/Unit/Models/RolePermissionTest.php | 6 +- tests/Unit/Models/RoleTest.php | 6 +- tests/Unit/Models/ScoreCommentTest.php | 6 +- tests/Unit/Models/SectionTest.php | 6 +- tests/Unit/Models/SemesterScoreTest.php | 6 +- tests/Unit/Models/SettingsTest.php | 6 +- tests/Unit/Models/StaffAttendanceTest.php | 6 +- tests/Unit/Models/StaffTest.php | 6 +- tests/Unit/Models/StatsTest.php | 6 +- tests/Unit/Models/StudentAllergyTest.php | 6 +- tests/Unit/Models/StudentClassTest.php | 6 +- .../Models/StudentMedicalConditionTest.php | 6 +- tests/Unit/Models/StudentTest.php | 6 +- tests/Unit/Models/SubjectCurriculumTest.php | 6 +- tests/Unit/Models/TeacherClassTest.php | 6 +- ...acherSubmissionNotificationHistoryTest.php | 6 +- tests/Unit/Models/TeacherTest.php | 6 +- tests/Unit/Models/UserNotificationTest.php | 6 +- tests/Unit/Models/UserRoleTest.php | 6 +- tests/Unit/Models/UserTest.php | 6 +- tests/Unit/Models/WhatsappGroupLinkTest.php | 6 +- .../Models/WhatsappGroupMembershipTest.php | 6 +- tests/Unit/Models/WhatsappInviteLogTest.php | 6 +- ...reAttendanceCommentTemplateRequestTest.php | 4 +- ...teAttendanceCommentTemplateRequestTest.php | 4 +- .../AdminNotificationSubjectServiceTest.php | 3 +- .../AdminNotificationUserServiceTest.php | 2 +- .../AdminPrintRecipientServiceTest.php | 3 +- .../AdministratorAbsenceServiceTest.php | 16 +- .../AdministratorDashboardServiceTest.php | 6 +- ...dministratorEnrollmentEventServiceTest.php | 2 +- ...dministratorEnrollmentQueryServiceTest.php | 15 +- ...ministratorEnrollmentRefundServiceTest.php | 3 +- .../AdministratorEnrollmentServiceTest.php | 6 +- ...ministratorEnrollmentStatusServiceTest.php | 15 +- .../AdministratorMetricsServiceTest.php | 10 +- .../AdministratorNotificationServiceTest.php | 6 +- .../AdministratorSharedServiceTest.php | 3 +- ...inistratorTeacherSubmissionServiceTest.php | 9 +- .../AdministratorUserSearchServiceTest.php | 2 +- ...acherSubmissionNotificationServiceTest.php | 6 +- .../TeacherSubmissionReportServiceTest.php | 6 +- .../TeacherSubmissionSupportServiceTest.php | 4 +- .../AssignmentContextServiceTest.php | 2 +- .../AssignmentDataLoaderServiceTest.php | 4 +- .../Assignment/AssignmentMetaServiceTest.php | 6 +- .../AssignmentSectionServiceTest.php | 2 +- tests/Unit/Services/AssignmentServiceTest.php | 7 - .../AttendanceAutoPublishJobServiceTest.php | 2 +- .../AttendanceAutoPublishServiceTest.php | 4 +- .../AttendanceCommentServiceTest.php | 4 +- .../AttendanceConsequenceServiceTest.php | 3 +- .../AttendancePolicyServiceTest.php | 8 +- .../Attendance/AttendanceQueryServiceTest.php | 88 +- .../AttendanceRecordSyncServiceTest.php | 5 +- .../AttendanceSemesterRangeServiceTest.php | 4 +- .../Attendance/AttendanceServiceTest.php | 20 +- .../AttendanceSummaryRebuildServiceTest.php | 2 +- .../Attendance/AttendanceTrackingService.php | 299 +- .../Attendance/SemesterRangeServiceTest.php | 8 +- .../Attendance/StaffAttendanceServiceTest.php | 15 +- .../StudentAttendanceWriterServiceTest.php | 6 +- ...TeacherAttendanceSubmissionServiceTest.php | 29 +- .../AttendanceManagementServiceTest.php | 4 +- .../AttendanceCaseQueryServiceTest.php | 15 +- ...endanceCommunicationSupportServiceTest.php | 15 +- .../AttendanceEmailComposerServiceTest.php | 5 +- .../AttendanceNotificationLogServiceTest.php | 3 +- ...endanceNotificationWorkflowServiceTest.php | 30 +- .../AttendanceParentLookupServiceTest.php | 4 +- .../AttendancePendingViolationServiceTest.php | 9 +- .../AttendanceTrackingServiceTest.php | 2 +- ...nceViolationStudentResolverServiceTest.php | 9 +- .../ViolationRuleEngineServiceTest.php | 6 +- .../Auth/PasswordResetCleanupServiceTest.php | 2 +- .../Auth/PermissionCheckServiceTest.php | 2 +- .../Auth/RegistrationCaptchaServiceTest.php | 4 +- .../Auth/RegistrationFormatterServiceTest.php | 4 +- .../Services/Auth/RegistrationServiceTest.php | 12 +- .../Services/Auth/UserRoleServiceTest.php | 2 +- .../Badges/BadgeFormDataServiceTest.php | 6 +- .../Services/Badges/BadgePdfServiceTest.php | 6 +- .../Badges/BadgePrintLogServiceTest.php | 8 +- .../Badges/BadgeTextFormatterTest.php | 6 +- .../Billing/BalanceCalculationServiceTest.php | 8 +- .../Billing/BillingTotalsServiceTest.php | 6 +- .../BroadcastEmailComposerServiceTest.php | 4 +- .../BroadcastEmailRecipientServiceTest.php | 4 +- ...BroadcastEmailSenderOptionsServiceTest.php | 4 +- .../ClassPrep/ClassRosterServiceTest.php | 2 +- .../ClassPrep/StickerCountServiceTest.php | 4 +- .../ClassPreparationAdjustmentServiceTest.php | 2 +- .../ClassPreparationCalculatorServiceTest.php | 4 +- .../ClassPreparationContextServiceTest.php | 2 +- .../ClassPreparationInventoryServiceTest.php | 2 +- .../ClassPreparationLogServiceTest.php | 5 +- .../ClassPreparationRosterServiceTest.php | 4 +- .../ClassProgressAttachmentServiceTest.php | 2 +- .../ClassProgressMetaServiceTest.php | 2 +- .../ClassProgressMutationServiceTest.php | 12 +- .../ClassProgressQueryServiceTest.php | 2 +- .../ClassProgressRuleServiceTest.php | 4 +- .../CommunicationTemplateServiceTest.php | 2 +- .../CompetitionScoresSaveServiceTest.php | 5 +- .../Discounts/DiscountApplyServiceTest.php | 2 +- .../Email/EmailExtractorServiceTest.php | 2 +- .../Email/EmailProfileServiceTest.php | 6 +- .../Events/EventChargeQueryServiceTest.php | 2 +- .../Services/Events/EventListServiceTest.php | 2 +- .../Events/EventStudentChargeServiceTest.php | 2 +- .../Exams/ExamDraftTeacherServiceTest.php | 2 +- .../Expenses/ExpenseReceiptServiceTest.php | 4 +- .../Expenses/ExpenseStaffServiceTest.php | 2 +- .../ExtraChargesMetaServiceTest.php | 4 +- .../Families/FamilyFinanceServiceTest.php | 2 +- .../Families/FamilyMutationServiceTest.php | 8 +- .../Families/FamilyQueryServiceTest.php | 10 +- .../Services/Fees/FeeGradeServiceTest.php | 4 +- .../Fees/FeeRefundCalculatorServiceTest.php | 6 +- .../Fees/FeeRefundDetailServiceTest.php | 2 +- .../Fees/FeeStudentFeeServiceTest.php | 2 +- .../Fees/TuitionCalculationServiceTest.php | 5 +- .../ExamDraftDownloadNameServiceTest.php | 2 +- .../Services/Files/FileServeServiceTest.php | 18 +- .../Finance/FinancialChartServiceTest.php | 2 +- .../Finance/FinancialPaymentServiceTest.php | 2 +- .../Finance/FinancialPdfReportServiceTest.php | 2 +- .../Finance/FinancialReportServiceTest.php | 4 +- .../FinancialSchoolYearServiceTest.php | 2 +- .../Finance/FinancialSummaryServiceTest.php | 2 +- .../FinancialUnpaidParentsServiceTest.php | 2 +- .../Frontend/ContactSubmissionServiceTest.php | 2 +- .../Frontend/FrontendPageServiceTest.php | 2 +- .../Frontend/StaticPageServiceTest.php | 6 +- .../Grading/GradingBelowSixtyServiceTest.php | 2 +- .../Grading/GradingLockServiceTest.php | 4 +- .../Grading/HomeworkTrackingServiceTest.php | 2 +- .../Incidents/IncidentAnalysisServiceTest.php | 2 +- .../Incidents/IncidentHistoryServiceTest.php | 2 +- .../Incidents/IncidentLookupServiceTest.php | 4 +- .../InventoryCategoryServiceTest.php | 4 +- .../Inventory/InventoryItemServiceTest.php | 4 +- .../InventoryMovementServiceTest.php | 2 +- .../Inventory/InventorySummaryServiceTest.php | 2 +- ...nventoryTeacherDistributionServiceTest.php | 8 +- .../Inventory/SupplierServiceTest.php | 4 +- .../Inventory/SupplyCategoryServiceTest.php | 4 +- .../Invoices/InvoiceConfigServiceTest.php | 2 +- .../Invoices/InvoiceGenerationServiceTest.php | 2 +- .../Invoices/InvoiceManagementServiceTest.php | 2 +- .../Invoices/InvoicePaymentServiceTest.php | 2 +- .../Invoices/InvoicePdfServiceTest.php | 2 +- .../Messaging/MessageCommandServiceTest.php | 4 +- .../Messaging/MessageQueryServiceTest.php | 2 +- .../Messaging/MessageRecipientServiceTest.php | 4 +- .../Navigation/NavBuilderServiceTest.php | 6 +- .../Services/Navigation/NavbarServiceTest.php | 2 +- .../NotificationActiveServiceTest.php | 2 +- .../NotificationCleanupServiceTest.php | 2 +- .../NotificationDeletedServiceTest.php | 2 +- .../NotificationDispatchServiceTest.php | 2 +- .../NotificationManagementServiceTest.php | 6 +- .../NotificationReadServiceTest.php | 4 +- .../NotificationRecipientServiceTest.php | 2 +- .../NotificationShowServiceTest.php | 4 +- .../NotificationUserListServiceTest.php | 2 +- .../UserNotificationDispatchServiceTest.php | 4 +- .../ParentAttendanceReportServiceTest.php | 6 +- .../Parents/ParentAttendanceServiceTest.php | 3 +- .../Parents/ParentConfigServiceTest.php | 2 +- .../ParentEmergencyContactServiceTest.php | 2 +- .../Parents/ParentEnrollmentServiceTest.php | 2 +- .../Parents/ParentInvoiceServiceTest.php | 2 +- .../Parents/ParentRegistrationServiceTest.php | 2 +- .../Payments/PaymentBalanceServiceTest.php | 2 +- .../PaymentEnrollmentEventServiceTest.php | 2 +- .../Payments/PaymentLookupServiceTest.php | 2 +- ...PaymentNotificationDispatchServiceTest.php | 2 +- .../Payments/PaymentPlanServiceTest.php | 2 +- .../PaymentTransactionServiceTest.php | 8 +- .../Payments/PaypalPaymentServiceTest.php | 2 +- .../PaypalTransactionsServiceTest.php | 2 +- .../Policy/PolicyContentServiceTest.php | 2 +- .../LevelProgressionServiceTest.php | 8 +- .../PromotionEligibilityServiceTest.php | 10 +- .../PromotionEnrollmentServiceTest.php | 2 +- .../Promotions/PromotionStatusServiceTest.php | 2 +- .../PurchaseOrderCreateServiceTest.php | 2 +- .../PurchaseOrderReceiveServiceTest.php | 2 +- .../PurchaseOrderStatusServiceTest.php | 2 +- .../Refunds/RefundDecisionServiceTest.php | 2 +- .../Refunds/RefundNotificationServiceTest.php | 2 +- .../Refunds/RefundOverpaymentServiceTest.php | 2 +- .../Refunds/RefundPayoutServiceTest.php | 2 +- .../Refunds/RefundPolicyServiceTest.php | 8 +- .../Refunds/RefundQueryServiceTest.php | 2 +- .../Refunds/RefundRequestServiceTest.php | 10 +- .../Refunds/RefundSummaryServiceTest.php | 2 +- ...eimbursementBatchAssignmentServiceTest.php | 2 +- .../ReimbursementBatchServiceTest.php | 2 +- .../ReimbursementDonationServiceTest.php | 2 +- .../ReimbursementRecipientServiceTest.php | 2 +- .../ReportCards/ReportCardServiceTest.php | 10 +- .../SlipPrinterFormatterServiceTest.php | 4 +- .../Stickers/StickerPresetServiceTest.php | 2 +- .../Stickers/StickerPrintServiceTest.php | 4 +- .../Stickers/StickerQueryServiceTest.php | 4 +- .../Roles/PermissionCrudServiceTest.php | 2 +- .../Roles/RoleAssignmentServiceTest.php | 3 +- .../Services/Roles/RoleCrudServiceTest.php | 2 +- .../Roles/RoleDashboardServiceTest.php | 2 +- .../Roles/RolePermissionServiceTest.php | 2 +- .../Services/Roles/RoleQueryServiceTest.php | 4 +- .../Services/Roles/RoleSwitchServiceTest.php | 2 +- .../School/SemesterSelectionServiceTest.php | 4 +- .../SchoolIdGenerationServiceTest.php | 2 +- .../Scores/AttendanceCalculatorTest.php | 2 +- .../Services/Scores/ExamScoreServiceTest.php | 4 +- .../Scores/HomeworkCalculatorTest.php | 2 +- .../Scores/HomeworkScoreServiceTest.php | 4 +- .../Scores/ParticipationScoreServiceTest.php | 4 +- .../Services/Scores/ProjectCalculatorTest.php | 2 +- .../Scores/ProjectScoreServiceTest.php | 6 +- .../Services/Scores/QuizCalculatorTest.php | 2 +- .../Services/Scores/QuizScoreServiceTest.php | 6 +- .../Scores/ScoreCommentServiceTest.php | 4 +- .../Scores/ScoreDashboardServiceTest.php | 4 +- .../Scores/ScorePredictorServiceTest.php | 4 +- .../Services/Scores/ScoreTermServiceTest.php | 4 +- .../Scores/SemesterScoreServiceTest.php | 22 +- .../Services/Security/Pbkdf2HasherTest.php | 2 +- .../Semesters/SemesterConfigServiceTest.php | 2 +- .../Semesters/SemesterRangeServiceTest.php | 6 +- .../Settings/ConfigurationServiceTest.php | 2 +- .../SchoolCalendarContextServiceTest.php | 2 +- .../SchoolCalendarFormatterServiceTest.php | 4 +- .../SchoolCalendarMeetingServiceTest.php | 2 +- .../SchoolCalendarMutationServiceTest.php | 4 +- .../SchoolCalendarQueryServiceTest.php | 4 +- .../Services/Settings/SettingsServiceTest.php | 4 +- .../Staff/StaffCommandServiceTest.php | 4 +- .../Services/Staff/StaffQueryServiceTest.php | 2 +- .../Students/StudentAssignmentServiceTest.php | 4 +- .../Students/StudentProfileServiceTest.php | 2 +- .../Students/StudentScoreCardServiceTest.php | 2 +- .../Subjects/SubjectCurriculumServiceTest.php | 4 +- .../Support/SupportRequestServiceTest.php | 2 +- .../System/CleanupSchedulerServiceTest.php | 6 +- .../System/ConfigUpdateServiceTest.php | 4 +- .../System/GlobalConfigServiceTest.php | 2 +- .../Unit/Services/System/TimeServiceTest.php | 2 +- .../Teachers/TeacherAbsenceServiceTest.php | 14 +- .../Teachers/TeacherAssignmentServiceTest.php | 4 +- .../Teachers/TeacherDashboardServiceTest.php | 2 +- tests/Unit/Services/Ui/UiStyleServiceTest.php | 2 +- .../Users/InactiveUserCleanupServiceTest.php | 2 +- .../Services/Users/UserListServiceTest.php | 1 + .../Whatsapp/WhatsappContactServiceTest.php | 2 +- .../Whatsapp/WhatsappContextServiceTest.php | 4 +- .../WhatsappInviteBundleServiceTest.php | 4 +- .../WhatsappInviteNotificationServiceTest.php | 7 +- .../Whatsapp/WhatsappInviteServiceTest.php | 9 +- .../Whatsapp/WhatsappLinkServiceTest.php | 4 +- .../WhatsappMembershipServiceTest.php | 4 +- 1480 files changed, 22587 insertions(+), 10762 deletions(-) mode change 100755 => 100644 bootstrap/cache/packages.php mode change 100755 => 100644 bootstrap/cache/services.php delete mode 100644 database/database.sqlite delete mode 100644 database/database.sqlite-journal create mode 100644 tests/Concerns/AssertsE2EApiResponses.php create mode 100644 tests/Feature/Api/ApiUseCaseCoverageMatrixTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiAcademicAttendanceFullSurfaceContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiAccountStatusLifecycleContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiAdminOperationsFullSurfaceContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiAttachmentUploadContentSafetyContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiAuditLogAndAdministrativeTraceabilityContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiAuthenticationLifecycleDepthContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiAuthorizationCacheInvalidationContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiBatch11RouteRegressionNetTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiBatch12AcademicGradeBoundaryContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiBatch12AttachmentLifecycleContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiBatch12AttendanceCorrectionAuditContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiBatch12BulkMutationBoundaryContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiBatch12CombinedRegressionSweepTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiBatch12ConsistencyUnderPartialPayloadContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiBatch12CrossActorDataLeakageContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiBatch12FinancialLedgerIntegrityContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiBatch12GuardianDelegationExpirationContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiBatch12LongRunningExportJobContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiBatch12MaintenanceModeAndOperationalControlContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiBatch12RouteMetadataRiskRegisterTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiBatch12SearchEnumerationResistanceContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiBatch12SensitiveConfigurationExposureContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiBatch12WebhookProviderMatrixContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiBatch13AccountRecoveryCredentialChangeContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiBatch13ApiPaginationPerformanceGuardContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiBatch13ContactMessagingAbuseModerationContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiBatch13DiscountVoucherScholarshipAbuseContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiBatch13FullSurfaceRegressionSuperSweepTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiBatch13InventoryProcurementReceivingIntegrityContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiBatch13LocaleCalendarReligiousDateContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiBatch13MobileOfflineSyncConflictContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiBatch13ObservableErrorCorrelationContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiBatch13ParentPortalInvoicePrivacyContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiBatch13PaymentProviderLedgerReconciliationContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiBatch13ReportCardTranscriptCertificateForgeryContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiBatch13RolePermissionMutationRaceContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiBatch13StaffHrPayrollBoundaryContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiBatch13StudentEnrollmentLifecycleIntegrityContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiBatch13TeacherRosterAndAssignmentBoundaryContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiBatch14AcademicPromotionRollbackIntegrityContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiBatch14AccountEnumerationAndIdentityPrivacyContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiBatch14AdminDelegationApprovalWorkflowContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiBatch14ApiConsumerContractCompatibilityTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiBatch14AttendanceDeviceAndKioskAbuseContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiBatch14AuditTrailTamperResistanceContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiBatch14BulkAssignmentCollisionContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiBatch14CrossModuleInvariantRiskRegisterTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiBatch14DataLifecycleArchivePurgeContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiBatch14FinanceReconciliationAndRefundAbuseContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiBatch14NotificationTemplateInjectionContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiBatch14PrintExportTamperingContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiBatch14RegressionMegaSweepTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiBatch14RelationshipOwnershipMatrixExpansionTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiBatch14SessionCookieCsrfBoundaryContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiBatch14StoragePathAndSignedUrlContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiBatch15AdminImpersonationSessionBoundaryContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiBatch15AuthTokenRotationAndRevocationContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiBatch15BackgroundCommandAndSchedulerContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiBatch15ConsentPrivacyAndDataSubjectRightsContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiBatch15CrossSchoolIdentityCollisionContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiBatch15DataResidencyBackupAndRestoreContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiBatch15DomainEventOutboxAndNotificationConsistencyContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiBatch15FeatureFlagExperimentAndRolloutContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiBatch15FormRequestAuthorizationCoverageContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiBatch15GlobalSearchAndDirectoryEnumerationContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiBatch15LocalizationContentDirectionAndEncodingContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiBatch15MetadataFilteringAndSparseFieldsetSecurityContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiBatch15RateLimitAndAbusePatternContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiBatch15RegressionUltraSweepTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiBatch15ReportDrilldownAndAggregatePrivacyContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiBatch15RouteParameterPoisoningExpansionTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiBatch15StudentSafetyIncidentEscalationContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiBulkImportExportAndPartialFailureExpansionTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiBusinessRuleEdgeCaseContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiCacheFreshnessAndConditionalRequestContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiCalendarScheduleConflictContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiClientCompatibilityVersioningContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiConcurrencyAndStaleWriteContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiContentNegotiationAndMediaTypeContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiContractCoverageHeatmapExpansionTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiContractSnapshotSeedTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiCrossModuleReferentialWorkflowExpansionTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiCrossVersionRouteCompatibilityMatrixTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiCsvExcelFormulaInjectionContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiDataExportLeastPrivilegeContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiDataMinimizationByRoleContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiDataShapeBackwardCompatibilityContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiDatabaseTransactionRollbackContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiDateMoneyAndTimezoneBoundaryContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiDeepAuthorizationIsolationMatrixTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiDeepDocumentationAndRouteParityContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiDeepFileImportExportAndPrintableContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiDeepIdempotencyAndDuplicateSubmissionContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiDeepPaginationSearchAndFilterContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiDeepResponseShapeAndEnvelopeContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiDeepStateTransitionInvariantContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiDeepValidationFailureContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiDestructiveActionSafeguardContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiEmptyDatasetAndNullStateContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiEnumAndStateMachineContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiErrorEnvelopeConsistencyContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiEtagConditionalMutationContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiFailureModeResilienceExpansionTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiFamilyPortalFullSurfaceContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiFeatureFlagAndConfigurationDriftContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiFileMediaMetadataSafetyContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiFinanceInventoryCommunicationsFullSurfaceContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiFrontendBackendContractParityTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiFullSurfaceEndToEndContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiHtmlJavaScriptEscapingContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiHttpSemanticsAndMethodSafetyContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiIdempotencyScopeAndReplayContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiImpersonationDelegationAndActingAsContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiImportTemplateAndCsvContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiLegacyCurrentAliasParityExpansionTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiLocalizationAndLocaleFallbackContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiMassAssignmentAndOverpostingContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiMigrationSafeDefaultContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiMobileClientToleranceContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiModelLifecycleInvariantExpansionTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiMutationReadAfterWriteConsistencyContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiNestedPayloadAndArrayAbuseContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiNotificationMessagingDeliveryContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiNotificationPreferenceAndOptOutContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiNullBooleanAndTypeCoercionContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiObservabilityRequestIdAndLoggingContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiOpenApiDocumentationDriftContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiOpenRedirectAndReturnUrlContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiOperationalEndpointExposureContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiOperationalResiliencePayloadSizeContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiOwnerBoundaryAndPrivacyExpansionTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiPaginationCursorAndOffsetAbuseContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiPermissionMatrixExhaustiveContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiPolicyAuthorizationDiscoveryContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiPolymorphicOwnershipBoundaryContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiPrivacyRetentionAndErasureContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiPublicAndAuthFullSurfaceContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiPublicPrivateCacheSeparationContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiQueryParameterFuzzingContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiQueueJobAndAsyncTriggerContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiRateLimitTokenAndSessionHardeningContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiReferentialIntegrityAndForeignKeyContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiRelationshipCardinalityContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiReportConsistencyAndAggregationExpansionTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiReportExportPrivacyAndAggregationContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiReportMathAndFinancialInvariantContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiRequestHeaderSpoofingContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiRoleWorkflowNegativeMatrixContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiRouteNamingVersioningAndHygieneTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiRouteParameterAbuseContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiRouteUseCaseCatalogCompletenessTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiSchemaKeyStabilityExpansionContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiSchoolCalendarAcademicBoundaryExpansionTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiSchoolOperationsRegressionBackstopTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiSearchIndexAndAutocompleteContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiSecurityAndTransportContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiSerializationSensitiveDataRedactionContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiSoftDeleteRestoreArchiveLifecycleContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiSortingFieldSelectionAndIncludeContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiTenantSchoolYearAndSemesterIsolationContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiTimezoneCalendarRecurrenceDepthContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiUnicodeNormalizationAndInputContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiUrlFetchSsrfAndCallbackSafetyContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiWebhookExternalProviderAndCallbackContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiWebhookReplayAndSignatureDepthContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/ApiWriteAuditAndTimestampContractTest.php create mode 100644 tests/Feature/Api/V1/FullSurface/Support/FullSurfaceE2EContractCase.php create mode 100644 tests/Feature/Api/V1/Workflows/ScenarioIPublicAuthAndSystemUseCaseTest.php create mode 100644 tests/Feature/Api/V1/Workflows/ScenarioJCrossModuleApiUseCaseSmokeTest.php create mode 100644 tests/Feature/Api/V1/Workflows/ScenarioKParentSelfServiceAndPrivacyTest.php create mode 100644 tests/Feature/Api/V1/Workflows/ScenarioLTeacherAcademicOperationsTest.php create mode 100644 tests/Feature/Api/V1/Workflows/ScenarioMAdministratorEnrollmentAndClassOperationsTest.php create mode 100644 tests/Feature/Api/V1/Workflows/ScenarioNCommunicationsAndSupportLifecycleTest.php create mode 100644 tests/Feature/Api/V1/Workflows/ScenarioOInventoryFinanceAndAuditControlsTest.php create mode 100644 tests/Feature/Api/V1/Workflows/ScenarioPSettingsConfigurationAndCalendarGovernanceTest.php create mode 100644 tests/Feature/Api/V1/Workflows/ScenarioQSchoolYearPromotionClosureLifecycleTest.php create mode 100644 tests/Feature/Api/V1/Workflows/ScenarioRAttendanceOperationsAndExceptionHandlingTest.php create mode 100644 tests/Feature/Api/V1/Workflows/ScenarioSScoringGradingAndReportCardLifecycleTest.php create mode 100644 tests/Feature/Api/V1/Workflows/ScenarioTDocumentsPrintBadgesAndCertificatesLifecycleTest.php create mode 100644 tests/Feature/Api/V1/Workflows/ScenarioUFinanceBillingPaymentRefundAndInstallmentLifecycleTest.php create mode 100644 tests/Feature/Api/V1/Workflows/ScenarioVProcurementReimbursementAndSupplyLifecycleTest.php create mode 100644 tests/Feature/Api/V1/Workflows/ScenarioWAccessControlRolesNavigationAndPreferencesTest.php create mode 100644 tests/Feature/Api/V1/Workflows/ScenarioXPublicKioskScannerAndVerificationLifecycleTest.php create mode 100644 tests/Feature/Api/V1/Workflows/ScenarioYFamilyGuardianAuthorizedUserAndWhatsappLifecycleTest.php create mode 100644 tests/Feature/Api/V1/Workflows/ScenarioZLegacyAliasesValidationAndCompatibilityTest.php diff --git a/app/Console/Commands/AttendanceAutoPublishCommand.php b/app/Console/Commands/AttendanceAutoPublishCommand.php index e775c110..fabeff3d 100644 --- a/app/Console/Commands/AttendanceAutoPublishCommand.php +++ b/app/Console/Commands/AttendanceAutoPublishCommand.php @@ -8,7 +8,6 @@ use Illuminate\Console\Command; class AttendanceAutoPublishCommand extends Command { protected $signature = 'attendance:auto-publish'; - protected $description = 'Auto-publish attendance days per "second Sunday backward" rule.'; public function handle(AttendanceAutoPublishJobService $service): int diff --git a/app/Console/Commands/CheckMissedPaymentsCommand.php b/app/Console/Commands/CheckMissedPaymentsCommand.php index ddd883a5..fff09126 100644 --- a/app/Console/Commands/CheckMissedPaymentsCommand.php +++ b/app/Console/Commands/CheckMissedPaymentsCommand.php @@ -8,7 +8,6 @@ use Illuminate\Console\Command; class CheckMissedPaymentsCommand extends Command { protected $signature = 'payments:check-missed'; - protected $description = 'Checks for users who missed payments and sends reminders.'; public function handle(PaymentMissedCheckService $service): int @@ -16,7 +15,6 @@ class CheckMissedPaymentsCommand extends Command $users = $service->findUsersWithMissedPayments(); if (empty($users)) { $this->info('No missed payments found.'); - return self::SUCCESS; } diff --git a/app/Console/Commands/CleanupExpiredNotificationsCommand.php b/app/Console/Commands/CleanupExpiredNotificationsCommand.php index 7e5a8237..94bcabee 100644 --- a/app/Console/Commands/CleanupExpiredNotificationsCommand.php +++ b/app/Console/Commands/CleanupExpiredNotificationsCommand.php @@ -8,7 +8,6 @@ use Illuminate\Console\Command; class CleanupExpiredNotificationsCommand extends Command { protected $signature = 'notifications:cleanup'; - protected $description = 'Deletes expired notifications from the database.'; public function handle(NotificationCleanupService $service): int @@ -17,7 +16,6 @@ class CleanupExpiredNotificationsCommand extends Command if ($count <= 0) { $this->info('No expired notifications found to soft delete.'); - return self::SUCCESS; } diff --git a/app/Console/Commands/CleanupPasswordResetsCommand.php b/app/Console/Commands/CleanupPasswordResetsCommand.php index 136b7845..16d1bdbd 100644 --- a/app/Console/Commands/CleanupPasswordResetsCommand.php +++ b/app/Console/Commands/CleanupPasswordResetsCommand.php @@ -8,7 +8,6 @@ use Illuminate\Console\Command; class CleanupPasswordResetsCommand extends Command { protected $signature = 'cleanup:password-resets {--days=30}'; - protected $description = 'Delete password reset requests older than N days.'; public function handle(PasswordResetCleanupService $service): int diff --git a/app/Console/Commands/ConfigUpdateCommand.php b/app/Console/Commands/ConfigUpdateCommand.php index 5a80c1d9..ac90ecbc 100644 --- a/app/Console/Commands/ConfigUpdateCommand.php +++ b/app/Console/Commands/ConfigUpdateCommand.php @@ -9,7 +9,6 @@ use Illuminate\Console\Command; class ConfigUpdateCommand extends Command { protected $signature = 'config:update {task?} {--task=} {--dry} {--force} {--tz=}'; - protected $description = 'Run a configuration update task (weekly cron).'; public function handle(ConfigUpdateService $service): int @@ -20,20 +19,18 @@ class ConfigUpdateCommand extends Command $tzName = (string) ($this->option('tz') ?: (config('School')->attendance['timezone'] ?? 'UTC')); $tz = new DateTimeZone($tzName); - if ($task === '' || ! in_array($task, $service->availableTasks(), true)) { - $this->error('Invalid or missing --task. Available: '.implode(', ', $service->availableTasks())); - + if ($task === '' || !in_array($task, $service->availableTasks(), true)) { + $this->error('Invalid or missing --task. Available: ' . implode(', ', $service->availableTasks())); return self::FAILURE; } $result = $service->runTask($task, $dry, $force, $tz); - if (! $result['ok']) { + if (!$result['ok']) { $this->error($result['message'] ?? "Task '{$task}' failed."); - return self::FAILURE; } - $this->info("Task '{$task}' finished successfully.".($dry ? ' [DRY RUN]' : '')); + $this->info("Task '{$task}' finished successfully." . ($dry ? ' [DRY RUN]' : '')); return self::SUCCESS; } diff --git a/app/Console/Commands/DeleteInactiveUsersCommand.php b/app/Console/Commands/DeleteInactiveUsersCommand.php index c2a0e199..baa3a450 100644 --- a/app/Console/Commands/DeleteInactiveUsersCommand.php +++ b/app/Console/Commands/DeleteInactiveUsersCommand.php @@ -8,7 +8,6 @@ use Illuminate\Console\Command; class DeleteInactiveUsersCommand extends Command { protected $signature = 'users:delete-inactive-users {--minutes=15}'; - protected $description = 'Delete users that are inactive and created more than N minutes ago.'; public function handle(InactiveUserCleanupService $service): int diff --git a/app/Console/Commands/DeleteUnverifiedUsersCommand.php b/app/Console/Commands/DeleteUnverifiedUsersCommand.php index 1742baf9..bb8a58da 100644 --- a/app/Console/Commands/DeleteUnverifiedUsersCommand.php +++ b/app/Console/Commands/DeleteUnverifiedUsersCommand.php @@ -10,7 +10,6 @@ use Illuminate\Support\Facades\Event; class DeleteUnverifiedUsersCommand extends Command { protected $signature = 'users:delete-unverified {--timeout=86400}'; - protected $description = 'Delete unverified users older than the configured timeout (seconds).'; public function handle(): int diff --git a/app/Console/Commands/RecalculateAttendanceCommand.php b/app/Console/Commands/RecalculateAttendanceCommand.php index 8ed1327a..f33ccec1 100644 --- a/app/Console/Commands/RecalculateAttendanceCommand.php +++ b/app/Console/Commands/RecalculateAttendanceCommand.php @@ -8,13 +8,12 @@ use Illuminate\Console\Command; class RecalculateAttendanceCommand extends Command { protected $signature = 'attendance:recalculate-summary'; - protected $description = 'Recalculates the attendance summary records from raw attendance data.'; public function handle(AttendanceSummaryRebuildService $service): int { $result = $service->rebuild(); - $this->info('Attendance summary recalculation finished. Inserted: '.$result['inserted']); + $this->info('Attendance summary recalculation finished. Inserted: ' . $result['inserted']); return self::SUCCESS; } diff --git a/app/Console/Commands/SendAbsenteesSummaryCommand.php b/app/Console/Commands/SendAbsenteesSummaryCommand.php index 60fc0d78..6f25f29d 100644 --- a/app/Console/Commands/SendAbsenteesSummaryCommand.php +++ b/app/Console/Commands/SendAbsenteesSummaryCommand.php @@ -8,7 +8,6 @@ use Illuminate\Console\Command; class SendAbsenteesSummaryCommand extends Command { protected $signature = 'attendance:absentees-summary'; - protected $description = 'Sends daily attendance summaries for absences to parents.'; public function handle(AttendanceDailySummaryService $service): int diff --git a/app/Console/Commands/SendLatesSummaryCommand.php b/app/Console/Commands/SendLatesSummaryCommand.php index c5cbd7ad..3489ccaf 100644 --- a/app/Console/Commands/SendLatesSummaryCommand.php +++ b/app/Console/Commands/SendLatesSummaryCommand.php @@ -8,7 +8,6 @@ use Illuminate\Console\Command; class SendLatesSummaryCommand extends Command { protected $signature = 'attendance:lates-summary'; - protected $description = 'Sends daily attendance summaries for lates to parents.'; public function handle(AttendanceDailySummaryService $service): int diff --git a/app/Console/Commands/SendMonthlyPaymentNotificationsCommand.php b/app/Console/Commands/SendMonthlyPaymentNotificationsCommand.php index 07a8ed25..a2ffccf1 100644 --- a/app/Console/Commands/SendMonthlyPaymentNotificationsCommand.php +++ b/app/Console/Commands/SendMonthlyPaymentNotificationsCommand.php @@ -9,7 +9,6 @@ use Illuminate\Console\Command; class SendMonthlyPaymentNotificationsCommand extends Command { protected $signature = 'payments:monthly-reminder {--force} {--email=} {--type=}'; - protected $description = 'Send monthly payment reminders on the first Saturday of every month.'; public function handle(PaymentNotificationService $service): int @@ -20,9 +19,8 @@ class SendMonthlyPaymentNotificationsCommand extends Command if ($email !== '') { $user = User::query()->where('email', $email)->first(); - if (! $user) { + if (!$user) { $this->error('Invalid --email provided.'); - return self::FAILURE; } @@ -32,14 +30,12 @@ class SendMonthlyPaymentNotificationsCommand extends Command 'force' => true, ]); - $this->info('Test reminder sent for '.$email.'.'); - + $this->info('Test reminder sent for ' . $email . '.'); return $result['failed'] > 0 ? self::FAILURE : self::SUCCESS; } - if (! $force && ! $this->isFirstSaturday(now())) { + if (!$force && !$this->isFirstSaturday(now())) { $this->info('Not the first Saturday of the month. Use --force to override.'); - return self::SUCCESS; } diff --git a/app/Console/Commands/SendTestPaymentNotificationCommand.php b/app/Console/Commands/SendTestPaymentNotificationCommand.php index 16873942..63002fbe 100644 --- a/app/Console/Commands/SendTestPaymentNotificationCommand.php +++ b/app/Console/Commands/SendTestPaymentNotificationCommand.php @@ -8,7 +8,6 @@ use Illuminate\Console\Command; class SendTestPaymentNotificationCommand extends Command { protected $signature = 'payments:send-test {--email=} {--type=no_payment}'; - protected $description = 'Send a single test non-payment or installment reminder to a specific email.'; public function handle(PaymentTestNotificationService $service): int @@ -16,20 +15,18 @@ class SendTestPaymentNotificationCommand extends Command $email = (string) $this->option('email'); $type = (string) $this->option('type'); - if ($email === '' || ! filter_var($email, FILTER_VALIDATE_EMAIL)) { + if ($email === '' || !filter_var($email, FILTER_VALIDATE_EMAIL)) { $this->error('Usage: php artisan payments:send-test --email=addr@example.com [--type=no_payment|installment]'); - return self::FAILURE; } $result = $service->send($email, $type); - if (! $result['ok']) { - $this->error('Failed to send test reminder to '.$email.'.'); - + if (!$result['ok']) { + $this->error('Failed to send test reminder to ' . $email . '.'); return self::FAILURE; } - $this->info('Sent test reminder to '.$email.'.'); + $this->info('Sent test reminder to ' . $email . '.'); return self::SUCCESS; } diff --git a/app/Console/Commands/SyncPaypalPaymentsCommand.php b/app/Console/Commands/SyncPaypalPaymentsCommand.php index f101ae11..7a5ba23f 100644 --- a/app/Console/Commands/SyncPaypalPaymentsCommand.php +++ b/app/Console/Commands/SyncPaypalPaymentsCommand.php @@ -8,7 +8,6 @@ use Illuminate\Console\Command; class SyncPaypalPaymentsCommand extends Command { protected $signature = 'payments:sync-paypal {--dry-run} {--report-only}'; - protected $description = 'Sync PayPal payments to internal payments table from paypal_payments.'; public function handle(PaypalPaymentSyncService $service): int @@ -19,7 +18,7 @@ class SyncPaypalPaymentsCommand extends Command $result = $service->sync($dryRun, $reportOnly); $this->info(sprintf('[%s] %d PayPal payments processed.', $result['mode'], $result['processed'])); - if (! empty($result['failed'])) { + if (!empty($result['failed'])) { $this->error(sprintf('[%s] Failed transactions: %s', $result['mode'], implode(', ', $result['failed']))); } diff --git a/app/Events/DeleteUnverifiedUser.php b/app/Events/DeleteUnverifiedUser.php index e033be77..b15e9c7c 100644 --- a/app/Events/DeleteUnverifiedUser.php +++ b/app/Events/DeleteUnverifiedUser.php @@ -4,5 +4,7 @@ namespace App\Events; class DeleteUnverifiedUser { - public function __construct(public int $userId) {} + public function __construct(public int $userId) + { + } } diff --git a/app/Events/WhatsappInvitesSend.php b/app/Events/WhatsappInvitesSend.php index 2a03b39f..af1cf788 100644 --- a/app/Events/WhatsappInvitesSend.php +++ b/app/Events/WhatsappInvitesSend.php @@ -10,5 +10,7 @@ class WhatsappInvitesSend use Dispatchable; use SerializesModels; - public function __construct(public array $payload) {} + public function __construct(public array $payload) + { + } } diff --git a/app/Helpers/app_helpers.php b/app/Helpers/app_helpers.php index 1a8e20b8..7cd96b32 100644 --- a/app/Helpers/app_helpers.php +++ b/app/Helpers/app_helpers.php @@ -3,34 +3,33 @@ use Carbon\CarbonImmutable; use Illuminate\Support\Facades\Hash; -if (! function_exists('utc_now')) { +if (!function_exists('utc_now')) { function utc_now(): string { return CarbonImmutable::now('UTC')->toDateTimeString(); } } -if (! function_exists('local_date')) { +if (!function_exists('local_date')) { function local_date(string $utcDateTime, string $format = 'Y-m-d H:i:s'): string { try { $carbon = CarbonImmutable::parse($utcDateTime)->setTimezone(config('app.timezone')); - return $carbon->format($format); - } catch (Throwable $e) { + } catch (\Throwable $e) { return CarbonImmutable::parse($utcDateTime)->format($format); } } } -if (! function_exists('user_timezone')) { +if (!function_exists('user_timezone')) { function user_timezone(): string { return (string) (config('School')->attendance['timezone'] ?? config('app.timezone', 'America/New_York')); } } -if (! function_exists('pbkdf2_hash')) { +if (!function_exists('pbkdf2_hash')) { function pbkdf2_hash(string $password, string $algo = 'sha256', int $iterations = 100000, int $length = 64): string { $salt = random_bytes(16); @@ -42,7 +41,7 @@ if (! function_exists('pbkdf2_hash')) { } } -if (! function_exists('pbkdf2_verify')) { +if (!function_exists('pbkdf2_verify')) { function pbkdf2_verify(string $password, string $storedHash): bool { $delim = strpos($storedHash, '$') !== false ? '$' : ':'; @@ -63,15 +62,14 @@ if (! function_exists('pbkdf2_verify')) { } $calc = hash_pbkdf2($algo, $password, $salt, (int) $iterations, strlen($derived), true); - return hash_equals($derived, $calc); } } -if (! function_exists('verify_stored_password')) { +if (!function_exists('verify_stored_password')) { function verify_stored_password(string $password, ?string $storedHash): bool { - if (! $storedHash) { + if (!$storedHash) { return false; } diff --git a/app/Http/Controllers/Api/Administrator/AdministratorAbsenceController.php b/app/Http/Controllers/Api/Administrator/AdministratorAbsenceController.php index 696f1ca8..498033ad 100644 --- a/app/Http/Controllers/Api/Administrator/AdministratorAbsenceController.php +++ b/app/Http/Controllers/Api/Administrator/AdministratorAbsenceController.php @@ -13,7 +13,8 @@ class AdministratorAbsenceController extends Controller { public function __construct( protected AdministratorAbsenceService $service - ) {} + ) { + } public function index(): JsonResponse { @@ -33,7 +34,7 @@ class AdministratorAbsenceController extends Controller } $payload = $request->all(); - if (! array_key_exists('dates', $payload) || ! array_key_exists('reason', $payload)) { + if (!array_key_exists('dates', $payload) || !array_key_exists('reason', $payload)) { $json = json_decode($request->getContent() ?: '', true); if (is_array($json)) { $payload = array_merge($payload, $json); diff --git a/app/Http/Controllers/Api/Administrator/AdministratorDashboardController.php b/app/Http/Controllers/Api/Administrator/AdministratorDashboardController.php index bb327ef1..67a82364 100644 --- a/app/Http/Controllers/Api/Administrator/AdministratorDashboardController.php +++ b/app/Http/Controllers/Api/Administrator/AdministratorDashboardController.php @@ -11,7 +11,8 @@ class AdministratorDashboardController extends Controller { public function __construct( protected AdministratorDashboardService $service - ) {} + ) { + } public function metrics(): JsonResponse { @@ -24,4 +25,4 @@ class AdministratorDashboardController extends Controller $this->service->userSearch((string) $request->query('query', '')) ); } -} +} \ No newline at end of file diff --git a/app/Http/Controllers/Api/Administrator/AdministratorEnrollmentController.php b/app/Http/Controllers/Api/Administrator/AdministratorEnrollmentController.php index 44e43c9d..2bc753e6 100644 --- a/app/Http/Controllers/Api/Administrator/AdministratorEnrollmentController.php +++ b/app/Http/Controllers/Api/Administrator/AdministratorEnrollmentController.php @@ -14,7 +14,8 @@ class AdministratorEnrollmentController extends Controller { public function __construct( protected AdministratorEnrollmentService $service - ) {} + ) { + } public function index(Request $request): JsonResponse { @@ -38,13 +39,13 @@ class AdministratorEnrollmentController extends Controller } $payload = $request->all(); - if (! array_key_exists('enrollment_status', $payload)) { + if (!array_key_exists('enrollment_status', $payload)) { $json = json_decode($request->getContent() ?: '', true); if (is_array($json) && array_key_exists('enrollment_status', $json)) { $payload['enrollment_status'] = $json['enrollment_status']; } } - if (! array_key_exists('enrollment_status', $payload)) { + if (!array_key_exists('enrollment_status', $payload)) { $payload['enrollment_status'] = $request->query('enrollment_status'); } diff --git a/app/Http/Controllers/Api/Administrator/AdministratorNotificationController.php b/app/Http/Controllers/Api/Administrator/AdministratorNotificationController.php index 67a0a5be..efc793ff 100644 --- a/app/Http/Controllers/Api/Administrator/AdministratorNotificationController.php +++ b/app/Http/Controllers/Api/Administrator/AdministratorNotificationController.php @@ -12,7 +12,8 @@ class AdministratorNotificationController extends Controller { public function __construct( protected AdministratorNotificationService $service - ) {} + ) { + } public function alerts(): JsonResponse { @@ -22,7 +23,7 @@ class AdministratorNotificationController extends Controller public function saveAlerts(Request $request): JsonResponse { $payload = $request->all(); - if (! array_key_exists('subjects', $payload)) { + if (!array_key_exists('subjects', $payload)) { $json = json_decode($request->getContent() ?: '', true); if (is_array($json)) { $payload = array_merge($payload, $json); @@ -59,7 +60,7 @@ class AdministratorNotificationController extends Controller public function savePrintRecipients(Request $request): JsonResponse { $payload = $request->all(); - if (! array_key_exists('notify', $payload)) { + if (!array_key_exists('notify', $payload)) { $json = json_decode($request->getContent() ?: '', true); if (is_array($json)) { $payload = array_merge($payload, $json); diff --git a/app/Http/Controllers/Api/Administrator/AdministratorPromotionController.php b/app/Http/Controllers/Api/Administrator/AdministratorPromotionController.php index 5631b479..320ae177 100644 --- a/app/Http/Controllers/Api/Administrator/AdministratorPromotionController.php +++ b/app/Http/Controllers/Api/Administrator/AdministratorPromotionController.php @@ -16,18 +16,18 @@ use App\Http\Resources\Promotions\PromotionReminderResource; use App\Http\Resources\Promotions\StudentPromotionResource; use App\Models\PromotionAuditLog; use App\Models\PromotionReminderLog; -use App\Models\SectionPlacementBatch; use App\Models\StudentPromotionRecord; use App\Services\Promotions\LevelProgressionService; -use App\Services\Promotions\Placement\SectionPlacementPreviewService; use App\Services\Promotions\PromotionAuditService; use App\Services\Promotions\PromotionEligibilityService; use App\Services\Promotions\PromotionEnrollmentService; use App\Services\Promotions\PromotionQueryService; use App\Services\Promotions\PromotionReminderService; use App\Services\Promotions\PromotionStatusService; -use Illuminate\Http\JsonResponse; +use App\Services\Promotions\Placement\SectionPlacementPreviewService; +use App\Models\SectionPlacementBatch; use Illuminate\Http\Request; +use Illuminate\Http\JsonResponse; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; use Symfony\Component\HttpFoundation\Response; @@ -54,7 +54,6 @@ class AdministratorPromotionController extends BaseApiController public function index(AdminListPromotionsRequest $request): JsonResponse { $result = $this->query->list($request->filters()); - return response()->json([ 'ok' => true, 'data' => StudentPromotionResource::collection($result['data']), @@ -74,7 +73,6 @@ class AdministratorPromotionController extends BaseApiController if ($record instanceof JsonResponse) { return $record; } - return response()->json([ 'ok' => true, 'data' => new StudentPromotionResource($this->query->detail($record)), @@ -84,7 +82,6 @@ class AdministratorPromotionController extends BaseApiController public function summary(AdminListPromotionsRequest $request): JsonResponse { $counts = $this->query->countsByStatus($request->filters()); - return response()->json([ 'ok' => true, 'counts_by_status' => $counts, @@ -128,7 +125,6 @@ class AdministratorPromotionController extends BaseApiController }; } catch (\Throwable $e) { Log::error('Promotion eligibility evaluation failed', ['exception' => $e]); - return response()->json([ 'ok' => false, 'message' => 'Failed to evaluate promotion eligibility.', @@ -141,6 +137,7 @@ class AdministratorPromotionController extends BaseApiController ]); } + public function createPlacementPreview(Request $request): JsonResponse { $payload = $request->validate([ @@ -160,7 +157,6 @@ class AdministratorPromotionController extends BaseApiController ); } catch (\Throwable $e) { Log::error('Section placement preview generation failed', ['exception' => $e]); - return response()->json([ 'ok' => false, 'message' => $e->getMessage(), @@ -176,7 +172,7 @@ class AdministratorPromotionController extends BaseApiController public function showPlacementBatch(int $batchId): JsonResponse { $batch = SectionPlacementBatch::query()->find($batchId); - if (! $batch) { + if (!$batch) { return response()->json(['ok' => false, 'message' => 'Placement batch not found.'], Response::HTTP_NOT_FOUND); } @@ -192,7 +188,6 @@ class AdministratorPromotionController extends BaseApiController $batch = $this->sectionPlacement->finalize($batchId, $this->getCurrentUserId()); } catch (\Throwable $e) { Log::error('Section placement finalization failed', ['batch_id' => $batchId, 'exception' => $e]); - return response()->json([ 'ok' => false, 'message' => $e->getMessage(), @@ -216,7 +211,7 @@ class AdministratorPromotionController extends BaseApiController $userId = $this->getCurrentUserId(); try { - $updated = ! empty($payload['force']) + $updated = !empty($payload['force']) ? $this->statusService->forceStatus($record, $payload['status'], $userId, $payload['notes'] ?? null) : $this->statusService->transition($record, $payload['status'], $userId, $payload['notes'] ?? null); } catch (\InvalidArgumentException $e) { @@ -249,7 +244,7 @@ class AdministratorPromotionController extends BaseApiController return $record; } $records = collect([$record]); - } elseif ($applyTo === 'filter' && ! empty($payload['promotion_ids'])) { + } elseif ($applyTo === 'filter' && !empty($payload['promotion_ids'])) { $records = StudentPromotionRecord::query() ->whereIn('promotion_id', $payload['promotion_ids']) ->get(); @@ -310,7 +305,6 @@ class AdministratorPromotionController extends BaseApiController 'promotion_id' => $promotionId, 'exception' => $e, ]); - return response()->json([ 'ok' => false, 'message' => 'Failed to send reminder.', @@ -327,7 +321,6 @@ class AdministratorPromotionController extends BaseApiController { $userId = $this->getCurrentUserId(); $result = $this->reminders->dispatchScheduledReminders(null, $userId); - return response()->json([ 'ok' => true, 'result' => $result, @@ -344,7 +337,6 @@ class AdministratorPromotionController extends BaseApiController ->forPromotion((int) $record->getKey()) ->orderByDesc('id') ->get(); - return response()->json([ 'ok' => true, 'reminders' => PromotionReminderResource::collection($rows), @@ -361,7 +353,6 @@ class AdministratorPromotionController extends BaseApiController ->forPromotion((int) $record->getKey()) ->orderByDesc('id') ->get(); - return response()->json([ 'ok' => true, 'entries' => PromotionAuditLogResource::collection($rows), @@ -371,7 +362,6 @@ class AdministratorPromotionController extends BaseApiController public function expireDeadlines(): JsonResponse { $result = $this->enrollmentService->markExpiredDeadlines(null, $this->getCurrentUserId()); - return response()->json([ 'ok' => true, 'result' => $result, @@ -414,7 +404,6 @@ class AdministratorPromotionController extends BaseApiController public function levelProgressions(): JsonResponse { $rows = $this->progression->list(false); - return response()->json([ 'ok' => true, 'levels' => LevelProgressionResource::collection($rows), @@ -424,7 +413,6 @@ class AdministratorPromotionController extends BaseApiController public function upsertLevelProgression(UpsertLevelProgressionRequest $request): JsonResponse { $row = $this->progression->upsertMapping($request->validated()); - return response()->json([ 'ok' => true, 'level' => new LevelProgressionResource($row), @@ -434,7 +422,6 @@ class AdministratorPromotionController extends BaseApiController public function seedLevelProgressions(): JsonResponse { $created = $this->progression->seedDefaults(); - return response()->json([ 'ok' => true, 'created' => $created, @@ -444,13 +431,12 @@ class AdministratorPromotionController extends BaseApiController private function findOrFail(int $promotionId): StudentPromotionRecord|JsonResponse { $record = StudentPromotionRecord::query()->find($promotionId); - if (! $record) { + if (!$record) { return response()->json([ 'ok' => false, 'message' => 'Promotion record not found.', ], Response::HTTP_NOT_FOUND); } - return $record; } } diff --git a/app/Http/Controllers/Api/Administrator/AdministratorTeacherSubmissionController.php b/app/Http/Controllers/Api/Administrator/AdministratorTeacherSubmissionController.php index 4a1e7ced..6960ac32 100644 --- a/app/Http/Controllers/Api/Administrator/AdministratorTeacherSubmissionController.php +++ b/app/Http/Controllers/Api/Administrator/AdministratorTeacherSubmissionController.php @@ -13,7 +13,8 @@ class AdministratorTeacherSubmissionController extends Controller { public function __construct( protected AdministratorTeacherSubmissionService $service - ) {} + ) { + } public function index(Request $request): JsonResponse { diff --git a/app/Http/Controllers/Api/Administrator/EmergencyContactController.php b/app/Http/Controllers/Api/Administrator/EmergencyContactController.php index 022989b3..5c685cf9 100644 --- a/app/Http/Controllers/Api/Administrator/EmergencyContactController.php +++ b/app/Http/Controllers/Api/Administrator/EmergencyContactController.php @@ -18,7 +18,8 @@ class EmergencyContactController extends Controller public function __construct( private EmergencyContactDirectoryService $directoryService, private EmergencyContactCrudService $crudService - ) {} + ) { + } public function index(Request $request): JsonResponse { @@ -37,10 +38,10 @@ class EmergencyContactController extends Controller $payload = $validator->validated(); $parentIds = []; - if (! empty($payload['parent_ids'])) { + if (!empty($payload['parent_ids'])) { $parentIds = array_values(array_unique(array_map('intval', $payload['parent_ids']))); } - if (! empty($payload['parent_id'])) { + if (!empty($payload['parent_id'])) { $parentIds[] = (int) $payload['parent_id']; } $parentIds = array_values(array_unique(array_filter($parentIds))); diff --git a/app/Http/Controllers/Api/Administrator/TeacherClassAssignmentController.php b/app/Http/Controllers/Api/Administrator/TeacherClassAssignmentController.php index 494cc994..7f841d28 100644 --- a/app/Http/Controllers/Api/Administrator/TeacherClassAssignmentController.php +++ b/app/Http/Controllers/Api/Administrator/TeacherClassAssignmentController.php @@ -61,6 +61,9 @@ class TeacherClassAssignmentController extends BaseApiController ], $status); } + /** + * @return int|JsonResponse + */ private function authenticatedUserIdOrUnauthorized(): int|JsonResponse { $userId = (int) (auth()->id() ?? 0); diff --git a/app/Http/Controllers/Api/Attendance/AdminAttendanceApiController.php b/app/Http/Controllers/Api/Attendance/AdminAttendanceApiController.php index 14bfd63b..9530777e 100644 --- a/app/Http/Controllers/Api/Attendance/AdminAttendanceApiController.php +++ b/app/Http/Controllers/Api/Attendance/AdminAttendanceApiController.php @@ -23,8 +23,8 @@ class AdminAttendanceApiController extends Controller { return new DailyAttendanceResource( $this->queryService->buildDailyAttendanceData( - (string) $request->query('semester', $this->attendanceService->currentSemester()), - (string) $request->query('school_year', $this->attendanceService->currentSchoolYear()) + (string)$request->query('semester', $this->attendanceService->currentSemester()), + (string)$request->query('school_year', $this->attendanceService->currentSchoolYear()) ) ); } @@ -60,4 +60,4 @@ class AdminAttendanceApiController extends Controller return response()->json(['message' => $e->getMessage()], 422); } } -} +} \ No newline at end of file diff --git a/app/Http/Controllers/Api/Attendance/AttendanceCommentTemplateController.php b/app/Http/Controllers/Api/Attendance/AttendanceCommentTemplateController.php index 33aa8ee2..6ae9a5ad 100644 --- a/app/Http/Controllers/Api/Attendance/AttendanceCommentTemplateController.php +++ b/app/Http/Controllers/Api/Attendance/AttendanceCommentTemplateController.php @@ -14,7 +14,8 @@ class AttendanceCommentTemplateController extends Controller public function __construct( protected AttendanceCommentTemplateService $service, protected ApplicationUrlService $urls, - ) {} + ) { + } public function bootstrapUrls(): JsonResponse { @@ -74,7 +75,7 @@ class AttendanceCommentTemplateController extends Controller } $data = $validator->validated(); - if (! array_key_exists('is_active', $data)) { + if (!array_key_exists('is_active', $data)) { $data['is_active'] = true; } diff --git a/app/Http/Controllers/Api/Attendance/EarlyDismissalsController.php b/app/Http/Controllers/Api/Attendance/EarlyDismissalsController.php index 4c9a6cba..fffae4f7 100644 --- a/app/Http/Controllers/Api/Attendance/EarlyDismissalsController.php +++ b/app/Http/Controllers/Api/Attendance/EarlyDismissalsController.php @@ -9,6 +9,7 @@ use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Validator; use Symfony\Component\HttpFoundation\Response; @@ -26,7 +27,7 @@ class EarlyDismissalsController extends BaseApiController public function index(Request $request): JsonResponse { $schoolYear = $request->query('school_year'); - $semester = $request->query('semester'); + $semester = $request->query('semester'); $query = DB::table('parent_attendance_reports as par') ->select([ @@ -61,15 +62,15 @@ class EarlyDismissalsController extends BaseApiController foreach ($rows as $row) { $date = $row->report_date ?? 'Unknown'; $groups[$date][] = [ - 'id' => $row->id, - 'firstname' => $row->firstname, - 'lastname' => $row->lastname, + 'id' => $row->id, + 'firstname' => $row->firstname, + 'lastname' => $row->lastname, 'class_section_name' => $row->class_section_name, - 'dismiss_time' => $row->dismiss_time, - 'reason' => $row->reason, - 'status' => $row->status, - 'school_year' => $row->school_year, - 'semester' => $row->semester, + 'dismiss_time' => $row->dismiss_time, + 'reason' => $row->reason, + 'status' => $row->status, + 'school_year' => $row->school_year, + 'semester' => $row->semester, ]; } @@ -81,13 +82,13 @@ class EarlyDismissalsController extends BaseApiController $signatureByDate = []; foreach ($signatures as $sig) { $signatureByDate[(string) $sig->report_date] = [ - 'filename' => $sig->filename, + 'filename' => $sig->filename, 'original_name' => $sig->original_name, ]; } return $this->success([ - 'groups' => $groups, + 'groups' => $groups, 'signatureByDate' => $signatureByDate, ]); } @@ -107,7 +108,7 @@ class EarlyDismissalsController extends BaseApiController ]) ->leftJoin('student_class as sc', function ($j) { $j->on('sc.student_id', '=', 's.id') - ->where('sc.school_year', '=', Configuration::getConfigValueByKey('school_year')); + ->where('sc.school_year', '=', Configuration::getConfigValueByKey('school_year')); }) ->leftJoin('classSection as cs', 'cs.class_section_id', '=', 'sc.class_section_id') ->where('s.is_active', 1) @@ -124,10 +125,10 @@ class EarlyDismissalsController extends BaseApiController public function store(Request $request): JsonResponse { $validator = Validator::make($request->all(), [ - 'student_id' => ['required', 'integer', 'min:1', 'exists:students,id'], - 'date' => ['required', 'date_format:Y-m-d'], + 'student_id' => ['required', 'integer', 'min:1', 'exists:students,id'], + 'date' => ['required', 'date_format:Y-m-d'], 'dismiss_time' => ['required', 'date_format:H:i'], - 'reason' => ['nullable', 'string', 'max:2000'], + 'reason' => ['nullable', 'string', 'max:2000'], ]); if ($validator->fails()) { @@ -137,7 +138,7 @@ class EarlyDismissalsController extends BaseApiController $data = $validator->validated(); $schoolYear = (string) Configuration::getConfigValueByKey('school_year'); - $semester = (string) Configuration::getConfigValueByKey('semester'); + $semester = (string) Configuration::getConfigValueByKey('semester'); $sc = DB::table('student_class') ->where('student_id', $data['student_id']) @@ -152,18 +153,18 @@ class EarlyDismissalsController extends BaseApiController ->value('parent_id') ?? Auth::id(); DB::table('parent_attendance_reports')->insert([ - 'parent_id' => $parentId ?? Auth::id(), - 'student_id' => $data['student_id'], + 'parent_id' => $parentId ?? Auth::id(), + 'student_id' => $data['student_id'], 'class_section_id' => $classSectionId, - 'report_date' => $data['date'], - 'type' => 'early_dismissal', - 'dismiss_time' => $data['dismiss_time'], - 'reason' => $data['reason'] ?? null, - 'semester' => $semester, - 'school_year' => $schoolYear, - 'status' => 'new', - 'created_at' => now(), - 'updated_at' => now(), + 'report_date' => $data['date'], + 'type' => 'early_dismissal', + 'dismiss_time' => $data['dismiss_time'], + 'reason' => $data['reason'] ?? null, + 'semester' => $semester, + 'school_year' => $schoolYear, + 'status' => 'new', + 'created_at' => now(), + 'updated_at' => now(), ]); return $this->success([], 'Early dismissal saved.', Response::HTTP_CREATED); @@ -176,9 +177,9 @@ class EarlyDismissalsController extends BaseApiController public function uploadSignature(Request $request): JsonResponse { $validator = Validator::make($request->all(), [ - 'report_date' => ['required', 'date_format:Y-m-d'], - 'school_year' => ['nullable', 'string', 'max:16'], - 'semester' => ['nullable', 'string', 'max:32'], + 'report_date' => ['required', 'date_format:Y-m-d'], + 'school_year' => ['nullable', 'string', 'max:16'], + 'semester' => ['nullable', 'string', 'max:32'], 'signature_file' => ['required', 'file', 'mimes:jpg,jpeg,png,webp,gif,pdf', 'max:5120'], ]); @@ -186,13 +187,13 @@ class EarlyDismissalsController extends BaseApiController return $this->respondValidationError($validator->errors()->toArray()); } - $file = $request->file('signature_file'); + $file = $request->file('signature_file'); $reportDate = $request->input('report_date'); $schoolYear = $request->input('school_year') ?: (string) Configuration::getConfigValueByKey('school_year'); - $semester = $request->input('semester') ?: (string) Configuration::getConfigValueByKey('semester'); + $semester = $request->input('semester') ?: (string) Configuration::getConfigValueByKey('semester'); - $dir = storage_path('uploads/early_dismissal_signatures'); - if (! is_dir($dir)) { + $dir = storage_path('uploads/early_dismissal_signatures'); + if (!is_dir($dir)) { mkdir($dir, 0755, true); } @@ -202,21 +203,21 @@ class EarlyDismissalsController extends BaseApiController ->where('semester', $semester) ->first(); - if ($existing?->filename && file_exists($dir.'/'.$existing->filename)) { - unlink($dir.'/'.$existing->filename); + if ($existing?->filename && file_exists($dir . '/' . $existing->filename)) { + unlink($dir . '/' . $existing->filename); } - $filename = uniqid('sig_', true).'.'.$file->getClientOriginalExtension(); + $filename = uniqid('sig_', true) . '.' . $file->getClientOriginalExtension(); $file->move($dir, $filename); EarlyDismissalSignature::updateOrCreate( ['report_date' => $reportDate, 'school_year' => $schoolYear, 'semester' => $semester], [ - 'filename' => $filename, + 'filename' => $filename, 'original_name' => $file->getClientOriginalName(), - 'mime_type' => $file->getClientMimeType(), - 'file_size' => $file->getSize(), - 'uploaded_by' => Auth::id(), + 'mime_type' => $file->getClientMimeType(), + 'file_size' => $file->getSize(), + 'uploaded_by' => Auth::id(), ] ); diff --git a/app/Http/Controllers/Api/Attendance/LateSlipLogsController.php b/app/Http/Controllers/Api/Attendance/LateSlipLogsController.php index 38237ebc..ba8d3b8e 100644 --- a/app/Http/Controllers/Api/Attendance/LateSlipLogsController.php +++ b/app/Http/Controllers/Api/Attendance/LateSlipLogsController.php @@ -42,7 +42,7 @@ class LateSlipLogsController extends BaseApiController public function show(int $id): JsonResponse { $log = $this->queryService->find($id); - if (! $log) { + if (!$log) { return $this->error('Late slip log not found.', Response::HTTP_NOT_FOUND); } @@ -56,7 +56,7 @@ class LateSlipLogsController extends BaseApiController public function destroy(int $id): JsonResponse { $log = $this->queryService->find($id); - if (! $log) { + if (!$log) { return $this->error('Late slip log not found.', Response::HTTP_NOT_FOUND); } @@ -65,12 +65,11 @@ class LateSlipLogsController extends BaseApiController try { $deleted = $this->commandService->delete($log); } catch (\Throwable $e) { - Log::error('Late slip log delete failed: '.$e->getMessage()); - + Log::error('Late slip log delete failed: ' . $e->getMessage()); return $this->error('Unable to delete late slip log.', Response::HTTP_INTERNAL_SERVER_ERROR); } - if (! $deleted) { + if (!$deleted) { return $this->error('Unable to delete late slip log.', Response::HTTP_INTERNAL_SERVER_ERROR); } diff --git a/app/Http/Controllers/Api/Attendance/StaffAttendanceApiController.php b/app/Http/Controllers/Api/Attendance/StaffAttendanceApiController.php index 4af30f2a..5fa4ae9f 100644 --- a/app/Http/Controllers/Api/Attendance/StaffAttendanceApiController.php +++ b/app/Http/Controllers/Api/Attendance/StaffAttendanceApiController.php @@ -23,8 +23,8 @@ class StaffAttendanceApiController extends Controller { return new StaffMonthResource( $this->staffAttendanceService->monthData( - (string) $request->query('semester'), - (string) $request->query('school_year') + (string)$request->query('semester'), + (string)$request->query('school_year') ) ); } @@ -33,9 +33,9 @@ class StaffAttendanceApiController extends Controller { return new AdminAttendanceResource( $this->staffAttendanceService->adminsAttendanceData( - (string) $request->query('date'), - (string) $request->query('semester'), - (string) $request->query('school_year') + (string)$request->query('date'), + (string)$request->query('semester'), + (string)$request->query('school_year') ) ); } @@ -75,9 +75,9 @@ class StaffAttendanceApiController extends Controller public function monthCsv(Request $request): StreamedResponse { return $this->staffAttendanceService->monthCsv( - (string) $request->query('semester'), - (string) $request->query('school_year'), - (string) $request->query('scope') + (string)$request->query('semester'), + (string)$request->query('school_year'), + (string)$request->query('scope') ); } -} +} \ No newline at end of file diff --git a/app/Http/Controllers/Api/Attendance/TeacherAttendanceApiController.php b/app/Http/Controllers/Api/Attendance/TeacherAttendanceApiController.php index fbefa6cd..84170381 100644 --- a/app/Http/Controllers/Api/Attendance/TeacherAttendanceApiController.php +++ b/app/Http/Controllers/Api/Attendance/TeacherAttendanceApiController.php @@ -21,10 +21,10 @@ class TeacherAttendanceApiController extends Controller public function grid(Request $request): TeacherGridResource { - $semester = (string) ($request->query('semester') ?: $this->attendanceService->currentSemester()); - $schoolYear = (string) ($request->query('school_year') ?: $this->attendanceService->currentSchoolYear()); - $date = (string) ($request->query('date') ?: now()->toDateString()); - $sectionCode = (int) $request->query('class_section_id', 0); + $semester = (string)($request->query('semester') ?: $this->attendanceService->currentSemester()); + $schoolYear = (string)($request->query('school_year') ?: $this->attendanceService->currentSchoolYear()); + $date = (string)($request->query('date') ?: now()->toDateString()); + $sectionCode = (int)$request->query('class_section_id', 0); return new TeacherGridResource( $this->queryService->teacherGrid($semester, $schoolYear, $date, $sectionCode) @@ -42,7 +42,7 @@ class TeacherAttendanceApiController extends Controller return new TeacherAttendanceFormResource( $this->queryService->teacherAttendanceFormData( $guard, - (int) $request->query('class_section_id', 0) + (int)$request->query('class_section_id', 0) ) ); } catch (Throwable $e) { @@ -66,6 +66,9 @@ class TeacherAttendanceApiController extends Controller } } + /** + * @return int|JsonResponse + */ private function authenticatedUserIdOrUnauthorized(): int|JsonResponse { $userId = (int) (auth()->id() ?? 0); @@ -75,4 +78,4 @@ class TeacherAttendanceApiController extends Controller return $userId; } -} +} \ No newline at end of file diff --git a/app/Http/Controllers/Api/AttendanceManagement/AttendanceManagementController.php b/app/Http/Controllers/Api/AttendanceManagement/AttendanceManagementController.php index ab97f748..906dfad8 100644 --- a/app/Http/Controllers/Api/AttendanceManagement/AttendanceManagementController.php +++ b/app/Http/Controllers/Api/AttendanceManagement/AttendanceManagementController.php @@ -41,9 +41,7 @@ class AttendanceManagementController extends BaseApiController 'reason' => ['nullable', 'string', 'max:255'], 'notes' => ['nullable', 'string'], ]); - if ($validator->fails()) { - return $this->respondValidationError($validator->errors()->toArray()); - } + if ($validator->fails()) return $this->respondValidationError($validator->errors()->toArray()); try { return $this->success(['event' => $this->service->manualEntry($validator->validated(), $request->user())], 'Manual attendance entry recorded.', Response::HTTP_CREATED); } catch (\InvalidArgumentException $e) { @@ -65,9 +63,7 @@ class AttendanceManagementController extends BaseApiController 'authorized' => ['nullable'], 'reason' => ['nullable', 'string', 'max:255'], ]); - if ($validator->fails()) { - return $this->respondValidationError($validator->errors()->toArray()); - } + if ($validator->fails()) return $this->respondValidationError($validator->errors()->toArray()); try { return $this->success(['event' => $this->service->badgeScan($validator->validated(), $request->user())], 'Badge scan classified.'); } catch (\InvalidArgumentException $e) { @@ -95,9 +91,7 @@ class AttendanceManagementController extends BaseApiController 'reason' => ['nullable', 'string', 'max:255'], 'notes' => ['nullable', 'string'], ]); - if ($validator->fails()) { - return $this->respondValidationError($validator->errors()->toArray()); - } + if ($validator->fails()) return $this->respondValidationError($validator->errors()->toArray()); try { return $this->success(['event' => $this->service->exitEntry($validator->validated(), $request->user())], 'Exit attendance entry recorded.', Response::HTTP_CREATED); } catch (\InvalidArgumentException $e) { @@ -119,9 +113,7 @@ class AttendanceManagementController extends BaseApiController 'report_status' => ['nullable', 'string', 'max:48'], 'reason' => ['nullable', 'string', 'max:255'], ]); - if ($validator->fails()) { - return $this->respondValidationError($validator->errors()->toArray()); - } + if ($validator->fails()) return $this->respondValidationError($validator->errors()->toArray()); try { return $this->success(['event' => $this->service->markAbsent($validator->validated(), $request->user())], 'Absence recorded.', Response::HTTP_CREATED); } catch (\InvalidArgumentException $e) { @@ -138,9 +130,7 @@ class AttendanceManagementController extends BaseApiController 'final_decision' => ['nullable', 'string', 'max:255'], 'notes' => ['nullable', 'string'], ]); - if ($validator->fails()) { - return $this->respondValidationError($validator->errors()->toArray()); - } + if ($validator->fails()) return $this->respondValidationError($validator->errors()->toArray()); try { return $this->success(['event' => $this->service->completeFollowUp($id, $validator->validated(), $request->user())], 'Follow-up completed.'); } catch (\RuntimeException $e) { @@ -157,9 +147,7 @@ class AttendanceManagementController extends BaseApiController 'slip_number' => ['nullable', 'string', 'max:64'], 'late_slip_log_id' => ['nullable', 'integer'], ]); - if ($validator->fails()) { - return $this->respondValidationError($validator->errors()->toArray()); - } + if ($validator->fails()) return $this->respondValidationError($validator->errors()->toArray()); try { return $this->success(['reprint' => $this->service->reprintLateSlip($id, $validator->validated(), $request->user())], 'Late slip reprint logged.', Response::HTTP_CREATED); } catch (\RuntimeException $e) { diff --git a/app/Http/Controllers/Api/AttendanceTracking/AttendanceTrackingController.php b/app/Http/Controllers/Api/AttendanceTracking/AttendanceTrackingController.php index 503524f5..6d53db9f 100644 --- a/app/Http/Controllers/Api/AttendanceTracking/AttendanceTrackingController.php +++ b/app/Http/Controllers/Api/AttendanceTracking/AttendanceTrackingController.php @@ -12,7 +12,8 @@ class AttendanceTrackingController extends Controller { public function __construct( protected AttendanceTrackingService $service - ) {} + ) { + } public function pendingViolations(Request $request): JsonResponse { diff --git a/app/Http/Controllers/Api/Auth/AuthController.php b/app/Http/Controllers/Api/Auth/AuthController.php index 91ffbbb4..b7eb842d 100644 --- a/app/Http/Controllers/Api/Auth/AuthController.php +++ b/app/Http/Controllers/Api/Auth/AuthController.php @@ -39,7 +39,7 @@ class AuthController extends BaseApiController } $user = User::query()->whereRaw('LOWER(email) = ?', [$email])->first(); - if (! $user) { + if (!$user) { $security->logIpAttempt((string) $ip); return response()->json([ @@ -70,7 +70,7 @@ class AuthController extends BaseApiController $security->logSuccessfulLogin($user->fresh(), $request); $fresh = $user->fresh(); - if (! $fresh) { + if (!$fresh) { return response()->json([ 'status' => false, 'message' => 'Invalid email or password.', @@ -100,7 +100,7 @@ class AuthController extends BaseApiController public function me(): JsonResponse { $user = Auth::user(); - if (! $user) { + if (!$user) { return $this->respondError('Unauthorized.', 401); } diff --git a/app/Http/Controllers/Api/Auth/IpBanController.php b/app/Http/Controllers/Api/Auth/IpBanController.php index 4eaf314c..3f45f8c8 100644 --- a/app/Http/Controllers/Api/Auth/IpBanController.php +++ b/app/Http/Controllers/Api/Auth/IpBanController.php @@ -44,7 +44,7 @@ class IpBanController extends BaseApiController public function show(int $id): JsonResponse { $ban = $this->queryService->find($id); - if (! $ban) { + if (!$ban) { return $this->error('IP record not found.', Response::HTTP_NOT_FOUND); } @@ -67,12 +67,11 @@ class IpBanController extends BaseApiController try { $ban = $this->commandService->banNow($id ? (int) $id : null, $ip ? (string) $ip : null, $hours); } catch (\Throwable $e) { - Log::error('IP ban failed: '.$e->getMessage()); - + Log::error('IP ban failed: ' . $e->getMessage()); return $this->error('Unable to ban IP.', Response::HTTP_INTERNAL_SERVER_ERROR); } - if (! $ban) { + if (!$ban) { return $this->error('IP record not found.', Response::HTTP_NOT_FOUND); } @@ -91,10 +90,9 @@ class IpBanController extends BaseApiController try { if ($all) { $count = $this->commandService->unbanAll(); - return $this->success([ 'count' => $count, - ], $count.' IP(s) unbanned.'); + ], $count . ' IP(s) unbanned.'); } $ban = $this->commandService->unbanOne( @@ -102,12 +100,11 @@ class IpBanController extends BaseApiController $validated['ip'] ?? null ); } catch (\Throwable $e) { - Log::error('IP unban failed: '.$e->getMessage()); - + Log::error('IP unban failed: ' . $e->getMessage()); return $this->error('Unable to unban IP.', Response::HTTP_INTERNAL_SERVER_ERROR); } - if (! $ban) { + if (!$ban) { return $this->error('IP record not found.', Response::HTTP_NOT_FOUND); } diff --git a/app/Http/Controllers/Api/Auth/RegisterController.php b/app/Http/Controllers/Api/Auth/RegisterController.php index 5e1cafef..0de4fd44 100644 --- a/app/Http/Controllers/Api/Auth/RegisterController.php +++ b/app/Http/Controllers/Api/Auth/RegisterController.php @@ -3,7 +3,6 @@ namespace App\Http\Controllers\Api\Auth; use App\Http\Controllers\Api\Core\BaseApiController; -use App\Http\Requests\Auth\RegisterRequest; use App\Http\Resources\Auth\RegisterResource; use App\Services\Auth\ApiRegistrationService; use App\Services\Auth\RegistrationCaptchaService; @@ -51,7 +50,7 @@ class RegisterController extends BaseApiController ]); } - $validator = Validator::make($request->all(), RegisterRequest::ruleset()); + $validator = Validator::make($request->all(), \App\Http\Requests\Auth\RegisterRequest::ruleset()); if ($validator->fails()) { return response()->json([ 'message' => 'Validation failed.', diff --git a/app/Http/Controllers/Api/Auth/RolePermissionController.php b/app/Http/Controllers/Api/Auth/RolePermissionController.php index e4e15825..3395caac 100644 --- a/app/Http/Controllers/Api/Auth/RolePermissionController.php +++ b/app/Http/Controllers/Api/Auth/RolePermissionController.php @@ -56,7 +56,7 @@ class RolePermissionController extends BaseApiController public function showRole(int $roleId): JsonResponse { $role = Role::query()->find($roleId); - if (! $role) { + if (!$role) { return $this->error('Role not found.', Response::HTTP_NOT_FOUND); } @@ -70,8 +70,7 @@ class RolePermissionController extends BaseApiController try { $role = $this->roleCrudService->create($request->validated()); } catch (\Throwable $e) { - Log::error('Role store failed: '.$e->getMessage()); - + Log::error('Role store failed: ' . $e->getMessage()); return $this->error('Failed to create role.', Response::HTTP_INTERNAL_SERVER_ERROR); } @@ -83,15 +82,14 @@ class RolePermissionController extends BaseApiController public function updateRole(RoleUpdateRequest $request, int $roleId): JsonResponse { $role = Role::query()->find($roleId); - if (! $role) { + if (!$role) { return $this->error('Role not found.', Response::HTTP_NOT_FOUND); } try { $role = $this->roleCrudService->update($role, $request->validated()); } catch (\Throwable $e) { - Log::error('Role update failed: '.$e->getMessage()); - + Log::error('Role update failed: ' . $e->getMessage()); return $this->error('Failed to update role.', Response::HTTP_INTERNAL_SERVER_ERROR); } @@ -103,15 +101,14 @@ class RolePermissionController extends BaseApiController public function deleteRole(int $roleId): JsonResponse { $role = Role::query()->find($roleId); - if (! $role) { + if (!$role) { return $this->error('Role not found.', Response::HTTP_NOT_FOUND); } try { $this->roleCrudService->delete($role); } catch (\Throwable $e) { - Log::error('Role delete failed: '.$e->getMessage()); - + Log::error('Role delete failed: ' . $e->getMessage()); return $this->error('Failed to delete role.', Response::HTTP_INTERNAL_SERVER_ERROR); } @@ -147,12 +144,11 @@ class RolePermissionController extends BaseApiController $actorId ); } catch (\Throwable $e) { - Log::error('Assign roles failed: '.$e->getMessage()); - + Log::error('Assign roles failed: ' . $e->getMessage()); return $this->error('Failed to update roles.', Response::HTTP_INTERNAL_SERVER_ERROR); } - if (! ($result['ok'] ?? false)) { + if (!($result['ok'] ?? false)) { return $this->error($result['message'] ?? 'Failed to update roles.', Response::HTTP_UNPROCESSABLE_ENTITY); } @@ -173,7 +169,7 @@ class RolePermissionController extends BaseApiController public function showPermission(int $permissionId): JsonResponse { $permission = Permission::query()->find($permissionId); - if (! $permission) { + if (!$permission) { return $this->error('Permission not found.', Response::HTTP_NOT_FOUND); } @@ -187,8 +183,7 @@ class RolePermissionController extends BaseApiController try { $permission = $this->permissionCrudService->create($request->validated()); } catch (\Throwable $e) { - Log::error('Permission store failed: '.$e->getMessage()); - + Log::error('Permission store failed: ' . $e->getMessage()); return $this->error('Failed to create permission.', Response::HTTP_INTERNAL_SERVER_ERROR); } @@ -200,15 +195,14 @@ class RolePermissionController extends BaseApiController public function updatePermission(PermissionUpdateRequest $request, int $permissionId): JsonResponse { $permission = Permission::query()->find($permissionId); - if (! $permission) { + if (!$permission) { return $this->error('Permission not found.', Response::HTTP_NOT_FOUND); } try { $permission = $this->permissionCrudService->update($permission, $request->validated()); } catch (\Throwable $e) { - Log::error('Permission update failed: '.$e->getMessage()); - + Log::error('Permission update failed: ' . $e->getMessage()); return $this->error('Failed to update permission.', Response::HTTP_INTERNAL_SERVER_ERROR); } @@ -220,15 +214,14 @@ class RolePermissionController extends BaseApiController public function deletePermission(int $permissionId): JsonResponse { $permission = Permission::query()->find($permissionId); - if (! $permission) { + if (!$permission) { return $this->error('Permission not found.', Response::HTTP_NOT_FOUND); } try { $this->permissionCrudService->delete($permission); } catch (\Throwable $e) { - Log::error('Permission delete failed: '.$e->getMessage()); - + Log::error('Permission delete failed: ' . $e->getMessage()); return $this->error('Failed to delete permission.', Response::HTTP_INTERNAL_SERVER_ERROR); } @@ -249,14 +242,16 @@ class RolePermissionController extends BaseApiController try { $this->permissionService->saveRolePermissions($roleId, $request->validated()['permissions'] ?? []); } catch (\Throwable $e) { - Log::error('Role permissions update failed: '.$e->getMessage()); - + Log::error('Role permissions update failed: ' . $e->getMessage()); return $this->error('Failed to update role permissions.', Response::HTTP_INTERNAL_SERVER_ERROR); } return $this->success(null, 'Permissions saved successfully.'); } + /** + * @return int|JsonResponse + */ private function authenticatedUserIdOrUnauthorized(): int|JsonResponse { $userId = (int) (auth()->id() ?? 0); diff --git a/app/Http/Controllers/Api/Auth/RoleSwitcherController.php b/app/Http/Controllers/Api/Auth/RoleSwitcherController.php index 569cbf1d..e5427d11 100644 --- a/app/Http/Controllers/Api/Auth/RoleSwitcherController.php +++ b/app/Http/Controllers/Api/Auth/RoleSwitcherController.php @@ -6,9 +6,9 @@ use App\Http\Controllers\Api\Core\BaseApiController; use App\Http\Requests\Roles\RoleSwitchRequest; use App\Services\Roles\RoleSwitchService; use Illuminate\Http\JsonResponse; -use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +use Symfony\Component\HttpFoundation\Response; class RoleSwitcherController extends BaseApiController { @@ -56,6 +56,9 @@ class RoleSwitcherController extends BaseApiController ], 'Role switched successfully.'); } + /** + * @return int|JsonResponse + */ private function authenticatedUserIdOrUnauthorized(): int|JsonResponse { $userId = (int) (auth()->id() ?? 0); diff --git a/app/Http/Controllers/Api/Auth/SessionTimeoutController.php b/app/Http/Controllers/Api/Auth/SessionTimeoutController.php index b29f19e9..6cb3a380 100644 --- a/app/Http/Controllers/Api/Auth/SessionTimeoutController.php +++ b/app/Http/Controllers/Api/Auth/SessionTimeoutController.php @@ -19,7 +19,8 @@ class SessionTimeoutController extends Controller public function __construct( private ApplicationUrlService $urls, - ) {} + ) { + } /** * Returns timeout metadata for the web portal. diff --git a/app/Http/Controllers/Api/Badges/BadgeController.php b/app/Http/Controllers/Api/Badges/BadgeController.php index b9777caa..9a2d1345 100644 --- a/app/Http/Controllers/Api/Badges/BadgeController.php +++ b/app/Http/Controllers/Api/Badges/BadgeController.php @@ -17,7 +17,8 @@ class BadgeController extends Controller protected BadgePdfService $badgePdfService, protected BadgePrintLogService $badgePrintLogService, protected BadgeFormDataService $badgeFormDataService - ) {} + ) { + } public function generatePdf(Request $request) { @@ -165,7 +166,7 @@ class BadgeController extends Controller if (is_string($ids)) { $ids = array_filter(array_map('trim', explode(',', $ids)), 'strlen'); - } elseif (! is_array($ids)) { + } elseif (!is_array($ids)) { $ids = $ids ? [$ids] : []; } @@ -198,7 +199,7 @@ class BadgeController extends Controller if (is_string($ids)) { $ids = array_filter(array_map('trim', explode(',', $ids)), 'strlen'); - } elseif (! is_array($ids)) { + } elseif (!is_array($ids)) { $ids = $ids ? [$ids] : []; } @@ -207,6 +208,9 @@ class BadgeController extends Controller return array_values(array_filter($ids, static fn ($v) => $v > 0)); } + /** + * @return int|JsonResponse + */ private function authenticatedUserIdOrUnauthorized(): int|JsonResponse { $userId = (int) (auth()->id() ?? 0); diff --git a/app/Http/Controllers/Api/BaseApiController.php b/app/Http/Controllers/Api/BaseApiController.php index 2134d855..00e32be0 100644 --- a/app/Http/Controllers/Api/BaseApiController.php +++ b/app/Http/Controllers/Api/BaseApiController.php @@ -4,4 +4,6 @@ namespace App\Http\Controllers\Api; use App\Http\Controllers\Api\Core\BaseApiController as CoreBaseApiController; -class BaseApiController extends CoreBaseApiController {} +class BaseApiController extends CoreBaseApiController +{ +} diff --git a/app/Http/Controllers/Api/ClassPreparation/ClassPreparationController.php b/app/Http/Controllers/Api/ClassPreparation/ClassPreparationController.php index 42bbbc45..86732288 100644 --- a/app/Http/Controllers/Api/ClassPreparation/ClassPreparationController.php +++ b/app/Http/Controllers/Api/ClassPreparation/ClassPreparationController.php @@ -20,16 +20,15 @@ use Symfony\Component\HttpFoundation\Response; class ClassPreparationController extends BaseApiController { private ClassPreparationService $service; - private StickerCountService $stickerCounts; - private ClassRosterService $roster; public function __construct( ClassPreparationService $service, StickerCountService $stickerCounts, ClassRosterService $roster - ) { + ) + { parent::__construct(); $this->service = $service; $this->stickerCounts = $stickerCounts; @@ -78,8 +77,7 @@ class ClassPreparationController extends BaseApiController try { $count = $this->service->saveAdjustments($classSectionId, $schoolYear, $validated['adjustments']); } catch (\Throwable $e) { - Log::error('Class prep adjustment save failed: '.$e->getMessage()); - + Log::error('Class prep adjustment save failed: ' . $e->getMessage()); return $this->error('Unable to save adjustments.', Response::HTTP_INTERNAL_SERVER_ERROR); } diff --git a/app/Http/Controllers/Api/ClassProgress/ClassProgressController.php b/app/Http/Controllers/Api/ClassProgress/ClassProgressController.php index cda25c37..45ddf489 100644 --- a/app/Http/Controllers/Api/ClassProgress/ClassProgressController.php +++ b/app/Http/Controllers/Api/ClassProgress/ClassProgressController.php @@ -94,9 +94,8 @@ class ClassProgressController extends BaseApiController $filters['semester'] = $filters['semester'] ?? ($meta['semester'] ?? null); $paginator = $this->queryService->listReports($auth, $filters); - if (! empty($filters['group_by_week'])) { + if (!empty($filters['group_by_week'])) { $grouped = ClassProgressGroupCollection::fromPaginator($paginator); - return $this->respondSuccess($grouped, 'Progress reports retrieved.'); } @@ -136,7 +135,6 @@ class ClassProgressController extends BaseApiController Log::error('Failed to create class progress reports.', [ 'error' => $e->getMessage(), ]); - return $this->respondError('Unable to save progress reports.', 500); } @@ -172,7 +170,6 @@ class ClassProgressController extends BaseApiController Log::error('Failed to update class progress report.', [ 'error' => $e->getMessage(), ]); - return $this->respondError('Unable to update progress report.', 500); } @@ -187,7 +184,6 @@ class ClassProgressController extends BaseApiController Log::error('Failed to delete class progress report.', [ 'error' => $e->getMessage(), ]); - return $this->respondError('Unable to delete progress report.', 500); } @@ -197,7 +193,7 @@ class ClassProgressController extends BaseApiController public function downloadAttachment(int $attachment): BinaryFileResponse|JsonResponse { $record = ClassProgressAttachment::query()->find($attachment); - if (! $record || ! $record->file_path) { + if (!$record || !$record->file_path) { return $this->respondError('Attachment not found.', 404); } @@ -206,12 +202,11 @@ class ClassProgressController extends BaseApiController } $path = $this->attachmentService->resolvePath($record->file_path); - if (! $path) { + if (!$path) { return $this->respondError('Attachment missing.', 404); } $downloadName = $record->original_name ?: basename($path); - return response()->download($path, $downloadName); } @@ -219,12 +214,12 @@ class ClassProgressController extends BaseApiController { $this->authorize('view', $class_progress); - if (! $class_progress->attachment_path) { + if (!$class_progress->attachment_path) { return $this->respondError('Attachment not found.', 404); } $path = $this->attachmentService->resolvePath($class_progress->attachment_path); - if (! $path) { + if (!$path) { return $this->respondError('Attachment missing.', 404); } @@ -233,18 +228,19 @@ class ClassProgressController extends BaseApiController /** * @param mixed $user + * @return User|JsonResponse */ private function requireAuthenticatedUser($user): User|JsonResponse { - if (! $user instanceof User) { + if (!$user instanceof User) { $user = auth()->user(); } - if (! $user instanceof User) { + if (!$user instanceof User) { $user = $this->laravelRequest->user(); } - if (! $user instanceof User) { + if (!$user instanceof User) { return $this->respondError('Unauthorized.', Response::HTTP_UNAUTHORIZED); } diff --git a/app/Http/Controllers/Api/Classes/ClassController.php b/app/Http/Controllers/Api/Classes/ClassController.php index 1a5f86c0..09991bc2 100644 --- a/app/Http/Controllers/Api/Classes/ClassController.php +++ b/app/Http/Controllers/Api/Classes/ClassController.php @@ -53,7 +53,7 @@ class ClassController extends BaseApiController $section = $this->queryService->find((int) $classSection->id); - if (! $section) { + if (!$section) { return $this->error('Class section not found.', Response::HTTP_NOT_FOUND); } @@ -88,7 +88,7 @@ class ClassController extends BaseApiController { $this->authorize('delete', $classSection); - if (! $this->commandService->delete($classSection)) { + if (!$this->commandService->delete($classSection)) { return $this->error('Class section not found.', Response::HTTP_NOT_FOUND); } @@ -125,8 +125,7 @@ class ClassController extends BaseApiController try { $created = $this->seedService->seedDefaults(); } catch (\Throwable $e) { - Log::error('Seeding default classes failed: '.$e->getMessage()); - + Log::error('Seeding default classes failed: ' . $e->getMessage()); return $this->error('Unable to seed default classes.', Response::HTTP_INTERNAL_SERVER_ERROR); } diff --git a/app/Http/Controllers/Api/Communication/CommunicationController.php b/app/Http/Controllers/Api/Communication/CommunicationController.php index b8281e11..e55655bc 100644 --- a/app/Http/Controllers/Api/Communication/CommunicationController.php +++ b/app/Http/Controllers/Api/Communication/CommunicationController.php @@ -14,13 +14,9 @@ use Illuminate\Http\Request; class CommunicationController extends BaseApiController { private CommunicationStudentService $students; - private CommunicationTemplateService $templates; - private CommunicationFamilyService $families; - private CommunicationPreviewService $previewService; - private CommunicationSendService $sendService; public function __construct( @@ -80,7 +76,7 @@ class CommunicationController extends BaseApiController $teacherName = $this->resolveTeacherName(); $result = $this->previewService->buildPreview($templateKey, $studentId, $familyId, $vars, $teacherName); - if (! $result['ok']) { + if (!$result['ok']) { return response()->json([ 'ok' => false, 'message' => $result['message'] ?? 'Preview failed.', @@ -120,7 +116,7 @@ class CommunicationController extends BaseApiController } $student = $this->students->getStudentBasic($studentId); - if (! $student) { + if (!$student) { return response()->json([ 'ok' => false, 'message' => 'Student not found.', @@ -141,11 +137,11 @@ class CommunicationController extends BaseApiController 'recipients' => $recipients, 'cc' => $cc, 'bcc' => $bcc, - 'student_name' => trim(($student['firstname'] ?? '').' '.($student['lastname'] ?? '')), + 'student_name' => trim(($student['firstname'] ?? '') . ' ' . ($student['lastname'] ?? '')), 'sent_by' => $senderId, ]); - if (! $result['ok']) { + if (!$result['ok']) { return response()->json([ 'ok' => false, 'message' => 'Failed to send email.', @@ -179,14 +175,15 @@ class CommunicationController extends BaseApiController { $user = auth()->user(); if ($user) { - $name = trim(($user->firstname ?? '').' '.($user->lastname ?? '')); - + $name = trim(($user->firstname ?? '') . ' ' . ($user->lastname ?? '')); return $name !== '' ? $name : 'Teacher'; } - return 'Teacher'; } + /** + * @return int|JsonResponse + */ private function authenticatedUserIdOrUnauthorized(): int|JsonResponse { $userId = (int) (auth()->id() ?? 0); diff --git a/app/Http/Controllers/Api/CompetitionScores/CompetitionScoresController.php b/app/Http/Controllers/Api/CompetitionScores/CompetitionScoresController.php index a1feb69b..09dc3cfa 100644 --- a/app/Http/Controllers/Api/CompetitionScores/CompetitionScoresController.php +++ b/app/Http/Controllers/Api/CompetitionScores/CompetitionScoresController.php @@ -14,11 +14,8 @@ use Illuminate\Http\Request; class CompetitionScoresController extends BaseApiController { private CompetitionScoresContextService $contextService; - private CompetitionScoresQueryService $queryService; - private CompetitionScoresRosterService $rosterService; - private CompetitionScoresSaveService $saveService; public function __construct( @@ -103,7 +100,7 @@ class CompetitionScoresController extends BaseApiController } $competition = Competition::query()->find($id); - if (! $competition) { + if (!$competition) { return response()->json([ 'ok' => false, 'message' => 'Competition not found.', @@ -123,7 +120,7 @@ class CompetitionScoresController extends BaseApiController ], 422); } - if (! in_array($classSectionId, $allowedClassIds, true)) { + if (!in_array($classSectionId, $allowedClassIds, true)) { return response()->json([ 'ok' => false, 'message' => 'You are not assigned to that class.', @@ -173,7 +170,7 @@ class CompetitionScoresController extends BaseApiController } $competition = Competition::query()->find($id); - if (! $competition) { + if (!$competition) { return response()->json([ 'ok' => false, 'message' => 'Competition not found.', @@ -197,7 +194,7 @@ class CompetitionScoresController extends BaseApiController ], 422); } - if (! in_array($classSectionId, $allowedClassIds, true)) { + if (!in_array($classSectionId, $allowedClassIds, true)) { return response()->json([ 'ok' => false, 'message' => 'You are not assigned to that class.', @@ -208,7 +205,7 @@ class CompetitionScoresController extends BaseApiController $scores = is_array($scores) ? $scores : []; [$cleanScores, $invalidScores] = $this->saveService->filterScores($scores); - if (! empty($invalidScores)) { + if (!empty($invalidScores)) { return response()->json([ 'ok' => false, 'message' => 'Scores must be whole numbers (no decimals).', @@ -225,6 +222,9 @@ class CompetitionScoresController extends BaseApiController ]); } + /** + * @return int|JsonResponse + */ private function authenticatedUserIdOrUnauthorized(): int|JsonResponse { $userId = (int) (auth()->id() ?? 0); diff --git a/app/Http/Controllers/Api/Core/BaseApiController.php b/app/Http/Controllers/Api/Core/BaseApiController.php index 6cb46bb2..3b05ff3c 100644 --- a/app/Http/Controllers/Api/Core/BaseApiController.php +++ b/app/Http/Controllers/Api/Core/BaseApiController.php @@ -6,7 +6,6 @@ use App\Http\Controllers\Controller; use App\Models\User; use Illuminate\Contracts\Validation\Validator as ValidatorContract; use Illuminate\Database\Eloquent\Builder as EloquentBuilder; -use Illuminate\Database\Query\Builder; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; @@ -17,8 +16,7 @@ use Symfony\Component\HttpFoundation\Response; class BaseApiController extends Controller { protected Request $laravelRequest; - - protected ?ValidatorContract $validator = null; + protected ValidatorContract|null $validator = null; public function __construct() { @@ -28,19 +26,19 @@ class BaseApiController extends Controller protected function success($data = null, string $message = 'Success', int $code = Response::HTTP_OK): JsonResponse { return response()->json([ - 'status' => true, + 'status' => true, 'message' => $message, - 'data' => $data, + 'data' => $data, ], $code); } protected function error(string $message = 'An error occurred', int $code = Response::HTTP_BAD_REQUEST, ?array $errors = null): JsonResponse { $payload = [ - 'status' => false, + 'status' => false, 'message' => $message, ]; - if (! empty($errors)) { + if (!empty($errors)) { $payload['errors'] = $errors; } @@ -93,7 +91,6 @@ class BaseApiController extends Controller { $payload = $this->payloadData(); $errors = $this->validateRequest($payload, $rules); - return empty($errors); } @@ -103,7 +100,6 @@ class BaseApiController extends Controller if (is_array($json)) { return $json; } - return array_merge($this->laravelRequest->query->all(), $this->laravelRequest->request->all()); } @@ -120,88 +116,71 @@ class BaseApiController extends Controller continue; } if (preg_match('/^max_length\\[(\\d+)\\]$/', $part, $m)) { - $result[] = 'max:'.$m[1]; - + $result[] = 'max:' . $m[1]; continue; } if (preg_match('/^min_length\\[(\\d+)\\]$/', $part, $m)) { - $result[] = 'min:'.$m[1]; - + $result[] = 'min:' . $m[1]; continue; } if (preg_match('/^in_list\\[(.+)\\]$/', $part, $m)) { - $result[] = 'in:'.$m[1]; - + $result[] = 'in:' . $m[1]; continue; } if (preg_match('/^greater_than_equal_to\\[(.+)\\]$/', $part, $m)) { - $result[] = 'gte:'.$m[1]; - + $result[] = 'gte:' . $m[1]; continue; } if (preg_match('/^less_than_equal_to\\[(.+)\\]$/', $part, $m)) { - $result[] = 'lte:'.$m[1]; - + $result[] = 'lte:' . $m[1]; continue; } if (preg_match('/^valid_date\\[(.+)\\]$/', $part, $m)) { - $result[] = 'date_format:'.$m[1]; - + $result[] = 'date_format:' . $m[1]; continue; } if ($part === 'valid_email') { $result[] = 'email'; - continue; } if ($part === 'permit_empty') { $result[] = 'nullable'; - continue; } if (preg_match('/^is_unique\\[([^\\.]+)\\.([^\\]]+)\\]$/', $part, $m)) { - $result[] = 'unique:'.$m[1].','.$m[2]; - + $result[] = 'unique:' . $m[1] . ',' . $m[2]; continue; } if ($part === 'string') { $result[] = 'string'; - continue; } if ($part === 'integer') { $result[] = 'integer'; - continue; } if ($part === 'numeric') { $result[] = 'numeric'; - continue; } if ($part === 'required') { $result[] = 'required'; - continue; } if ($part === 'alpha_numeric') { $result[] = 'alpha_num'; - continue; } if ($part === 'in_array') { $result[] = 'in_array'; - continue; } if ($part === 'valid_url') { $result[] = 'url'; - continue; } if (strpos($part, 'max_size') === 0) { $result[] = str_replace('max_size', 'max', $part); - continue; } $result[] = $part; @@ -212,11 +191,11 @@ class BaseApiController extends Controller protected function paginate($source, int $page = 1, int $perPage = 20): array { - $page = max(1, $page); + $page = max(1, $page); $perPage = max(1, $perPage); - $offset = ($page - 1) * $perPage; + $offset = ($page - 1) * $perPage; - if ($source instanceof EloquentBuilder || $source instanceof Builder) { + if ($source instanceof EloquentBuilder || $source instanceof \Illuminate\Database\Query\Builder) { $total = $source->toBase()->getCountForPagination(); $items = $source->offset($offset)->limit($perPage)->get()->toArray(); } elseif (is_array($source)) { @@ -227,9 +206,9 @@ class BaseApiController extends Controller 'data' => [], 'pagination' => [ 'current_page' => $page, - 'per_page' => $perPage, - 'total' => 0, - 'total_pages' => 0, + 'per_page' => $perPage, + 'total' => 0, + 'total_pages' => 0, ], ]; } @@ -238,9 +217,9 @@ class BaseApiController extends Controller 'data' => array_values($items), 'pagination' => [ 'current_page' => $page, - 'per_page' => $perPage, - 'total' => $total, - 'total_pages' => (int) ceil($total / $perPage), + 'per_page' => $perPage, + 'total' => $total, + 'total_pages' => (int) ceil($total / $perPage), ], ]; } @@ -254,30 +233,29 @@ class BaseApiController extends Controller if ($userId <= 0) { $userId = (int) (optional($this->laravelRequest->user())->id ?? 0); } - return $userId > 0 ? $userId : null; } protected function getCurrentUser(): ?object { $userId = $this->getCurrentUserId(); - if (! $userId) { + if (!$userId) { return null; } /** @var User|null $row */ $row = User::query()->find($userId); - if (! $row) { + if (!$row) { return null; } $firstname = (string) ($row->firstname ?? ''); - $lastname = (string) ($row->lastname ?? ''); - $email = $row->email; + $lastname = (string) ($row->lastname ?? ''); + $email = $row->email; - $name = trim($firstname.' '.$lastname); + $name = trim($firstname . ' ' . $lastname); if ($name === '') { - $name = $email ?? 'User #'.$userId; + $name = $email ?? 'User #' . $userId; } try { @@ -291,13 +269,13 @@ class BaseApiController extends Controller ->values() ->all(); } catch (\Throwable $e) { - Log::error('Unable to load user roles: '.$e->getMessage()); + Log::error('Unable to load user roles: ' . $e->getMessage()); $roles = []; } return (object) [ - 'id' => (int) $userId, - 'name' => $name, + 'id' => (int) $userId, + 'name' => $name, 'email' => $email, 'roles' => $roles, ]; diff --git a/app/Http/Controllers/Api/Discounts/DiscountController.php b/app/Http/Controllers/Api/Discounts/DiscountController.php index 0784d899..f1fb5183 100644 --- a/app/Http/Controllers/Api/Discounts/DiscountController.php +++ b/app/Http/Controllers/Api/Discounts/DiscountController.php @@ -17,11 +17,8 @@ use Illuminate\Http\JsonResponse; class DiscountController extends BaseApiController { private DiscountContextService $context; - private DiscountVoucherService $voucherService; - private DiscountParentService $parentService; - private DiscountApplyService $applyService; public function __construct( @@ -72,7 +69,6 @@ class DiscountController extends BaseApiController public function listVouchers(): JsonResponse { $vouchers = $this->voucherService->listAll(); - return response()->json([ 'ok' => true, 'vouchers' => DiscountVoucherResource::collection($vouchers), @@ -82,7 +78,6 @@ class DiscountController extends BaseApiController public function storeVoucher(StoreDiscountVoucherRequest $request): JsonResponse { $voucher = $this->voucherService->create($request->validated()); - return response()->json([ 'ok' => true, 'voucher' => new DiscountVoucherResource($voucher), @@ -92,7 +87,6 @@ class DiscountController extends BaseApiController public function updateVoucher(UpdateDiscountVoucherRequest $request, int $id): JsonResponse { $voucher = $this->voucherService->update($id, $request->validated()); - return response()->json([ 'ok' => true, 'voucher' => new DiscountVoucherResource($voucher), @@ -102,7 +96,6 @@ class DiscountController extends BaseApiController public function showVoucher(int $id): JsonResponse { $voucher = $this->voucherService->find($id); - return response()->json([ 'ok' => true, 'voucher' => new DiscountVoucherResource($voucher), @@ -112,12 +105,14 @@ class DiscountController extends BaseApiController public function deleteVoucher(int $id): JsonResponse { $this->voucherService->delete($id); - return response()->json([ 'ok' => true, ]); } + /** + * @return int|JsonResponse + */ private function authenticatedUserIdOrUnauthorized(): int|JsonResponse { $userId = (int) (auth()->id() ?? 0); diff --git a/app/Http/Controllers/Api/Email/BroadcastEmailController.php b/app/Http/Controllers/Api/Email/BroadcastEmailController.php index 200506b4..797d579c 100644 --- a/app/Http/Controllers/Api/Email/BroadcastEmailController.php +++ b/app/Http/Controllers/Api/Email/BroadcastEmailController.php @@ -14,13 +14,9 @@ use Illuminate\Http\Request; class BroadcastEmailController extends BaseApiController { private BroadcastEmailSenderOptionsService $senderOptions; - private BroadcastEmailRecipientService $recipients; - private BroadcastEmailComposerService $composer; - private BroadcastEmailDispatchService $dispatch; - private BroadcastEmailImageService $imageService; public function __construct( @@ -144,7 +140,7 @@ class BroadcastEmailController extends BaseApiController public function uploadImage(Request $request): JsonResponse { $file = $request->file('image'); - if (! $file || ! $file->isValid()) { + if (!$file || !$file->isValid()) { return response()->json([ 'success' => false, 'error' => 'No image uploaded', @@ -152,7 +148,7 @@ class BroadcastEmailController extends BaseApiController } $result = $this->imageService->store($file); - if (! $result['ok']) { + if (!$result['ok']) { return response()->json([ 'success' => false, 'error' => $result['error'] ?? 'Upload failed', @@ -178,7 +174,6 @@ class BroadcastEmailController extends BaseApiController foreach (explode(',', $value) as $piece) { $ids[] = (int) $piece; } - continue; } $ids[] = (int) $value; diff --git a/app/Http/Controllers/Api/Email/EmailController.php b/app/Http/Controllers/Api/Email/EmailController.php index 78d1d5de..696316a9 100644 --- a/app/Http/Controllers/Api/Email/EmailController.php +++ b/app/Http/Controllers/Api/Email/EmailController.php @@ -46,7 +46,7 @@ class EmailController extends BaseApiController $attachments ); - if (! $ok) { + if (!$ok) { return response()->json([ 'ok' => false, 'message' => 'Email failed to send.', diff --git a/app/Http/Controllers/Api/Email/EmailExtractorController.php b/app/Http/Controllers/Api/Email/EmailExtractorController.php index e1b797cd..f63cbd33 100644 --- a/app/Http/Controllers/Api/Email/EmailExtractorController.php +++ b/app/Http/Controllers/Api/Email/EmailExtractorController.php @@ -8,6 +8,7 @@ use App\Http\Resources\Email\EmailExtractorCompareResource; use App\Http\Resources\Email\EmailExtractorEmailsResource; use App\Services\Email\EmailExtractorService; use Illuminate\Http\JsonResponse; +use Illuminate\Http\Request; class EmailExtractorController extends BaseApiController { diff --git a/app/Http/Controllers/Api/Exams/ExamDraftController.php b/app/Http/Controllers/Api/Exams/ExamDraftController.php index 4798260f..b434813c 100644 --- a/app/Http/Controllers/Api/Exams/ExamDraftController.php +++ b/app/Http/Controllers/Api/Exams/ExamDraftController.php @@ -49,7 +49,7 @@ class ExamDraftController extends BaseApiController } $result = $this->teacherService->store($guard, $request->validated(), $request->file('draft_file')); - if (! $result['ok']) { + if (!$result['ok']) { return response()->json(['ok' => false, 'message' => $result['message'] ?? 'Unable to save.'], 422); } @@ -85,7 +85,7 @@ class ExamDraftController extends BaseApiController } $result = $this->adminService->uploadLegacy($guard, $request->validated(), $request->file('old_exam_file')); - if (! $result['ok']) { + if (!$result['ok']) { return response()->json(['ok' => false, 'message' => $result['message'] ?? 'Unable to save.'], 422); } @@ -103,7 +103,7 @@ class ExamDraftController extends BaseApiController } $result = $this->adminService->review($guard, $request->validated(), $request->file('final_file')); - if (! $result['ok']) { + if (!$result['ok']) { return response()->json(['ok' => false, 'message' => $result['message'] ?? 'Unable to save.'], 422); } @@ -113,6 +113,9 @@ class ExamDraftController extends BaseApiController ]); } + /** + * @return int|JsonResponse + */ private function authenticatedUserIdOrUnauthorized(): int|JsonResponse { $userId = (int) (auth()->id() ?? 0); diff --git a/app/Http/Controllers/Api/Expenses/ExpenseController.php b/app/Http/Controllers/Api/Expenses/ExpenseController.php index 771b573e..a7440736 100644 --- a/app/Http/Controllers/Api/Expenses/ExpenseController.php +++ b/app/Http/Controllers/Api/Expenses/ExpenseController.php @@ -51,7 +51,6 @@ class ExpenseController extends BaseApiController $rows = $this->queryService->listAll(); $rows = array_map(function ($row) { $row['receipt_url'] = $this->receiptService->receiptUrl($row['receipt_path'] ?? null); - return $row; }, $rows); @@ -64,7 +63,7 @@ class ExpenseController extends BaseApiController public function show(int $id): JsonResponse { $row = $this->queryService->findById($id); - if (! $row) { + if (!$row) { return response()->json(['ok' => false, 'message' => 'Expense not found.'], 404); } @@ -122,7 +121,7 @@ class ExpenseController extends BaseApiController } $expense = Expense::query()->find($id); - if (! $expense) { + if (!$expense) { return response()->json(['ok' => false, 'message' => 'Expense not found.'], 404); } @@ -132,7 +131,7 @@ class ExpenseController extends BaseApiController if ($request->hasFile('receipt')) { $receiptName = $this->receiptService->storeReceipt($request->file('receipt')); } - if (! empty($payload['remove_receipt'])) { + if (!empty($payload['remove_receipt'])) { $receiptName = null; } @@ -177,7 +176,7 @@ class ExpenseController extends BaseApiController } $expense = Expense::query()->find($id); - if (! $expense) { + if (!$expense) { return response()->json(['ok' => false, 'message' => 'Expense not found.'], 404); } @@ -191,6 +190,9 @@ class ExpenseController extends BaseApiController ]); } + /** + * @return int|JsonResponse + */ private function authenticatedUserIdOrUnauthorized(): int|JsonResponse { $userId = (int) (auth()->id() ?? 0); diff --git a/app/Http/Controllers/Api/ExtraCharges/ExtraChargesController.php b/app/Http/Controllers/Api/ExtraCharges/ExtraChargesController.php index 696880de..fac7d96d 100644 --- a/app/Http/Controllers/Api/ExtraCharges/ExtraChargesController.php +++ b/app/Http/Controllers/Api/ExtraCharges/ExtraChargesController.php @@ -103,7 +103,7 @@ class ExtraChargesController extends BaseApiController public function update(UpdateExtraChargeRequest $request, int $id): JsonResponse { $charge = AdditionalCharge::query()->find($id); - if (! $charge) { + if (!$charge) { return response()->json(['ok' => false, 'error' => 'Charge not found'], 404); } @@ -126,7 +126,7 @@ class ExtraChargesController extends BaseApiController public function void(int $id): JsonResponse { $charge = AdditionalCharge::query()->find($id); - if (! $charge) { + if (!$charge) { return response()->json(['ok' => false, 'error' => 'Charge not found'], 404); } @@ -140,7 +140,7 @@ class ExtraChargesController extends BaseApiController public function reverse(int $id): JsonResponse { $charge = AdditionalCharge::query()->find($id); - if (! $charge) { + if (!$charge) { return response()->json(['ok' => false, 'error' => 'Charge not found'], 404); } @@ -151,6 +151,9 @@ class ExtraChargesController extends BaseApiController ], $ok ? 200 : 422); } + /** + * @return int|JsonResponse + */ private function authenticatedUserIdOrUnauthorized(): int|JsonResponse { $userId = (int) (auth()->id() ?? 0); diff --git a/app/Http/Controllers/Api/Family/FamilyAdminController.php b/app/Http/Controllers/Api/Family/FamilyAdminController.php index cc0ff8e9..02d2007c 100644 --- a/app/Http/Controllers/Api/Family/FamilyAdminController.php +++ b/app/Http/Controllers/Api/Family/FamilyAdminController.php @@ -72,7 +72,7 @@ class FamilyAdminController extends BaseApiController $schoolYear ); - if (! $family) { + if (!$family) { return $this->error('Family not found.', Response::HTTP_NOT_FOUND); } @@ -92,9 +92,8 @@ class FamilyAdminController extends BaseApiController $payload['reply_to_name'] ?? null ); - if (! $ok) { + if (!$ok) { Log::warning('Compose email failed for family admin', ['recipient' => $payload['recipient']]); - return $this->error('Unable to send email.', Response::HTTP_INTERNAL_SERVER_ERROR); } diff --git a/app/Http/Controllers/Api/Family/FamilyController.php b/app/Http/Controllers/Api/Family/FamilyController.php index 047755c4..63f3c6ad 100644 --- a/app/Http/Controllers/Api/Family/FamilyController.php +++ b/app/Http/Controllers/Api/Family/FamilyController.php @@ -53,8 +53,7 @@ class FamilyController extends BaseApiController try { $result = $this->mutationService->bootstrapFamilies(); } catch (\Throwable $e) { - Log::error('Family bootstrap failed: '.$e->getMessage()); - + Log::error('Family bootstrap failed: ' . $e->getMessage()); return $this->error('Family bootstrap failed.', Response::HTTP_INTERNAL_SERVER_ERROR); } @@ -70,7 +69,7 @@ class FamilyController extends BaseApiController (string) ($payload['relation'] ?? 'secondary') ); - if (! ($result['ok'] ?? false)) { + if (!($result['ok'] ?? false)) { return $this->error($result['message'] ?? 'Unable to attach guardian.', Response::HTTP_UNPROCESSABLE_ENTITY); } @@ -89,12 +88,11 @@ class FamilyController extends BaseApiController (string) ($payload['relation'] ?? 'secondary') ); } catch (\Throwable $e) { - Log::error('Attach guardian by email failed: '.$e->getMessage()); - + Log::error('Attach guardian by email failed: ' . $e->getMessage()); return $this->error('Unable to attach guardian.', Response::HTTP_INTERNAL_SERVER_ERROR); } - if (! ($result['ok'] ?? false)) { + if (!($result['ok'] ?? false)) { return $this->error($result['message'] ?? 'Unable to attach guardian.', Response::HTTP_UNPROCESSABLE_ENTITY); } @@ -158,12 +156,11 @@ class FamilyController extends BaseApiController try { $result = $this->mutationService->importSecondParentsFromLegacy(); } catch (\Throwable $e) { - Log::error('Legacy second parent import failed: '.$e->getMessage()); - + Log::error('Legacy second parent import failed: ' . $e->getMessage()); return $this->error('Legacy import failed.', Response::HTTP_INTERNAL_SERVER_ERROR); } - if (! ($result['ok'] ?? true)) { + if (!($result['ok'] ?? true)) { return $this->error($result['message'] ?? 'Legacy import failed.', Response::HTTP_UNPROCESSABLE_ENTITY); } diff --git a/app/Http/Controllers/Api/Finance/BalanceCarryforwardController.php b/app/Http/Controllers/Api/Finance/BalanceCarryforwardController.php index 43b268c9..2a8af7d8 100644 --- a/app/Http/Controllers/Api/Finance/BalanceCarryforwardController.php +++ b/app/Http/Controllers/Api/Finance/BalanceCarryforwardController.php @@ -38,14 +38,12 @@ class BalanceCarryforwardController extends Controller public function waive(CarryforwardAdjustmentRequest $request, int $carryforward): JsonResponse { $data = $request->validated(); - return response()->json(['ok' => true, 'carryforward' => $this->service->waive($carryforward, (float) $data['amount'], $data['reason'] ?? null, optional($request->user())->id)]); } public function adjust(CarryforwardAdjustmentRequest $request, int $carryforward): JsonResponse { $data = $request->validated(); - return response()->json(['ok' => true, 'carryforward' => $this->service->adjust($carryforward, (float) $data['amount'], $data['reason'] ?? null)]); } @@ -57,13 +55,10 @@ class BalanceCarryforwardController extends Controller public function reportCsv(Request $request): StreamedResponse { $rows = $this->service->csvRows($this->service->report($request->query())); - return response()->streamDownload(function () use ($rows) { $out = fopen('php://output', 'w'); - foreach ($rows as $row) { - fputcsv($out, $row); - } + foreach ($rows as $row) fputcsv($out, $row); fclose($out); - }, 'carryforward_report_'.date('Ymd_His').'.csv', ['Content-Type' => 'text/csv']); + }, 'carryforward_report_' . date('Ymd_His') . '.csv', ['Content-Type' => 'text/csv']); } } diff --git a/app/Http/Controllers/Api/Finance/ChargeController.php b/app/Http/Controllers/Api/Finance/ChargeController.php index 6ae5ea37..ccb535ca 100644 --- a/app/Http/Controllers/Api/Finance/ChargeController.php +++ b/app/Http/Controllers/Api/Finance/ChargeController.php @@ -143,6 +143,9 @@ class ChargeController extends BaseApiController ], $status); } + /** + * @return int|JsonResponse + */ private function authenticatedUserIdOrUnauthorized(): int|JsonResponse { $userId = (int) (auth()->id() ?? 0); diff --git a/app/Http/Controllers/Api/Finance/EventChargeController.php b/app/Http/Controllers/Api/Finance/EventChargeController.php index e5ed0654..8d85d5e3 100644 --- a/app/Http/Controllers/Api/Finance/EventChargeController.php +++ b/app/Http/Controllers/Api/Finance/EventChargeController.php @@ -15,24 +15,15 @@ class EventChargeController extends Controller public function index(Request $request): JsonResponse { $q = EventCharge::query(); - foreach (['parent_id', 'student_id', 'event_id', 'school_year', 'semester'] as $field) { - if ($request->filled($field)) { - $q->where($field, $request->query($field)); - } + foreach (['parent_id','student_id','event_id','school_year','semester'] as $field) { + if ($request->filled($field)) $q->where($field, $request->query($field)); } if ($request->filled('status')) { $status = $request->query('status'); - if (Schema::hasColumn('event_charges', 'status')) { - $q->where('status', $status); - } elseif ($status === 'paid') { - $q->where('event_paid', 1); - } elseif ($status === 'pending') { - $q->where(function ($qq) { - $qq->whereNull('event_paid')->orWhere('event_paid', 0); - }); - } + if (Schema::hasColumn('event_charges', 'status')) $q->where('status', $status); + elseif ($status === 'paid') $q->where('event_paid', 1); + elseif ($status === 'pending') $q->where(function ($qq) { $qq->whereNull('event_paid')->orWhere('event_paid', 0); }); } - return response()->json(['ok' => true, 'event_charges' => $q->orderByDesc('id')->paginate((int) $request->query('per_page', 25))]); } @@ -40,11 +31,8 @@ class EventChargeController extends Controller { $data = $this->normalizePayload($request->validated()); $data['created_by'] = optional($request->user())->id; - if (Schema::hasColumn('event_charges', 'status')) { - $data['status'] = $data['status'] ?? 'draft'; - } + if (Schema::hasColumn('event_charges', 'status')) $data['status'] = $data['status'] ?? 'draft'; $charge = EventCharge::query()->create($data); - return response()->json(['ok' => true, 'event_charge' => $charge], 201); } @@ -57,7 +45,6 @@ class EventChargeController extends Controller { $charge = EventCharge::query()->findOrFail($eventCharge); $charge->fill($this->normalizePayload($request->validated()))->save(); - return response()->json(['ok' => true, 'event_charge' => $charge->fresh()]); } @@ -69,58 +56,38 @@ class EventChargeController extends Controller public function approve(int $eventCharge): JsonResponse { $charge = EventCharge::query()->findOrFail($eventCharge); - if (Schema::hasColumn('event_charges', 'status')) { - $charge->status = 'approved'; - } - if (Schema::hasColumn('event_charges', 'charged')) { - $charge->charged = 1; - } + if (Schema::hasColumn('event_charges', 'status')) $charge->status = 'approved'; + if (Schema::hasColumn('event_charges', 'charged')) $charge->charged = 1; $charge->save(); - return response()->json(['ok' => true, 'event_charge' => $charge->fresh()]); } public function void(int $eventCharge): JsonResponse { $charge = EventCharge::query()->findOrFail($eventCharge); - if (Schema::hasColumn('event_charges', 'status')) { - $charge->status = 'voided'; - } - if (Schema::hasColumn('event_charges', 'charged')) { - $charge->charged = 0; - } + if (Schema::hasColumn('event_charges', 'status')) $charge->status = 'voided'; + if (Schema::hasColumn('event_charges', 'charged')) $charge->charged = 0; $charge->save(); - return response()->json(['ok' => true, 'event_charge' => $charge->fresh()]); } public function attachToInvoice(Request $request, int $eventCharge): JsonResponse { - $request->validate(['invoice_id' => ['required', 'integer']]); + $request->validate(['invoice_id' => ['required','integer']]); $charge = EventCharge::query()->findOrFail($eventCharge); $invoiceId = (int) $request->input('invoice_id'); - abort_if(! DB::table('invoices')->where('id', $invoiceId)->exists(), 404, 'Invoice not found.'); - if (Schema::hasColumn('event_charges', 'invoice_id')) { - $charge->invoice_id = $invoiceId; - } - if (Schema::hasColumn('event_charges', 'status')) { - $charge->status = 'invoiced'; - } - if (Schema::hasColumn('event_charges', 'charged')) { - $charge->charged = 1; - } + abort_if(!DB::table('invoices')->where('id', $invoiceId)->exists(), 404, 'Invoice not found.'); + if (Schema::hasColumn('event_charges', 'invoice_id')) $charge->invoice_id = $invoiceId; + if (Schema::hasColumn('event_charges', 'status')) $charge->status = 'invoiced'; + if (Schema::hasColumn('event_charges', 'charged')) $charge->charged = 1; $charge->save(); - - return response()->json(['ok' => true, 'event_charge' => $charge->fresh(), 'warning' => Schema::hasColumn('event_charges', 'invoice_id') ? null : 'invoice_id column does not exist; charge marked as charged only.']); + return response()->json(['ok' => true, 'event_charge' => $charge->fresh(), 'warning' => Schema::hasColumn('event_charges','invoice_id') ? null : 'invoice_id column does not exist; charge marked as charged only.']); } private function normalizePayload(array $data): array { - if (isset($data['title']) && ! isset($data['event_name'])) { - $data['event_name'] = $data['title']; - } + if (isset($data['title']) && !isset($data['event_name'])) $data['event_name'] = $data['title']; unset($data['title'], $data['invoice_id']); - - return array_filter($data, fn ($v) => $v !== null); + return array_filter($data, fn($v) => $v !== null); } } diff --git a/app/Http/Controllers/Api/Finance/FinanceNotificationController.php b/app/Http/Controllers/Api/Finance/FinanceNotificationController.php index 6b0e7740..6b17e1a6 100644 --- a/app/Http/Controllers/Api/Finance/FinanceNotificationController.php +++ b/app/Http/Controllers/Api/Finance/FinanceNotificationController.php @@ -16,7 +16,7 @@ class FinanceNotificationController extends Controller public function sendPaymentReceipt(Request $request, int $payment): JsonResponse { $p = DB::table('payments')->where('id', $payment)->first(); - abort_if(! $p, 404, 'Payment not found.'); + abort_if(!$p, 404, 'Payment not found.'); $receipt = $this->service->nextReceiptNumber($p->school_year ?? date('Y')); $log = $this->service->log([ 'parent_id' => $p->parent_id ?? null, @@ -25,39 +25,34 @@ class FinanceNotificationController extends Controller 'notification_type' => 'payment_receipt', 'channel' => 'database', 'recipient' => (string) ($p->parent_id ?? ''), - 'subject' => 'Payment receipt '.$receipt, + 'subject' => 'Payment receipt ' . $receipt, ], optional($request->user())->id); - return response()->json(['ok' => true, 'receipt_number' => $receipt, 'notification_log' => $log]); } public function sendRefundReceipt(Request $request, int $refund): JsonResponse { $log = $this->service->log(['refund_id' => $refund, 'notification_type' => 'refund_receipt', 'channel' => 'database', 'subject' => 'Refund receipt'], optional($request->user())->id); - return response()->json(['ok' => true, 'notification_log' => $log]); } public function sendInvoiceStatement(Request $request, int $invoice): JsonResponse { $inv = DB::table('invoices')->where('id', $invoice)->first(); - abort_if(! $inv, 404, 'Invoice not found.'); + abort_if(!$inv, 404, 'Invoice not found.'); $log = $this->service->log(['parent_id' => $inv->parent_id ?? null, 'invoice_id' => $invoice, 'notification_type' => 'statement_available', 'channel' => 'database', 'subject' => 'Statement available'], optional($request->user())->id); - return response()->json(['ok' => true, 'notification_log' => $log]); } public function sendOverdueReminder(Request $request, int $parent): JsonResponse { $log = $this->service->log(['parent_id' => $parent, 'notification_type' => 'overdue_balance_reminder', 'channel' => 'database', 'subject' => 'Overdue balance reminder'], optional($request->user())->id); - return response()->json(['ok' => true, 'notification_log' => $log]); } public function sendInstallmentReminder(Request $request, int $installment): JsonResponse { $log = $this->service->log(['installment_id' => $installment, 'notification_type' => 'installment_reminder', 'channel' => 'database', 'subject' => 'Installment reminder'], optional($request->user())->id); - return response()->json(['ok' => true, 'notification_log' => $log]); } diff --git a/app/Http/Controllers/Api/Finance/FinancialController.php b/app/Http/Controllers/Api/Finance/FinancialController.php index ba354af4..a78c8e36 100644 --- a/app/Http/Controllers/Api/Finance/FinancialController.php +++ b/app/Http/Controllers/Api/Finance/FinancialController.php @@ -5,10 +5,10 @@ namespace App\Http\Controllers\Api\Finance; use App\Http\Controllers\Api\Core\BaseApiController; use App\Http\Requests\Finance\FinancialReportRequest; use App\Http\Requests\Finance\FinancialSummaryRequest; -use App\Http\Requests\Finance\FinancialUnpaidParentsRequest; -use App\Http\Requests\Finance\FollowUpNoteRequest; -use App\Http\Requests\Finance\ParentPaymentFollowUpRequest; use App\Http\Requests\Finance\StakeholderFinancialAnalysisRequest; +use App\Http\Requests\Finance\FinancialUnpaidParentsRequest; +use App\Http\Requests\Finance\ParentPaymentFollowUpRequest; +use App\Http\Requests\Finance\FollowUpNoteRequest; use App\Http\Resources\Finance\FinancialReportResource; use App\Http\Resources\Finance\FinancialSummaryResource; use App\Http\Resources\Finance\FinancialUnpaidParentResource; @@ -16,9 +16,9 @@ use App\Services\Finance\FinancialPdfReportService; use App\Services\Finance\FinancialReportService; use App\Services\Finance\FinancialSchoolYearService; use App\Services\Finance\FinancialSummaryService; +use App\Services\Finance\StakeholderFinancialAnalysisService; use App\Services\Finance\FinancialUnpaidParentsService; use App\Services\Finance\ParentPaymentFollowUpService; -use App\Services\Finance\StakeholderFinancialAnalysisService; use Illuminate\Http\JsonResponse; use Symfony\Component\HttpFoundation\StreamedResponse; @@ -82,6 +82,9 @@ class FinancialController extends BaseApiController ]); } + + + public function parentPaymentFollowups(ParentPaymentFollowUpRequest $request): JsonResponse { return response()->json([ @@ -101,7 +104,7 @@ class FinancialController extends BaseApiController fputcsv($out, $row); } fclose($out); - }, 'parent_payment_followups_'.($report['schoolYear'] ?? 'report').'_'.date('Ymd_His').'.csv', ['Content-Type' => 'text/csv']); + }, 'parent_payment_followups_' . ($report['schoolYear'] ?? 'report') . '_' . date('Ymd_His') . '.csv', ['Content-Type' => 'text/csv']); } public function storeParentFollowUpNote(FollowUpNoteRequest $request, int $parent): JsonResponse @@ -115,21 +118,18 @@ class FinancialController extends BaseApiController public function markParentContacted(FollowUpNoteRequest $request, int $parent): JsonResponse { $payload = array_merge($request->validated(), ['status' => $request->input('status', 'contacted')]); - return response()->json(['ok' => true, 'note' => $this->parentPaymentFollowUpService->storeNote($parent, $payload, optional($request->user())->id)], 201); } public function storePromiseToPay(FollowUpNoteRequest $request, int $parent): JsonResponse { $payload = array_merge($request->validated(), ['status' => 'promise_to_pay']); - return response()->json(['ok' => true, 'note' => $this->parentPaymentFollowUpService->storeNote($parent, $payload, optional($request->user())->id)], 201); } public function resolveParentFollowUp(FollowUpNoteRequest $request, int $parent): JsonResponse { $payload = array_merge($request->validated(), ['status' => 'resolved']); - return response()->json(['ok' => true, 'note' => $this->parentPaymentFollowUpService->storeNote($parent, $payload, optional($request->user())->id)], 201); } @@ -168,7 +168,7 @@ class FinancialController extends BaseApiController fputcsv($out, $row); } fclose($out); - }, 'stakeholder_financial_analysis_'.$schoolYear.'_'.date('Ymd_His').'.csv', ['Content-Type' => 'text/csv']); + }, 'stakeholder_financial_analysis_' . $schoolYear . '_' . date('Ymd_His') . '.csv', ['Content-Type' => 'text/csv']); } public function downloadCsv(FinancialReportRequest $request): StreamedResponse @@ -200,7 +200,7 @@ class FinancialController extends BaseApiController $breakdown = $report['paymentBreakdown'] ?? []; - $filename = 'financial_report_'.date('Ymd_His').'.csv'; + $filename = 'financial_report_' . date('Ymd_His') . '.csv'; return response()->streamDownload(function () use ($report, $paymentsMap, $refundsMap, $discountsMap, $breakdown) { $out = fopen('php://output', 'w'); @@ -309,6 +309,6 @@ class FinancialController extends BaseApiController return response()->streamDownload(function () use ($pdfBytes) { echo $pdfBytes; - }, 'Financial_Report_'.$schoolYear.'.pdf', ['Content-Type' => 'application/pdf']); + }, 'Financial_Report_' . $schoolYear . '.pdf', ['Content-Type' => 'application/pdf']); } } diff --git a/app/Http/Controllers/Api/Finance/InstallmentPlanController.php b/app/Http/Controllers/Api/Finance/InstallmentPlanController.php index 98e44832..796c66e5 100644 --- a/app/Http/Controllers/Api/Finance/InstallmentPlanController.php +++ b/app/Http/Controllers/Api/Finance/InstallmentPlanController.php @@ -13,52 +13,17 @@ class InstallmentPlanController extends Controller { public function __construct(private InstallmentPlanService $service) {} - public function index(Request $request): JsonResponse - { - return response()->json(['ok' => true, 'plans' => $this->service->list($request->query())]); - } - - public function store(InstallmentPlanRequest $request, int $invoice): JsonResponse - { - return response()->json(['ok' => true, 'plan' => $this->service->createForInvoice($invoice, $request->validated(), optional($request->user())->id)], 201); - } - - public function show(int $plan): JsonResponse - { - return response()->json(['ok' => true, 'plan' => $this->service->show($plan)]); - } - - public function activate(Request $request, int $plan): JsonResponse - { - return response()->json(['ok' => true, 'plan' => $this->service->activate($plan, optional($request->user())->id)]); - } - - public function cancel(int $plan): JsonResponse - { - return response()->json(['ok' => true, 'plan' => $this->service->cancel($plan)]); - } - + public function index(Request $request): JsonResponse { return response()->json(['ok' => true, 'plans' => $this->service->list($request->query())]); } + public function store(InstallmentPlanRequest $request, int $invoice): JsonResponse { return response()->json(['ok' => true, 'plan' => $this->service->createForInvoice($invoice, $request->validated(), optional($request->user())->id)], 201); } + public function show(int $plan): JsonResponse { return response()->json(['ok' => true, 'plan' => $this->service->show($plan)]); } + public function activate(Request $request, int $plan): JsonResponse { return response()->json(['ok' => true, 'plan' => $this->service->activate($plan, optional($request->user())->id)]); } + public function cancel(int $plan): JsonResponse { return response()->json(['ok' => true, 'plan' => $this->service->cancel($plan)]); } public function restructure(InstallmentPlanRequest $request, int $plan): JsonResponse { $old = $this->service->cancel($plan); - return response()->json(['ok' => true, 'old_plan' => $old, 'message' => 'Old plan cancelled. Create a replacement plan against the invoice to preserve audit history.']); } - - public function allocatePayment(PaymentAllocationRequest $request, int $payment): JsonResponse - { - return response()->json(['ok' => true, 'result' => $this->service->allocatePayment($payment, $request->validated())]); - } - - public function due(): JsonResponse - { - return response()->json(['ok' => true, 'installments' => $this->service->due(false)]); - } - - public function overdue(): JsonResponse - { - $this->service->markOverdue(); - - return response()->json(['ok' => true, 'installments' => $this->service->due(true)]); - } + public function allocatePayment(PaymentAllocationRequest $request, int $payment): JsonResponse { return response()->json(['ok' => true, 'result' => $this->service->allocatePayment($payment, $request->validated())]); } + public function due(): JsonResponse { return response()->json(['ok' => true, 'installments' => $this->service->due(false)]); } + public function overdue(): JsonResponse { $this->service->markOverdue(); return response()->json(['ok' => true, 'installments' => $this->service->due(true)]); } } diff --git a/app/Http/Controllers/Api/Finance/InvoiceController.php b/app/Http/Controllers/Api/Finance/InvoiceController.php index 0a2dc585..153ba53a 100644 --- a/app/Http/Controllers/Api/Finance/InvoiceController.php +++ b/app/Http/Controllers/Api/Finance/InvoiceController.php @@ -111,7 +111,7 @@ class InvoiceController extends BaseApiController $status = $request->validated()['status']; $updated = Invoice::updateInvoiceStatus($invoiceId, $status); - if (! $updated) { + if (!$updated) { return response()->json(['ok' => false, 'message' => 'Invoice not found.'], 404); } @@ -151,9 +151,12 @@ class InvoiceController extends BaseApiController return response()->streamDownload(function () use ($pdfBytes) { echo $pdfBytes; - }, 'invoice_'.$invoiceId.'.pdf', ['Content-Type' => 'application/pdf']); + }, 'invoice_' . $invoiceId . '.pdf', ['Content-Type' => 'application/pdf']); } + /** + * @return int|JsonResponse + */ private function authenticatedUserIdOrUnauthorized(): int|JsonResponse { $userId = (int) (auth()->id() ?? 0); diff --git a/app/Http/Controllers/Api/Finance/PaymentController.php b/app/Http/Controllers/Api/Finance/PaymentController.php index 100e6462..db5c5477 100644 --- a/app/Http/Controllers/Api/Finance/PaymentController.php +++ b/app/Http/Controllers/Api/Finance/PaymentController.php @@ -82,7 +82,7 @@ class PaymentController extends BaseApiController $payload = $validator->validated(); $updated = $this->balanceService->updateBalance($paymentId, (float) $payload['paid_amount']); - if (! $updated) { + if (!$updated) { return response()->json(['ok' => false, 'message' => 'Payment not found.'], 404); } diff --git a/app/Http/Controllers/Api/Finance/PaymentEventChargesController.php b/app/Http/Controllers/Api/Finance/PaymentEventChargesController.php index bc8dc02f..5eeff71e 100644 --- a/app/Http/Controllers/Api/Finance/PaymentEventChargesController.php +++ b/app/Http/Controllers/Api/Finance/PaymentEventChargesController.php @@ -69,6 +69,9 @@ class PaymentEventChargesController extends BaseApiController return response()->json(['ok' => true, 'students' => $students]); } + /** + * @return int|JsonResponse + */ private function authenticatedUserIdOrUnauthorized(): int|JsonResponse { $userId = (int) (auth()->id() ?? 0); diff --git a/app/Http/Controllers/Api/Finance/PaymentManualController.php b/app/Http/Controllers/Api/Finance/PaymentManualController.php index 3f63440c..d8969126 100644 --- a/app/Http/Controllers/Api/Finance/PaymentManualController.php +++ b/app/Http/Controllers/Api/Finance/PaymentManualController.php @@ -110,10 +110,12 @@ class PaymentManualController extends BaseApiController public function file(Request $request, string $filename) { $mode = (string) ($request->query('mode') ?? 'download'); - return $this->service->serveCheckFile($filename, $mode); } + /** + * @return int|JsonResponse + */ private function authenticatedUserIdOrUnauthorized(): int|JsonResponse { $userId = (int) (auth()->id() ?? 0); diff --git a/app/Http/Controllers/Api/Finance/PaymentTransactionController.php b/app/Http/Controllers/Api/Finance/PaymentTransactionController.php index 25ff846b..f52645a1 100644 --- a/app/Http/Controllers/Api/Finance/PaymentTransactionController.php +++ b/app/Http/Controllers/Api/Finance/PaymentTransactionController.php @@ -83,7 +83,7 @@ class PaymentTransactionController extends BaseApiController $updated = $this->service->updateStatus($transactionId, (string) $status); - if (! $updated) { + if (!$updated) { return response()->json(['ok' => false, 'message' => 'Transaction not found.'], 404); } diff --git a/app/Http/Controllers/Api/Finance/PaypalTransactionsController.php b/app/Http/Controllers/Api/Finance/PaypalTransactionsController.php index f9bdf486..d6a75e3b 100644 --- a/app/Http/Controllers/Api/Finance/PaypalTransactionsController.php +++ b/app/Http/Controllers/Api/Finance/PaypalTransactionsController.php @@ -39,7 +39,7 @@ class PaypalTransactionsController extends BaseApiController $payload = $request->validated(); $rows = $this->service->listAll($payload['q'] ?? null); - $filename = 'paypal_transactions_'.date('Ymd_His').'.csv'; + $filename = 'paypal_transactions_' . date('Ymd_His') . '.csv'; return response()->streamDownload(function () use ($rows) { $out = fopen('php://output', 'w'); diff --git a/app/Http/Controllers/Api/Finance/PurchaseOrderController.php b/app/Http/Controllers/Api/Finance/PurchaseOrderController.php index e8ef46b9..7be4dd29 100644 --- a/app/Http/Controllers/Api/Finance/PurchaseOrderController.php +++ b/app/Http/Controllers/Api/Finance/PurchaseOrderController.php @@ -52,7 +52,7 @@ class PurchaseOrderController extends BaseApiController public function show(int $id): JsonResponse { $data = $this->queryService->find($id); - if (! $data) { + if (!$data) { return response()->json(['ok' => false, 'message' => 'Purchase order not found.'], 404); } @@ -69,7 +69,7 @@ class PurchaseOrderController extends BaseApiController $validator = Validator::make($data, [ 'po_number' => ['required', 'string', 'max:60'], 'supplier_id' => ['required', 'integer', 'min:1', 'exists:suppliers,id'], - 'status' => ['nullable', 'string', 'in:'.implode(',', PurchaseOrder::allowedStatuses())], + 'status' => ['nullable', 'string', 'in:' . implode(',', PurchaseOrder::allowedStatuses())], 'order_date' => ['nullable', 'date'], 'expected_date' => ['nullable', 'date'], 'notes' => ['nullable', 'string', 'max:5000'], @@ -88,7 +88,7 @@ class PurchaseOrderController extends BaseApiController } $payload = $validator->validated(); - if (! empty($payload['order_date']) && ! empty($payload['expected_date'])) { + if (!empty($payload['order_date']) && !empty($payload['expected_date'])) { $orderTs = strtotime((string) $payload['order_date']); $expectedTs = strtotime((string) $payload['expected_date']); if ($orderTs !== false && $expectedTs !== false && $expectedTs < $orderTs) { @@ -136,7 +136,7 @@ class PurchaseOrderController extends BaseApiController $payload = $validator->validated(); $po = PurchaseOrder::query()->find($id); - if (! $po) { + if (!$po) { return response()->json(['ok' => false, 'message' => 'Purchase order not found.'], 404); } @@ -169,7 +169,7 @@ class PurchaseOrderController extends BaseApiController } $po = PurchaseOrder::query()->find($id); - if (! $po) { + if (!$po) { return response()->json(['ok' => false, 'message' => 'Purchase order not found.'], 404); } diff --git a/app/Http/Controllers/Api/Finance/RefundController.php b/app/Http/Controllers/Api/Finance/RefundController.php index 76ffbbf2..6c503880 100644 --- a/app/Http/Controllers/Api/Finance/RefundController.php +++ b/app/Http/Controllers/Api/Finance/RefundController.php @@ -48,7 +48,7 @@ class RefundController extends BaseApiController public function show(int $refundId): JsonResponse { $refund = $this->queryService->find($refundId); - if (! $refund) { + if (!$refund) { return response()->json(['ok' => false, 'message' => 'Refund not found.'], 404); } @@ -71,7 +71,6 @@ class RefundController extends BaseApiController $result = $this->requestService->requestRefund($payload, $guard); } catch (\Throwable $e) { Log::error('Refund request failed.', ['error' => $e->getMessage()]); - return response()->json(['ok' => false, 'message' => 'Failed to submit refund request.'], 500); } @@ -213,6 +212,9 @@ class RefundController extends BaseApiController ]); } + /** + * @return int|JsonResponse + */ private function authenticatedUserIdOrUnauthorized(): int|JsonResponse { $userId = (int) (auth()->id() ?? 0); diff --git a/app/Http/Controllers/Api/Finance/ReimbursementController.php b/app/Http/Controllers/Api/Finance/ReimbursementController.php index f343854d..114b9fdb 100644 --- a/app/Http/Controllers/Api/Finance/ReimbursementController.php +++ b/app/Http/Controllers/Api/Finance/ReimbursementController.php @@ -93,7 +93,6 @@ class ReimbursementController extends BaseApiController } catch (RuntimeException $e) { $message = $e->getMessage(); $status = str_contains($message, 'not found') ? 404 : (str_contains($message, 'already') ? 409 : 422); - return response()->json(['ok' => false, 'message' => $message], $status); } catch (\Throwable $e) { return response()->json(['ok' => false, 'message' => 'Unable to mark donation right now.'], 500); @@ -145,7 +144,7 @@ class ReimbursementController extends BaseApiController $adminIdRaw = $payload['admin_id'] ?? null; $adminId = ($adminIdRaw === '' || $adminIdRaw === null) ? null : (int) $adminIdRaw; - $reimbursementId = ! empty($payload['reimbursement_id']) ? (int) $payload['reimbursement_id'] : null; + $reimbursementId = !empty($payload['reimbursement_id']) ? (int) $payload['reimbursement_id'] : null; try { $result = $this->assignmentService->updateAssignment( @@ -185,7 +184,6 @@ class ReimbursementController extends BaseApiController } catch (RuntimeException $e) { $message = $e->getMessage(); $status = str_contains($message, 'check file') ? 409 : 404; - return response()->json(['ok' => false, 'message' => $message], $status); } catch (\Throwable $e) { return response()->json(['ok' => false, 'message' => 'Unable to lock batch right now.'], 500); @@ -206,7 +204,7 @@ class ReimbursementController extends BaseApiController $adminId = (int) $payload['admin_id']; $batch = ReimbursementBatch::query()->find($batchId); - if (! $batch) { + if (!$batch) { return response()->json(['ok' => false, 'message' => 'Batch not found.'], 404); } if (strtolower((string) ($batch->status ?? '')) !== ReimbursementBatch::STATUS_OPEN) { @@ -233,7 +231,7 @@ class ReimbursementController extends BaseApiController return response(file_get_contents($meta['path']), 200, [ 'Content-Type' => $meta['mime'], - 'Content-Disposition' => $disposition.'; filename="'.$meta['download_name'].'"', + 'Content-Disposition' => $disposition . '; filename="' . $meta['download_name'] . '"', 'Content-Length' => (string) $meta['size'], 'ETag' => $meta['etag'], 'Last-Modified' => $meta['last_modified'], @@ -283,7 +281,7 @@ class ReimbursementController extends BaseApiController return response()->json(['ok' => false, 'message' => $e->getMessage()], 404); } - if (! $ok) { + if (!$ok) { return response()->json(['ok' => false, 'message' => 'Failed to send email. Please try again.'], 500); } @@ -303,7 +301,7 @@ class ReimbursementController extends BaseApiController return response()->streamDownload(function () use ($csv) { $out = fopen('php://output', 'w'); - fprintf($out, chr(0xEF).chr(0xBB).chr(0xBF)); + fprintf($out, chr(0xEF) . chr(0xBB) . chr(0xBF)); foreach ($csv['rows'] as $row) { fputcsv($out, $row); } @@ -320,7 +318,7 @@ class ReimbursementController extends BaseApiController return response()->streamDownload(function () use ($csv) { $out = fopen('php://output', 'w'); - fprintf($out, chr(0xEF).chr(0xBB).chr(0xBF)); + fprintf($out, chr(0xEF) . chr(0xBB) . chr(0xBF)); foreach ($csv['rows'] as $row) { fputcsv($out, $row); } @@ -438,7 +436,7 @@ class ReimbursementController extends BaseApiController } $reimb = Reimbursement::query()->find($id); - if (! $reimb) { + if (!$reimb) { return response()->json(['ok' => false, 'message' => 'Reimbursement not found.'], 404); } @@ -466,7 +464,7 @@ class ReimbursementController extends BaseApiController if ($request->hasFile('receipt')) { $receiptName = $this->fileService->storeReceipt($request->file('receipt')); } - if (! empty($payload['remove_receipt'])) { + if (!empty($payload['remove_receipt'])) { $receiptName = null; } @@ -480,6 +478,9 @@ class ReimbursementController extends BaseApiController ]); } + /** + * @return int|JsonResponse + */ private function authenticatedUserIdOrUnauthorized(): int|JsonResponse { $userId = (int) (auth()->id() ?? 0); diff --git a/app/Http/Controllers/Api/Frontend/FrontendController.php b/app/Http/Controllers/Api/Frontend/FrontendController.php index 12977b61..95664035 100644 --- a/app/Http/Controllers/Api/Frontend/FrontendController.php +++ b/app/Http/Controllers/Api/Frontend/FrontendController.php @@ -61,7 +61,7 @@ class FrontendController extends BaseApiController public function fetchUser(): JsonResponse { $user = auth()->user(); - if (! $user) { + if (!$user) { return $this->error('Unauthorized.', Response::HTTP_UNAUTHORIZED); } diff --git a/app/Http/Controllers/Api/Frontend/InfoIconController.php b/app/Http/Controllers/Api/Frontend/InfoIconController.php index c6649574..150dcdda 100644 --- a/app/Http/Controllers/Api/Frontend/InfoIconController.php +++ b/app/Http/Controllers/Api/Frontend/InfoIconController.php @@ -29,6 +29,9 @@ class InfoIconController extends BaseApiController ]); } + /** + * @return int|JsonResponse + */ private function authenticatedUserIdOrUnauthorized(): int|JsonResponse { $userId = (int) (auth()->id() ?? 0); diff --git a/app/Http/Controllers/Api/Frontend/LandingPageController.php b/app/Http/Controllers/Api/Frontend/LandingPageController.php index 5e019a68..cc792f02 100644 --- a/app/Http/Controllers/Api/Frontend/LandingPageController.php +++ b/app/Http/Controllers/Api/Frontend/LandingPageController.php @@ -19,7 +19,7 @@ class LandingPageController extends BaseApiController public function index(LandingTeacherRequest $request): JsonResponse { $user = $request->user(); - if (! $user) { + if (!$user) { return $this->error('Unauthorized.', Response::HTTP_UNAUTHORIZED); } @@ -36,7 +36,7 @@ class LandingPageController extends BaseApiController public function teacher(LandingTeacherRequest $request): JsonResponse { $user = $request->user(); - if (! $user) { + if (!$user) { return $this->error('Unauthorized.', Response::HTTP_UNAUTHORIZED); } @@ -53,7 +53,7 @@ class LandingPageController extends BaseApiController public function parent(): JsonResponse { $user = auth()->user(); - if (! $user) { + if (!$user) { return $this->error('Unauthorized.', Response::HTTP_UNAUTHORIZED); } diff --git a/app/Http/Controllers/Api/Frontend/PageController.php b/app/Http/Controllers/Api/Frontend/PageController.php index 2beeb093..0aefcccf 100644 --- a/app/Http/Controllers/Api/Frontend/PageController.php +++ b/app/Http/Controllers/Api/Frontend/PageController.php @@ -51,7 +51,7 @@ class PageController extends BaseApiController private function renderStatic(string $filename, string $type): JsonResponse { $result = $this->pageService->load($filename, $type); - if (! $result['ok']) { + if (!$result['ok']) { return $this->error($result['error'], Response::HTTP_NOT_FOUND); } diff --git a/app/Http/Controllers/Api/Grading/GradingController.php b/app/Http/Controllers/Api/Grading/GradingController.php index 312f0985..869ab0d2 100644 --- a/app/Http/Controllers/Api/Grading/GradingController.php +++ b/app/Http/Controllers/Api/Grading/GradingController.php @@ -3,17 +3,17 @@ namespace App\Http\Controllers\Api\Grading; use App\Http\Controllers\Api\Core\BaseApiController; +use App\Http\Requests\Grading\BelowSixtyEmailRequest; use App\Http\Requests\Grading\AllDecisionsRequest; use App\Http\Requests\Grading\BelowSixtyDecisionDetailsRequest; use App\Http\Requests\Grading\BelowSixtyDecisionListRequest; use App\Http\Requests\Grading\BelowSixtyDecisionSaveRequest; use App\Http\Requests\Grading\BelowSixtyDecisionSendRequest; -use App\Http\Requests\Grading\BelowSixtyEmailRequest; -use App\Http\Requests\Grading\BelowSixtyListRequest; use App\Http\Requests\Grading\BelowSixtyMeetingRequest; use App\Http\Requests\Grading\BelowSixtyMeetingSaveRequest; use App\Http\Requests\Grading\BelowSixtySendRequest; use App\Http\Requests\Grading\BelowSixtyStatusRequest; +use App\Http\Requests\Grading\BelowSixtyListRequest; use App\Http\Requests\Grading\GenerateAllDecisionsRequest; use App\Http\Requests\Grading\GradingLockAllRequest; use App\Http\Requests\Grading\GradingLockRequest; @@ -23,8 +23,8 @@ use App\Http\Requests\Grading\GradingScoreShowRequest; use App\Http\Requests\Grading\GradingScoreUpdateRequest; use App\Http\Requests\Grading\PlacementBatchRequest; use App\Http\Requests\Grading\PlacementBatchUpdateRequest; -use App\Http\Requests\Grading\PlacementLevelRequest; use App\Http\Requests\Grading\PlacementLevelsRequest; +use App\Http\Requests\Grading\PlacementLevelRequest; use App\Http\Requests\Grading\PlacementRequest; use App\Http\Resources\Grading\BelowSixtyStudentResource; use App\Http\Resources\Grading\GradingScoreResource; @@ -419,6 +419,9 @@ class GradingController extends BaseApiController return response()->json(['ok' => true]); } + /** + * @return int|JsonResponse + */ private function authenticatedUserIdOrUnauthorized(): int|JsonResponse { $userId = (int) (auth()->id() ?? 0); diff --git a/app/Http/Controllers/Api/Incidents/IncidentController.php b/app/Http/Controllers/Api/Incidents/IncidentController.php index b74dfedc..e2849d2d 100644 --- a/app/Http/Controllers/Api/Incidents/IncidentController.php +++ b/app/Http/Controllers/Api/Incidents/IncidentController.php @@ -4,6 +4,7 @@ namespace App\Http\Controllers\Api\Incidents; use App\Http\Controllers\Api\Core\BaseApiController; use App\Http\Requests\Incidents\IncidentCancelRequest; +use App\Http\Requests\Incidents\IncidentCloseRequest; use App\Http\Requests\Incidents\IncidentListRequest; use App\Http\Requests\Incidents\IncidentStateRequest; use App\Http\Resources\Incidents\IncidentAnalysisStudentResource; @@ -208,6 +209,9 @@ class IncidentController extends BaseApiController ]); } + /** + * @return int|JsonResponse + */ private function authenticatedUserIdOrUnauthorized(): int|JsonResponse { $userId = (int) (auth()->id() ?? 0); diff --git a/app/Http/Controllers/Api/Inventory/InventoryCategoryController.php b/app/Http/Controllers/Api/Inventory/InventoryCategoryController.php index 6f54b921..39501c5c 100644 --- a/app/Http/Controllers/Api/Inventory/InventoryCategoryController.php +++ b/app/Http/Controllers/Api/Inventory/InventoryCategoryController.php @@ -31,7 +31,7 @@ class InventoryCategoryController extends BaseApiController public function store(InventoryCategoryStoreRequest $request): JsonResponse { $result = $this->categories->create($request->validated()); - if (! ($result['ok'] ?? false)) { + if (!($result['ok'] ?? false)) { return $this->error($result['message'] ?? 'Failed to save category.', Response::HTTP_UNPROCESSABLE_ENTITY); } @@ -46,7 +46,7 @@ class InventoryCategoryController extends BaseApiController return $this->error($e->getMessage(), Response::HTTP_UNPROCESSABLE_ENTITY); } - if (! ($result['ok'] ?? false)) { + if (!($result['ok'] ?? false)) { return $this->error($result['message'] ?? 'Failed to update category.', Response::HTTP_UNPROCESSABLE_ENTITY); } @@ -56,7 +56,7 @@ class InventoryCategoryController extends BaseApiController public function destroy(int $id): JsonResponse { $result = $this->categories->delete($id); - if (! ($result['ok'] ?? false)) { + if (!($result['ok'] ?? false)) { return $this->error($result['message'] ?? 'Failed to delete category.', Response::HTTP_UNPROCESSABLE_ENTITY); } diff --git a/app/Http/Controllers/Api/Inventory/InventoryController.php b/app/Http/Controllers/Api/Inventory/InventoryController.php index b2c9214c..669ea5dd 100644 --- a/app/Http/Controllers/Api/Inventory/InventoryController.php +++ b/app/Http/Controllers/Api/Inventory/InventoryController.php @@ -48,7 +48,7 @@ class InventoryController extends BaseApiController public function show(int $id): JsonResponse { $item = $this->items->find($id); - if (! $item) { + if (!$item) { return $this->error('Item not found.', Response::HTTP_NOT_FOUND); } @@ -63,7 +63,7 @@ class InventoryController extends BaseApiController } $result = $this->items->create($request->validated(), $actor); - if (! ($result['ok'] ?? false)) { + if (!($result['ok'] ?? false)) { return $this->error($result['message'] ?? 'Failed to create item.', Response::HTTP_UNPROCESSABLE_ENTITY); } @@ -78,7 +78,7 @@ class InventoryController extends BaseApiController } $result = $this->items->update($id, $request->validated(), $actor); - if (! ($result['ok'] ?? false)) { + if (!($result['ok'] ?? false)) { return $this->error($result['message'] ?? 'Failed to update item.', Response::HTTP_UNPROCESSABLE_ENTITY); } @@ -88,7 +88,7 @@ class InventoryController extends BaseApiController public function destroy(int $id): JsonResponse { $result = $this->items->delete($id); - if (! ($result['ok'] ?? false)) { + if (!($result['ok'] ?? false)) { return $this->error($result['message'] ?? 'Failed to delete item.', Response::HTTP_UNPROCESSABLE_ENTITY); } @@ -103,7 +103,7 @@ class InventoryController extends BaseApiController } $result = $this->items->auditClassroom($id, $request->validated(), $actor); - if (! ($result['ok'] ?? false)) { + if (!($result['ok'] ?? false)) { return $this->error($result['message'] ?? 'Failed to audit item.', Response::HTTP_UNPROCESSABLE_ENTITY); } @@ -118,7 +118,7 @@ class InventoryController extends BaseApiController } $result = $this->items->adjustStock($id, $request->validated(), $actor); - if (! ($result['ok'] ?? false)) { + if (!($result['ok'] ?? false)) { return $this->error($result['message'] ?? 'Failed to adjust stock.', Response::HTTP_UNPROCESSABLE_ENTITY); } @@ -128,7 +128,6 @@ class InventoryController extends BaseApiController public function summary(string $type): JsonResponse { $payload = $this->summary->summary($type, request()->query('school_year')); - return $this->success($payload); } @@ -138,7 +137,6 @@ class InventoryController extends BaseApiController request()->query('school_year'), request()->query('type') ); - return $this->success($payload); } @@ -156,7 +154,7 @@ class InventoryController extends BaseApiController $payload['item_id'] ?? null ); - if (! ($result['ok'] ?? false)) { + if (!($result['ok'] ?? false)) { return $this->error($result['message'] ?? 'Unable to load distribution form.', Response::HTTP_UNPROCESSABLE_ENTITY); } @@ -173,18 +171,20 @@ class InventoryController extends BaseApiController try { $result = $this->distribution->distribute($actor, $request->validated()); } catch (\Throwable $e) { - Log::error('Inventory distribution failed: '.$e->getMessage()); - + Log::error('Inventory distribution failed: ' . $e->getMessage()); return $this->error('Unable to distribute inventory.', Response::HTTP_INTERNAL_SERVER_ERROR); } - if (! ($result['ok'] ?? false)) { + if (!($result['ok'] ?? false)) { return $this->error($result['message'] ?? 'Unable to distribute inventory.', Response::HTTP_UNPROCESSABLE_ENTITY); } return $this->success($result); } + /** + * @return int|JsonResponse + */ private function authenticatedUserIdOrUnauthorized(): int|JsonResponse { $userId = (int) (auth()->id() ?? 0); diff --git a/app/Http/Controllers/Api/Inventory/InventoryMovementController.php b/app/Http/Controllers/Api/Inventory/InventoryMovementController.php index 6b86aa91..6e18cf08 100644 --- a/app/Http/Controllers/Api/Inventory/InventoryMovementController.php +++ b/app/Http/Controllers/Api/Inventory/InventoryMovementController.php @@ -36,7 +36,7 @@ class InventoryMovementController extends BaseApiController } $result = $this->movements->create($request->validated(), $actor); - if (! ($result['ok'] ?? false)) { + if (!($result['ok'] ?? false)) { return $this->error($result['message'] ?? 'Failed to create movement.', Response::HTTP_UNPROCESSABLE_ENTITY); } @@ -46,7 +46,7 @@ class InventoryMovementController extends BaseApiController public function update(InventoryMovementUpdateRequest $request, int $id): JsonResponse { $result = $this->movements->update($id, $request->validated()); - if (! ($result['ok'] ?? false)) { + if (!($result['ok'] ?? false)) { return $this->error($result['message'] ?? 'Failed to update movement.', Response::HTTP_UNPROCESSABLE_ENTITY); } @@ -56,7 +56,7 @@ class InventoryMovementController extends BaseApiController public function destroy(int $id): JsonResponse { $result = $this->movements->delete($id); - if (! ($result['ok'] ?? false)) { + if (!($result['ok'] ?? false)) { return $this->error($result['message'] ?? 'Failed to delete movement.', Response::HTTP_UNPROCESSABLE_ENTITY); } @@ -66,13 +66,16 @@ class InventoryMovementController extends BaseApiController public function bulkDelete(InventoryMovementBulkDeleteRequest $request): JsonResponse { $result = $this->movements->bulkDelete($request->validated()['ids']); - if (! ($result['ok'] ?? false)) { + if (!($result['ok'] ?? false)) { return $this->error($result['message'] ?? 'Bulk delete failed.', Response::HTTP_UNPROCESSABLE_ENTITY); } return $this->success($result); } + /** + * @return int|JsonResponse + */ private function authenticatedUserIdOrUnauthorized(): int|JsonResponse { $userId = (int) (auth()->id() ?? 0); diff --git a/app/Http/Controllers/Api/Inventory/SupplierController.php b/app/Http/Controllers/Api/Inventory/SupplierController.php index c64c9061..509488a9 100644 --- a/app/Http/Controllers/Api/Inventory/SupplierController.php +++ b/app/Http/Controllers/Api/Inventory/SupplierController.php @@ -33,7 +33,7 @@ class SupplierController extends BaseApiController public function store(SupplierStoreRequest $request): JsonResponse { $result = $this->suppliers->create($request->validated()); - if (! ($result['ok'] ?? false)) { + if (!($result['ok'] ?? false)) { return $this->error($result['message'] ?? 'Failed to save supplier.', Response::HTTP_UNPROCESSABLE_ENTITY); } @@ -43,7 +43,7 @@ class SupplierController extends BaseApiController public function update(SupplierUpdateRequest $request, int $id): JsonResponse { $result = $this->suppliers->update($id, $request->validated()); - if (! ($result['ok'] ?? false)) { + if (!($result['ok'] ?? false)) { return $this->error($result['message'] ?? 'Failed to update supplier.', Response::HTTP_UNPROCESSABLE_ENTITY); } @@ -53,7 +53,7 @@ class SupplierController extends BaseApiController public function destroy(int $id): JsonResponse { $result = $this->suppliers->delete($id); - if (! ($result['ok'] ?? false)) { + if (!($result['ok'] ?? false)) { return $this->error($result['message'] ?? 'Failed to delete supplier.', Response::HTTP_UNPROCESSABLE_ENTITY); } diff --git a/app/Http/Controllers/Api/Inventory/SupplyCategoryController.php b/app/Http/Controllers/Api/Inventory/SupplyCategoryController.php index 25a5bc87..4a0b0e89 100644 --- a/app/Http/Controllers/Api/Inventory/SupplyCategoryController.php +++ b/app/Http/Controllers/Api/Inventory/SupplyCategoryController.php @@ -29,7 +29,7 @@ class SupplyCategoryController extends BaseApiController public function store(SupplyCategoryStoreRequest $request): JsonResponse { $result = $this->categories->create($request->validated()); - if (! ($result['ok'] ?? false)) { + if (!($result['ok'] ?? false)) { return $this->error($result['message'] ?? 'Failed to save category.', Response::HTTP_UNPROCESSABLE_ENTITY); } @@ -39,7 +39,7 @@ class SupplyCategoryController extends BaseApiController public function update(SupplyCategoryUpdateRequest $request, int $id): JsonResponse { $result = $this->categories->update($id, $request->validated()); - if (! ($result['ok'] ?? false)) { + if (!($result['ok'] ?? false)) { return $this->error($result['message'] ?? 'Failed to update category.', Response::HTTP_UNPROCESSABLE_ENTITY); } @@ -49,7 +49,7 @@ class SupplyCategoryController extends BaseApiController public function destroy(int $id): JsonResponse { $result = $this->categories->delete($id); - if (! ($result['ok'] ?? false)) { + if (!($result['ok'] ?? false)) { return $this->error($result['message'] ?? 'Failed to delete category.', Response::HTTP_UNPROCESSABLE_ENTITY); } diff --git a/app/Http/Controllers/Api/Messaging/MessagesController.php b/app/Http/Controllers/Api/Messaging/MessagesController.php index f6fffc8e..6f7374ee 100644 --- a/app/Http/Controllers/Api/Messaging/MessagesController.php +++ b/app/Http/Controllers/Api/Messaging/MessagesController.php @@ -75,7 +75,7 @@ class MessagesController extends BaseApiController public function show(int $id): JsonResponse { $message = $this->queryService->findForUser($id); - if (! $message) { + if (!$message) { return $this->error('Message not found.', Response::HTTP_NOT_FOUND); } @@ -104,8 +104,7 @@ class MessagesController extends BaseApiController try { $message = $this->commandService->create($guard, $request->validated(), $request->file('attachment')); } catch (\Throwable $e) { - Log::error('Message send failed: '.$e->getMessage()); - + Log::error('Message send failed: ' . $e->getMessage()); return $this->error($e->getMessage(), Response::HTTP_BAD_REQUEST); } @@ -151,7 +150,7 @@ class MessagesController extends BaseApiController public function update(MessageUpdateRequest $request, int $id): JsonResponse { $message = Message::query()->find($id); - if (! $message) { + if (!$message) { return $this->error('Message not found.', Response::HTTP_NOT_FOUND); } @@ -160,8 +159,7 @@ class MessagesController extends BaseApiController try { $updated = $this->commandService->update($message, $request->validated()); } catch (\Throwable $e) { - Log::error('Message update failed: '.$e->getMessage()); - + Log::error('Message update failed: ' . $e->getMessage()); return $this->error('Unable to update message.', Response::HTTP_INTERNAL_SERVER_ERROR); } @@ -173,7 +171,7 @@ class MessagesController extends BaseApiController public function destroy(int $id): JsonResponse { $message = Message::query()->find($id); - if (! $message) { + if (!$message) { return $this->error('Message not found.', Response::HTTP_NOT_FOUND); } @@ -182,12 +180,11 @@ class MessagesController extends BaseApiController try { $trashed = $this->commandService->trash($message); } catch (\Throwable $e) { - Log::error('Message delete failed: '.$e->getMessage()); - + Log::error('Message delete failed: ' . $e->getMessage()); return $this->error('Unable to delete message.', Response::HTTP_INTERNAL_SERVER_ERROR); } - if (! $trashed) { + if (!$trashed) { return $this->error('Unable to delete message.', Response::HTTP_INTERNAL_SERVER_ERROR); } @@ -223,6 +220,9 @@ class MessagesController extends BaseApiController ]); } + /** + * @return int|JsonResponse + */ private function authenticatedUserIdOrUnauthorized(): int|JsonResponse { $userId = (int) (auth()->id() ?? 0); diff --git a/app/Http/Controllers/Api/Messaging/WhatsappController.php b/app/Http/Controllers/Api/Messaging/WhatsappController.php index 0a225c8d..a7d52d28 100644 --- a/app/Http/Controllers/Api/Messaging/WhatsappController.php +++ b/app/Http/Controllers/Api/Messaging/WhatsappController.php @@ -117,12 +117,11 @@ class WhatsappController extends BaseApiController try { $result = $this->inviteService->send($request->validated()); } catch (\Throwable $e) { - Log::error('WhatsApp invite send failed: '.$e->getMessage()); - + Log::error('WhatsApp invite send failed: ' . $e->getMessage()); return $this->error('Invite processing failed.', Response::HTTP_INTERNAL_SERVER_ERROR); } - if (! ($result['ok'] ?? false)) { + if (!($result['ok'] ?? false)) { return $this->error($result['message'] ?? 'Invite processing failed.', Response::HTTP_UNPROCESSABLE_ENTITY); } @@ -141,12 +140,11 @@ class WhatsappController extends BaseApiController try { $result = $this->membershipService->updateMembership($request->validated(), $userId); } catch (\Throwable $e) { - Log::error('WhatsApp membership update failed: '.$e->getMessage()); - + Log::error('WhatsApp membership update failed: ' . $e->getMessage()); return $this->error('Failed to save membership.', Response::HTTP_INTERNAL_SERVER_ERROR); } - if (! ($result['ok'] ?? false)) { + if (!($result['ok'] ?? false)) { $code = str_contains((string) ($result['message'] ?? ''), 'missing') ? Response::HTTP_INTERNAL_SERVER_ERROR : Response::HTTP_UNPROCESSABLE_ENTITY; @@ -160,6 +158,9 @@ class WhatsappController extends BaseApiController ], 'Membership saved.'); } + /** + * @return int|JsonResponse + */ private function authenticatedUserIdOrUnauthorized(): int|JsonResponse { $userId = (int) (auth()->id() ?? 0); diff --git a/app/Http/Controllers/Api/Notifications/NotificationController.php b/app/Http/Controllers/Api/Notifications/NotificationController.php index aaa70ec8..f6e189c9 100644 --- a/app/Http/Controllers/Api/Notifications/NotificationController.php +++ b/app/Http/Controllers/Api/Notifications/NotificationController.php @@ -59,7 +59,7 @@ class NotificationController extends BaseApiController } $row = $this->showService->getForUser($guard, $notificationId); - if (! $row) { + if (!$row) { return response()->json(['ok' => false, 'message' => 'Notification not found.'], 404); } @@ -82,7 +82,6 @@ class NotificationController extends BaseApiController $result = $this->sendService->send($payload, $guard); } catch (\Throwable $e) { Log::error('Notification send failed.', ['error' => $e->getMessage()]); - return response()->json(['ok' => false, 'message' => 'Failed to send notification.'], 500); } @@ -115,7 +114,7 @@ class NotificationController extends BaseApiController public function destroy(int $notificationId): JsonResponse { $deleted = $this->managementService->delete($notificationId); - if (! $deleted) { + if (!$deleted) { return response()->json(['ok' => false, 'message' => 'Notification not found.'], 404); } @@ -125,7 +124,7 @@ class NotificationController extends BaseApiController public function restore(int $notificationId): JsonResponse { $restored = $this->managementService->restore($notificationId); - if (! $restored) { + if (!$restored) { return response()->json(['ok' => false, 'message' => 'Notification not found.'], 404); } @@ -140,7 +139,7 @@ class NotificationController extends BaseApiController } $ok = $this->readService->markRead($guard, $notificationId); - if (! $ok) { + if (!$ok) { return response()->json(['ok' => false, 'message' => 'Notification not found.'], 404); } @@ -168,6 +167,9 @@ class NotificationController extends BaseApiController ]); } + /** + * @return int|JsonResponse + */ private function authenticatedUserIdOrUnauthorized(): int|JsonResponse { $userId = (int) (auth()->id() ?? 0); diff --git a/app/Http/Controllers/Api/Parents/ParentAttendanceReportController.php b/app/Http/Controllers/Api/Parents/ParentAttendanceReportController.php index 0710ea7f..271ba93c 100644 --- a/app/Http/Controllers/Api/Parents/ParentAttendanceReportController.php +++ b/app/Http/Controllers/Api/Parents/ParentAttendanceReportController.php @@ -3,9 +3,9 @@ namespace App\Http\Controllers\Api\Parents; use App\Http\Controllers\Api\Core\BaseApiController; -use App\Http\Requests\Parents\ParentAttendanceReportCheckRequest; -use App\Http\Requests\Parents\ParentAttendanceReportListRequest; use App\Http\Requests\Parents\ParentAttendanceReportSubmitRequest; +use App\Http\Requests\Parents\ParentAttendanceReportListRequest; +use App\Http\Requests\Parents\ParentAttendanceReportCheckRequest; use App\Http\Requests\Parents\ParentAttendanceReportUpdateRequest; use App\Http\Resources\Parents\ParentAttendanceReportResource; use App\Services\Parents\ParentAttendanceReportService; @@ -116,6 +116,9 @@ class ParentAttendanceReportController extends BaseApiController return response()->json(['ok' => true]); } + /** + * @return int|JsonResponse + */ private function parentIdOrUnauthorized(): int|JsonResponse { $parentId = (int) (auth()->id() ?? 0); diff --git a/app/Http/Controllers/Api/Parents/ParentController.php b/app/Http/Controllers/Api/Parents/ParentController.php index d1b0f2d8..1dd0f3b8 100644 --- a/app/Http/Controllers/Api/Parents/ParentController.php +++ b/app/Http/Controllers/Api/Parents/ParentController.php @@ -4,18 +4,18 @@ namespace App\Http\Controllers\Api\Parents; use App\Http\Controllers\Api\Core\BaseApiController; use App\Http\Requests\Parents\ParentAttendanceRequest; -use App\Http\Requests\Parents\ParentEmergencyContactRequest; use App\Http\Requests\Parents\ParentEnrollmentActionRequest; use App\Http\Requests\Parents\ParentEnrollmentRequest; +use App\Http\Requests\Parents\ParentEmergencyContactRequest; use App\Http\Requests\Parents\ParentEventParticipationRequest; use App\Http\Requests\Parents\ParentInvoiceRequest; use App\Http\Requests\Parents\ParentProfileUpdateRequest; use App\Http\Requests\Parents\ParentRegistrationRequest; use App\Http\Requests\Parents\ParentStudentUpdateRequest; -use App\Http\Resources\Invoices\InvoiceResource; use App\Http\Resources\Parents\ParentAttendanceResource; use App\Http\Resources\Parents\ParentEmergencyContactResource; use App\Http\Resources\Parents\ParentStudentResource; +use App\Http\Resources\Invoices\InvoiceResource; use App\Services\Parents\ParentAttendanceService; use App\Services\Parents\ParentEmergencyContactService; use App\Services\Parents\ParentEnrollmentService; diff --git a/app/Http/Controllers/Api/Parents/ParentPromotionController.php b/app/Http/Controllers/Api/Parents/ParentPromotionController.php index 24bb93fa..6b861b4a 100644 --- a/app/Http/Controllers/Api/Parents/ParentPromotionController.php +++ b/app/Http/Controllers/Api/Parents/ParentPromotionController.php @@ -140,21 +140,20 @@ class ParentPromotionController extends BaseApiController private function loadOwnedRecord(int $promotionId, int $parentId): StudentPromotionRecord|JsonResponse { $record = StudentPromotionRecord::query()->find($promotionId); - if (! $record) { + if (!$record) { return response()->json(['ok' => false, 'message' => 'Promotion record not found.'], Response::HTTP_NOT_FOUND); } $owns = (int) $record->parent_id === $parentId; - if (! $owns) { + if (!$owns) { $studentParentId = (int) (Student::query() ->where('id', $record->student_id) ->value('parent_id') ?? 0); $owns = $studentParentId === $parentId; } - if (! $owns) { + if (!$owns) { return response()->json(['ok' => false, 'message' => 'You are not authorised to view this promotion record.'], Response::HTTP_FORBIDDEN); } - return $record; } @@ -169,7 +168,6 @@ class ParentPromotionController extends BaseApiController if ($parentId <= 0) { return response()->json(['ok' => false, 'message' => 'Unauthorized.'], Response::HTTP_UNAUTHORIZED); } - return $parentId; } } diff --git a/app/Http/Controllers/Api/PrintRequests/PrintRequestsController.php b/app/Http/Controllers/Api/PrintRequests/PrintRequestsController.php index 8fce19e9..ca3ba992 100644 --- a/app/Http/Controllers/Api/PrintRequests/PrintRequestsController.php +++ b/app/Http/Controllers/Api/PrintRequests/PrintRequestsController.php @@ -8,7 +8,6 @@ use App\Models\User; use App\Services\PrintRequests\PrintRequestsPortalService; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; -use Illuminate\Http\UploadedFile; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Validator; use Illuminate\Validation\Rule; @@ -60,7 +59,7 @@ class PrintRequestsController extends BaseApiController return $this->respondValidationError($validator->errors()->toArray()); } - /** @var UploadedFile $file */ + /** @var \Illuminate\Http\UploadedFile $file */ $file = $request->file('file'); $model = $this->portal->storeTeacherUpload($validator->validated(), $file, $uid); diff --git a/app/Http/Controllers/Api/Reports/ReportCardsController.php b/app/Http/Controllers/Api/Reports/ReportCardsController.php index d9145948..1fc98ea0 100644 --- a/app/Http/Controllers/Api/Reports/ReportCardsController.php +++ b/app/Http/Controllers/Api/Reports/ReportCardsController.php @@ -52,7 +52,7 @@ class ReportCardsController extends BaseApiController $classSectionId = (int) ($payload['class_section_id'] ?? 0); $result = $this->service->completeness($schoolYear, $semester, $classSectionId); - if (! ($result['ok'] ?? false)) { + if (!($result['ok'] ?? false)) { return $this->error($result['message'] ?? 'Unable to generate completeness report.', Response::HTTP_UNPROCESSABLE_ENTITY); } @@ -101,12 +101,11 @@ class ReportCardsController extends BaseApiController try { $result = $this->service->generateSingleReport($studentId, $request->validated()); } catch (\Throwable $e) { - Log::error('Report card PDF generation failed: '.$e->getMessage()); - + Log::error('Report card PDF generation failed: ' . $e->getMessage()); return $this->error('Failed to generate report card.', Response::HTTP_INTERNAL_SERVER_ERROR); } - if (! ($result['ok'] ?? false)) { + if (!($result['ok'] ?? false)) { return $this->error($result['message'] ?? 'Unable to generate report card.', Response::HTTP_UNPROCESSABLE_ENTITY); } @@ -114,7 +113,7 @@ class ReportCardsController extends BaseApiController return response($result['pdf'], 200, [ 'Content-Type' => 'application/pdf', - 'Content-Disposition' => $disposition.'; filename="'.($result['filename'] ?? 'ReportCard.pdf').'"', + 'Content-Disposition' => $disposition . '; filename="' . ($result['filename'] ?? 'ReportCard.pdf') . '"', 'Cache-Control' => 'private, max-age=0, must-revalidate', 'Pragma' => 'public', ]); @@ -125,12 +124,11 @@ class ReportCardsController extends BaseApiController try { $result = $this->service->generateClassReport($classSectionId, $request->validated()); } catch (\Throwable $e) { - Log::error('Class report card PDF generation failed: '.$e->getMessage()); - + Log::error('Class report card PDF generation failed: ' . $e->getMessage()); return $this->error('Failed to generate class report cards.', Response::HTTP_INTERNAL_SERVER_ERROR); } - if (! ($result['ok'] ?? false)) { + if (!($result['ok'] ?? false)) { return $this->error($result['message'] ?? 'Unable to generate class report cards.', Response::HTTP_UNPROCESSABLE_ENTITY); } @@ -138,7 +136,7 @@ class ReportCardsController extends BaseApiController return response($result['pdf'], 200, [ 'Content-Type' => 'application/pdf', - 'Content-Disposition' => $disposition.'; filename="'.($result['filename'] ?? 'ClassReportCards.pdf').'"', + 'Content-Disposition' => $disposition . '; filename="' . ($result['filename'] ?? 'ClassReportCards.pdf') . '"', 'Cache-Control' => 'private, max-age=0, must-revalidate', 'Pragma' => 'public', ]); @@ -150,7 +148,6 @@ class ReportCardsController extends BaseApiController if ($value !== '') { return $value; } - return trim((string) (Configuration::getConfig('school_year') ?? '')); } @@ -160,7 +157,6 @@ class ReportCardsController extends BaseApiController if ($value !== '') { return $value; } - return trim((string) (Configuration::getConfig('semester') ?? '')); } } diff --git a/app/Http/Controllers/Api/Reports/SlipPrinterController.php b/app/Http/Controllers/Api/Reports/SlipPrinterController.php index b285b9f7..44b3685e 100644 --- a/app/Http/Controllers/Api/Reports/SlipPrinterController.php +++ b/app/Http/Controllers/Api/Reports/SlipPrinterController.php @@ -27,13 +27,13 @@ class SlipPrinterController extends BaseApiController $result = $this->service->print($request->validated(), $guard); - if (! $result['ok']) { + if (!$result['ok']) { return response()->json(['ok' => false, 'message' => $result['message'] ?? 'Print failed.'], 422); } return response($result['pdf'], 200, [ 'Content-Type' => 'application/pdf', - 'Content-Disposition' => 'inline; filename="'.$result['filename'].'"', + 'Content-Disposition' => 'inline; filename="' . $result['filename'] . '"', ]); } @@ -78,16 +78,19 @@ class SlipPrinterController extends BaseApiController $result = $this->service->reprint((int) $request->validated()['id'], $guard); - if (! $result['ok']) { + if (!$result['ok']) { return response()->json(['ok' => false, 'message' => $result['message'] ?? 'Slip not found.'], 404); } return response($result['pdf'], 200, [ 'Content-Type' => 'application/pdf', - 'Content-Disposition' => 'inline; filename="'.$result['filename'].'"', + 'Content-Disposition' => 'inline; filename="' . $result['filename'] . '"', ]); } + /** + * @return int|JsonResponse + */ private function authenticatedUserIdOrUnauthorized(): int|JsonResponse { $userId = (int) (auth()->id() ?? 0); diff --git a/app/Http/Controllers/Api/Reports/StickersController.php b/app/Http/Controllers/Api/Reports/StickersController.php index 8cfa11d2..a5129532 100644 --- a/app/Http/Controllers/Api/Reports/StickersController.php +++ b/app/Http/Controllers/Api/Reports/StickersController.php @@ -64,18 +64,17 @@ class StickersController extends BaseApiController try { $result = $this->printService->generate($request->validated()); } catch (\Throwable $e) { - Log::error('Sticker print failed: '.$e->getMessage()); - + Log::error('Sticker print failed: ' . $e->getMessage()); return $this->error('Failed to generate stickers.', Response::HTTP_INTERNAL_SERVER_ERROR); } - if (! ($result['ok'] ?? false)) { + if (!($result['ok'] ?? false)) { return $this->error($result['message'] ?? 'Unable to generate stickers.', Response::HTTP_UNPROCESSABLE_ENTITY); } return response($result['pdf'], 200, [ 'Content-Type' => 'application/pdf', - 'Content-Disposition' => 'inline; filename="'.($result['filename'] ?? 'Student_Stickers.pdf').'"', + 'Content-Disposition' => 'inline; filename="' . ($result['filename'] ?? 'Student_Stickers.pdf') . '"', ]); } } diff --git a/app/Http/Controllers/Api/RolePermissionController.php b/app/Http/Controllers/Api/RolePermissionController.php index 0013504e..4b331fd4 100644 --- a/app/Http/Controllers/Api/RolePermissionController.php +++ b/app/Http/Controllers/Api/RolePermissionController.php @@ -55,7 +55,7 @@ class RolePermissionController extends BaseApiController public function showRole(int $roleId): JsonResponse { $role = Role::query()->find($roleId); - if (! $role) { + if (!$role) { return $this->error('Role not found.', Response::HTTP_NOT_FOUND); } @@ -69,8 +69,7 @@ class RolePermissionController extends BaseApiController try { $role = $this->roleCrudService->create($request->validated()); } catch (\Throwable $e) { - Log::error('Role store failed: '.$e->getMessage()); - + Log::error('Role store failed: ' . $e->getMessage()); return $this->error('Failed to create role.', Response::HTTP_INTERNAL_SERVER_ERROR); } @@ -82,15 +81,14 @@ class RolePermissionController extends BaseApiController public function updateRole(RoleUpdateRequest $request, int $roleId): JsonResponse { $role = Role::query()->find($roleId); - if (! $role) { + if (!$role) { return $this->error('Role not found.', Response::HTTP_NOT_FOUND); } try { $role = $this->roleCrudService->update($role, $request->validated()); } catch (\Throwable $e) { - Log::error('Role update failed: '.$e->getMessage()); - + Log::error('Role update failed: ' . $e->getMessage()); return $this->error('Failed to update role.', Response::HTTP_INTERNAL_SERVER_ERROR); } @@ -102,15 +100,14 @@ class RolePermissionController extends BaseApiController public function deleteRole(int $roleId): JsonResponse { $role = Role::query()->find($roleId); - if (! $role) { + if (!$role) { return $this->error('Role not found.', Response::HTTP_NOT_FOUND); } try { $this->roleCrudService->delete($role); } catch (\Throwable $e) { - Log::error('Role delete failed: '.$e->getMessage()); - + Log::error('Role delete failed: ' . $e->getMessage()); return $this->error('Failed to delete role.', Response::HTTP_INTERNAL_SERVER_ERROR); } @@ -141,12 +138,11 @@ class RolePermissionController extends BaseApiController (int) (auth()->id() ?? 0) ); } catch (\Throwable $e) { - Log::error('Assign roles failed: '.$e->getMessage()); - + Log::error('Assign roles failed: ' . $e->getMessage()); return $this->error('Failed to update roles.', Response::HTTP_INTERNAL_SERVER_ERROR); } - if (! ($result['ok'] ?? false)) { + if (!($result['ok'] ?? false)) { return $this->error($result['message'] ?? 'Failed to update roles.', Response::HTTP_UNPROCESSABLE_ENTITY); } @@ -167,7 +163,7 @@ class RolePermissionController extends BaseApiController public function showPermission(int $permissionId): JsonResponse { $permission = Permission::query()->find($permissionId); - if (! $permission) { + if (!$permission) { return $this->error('Permission not found.', Response::HTTP_NOT_FOUND); } @@ -181,8 +177,7 @@ class RolePermissionController extends BaseApiController try { $permission = $this->permissionCrudService->create($request->validated()); } catch (\Throwable $e) { - Log::error('Permission store failed: '.$e->getMessage()); - + Log::error('Permission store failed: ' . $e->getMessage()); return $this->error('Failed to create permission.', Response::HTTP_INTERNAL_SERVER_ERROR); } @@ -194,15 +189,14 @@ class RolePermissionController extends BaseApiController public function updatePermission(PermissionUpdateRequest $request, int $permissionId): JsonResponse { $permission = Permission::query()->find($permissionId); - if (! $permission) { + if (!$permission) { return $this->error('Permission not found.', Response::HTTP_NOT_FOUND); } try { $permission = $this->permissionCrudService->update($permission, $request->validated()); } catch (\Throwable $e) { - Log::error('Permission update failed: '.$e->getMessage()); - + Log::error('Permission update failed: ' . $e->getMessage()); return $this->error('Failed to update permission.', Response::HTTP_INTERNAL_SERVER_ERROR); } @@ -214,15 +208,14 @@ class RolePermissionController extends BaseApiController public function deletePermission(int $permissionId): JsonResponse { $permission = Permission::query()->find($permissionId); - if (! $permission) { + if (!$permission) { return $this->error('Permission not found.', Response::HTTP_NOT_FOUND); } try { $this->permissionCrudService->delete($permission); } catch (\Throwable $e) { - Log::error('Permission delete failed: '.$e->getMessage()); - + Log::error('Permission delete failed: ' . $e->getMessage()); return $this->error('Failed to delete permission.', Response::HTTP_INTERNAL_SERVER_ERROR); } @@ -243,8 +236,7 @@ class RolePermissionController extends BaseApiController try { $this->permissionService->saveRolePermissions($roleId, $request->validated()['permissions'] ?? []); } catch (\Throwable $e) { - Log::error('Role permissions update failed: '.$e->getMessage()); - + Log::error('Role permissions update failed: ' . $e->getMessage()); return $this->error('Failed to update role permissions.', Response::HTTP_INTERNAL_SERVER_ERROR); } diff --git a/app/Http/Controllers/Api/RoleSwitcherController.php b/app/Http/Controllers/Api/RoleSwitcherController.php index 0758dae3..2f22e748 100644 --- a/app/Http/Controllers/Api/RoleSwitcherController.php +++ b/app/Http/Controllers/Api/RoleSwitcherController.php @@ -5,9 +5,9 @@ namespace App\Http\Controllers\Api; use App\Http\Requests\Roles\RoleSwitchRequest; use App\Services\Roles\RoleSwitchService; use Illuminate\Http\JsonResponse; -use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +use Symfony\Component\HttpFoundation\Response; class RoleSwitcherController extends BaseApiController { diff --git a/app/Http/Controllers/Api/ScannerController.php b/app/Http/Controllers/Api/ScannerController.php index 26dde969..d8d3264d 100644 --- a/app/Http/Controllers/Api/ScannerController.php +++ b/app/Http/Controllers/Api/ScannerController.php @@ -23,13 +23,12 @@ use Illuminate\Support\Facades\Schema; class ScannerController extends BaseApiController { private const LATE_CUTOFF_CONFIG_KEY = 'attendance_late_cutoff_time'; - private const DEFAULT_LATE_CUTOFF = '10:00 AM'; public function process(Request $request): JsonResponse { $data = $request->json()->all(); - if (! is_array($data) || $data === []) { + if (!is_array($data) || $data === []) { $data = $request->all(); } @@ -42,11 +41,10 @@ class ScannerController extends BaseApiController if ($badgeCode === '') { $this->logScan($badgeCode, null, null, $action, 'rejected', $operatorId, $stationId, 'Badge code is required.', $scannedAt); - return response()->json($this->failure('Badge code is required.'), 400); } - if (! in_array($scanStatus, ['present', 'late'], true)) { + if (!in_array($scanStatus, ['present', 'late'], true)) { $scanStatus = 'present'; } @@ -61,7 +59,6 @@ class ScannerController extends BaseApiController } $this->logScan($badgeCode, null, null, $action, 'not_found', $operatorId, $stationId, 'Badge not found.', $scannedAt); - return response()->json($this->failure('Badge not found.'), 404); } @@ -77,9 +74,8 @@ class ScannerController extends BaseApiController } $assignment = $this->studentAssignment((int) $student->id, $term['semester'], $term['school_year']); - if (! $assignment) { + if (!$assignment) { $this->logScan($badgeCode, 'student', (int) $student->id, $action, 'rejected', $operatorId, $stationId, 'Student has no class assignment for current term.', $scannedAt); - return response()->json($this->failure('Student has no class assignment for current term.'), 409); } @@ -154,7 +150,7 @@ class ScannerController extends BaseApiController $operatorId, $existing ? (string) ($oldStatus ?? '') : null ); - $warnings[] = 'Student arrived after '.$lateCutoff->format('h:i A').'. Late slip receipt generated.'; + $warnings[] = 'Student arrived after ' . $lateCutoff->format('h:i A') . '. Late slip receipt generated.'; } return response()->json($this->successPayload([ @@ -198,7 +194,7 @@ class ScannerController extends BaseApiController $person = [ 'id' => (int) $user->id, - 'name' => trim((string) ($user->firstname ?? '').' '.(string) ($user->lastname ?? '')), + 'name' => trim((string) ($user->firstname ?? '') . ' ' . (string) ($user->lastname ?? '')), 'email' => $user->email, 'role' => $roleName, 'badgeCode' => $badgeCode, @@ -242,7 +238,7 @@ class ScannerController extends BaseApiController }) ->first(); - if (! $user || ! $this->isStaffUser((int) $user->id)) { + if (!$user || !$this->isStaffUser((int) $user->id)) { return null; } @@ -259,7 +255,7 @@ class ScannerController extends BaseApiController foreach ($roles as $role) { $name = trim((string) ($role->role_name ?? '')); - if ($name !== '' && ! in_array($name, ['parent', 'guest', 'student'], true)) { + if ($name !== '' && !in_array($name, ['parent', 'guest', 'student'], true)) { return true; } } @@ -291,7 +287,7 @@ class ScannerController extends BaseApiController } $row = $builder->orderByDesc('sc.id')->first(); - if (! $row && Schema::hasColumn('student_class', 'school_year')) { + if (!$row && Schema::hasColumn('student_class', 'school_year')) { $row = DB::table('student_class as sc') ->select('sc.class_section_id', 'cs.class_id', 'cs.class_section_name') ->leftJoin('classSection as cs', 'cs.class_section_id', '=', 'sc.class_section_id') @@ -333,9 +329,8 @@ class ScannerController extends BaseApiController ->where('school_year', $schoolYear) ->first(); - if (! $record) { + if (!$record) { $this->addAttendanceRecord($classSectionId, $studentId, $schoolId, $newStatus, $semester, $schoolYear, $operatorId); - return; } @@ -388,7 +383,6 @@ class ScannerController extends BaseApiController 'updated_at' => utc_now(), ]); $record->save(); - return; } @@ -430,13 +424,13 @@ class ScannerController extends BaseApiController return [ 'id' => (int) ($student['id'] ?? 0), 'schoolId' => $student['school_id'] ?? null, - 'name' => trim((string) ($student['firstname'] ?? '').' '.(string) ($student['lastname'] ?? '')), + 'name' => trim((string) ($student['firstname'] ?? '') . ' ' . (string) ($student['lastname'] ?? '')), 'firstName' => $student['firstname'] ?? null, 'lastName' => $student['lastname'] ?? null, 'grade' => $student['registration_grade'] ?? null, 'classSectionId' => isset($assignment['class_section_id']) ? (int) $assignment['class_section_id'] : null, 'classSectionName' => $assignment['class_section_name'] ?? null, - 'badgeCode' => ! empty($student['rfid_tag']) ? $student['rfid_tag'] : ($student['school_id'] ?? null), + 'badgeCode' => !empty($student['rfid_tag']) ? $student['rfid_tag'] : ($student['school_id'] ?? null), ]; } @@ -533,7 +527,7 @@ class ScannerController extends BaseApiController $outstandingBalance = 0.0; $paymentStatus = 'Unknown'; - if (! empty($payments)) { + if (!empty($payments)) { $latest = $payments[0]; $paymentStatus = (string) ($latest['status'] ?? 'Unknown'); $outstandingBalance = (float) ($latest['balance'] ?? 0); @@ -556,7 +550,7 @@ class ScannerController extends BaseApiController { $issues = []; $payments = $summary['payments'] ?? []; - if (! empty($payments)) { + if (!empty($payments)) { $latest = $payments[0]; $balance = (float) ($latest['balance'] ?? 0); $status = strtolower(trim((string) ($latest['status'] ?? ''))); @@ -565,9 +559,9 @@ class ScannerController extends BaseApiController 'category' => 'payment', 'severity' => 'critical', 'title' => 'Payment balance due', - 'detail' => '$'.number_format($balance, 2).' outstanding', + 'detail' => '$' . number_format($balance, 2) . ' outstanding', ]; - } elseif ($status !== '' && ! in_array($status, ['paid', 'full', 'full payment', 'complete', 'completed'], true)) { + } elseif ($status !== '' && !in_array($status, ['paid', 'full', 'full payment', 'complete', 'completed'], true)) { $issues[] = [ 'category' => 'payment', 'severity' => 'warning', @@ -584,7 +578,7 @@ class ScannerController extends BaseApiController 'category' => 'grades', 'severity' => (float) $semesterScore < 60 ? 'critical' : 'warning', 'title' => 'Grade concern', - 'detail' => 'Semester score '.number_format((float) $semesterScore, 1), + 'detail' => 'Semester score ' . number_format((float) $semesterScore, 1), ]; } @@ -593,7 +587,7 @@ class ScannerController extends BaseApiController $issues[] = [ 'category' => 'grades', 'severity' => 'warning', - 'title' => $label.' average below target', + 'title' => $label . ' average below target', 'detail' => number_format((float) $grading[$key], 1), ]; } @@ -605,7 +599,7 @@ class ScannerController extends BaseApiController 'category' => 'attendance', 'severity' => 'warning', 'title' => 'Repeated lateness', - 'detail' => (int) $attendance['late'].' late attendance records', + 'detail' => (int) $attendance['late'] . ' late attendance records', ]; } @@ -653,7 +647,7 @@ class ScannerController extends BaseApiController { $label = trim(str_replace('_', ' ', $flagType)); if ($label !== '') { - return ucwords($label).' flag'; + return ucwords($label) . ' flag'; } return match ($category) { @@ -685,10 +679,10 @@ class ScannerController extends BaseApiController private function createLateSlipPayload(array $student, array $assignment, array $term, \DateTimeImmutable $schoolNow, \DateTimeImmutable $lateCutoff, int $operatorId, ?string $previousStatus): array { - $studentName = trim((string) ($student['firstname'] ?? '').' '.(string) ($student['lastname'] ?? '')); + $studentName = trim((string) ($student['firstname'] ?? '') . ' ' . (string) ($student['lastname'] ?? '')); $grade = (string) ($student['registration_grade'] ?? $assignment['class_section_name'] ?? ''); $cutoffLabel = $lateCutoff->format('h:i A'); - $reason = 'Arrived after '.$cutoffLabel; + $reason = 'Arrived after ' . $cutoffLabel; $adminName = $this->operatorName($operatorId); $slip = [ 'school_year' => $term['school_year'], @@ -720,7 +714,7 @@ class ScannerController extends BaseApiController $logId = (int) ($created->id ?? 0); $logged = $logId > 0; } catch (\Throwable $e) { - Log::error('Badge scan late slip log failed: '.$e->getMessage()); + Log::error('Badge scan late slip log failed: ' . $e->getMessage()); } } @@ -741,15 +735,15 @@ class ScannerController extends BaseApiController 'reason' => $reason, 'adminName' => $adminName, 'cutoffTime' => $cutoffLabel, - 'message' => 'Late slip receipt generated for arrival after '.$cutoffLabel.'.', + 'message' => 'Late slip receipt generated for arrival after ' . $cutoffLabel . '.', 'text' => implode("\n", [ 'Al Rahma School at ISGL', - 'School Year: '.$slip['school_year'], - 'Student Name: '.$studentName, - 'Date: '.$displayDate.' Time In: '.$displayTime, - 'Grade: '.$grade, - 'Reason: '.$reason, - 'Admin: '.$adminName, + 'School Year: ' . $slip['school_year'], + 'Student Name: ' . $studentName, + 'Date: ' . $displayDate . ' Time In: ' . $displayTime, + 'Grade: ' . $grade, + 'Reason: ' . $reason, + 'Admin: ' . $adminName, ]), ]; } @@ -766,19 +760,18 @@ class ScannerController extends BaseApiController $date = $schoolNow->format('Y-m-d'); foreach (['H:i:s', 'H:i', 'g:i A', 'h:i A', 'g:i a', 'h:i a'] as $format) { - $parsed = \DateTimeImmutable::createFromFormat('!'.$format, $configured, $timezone); + $parsed = \DateTimeImmutable::createFromFormat('!' . $format, $configured, $timezone); if ($parsed instanceof \DateTimeImmutable) { return $schoolNow->setTime((int) $parsed->format('H'), (int) $parsed->format('i'), (int) $parsed->format('s')); } } - $timestamp = strtotime($date.' '.$configured); + $timestamp = strtotime($date . ' ' . $configured); if ($timestamp !== false) { - return (new \DateTimeImmutable('@'.$timestamp))->setTimezone($timezone); + return (new \DateTimeImmutable('@' . $timestamp))->setTimezone($timezone); } - Log::warning(self::LATE_CUTOFF_CONFIG_KEY.' has invalid value "'.$configured.'"; using '.self::DEFAULT_LATE_CUTOFF); - + Log::warning(self::LATE_CUTOFF_CONFIG_KEY . ' has invalid value "' . $configured . '"; using ' . self::DEFAULT_LATE_CUTOFF); return $schoolNow->setTime(10, 0, 0); } @@ -787,7 +780,6 @@ class ScannerController extends BaseApiController $timezone = new \DateTimeZone('America/New_York'); try { $utc = new \DateTimeZone('UTC'); - return (new \DateTimeImmutable($scannedAt, $utc))->setTimezone($timezone); } catch (\Throwable) { return new \DateTimeImmutable('now', $timezone); @@ -800,7 +792,7 @@ class ScannerController extends BaseApiController try { $user = User::query()->select('firstname', 'lastname', 'email')->find($operatorId); if ($user) { - $name = trim((string) ($user->firstname ?? '').' '.(string) ($user->lastname ?? '')); + $name = trim((string) ($user->firstname ?? '') . ' ' . (string) ($user->lastname ?? '')); if ($name !== '') { return $name; } @@ -808,7 +800,7 @@ class ScannerController extends BaseApiController return (string) ($user->email ?? ''); } } catch (\Throwable $e) { - Log::error('Badge scan operator lookup failed: '.$e->getMessage()); + Log::error('Badge scan operator lookup failed: ' . $e->getMessage()); } } @@ -841,7 +833,7 @@ class ScannerController extends BaseApiController private function logScan(string $badgeCode, ?string $personType, ?int $personId, string $action, string $status, int $operatorId, string $stationId, string $message, string $scannedAt): void { try { - if (! Schema::hasTable('badge_scan_logs')) { + if (!Schema::hasTable('badge_scan_logs')) { return; } @@ -858,7 +850,7 @@ class ScannerController extends BaseApiController 'created_at' => utc_now(), ]); } catch (\Throwable $e) { - Log::error('Badge scan log failed: '.$e->getMessage()); + Log::error('Badge scan log failed: ' . $e->getMessage()); } } diff --git a/app/Http/Controllers/Api/Scores/FinalController.php b/app/Http/Controllers/Api/Scores/FinalController.php index 14812192..4b9b705c 100644 --- a/app/Http/Controllers/Api/Scores/FinalController.php +++ b/app/Http/Controllers/Api/Scores/FinalController.php @@ -135,6 +135,9 @@ class FinalController extends BaseApiController } } + /** + * @return int|JsonResponse + */ private function authenticatedUserIdOrUnauthorized(): int|JsonResponse { $userId = (int) (auth()->id() ?? 0); diff --git a/app/Http/Controllers/Api/Scores/HomeworkController.php b/app/Http/Controllers/Api/Scores/HomeworkController.php index 8c44ab69..f38e491c 100644 --- a/app/Http/Controllers/Api/Scores/HomeworkController.php +++ b/app/Http/Controllers/Api/Scores/HomeworkController.php @@ -158,6 +158,9 @@ class HomeworkController extends BaseApiController } } + /** + * @return int|JsonResponse + */ private function authenticatedUserIdOrUnauthorized(): int|JsonResponse { $userId = (int) (auth()->id() ?? 0); diff --git a/app/Http/Controllers/Api/Scores/MidtermController.php b/app/Http/Controllers/Api/Scores/MidtermController.php index a88d13b7..8ddcb304 100644 --- a/app/Http/Controllers/Api/Scores/MidtermController.php +++ b/app/Http/Controllers/Api/Scores/MidtermController.php @@ -135,6 +135,9 @@ class MidtermController extends BaseApiController } } + /** + * @return int|JsonResponse + */ private function authenticatedUserIdOrUnauthorized(): int|JsonResponse { $userId = (int) (auth()->id() ?? 0); diff --git a/app/Http/Controllers/Api/Scores/ParticipationController.php b/app/Http/Controllers/Api/Scores/ParticipationController.php index fa5ed8d2..98b18796 100644 --- a/app/Http/Controllers/Api/Scores/ParticipationController.php +++ b/app/Http/Controllers/Api/Scores/ParticipationController.php @@ -138,6 +138,9 @@ class ParticipationController extends BaseApiController } } + /** + * @return int|JsonResponse + */ private function authenticatedUserIdOrUnauthorized(): int|JsonResponse { $userId = (int) (auth()->id() ?? 0); diff --git a/app/Http/Controllers/Api/Scores/ProjectController.php b/app/Http/Controllers/Api/Scores/ProjectController.php index 85566cdc..9954763f 100644 --- a/app/Http/Controllers/Api/Scores/ProjectController.php +++ b/app/Http/Controllers/Api/Scores/ProjectController.php @@ -159,6 +159,9 @@ class ProjectController extends BaseApiController } } + /** + * @return int|JsonResponse + */ private function authenticatedUserIdOrUnauthorized(): int|JsonResponse { $userId = (int) (auth()->id() ?? 0); diff --git a/app/Http/Controllers/Api/Scores/QuizController.php b/app/Http/Controllers/Api/Scores/QuizController.php index 4b979b65..2a17f95d 100644 --- a/app/Http/Controllers/Api/Scores/QuizController.php +++ b/app/Http/Controllers/Api/Scores/QuizController.php @@ -158,6 +158,9 @@ class QuizController extends BaseApiController } } + /** + * @return int|JsonResponse + */ private function authenticatedUserIdOrUnauthorized(): int|JsonResponse { $userId = (int) (auth()->id() ?? 0); diff --git a/app/Http/Controllers/Api/Scores/ScoreCommentController.php b/app/Http/Controllers/Api/Scores/ScoreCommentController.php index ad2e3a82..24a1c873 100644 --- a/app/Http/Controllers/Api/Scores/ScoreCommentController.php +++ b/app/Http/Controllers/Api/Scores/ScoreCommentController.php @@ -88,7 +88,7 @@ class ScoreCommentController extends BaseApiController $userId ); - if (! empty($result['errors'])) { + if (!empty($result['errors'])) { return response()->json(['ok' => false, 'errors' => $result['errors']], 422); } @@ -112,13 +112,16 @@ class ScoreCommentController extends BaseApiController $userId ); - if (! empty($result['errors'])) { + if (!empty($result['errors'])) { return response()->json(['ok' => false, 'errors' => $result['errors']], 422); } return response()->json(['ok' => true]); } + /** + * @return int|JsonResponse + */ private function authenticatedUserIdOrUnauthorized(): int|JsonResponse { $userId = (int) (auth()->id() ?? 0); diff --git a/app/Http/Controllers/Api/Scores/ScoreController.php b/app/Http/Controllers/Api/Scores/ScoreController.php index 825bd21c..c8b465a2 100644 --- a/app/Http/Controllers/Api/Scores/ScoreController.php +++ b/app/Http/Controllers/Api/Scores/ScoreController.php @@ -81,6 +81,9 @@ class ScoreController extends BaseApiController return response()->json(['ok' => true] + $data); } + /** + * @return int|JsonResponse + */ private function authenticatedUserIdOrUnauthorized(): int|JsonResponse { $userId = (int) (request()->user()?->id ?? auth()->id() ?? 0); diff --git a/app/Http/Controllers/Api/Settings/ConfigurationAdminController.php b/app/Http/Controllers/Api/Settings/ConfigurationAdminController.php index 62b1ef1b..b95add77 100644 --- a/app/Http/Controllers/Api/Settings/ConfigurationAdminController.php +++ b/app/Http/Controllers/Api/Settings/ConfigurationAdminController.php @@ -39,7 +39,7 @@ class ConfigurationAdminController extends BaseApiController public function update(ConfigurationUpdateRequest $request, int $id): JsonResponse { $config = $this->service->update($id, $request->validated()); - if (! $config) { + if (!$config) { return response()->json(['ok' => false, 'message' => 'Configuration not found.'], 404); } @@ -52,7 +52,7 @@ class ConfigurationAdminController extends BaseApiController public function destroy(int $id): JsonResponse { $deleted = $this->service->delete($id); - if (! $deleted) { + if (!$deleted) { return response()->json(['ok' => false, 'message' => 'Configuration not found.'], 404); } @@ -62,7 +62,7 @@ class ConfigurationAdminController extends BaseApiController public function getByKey(string $configKey): JsonResponse { $config = $this->service->getByKey($configKey); - if (! $config) { + if (!$config) { return response()->json(['ok' => false, 'message' => 'Configuration not found.'], 404); } diff --git a/app/Http/Controllers/Api/Settings/EventController.php b/app/Http/Controllers/Api/Settings/EventController.php index 6bbb4f35..f245ccf8 100644 --- a/app/Http/Controllers/Api/Settings/EventController.php +++ b/app/Http/Controllers/Api/Settings/EventController.php @@ -50,7 +50,7 @@ class EventController extends BaseApiController public function show(int $eventId): JsonResponse { $event = $this->listService->find($eventId); - if (! $event) { + if (!$event) { return response()->json(['ok' => false, 'message' => 'Event not found.'], 404); } @@ -73,7 +73,6 @@ class EventController extends BaseApiController $result = $this->managementService->create($payload, $request->file('flyer'), $guard); } catch (\Throwable $e) { Log::error('Event creation failed.', ['error' => $e->getMessage()]); - return response()->json(['ok' => false, 'message' => 'Failed to create event.'], 500); } @@ -177,6 +176,9 @@ class EventController extends BaseApiController ]); } + /** + * @return int|JsonResponse + */ private function authenticatedUserIdOrUnauthorized(): int|JsonResponse { $userId = (int) (auth()->id() ?? 0); diff --git a/app/Http/Controllers/Api/Settings/PreferencesController.php b/app/Http/Controllers/Api/Settings/PreferencesController.php index 633414bb..2a93b8c2 100644 --- a/app/Http/Controllers/Api/Settings/PreferencesController.php +++ b/app/Http/Controllers/Api/Settings/PreferencesController.php @@ -58,7 +58,7 @@ class PreferencesController extends BaseApiController public function showForUser(int $userId): JsonResponse { $row = Preferences::query()->where('user_id', $userId)->first(); - if (! $row) { + if (!$row) { return $this->error('Preferences not found.', Response::HTTP_NOT_FOUND); } @@ -82,8 +82,7 @@ class PreferencesController extends BaseApiController try { $saved = $this->commandService->upsert($guard, $request->validated()); } catch (\Throwable $e) { - Log::error('Preferences save failed: '.$e->getMessage()); - + Log::error('Preferences save failed: ' . $e->getMessage()); return $this->error('Unable to save preferences.', Response::HTTP_INTERNAL_SERVER_ERROR); } @@ -104,8 +103,7 @@ class PreferencesController extends BaseApiController try { $saved = $this->commandService->upsert($userId, $request->validated()); } catch (\Throwable $e) { - Log::error('Preferences update failed: '.$e->getMessage()); - + Log::error('Preferences update failed: ' . $e->getMessage()); return $this->error('Unable to save preferences.', Response::HTTP_INTERNAL_SERVER_ERROR); } @@ -119,7 +117,7 @@ class PreferencesController extends BaseApiController public function destroy(int $userId): JsonResponse { $row = Preferences::query()->where('user_id', $userId)->first(); - if (! $row) { + if (!$row) { return $this->error('Preferences not found.', Response::HTTP_NOT_FOUND); } @@ -128,18 +126,20 @@ class PreferencesController extends BaseApiController try { $deleted = $this->commandService->delete($row); } catch (\Throwable $e) { - Log::error('Preferences delete failed: '.$e->getMessage()); - + Log::error('Preferences delete failed: ' . $e->getMessage()); return $this->error('Unable to delete preferences.', Response::HTTP_INTERNAL_SERVER_ERROR); } - if (! $deleted) { + if (!$deleted) { return $this->error('Unable to delete preferences.', Response::HTTP_INTERNAL_SERVER_ERROR); } return $this->success(null, 'Preferences deleted.'); } + /** + * @return int|JsonResponse + */ private function authenticatedUserIdOrUnauthorized(): int|JsonResponse { $userId = (int) (auth()->id() ?? 0); diff --git a/app/Http/Controllers/Api/Settings/SchoolCalendarController.php b/app/Http/Controllers/Api/Settings/SchoolCalendarController.php index 53eccb0a..d6cf29e8 100644 --- a/app/Http/Controllers/Api/Settings/SchoolCalendarController.php +++ b/app/Http/Controllers/Api/Settings/SchoolCalendarController.php @@ -99,7 +99,7 @@ class SchoolCalendarController extends BaseApiController public function show(int $eventId): JsonResponse { $event = $this->queryService->findEvent($eventId); - if (! $event) { + if (!$event) { return $this->error('Event not found.', Response::HTTP_NOT_FOUND); } @@ -116,8 +116,7 @@ class SchoolCalendarController extends BaseApiController try { $result = $this->mutationService->create($payload, $targets); } catch (\Throwable $e) { - Log::error('SchoolCalendar store failed: '.$e->getMessage()); - + Log::error('SchoolCalendar store failed: ' . $e->getMessage()); return $this->error('Failed to save event.', Response::HTTP_INTERNAL_SERVER_ERROR); } @@ -130,7 +129,7 @@ class SchoolCalendarController extends BaseApiController public function update(SchoolCalendarUpdateRequest $request, int $eventId): JsonResponse { $event = $this->queryService->findEvent($eventId); - if (! $event) { + if (!$event) { return $this->error('Event not found.', Response::HTTP_NOT_FOUND); } @@ -140,8 +139,7 @@ class SchoolCalendarController extends BaseApiController try { $result = $this->mutationService->update($event, $payload, $targets); } catch (\Throwable $e) { - Log::error('SchoolCalendar update failed: '.$e->getMessage()); - + Log::error('SchoolCalendar update failed: ' . $e->getMessage()); return $this->error('Failed to update event.', Response::HTTP_INTERNAL_SERVER_ERROR); } @@ -154,15 +152,14 @@ class SchoolCalendarController extends BaseApiController public function destroy(int $eventId): JsonResponse { $event = $this->queryService->findEvent($eventId); - if (! $event) { + if (!$event) { return $this->error('Event not found.', Response::HTTP_NOT_FOUND); } try { $this->mutationService->delete($event); } catch (\Throwable $e) { - Log::error('SchoolCalendar delete failed: '.$e->getMessage()); - + Log::error('SchoolCalendar delete failed: ' . $e->getMessage()); return $this->error('Failed to delete event.', Response::HTTP_INTERNAL_SERVER_ERROR); } @@ -172,12 +169,15 @@ class SchoolCalendarController extends BaseApiController private function extractEmailTargets(array $payload): array { return [ - 'parents' => ! empty($payload['send_email_parent']), - 'teachers' => ! empty($payload['send_email_teacher']), - 'admins' => ! empty($payload['send_email_admin']), + 'parents' => !empty($payload['send_email_parent']), + 'teachers' => !empty($payload['send_email_teacher']), + 'admins' => !empty($payload['send_email_admin']), ]; } + /** + * @return int|JsonResponse + */ private function authenticatedUserIdOrUnauthorized(): int|JsonResponse { $userId = (int) (auth()->id() ?? 0); diff --git a/app/Http/Controllers/Api/Settings/SettingsController.php b/app/Http/Controllers/Api/Settings/SettingsController.php index 396b9ae9..2c37f826 100644 --- a/app/Http/Controllers/Api/Settings/SettingsController.php +++ b/app/Http/Controllers/Api/Settings/SettingsController.php @@ -40,8 +40,7 @@ class SettingsController extends BaseApiController try { $updated = $this->settingsService->update($request->validated(), $guard); } catch (\Throwable $e) { - Log::error('Settings update failed: '.$e->getMessage()); - + Log::error('Settings update failed: ' . $e->getMessage()); return $this->error('Unable to update settings.', Response::HTTP_INTERNAL_SERVER_ERROR); } @@ -50,6 +49,9 @@ class SettingsController extends BaseApiController ], 'Settings updated.'); } + /** + * @return int|JsonResponse + */ private function authenticatedUserIdOrUnauthorized(): int|JsonResponse { $userId = (int) (auth()->id() ?? 0); diff --git a/app/Http/Controllers/Api/Staff/StaffController.php b/app/Http/Controllers/Api/Staff/StaffController.php index 38e53478..38418b0e 100644 --- a/app/Http/Controllers/Api/Staff/StaffController.php +++ b/app/Http/Controllers/Api/Staff/StaffController.php @@ -9,7 +9,6 @@ use App\Http\Requests\Staff\StaffUpdateRequest; use App\Http\Resources\Staff\StaffCollection; use App\Http\Resources\Staff\StaffResource; use App\Models\Staff; -use App\Models\User; use App\Services\Staff\StaffCommandService; use App\Services\Staff\StaffQueryService; use Illuminate\Http\JsonResponse; @@ -48,12 +47,12 @@ class StaffController extends BaseApiController public function show(int $id): JsonResponse { $staff = $this->queryService->find($id); - if (! $staff) { + if (!$staff) { return $this->error('Staff member not found.', Response::HTTP_NOT_FOUND); } $this->authorize('view', $staff); - $staff->school_id = User::getSchoolIdByUserId((int) ($staff->user_id ?? 0)); + $staff->school_id = \App\Models\User::getSchoolIdByUserId((int) ($staff->user_id ?? 0)); return $this->success([ 'staff' => new StaffResource($staff), @@ -67,8 +66,7 @@ class StaffController extends BaseApiController try { $staff = $this->commandService->create($request->validated()); } catch (\Throwable $e) { - Log::error('Staff create failed: '.$e->getMessage()); - + Log::error('Staff create failed: ' . $e->getMessage()); return $this->error($e->getMessage(), Response::HTTP_BAD_REQUEST); } @@ -80,7 +78,7 @@ class StaffController extends BaseApiController public function update(StaffUpdateRequest $request, int $id): JsonResponse { $staff = $this->queryService->find($id); - if (! $staff) { + if (!$staff) { return $this->error('Staff member not found.', Response::HTTP_NOT_FOUND); } @@ -89,8 +87,7 @@ class StaffController extends BaseApiController try { $updated = $this->commandService->update($staff, $request->validated()); } catch (\Throwable $e) { - Log::error('Staff update failed: '.$e->getMessage()); - + Log::error('Staff update failed: ' . $e->getMessage()); return $this->error('Unable to update staff.', Response::HTTP_INTERNAL_SERVER_ERROR); } @@ -102,7 +99,7 @@ class StaffController extends BaseApiController public function destroy(int $id): JsonResponse { $staff = $this->queryService->find($id); - if (! $staff) { + if (!$staff) { return $this->error('Staff member not found.', Response::HTTP_NOT_FOUND); } @@ -111,12 +108,11 @@ class StaffController extends BaseApiController try { $deleted = $this->commandService->delete($staff); } catch (\Throwable $e) { - Log::error('Staff delete failed: '.$e->getMessage()); - + Log::error('Staff delete failed: ' . $e->getMessage()); return $this->error('Unable to delete staff.', Response::HTTP_INTERNAL_SERVER_ERROR); } - if (! $deleted) { + if (!$deleted) { return $this->error('Unable to delete staff.', Response::HTTP_INTERNAL_SERVER_ERROR); } diff --git a/app/Http/Controllers/Api/Staff/TeacherController.php b/app/Http/Controllers/Api/Staff/TeacherController.php index fa761c7f..83c2ea10 100644 --- a/app/Http/Controllers/Api/Staff/TeacherController.php +++ b/app/Http/Controllers/Api/Staff/TeacherController.php @@ -124,12 +124,12 @@ class TeacherController extends BaseApiController private function teacherPayload(?array $teacher, int $userId): ?array { - if (! $teacher) { + if (!$teacher) { $model = User::query()->find($userId); $teacher = $model?->toArray(); } - if (! $teacher) { + if (!$teacher) { return null; } @@ -142,6 +142,9 @@ class TeacherController extends BaseApiController ]; } + /** + * @return int|JsonResponse + */ private function authenticatedUserIdOrUnauthorized(): int|JsonResponse { $userId = (int) (auth()->id() ?? 0); diff --git a/app/Http/Controllers/Api/StatsController.php b/app/Http/Controllers/Api/StatsController.php index e4aa3575..b243c89b 100644 --- a/app/Http/Controllers/Api/StatsController.php +++ b/app/Http/Controllers/Api/StatsController.php @@ -2,6 +2,7 @@ namespace App\Http\Controllers\Api; +use App\Http\Controllers\Api\BaseApiController; use App\Models\Stats; use Illuminate\Support\Facades\Log; use Symfony\Component\HttpFoundation\Response; @@ -12,11 +13,9 @@ class StatsController extends BaseApiController { try { $stats = Stats::query()->get()->toArray(); - return $this->success($stats, 'Statistics retrieved successfully'); } catch (\Throwable $e) { Log::error('Stats retrieval error: '.$e->getMessage()); - return $this->respondError('Failed to retrieve statistics', Response::HTTP_INTERNAL_SERVER_ERROR); } } diff --git a/app/Http/Controllers/Api/Students/StudentController.php b/app/Http/Controllers/Api/Students/StudentController.php index 3800bb6b..7f3d0e11 100644 --- a/app/Http/Controllers/Api/Students/StudentController.php +++ b/app/Http/Controllers/Api/Students/StudentController.php @@ -15,6 +15,7 @@ use App\Http\Resources\Students\StudentClassSectionResource; use App\Http\Resources\Students\StudentRemovedResource; use App\Http\Resources\Students\StudentScoreCardRowResource; use App\Models\AttendanceRecord; +use App\Models\ClassSection; use App\Models\EmergencyContact; use App\Models\Incident; use App\Models\SemesterScore; @@ -81,10 +82,10 @@ class StudentController extends BaseApiController $payload = $validator->validated(); $parentIds = []; - if (! empty($payload['parent_ids'])) { + if (!empty($payload['parent_ids'])) { $parentIds = array_values(array_unique(array_map('intval', $payload['parent_ids']))); } - if (! empty($payload['parent_id'])) { + if (!empty($payload['parent_id'])) { $parentIds[] = (int) $payload['parent_id']; } $parentIds = array_values(array_unique(array_filter($parentIds))); @@ -148,7 +149,7 @@ class StudentController extends BaseApiController public function show(int $studentId): JsonResponse { $student = Student::query()->find($studentId); - if (! $student) { + if (!$student) { return response()->json(['ok' => false, 'message' => 'Student not found.'], 404); } @@ -243,7 +244,7 @@ class StudentController extends BaseApiController if (array_key_exists('allergies', $payload)) { $healthPayload['allergies'] = $payload['allergies']; } - if (! empty($healthPayload)) { + if (!empty($healthPayload)) { $this->profileService->updateStudent($student->id, $healthPayload); } @@ -360,7 +361,7 @@ class StudentController extends BaseApiController public function destroy(int $studentId): JsonResponse { $student = Student::query()->find($studentId); - if (! $student) { + if (!$student) { return response()->json(['ok' => false, 'message' => 'Student not found.'], 404); } @@ -502,8 +503,8 @@ class StudentController extends BaseApiController $rows = AttendanceRecord::query() ->where('student_id', $studentId) - ->when(! empty($payload['school_year']), fn ($q) => $q->where('school_year', $payload['school_year'])) - ->when(! empty($payload['semester']), fn ($q) => $q->where('semester', $payload['semester'])) + ->when(!empty($payload['school_year']), fn ($q) => $q->where('school_year', $payload['school_year'])) + ->when(!empty($payload['semester']), fn ($q) => $q->where('semester', $payload['semester'])) ->orderBy('school_year', 'desc') ->orderBy('semester', 'desc') ->get() @@ -534,8 +535,8 @@ class StudentController extends BaseApiController $rows = Incident::query() ->where('student_id', $studentId) - ->when(! empty($payload['school_year']), fn ($q) => $q->where('school_year', $payload['school_year'])) - ->when(! empty($payload['semester']), fn ($q) => $q->where('semester', $payload['semester'])) + ->when(!empty($payload['school_year']), fn ($q) => $q->where('school_year', $payload['school_year'])) + ->when(!empty($payload['semester']), fn ($q) => $q->where('semester', $payload['semester'])) ->orderByDesc('incident_datetime') ->get() ->toArray(); @@ -565,8 +566,8 @@ class StudentController extends BaseApiController $rows = SemesterScore::query() ->where('student_id', $studentId) - ->when(! empty($payload['school_year']), fn ($q) => $q->where('school_year', $payload['school_year'])) - ->when(! empty($payload['semester']), fn ($q) => $q->where('semester', $payload['semester'])) + ->when(!empty($payload['school_year']), fn ($q) => $q->where('school_year', $payload['school_year'])) + ->when(!empty($payload['semester']), fn ($q) => $q->where('semester', $payload['semester'])) ->orderBy('school_year', 'desc') ->orderBy('semester', 'desc') ->get() @@ -591,12 +592,12 @@ class StudentController extends BaseApiController public function parents(int $studentId): JsonResponse { $student = Student::query()->find($studentId); - if (! $student) { + if (!$student) { return response()->json(['ok' => false, 'message' => 'Student not found.'], 404); } $parent = null; - if (! empty($student->parent_id)) { + if (!empty($student->parent_id)) { $parent = User::query() ->select(['id', 'firstname', 'lastname', 'email', 'cellphone']) ->find((int) $student->parent_id); @@ -612,7 +613,7 @@ class StudentController extends BaseApiController public function emergencyContacts(int $studentId): JsonResponse { $student = Student::query()->find($studentId); - if (! $student) { + if (!$student) { return response()->json(['ok' => false, 'message' => 'Student not found.'], 404); } @@ -620,7 +621,7 @@ class StudentController extends BaseApiController if (Schema::hasColumn('emergency_contacts', 'student_id')) { $query->where('student_id', $studentId); } - if (! empty($student->parent_id)) { + if (!empty($student->parent_id)) { $query->orWhere('parent_id', $student->parent_id); } @@ -639,7 +640,7 @@ class StudentController extends BaseApiController public function addEmergencyContact(Request $request, int $studentId): JsonResponse { $student = Student::query()->find($studentId); - if (! $student) { + if (!$student) { return response()->json(['ok' => false, 'message' => 'Student not found.'], 404); } @@ -682,7 +683,7 @@ class StudentController extends BaseApiController public function updateEmergencyContact(Request $request, int $studentId, int $contactId): JsonResponse { $student = Student::query()->find($studentId); - if (! $student) { + if (!$student) { return response()->json(['ok' => false, 'message' => 'Student not found.'], 404); } @@ -704,7 +705,7 @@ class StudentController extends BaseApiController } $contact = EmergencyContact::query()->find($contactId); - if (! $contact) { + if (!$contact) { return response()->json(['ok' => false, 'message' => 'Contact not found.'], 404); } @@ -735,7 +736,7 @@ class StudentController extends BaseApiController } $student = Student::query()->find($studentId); - if (! $student) { + if (!$student) { return response()->json(['ok' => false, 'message' => 'Student not found.'], 404); } @@ -758,14 +759,14 @@ class StudentController extends BaseApiController } $student = Student::query()->find($studentId); - if (! $student) { + if (!$student) { return response()->json(['ok' => false, 'message' => 'Student not found.'], 404); } $file = $request->file('photo'); $ext = $file->getClientOriginalExtension() ?: 'jpg'; - $path = "student-photos/{$studentId}.".strtolower($ext); - Storage::disk('public')->putFileAs('student-photos', $file, "{$studentId}.".strtolower($ext)); + $path = "student-photos/{$studentId}." . strtolower($ext); + Storage::disk('public')->putFileAs('student-photos', $file, "{$studentId}." . strtolower($ext)); return response()->json([ 'ok' => true, @@ -780,7 +781,7 @@ class StudentController extends BaseApiController $disk = Storage::disk('public'); foreach ($candidates as $ext) { - $path = $base.'.'.$ext; + $path = $base . '.' . $ext; if ($disk->exists($path)) { return response()->json([ 'ok' => true, @@ -795,14 +796,13 @@ class StudentController extends BaseApiController private function generateSchoolId(int $studentId): string { $year = (int) date('y'); - - return 'STU'.$year.str_pad((string) $studentId, 4, '0', STR_PAD_LEFT); + return 'STU' . $year . str_pad((string) $studentId, 4, '0', STR_PAD_LEFT); } public function scoreCard(int $studentId): JsonResponse { $result = $this->scoreCardService->scoreCard($studentId); - if (! $result['ok']) { + if (!$result['ok']) { return response()->json(['ok' => false, 'message' => $result['message'] ?? 'Not found.'], 404); } diff --git a/app/Http/Controllers/Api/Subjects/SubjectCurriculumController.php b/app/Http/Controllers/Api/Subjects/SubjectCurriculumController.php index 9602d637..d9681de7 100644 --- a/app/Http/Controllers/Api/Subjects/SubjectCurriculumController.php +++ b/app/Http/Controllers/Api/Subjects/SubjectCurriculumController.php @@ -35,7 +35,7 @@ class SubjectCurriculumController extends BaseApiController public function show(int $id): JsonResponse { $entry = SubjectCurriculum::query()->find($id); - if (! $entry) { + if (!$entry) { return response()->json(['ok' => false, 'message' => 'Curriculum entry not found.'], 404); } @@ -58,7 +58,7 @@ class SubjectCurriculumController extends BaseApiController public function update(SubjectCurriculumUpdateRequest $request, int $id): JsonResponse { $entry = $this->service->update($id, $request->validated()); - if (! $entry) { + if (!$entry) { return response()->json(['ok' => false, 'message' => 'Curriculum entry not found.'], 404); } @@ -71,7 +71,7 @@ class SubjectCurriculumController extends BaseApiController public function destroy(int $id): JsonResponse { $deleted = $this->service->delete($id); - if (! $deleted) { + if (!$deleted) { return response()->json(['ok' => false, 'message' => 'Curriculum entry not found.'], 404); } diff --git a/app/Http/Controllers/Api/Support/SupportController.php b/app/Http/Controllers/Api/Support/SupportController.php index 075a8fe2..beb0522d 100644 --- a/app/Http/Controllers/Api/Support/SupportController.php +++ b/app/Http/Controllers/Api/Support/SupportController.php @@ -64,13 +64,12 @@ class SupportController extends BaseApiController $payload = $request->validated(); $payload['user_email'] = $user->email ?? null; - $payload['user_name'] = trim(($user->firstname ?? '').' '.($user->lastname ?? '')); + $payload['user_name'] = trim(($user->firstname ?? '') . ' ' . ($user->lastname ?? '')); try { $result = $this->supportService->create((int) $user->id, $payload); } catch (\Throwable $e) { - Log::error('Support request failed: '.$e->getMessage()); - + Log::error('Support request failed: ' . $e->getMessage()); return $this->error('Unable to submit support request.', Response::HTTP_INTERNAL_SERVER_ERROR); } @@ -87,6 +86,9 @@ class SupportController extends BaseApiController ]); } + /** + * @return int|JsonResponse + */ private function authenticatedUserIdOrUnauthorized(): int|JsonResponse { $userId = (int) (auth()->id() ?? 0); diff --git a/app/Http/Controllers/Api/System/DashboardController.php b/app/Http/Controllers/Api/System/DashboardController.php index 5e4f9544..0acb1e99 100644 --- a/app/Http/Controllers/Api/System/DashboardController.php +++ b/app/Http/Controllers/Api/System/DashboardController.php @@ -18,7 +18,7 @@ class DashboardController extends BaseApiController public function route(): JsonResponse { $user = auth()->user(); - if (! $user) { + if (!$user) { return $this->error('Unauthorized.', Response::HTTP_UNAUTHORIZED); } diff --git a/app/Http/Controllers/Api/System/DashboardRedirectController.php b/app/Http/Controllers/Api/System/DashboardRedirectController.php index a4c1a75d..d4de52d8 100644 --- a/app/Http/Controllers/Api/System/DashboardRedirectController.php +++ b/app/Http/Controllers/Api/System/DashboardRedirectController.php @@ -15,12 +15,13 @@ class DashboardRedirectController extends Controller public function __construct( private DashboardRouteService $service, private ApplicationUrlService $urls, - ) {} + ) { + } public function dashboard(): RedirectResponse { $user = auth()->user(); - if (! $user) { + if (!$user) { return redirect()->guest($this->urls->webLoginUrl()); } diff --git a/app/Http/Controllers/Api/System/DatabaseHealthController.php b/app/Http/Controllers/Api/System/DatabaseHealthController.php index 70b9bfbd..92f9995f 100644 --- a/app/Http/Controllers/Api/System/DatabaseHealthController.php +++ b/app/Http/Controllers/Api/System/DatabaseHealthController.php @@ -17,7 +17,7 @@ class DatabaseHealthController extends BaseApiController public function index(): JsonResponse { $result = $this->healthService->check(); - if (! $result['ok']) { + if (!$result['ok']) { return $this->error('Database connection failed.', Response::HTTP_SERVICE_UNAVAILABLE, [ 'error' => $result['error'], ]); diff --git a/app/Http/Controllers/Api/System/NavBuilderController.php b/app/Http/Controllers/Api/System/NavBuilderController.php index 944bcfbf..eecb1ca7 100644 --- a/app/Http/Controllers/Api/System/NavBuilderController.php +++ b/app/Http/Controllers/Api/System/NavBuilderController.php @@ -8,8 +8,8 @@ use App\Http\Requests\System\NavItemStoreRequest; use App\Http\Resources\System\NavBuilderDataResource; use App\Http\Resources\System\NavItemResource; use App\Models\NavItem; -use App\Services\Navigation\NavbarService; use App\Services\Navigation\NavBuilderService; +use App\Services\Navigation\NavbarService; use Illuminate\Http\JsonResponse; use Illuminate\Support\Facades\Log; use Symfony\Component\HttpFoundation\Response; @@ -67,7 +67,7 @@ class NavBuilderController extends BaseApiController $deleted = $this->builderService->delete($id); - if (! $deleted) { + if (!$deleted) { return $this->error('Menu item not found.', Response::HTTP_NOT_FOUND); } @@ -81,8 +81,7 @@ class NavBuilderController extends BaseApiController try { $this->builderService->reorder($request->validated()['orders']); } catch (\Throwable $e) { - Log::error('Nav reorder failed: '.$e->getMessage()); - + Log::error('Nav reorder failed: ' . $e->getMessage()); return $this->error('Unable to reorder items.', Response::HTTP_INTERNAL_SERVER_ERROR); } diff --git a/app/Http/Controllers/Api/System/StatsController.php b/app/Http/Controllers/Api/System/StatsController.php index 1a884382..665550fb 100644 --- a/app/Http/Controllers/Api/System/StatsController.php +++ b/app/Http/Controllers/Api/System/StatsController.php @@ -13,11 +13,9 @@ class StatsController extends BaseApiController { try { $stats = Stats::query()->get()->toArray(); - return $this->success($stats, 'Statistics retrieved successfully'); } catch (\Throwable $e) { Log::error('Stats retrieval error: '.$e->getMessage()); - return $this->respondError('Failed to retrieve statistics', Response::HTTP_INTERNAL_SERVER_ERROR); } } diff --git a/app/Http/Controllers/Api/Ui/UiController.php b/app/Http/Controllers/Api/Ui/UiController.php index e0c5e405..9a3f177c 100644 --- a/app/Http/Controllers/Api/Ui/UiController.php +++ b/app/Http/Controllers/Api/Ui/UiController.php @@ -30,6 +30,9 @@ class UiController extends BaseApiController ], 'Style updated.'); } + /** + * @return int|JsonResponse + */ private function authenticatedUserIdOrUnauthorized(): int|JsonResponse { $userId = (int) (auth()->id() ?? 0); diff --git a/app/Http/Controllers/Api/Users/UserController.php b/app/Http/Controllers/Api/Users/UserController.php index f12d4d32..f477641a 100644 --- a/app/Http/Controllers/Api/Users/UserController.php +++ b/app/Http/Controllers/Api/Users/UserController.php @@ -151,6 +151,9 @@ class UserController extends BaseApiController ]); } + /** + * @return int|JsonResponse + */ private function authenticatedUserIdOrUnauthorized(): int|JsonResponse { $userId = (int) (auth()->id() ?? 0); diff --git a/app/Http/Controllers/Web/SchoolYearAdminPageController.php b/app/Http/Controllers/Web/SchoolYearAdminPageController.php index 7aac1b71..84c0714a 100644 --- a/app/Http/Controllers/Web/SchoolYearAdminPageController.php +++ b/app/Http/Controllers/Web/SchoolYearAdminPageController.php @@ -17,7 +17,8 @@ class SchoolYearAdminPageController extends Controller public function __construct( private SchoolYearClosureService $service, private SchoolYearResolver $resolver - ) {} + ) { + } public function show(Request $request): View { diff --git a/app/Http/Controllers/Web/TeacherCalendarPageController.php b/app/Http/Controllers/Web/TeacherCalendarPageController.php index e1555272..d9a9b807 100644 --- a/app/Http/Controllers/Web/TeacherCalendarPageController.php +++ b/app/Http/Controllers/Web/TeacherCalendarPageController.php @@ -16,7 +16,8 @@ class TeacherCalendarPageController extends Controller private SchoolCalendarQueryService $queryService, private SchoolCalendarMeetingService $meetingService, private SchoolCalendarFormatterService $formatterService, - ) {} + ) { + } public function show(): View { diff --git a/app/Http/Middleware/ApiDocsAuth.php b/app/Http/Middleware/ApiDocsAuth.php index 14ee1c02..21e7fae4 100644 --- a/app/Http/Middleware/ApiDocsAuth.php +++ b/app/Http/Middleware/ApiDocsAuth.php @@ -15,7 +15,7 @@ class ApiDocsAuth public function handle(Request $request, Closure $next): Response { $user = Auth::user(); - if (! $user) { + if (!$user) { $authHeader = (string) $request->header('Authorization'); if ($authHeader !== '' && stripos($authHeader, 'Bearer ') === 0) { $token = trim(substr($authHeader, 7)); @@ -34,11 +34,11 @@ class ApiDocsAuth } } - if (! $user) { + if (!$user) { return $this->deny('Unauthorized access to documentation.', 401); } - if (! $this->userIsAdmin((int) $user->id)) { + if (!$this->userIsAdmin((int) $user->id)) { return $this->deny('Admin privileges required.', 403); } diff --git a/app/Http/Middleware/ApiJwtAuth.php b/app/Http/Middleware/ApiJwtAuth.php index afcfaa48..f6f2d6c1 100644 --- a/app/Http/Middleware/ApiJwtAuth.php +++ b/app/Http/Middleware/ApiJwtAuth.php @@ -15,7 +15,7 @@ class ApiJwtAuth { try { $user = JWTAuth::parseToken()->authenticate(); - if (! $user) { + if (!$user) { return response()->json(['status' => false, 'message' => 'Unauthorized.'], 401); } diff --git a/app/Http/Middleware/CleanupScheduler.php b/app/Http/Middleware/CleanupScheduler.php index f383011a..f2fe3ac1 100644 --- a/app/Http/Middleware/CleanupScheduler.php +++ b/app/Http/Middleware/CleanupScheduler.php @@ -9,7 +9,9 @@ use Symfony\Component\HttpFoundation\Response; class CleanupScheduler { - public function __construct(private CleanupSchedulerService $service) {} + public function __construct(private CleanupSchedulerService $service) + { + } public function handle(Request $request, Closure $next): Response { diff --git a/app/Http/Middleware/EnsurePrintRequestsAdminAccess.php b/app/Http/Middleware/EnsurePrintRequestsAdminAccess.php index 467089e5..32055dc3 100644 --- a/app/Http/Middleware/EnsurePrintRequestsAdminAccess.php +++ b/app/Http/Middleware/EnsurePrintRequestsAdminAccess.php @@ -28,7 +28,8 @@ class EnsurePrintRequestsAdminAccess ->toArray(); foreach ( - ['administrator', 'admin', 'principal', 'vice_principal', 'vice-principal'] as $allowed + ['administrator', 'admin', 'principal', 'vice_principal', 'vice-principal'] + as $allowed ) { if (in_array($allowed, $roles, true)) { return $next($request); diff --git a/app/Http/Middleware/EnsureSchoolYearEditable.php b/app/Http/Middleware/EnsureSchoolYearEditable.php index dc96c1a9..7bbfc881 100644 --- a/app/Http/Middleware/EnsureSchoolYearEditable.php +++ b/app/Http/Middleware/EnsureSchoolYearEditable.php @@ -12,11 +12,13 @@ use Symfony\Component\HttpFoundation\Response; class EnsureSchoolYearEditable { - public function __construct(private SchoolYearWriteGuard $guard) {} + public function __construct(private SchoolYearWriteGuard $guard) + { + } public function handle(Request $request, Closure $next): Response { - if (! in_array(strtoupper($request->method()), ['POST', 'PUT', 'PATCH', 'DELETE'], true)) { + if (!in_array(strtoupper($request->method()), ['POST', 'PUT', 'PATCH', 'DELETE'], true)) { return $next($request); } @@ -82,7 +84,7 @@ class EnsureSchoolYearEditable private function lookupYear(string $id, string $table, string $column): ?string { - if (! ctype_digit($id) || ! DB::getSchemaBuilder()->hasTable($table)) { + if (!ctype_digit($id) || !DB::getSchemaBuilder()->hasTable($table)) { return null; } diff --git a/app/Http/Middleware/MultiAuth.php b/app/Http/Middleware/MultiAuth.php index bc1cd473..ee12e081 100644 --- a/app/Http/Middleware/MultiAuth.php +++ b/app/Http/Middleware/MultiAuth.php @@ -16,7 +16,6 @@ class MultiAuth $sanctumUser = Auth::guard('sanctum')->user(); if ($sanctumUser) { Auth::setUser($sanctumUser); - return $next($request); } @@ -26,7 +25,6 @@ class MultiAuth $jwtUser = JWTAuth::setToken($token)->authenticate(); if ($jwtUser) { Auth::setUser($jwtUser); - return $next($request); } } catch (JWTException $e) { diff --git a/app/Http/Middleware/PrimeTimezone.php b/app/Http/Middleware/PrimeTimezone.php index ab71d84c..34811419 100644 --- a/app/Http/Middleware/PrimeTimezone.php +++ b/app/Http/Middleware/PrimeTimezone.php @@ -9,7 +9,9 @@ use Symfony\Component\HttpFoundation\Response; class PrimeTimezone { - public function __construct(private TimeService $timeService) {} + public function __construct(private TimeService $timeService) + { + } public function handle(Request $request, Closure $next): Response { diff --git a/app/Http/Middleware/RequirePermission.php b/app/Http/Middleware/RequirePermission.php index b78d9f0a..bd5ae72b 100644 --- a/app/Http/Middleware/RequirePermission.php +++ b/app/Http/Middleware/RequirePermission.php @@ -9,7 +9,9 @@ use Symfony\Component\HttpFoundation\Response; class RequirePermission { - public function __construct(private PermissionCheckService $permissions) {} + public function __construct(private PermissionCheckService $permissions) + { + } public function handle(Request $request, Closure $next, string $permission): Response { @@ -18,11 +20,10 @@ class RequirePermission return response()->json(['status' => false, 'message' => 'Unauthorized.'], 401); } - if (! $this->permissions->hasPermission($userId, $permission)) { + if (!$this->permissions->hasPermission($userId, $permission)) { if ($request->expectsJson()) { return response()->json(['status' => false, 'message' => 'Access denied.'], 403); } - return response()->json(['status' => false, 'message' => 'Access denied.'], 403); } diff --git a/app/Http/Requests/Admin/SaveCompetitionScoresRequest.php b/app/Http/Requests/Admin/SaveCompetitionScoresRequest.php index 74d8e7de..c9b71b46 100644 --- a/app/Http/Requests/Admin/SaveCompetitionScoresRequest.php +++ b/app/Http/Requests/Admin/SaveCompetitionScoresRequest.php @@ -2,11 +2,10 @@ namespace App\Http\Requests\Admin; -use App\Controllers\Admin\CompetitionWinnersController; use Illuminate\Foundation\Http\FormRequest; /** - * Mirrors legacy {@see CompetitionWinnersController::saveScores()} accepted fields. + * Mirrors legacy {@see \App\Controllers\Admin\CompetitionWinnersController::saveScores()} accepted fields. */ class SaveCompetitionScoresRequest extends FormRequest { diff --git a/app/Http/Requests/Admin/UpdateCompetitionWinnerRequest.php b/app/Http/Requests/Admin/UpdateCompetitionWinnerRequest.php index 11da37fc..5610abd3 100644 --- a/app/Http/Requests/Admin/UpdateCompetitionWinnerRequest.php +++ b/app/Http/Requests/Admin/UpdateCompetitionWinnerRequest.php @@ -3,4 +3,6 @@ namespace App\Http\Requests\Admin; /** Validation for updating competition settings (legacy `CompetitionWinnersController::update`). */ -class UpdateCompetitionWinnerRequest extends StoreCompetitionWinnerRequest {} +class UpdateCompetitionWinnerRequest extends StoreCompetitionWinnerRequest +{ +} diff --git a/app/Http/Requests/Administrator/SubmitAdministratorAbsenceRequest.php b/app/Http/Requests/Administrator/SubmitAdministratorAbsenceRequest.php index 78efdab5..fc2ace6f 100644 --- a/app/Http/Requests/Administrator/SubmitAdministratorAbsenceRequest.php +++ b/app/Http/Requests/Administrator/SubmitAdministratorAbsenceRequest.php @@ -31,4 +31,4 @@ class SubmitAdministratorAbsenceRequest extends ApiFormRequest 'reason.required' => 'Reason is required.', ]; } -} +} \ No newline at end of file diff --git a/app/Http/Requests/Api/Admin/IndexAdminProgressRequest.php b/app/Http/Requests/Api/Admin/IndexAdminProgressRequest.php index 2a54a14a..0ef66ddf 100644 --- a/app/Http/Requests/Api/Admin/IndexAdminProgressRequest.php +++ b/app/Http/Requests/Api/Admin/IndexAdminProgressRequest.php @@ -2,6 +2,7 @@ namespace App\Http\Requests\Api\Admin; +use App\Http\Requests\ApiFormRequest; use Illuminate\Validation\Rule; class IndexAdminProgressRequest extends FormRequest @@ -35,4 +36,4 @@ class IndexAdminProgressRequest extends FormRequest 'status' => $this->filled('status') ? (string) $this->query('status') : '', ]); } -} +} \ No newline at end of file diff --git a/app/Http/Requests/Api/Admin/SaveAdminNotificationSubjectsRequest.php b/app/Http/Requests/Api/Admin/SaveAdminNotificationSubjectsRequest.php index f9ea9b87..7b00fa43 100644 --- a/app/Http/Requests/Api/Admin/SaveAdminNotificationSubjectsRequest.php +++ b/app/Http/Requests/Api/Admin/SaveAdminNotificationSubjectsRequest.php @@ -2,6 +2,8 @@ namespace App\Http\Requests\Admin; +use App\Http\Requests\ApiFormRequest; + class SaveAdminNotificationSubjectsRequest extends FormRequest { public function authorize(): bool @@ -16,4 +18,4 @@ class SaveAdminNotificationSubjectsRequest extends FormRequest // dynamic structure: subjects[adminId][subjectKey]=true OR array of subject strings ]; } -} +} \ No newline at end of file diff --git a/app/Http/Requests/Api/Admin/SavePrintNotificationRecipientsRequest.php b/app/Http/Requests/Api/Admin/SavePrintNotificationRecipientsRequest.php index f37a6ee8..b5c72993 100644 --- a/app/Http/Requests/Api/Admin/SavePrintNotificationRecipientsRequest.php +++ b/app/Http/Requests/Api/Admin/SavePrintNotificationRecipientsRequest.php @@ -2,6 +2,8 @@ namespace App\Http\Requests\Admin; +use App\Http\Requests\ApiFormRequest; + class SavePrintNotificationRecipientsRequest extends FormRequest { public function authorize(): bool @@ -15,4 +17,4 @@ class SavePrintNotificationRecipientsRequest extends FormRequest 'notify' => ['required', 'array'], ]; } -} +} \ No newline at end of file diff --git a/app/Http/Requests/Api/Admin/SendTeacherSubmissionNotificationsRequest.php b/app/Http/Requests/Api/Admin/SendTeacherSubmissionNotificationsRequest.php index 5e58cc8a..5819b909 100644 --- a/app/Http/Requests/Api/Admin/SendTeacherSubmissionNotificationsRequest.php +++ b/app/Http/Requests/Api/Admin/SendTeacherSubmissionNotificationsRequest.php @@ -2,6 +2,8 @@ namespace App\Http\Requests\Admin; +use App\Http\Requests\ApiFormRequest; + class SendTeacherSubmissionNotificationsRequest extends FormRequest { public function authorize(): bool @@ -17,4 +19,4 @@ class SendTeacherSubmissionNotificationsRequest extends FormRequest 'missing_items' => ['nullable', 'array'], ]; } -} +} \ No newline at end of file diff --git a/app/Http/Requests/Assignment/StoreStudentAssignmentRequest.php b/app/Http/Requests/Assignment/StoreStudentAssignmentRequest.php index 8c7485a0..2e9069e5 100644 --- a/app/Http/Requests/Assignment/StoreStudentAssignmentRequest.php +++ b/app/Http/Requests/Assignment/StoreStudentAssignmentRequest.php @@ -2,6 +2,8 @@ namespace App\Http\Requests; +use App\Http\Requests\ApiFormRequest; + class StoreStudentAssignmentRequest extends FormRequest { public function authorize(): bool @@ -12,11 +14,11 @@ class StoreStudentAssignmentRequest extends FormRequest public function rules(): array { return [ - 'student_id' => ['required', 'integer', 'exists:students,id'], + 'student_id' => ['required', 'integer', 'exists:students,id'], 'class_section_id' => ['required', 'integer', 'exists:class_sections,id'], - 'semester' => ['nullable', 'string', 'max:50'], - 'school_year' => ['nullable', 'string', 'max:50'], - 'description' => ['nullable', 'string'], + 'semester' => ['nullable', 'string', 'max:50'], + 'school_year' => ['nullable', 'string', 'max:50'], + 'description' => ['nullable', 'string'], ]; } -} +} \ No newline at end of file diff --git a/app/Http/Requests/Attendance/AdminAddAttendanceEntryRequest.php b/app/Http/Requests/Attendance/AdminAddAttendanceEntryRequest.php index bdf7828d..fe24e995 100644 --- a/app/Http/Requests/Attendance/AdminAddAttendanceEntryRequest.php +++ b/app/Http/Requests/Attendance/AdminAddAttendanceEntryRequest.php @@ -24,4 +24,4 @@ class AdminAddAttendanceEntryRequest extends FormRequest 'is_reported' => ['nullable', 'in:yes,no'], ]; } -} +} \ No newline at end of file diff --git a/app/Http/Requests/Attendance/SaveAdminAttendanceRequest.php b/app/Http/Requests/Attendance/SaveAdminAttendanceRequest.php index b6cbc816..630d5e2f 100644 --- a/app/Http/Requests/Attendance/SaveAdminAttendanceRequest.php +++ b/app/Http/Requests/Attendance/SaveAdminAttendanceRequest.php @@ -21,4 +21,4 @@ class SaveAdminAttendanceRequest extends FormRequest 'staff' => ['nullable', 'array'], ]; } -} +} \ No newline at end of file diff --git a/app/Http/Requests/Attendance/SaveStaffAttendanceCellRequest.php b/app/Http/Requests/Attendance/SaveStaffAttendanceCellRequest.php index 40c79fdb..bea0e6ef 100644 --- a/app/Http/Requests/Attendance/SaveStaffAttendanceCellRequest.php +++ b/app/Http/Requests/Attendance/SaveStaffAttendanceCellRequest.php @@ -23,4 +23,4 @@ class SaveStaffAttendanceCellRequest extends FormRequest 'reason' => ['nullable', 'string'], ]; } -} +} \ No newline at end of file diff --git a/app/Http/Requests/Attendance/TeacherSubmitAttendanceRequest.php b/app/Http/Requests/Attendance/TeacherSubmitAttendanceRequest.php index dd32766d..fafef9bb 100644 --- a/app/Http/Requests/Attendance/TeacherSubmitAttendanceRequest.php +++ b/app/Http/Requests/Attendance/TeacherSubmitAttendanceRequest.php @@ -26,4 +26,4 @@ class TeacherSubmitAttendanceRequest extends FormRequest 'teachers' => ['required', 'array', 'min:1'], ]; } -} +} \ No newline at end of file diff --git a/app/Http/Requests/Attendance/UpdateAttendanceManagementRequest.php b/app/Http/Requests/Attendance/UpdateAttendanceManagementRequest.php index 325c34af..eded3b8c 100644 --- a/app/Http/Requests/Attendance/UpdateAttendanceManagementRequest.php +++ b/app/Http/Requests/Attendance/UpdateAttendanceManagementRequest.php @@ -26,4 +26,4 @@ class UpdateAttendanceManagementRequest extends FormRequest 'is_reported' => ['nullable'], ]; } -} +} \ No newline at end of file diff --git a/app/Http/Requests/AttendanceTracking/ComposeAttendanceEmailRequest.php b/app/Http/Requests/AttendanceTracking/ComposeAttendanceEmailRequest.php index 6d8c2614..ad954676 100644 --- a/app/Http/Requests/AttendanceTracking/ComposeAttendanceEmailRequest.php +++ b/app/Http/Requests/AttendanceTracking/ComposeAttendanceEmailRequest.php @@ -14,9 +14,9 @@ class ComposeAttendanceEmailRequest extends ApiFormRequest public function rules(): array { return [ - 'student_id' => ['required', 'integer', 'min:1'], - 'code' => ['nullable', 'string'], - 'variant' => ['nullable', 'string'], + 'student_id' => ['required', 'integer', 'min:1'], + 'code' => ['nullable', 'string'], + 'variant' => ['nullable', 'string'], 'incident_date' => ['nullable', 'date_format:Y-m-d'], ]; } diff --git a/app/Http/Requests/AttendanceTracking/RecordAttendanceTrackingRequest.php b/app/Http/Requests/AttendanceTracking/RecordAttendanceTrackingRequest.php index 9e71011d..9c00dec7 100644 --- a/app/Http/Requests/AttendanceTracking/RecordAttendanceTrackingRequest.php +++ b/app/Http/Requests/AttendanceTracking/RecordAttendanceTrackingRequest.php @@ -14,13 +14,13 @@ class RecordAttendanceTrackingRequest extends ApiFormRequest public function rules(): array { return [ - 'student_id' => ['required', 'integer', 'min:1'], - 'date' => ['required', 'date_format:Y-m-d'], + 'student_id' => ['required', 'integer', 'min:1'], + 'date' => ['required', 'date_format:Y-m-d'], 'parent_email' => ['nullable', 'email'], - 'parent_name' => ['nullable', 'string'], + 'parent_name' => ['nullable', 'string'], 'subject_type' => ['nullable', 'string'], - 'semester' => ['nullable', 'string'], - 'school_year' => ['nullable', 'string'], + 'semester' => ['nullable', 'string'], + 'school_year' => ['nullable', 'string'], ]; } } diff --git a/app/Http/Requests/AttendanceTracking/SaveAttendanceNotificationNoteRequest.php b/app/Http/Requests/AttendanceTracking/SaveAttendanceNotificationNoteRequest.php index 9276272a..1d57f480 100644 --- a/app/Http/Requests/AttendanceTracking/SaveAttendanceNotificationNoteRequest.php +++ b/app/Http/Requests/AttendanceTracking/SaveAttendanceNotificationNoteRequest.php @@ -14,13 +14,13 @@ class SaveAttendanceNotificationNoteRequest extends ApiFormRequest public function rules(): array { return [ - 'student_id' => ['required', 'integer', 'min:1'], - 'code' => ['required', 'string'], - 'note' => ['required', 'string'], + 'student_id' => ['required', 'integer', 'min:1'], + 'code' => ['required', 'string'], + 'note' => ['required', 'string'], 'incident_date' => ['nullable', 'date_format:Y-m-d'], - 'to' => ['nullable', 'email'], - 'subject' => ['nullable', 'string'], - 'variant' => ['nullable', 'string'], + 'to' => ['nullable', 'email'], + 'subject' => ['nullable', 'string'], + 'variant' => ['nullable', 'string'], ]; } } diff --git a/app/Http/Requests/AttendanceTracking/SendAttendanceManualEmailRequest.php b/app/Http/Requests/AttendanceTracking/SendAttendanceManualEmailRequest.php index bca44af7..0994b8d9 100644 --- a/app/Http/Requests/AttendanceTracking/SendAttendanceManualEmailRequest.php +++ b/app/Http/Requests/AttendanceTracking/SendAttendanceManualEmailRequest.php @@ -14,12 +14,12 @@ class SendAttendanceManualEmailRequest extends ApiFormRequest public function rules(): array { return [ - 'student_id' => ['required', 'integer', 'min:1'], - 'to' => ['required', 'email'], - 'subject' => ['required', 'string'], - 'body_html' => ['required', 'string'], - 'code' => ['required', 'string'], - 'variant' => ['nullable', 'string'], + 'student_id' => ['required', 'integer', 'min:1'], + 'to' => ['required', 'email'], + 'subject' => ['required', 'string'], + 'body_html' => ['required', 'string'], + 'code' => ['required', 'string'], + 'variant' => ['nullable', 'string'], 'incident_date' => ['nullable', 'date_format:Y-m-d'], ]; } diff --git a/app/Http/Requests/Auth/RegisterRequest.php b/app/Http/Requests/Auth/RegisterRequest.php index ff57feda..13401f00 100644 --- a/app/Http/Requests/Auth/RegisterRequest.php +++ b/app/Http/Requests/Auth/RegisterRequest.php @@ -8,7 +8,7 @@ class RegisterRequest extends ApiFormRequest { public static function ruleset(): array { - return (new self)->rules(); + return (new self())->rules(); } public function authorize(): bool diff --git a/app/Http/Requests/Badges/BadgePrintStatusRequest.php b/app/Http/Requests/Badges/BadgePrintStatusRequest.php index 86429035..4cf174a0 100644 --- a/app/Http/Requests/Badges/BadgePrintStatusRequest.php +++ b/app/Http/Requests/Badges/BadgePrintStatusRequest.php @@ -17,7 +17,7 @@ class BadgePrintStatusRequest extends FormRequest if (is_string($userIds)) { $userIds = array_filter(array_map('trim', explode(',', $userIds)), 'strlen'); - } elseif (! is_array($userIds)) { + } elseif (!is_array($userIds)) { $userIds = $userIds ? [$userIds] : []; } @@ -40,4 +40,4 @@ class BadgePrintStatusRequest extends FormRequest { return $this->input('user_ids', []); } -} +} \ No newline at end of file diff --git a/app/Http/Requests/Badges/GenerateBadgePdfRequest.php b/app/Http/Requests/Badges/GenerateBadgePdfRequest.php index c1bc01a8..20f7684b 100644 --- a/app/Http/Requests/Badges/GenerateBadgePdfRequest.php +++ b/app/Http/Requests/Badges/GenerateBadgePdfRequest.php @@ -17,7 +17,7 @@ class GenerateBadgePdfRequest extends FormRequest if (is_string($userIds)) { $userIds = array_filter(array_map('trim', explode(',', $userIds)), 'strlen'); - } elseif (! is_array($userIds)) { + } elseif (!is_array($userIds)) { $userIds = $userIds ? [$userIds] : []; } @@ -38,4 +38,4 @@ class GenerateBadgePdfRequest extends FormRequest 'classes' => ['nullable', 'array'], ]; } -} +} \ No newline at end of file diff --git a/app/Http/Requests/Badges/LogBadgePrintRequest.php b/app/Http/Requests/Badges/LogBadgePrintRequest.php index b2fabafd..2e5362c3 100644 --- a/app/Http/Requests/Badges/LogBadgePrintRequest.php +++ b/app/Http/Requests/Badges/LogBadgePrintRequest.php @@ -17,7 +17,7 @@ class LogBadgePrintRequest extends FormRequest if (is_string($userIds)) { $userIds = array_filter(array_map('trim', explode(',', $userIds)), 'strlen'); - } elseif (! is_array($userIds)) { + } elseif (!is_array($userIds)) { $userIds = $userIds ? [$userIds] : []; } @@ -38,4 +38,4 @@ class LogBadgePrintRequest extends FormRequest 'classes' => ['nullable', 'array'], ]; } -} +} \ No newline at end of file diff --git a/app/Http/Requests/ClassProgress/ClassProgressIndexRequest.php b/app/Http/Requests/ClassProgress/ClassProgressIndexRequest.php index fd6ac3e0..aded861c 100644 --- a/app/Http/Requests/ClassProgress/ClassProgressIndexRequest.php +++ b/app/Http/Requests/ClassProgress/ClassProgressIndexRequest.php @@ -2,6 +2,8 @@ namespace App\Http\Requests\ClassProgress; +use App\Http\Requests\ClassProgress\ClassProgressFormRequest; + class ClassProgressIndexRequest extends ClassProgressFormRequest { public function authorize(): bool @@ -21,7 +23,7 @@ class ClassProgressIndexRequest extends ClassProgressFormRequest 'week_start' => ['nullable', 'date_format:Y-m-d'], 'week_end' => ['nullable', 'date_format:Y-m-d'], 'subject' => ['nullable', 'string', 'max:120'], - 'status' => $statuses ? ['nullable', 'in:'.implode(',', $statuses)] : ['nullable', 'string'], + 'status' => $statuses ? ['nullable', 'in:' . implode(',', $statuses)] : ['nullable', 'string'], 'group_by_week' => ['nullable', 'boolean'], 'page' => ['nullable', 'integer', 'min:1'], 'per_page' => ['nullable', 'integer', 'min:1', 'max:200'], diff --git a/app/Http/Requests/ClassProgress/ClassProgressMetaRequest.php b/app/Http/Requests/ClassProgress/ClassProgressMetaRequest.php index ba56c177..cb7f61ad 100644 --- a/app/Http/Requests/ClassProgress/ClassProgressMetaRequest.php +++ b/app/Http/Requests/ClassProgress/ClassProgressMetaRequest.php @@ -2,6 +2,8 @@ namespace App\Http\Requests\ClassProgress; +use App\Http\Requests\ClassProgress\ClassProgressFormRequest; + class ClassProgressMetaRequest extends ClassProgressFormRequest { public function authorize(): bool diff --git a/app/Http/Requests/ClassProgress/ClassProgressStoreRequest.php b/app/Http/Requests/ClassProgress/ClassProgressStoreRequest.php index 81dbbdb1..ea3d0e69 100644 --- a/app/Http/Requests/ClassProgress/ClassProgressStoreRequest.php +++ b/app/Http/Requests/ClassProgress/ClassProgressStoreRequest.php @@ -2,6 +2,8 @@ namespace App\Http\Requests\ClassProgress; +use App\Http\Requests\ClassProgress\ClassProgressFormRequest; + class ClassProgressStoreRequest extends ClassProgressFormRequest { public function authorize(): bool @@ -25,7 +27,7 @@ class ClassProgressStoreRequest extends ClassProgressFormRequest $sections = (array) config('progress.subject_sections', []); $maxFileKb = (int) config('progress.attachments.max_kb', 5120); $mimes = (array) config('progress.attachments.mimes', ['pdf', 'jpg', 'jpeg', 'png']); - $mimeRule = 'mimes:'.implode(',', $mimes); + $mimeRule = 'mimes:' . implode(',', $mimes); foreach ($sections as $slug => $section) { $rules["covered_{$slug}"] = ['nullable', 'string']; @@ -35,7 +37,7 @@ class ClassProgressStoreRequest extends ClassProgressFormRequest $rules["chapter_{$slug}"] = ['nullable', 'array']; $rules["chapter_{$slug}.*"] = ['nullable', 'string', 'max:120']; $rules["attachment_{$slug}"] = ['nullable', 'array']; - $rules["attachment_{$slug}.*"] = ['nullable', 'file', $mimeRule, 'max:'.$maxFileKb]; + $rules["attachment_{$slug}.*"] = ['nullable', 'file', $mimeRule, 'max:' . $maxFileKb]; } return $rules; diff --git a/app/Http/Requests/ClassProgress/ClassProgressUpdateRequest.php b/app/Http/Requests/ClassProgress/ClassProgressUpdateRequest.php index c72f8030..1919b7e5 100644 --- a/app/Http/Requests/ClassProgress/ClassProgressUpdateRequest.php +++ b/app/Http/Requests/ClassProgress/ClassProgressUpdateRequest.php @@ -2,6 +2,8 @@ namespace App\Http\Requests\ClassProgress; +use App\Http\Requests\ClassProgress\ClassProgressFormRequest; + class ClassProgressUpdateRequest extends ClassProgressFormRequest { public function authorize(): bool @@ -14,7 +16,7 @@ class ClassProgressUpdateRequest extends ClassProgressFormRequest $statuses = array_keys((array) config('progress.status_options', [])); $maxFileKb = (int) config('progress.attachments.max_kb', 5120); $mimes = (array) config('progress.attachments.mimes', ['pdf', 'jpg', 'jpeg', 'png']); - $mimeRule = 'mimes:'.implode(',', $mimes); + $mimeRule = 'mimes:' . implode(',', $mimes); return [ 'week_start' => ['nullable', 'date_format:Y-m-d'], @@ -22,11 +24,11 @@ class ClassProgressUpdateRequest extends ClassProgressFormRequest 'covered' => ['nullable', 'string'], 'homework' => ['nullable', 'string'], 'unit_title' => ['nullable', 'string', 'max:120'], - 'status' => $statuses ? ['nullable', 'in:'.implode(',', $statuses)] : ['nullable', 'string'], + 'status' => $statuses ? ['nullable', 'in:' . implode(',', $statuses)] : ['nullable', 'string'], 'support_needed' => ['nullable', 'string'], 'flags' => ['nullable', 'array'], 'attachments' => ['nullable', 'array'], - 'attachments.*' => ['nullable', 'file', $mimeRule, 'max:'.$maxFileKb], + 'attachments.*' => ['nullable', 'file', $mimeRule, 'max:' . $maxFileKb], ]; } diff --git a/app/Http/Requests/Families/FamilyComposeEmailRequest.php b/app/Http/Requests/Families/FamilyComposeEmailRequest.php index caf48ad7..3058a88c 100644 --- a/app/Http/Requests/Families/FamilyComposeEmailRequest.php +++ b/app/Http/Requests/Families/FamilyComposeEmailRequest.php @@ -10,7 +10,7 @@ class FamilyComposeEmailRequest extends ApiFormRequest { parent::prepareForValidation(); - if (! $this->has('html_message') && $this->has('html')) { + if (!$this->has('html_message') && $this->has('html')) { $this->merge(['html_message' => $this->input('html')]); } } diff --git a/app/Http/Requests/Finance/CarryforwardAdjustmentRequest.php b/app/Http/Requests/Finance/CarryforwardAdjustmentRequest.php index 7e3054bb..251e49e9 100644 --- a/app/Http/Requests/Finance/CarryforwardAdjustmentRequest.php +++ b/app/Http/Requests/Finance/CarryforwardAdjustmentRequest.php @@ -6,16 +6,12 @@ use Illuminate\Foundation\Http\FormRequest; class CarryforwardAdjustmentRequest extends FormRequest { - public function authorize(): bool - { - return true; - } - + public function authorize(): bool { return true; } public function rules(): array { return [ - 'amount' => ['required', 'numeric', 'min:0'], - 'reason' => ['nullable', 'string', 'max:255'], + 'amount' => ['required','numeric','min:0'], + 'reason' => ['nullable','string','max:255'], ]; } } diff --git a/app/Http/Requests/Finance/CarryforwardFinalizeRequest.php b/app/Http/Requests/Finance/CarryforwardFinalizeRequest.php index bdd72381..2cd75e8f 100644 --- a/app/Http/Requests/Finance/CarryforwardFinalizeRequest.php +++ b/app/Http/Requests/Finance/CarryforwardFinalizeRequest.php @@ -6,18 +6,14 @@ use Illuminate\Foundation\Http\FormRequest; class CarryforwardFinalizeRequest extends FormRequest { - public function authorize(): bool - { - return true; - } - + public function authorize(): bool { return true; } public function rules(): array { return [ - 'from_school_year' => ['required_without:id', 'string', 'max:20'], - 'to_school_year' => ['required_without:id', 'string', 'max:20'], - 'parent_id' => ['nullable', 'integer'], - 'reason' => ['nullable', 'string', 'max:255'], + 'from_school_year' => ['required_without:id','string','max:20'], + 'to_school_year' => ['required_without:id','string','max:20'], + 'parent_id' => ['nullable','integer'], + 'reason' => ['nullable','string','max:255'], ]; } } diff --git a/app/Http/Requests/Finance/CarryforwardPreviewRequest.php b/app/Http/Requests/Finance/CarryforwardPreviewRequest.php index 813fc380..7d660220 100644 --- a/app/Http/Requests/Finance/CarryforwardPreviewRequest.php +++ b/app/Http/Requests/Finance/CarryforwardPreviewRequest.php @@ -6,18 +6,14 @@ use Illuminate\Foundation\Http\FormRequest; class CarryforwardPreviewRequest extends FormRequest { - public function authorize(): bool - { - return true; - } - + public function authorize(): bool { return true; } public function rules(): array { return [ - 'from_school_year' => ['required', 'string', 'max:20'], - 'to_school_year' => ['required', 'string', 'max:20'], - 'parent_id' => ['nullable', 'integer'], - 'minimum_balance' => ['nullable', 'numeric', 'min:0'], + 'from_school_year' => ['required','string','max:20'], + 'to_school_year' => ['required','string','max:20'], + 'parent_id' => ['nullable','integer'], + 'minimum_balance' => ['nullable','numeric','min:0'], ]; } } diff --git a/app/Http/Requests/Finance/EventChargeLifecycleRequest.php b/app/Http/Requests/Finance/EventChargeLifecycleRequest.php index d689a18a..e2a4371c 100644 --- a/app/Http/Requests/Finance/EventChargeLifecycleRequest.php +++ b/app/Http/Requests/Finance/EventChargeLifecycleRequest.php @@ -6,24 +6,20 @@ use Illuminate\Foundation\Http\FormRequest; class EventChargeLifecycleRequest extends FormRequest { - public function authorize(): bool - { - return true; - } - + public function authorize(): bool { return true; } public function rules(): array { return [ - 'event_id' => ['nullable', 'integer'], - 'parent_id' => ['nullable', 'integer'], - 'student_id' => ['nullable', 'integer'], - 'school_year' => ['nullable', 'string', 'max:20'], - 'semester' => ['nullable', 'string', 'max:40'], - 'event_name' => ['nullable', 'string', 'max:255'], - 'title' => ['nullable', 'string', 'max:255'], - 'description' => ['nullable', 'string'], - 'amount' => ['nullable', 'numeric', 'min:0'], - 'invoice_id' => ['nullable', 'integer'], + 'event_id' => ['nullable','integer'], + 'parent_id' => ['nullable','integer'], + 'student_id' => ['nullable','integer'], + 'school_year' => ['nullable','string','max:20'], + 'semester' => ['nullable','string','max:40'], + 'event_name' => ['nullable','string','max:255'], + 'title' => ['nullable','string','max:255'], + 'description' => ['nullable','string'], + 'amount' => ['nullable','numeric','min:0'], + 'invoice_id' => ['nullable','integer'], ]; } } diff --git a/app/Http/Requests/Finance/FinanceNotificationLogRequest.php b/app/Http/Requests/Finance/FinanceNotificationLogRequest.php index 970299b4..e5e38aff 100644 --- a/app/Http/Requests/Finance/FinanceNotificationLogRequest.php +++ b/app/Http/Requests/Finance/FinanceNotificationLogRequest.php @@ -6,19 +6,15 @@ use Illuminate\Foundation\Http\FormRequest; class FinanceNotificationLogRequest extends FormRequest { - public function authorize(): bool - { - return true; - } - + public function authorize(): bool { return true; } public function rules(): array { return [ - 'status' => ['nullable', 'string', 'max:40'], - 'notification_type' => ['nullable', 'string', 'max:80'], - 'parent_id' => ['nullable', 'integer'], - 'date_from' => ['nullable', 'date'], - 'date_to' => ['nullable', 'date'], + 'status' => ['nullable','string','max:40'], + 'notification_type' => ['nullable','string','max:80'], + 'parent_id' => ['nullable','integer'], + 'date_from' => ['nullable','date'], + 'date_to' => ['nullable','date'], ]; } } diff --git a/app/Http/Requests/Finance/FinancialSummaryRequest.php b/app/Http/Requests/Finance/FinancialSummaryRequest.php index 36548dfb..1f1c43dd 100644 --- a/app/Http/Requests/Finance/FinancialSummaryRequest.php +++ b/app/Http/Requests/Finance/FinancialSummaryRequest.php @@ -2,4 +2,6 @@ namespace App\Http\Requests\Finance; -class FinancialSummaryRequest extends FinancialReportRequest {} +class FinancialSummaryRequest extends FinancialReportRequest +{ +} diff --git a/app/Http/Requests/Finance/FollowUpNoteRequest.php b/app/Http/Requests/Finance/FollowUpNoteRequest.php index e4979a54..23f0caa6 100644 --- a/app/Http/Requests/Finance/FollowUpNoteRequest.php +++ b/app/Http/Requests/Finance/FollowUpNoteRequest.php @@ -6,23 +6,19 @@ use Illuminate\Foundation\Http\FormRequest; class FollowUpNoteRequest extends FormRequest { - public function authorize(): bool - { - return true; - } - + public function authorize(): bool { return true; } public function rules(): array { return [ - 'invoice_id' => ['nullable', 'integer'], - 'school_year' => ['nullable', 'string', 'max:20'], - 'status' => ['nullable', 'string', 'max:60'], - 'contact_method' => ['nullable', 'string', 'max:60'], - 'promise_to_pay_date' => ['nullable', 'date'], - 'promise_to_pay_amount' => ['nullable', 'numeric', 'min:0'], - 'next_follow_up_date' => ['nullable', 'date'], - 'escalation_level' => ['nullable', 'integer', 'min:0', 'max:10'], - 'note' => ['nullable', 'string', 'max:10000'], + 'invoice_id' => ['nullable','integer'], + 'school_year' => ['nullable','string','max:20'], + 'status' => ['nullable','string','max:60'], + 'contact_method' => ['nullable','string','max:60'], + 'promise_to_pay_date' => ['nullable','date'], + 'promise_to_pay_amount' => ['nullable','numeric','min:0'], + 'next_follow_up_date' => ['nullable','date'], + 'escalation_level' => ['nullable','integer','min:0','max:10'], + 'note' => ['nullable','string','max:10000'], ]; } } diff --git a/app/Http/Requests/Finance/InstallmentPlanRequest.php b/app/Http/Requests/Finance/InstallmentPlanRequest.php index 57ff6913..e53b74d9 100644 --- a/app/Http/Requests/Finance/InstallmentPlanRequest.php +++ b/app/Http/Requests/Finance/InstallmentPlanRequest.php @@ -6,19 +6,15 @@ use Illuminate\Foundation\Http\FormRequest; class InstallmentPlanRequest extends FormRequest { - public function authorize(): bool - { - return true; - } - + public function authorize(): bool { return true; } public function rules(): array { return [ - 'plan_name' => ['nullable', 'string', 'max:120'], - 'total_amount' => ['nullable', 'numeric', 'min:0'], - 'number_of_installments' => ['required', 'integer', 'min:1', 'max:24'], - 'first_due_date' => ['required', 'date'], - 'interval_months' => ['nullable', 'integer', 'min:1', 'max:12'], + 'plan_name' => ['nullable','string','max:120'], + 'total_amount' => ['nullable','numeric','min:0'], + 'number_of_installments' => ['required','integer','min:1','max:24'], + 'first_due_date' => ['required','date'], + 'interval_months' => ['nullable','integer','min:1','max:12'], ]; } } diff --git a/app/Http/Requests/Finance/ParentPaymentFollowUpRequest.php b/app/Http/Requests/Finance/ParentPaymentFollowUpRequest.php index 24fedbe1..7309f768 100644 --- a/app/Http/Requests/Finance/ParentPaymentFollowUpRequest.php +++ b/app/Http/Requests/Finance/ParentPaymentFollowUpRequest.php @@ -6,27 +6,23 @@ use Illuminate\Foundation\Http\FormRequest; class ParentPaymentFollowUpRequest extends FormRequest { - public function authorize(): bool - { - return true; - } - + public function authorize(): bool { return true; } public function rules(): array { return [ - 'school_year' => ['nullable', 'string', 'max:20'], - 'date_from' => ['nullable', 'date'], - 'date_to' => ['nullable', 'date'], - 'minimum_balance' => ['nullable', 'numeric', 'min:0'], - 'aging_bucket' => ['nullable', 'string', 'max:40'], - 'parent_id' => ['nullable', 'integer'], - 'student_id' => ['nullable', 'integer'], - 'follow_up_status' => ['nullable', 'string', 'max:60'], - 'last_contacted_before' => ['nullable', 'date'], - 'next_follow_up_due_before' => ['nullable', 'date'], - 'promise_to_pay_due_before' => ['nullable', 'date'], - 'include_paid_invoices' => ['nullable', 'boolean'], - 'include_inactive_students' => ['nullable', 'boolean'], + 'school_year' => ['nullable','string','max:20'], + 'date_from' => ['nullable','date'], + 'date_to' => ['nullable','date'], + 'minimum_balance' => ['nullable','numeric','min:0'], + 'aging_bucket' => ['nullable','string','max:40'], + 'parent_id' => ['nullable','integer'], + 'student_id' => ['nullable','integer'], + 'follow_up_status' => ['nullable','string','max:60'], + 'last_contacted_before' => ['nullable','date'], + 'next_follow_up_due_before' => ['nullable','date'], + 'promise_to_pay_due_before' => ['nullable','date'], + 'include_paid_invoices' => ['nullable','boolean'], + 'include_inactive_students' => ['nullable','boolean'], ]; } } diff --git a/app/Http/Requests/Finance/PaymentAllocationRequest.php b/app/Http/Requests/Finance/PaymentAllocationRequest.php index a201e3f8..36f714bf 100644 --- a/app/Http/Requests/Finance/PaymentAllocationRequest.php +++ b/app/Http/Requests/Finance/PaymentAllocationRequest.php @@ -6,17 +6,13 @@ use Illuminate\Foundation\Http\FormRequest; class PaymentAllocationRequest extends FormRequest { - public function authorize(): bool - { - return true; - } - + public function authorize(): bool { return true; } public function rules(): array { return [ - 'installments' => ['nullable', 'array'], - 'installments.*.installment_id' => ['required_with:installments', 'integer'], - 'installments.*.amount' => ['required_with:installments', 'numeric', 'min:0.01'], + 'installments' => ['nullable','array'], + 'installments.*.installment_id' => ['required_with:installments','integer'], + 'installments.*.amount' => ['required_with:installments','numeric','min:0.01'], ]; } } diff --git a/app/Http/Requests/Incidents/IncidentStoreRequest.php b/app/Http/Requests/Incidents/IncidentStoreRequest.php index 0368835c..b9de4dbb 100644 --- a/app/Http/Requests/Incidents/IncidentStoreRequest.php +++ b/app/Http/Requests/Incidents/IncidentStoreRequest.php @@ -44,7 +44,7 @@ class IncidentStoreRequest extends ApiFormRequest return [ 'student_id' => ['required', 'integer', 'min:1', 'exists:students,id'], 'grade' => ['required', 'string', 'max:50'], - 'incident' => ['required', 'string', 'in:'.implode(',', $incidentTypes)], + 'incident' => ['required', 'string', 'in:' . implode(',', $incidentTypes)], 'description' => ['required', 'string', 'max:2000'], 'incident_state' => ['nullable', 'string', 'in:Open,Closed,Canceled'], 'state_description' => ['nullable', 'string', 'max:2000'], diff --git a/app/Http/Requests/Promotions/AdminListPromotionsRequest.php b/app/Http/Requests/Promotions/AdminListPromotionsRequest.php index e90290e3..f6b54c47 100644 --- a/app/Http/Requests/Promotions/AdminListPromotionsRequest.php +++ b/app/Http/Requests/Promotions/AdminListPromotionsRequest.php @@ -30,7 +30,7 @@ class AdminListPromotionsRequest extends ApiFormRequest $status = $data['status'] ?? null; if (is_string($status) && $status !== '') { $status = [$status]; - } elseif (! is_array($status)) { + } elseif (!is_array($status)) { $status = null; } diff --git a/app/Http/Requests/Promotions/ParentEnrollmentStepRequest.php b/app/Http/Requests/Promotions/ParentEnrollmentStepRequest.php index 9b4103d7..8689d033 100644 --- a/app/Http/Requests/Promotions/ParentEnrollmentStepRequest.php +++ b/app/Http/Requests/Promotions/ParentEnrollmentStepRequest.php @@ -25,7 +25,6 @@ class ParentEnrollmentStepRequest extends ApiFormRequest $steps[$field] = (bool) $data[$field]; } } - return $steps; } } diff --git a/app/Http/Requests/PurchaseOrders/PurchaseOrderStoreRequest.php b/app/Http/Requests/PurchaseOrders/PurchaseOrderStoreRequest.php index 6bc12b16..aea68722 100644 --- a/app/Http/Requests/PurchaseOrders/PurchaseOrderStoreRequest.php +++ b/app/Http/Requests/PurchaseOrders/PurchaseOrderStoreRequest.php @@ -17,7 +17,7 @@ class PurchaseOrderStoreRequest extends ApiFormRequest return [ 'po_number' => ['required', 'string', 'max:60'], 'supplier_id' => ['required', 'integer', 'min:1', 'exists:suppliers,id'], - 'status' => ['nullable', 'string', 'in:'.implode(',', PurchaseOrder::allowedStatuses())], + 'status' => ['nullable', 'string', 'in:' . implode(',', PurchaseOrder::allowedStatuses())], 'order_date' => ['nullable', 'date'], 'expected_date' => ['nullable', 'date', 'after_or_equal:order_date'], 'notes' => ['nullable', 'string', 'max:5000'], diff --git a/app/Http/Requests/Refunds/RefundDecisionRequest.php b/app/Http/Requests/Refunds/RefundDecisionRequest.php index 788a76ec..e5db2dea 100644 --- a/app/Http/Requests/Refunds/RefundDecisionRequest.php +++ b/app/Http/Requests/Refunds/RefundDecisionRequest.php @@ -15,7 +15,7 @@ class RefundDecisionRequest extends ApiFormRequest public function rules(): array { return [ - 'status' => ['required', 'string', 'in:'.implode(',', [ + 'status' => ['required', 'string', 'in:' . implode(',', [ Refund::STATUS_APPROVED, Refund::STATUS_REJECTED, ])], diff --git a/app/Http/Requests/Refunds/RefundIndexRequest.php b/app/Http/Requests/Refunds/RefundIndexRequest.php index 17ffb2cd..3f18109b 100644 --- a/app/Http/Requests/Refunds/RefundIndexRequest.php +++ b/app/Http/Requests/Refunds/RefundIndexRequest.php @@ -15,7 +15,7 @@ class RefundIndexRequest extends ApiFormRequest public function rules(): array { return [ - 'status' => ['nullable', 'string', 'in:'.implode(',', [ + 'status' => ['nullable', 'string', 'in:' . implode(',', [ Refund::STATUS_PENDING, Refund::STATUS_APPROVED, Refund::STATUS_PARTIAL, diff --git a/app/Http/Requests/Reports/Stickers/StickerFormDataRequest.php b/app/Http/Requests/Reports/Stickers/StickerFormDataRequest.php index a1b04b3f..9644faf2 100644 --- a/app/Http/Requests/Reports/Stickers/StickerFormDataRequest.php +++ b/app/Http/Requests/Reports/Stickers/StickerFormDataRequest.php @@ -10,7 +10,7 @@ class StickerFormDataRequest extends ApiFormRequest { parent::prepareForValidation(); - if (! $this->has('class_section_id') && $this->has('class_id')) { + if (!$this->has('class_section_id') && $this->has('class_id')) { $this->merge(['class_section_id' => $this->input('class_id')]); } } diff --git a/app/Http/Requests/Reports/Stickers/StickerPrintRequest.php b/app/Http/Requests/Reports/Stickers/StickerPrintRequest.php index 2522926e..651ec973 100644 --- a/app/Http/Requests/Reports/Stickers/StickerPrintRequest.php +++ b/app/Http/Requests/Reports/Stickers/StickerPrintRequest.php @@ -11,7 +11,7 @@ class StickerPrintRequest extends ApiFormRequest { parent::prepareForValidation(); - if (! $this->has('class_section_id') && $this->has('class_id')) { + if (!$this->has('class_section_id') && $this->has('class_id')) { $this->merge(['class_section_id' => $this->input('class_id')]); } } @@ -53,7 +53,7 @@ class StickerPrintRequest extends ApiFormRequest $hasClass = $this->filled('class_section_id'); $printAll = (bool) $this->input('print_all', false); - if (! $hasStudent && ! $hasClass && ! $printAll) { + if (!$hasStudent && !$hasClass && !$printAll) { $validator->errors()->add('selection', 'Please select a student or class, or set print_all.'); } }); diff --git a/app/Http/Requests/Reports/Stickers/StickerStudentsByClassRequest.php b/app/Http/Requests/Reports/Stickers/StickerStudentsByClassRequest.php index 5ae919ca..089ff9a0 100644 --- a/app/Http/Requests/Reports/Stickers/StickerStudentsByClassRequest.php +++ b/app/Http/Requests/Reports/Stickers/StickerStudentsByClassRequest.php @@ -10,7 +10,7 @@ class StickerStudentsByClassRequest extends ApiFormRequest { parent::prepareForValidation(); - if (! $this->has('class_section_id') && $this->has('class_id')) { + if (!$this->has('class_section_id') && $this->has('class_id')) { $this->merge(['class_section_id' => $this->input('class_id')]); } } diff --git a/app/Http/Requests/Roles/PermissionStoreRequest.php b/app/Http/Requests/Roles/PermissionStoreRequest.php index ee365b15..cd97a21c 100644 --- a/app/Http/Requests/Roles/PermissionStoreRequest.php +++ b/app/Http/Requests/Roles/PermissionStoreRequest.php @@ -3,6 +3,7 @@ namespace App\Http\Requests\Roles; use App\Http\Requests\ApiFormRequest; +use Illuminate\Validation\Rule; class PermissionStoreRequest extends ApiFormRequest { diff --git a/app/Http/Requests/Security/IpBanRequest.php b/app/Http/Requests/Security/IpBanRequest.php index e599414d..1c89a523 100644 --- a/app/Http/Requests/Security/IpBanRequest.php +++ b/app/Http/Requests/Security/IpBanRequest.php @@ -26,7 +26,7 @@ class IpBanRequest extends ApiFormRequest $validator->after(function (Validator $validator) { $id = $this->input('id'); $ip = $this->input('ip'); - if (! $id && ! $ip) { + if (!$id && !$ip) { $validator->errors()->add('id', 'Either id or ip is required.'); } }); diff --git a/app/Http/Requests/Security/IpUnbanRequest.php b/app/Http/Requests/Security/IpUnbanRequest.php index 31291e76..e5ad0e3a 100644 --- a/app/Http/Requests/Security/IpUnbanRequest.php +++ b/app/Http/Requests/Security/IpUnbanRequest.php @@ -28,7 +28,7 @@ class IpUnbanRequest extends ApiFormRequest $ip = $this->input('ip'); $all = filter_var($this->input('all'), FILTER_VALIDATE_BOOLEAN); - if (! $all && ! $id && ! $ip) { + if (!$all && !$id && !$ip) { $validator->errors()->add('id', 'Either id, ip, or all=true is required.'); } }); diff --git a/app/Http/Resources/Admin/AdminNotificationAlertsResource.php b/app/Http/Resources/Admin/AdminNotificationAlertsResource.php index cf371d8a..0f0a1dc6 100644 --- a/app/Http/Resources/Admin/AdminNotificationAlertsResource.php +++ b/app/Http/Resources/Admin/AdminNotificationAlertsResource.php @@ -16,4 +16,4 @@ class AdminNotificationAlertsResource extends JsonResource 'table_ready' => (bool) ($this['table_ready'] ?? false), ]; } -} +} \ No newline at end of file diff --git a/app/Http/Resources/Admin/PrintRecipientsResource.php b/app/Http/Resources/Admin/PrintRecipientsResource.php index db8dd1e4..867d2dc8 100644 --- a/app/Http/Resources/Admin/PrintRecipientsResource.php +++ b/app/Http/Resources/Admin/PrintRecipientsResource.php @@ -15,4 +15,4 @@ class PrintRecipientsResource extends JsonResource 'table_ready' => (bool) ($this['table_ready'] ?? false), ]; } -} +} \ No newline at end of file diff --git a/app/Http/Resources/Admin/Progress/ProgressGroupResource.php b/app/Http/Resources/Admin/Progress/ProgressGroupResource.php index 8b1c25d9..e768b766 100644 --- a/app/Http/Resources/Admin/Progress/ProgressGroupResource.php +++ b/app/Http/Resources/Admin/Progress/ProgressGroupResource.php @@ -19,4 +19,4 @@ class ProgressGroupResource extends JsonResource 'reports' => $reports, // keyed by subject for compatibility ]; } -} +} \ No newline at end of file diff --git a/app/Http/Resources/Admin/Progress/ProgressReportDetailResource.php b/app/Http/Resources/Admin/Progress/ProgressReportDetailResource.php index 40f31809..67db6e36 100644 --- a/app/Http/Resources/Admin/Progress/ProgressReportDetailResource.php +++ b/app/Http/Resources/Admin/Progress/ProgressReportDetailResource.php @@ -15,4 +15,4 @@ class ProgressReportDetailResource extends JsonResource 'subject_sections' => $this['subject_sections'] ?? [], ]; } -} +} \ No newline at end of file diff --git a/app/Http/Resources/Admin/TeacherSubmissionReportResource.php b/app/Http/Resources/Admin/TeacherSubmissionReportResource.php index 47b673f9..17a1b776 100644 --- a/app/Http/Resources/Admin/TeacherSubmissionReportResource.php +++ b/app/Http/Resources/Admin/TeacherSubmissionReportResource.php @@ -22,4 +22,4 @@ class TeacherSubmissionReportResource extends JsonResource ], ]; } -} +} \ No newline at end of file diff --git a/app/Http/Resources/Attendance/AdminAttendanceResource.php b/app/Http/Resources/Attendance/AdminAttendanceResource.php index 9241743e..c48f9509 100644 --- a/app/Http/Resources/Attendance/AdminAttendanceResource.php +++ b/app/Http/Resources/Attendance/AdminAttendanceResource.php @@ -16,4 +16,4 @@ class AdminAttendanceResource extends JsonResource 'admins_grid' => $this['admins_grid'] ?? [], ]; } -} +} \ No newline at end of file diff --git a/app/Http/Resources/Attendance/DailyAttendanceResource.php b/app/Http/Resources/Attendance/DailyAttendanceResource.php index ef3f5e94..ce0a40ad 100644 --- a/app/Http/Resources/Attendance/DailyAttendanceResource.php +++ b/app/Http/Resources/Attendance/DailyAttendanceResource.php @@ -25,4 +25,4 @@ class DailyAttendanceResource extends JsonResource 'current_admin_name' => $this['currentAdminName'] ?? null, ]; } -} +} \ No newline at end of file diff --git a/app/Http/Resources/Attendance/TeacherAttendanceFormResource.php b/app/Http/Resources/Attendance/TeacherAttendanceFormResource.php index f6b1d306..0e72ddfc 100644 --- a/app/Http/Resources/Attendance/TeacherAttendanceFormResource.php +++ b/app/Http/Resources/Attendance/TeacherAttendanceFormResource.php @@ -9,7 +9,7 @@ class TeacherAttendanceFormResource extends JsonResource { public function toArray(Request $request): array { - $enable = (int) ($this['enable_attendance'] ?? 0); + $enable = (int)($this['enable_attendance'] ?? 0); $res = $this->resource; $hasCanEditKey = is_array($res) && array_key_exists('can_edit', $res); @@ -29,4 +29,4 @@ class TeacherAttendanceFormResource extends JsonResource 'locked_by_student' => $this['locked_by_student'] ?? [], ]; } -} +} \ No newline at end of file diff --git a/app/Http/Resources/Attendance/TeacherGridResource.php b/app/Http/Resources/Attendance/TeacherGridResource.php index 313ab743..3daa03bf 100644 --- a/app/Http/Resources/Attendance/TeacherGridResource.php +++ b/app/Http/Resources/Attendance/TeacherGridResource.php @@ -16,7 +16,7 @@ class TeacherGridResource extends JsonResource 'class_section_id' => $this['class_section_id'] ?? null, 'sections' => $this['sections'] ?? [], 'grid' => $this['grid'] ?? [], - 'locked' => (bool) ($this['locked'] ?? false), + 'locked' => (bool)($this['locked'] ?? false), ]; } -} +} \ No newline at end of file diff --git a/app/Http/Resources/ClassProgress/ClassProgressGroupCollection.php b/app/Http/Resources/ClassProgress/ClassProgressGroupCollection.php index 8ba9c2df..daa20591 100644 --- a/app/Http/Resources/ClassProgress/ClassProgressGroupCollection.php +++ b/app/Http/Resources/ClassProgress/ClassProgressGroupCollection.php @@ -23,9 +23,9 @@ class ClassProgressGroupCollection extends ResourceCollection continue; } - $key = $sectionId.'|'.$weekStart; + $key = $sectionId . '|' . $weekStart; - if (! isset($groups[$key])) { + if (!isset($groups[$key])) { $groups[$key] = [ 'week_start' => $weekStart, 'week_end' => $report->week_end?->format('Y-m-d'), diff --git a/app/Http/Resources/Events/EventChargeResource.php b/app/Http/Resources/Events/EventChargeResource.php index 9b062c88..d9622d53 100644 --- a/app/Http/Resources/Events/EventChargeResource.php +++ b/app/Http/Resources/Events/EventChargeResource.php @@ -14,9 +14,9 @@ class EventChargeResource extends JsonResource 'event_id' => (int) ($this->event_id ?? 0), 'event_name' => $this->event_name ?? null, 'parent_id' => (int) ($this->parent_id ?? 0), - 'parent_name' => trim(($this->parent_firstname ?? '').' '.($this->parent_lastname ?? '')), + 'parent_name' => trim(($this->parent_firstname ?? '') . ' ' . ($this->parent_lastname ?? '')), 'student_id' => (int) ($this->student_id ?? 0), - 'student_name' => trim(($this->student_firstname ?? '').' '.($this->student_lastname ?? '')), + 'student_name' => trim(($this->student_firstname ?? '') . ' ' . ($this->student_lastname ?? '')), 'participation' => $this->getRawOriginal('participation') ?? $this->participation, 'charged' => (float) ($this->getRawOriginal('charged') ?? $this->charged ?? 0), 'school_year' => $this->school_year, diff --git a/app/Http/Resources/Messaging/MessageCollection.php b/app/Http/Resources/Messaging/MessageCollection.php index 7b5c7ad9..3703c1a4 100644 --- a/app/Http/Resources/Messaging/MessageCollection.php +++ b/app/Http/Resources/Messaging/MessageCollection.php @@ -8,7 +8,7 @@ class MessageCollection extends ResourceCollection { public function with($request): array { - if (! method_exists($this->resource, 'total')) { + if (!method_exists($this->resource, 'total')) { return []; } diff --git a/app/Http/Resources/Messaging/MessageResource.php b/app/Http/Resources/Messaging/MessageResource.php index 3c0c0478..3acb8ac3 100644 --- a/app/Http/Resources/Messaging/MessageResource.php +++ b/app/Http/Resources/Messaging/MessageResource.php @@ -24,8 +24,8 @@ class MessageResource extends JsonResource 'status' => $this->status ?? null, 'semester' => $this->semester ?? null, 'school_year' => $this->school_year ?? null, - 'sender_name' => $this->sender ? trim($this->sender->firstname.' '.$this->sender->lastname) : null, - 'recipient_name' => $this->recipient ? trim($this->recipient->firstname.' '.$this->recipient->lastname) : null, + 'sender_name' => $this->sender ? trim($this->sender->firstname . ' ' . $this->sender->lastname) : null, + 'recipient_name' => $this->recipient ? trim($this->recipient->firstname . ' ' . $this->recipient->lastname) : null, ]; } } diff --git a/app/Http/Resources/Payments/PaymentNotificationLogResource.php b/app/Http/Resources/Payments/PaymentNotificationLogResource.php index bec8d35a..78b5a744 100644 --- a/app/Http/Resources/Payments/PaymentNotificationLogResource.php +++ b/app/Http/Resources/Payments/PaymentNotificationLogResource.php @@ -10,7 +10,7 @@ class PaymentNotificationLogResource extends JsonResource public function toArray(Request $request): array { $parent = $this->resource->parent ?? null; - $parentName = $parent ? trim(($parent->firstname ?? '').' '.($parent->lastname ?? '')) : null; + $parentName = $parent ? trim(($parent->firstname ?? '') . ' ' . ($parent->lastname ?? '')) : null; return [ 'id' => (int) ($this->resource->id ?? 0), diff --git a/app/Http/Resources/Promotions/StudentPromotionResource.php b/app/Http/Resources/Promotions/StudentPromotionResource.php index c0711435..ec6455c0 100644 --- a/app/Http/Resources/Promotions/StudentPromotionResource.php +++ b/app/Http/Resources/Promotions/StudentPromotionResource.php @@ -19,7 +19,6 @@ class StudentPromotionResource extends JsonResource if (is_array($r)) { return $r[$key] ?? $default; } - return $r->{$key} ?? $default; }; diff --git a/app/Http/Resources/Reimbursements/ReimbursementRecipientResource.php b/app/Http/Resources/Reimbursements/ReimbursementRecipientResource.php index 0f16fbff..70f2bfd4 100644 --- a/app/Http/Resources/Reimbursements/ReimbursementRecipientResource.php +++ b/app/Http/Resources/Reimbursements/ReimbursementRecipientResource.php @@ -12,7 +12,7 @@ class ReimbursementRecipientResource extends JsonResource $id = (int) ($this->resource['id'] ?? 0); $first = $this->resource['firstname'] ?? ''; $last = $this->resource['lastname'] ?? ''; - $name = trim($first.' '.$last); + $name = trim($first . ' ' . $last); return [ 'id' => $id, diff --git a/app/Http/Resources/Staff/StaffCollection.php b/app/Http/Resources/Staff/StaffCollection.php index 71532432..7eb263e1 100644 --- a/app/Http/Resources/Staff/StaffCollection.php +++ b/app/Http/Resources/Staff/StaffCollection.php @@ -15,7 +15,7 @@ class StaffCollection extends ResourceCollection public function with($request): array { - if (! method_exists($this->resource, 'total')) { + if (!method_exists($this->resource, 'total')) { return []; } diff --git a/app/Http/Resources/Support/SupportRequestCollection.php b/app/Http/Resources/Support/SupportRequestCollection.php index 3066c892..fda04237 100644 --- a/app/Http/Resources/Support/SupportRequestCollection.php +++ b/app/Http/Resources/Support/SupportRequestCollection.php @@ -15,7 +15,7 @@ class SupportRequestCollection extends ResourceCollection public function with($request): array { - if (! method_exists($this->resource, 'total')) { + if (!method_exists($this->resource, 'total')) { return []; } diff --git a/app/Http/Resources/System/NavItemResource.php b/app/Http/Resources/System/NavItemResource.php index 60360056..67b0d82b 100644 --- a/app/Http/Resources/System/NavItemResource.php +++ b/app/Http/Resources/System/NavItemResource.php @@ -20,7 +20,7 @@ class NavItemResource extends JsonResource 'target' => $item['target'] ?? null, 'sort_order' => (int) ($item['sort_order'] ?? 0), 'is_enabled' => (int) ($item['is_enabled'] ?? 0), - 'children' => ! empty($item['children']) + 'children' => !empty($item['children']) ? NavItemResource::collection($item['children']) : [], ]; diff --git a/app/Http/Resources/TeacherSubmissionRowResource.php b/app/Http/Resources/TeacherSubmissionRowResource.php index 9c7e1ef9..c6c15918 100644 --- a/app/Http/Resources/TeacherSubmissionRowResource.php +++ b/app/Http/Resources/TeacherSubmissionRowResource.php @@ -24,4 +24,4 @@ class TeacherSubmissionRowResource extends JsonResource 'missing_items' => $this['missing_items'] ?? [], ]; } -} +} \ No newline at end of file diff --git a/app/Libraries/StaffTimeOffLinkService.php b/app/Libraries/StaffTimeOffLinkService.php index b46e8ed2..e44fc38d 100644 --- a/app/Libraries/StaffTimeOffLinkService.php +++ b/app/Libraries/StaffTimeOffLinkService.php @@ -6,7 +6,9 @@ use App\Services\Staff\StaffTimeOffLinkService as StaffTimeOffLinkTokenService; class StaffTimeOffLinkService { - public function __construct(private StaffTimeOffLinkTokenService $service = new StaffTimeOffLinkTokenService) {} + public function __construct(private StaffTimeOffLinkTokenService $service = new StaffTimeOffLinkTokenService()) + { + } public function createToken(array $payload): string { diff --git a/app/Listeners/AttendanceConsequenceListener.php b/app/Listeners/AttendanceConsequenceListener.php index b8f56b73..1238751c 100644 --- a/app/Listeners/AttendanceConsequenceListener.php +++ b/app/Listeners/AttendanceConsequenceListener.php @@ -6,7 +6,9 @@ use App\Services\Attendance\AttendanceConsequenceService; class AttendanceConsequenceListener { - public function __construct(private AttendanceConsequenceService $service) {} + public function __construct(private AttendanceConsequenceService $service) + { + } public function followUp(array $payload): void { diff --git a/app/Listeners/BelowSixtyEmailListener.php b/app/Listeners/BelowSixtyEmailListener.php index fe021b58..0e9c54b9 100644 --- a/app/Listeners/BelowSixtyEmailListener.php +++ b/app/Listeners/BelowSixtyEmailListener.php @@ -6,7 +6,9 @@ use App\Services\Grading\BelowSixtyEmailService; class BelowSixtyEmailListener { - public function __construct(private BelowSixtyEmailService $service) {} + public function __construct(private BelowSixtyEmailService $service) + { + } public function handle(array $payload): void { diff --git a/app/Listeners/DeleteUnverifiedUserListener.php b/app/Listeners/DeleteUnverifiedUserListener.php index 4108eed6..e84bb7f6 100644 --- a/app/Listeners/DeleteUnverifiedUserListener.php +++ b/app/Listeners/DeleteUnverifiedUserListener.php @@ -7,7 +7,9 @@ use App\Services\Users\DeleteUnverifiedUserService; class DeleteUnverifiedUserListener { - public function __construct(private DeleteUnverifiedUserService $service) {} + public function __construct(private DeleteUnverifiedUserService $service) + { + } public function handle(DeleteUnverifiedUser $event): void { diff --git a/app/Listeners/SchoolEventListener.php b/app/Listeners/SchoolEventListener.php index 2ca41471..3ed583d5 100644 --- a/app/Listeners/SchoolEventListener.php +++ b/app/Listeners/SchoolEventListener.php @@ -12,7 +12,8 @@ class SchoolEventListener private EnrollmentEventService $enrollment, private PaymentEventService $payments, private AccountEventService $accounts - ) {} + ) { + } public function handleAdmissionUnderReview(array $data, array $studentdata): void { diff --git a/app/Listeners/WhatsappInviteListener.php b/app/Listeners/WhatsappInviteListener.php index f7925cd3..da7c4409 100644 --- a/app/Listeners/WhatsappInviteListener.php +++ b/app/Listeners/WhatsappInviteListener.php @@ -7,7 +7,9 @@ use App\Services\Whatsapp\WhatsappInviteEmailService; class WhatsappInviteListener { - public function __construct(private WhatsappInviteEmailService $service) {} + public function __construct(private WhatsappInviteEmailService $service) + { + } public function handle(WhatsappInvitesSend $event): void { diff --git a/app/Mail/TeacherSubmissionReminderMail.php b/app/Mail/TeacherSubmissionReminderMail.php index 7161e567..a1d71a3e 100644 --- a/app/Mail/TeacherSubmissionReminderMail.php +++ b/app/Mail/TeacherSubmissionReminderMail.php @@ -14,7 +14,8 @@ class TeacherSubmissionReminderMail extends Mailable public function __construct( protected string $subjectLine, protected string $htmlBody - ) {} + ) { + } public function build(): self { diff --git a/app/Models/AdditionalCharge.php b/app/Models/AdditionalCharge.php index 91e5f242..1c50fd08 100644 --- a/app/Models/AdditionalCharge.php +++ b/app/Models/AdditionalCharge.php @@ -3,6 +3,7 @@ namespace App\Models; use Illuminate\Database\Eloquent\Builder; +use App\Models\BaseModel; use Illuminate\Support\Facades\DB; class AdditionalCharge extends BaseModel @@ -27,10 +28,10 @@ class AdditionalCharge extends BaseModel ]; protected $casts = [ - 'parent_id' => 'integer', + 'parent_id' => 'integer', 'invoice_id' => 'integer', - 'amount' => 'decimal:2', // adjust if you store more precision - 'due_date' => 'date', // if due_date is DATE/DATETIME + 'amount' => 'decimal:2', // adjust if you store more precision + 'due_date' => 'date', // if due_date is DATE/DATETIME 'created_by' => 'integer', ]; @@ -119,7 +120,7 @@ class AdditionalCharge extends BaseModel $affected = static::query() ->whereIn('id', $ids) ->update([ - 'status' => 'applied', + 'status' => 'applied', 'invoice_id' => $invoiceId, ]); @@ -156,18 +157,18 @@ class AdditionalCharge extends BaseModel $query->where('additional_charges.semester', $semFilter); } - if (! empty($status)) { + if (!empty($status)) { $query->where('additional_charges.status', $status); } - if (! empty($q)) { + if (!empty($q)) { $term = trim($q); $query->where(function ($w) use ($term) { $w->where('additional_charges.title', 'like', "%{$term}%") - ->orWhere('additional_charges.description', 'like', "%{$term}%") - ->orWhere('users.firstname', 'like', "%{$term}%") - ->orWhere('users.lastname', 'like', "%{$term}%") - ->orWhere('invoices.invoice_number', 'like', "%{$term}%"); + ->orWhere('additional_charges.description', 'like', "%{$term}%") + ->orWhere('users.firstname', 'like', "%{$term}%") + ->orWhere('users.lastname', 'like', "%{$term}%") + ->orWhere('invoices.invoice_number', 'like', "%{$term}%"); }); } diff --git a/app/Models/AdminNotificationSubject.php b/app/Models/AdminNotificationSubject.php index 747ccf6f..469a54c3 100644 --- a/app/Models/AdminNotificationSubject.php +++ b/app/Models/AdminNotificationSubject.php @@ -2,6 +2,8 @@ namespace App\Models; +use App\Models\BaseModel; + class AdminNotificationSubject extends BaseModel { protected $table = 'admin_notification_subjects'; @@ -14,8 +16,7 @@ class AdminNotificationSubject extends BaseModel ]; // legacy uses created_at / updated_at - public $timestamps = true; - + public $timestamps = true; protected $casts = [ 'admin_id' => 'integer', ]; @@ -25,4 +26,4 @@ class AdminNotificationSubject extends BaseModel { return $this->belongsTo(User::class, 'admin_id'); } -} +} \ No newline at end of file diff --git a/app/Models/AttendanceCommentTemplate.php b/app/Models/AttendanceCommentTemplate.php index 96551754..bfd0df1b 100644 --- a/app/Models/AttendanceCommentTemplate.php +++ b/app/Models/AttendanceCommentTemplate.php @@ -2,6 +2,8 @@ namespace App\Models; +use App\Models\BaseModel; + class AttendanceCommentTemplate extends BaseModel { protected $table = 'attendance_comment_template'; @@ -41,4 +43,4 @@ class AttendanceCommentTemplate extends BaseModel { return $q->where('is_active', 1); } -} +} \ No newline at end of file diff --git a/app/Models/AttendanceData.php b/app/Models/AttendanceData.php index a5e55d94..51770c6c 100644 --- a/app/Models/AttendanceData.php +++ b/app/Models/AttendanceData.php @@ -2,13 +2,13 @@ namespace App\Models; +use App\Models\BaseModel; use Illuminate\Support\Carbon; use Illuminate\Support\Facades\DB; class AttendanceData extends BaseModel { protected $table = 'attendance_data'; - public $timestamps = true; // uses created_at / updated_at protected $fillable = [ @@ -30,12 +30,12 @@ class AttendanceData extends BaseModel ]; protected $casts = [ - 'class_id' => 'integer', + 'class_id' => 'integer', 'class_section_id' => 'integer', - 'school_id' => 'integer', - 'student_id' => 'integer', - 'modified_by' => 'integer', - 'date' => 'datetime', // ✅ DATETIME column + 'school_id' => 'integer', + 'student_id' => 'integer', + 'modified_by' => 'integer', + 'date' => 'datetime', // ✅ DATETIME column ]; /* ========================= @@ -59,7 +59,6 @@ class AttendanceData extends BaseModel $day = substr($day, 0, 10); // YYYY-MM-DD $start = Carbon::parse($day)->startOfDay(); $endEx = Carbon::parse($day)->addDay()->startOfDay(); // exclusive - return [$start, $endEx]; } @@ -88,7 +87,6 @@ class AttendanceData extends BaseModel public static function updateAttendance(int $attendanceId, array $data): bool { $affected = static::query()->whereKey($attendanceId)->update($data); - return $affected >= 0; } @@ -115,7 +113,7 @@ class AttendanceData extends BaseModel $q = static::query(); static::applyReportedFilter($q); - [$start] = static::dayRange($startDate); + [$start, ] = static::dayRange($startDate); if ($endDate) { [, $endEx] = static::dayRange($endDate); // end exclusive of next day @@ -136,19 +134,13 @@ class AttendanceData extends BaseModel ->where('date', '>=', $start) ->where('date', '<', $endEx) ->where(function ($w) { - $w->whereIn(DB::raw('UPPER(TRIM(status))'), ['ABSENT', 'ABS', 'A']) - ->orWhereRaw("UPPER(TRIM(status)) LIKE 'ABS%'"); + $w->whereIn(DB::raw("UPPER(TRIM(status))"), ['ABSENT', 'ABS', 'A']) + ->orWhereRaw("UPPER(TRIM(status)) LIKE 'ABS%'"); }); - if ($classSectionId !== null) { - $q->where('class_section_id', $classSectionId); - } - if ($schoolYear !== null) { - $q->where('school_year', $schoolYear); - } - if ($semester !== null) { - $q->where('semester', $semester); - } + if ($classSectionId !== null) $q->where('class_section_id', $classSectionId); + if ($schoolYear !== null) $q->where('school_year', $schoolYear); + if ($semester !== null) $q->where('semester', $semester); return $q->get(); } @@ -161,19 +153,13 @@ class AttendanceData extends BaseModel ->where('date', '>=', $start) ->where('date', '<', $endEx) ->where(function ($w) { - $w->whereIn(DB::raw('UPPER(TRIM(status))'), ['LATE', 'L']) - ->orWhereRaw("UPPER(TRIM(status)) LIKE 'LATE%'"); + $w->whereIn(DB::raw("UPPER(TRIM(status))"), ['LATE', 'L']) + ->orWhereRaw("UPPER(TRIM(status)) LIKE 'LATE%'"); }); - if ($classSectionId !== null) { - $q->where('class_section_id', $classSectionId); - } - if ($schoolYear !== null) { - $q->where('school_year', $schoolYear); - } - if ($semester !== null) { - $q->where('semester', $semester); - } + if ($classSectionId !== null) $q->where('class_section_id', $classSectionId); + if ($schoolYear !== null) $q->where('school_year', $schoolYear); + if ($semester !== null) $q->where('semester', $semester); return $q->get(); } @@ -185,8 +171,8 @@ class AttendanceData extends BaseModel ->where('school_year', $schoolYear) ->where('semester', $semester) ->where(function ($w) { - $w->whereIn(DB::raw('UPPER(TRIM(status))'), ['ABSENT', 'ABS', 'A']) - ->orWhereRaw("UPPER(TRIM(status)) LIKE 'ABS%'"); + $w->whereIn(DB::raw("UPPER(TRIM(status))"), ['ABSENT', 'ABS', 'A']) + ->orWhereRaw("UPPER(TRIM(status)) LIKE 'ABS%'"); }) ->orderByDesc('date') ->get(); @@ -199,26 +185,22 @@ class AttendanceData extends BaseModel public static function getStudentAttendance( int $studentId, ?string $startDate = null, - ?string $endDate = null, + ?string $endDate = null, ?string $schoolYear = null, - ?string $semester = null + ?string $semester = null ) { $q = static::query()->where('student_id', $studentId); - if (! empty($startDate)) { - [$start] = static::dayRange($startDate); + if (!empty($startDate)) { + [$start, ] = static::dayRange($startDate); $q->where('date', '>=', $start); } - if (! empty($endDate)) { + if (!empty($endDate)) { [, $endEx] = static::dayRange($endDate); $q->where('date', '<', $endEx); } - if (! empty($schoolYear)) { - $q->where('school_year', $schoolYear); - } - if (! empty($semester)) { - $q->where('semester', $semester); - } + if (!empty($schoolYear)) $q->where('school_year', $schoolYear); + if (!empty($semester)) $q->where('semester', $semester); return $q->orderByDesc('date')->get(); } @@ -243,13 +225,13 @@ class AttendanceData extends BaseModel public static function unreportedTermRows(array $studentIds, string $schoolYear, ?string $semester) { $q = static::query() - ->select(['id', 'student_id', 'class_id', 'class_section_id', 'date', 'status', 'reason', 'school_year', 'semester', 'is_reported']) + ->select(['id','student_id','class_id','class_section_id','date','status','reason','school_year','semester','is_reported']) ->where('school_year', $schoolYear); if ($semester !== null && $semester !== '') { $q->where('semester', $semester); } - if (! empty($studentIds)) { + if (!empty($studentIds)) { $q->whereIn('student_id', array_map('intval', $studentIds)); } @@ -265,30 +247,26 @@ class AttendanceData extends BaseModel public static function getUnreportedAbsencesAndLates(?string $schoolYear = null, ?string $semester = null): array { $q = static::query() - ->select(['id', 'student_id', 'class_id', 'class_section_id', 'date', 'status', 'reason', 'school_year', 'semester', 'is_reported']); + ->select(['id','student_id','class_id','class_section_id','date','status','reason','school_year','semester','is_reported']); static::applyUnreportedFilter($q); // Not notified yet: supports 0/'0'/NULL and string enums like 'yes' $q->where(function ($w) { $w->whereNull('is_notified') - ->orWhere('is_notified', 0) - ->orWhere('is_notified', '0') - ->orWhereRaw("LOWER(TRIM(COALESCE(is_notified, ''))) != 'yes'"); + ->orWhere('is_notified', 0) + ->orWhere('is_notified', '0') + ->orWhereRaw("LOWER(TRIM(COALESCE(is_notified, ''))) != 'yes'"); }); - if (! empty($schoolYear)) { - $q->where('school_year', $schoolYear); - } - if (! empty($semester)) { - $q->where('semester', $semester); - } + if (!empty($schoolYear)) $q->where('school_year', $schoolYear); + if (!empty($semester)) $q->where('semester', $semester); // Status filter: exact + prefixes (ABS_3, LATE_2, etc.) $q->where(function ($w) { - $w->whereIn(DB::raw('UPPER(TRIM(status))'), ['ABS', 'ABSENT', 'LATE', 'A', 'L']) - ->orWhereRaw("UPPER(TRIM(status)) LIKE 'ABS%'") - ->orWhereRaw("UPPER(TRIM(status)) LIKE 'LATE%'"); + $w->whereIn(DB::raw("UPPER(TRIM(status))"), ['ABS','ABSENT','LATE','A','L']) + ->orWhereRaw("UPPER(TRIM(status)) LIKE 'ABS%'") + ->orWhereRaw("UPPER(TRIM(status)) LIKE 'LATE%'"); }); $rows = $q->orderBy('student_id')->orderByDesc('date')->get()->toArray(); @@ -297,27 +275,27 @@ class AttendanceData extends BaseModel foreach ($rows as $r) { $sid = (int) ($r['student_id'] ?? 0); - if (! isset($out[$sid])) { + if (!isset($out[$sid])) { $out[$sid] = [ 'absences' => [], - 'lates' => [], - 'counts' => ['absences' => 0, 'lates' => 0, 'total' => 0], + 'lates' => [], + 'counts' => ['absences' => 0, 'lates' => 0, 'total' => 0], ]; } - $st = strtoupper(trim((string) ($r['status'] ?? ''))); - $isAbs = ($st === 'ABS' || $st === 'ABSENT' || $st === 'A' || str_starts_with($st, 'ABS')); + $st = strtoupper(trim((string)($r['status'] ?? ''))); + $isAbs = ($st === 'ABS' || $st === 'ABSENT' || $st === 'A' || str_starts_with($st, 'ABS')); $isLate = ($st === 'LATE' || $st === 'L' || str_starts_with($st, 'LATE')); $rec = [ - 'id' => $r['id'] ?? null, - 'date' => $r['date'] ?? null, - 'status' => $r['status'] ?? null, - 'reason' => $r['reason'] ?? null, - 'class_id' => $r['class_id'] ?? null, + 'id' => $r['id'] ?? null, + 'date' => $r['date'] ?? null, + 'status' => $r['status'] ?? null, + 'reason' => $r['reason'] ?? null, + 'class_id' => $r['class_id'] ?? null, 'class_section_id' => $r['class_section_id'] ?? null, - 'school_year' => $r['school_year'] ?? null, - 'semester' => $r['semester'] ?? null, + 'school_year' => $r['school_year'] ?? null, + 'semester' => $r['semester'] ?? null, ]; if ($isAbs) { @@ -351,16 +329,12 @@ class AttendanceData extends BaseModel ->where('date', '>=', $start) ->where('date', '<', $endEx); - if (! empty($semester)) { - $q->where('semester', $semester); - } - if (! empty($schoolYear)) { - $q->where('school_year', $schoolYear); - } + if (!empty($semester)) $q->where('semester', $semester); + if (!empty($schoolYear)) $q->where('school_year', $schoolYear); $affected = $q->update([ 'is_notified' => 'yes', - 'updated_at' => now(), + 'updated_at' => now(), ]); return $affected >= 0; @@ -375,20 +349,14 @@ class AttendanceData extends BaseModel return is_string($d) && preg_match('/^\d{4}-\d{2}-\d{2}$/', $d); }))); - if (empty($dates)) { - return true; - } + if (empty($dates)) return true; $q = static::query() ->where('student_id', $studentId) ->whereRaw("LOWER(TRIM(status)) IN ('absent','late')"); - if (! empty($semester)) { - $q->where('semester', $semester); - } - if (! empty($schoolYear)) { - $q->where('school_year', $schoolYear); - } + if (!empty($semester)) $q->where('semester', $semester); + if (!empty($schoolYear)) $q->where('school_year', $schoolYear); // OR day ranges instead of DATE(date) $q->where(function ($w) use ($dates) { @@ -402,17 +370,13 @@ class AttendanceData extends BaseModel }); $payload = [ - 'updated_at' => now(), + 'updated_at' => now(), 'is_notified' => 'yes', ]; $cols = static::reportedColumns(); - if (! empty($cols['is_reported'])) { - $payload['is_reported'] = 'yes'; - } - if (! empty($cols['reported'])) { - $payload['reported'] = 1; - } + if (!empty($cols['is_reported'])) $payload['is_reported'] = 'yes'; + if (!empty($cols['reported'])) $payload['reported'] = 1; $affected = $q->update($payload); @@ -430,9 +394,7 @@ class AttendanceData extends BaseModel ?string $semester = null, ?string $schoolYear = null ): array { - if ($studentId <= 0 || empty($statuses)) { - return []; - } + if ($studentId <= 0 || empty($statuses)) return []; $statuses = array_map('strtolower', $statuses); @@ -441,7 +403,7 @@ class AttendanceData extends BaseModel : now()->toDateString(); $startDay = Carbon::parse($day)->subDays(max(0, $lookbackDays))->toDateString(); - [$start] = static::dayRange($startDay); + [$start, ] = static::dayRange($startDay); [, $endEx] = static::dayRange($day); $q = static::query() @@ -449,21 +411,17 @@ class AttendanceData extends BaseModel ->where('student_id', $studentId) ->where('date', '>=', $start) ->where('date', '<', $endEx) - ->whereIn(DB::raw('LOWER(TRIM(status))'), $statuses); + ->whereIn(DB::raw("LOWER(TRIM(status))"), $statuses); static::applyUnreportedFilter($q); - if (! empty($semester)) { - $q->where('semester', $semester); - } - if (! empty($schoolYear)) { - $q->where('school_year', $schoolYear); - } + if (!empty($semester)) $q->where('semester', $semester); + if (!empty($schoolYear)) $q->where('school_year', $schoolYear); $rows = $q->orderByDesc('date')->pluck('date')->all(); return array_values(array_unique(array_map(static function ($d) { - return substr((string) $d, 0, 10); // YYYY-MM-DD + return substr((string)$d, 0, 10); // YYYY-MM-DD }, $rows))); } @@ -474,15 +432,13 @@ class AttendanceData extends BaseModel private static function reportedColumns(): array { static $cache = null; - if ($cache !== null) { - return $cache; - } + if ($cache !== null) return $cache; $table = (new static)->getTable(); $cache = [ 'is_reported' => static::columnExists($table, 'is_reported'), - 'reported' => static::columnExists($table, 'reported'), + 'reported' => static::columnExists($table, 'reported'), ]; return $cache; @@ -508,16 +464,16 @@ class AttendanceData extends BaseModel { $cols = static::reportedColumns(); - if (! empty($cols['is_reported'])) { + if (!empty($cols['is_reported'])) { $q->where(function ($w) { $w->whereNull('is_reported') - ->orWhere('is_reported', 0) - ->orWhere('is_reported', '0') - ->orWhereRaw("LOWER(TRIM(COALESCE(is_reported, ''))) = 'no'"); + ->orWhere('is_reported', 0) + ->orWhere('is_reported', '0') + ->orWhereRaw("LOWER(TRIM(COALESCE(is_reported, ''))) = 'no'"); }); } - if (! empty($cols['reported'])) { + if (!empty($cols['reported'])) { $q->whereRaw("(LOWER(TRIM(COALESCE(reported, ''))) NOT IN ('yes','1','true','y','on'))"); } } @@ -529,8 +485,8 @@ class AttendanceData extends BaseModel { $q->where(function ($w) { $w->where('is_reported', 1) - ->orWhere('is_reported', '1') - ->orWhereRaw("LOWER(TRIM(COALESCE(is_reported, ''))) = 'yes'"); + ->orWhere('is_reported', '1') + ->orWhereRaw("LOWER(TRIM(COALESCE(is_reported, ''))) = 'yes'"); }); } } diff --git a/app/Models/AttendanceDay.php b/app/Models/AttendanceDay.php index 4c82dfea..b9c00c22 100644 --- a/app/Models/AttendanceDay.php +++ b/app/Models/AttendanceDay.php @@ -2,8 +2,9 @@ namespace App\Models; -use Illuminate\Database\QueryException; +use App\Models\BaseModel; use Illuminate\Support\Carbon; +use Illuminate\Database\QueryException; class AttendanceDay extends BaseModel { @@ -38,20 +39,20 @@ class AttendanceDay extends BaseModel protected $casts = [ 'class_section_id' => 'integer', - 'submitted_by' => 'integer', - 'published_by' => 'integer', - 'reopened_by' => 'integer', + 'submitted_by' => 'integer', + 'published_by' => 'integer', + 'reopened_by' => 'integer', // If these are DATETIME columns: - 'submitted_at' => 'datetime', - 'published_at' => 'datetime', - 'auto_publish_at' => 'datetime', - 'reopened_at' => 'datetime', - 'created_at' => 'datetime', - 'updated_at' => 'datetime', + 'submitted_at' => 'datetime', + 'published_at' => 'datetime', + 'auto_publish_at' => 'datetime', + 'reopened_at' => 'datetime', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', // If date is stored as DATE string; if it's DATETIME, change to 'datetime' - 'date' => 'date', + 'date' => 'date', ]; /* ========================= @@ -74,15 +75,11 @@ class AttendanceDay extends BaseModel ->whereDate('date', $date) ->where(function ($w) { $w->where('status', 'published') - ->orWhere('status', 'finalized'); // legacy + ->orWhere('status', 'finalized'); // legacy }); - if ($semester !== null) { - $q->where('semester', $semester); - } - if ($schoolYear !== null) { - $q->where('school_year', $schoolYear); - } + if ($semester !== null) $q->where('semester', $semester); + if ($schoolYear !== null) $q->where('school_year', $schoolYear); return $q->exists(); } @@ -116,9 +113,7 @@ class AttendanceDay extends BaseModel int $userId = 0 ): self { $row = static::findBySectionDate($classSectionId, $date, $semester, $schoolYear); - if ($row) { - return $row; - } + if ($row) return $row; $now = now(); $normalizedDate = Carbon::parse($date)->toDateString(); @@ -126,27 +121,25 @@ class AttendanceDay extends BaseModel try { return static::create([ 'class_section_id' => $classSectionId, - 'date' => $normalizedDate, - 'status' => 'draft', - 'submitted_by' => null, - 'submitted_at' => null, - 'published_by' => null, - 'published_at' => null, - 'auto_publish_at' => null, - 'reopened_by' => null, - 'reopened_at' => null, - 'reopen_reason' => null, - 'semester' => $semester, - 'school_year' => $schoolYear, - 'created_at' => $now, - 'updated_at' => $now, + 'date' => $normalizedDate, + 'status' => 'draft', + 'submitted_by' => null, + 'submitted_at' => null, + 'published_by' => null, + 'published_at' => null, + 'auto_publish_at' => null, + 'reopened_by' => null, + 'reopened_at' => null, + 'reopen_reason' => null, + 'semester' => $semester, + 'school_year' => $schoolYear, + 'created_at' => $now, + 'updated_at' => $now, ]); } catch (QueryException $e) { // Race: someone inserted same unique key. $row = static::findBySectionDate($classSectionId, $date, $semester, $schoolYear); - if ($row) { - return $row; - } + if ($row) return $row; throw $e; } } @@ -167,10 +160,10 @@ class AttendanceDay extends BaseModel $now = now(); $payload = [ - 'status' => 'submitted', + 'status' => 'submitted', 'submitted_by' => $userId, 'submitted_at' => $now, - 'updated_at' => $now, + 'updated_at' => $now, ]; if ($autoPublishAt !== null) { @@ -188,10 +181,10 @@ class AttendanceDay extends BaseModel $now = now(); return static::query()->whereKey($this->id)->update([ - 'status' => 'published', + 'status' => 'published', 'published_by' => $userId, 'published_at' => $now, - 'updated_at' => $now, + 'updated_at' => $now, ]) >= 0; } @@ -200,18 +193,18 @@ class AttendanceDay extends BaseModel */ public function reopen(int $userId, string $toStatus = 'draft', ?string $reason = null): bool { - if (! in_array($toStatus, ['draft', 'submitted'], true)) { + if (!in_array($toStatus, ['draft', 'submitted'], true)) { throw new \InvalidArgumentException('toStatus must be "draft" or "submitted".'); } $now = now(); return static::query()->whereKey($this->id)->update([ - 'status' => $toStatus, - 'reopened_by' => $userId, - 'reopened_at' => $now, + 'status' => $toStatus, + 'reopened_by' => $userId, + 'reopened_at' => $now, 'reopen_reason' => $reason, - 'updated_at' => $now, + 'updated_at' => $now, ]) >= 0; } } diff --git a/app/Models/AttendanceEmailTemplate.php b/app/Models/AttendanceEmailTemplate.php index 43b4f0a4..99496cb7 100644 --- a/app/Models/AttendanceEmailTemplate.php +++ b/app/Models/AttendanceEmailTemplate.php @@ -2,6 +2,8 @@ namespace App\Models; +use App\Models\BaseModel; + class AttendanceEmailTemplate extends BaseModel { protected $table = 'email_templates'; @@ -20,7 +22,7 @@ class AttendanceEmailTemplate extends BaseModel ]; protected $casts = [ - 'is_active' => 'boolean', + 'is_active' => 'boolean', 'updated_by' => 'integer', ]; diff --git a/app/Models/AttendanceRecord.php b/app/Models/AttendanceRecord.php index 2f3a46d4..79756eec 100644 --- a/app/Models/AttendanceRecord.php +++ b/app/Models/AttendanceRecord.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Models\BaseModel; use Illuminate\Support\Facades\DB; class AttendanceRecord extends BaseModel @@ -27,14 +28,14 @@ class AttendanceRecord extends BaseModel ]; protected $casts = [ - 'class_section_id' => 'integer', - 'student_id' => 'integer', - 'school_id' => 'integer', - 'total_absence' => 'integer', - 'total_late' => 'integer', - 'total_presence' => 'integer', - 'total_attendance' => 'integer', - 'modified_by' => 'integer', + 'class_section_id' => 'integer', + 'student_id' => 'integer', + 'school_id' => 'integer', + 'total_absence' => 'integer', + 'total_late' => 'integer', + 'total_presence' => 'integer', + 'total_attendance' => 'integer', + 'modified_by' => 'integer', ]; /* ========================= @@ -85,7 +86,6 @@ class AttendanceRecord extends BaseModel public static function updateAttendanceRecord(int $attendanceId, array $data): bool { $affected = static::query()->whereKey($attendanceId)->update($data); - return $affected >= 0; } @@ -101,26 +101,20 @@ class AttendanceRecord extends BaseModel string $schoolYear, array $increments ): bool { - $allowed = ['total_absence', 'total_late', 'total_presence', 'total_attendance']; + $allowed = ['total_absence','total_late','total_presence','total_attendance']; $inc = array_intersect_key($increments, array_flip($allowed)); - if (empty($inc)) { - return true; - } + if (empty($inc)) return true; // Build atomic update: field = field + X $update = []; foreach ($inc as $field => $value) { $value = (int) $value; - if ($value === 0) { - continue; - } + if ($value === 0) continue; $update[$field] = DB::raw("COALESCE($field,0) + {$value}"); } - if (empty($update)) { - return true; - } + if (empty($update)) return true; $affected = static::query() ->where('student_id', $studentId) @@ -145,4 +139,4 @@ class AttendanceRecord extends BaseModel ->where('school_year', $schoolYear) ->sum('total_absence'); } -} +} \ No newline at end of file diff --git a/app/Models/AttendanceTracking.php b/app/Models/AttendanceTracking.php index 496a680a..c7148bd9 100644 --- a/app/Models/AttendanceTracking.php +++ b/app/Models/AttendanceTracking.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Models\BaseModel; use Illuminate\Support\Carbon; use Illuminate\Support\Facades\DB; @@ -25,11 +26,11 @@ class AttendanceTracking extends BaseModel ]; protected $casts = [ - 'student_id' => 'integer', - 'date' => 'datetime', // legacy dateFormat = datetime - 'is_reported' => 'integer', - 'is_notified' => 'integer', - 'notif_counter' => 'integer', + 'student_id' => 'integer', + 'date' => 'datetime', // legacy dateFormat = datetime + 'is_reported' => 'integer', + 'is_notified' => 'integer', + 'notif_counter' => 'integer', ]; /* ----------------------- Helpers ----------------------- */ @@ -41,7 +42,7 @@ class AttendanceTracking extends BaseModel return $date; } if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) { - return $date.' 00:00:00'; + return $date . ' 00:00:00'; } try { @@ -60,7 +61,6 @@ class AttendanceTracking extends BaseModel if ($schoolYear !== null && $schoolYear !== '') { $q->where('school_year', $schoolYear); } - return $q; } @@ -77,8 +77,8 @@ class AttendanceTracking extends BaseModel $y = (int) $d->format('Y'); $semester = ($m >= 8 && $m <= 12) ? 'Fall' : 'Spring'; - $startY = ($m >= 8) ? $y : $y - 1; - $endY = $startY + 1; + $startY = ($m >= 8) ? $y : $y - 1; + $endY = $startY + 1; return [$semester, sprintf('%d-%d', $startY, $endY)]; } @@ -89,7 +89,6 @@ class AttendanceTracking extends BaseModel $day = substr($ymd, 0, 10); $start = Carbon::parse($day, 'UTC')->startOfDay(); $endEx = Carbon::parse($day, 'UTC')->addDay()->startOfDay(); // exclusive - return [$start, $endEx]; } @@ -112,19 +111,19 @@ class AttendanceTracking extends BaseModel if ($semester === null || $schoolYear === null) { [$sem, $yr] = static::deriveTermFromDate($dt); - $semester = $semester ?? $sem; + $semester = $semester ?? $sem; $schoolYear = $schoolYear ?? $yr; } $row = static::create([ - 'student_id' => $studentId, - 'date' => $dt, - 'is_reported' => $isExcused ? 1 : 0, - 'reason' => $reason, - 'is_notified' => 0, - 'notif_counter' => 0, - 'semester' => $semester, - 'school_year' => $schoolYear, + 'student_id' => $studentId, + 'date' => $dt, + 'is_reported' => $isExcused ? 1 : 0, + 'reason' => $reason, + 'is_notified' => 0, + 'notif_counter' => 0, + 'semester' => $semester, + 'school_year' => $schoolYear, ]); return (int) $row->id; @@ -145,7 +144,7 @@ class AttendanceTracking extends BaseModel if ($startDate) { if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $startDate)) { - [$start] = static::dayRange($startDate); + [$start, ] = static::dayRange($startDate); $q->where('date', '>=', $start); } else { $q->where('date', '>=', static::normalizeDateTime($startDate)); @@ -202,7 +201,7 @@ class AttendanceTracking extends BaseModel ?string $schoolYear = null ): bool { $startDay = Carbon::now('UTC')->subDays($days)->toDateString(); - [$start] = static::dayRange($startDay); + [$start, ] = static::dayRange($startDay); $q = static::query() ->where('student_id', $studentId) @@ -232,17 +231,13 @@ class AttendanceTracking extends BaseModel $absences = $q->orderByDesc('date')->limit($threshold)->get()->toArray(); - if (count($absences) < $threshold) { - return false; - } + if (count($absences) < $threshold) return false; for ($i = 0; $i < $threshold - 1; $i++) { $d1 = new \DateTime($absences[$i]['date']); $d2 = new \DateTime($absences[$i + 1]['date']); $diffDays = (int) $d2->diff($d1)->format('%a'); - if ($diffDays !== 7) { - return false; - } + if ($diffDays !== 7) return false; } return true; @@ -269,13 +264,11 @@ class AttendanceTracking extends BaseModel $records = $q->orderByDesc('date')->limit($weeksStreak)->get()->toArray(); - if (count($records) < $weeksStreak) { - return false; - } + if (count($records) < $weeksStreak) return false; $count = 0; foreach ($records as $r) { - if ((int) ($r['is_reported'] ?? 0) === 0 && (int) ($r['is_notified'] ?? 0) === 0) { + if ((int)($r['is_reported'] ?? 0) === 0 && (int)($r['is_notified'] ?? 0) === 0) { $count++; } } @@ -304,9 +297,9 @@ class AttendanceTracking extends BaseModel $last = $q3->orderByDesc('date')->first(); return [ - 'total_absences' => $total, + 'total_absences' => $total, 'unexcused_absences' => $unexcused, - 'last_absence' => $last ? $last->toArray() : null, + 'last_absence' => $last ? $last->toArray() : null, ]; } @@ -320,10 +313,7 @@ class AttendanceTracking extends BaseModel ) { $q = static::query(); static::applyTermFilters($q, $semester, $schoolYear); - if ($limit > 0) { - $q->limit($limit); - } - + if ($limit > 0) $q->limit($limit); return $q->orderByDesc('date')->get(); } @@ -345,8 +335,8 @@ class AttendanceTracking extends BaseModel $students = (int) $q3->distinct('student_id')->count('student_id'); return [ - 'total_absences' => $total, - 'unexcused_absences' => $unexcused, + 'total_absences' => $total, + 'unexcused_absences' => $unexcused, 'students_with_absences' => $students, ]; } @@ -362,28 +352,26 @@ class AttendanceTracking extends BaseModel foreach ($studentsWithViolations as $student) { if (empty($student['absences'])) { $results['skipped']++; - continue; } foreach ($student['absences'] as $absence) { - $ymd = substr((string) ($absence['date'] ?? now()->toDateString()), 0, 10); + $ymd = substr((string)($absence['date'] ?? now()->toDateString()), 0, 10); [$start, $endEx] = static::dayRange($ymd); $data = [ - 'student_id' => (int) ($student['id'] ?? 0), - 'date' => static::normalizeDateTime($ymd), - 'is_reported' => 0, - 'reason' => (string) ($student['violation'] ?? ''), - 'is_notified' => 0, - 'notif_counter' => 0, - 'semester' => $semester, - 'school_year' => $schoolYear, + 'student_id' => (int)($student['id'] ?? 0), + 'date' => static::normalizeDateTime($ymd), + 'is_reported' => 0, + 'reason' => (string)($student['violation'] ?? ''), + 'is_notified' => 0, + 'notif_counter' => 0, + 'semester' => $semester, + 'school_year' => $schoolYear, ]; if ($data['student_id'] <= 0) { $results['skipped']++; - continue; } @@ -403,8 +391,8 @@ class AttendanceTracking extends BaseModel $existing->fill($data)->save(); $results['updated']++; - if (! empty($existing->is_notified)) { - static::resetNotification((int) $existing->id); + if (!empty($existing->is_notified)) { + static::resetNotification((int)$existing->id); $results['notified']++; } } else { @@ -416,7 +404,7 @@ class AttendanceTracking extends BaseModel // Only match standalone "4" if (preg_match('/\b4\b/', $data['reason'])) { - static::markAsNotified((int) $new->id); + static::markAsNotified((int)$new->id); $results['notified']++; } } @@ -439,8 +427,8 @@ class AttendanceTracking extends BaseModel ->whereKey($id) ->update([ 'notif_counter' => DB::raw('COALESCE(notif_counter,0)+1'), - 'is_notified' => 1, - 'updated_at' => now(), + 'is_notified' => 1, + 'updated_at' => now(), ]); return $affected > 0; @@ -451,9 +439,9 @@ class AttendanceTracking extends BaseModel $affected = static::query() ->whereKey($id) ->update([ - 'is_notified' => 0, + 'is_notified' => 0, 'notif_counter' => 0, - 'updated_at' => now(), + 'updated_at' => now(), ]); return $affected >= 0; @@ -475,7 +463,7 @@ class AttendanceTracking extends BaseModel // Here we require term to be passed or derived from date. if ($semester === null || $schoolYear === null) { [$sem, $yr] = static::deriveTermFromDate($ymd); - $semester = $semester ?? $sem; + $semester = $semester ?? $sem; $schoolYear = $schoolYear ?? $yr; } @@ -487,33 +475,33 @@ class AttendanceTracking extends BaseModel ->where('semester', $semester) ->where('school_year', $schoolYear) ->where('date', '>=', $start) - ->where('date', '<', $endEx) + ->where('date', '<', $endEx) ->where('reason', 'like', "%{$code}%") ->orderByDesc('date') ->first(); // 2) Fallback: match by day only - if (! $row) { + if (!$row) { $row = static::query() ->where('student_id', $studentId) ->where('semester', $semester) ->where('school_year', $schoolYear) ->where('date', '>=', $start) - ->where('date', '<', $endEx) + ->where('date', '<', $endEx) ->orderByDesc('date') ->first(); } if ($row) { - $counter = ((int) ($row->notif_counter ?? 0)) + 1; + $counter = ((int)($row->notif_counter ?? 0)) + 1; $affected = static::query() - ->whereKey((int) $row->id) + ->whereKey((int)$row->id) ->update([ - 'is_notified' => 1, + 'is_notified' => 1, 'notif_counter' => $counter, - 'is_reported' => 0, - 'updated_at' => now(), + 'is_reported' => 0, + 'updated_at' => now(), ]); return $affected > 0; @@ -521,18 +509,18 @@ class AttendanceTracking extends BaseModel // Insert a row so future runs see it as notified $created = static::create([ - 'student_id' => $studentId, - 'date' => static::normalizeDateTime($ymd), - 'is_reported' => 0, - 'reason' => $code, - 'is_notified' => 1, - 'notif_counter' => 1, - 'semester' => $semester, - 'school_year' => $schoolYear, - 'note' => null, + 'student_id' => $studentId, + 'date' => static::normalizeDateTime($ymd), + 'is_reported' => 0, + 'reason' => $code, + 'is_notified' => 1, + 'notif_counter' => 1, + 'semester' => $semester, + 'school_year' => $schoolYear, + 'note' => null, ]); - return ! empty($created->id); + return !empty($created->id); } /** @@ -550,11 +538,11 @@ class AttendanceTracking extends BaseModel ->where('student_id', $studentId) ->where('school_year', $schoolYear); - if (! empty($semester)) { + if (!empty($semester)) { $q->where('semester', $semester); } - if (! empty($lastDate)) { + if (!empty($lastDate)) { $q->where('date', '<=', static::normalizeDateTime($lastDate)); } @@ -562,20 +550,18 @@ class AttendanceTracking extends BaseModel ->orderByDesc('date') ->first(); - if (! $row) { - return 0; - } + if (!$row) return 0; - $counter = ((int) ($row->notif_counter ?? 0)) + 1; + $counter = ((int)($row->notif_counter ?? 0)) + 1; $affected = static::query() - ->whereKey((int) $row->id) + ->whereKey((int)$row->id) ->update([ - 'is_notified' => 1, + 'is_notified' => 1, 'notif_counter' => $counter, - 'updated_at' => now(), + 'updated_at' => now(), ]); return $affected > 0 ? 1 : 0; } -} +} \ No newline at end of file diff --git a/app/Models/AuthorizedUser.php b/app/Models/AuthorizedUser.php index 3f9333c8..d51ddb81 100644 --- a/app/Models/AuthorizedUser.php +++ b/app/Models/AuthorizedUser.php @@ -2,6 +2,8 @@ namespace App\Models; +use App\Models\BaseModel; + class AuthorizedUser extends BaseModel { protected $table = 'authorized_users'; @@ -25,7 +27,7 @@ class AuthorizedUser extends BaseModel ]; protected $casts = [ - 'user_id' => 'integer', + 'user_id' => 'integer', 'authorized_user_id' => 'integer', ]; @@ -42,4 +44,4 @@ class AuthorizedUser extends BaseModel { return $this->belongsTo(User::class, 'authorized_user_id'); } -} +} \ No newline at end of file diff --git a/app/Models/BadgePrintLog.php b/app/Models/BadgePrintLog.php index 88a5206a..af7d3fcd 100644 --- a/app/Models/BadgePrintLog.php +++ b/app/Models/BadgePrintLog.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Models\BaseModel; use Illuminate\Database\QueryException; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; @@ -24,19 +25,21 @@ class BadgePrintLog extends BaseModel ]; protected $casts = [ - 'user_id' => 'integer', - 'printed_by' => 'integer', - 'printed_at' => 'datetime', - 'copies' => 'integer', + 'user_id' => 'integer', + 'printed_by' => 'integer', + 'printed_at' => 'datetime', + 'copies' => 'integer', ]; /** * Insert a batch of print logs. Swallows errors if table is missing. * - * @param int[] $userIds - * @param array $roleMap userId => role label - * @param array $classMap userId => class name - * @param int $copies copies per user (defaults to 1) + * @param int[] $userIds + * @param int|null $printedBy + * @param string|null $schoolYear + * @param array $roleMap userId => role label + * @param array $classMap userId => class name + * @param int $copies copies per user (defaults to 1) * @return int rows inserted (best-effort) */ public static function logPrints( @@ -52,38 +55,31 @@ class BadgePrintLog extends BaseModel foreach ($userIds as $uid) { $uid = (int) $uid; - if ($uid <= 0) { - continue; - } + if ($uid <= 0) continue; $rows[] = [ - 'user_id' => $uid, - 'printed_by' => $printedBy, - 'school_year' => $schoolYear, - 'printed_at' => $now, - 'role' => (string) ($roleMap[$uid] ?? ''), - 'class_section_name' => (string) ($classMap[$uid] ?? ''), - 'copies' => max(1, (int) $copies), + 'user_id' => $uid, + 'printed_by' => $printedBy, + 'school_year' => $schoolYear, + 'printed_at' => $now, + 'role' => (string)($roleMap[$uid] ?? ''), + 'class_section_name' => (string)($classMap[$uid] ?? ''), + 'copies' => max(1, (int) $copies), ]; } - if (empty($rows)) { - return 0; - } + if (empty($rows)) return 0; try { // insert returns bool; we return count best-effort like legacy DB::table((new static)->getTable())->insert($rows); - return count($rows); } catch (QueryException $e) { // Avoid breaking badge/PDF generation if the table is missing or other DB error - Log::error('BadgePrintLog::logPrints failed: '.$e->getMessage()); - + Log::error('BadgePrintLog::logPrints failed: ' . $e->getMessage()); return 0; } catch (\Throwable $e) { - Log::error('BadgePrintLog::logPrints failed: '.$e->getMessage()); - + Log::error('BadgePrintLog::logPrints failed: ' . $e->getMessage()); return 0; } } @@ -91,35 +87,31 @@ class BadgePrintLog extends BaseModel /** * Return counts and last printed info for given user IDs. * - * @param int[] $userIds - * @param string|null $schoolYear Optional filter by school year + * @param int[] $userIds + * @param string|null $schoolYear Optional filter by school year * @return array userId => [count, last_printed_at, last_printed_by] */ public static function getStatus(array $userIds, ?string $schoolYear = null): array { $userIds = array_values(array_unique(array_map('intval', array_filter($userIds)))); - if (empty($userIds)) { - return []; - } + if (empty($userIds)) return []; try { - $q = DB::table((new static)->getTable().' as l') + $q = DB::table((new static)->getTable() . ' as l') ->selectRaw('l.user_id, COUNT(*) AS prints, MAX(l.printed_at) AS last_printed_at, MAX(l.printed_by) AS last_printed_by') ->whereIn('l.user_id', $userIds) ->groupBy('l.user_id'); - if (! empty($schoolYear)) { + if (!empty($schoolYear)) { $q->where('l.school_year', $schoolYear); } $rows = $q->get(); } catch (QueryException $e) { - Log::error('BadgePrintLog::getStatus failed: '.$e->getMessage()); - + Log::error('BadgePrintLog::getStatus failed: ' . $e->getMessage()); return []; } catch (\Throwable $e) { - Log::error('BadgePrintLog::getStatus failed: '.$e->getMessage()); - + Log::error('BadgePrintLog::getStatus failed: ' . $e->getMessage()); return []; } @@ -127,7 +119,7 @@ class BadgePrintLog extends BaseModel foreach ($rows as $r) { $uid = (int) ($r->user_id ?? 0); $out[$uid] = [ - 'count' => (int) ($r->prints ?? 0), + 'count' => (int) ($r->prints ?? 0), 'last_printed_at' => (string) ($r->last_printed_at ?? ''), 'last_printed_by' => isset($r->last_printed_by) ? (int) $r->last_printed_by : null, ]; @@ -146,4 +138,4 @@ class BadgePrintLog extends BaseModel { return $this->belongsTo(User::class, 'printed_by'); } -} +} \ No newline at end of file diff --git a/app/Models/BaseModel.php b/app/Models/BaseModel.php index bcdf9fa0..6d35fb69 100644 --- a/app/Models/BaseModel.php +++ b/app/Models/BaseModel.php @@ -3,7 +3,6 @@ namespace App\Models; use Illuminate\Database\Eloquent\Model; -use Illuminate\Foundation\Application; class BaseModel extends Model { @@ -13,9 +12,8 @@ class BaseModel extends Model { if ($this->isTestingEnv()) { $columns = $this->tableColumns(); - if (! empty($columns)) { + if (!empty($columns)) { $key = $this->getKeyName(); - return array_values(array_filter($columns, static function ($col) use ($key) { return $col !== $key; })); @@ -24,10 +22,10 @@ class BaseModel extends Model $fillable = parent::getFillable(); if ($this->timestamps) { - if (! in_array('created_at', $fillable, true)) { + if (!in_array('created_at', $fillable, true)) { $fillable[] = 'created_at'; } - if (! in_array('updated_at', $fillable, true)) { + if (!in_array('updated_at', $fillable, true)) { $fillable[] = 'updated_at'; } } @@ -51,7 +49,7 @@ class BaseModel extends Model if (function_exists('app')) { try { $app = app(); - if ($app instanceof Application && $app->runningUnitTests()) { + if ($app instanceof \Illuminate\Foundation\Application && $app->runningUnitTests()) { return true; } } catch (\Throwable $e) { @@ -84,7 +82,7 @@ class BaseModel extends Model } if ($pattern === null) { - $pattern = __DIR__.'/../../database/migrations/*.php'; + $pattern = __DIR__ . '/../../database/migrations/*.php'; } $files = glob($pattern) ?: []; @@ -100,7 +98,7 @@ class BaseModel extends Model } foreach ($this->parseSchemaCreateTables($contents) as $table => $columns) { - if (! isset($map[$table]) || empty($map[$table])) { + if (!isset($map[$table]) || empty($map[$table])) { $map[$table] = $columns; } } @@ -118,7 +116,7 @@ class BaseModel extends Model $colsBlock = $match[2]; preg_match_all('/`([^`]+)`\\s+[^,]+/m', $colsBlock, $colsMatch); $columns = $colsMatch[1] ?? []; - if (! empty($columns)) { + if (!empty($columns)) { $map[$table] = $columns; } } @@ -130,7 +128,7 @@ class BaseModel extends Model private function parseSchemaCreateTables(string $contents): array { $map = []; - if (! preg_match_all('/Schema::create\\(\\s*[\'"]([^\'"]+)[\'"]\\s*,\\s*function\\s*\\([^)]*\\)\\s*\\{(.*?)\\n\\s*\\}\\);/is', $contents, $matches, PREG_SET_ORDER)) { + if (!preg_match_all('/Schema::create\\(\\s*[\'"]([^\'"]+)[\'"]\\s*,\\s*function\\s*\\([^)]*\\)\\s*\\{(.*?)\\n\\s*\\}\\);/is', $contents, $matches, PREG_SET_ORDER)) { return $map; } @@ -152,7 +150,7 @@ class BaseModel extends Model $columns[] = 'deleted_at'; } - if (! empty($columns)) { + if (!empty($columns)) { $map[$table] = $columns; } } diff --git a/app/Models/Calendar.php b/app/Models/Calendar.php index a1dfd891..e1692f17 100644 --- a/app/Models/Calendar.php +++ b/app/Models/Calendar.php @@ -2,4 +2,6 @@ namespace App\Models; -class Calendar extends CalendarEvent {} +class Calendar extends CalendarEvent +{ +} diff --git a/app/Models/CalendarEvent.php b/app/Models/CalendarEvent.php index bab56d26..bcb814bf 100644 --- a/app/Models/CalendarEvent.php +++ b/app/Models/CalendarEvent.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Models\BaseModel; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Schema; @@ -30,12 +31,12 @@ class CalendarEvent extends BaseModel protected $casts = [ // If your column is DATE use 'date', if it's DATETIME use 'datetime' - 'date' => 'date', - 'notify_parent' => 'boolean', - 'notify_admin' => 'boolean', + 'date' => 'date', + 'notify_parent' => 'boolean', + 'notify_admin' => 'boolean', 'notify_teacher' => 'boolean', - 'no_school' => 'integer', - 'event_type' => 'string', + 'no_school' => 'integer', + 'event_type' => 'string', ]; private static ?bool $hasEventTypeColumn = null; @@ -47,21 +48,18 @@ class CalendarEvent extends BaseModel public static function getEvents() { $events = static::query()->get()->toArray(); - return static::withNoSchoolFlag($events); } public static function getEventByDate(string $date): array { $events = static::query()->where('date', $date)->get()->toArray(); - return static::withNoSchoolFlag($events); } public static function addEvent(array $data): int { $row = static::create($data); - return (int) $row->id; } @@ -94,7 +92,6 @@ class CalendarEvent extends BaseModel if (static::$hasEventTypeColumn === null) { static::$hasEventTypeColumn = static::columnExists((new static)->getTable(), 'event_type'); } - return (bool) static::$hasEventTypeColumn; } @@ -126,9 +123,9 @@ class CalendarEvent extends BaseModel */ protected static function isNoSchoolEvent(array $event): bool { - $title = strtolower((string) ($event['title'] ?? '')); - $desc = strtolower((string) ($event['description'] ?? '')); - $text = $title.' '.$desc; + $title = strtolower((string)($event['title'] ?? '')); + $desc = strtolower((string)($event['description'] ?? '')); + $text = $title . ' ' . $desc; $keywords = ['no school', 'no-school', 'cancel', 'vacation', 'holiday', 'break']; diff --git a/app/Models/ClassModel.php b/app/Models/ClassModel.php index b63baabc..2c2d287f 100644 --- a/app/Models/ClassModel.php +++ b/app/Models/ClassModel.php @@ -2,4 +2,6 @@ namespace App\Models; -class ClassModel extends SchoolClass {} +class ClassModel extends SchoolClass +{ +} diff --git a/app/Models/ClassPrepAdjustment.php b/app/Models/ClassPrepAdjustment.php index cc353221..5fafae83 100644 --- a/app/Models/ClassPrepAdjustment.php +++ b/app/Models/ClassPrepAdjustment.php @@ -2,6 +2,8 @@ namespace App\Models; +use App\Models\BaseModel; + class ClassPrepAdjustment extends BaseModel { protected $table = 'class_prep_adjustments'; @@ -19,7 +21,7 @@ class ClassPrepAdjustment extends BaseModel protected $casts = [ 'class_section_id' => 'integer', - 'adjustment' => 'integer', // change to 'decimal:2' if it's not an int + 'adjustment' => 'integer', // change to 'decimal:2' if it's not an int ]; // Optional relationship @@ -27,4 +29,4 @@ class ClassPrepAdjustment extends BaseModel { return $this->belongsTo(ClassSection::class, 'class_section_id'); } -} +} \ No newline at end of file diff --git a/app/Models/ClassPreparationLog.php b/app/Models/ClassPreparationLog.php index 11891993..2601b91b 100644 --- a/app/Models/ClassPreparationLog.php +++ b/app/Models/ClassPreparationLog.php @@ -2,6 +2,8 @@ namespace App\Models; +use App\Models\BaseModel; + class ClassPreparationLog extends BaseModel { protected $table = 'class_preparation_log'; @@ -20,7 +22,7 @@ class ClassPreparationLog extends BaseModel protected $casts = [ 'class_section_id' => 'integer', // If prep_data is JSON in DB, use 'array' (or 'json') cast: - 'prep_data' => 'array', + 'prep_data' => 'array', ]; // Optional relationship @@ -28,4 +30,4 @@ class ClassPreparationLog extends BaseModel { return $this->belongsTo(ClassSection::class, 'class_section_id'); } -} +} \ No newline at end of file diff --git a/app/Models/ClassProgressAttachment.php b/app/Models/ClassProgressAttachment.php index bfd1ed8d..b0c77514 100644 --- a/app/Models/ClassProgressAttachment.php +++ b/app/Models/ClassProgressAttachment.php @@ -2,12 +2,12 @@ namespace App\Models; +use App\Models\BaseModel; use Illuminate\Database\Eloquent\Factories\HasFactory; class ClassProgressAttachment extends BaseModel { use HasFactory; - protected $table = 'class_progress_attachments'; // legacy: useTimestamps = false @@ -23,8 +23,8 @@ class ClassProgressAttachment extends BaseModel ]; protected $casts = [ - 'report_id' => 'integer', - 'file_size' => 'integer', + 'report_id' => 'integer', + 'file_size' => 'integer', ]; // Optional relationship diff --git a/app/Models/ClassProgressReport.php b/app/Models/ClassProgressReport.php index 89e224b3..9badfcd8 100644 --- a/app/Models/ClassProgressReport.php +++ b/app/Models/ClassProgressReport.php @@ -2,12 +2,12 @@ namespace App\Models; +use App\Models\BaseModel; use Illuminate\Database\Eloquent\Factories\HasFactory; class ClassProgressReport extends BaseModel { use HasFactory; - protected $table = 'class_progress_reports'; // ✅ legacy: useTimestamps = true (created_at/updated_at) @@ -36,14 +36,14 @@ class ClassProgressReport extends BaseModel protected $casts = [ 'class_section_id' => 'integer', - 'teacher_id' => 'integer', + 'teacher_id' => 'integer', // change to 'datetime' if your columns are DATETIME - 'week_start' => 'date', - 'week_end' => 'date', + 'week_start' => 'date', + 'week_end' => 'date', // If flags_json is JSON in DB, this makes it an array automatically - 'flags_json' => 'array', + 'flags_json' => 'array', ]; /* Optional relationships */ diff --git a/app/Models/ClassSection.php b/app/Models/ClassSection.php index 0c6477d5..0f517359 100644 --- a/app/Models/ClassSection.php +++ b/app/Models/ClassSection.php @@ -2,13 +2,13 @@ namespace App\Models; +use App\Models\BaseModel; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Support\Facades\DB; class ClassSection extends BaseModel { use HasFactory; - // legacy table name is "classSection" protected $table = 'classSection'; @@ -24,7 +24,7 @@ class ClassSection extends BaseModel ]; protected $casts = [ - 'class_id' => 'integer', + 'class_id' => 'integer', 'class_section_id' => 'integer', ]; diff --git a/app/Models/Communication.php b/app/Models/Communication.php index 8703135a..abdcab5a 100644 --- a/app/Models/Communication.php +++ b/app/Models/Communication.php @@ -2,4 +2,6 @@ namespace App\Models; -class Communication extends CommunicationLog {} +class Communication extends CommunicationLog +{ +} diff --git a/app/Models/CommunicationLog.php b/app/Models/CommunicationLog.php index db0c87c6..6b4cb08a 100644 --- a/app/Models/CommunicationLog.php +++ b/app/Models/CommunicationLog.php @@ -2,6 +2,8 @@ namespace App\Models; +use App\Models\BaseModel; + class CommunicationLog extends BaseModel { protected $table = 'communication_logs'; @@ -32,16 +34,16 @@ class CommunicationLog extends BaseModel } protected $casts = [ - 'student_id' => 'integer', - 'family_id' => 'integer', - 'sent_by' => 'integer', + 'student_id' => 'integer', + 'family_id' => 'integer', + 'sent_by' => 'integer', // If these columns are JSON in DB, this will auto convert to arrays - 'recipients' => 'array', - 'cc' => 'array', - 'bcc' => 'array', - 'attachments' => 'array', - 'metadata' => 'array', + 'recipients' => 'array', + 'cc' => 'array', + 'bcc' => 'array', + 'attachments' => 'array', + 'metadata' => 'array', ]; /* Optional relationships */ diff --git a/app/Models/Competition.php b/app/Models/Competition.php index 660a682b..40430f46 100644 --- a/app/Models/Competition.php +++ b/app/Models/Competition.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Models\BaseModel; use Illuminate\Database\Eloquent\SoftDeletes; class Competition extends BaseModel @@ -33,20 +34,20 @@ class Competition extends BaseModel protected $casts = [ 'class_section_id' => 'integer', - 'winners_count' => 'integer', - 'prize_per_point' => 'decimal:2', // adjust precision if needed + 'winners_count' => 'integer', + 'prize_per_point' => 'decimal:2', // adjust precision if needed - 'is_published' => 'boolean', - 'is_locked' => 'boolean', + 'is_published' => 'boolean', + 'is_locked' => 'boolean', // change to 'date' if your columns are DATE (not DATETIME) - 'start_date' => 'date', - 'end_date' => 'date', - 'published_at' => 'datetime', - 'locked_at' => 'datetime', + 'start_date' => 'date', + 'end_date' => 'date', + 'published_at' => 'datetime', + 'locked_at' => 'datetime', - 'locked_by' => 'integer', - 'created_by' => 'integer', + 'locked_by' => 'integer', + 'created_by' => 'integer', ]; /* Optional relationships */ @@ -69,4 +70,4 @@ class Competition extends BaseModel { return $this->hasMany(CompetitionClassWinner::class, 'competition_id'); } -} +} \ No newline at end of file diff --git a/app/Models/CompetitionClassWinner.php b/app/Models/CompetitionClassWinner.php index a718a992..3d3c5ef4 100644 --- a/app/Models/CompetitionClassWinner.php +++ b/app/Models/CompetitionClassWinner.php @@ -2,6 +2,8 @@ namespace App\Models; +use App\Models\BaseModel; + class CompetitionClassWinner extends BaseModel { protected $table = 'competition_class_winners'; @@ -23,10 +25,10 @@ class CompetitionClassWinner extends BaseModel ]; protected $casts = [ - 'competition_id' => 'integer', - 'class_section_id' => 'integer', - 'question_count' => 'integer', - 'winners_override' => 'integer', + 'competition_id' => 'integer', + 'class_section_id' => 'integer', + 'question_count' => 'integer', + 'winners_override' => 'integer', ]; /* Optional relationships */ @@ -39,4 +41,4 @@ class CompetitionClassWinner extends BaseModel { return $this->belongsTo(ClassSection::class, 'class_section_id'); } -} +} \ No newline at end of file diff --git a/app/Models/CompetitionScore.php b/app/Models/CompetitionScore.php index d4a49133..0d745cd0 100644 --- a/app/Models/CompetitionScore.php +++ b/app/Models/CompetitionScore.php @@ -2,6 +2,8 @@ namespace App\Models; +use App\Models\BaseModel; + class CompetitionScore extends BaseModel { protected $table = 'competition_scores'; @@ -18,10 +20,10 @@ class CompetitionScore extends BaseModel ]; protected $casts = [ - 'competition_id' => 'integer', - 'student_id' => 'integer', + 'competition_id' => 'integer', + 'student_id' => 'integer', 'class_section_id' => 'integer', - 'score' => 'integer', // change to 'decimal:2' if score is not int + 'score' => 'integer', // change to 'decimal:2' if score is not int ]; /* Optional relationships */ @@ -39,4 +41,4 @@ class CompetitionScore extends BaseModel { return $this->belongsTo(ClassSection::class, 'class_section_id'); } -} +} \ No newline at end of file diff --git a/app/Models/CompetitionWinner.php b/app/Models/CompetitionWinner.php index c9b47616..84c1126d 100644 --- a/app/Models/CompetitionWinner.php +++ b/app/Models/CompetitionWinner.php @@ -2,6 +2,8 @@ namespace App\Models; +use App\Models\BaseModel; + class CompetitionWinner extends BaseModel { protected $table = 'competition_winners'; @@ -20,13 +22,13 @@ class CompetitionWinner extends BaseModel ]; protected $casts = [ - 'competition_id' => 'integer', - 'student_id' => 'integer', + 'competition_id' => 'integer', + 'student_id' => 'integer', 'class_section_id' => 'integer', - 'rank' => 'integer', - 'score' => 'integer', // change to 'decimal:2' if needed - 'prize_amount' => 'decimal:2', // adjust precision if needed - 'created_at' => 'datetime', + 'rank' => 'integer', + 'score' => 'integer', // change to 'decimal:2' if needed + 'prize_amount' => 'decimal:2', // adjust precision if needed + 'created_at' => 'datetime', ]; /* Optional relationships */ @@ -44,4 +46,4 @@ class CompetitionWinner extends BaseModel { return $this->belongsTo(ClassSection::class, 'class_section_id'); } -} +} \ No newline at end of file diff --git a/app/Models/Configuration.php b/app/Models/Configuration.php index f5c433ef..c8cc91ab 100644 --- a/app/Models/Configuration.php +++ b/app/Models/Configuration.php @@ -2,7 +2,8 @@ namespace App\Models; -use App\Services\SemesterRangeService; +use App\Models\BaseModel; +use Illuminate\Support\Facades\DB; class Configuration extends BaseModel { @@ -57,11 +58,11 @@ class Configuration extends BaseModel } $row = static::query()->create([ - 'config_key' => $key, + 'config_key' => $key, 'config_value' => $value, ]); - return ! empty($row->id); + return !empty($row->id); } /** @@ -86,7 +87,6 @@ class Configuration extends BaseModel public static function addConfig(array $data): int { $row = static::query()->create($data); - return (int) $row->id; } @@ -104,7 +104,7 @@ class Configuration extends BaseModel try { // If you have this service in Laravel, inject/resolve it from container. // This call assumes the service constructor can accept Configuration model (like legacy did). - $semester = app(SemesterRangeService::class)->getSemesterForDate(); + $semester = app(\App\Services\SemesterRangeService::class)->getSemesterForDate(); if (is_string($semester) && $semester !== '') { return $semester; } diff --git a/app/Models/ContactUs.php b/app/Models/ContactUs.php index 0bfd8c0f..e891a22f 100644 --- a/app/Models/ContactUs.php +++ b/app/Models/ContactUs.php @@ -2,6 +2,8 @@ namespace App\Models; +use App\Models\BaseModel; + class ContactUs extends BaseModel { protected $table = 'contactus'; @@ -21,7 +23,7 @@ class ContactUs extends BaseModel ]; protected $casts = [ - 'sender_id' => 'integer', + 'sender_id' => 'integer', 'reciever_id' => 'integer', ]; @@ -77,4 +79,4 @@ class ContactUs extends BaseModel { return static::query()->whereKey($id)->update($data) >= 0; } -} +} \ No newline at end of file diff --git a/app/Models/CurrentFlag.php b/app/Models/CurrentFlag.php index 7e9cf71d..1382c2df 100644 --- a/app/Models/CurrentFlag.php +++ b/app/Models/CurrentFlag.php @@ -2,6 +2,8 @@ namespace App\Models; +use App\Models\BaseModel; + class CurrentFlag extends BaseModel { protected $table = 'current_flag'; @@ -31,11 +33,11 @@ class CurrentFlag extends BaseModel ]; protected $casts = [ - 'student_id' => 'integer', - 'updated_by_open' => 'integer', - 'updated_by_closed' => 'integer', - 'updated_by_canceled' => 'integer', - 'flag_datetime' => 'datetime', + 'student_id' => 'integer', + 'updated_by_open' => 'integer', + 'updated_by_closed' => 'integer', + 'updated_by_canceled'=> 'integer', + 'flag_datetime' => 'datetime', ]; /* Optional relationships */ @@ -58,4 +60,4 @@ class CurrentFlag extends BaseModel { return $this->belongsTo(User::class, 'updated_by_canceled'); } -} +} \ No newline at end of file diff --git a/app/Models/CurrentIncident.php b/app/Models/CurrentIncident.php index b106466a..4cf08894 100644 --- a/app/Models/CurrentIncident.php +++ b/app/Models/CurrentIncident.php @@ -2,10 +2,11 @@ namespace App\Models; +use App\Models\BaseModel; + class CurrentIncident extends BaseModel { protected $table = 'current_incident'; - public $timestamps = true; protected $fillable = [ diff --git a/app/Models/DiscountUsage.php b/app/Models/DiscountUsage.php index b7620201..fc780085 100644 --- a/app/Models/DiscountUsage.php +++ b/app/Models/DiscountUsage.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Models\BaseModel; use Illuminate\Support\Facades\DB; class DiscountUsage extends BaseModel @@ -23,12 +24,12 @@ class DiscountUsage extends BaseModel ]; protected $casts = [ - 'voucher_id' => 'integer', - 'invoice_id' => 'integer', - 'discount_amount' => 'decimal:2', // adjust precision if needed - 'updated_by' => 'integer', - 'parent_id' => 'integer', - 'used_at' => 'datetime', + 'voucher_id' => 'integer', + 'invoice_id' => 'integer', + 'discount_amount' => 'decimal:2', // adjust precision if needed + 'updated_by' => 'integer', + 'parent_id' => 'integer', + 'used_at' => 'datetime', ]; /* Optional relationships */ @@ -55,4 +56,4 @@ class DiscountUsage extends BaseModel return (float) $total; } -} +} \ No newline at end of file diff --git a/app/Models/DiscountVoucher.php b/app/Models/DiscountVoucher.php index 6defb93f..21a99b29 100644 --- a/app/Models/DiscountVoucher.php +++ b/app/Models/DiscountVoucher.php @@ -2,7 +2,9 @@ namespace App\Models; +use App\Models\BaseModel; use Illuminate\Support\Carbon; +use Illuminate\Support\Facades\DB; class DiscountVoucher extends BaseModel { @@ -29,11 +31,11 @@ class DiscountVoucher extends BaseModel protected $casts = [ 'discount_value' => 'decimal:2', - 'max_uses' => 'integer', - 'times_used' => 'integer', - 'is_active' => 'boolean', - 'valid_from' => 'date', - 'valid_until' => 'date', + 'max_uses' => 'integer', + 'times_used' => 'integer', + 'is_active' => 'boolean', + 'valid_from' => 'date', + 'valid_until' => 'date', ]; /* ========================= @@ -88,8 +90,8 @@ class DiscountVoucher extends BaseModel private function validateDatesOrder(): void { - if (! empty($this->valid_from) && ! empty($this->valid_until)) { - $from = Carbon::parse($this->valid_from)->toDateString(); + if (!empty($this->valid_from) && !empty($this->valid_until)) { + $from = Carbon::parse($this->valid_from)->toDateString(); $until = Carbon::parse($this->valid_until)->toDateString(); if ($from > $until) { // In Laravel, prefer validating in FormRequest. @@ -121,24 +123,24 @@ class DiscountVoucher extends BaseModel // Left join usages for this student only ->leftJoin('discount_usages as u', function ($join) use ($studentId) { $join->on('u.voucher_id', '=', 'v.id') - ->where('u.student_id', '=', (int) $studentId); + ->where('u.student_id', '=', (int) $studentId); }) ->where('v.code', $code) ->where('v.is_active', 1) // valid_from is null or <= today ->where(function ($w) use ($today) { $w->whereNull('v.valid_from') - ->orWhere('v.valid_from', '<=', $today); + ->orWhere('v.valid_from', '<=', $today); }) // valid_until is null or >= today ->where(function ($w) use ($today) { $w->whereNull('v.valid_until') - ->orWhere('v.valid_until', '>=', $today); + ->orWhere('v.valid_until', '>=', $today); }) // max_uses is null or times_used < max_uses ->where(function ($w) { $w->whereNull('v.max_uses') - ->orWhereColumn('v.times_used', '<', 'v.max_uses'); + ->orWhereColumn('v.times_used', '<', 'v.max_uses'); }) // Not previously used by this student (no usage row) ->whereNull('u.id') @@ -146,4 +148,4 @@ class DiscountVoucher extends BaseModel return $q->first(); } -} +} \ No newline at end of file diff --git a/app/Models/EarlyDismissalSignature.php b/app/Models/EarlyDismissalSignature.php index 63d0d7d5..f9a703c4 100644 --- a/app/Models/EarlyDismissalSignature.php +++ b/app/Models/EarlyDismissalSignature.php @@ -2,6 +2,8 @@ namespace App\Models; +use App\Models\BaseModel; + class EarlyDismissalSignature extends BaseModel { protected $table = 'early_dismissal_signatures'; @@ -24,7 +26,7 @@ class EarlyDismissalSignature extends BaseModel // report_date is stored as Y-m-d in legacy validation; keep as date 'report_date' => 'date', - 'file_size' => 'integer', + 'file_size' => 'integer', 'uploaded_by' => 'integer', ]; @@ -33,4 +35,4 @@ class EarlyDismissalSignature extends BaseModel { return $this->belongsTo(User::class, 'uploaded_by'); } -} +} \ No newline at end of file diff --git a/app/Models/EmailTemplate.php b/app/Models/EmailTemplate.php index b604b1b0..cf05bacf 100644 --- a/app/Models/EmailTemplate.php +++ b/app/Models/EmailTemplate.php @@ -2,6 +2,8 @@ namespace App\Models; +use App\Models\BaseModel; + class EmailTemplate extends BaseModel { protected $table = 'email_templates'; @@ -29,9 +31,9 @@ class EmailTemplate extends BaseModel ->where('is_active', 1) ->where(function ($q) use ($variant) { $q->where('variant', $variant) - ->orWhere('variant', 'default'); + ->orWhere('variant', 'default'); }) - ->orderByRaw('CASE WHEN variant = ? THEN 0 ELSE 1 END', [$variant]) + ->orderByRaw("CASE WHEN variant = ? THEN 0 ELSE 1 END", [$variant]) ->first(); return $row; diff --git a/app/Models/EmergencyContact.php b/app/Models/EmergencyContact.php index 555d1d53..23d898df 100644 --- a/app/Models/EmergencyContact.php +++ b/app/Models/EmergencyContact.php @@ -2,6 +2,8 @@ namespace App\Models; +use App\Models\BaseModel; + class EmergencyContact extends BaseModel { protected $table = 'emergency_contacts'; @@ -22,8 +24,8 @@ class EmergencyContact extends BaseModel ]; protected $casts = [ - 'parent_id' => 'integer', - 'student_id' => 'integer', + 'parent_id' => 'integer', + 'student_id' => 'integer', 'authorized_user_id' => 'integer', ]; @@ -70,7 +72,7 @@ class EmergencyContact extends BaseModel return static::query() ->where(function ($q) use ($studentId) { $q->where('student_id', $studentId) - ->orWhere('parent_id', $studentId); + ->orWhere('parent_id', $studentId); }) ->get(); } diff --git a/app/Models/Enrollment.php b/app/Models/Enrollment.php index 605c3731..5ff8e09f 100644 --- a/app/Models/Enrollment.php +++ b/app/Models/Enrollment.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Models\BaseModel; use Illuminate\Support\Facades\DB; class Enrollment extends BaseModel @@ -27,14 +28,14 @@ class Enrollment extends BaseModel ]; protected $casts = [ - 'student_id' => 'integer', + 'student_id' => 'integer', 'class_section_id' => 'integer', - 'parent_id' => 'integer', - 'is_withdrawn' => 'boolean', + 'parent_id' => 'integer', + 'is_withdrawn' => 'boolean', // change to 'datetime' if your columns are DATETIME - 'enrollment_date' => 'date', - 'withdrawal_date' => 'date', + 'enrollment_date' => 'date', + 'withdrawal_date' => 'date', ]; /* Optional relationships */ @@ -68,11 +69,11 @@ class Enrollment extends BaseModel ->select('students.*') ->where('enrollments.parent_id', $parentId); - if (! empty($schoolYear)) { + if (!empty($schoolYear)) { $q->where('enrollments.school_year', $schoolYear); } - if (! empty($semester)) { + if (!empty($semester)) { $q->where('enrollments.semester', $semester); } @@ -91,10 +92,10 @@ class Enrollment extends BaseModel ->where('enrollments.parent_id', $parentId) ->where(function ($w) { $w->where('enrollments.enrollment_status', 'enrolled') - ->orWhere('enrollments.enrollment_status', 'payment pending'); + ->orWhere('enrollments.enrollment_status', 'payment pending'); }); - if (! empty($schoolYear)) { + if (!empty($schoolYear)) { $q->where('enrollments.school_year', $schoolYear); } @@ -117,4 +118,4 @@ class Enrollment extends BaseModel return $row?->enrollment_status; } -} +} \ No newline at end of file diff --git a/app/Models/Event.php b/app/Models/Event.php index 38bccfc2..4eae8188 100644 --- a/app/Models/Event.php +++ b/app/Models/Event.php @@ -2,6 +2,8 @@ namespace App\Models; +use App\Models\BaseModel; + class Event extends BaseModel { protected $table = 'events'; @@ -24,9 +26,9 @@ class Event extends BaseModel ]; protected $casts = [ - 'amount' => 'decimal:2', // adjust precision if needed + 'amount' => 'decimal:2', // adjust precision if needed 'expiration_date' => 'date', // change to 'datetime' if stored as DATETIME - 'created_by' => 'integer', + 'created_by' => 'integer', ]; /* Optional relationships */ @@ -52,7 +54,7 @@ class Event extends BaseModel $q = static::query() ->whereDate('expiration_date', '>=', now()->toDateString()); - if (! empty($schoolYear)) { + if (!empty($schoolYear)) { $q->where('school_year', $schoolYear); } @@ -65,9 +67,9 @@ class Event extends BaseModel public static function getEventsByName(string $name, ?string $schoolYear = null) { $q = static::query() - ->where('event_name', 'like', '%'.$name.'%'); + ->where('event_name', 'like', '%' . $name . '%'); - if (! empty($schoolYear)) { + if (!empty($schoolYear)) { $q->where('school_year', $schoolYear); } @@ -81,7 +83,7 @@ class Event extends BaseModel { $q = static::query(); - if (! empty($schoolYear)) { + if (!empty($schoolYear)) { $q->where('school_year', $schoolYear); } @@ -94,7 +96,6 @@ class Event extends BaseModel public static function addEvent(array $data): int { $row = static::create($data); - return (int) $row->id; } @@ -105,7 +106,7 @@ class Event extends BaseModel { $q = static::query()->whereDate('expiration_date', $date); - if (! empty($schoolYear)) { + if (!empty($schoolYear)) { $q->where('school_year', $schoolYear); } @@ -120,11 +121,11 @@ class Event extends BaseModel $q = static::query() ->whereDate('expiration_date', '>=', now()->toDateString()); - if (! empty($schoolYear)) { + if (!empty($schoolYear)) { $q->where('school_year', $schoolYear); } - if (! empty($semester)) { + if (!empty($semester)) { $q->where('semester', $semester); } @@ -138,7 +139,7 @@ class Event extends BaseModel { $q = static::query()->whereKey($id); - if (! empty($schoolYear)) { + if (!empty($schoolYear)) { $q->where('school_year', $schoolYear); } @@ -152,7 +153,7 @@ class Event extends BaseModel { $q = static::query()->whereKey($id); - if (! empty($schoolYear)) { + if (!empty($schoolYear)) { $q->where('school_year', $schoolYear); } @@ -166,10 +167,10 @@ class Event extends BaseModel { $q = static::query()->whereKey($id); - if (! empty($schoolYear)) { + if (!empty($schoolYear)) { $q->where('school_year', $schoolYear); } return (bool) $q->delete(); } -} +} \ No newline at end of file diff --git a/app/Models/EventCharge.php b/app/Models/EventCharge.php index 7b6a7ebd..5f754a2e 100644 --- a/app/Models/EventCharge.php +++ b/app/Models/EventCharge.php @@ -2,4 +2,6 @@ namespace App\Models; -class EventCharge extends EventCharges {} +class EventCharge extends EventCharges +{ +} diff --git a/app/Models/EventCharges.php b/app/Models/EventCharges.php index 6eb27912..fd6b17e6 100644 --- a/app/Models/EventCharges.php +++ b/app/Models/EventCharges.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Models\BaseModel; use Illuminate\Support\Facades\DB; class EventCharges extends BaseModel @@ -40,12 +41,12 @@ class EventCharges extends BaseModel ]; protected $casts = [ - 'event_id' => 'integer', - 'parent_id' => 'integer', - 'student_id' => 'integer', - 'participation' => 'boolean', // change if stored as string/enum - 'charged' => 'boolean', // change if stored as string/enum - 'updated_by' => 'integer', + 'event_id' => 'integer', + 'parent_id' => 'integer', + 'student_id' => 'integer', + 'participation' => 'boolean', // change if stored as string/enum + 'charged' => 'boolean', // change if stored as string/enum + 'updated_by' => 'integer', ]; /* Optional relationships */ @@ -74,15 +75,15 @@ class EventCharges extends BaseModel ->leftJoin('events', 'events.id', '=', 'event_charges.event_id') ->select('event_charges.*', 'events.event_name', 'events.amount'); - if (! empty($parentId)) { + if (!empty($parentId)) { $q->where('event_charges.parent_id', $parentId); } - if (! empty($schoolYear)) { + if (!empty($schoolYear)) { $q->where('event_charges.school_year', $schoolYear); } - if (! empty($semester)) { + if (!empty($semester)) { $q->where('event_charges.semester', $semester); } diff --git a/app/Models/Exam.php b/app/Models/Exam.php index 3bc386dc..2cf1ec1f 100644 --- a/app/Models/Exam.php +++ b/app/Models/Exam.php @@ -2,6 +2,8 @@ namespace App\Models; +use App\Models\BaseModel; + class Exam extends BaseModel { protected $table = 'exams'; @@ -14,7 +16,6 @@ class Exam extends BaseModel * We'll keep auto created_at, no updated_at: */ public $timestamps = true; - const UPDATED_AT = null; // ✅ no updated_at column protected $fillable = [ @@ -26,8 +27,8 @@ class Exam extends BaseModel ]; protected $casts = [ - 'student_id' => 'integer', - 'school_id' => 'integer', + 'student_id' => 'integer', + 'school_id' => 'integer', 'class_section_id' => 'integer', ]; @@ -41,4 +42,4 @@ class Exam extends BaseModel { return $this->belongsTo(ClassSection::class, 'class_section_id'); } -} +} \ No newline at end of file diff --git a/app/Models/ExamDraft.php b/app/Models/ExamDraft.php index 577af180..50e69551 100644 --- a/app/Models/ExamDraft.php +++ b/app/Models/ExamDraft.php @@ -2,6 +2,8 @@ namespace App\Models; +use App\Models\BaseModel; + class ExamDraft extends BaseModel { protected $table = 'exam_drafts'; @@ -41,15 +43,15 @@ class ExamDraft extends BaseModel ]; protected $casts = [ - 'teacher_id' => 'integer', - 'class_section_id' => 'integer', - 'admin_id' => 'integer', - 'is_legacy' => 'boolean', + 'teacher_id' => 'integer', + 'class_section_id' => 'integer', + 'admin_id' => 'integer', + 'is_legacy' => 'boolean', - 'version' => 'integer', + 'version' => 'integer', 'previous_draft_id' => 'integer', - 'reviewed_at' => 'datetime', + 'reviewed_at' => 'datetime', ]; /** @@ -63,7 +65,7 @@ class ExamDraft extends BaseModel */ public function forceSave(array $attributes = []): bool { - if (! empty($attributes)) { + if (!empty($attributes)) { $this->fill($attributes); } diff --git a/app/Models/Expense.php b/app/Models/Expense.php index f5e98940..c7f841be 100644 --- a/app/Models/Expense.php +++ b/app/Models/Expense.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Models\BaseModel; use Illuminate\Support\Facades\DB; class Expense extends BaseModel @@ -30,14 +31,14 @@ class Expense extends BaseModel ]; protected $casts = [ - 'amount' => 'decimal:2', // adjust precision if needed - 'date_of_purchase' => 'date', // change to 'datetime' if stored as DATETIME + 'amount' => 'decimal:2', // adjust precision if needed + 'date_of_purchase'=> 'date', // change to 'datetime' if stored as DATETIME - 'purchased_by' => 'integer', - 'added_by' => 'integer', - 'reimbursement_id' => 'integer', - 'approved_by' => 'integer', - 'updated_by' => 'integer', + 'purchased_by' => 'integer', + 'added_by' => 'integer', + 'reimbursement_id'=> 'integer', + 'approved_by' => 'integer', + 'updated_by' => 'integer', ]; /* Optional relationships */ @@ -70,7 +71,7 @@ class Expense extends BaseModel public static function getReimbursedExpensesWithDetails(array $filters = []) { $q = DB::table('expenses') - ->selectRaw(' + ->selectRaw(" expenses.*, r.id AS reimb_id, r.amount AS reimb_amount, r.reimbursement_method, r.check_number, @@ -79,24 +80,24 @@ class Expense extends BaseModel r.reimbursed_to AS reimb_recipient_id, reimb.firstname AS reimb_firstname, reimb.lastname AS reimb_lastname, approver.firstname AS approver_firstname, approver.lastname AS approver_lastname - ') + ") ->join('reimbursements as r', 'r.expense_id', '=', 'expenses.id') ->leftJoin('users as reimb', 'reimb.id', '=', 'r.reimbursed_to') ->leftJoin('users as approver', 'approver.id', '=', 'r.approved_by'); - if (! empty($filters['school_year'])) { + if (!empty($filters['school_year'])) { $q->where('r.school_year', $filters['school_year']); } - if (! empty($filters['semester'])) { + if (!empty($filters['semester'])) { $q->where('r.semester', $filters['semester']); } - if (! empty($filters['status'])) { + if (!empty($filters['status'])) { $q->where('r.status', $filters['status']); } - if (! empty($filters['user_id'])) { + if (!empty($filters['user_id'])) { $q->where('r.reimbursed_to', $filters['user_id']); } return $q->orderByDesc('r.created_at'); } -} +} \ No newline at end of file diff --git a/app/Models/Family.php b/app/Models/Family.php index 0b5aa216..fdc35785 100644 --- a/app/Models/Family.php +++ b/app/Models/Family.php @@ -2,6 +2,8 @@ namespace App\Models; +use App\Models\BaseModel; + class Family extends BaseModel { protected $table = 'families'; @@ -38,4 +40,4 @@ class Family extends BaseModel { return $this->hasMany(FamilyCommPref::class, 'family_id'); } -} +} \ No newline at end of file diff --git a/app/Models/FamilyCommPref.php b/app/Models/FamilyCommPref.php index 20211ac0..54133289 100644 --- a/app/Models/FamilyCommPref.php +++ b/app/Models/FamilyCommPref.php @@ -2,6 +2,8 @@ namespace App\Models; +use App\Models\BaseModel; + class FamilyCommPref extends BaseModel { protected $table = 'family_comm_prefs'; @@ -18,10 +20,10 @@ class FamilyCommPref extends BaseModel ]; protected $casts = [ - 'family_id' => 'integer', - 'via_email' => 'boolean', - 'via_sms' => 'boolean', - 'cc_all_guardians' => 'boolean', + 'family_id' => 'integer', + 'via_email' => 'boolean', + 'via_sms' => 'boolean', + 'cc_all_guardians' => 'boolean', ]; // Optional relationship @@ -29,4 +31,4 @@ class FamilyCommPref extends BaseModel { return $this->belongsTo(Family::class, 'family_id'); } -} +} \ No newline at end of file diff --git a/app/Models/FamilyGuardian.php b/app/Models/FamilyGuardian.php index 8bd3a5eb..b4d957ac 100644 --- a/app/Models/FamilyGuardian.php +++ b/app/Models/FamilyGuardian.php @@ -2,6 +2,8 @@ namespace App\Models; +use App\Models\BaseModel; + class FamilyGuardian extends BaseModel { protected $table = 'family_guardians'; @@ -20,11 +22,11 @@ class FamilyGuardian extends BaseModel ]; protected $casts = [ - 'family_id' => 'integer', - 'user_id' => 'integer', - 'is_primary' => 'boolean', - 'receive_emails' => 'boolean', - 'receive_sms' => 'boolean', + 'family_id' => 'integer', + 'user_id' => 'integer', + 'is_primary' => 'boolean', + 'receive_emails' => 'boolean', + 'receive_sms' => 'boolean', ]; // Optional relationships @@ -37,4 +39,4 @@ class FamilyGuardian extends BaseModel { return $this->belongsTo(User::class, 'user_id'); } -} +} \ No newline at end of file diff --git a/app/Models/FamilyStudent.php b/app/Models/FamilyStudent.php index 80c3f880..c472f3b1 100644 --- a/app/Models/FamilyStudent.php +++ b/app/Models/FamilyStudent.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Models\BaseModel; use Illuminate\Support\Facades\DB; class FamilyStudent extends BaseModel @@ -19,9 +20,9 @@ class FamilyStudent extends BaseModel ]; protected $casts = [ - 'family_id' => 'integer', - 'student_id' => 'integer', - 'is_primary_home' => 'boolean', + 'family_id' => 'integer', + 'student_id' => 'integer', + 'is_primary_home' => 'boolean', ]; /* Optional relationships */ @@ -52,4 +53,4 @@ class FamilyStudent extends BaseModel ->map(fn ($r) => (array) $r) ->all(); } -} +} \ No newline at end of file diff --git a/app/Models/FinalExam.php b/app/Models/FinalExam.php index 23f8abb1..499d7571 100644 --- a/app/Models/FinalExam.php +++ b/app/Models/FinalExam.php @@ -2,6 +2,8 @@ namespace App\Models; +use App\Models\BaseModel; + class FinalExam extends BaseModel { protected $table = 'final_exam'; @@ -31,19 +33,19 @@ class FinalExam extends BaseModel ]; protected $casts = [ - 'student_id' => 'integer', - 'school_id' => 'integer', + 'student_id' => 'integer', + 'school_id' => 'integer', 'class_section_id' => 'integer', - 'updated_by' => 'integer', + 'updated_by' => 'integer', // score is often numeric; adjust if yours is integer - 'score' => 'decimal:2', - 'max_points' => 'decimal:2', - 'locked_by' => 'integer', - 'locked_at' => 'datetime', + 'score' => 'decimal:2', + 'max_points' => 'decimal:2', + 'locked_by' => 'integer', + 'locked_at' => 'datetime', - 'created_at' => 'datetime', - 'updated_at' => 'datetime', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', ]; /* Optional relationships */ @@ -80,4 +82,4 @@ class FinalExam extends BaseModel return $row?->score; } -} +} \ No newline at end of file diff --git a/app/Models/FinalScore.php b/app/Models/FinalScore.php index 2b6ef597..88e4e46a 100644 --- a/app/Models/FinalScore.php +++ b/app/Models/FinalScore.php @@ -2,6 +2,8 @@ namespace App\Models; +use App\Models\BaseModel; + class FinalScore extends BaseModel { protected $table = 'final_score'; @@ -25,11 +27,11 @@ class FinalScore extends BaseModel ]; protected $casts = [ - 'student_id' => 'integer', - 'school_id' => 'integer', + 'student_id' => 'integer', + 'school_id' => 'integer', 'class_section_id' => 'integer', - 'teacher_id' => 'integer', - 'score' => 'decimal:2', // change to 'integer' if score is whole number + 'teacher_id' => 'integer', + 'score' => 'decimal:2', // change to 'integer' if score is whole number ]; /* Optional relationships */ @@ -62,4 +64,4 @@ class FinalScore extends BaseModel return $row?->score; } -} +} \ No newline at end of file diff --git a/app/Models/FinanceBalanceCarryforward.php b/app/Models/FinanceBalanceCarryforward.php index cee21143..776b63b8 100644 --- a/app/Models/FinanceBalanceCarryforward.php +++ b/app/Models/FinanceBalanceCarryforward.php @@ -5,8 +5,6 @@ namespace App\Models; class FinanceBalanceCarryforward extends BaseModel { protected $guarded = []; - protected $table = 'finance_balance_carryforwards'; - protected $casts = ['source_invoice_ids_json' => 'array', 'metadata_json' => 'array']; } diff --git a/app/Models/FinanceFollowUpNote.php b/app/Models/FinanceFollowUpNote.php index 34ecbdd5..bd292657 100644 --- a/app/Models/FinanceFollowUpNote.php +++ b/app/Models/FinanceFollowUpNote.php @@ -5,6 +5,5 @@ namespace App\Models; class FinanceFollowUpNote extends BaseModel { protected $guarded = []; - protected $table = 'finance_follow_up_notes'; } diff --git a/app/Models/FinanceNotificationLog.php b/app/Models/FinanceNotificationLog.php index 7a9e58eb..d4145137 100644 --- a/app/Models/FinanceNotificationLog.php +++ b/app/Models/FinanceNotificationLog.php @@ -5,6 +5,5 @@ namespace App\Models; class FinanceNotificationLog extends BaseModel { protected $guarded = []; - protected $table = 'finance_notification_logs'; } diff --git a/app/Models/FinanceNotificationPreference.php b/app/Models/FinanceNotificationPreference.php index 3544107d..4a05d1a5 100644 --- a/app/Models/FinanceNotificationPreference.php +++ b/app/Models/FinanceNotificationPreference.php @@ -5,6 +5,5 @@ namespace App\Models; class FinanceNotificationPreference extends BaseModel { protected $guarded = []; - protected $table = 'finance_notification_preferences'; } diff --git a/app/Models/FinanceReceiptSequence.php b/app/Models/FinanceReceiptSequence.php index 94494dcc..28a2d452 100644 --- a/app/Models/FinanceReceiptSequence.php +++ b/app/Models/FinanceReceiptSequence.php @@ -5,6 +5,5 @@ namespace App\Models; class FinanceReceiptSequence extends BaseModel { protected $guarded = []; - protected $table = 'finance_receipt_sequences'; } diff --git a/app/Models/GradingLock.php b/app/Models/GradingLock.php index 24865d70..314135a9 100644 --- a/app/Models/GradingLock.php +++ b/app/Models/GradingLock.php @@ -2,6 +2,8 @@ namespace App\Models; +use App\Models\BaseModel; + class GradingLock extends BaseModel { protected $table = 'grading_locks'; @@ -22,9 +24,9 @@ class GradingLock extends BaseModel protected $casts = [ 'class_section_id' => 'integer', - 'is_locked' => 'boolean', - 'locked_by' => 'integer', - 'locked_at' => 'datetime', + 'is_locked' => 'boolean', + 'locked_by' => 'integer', + 'locked_at' => 'datetime', ]; /* Optional relationships */ @@ -61,7 +63,6 @@ class GradingLock extends BaseModel public static function isLocked(int $classSectionId, string $semester, string $schoolYear): bool { $row = static::getLock($classSectionId, $semester, $schoolYear); - return (bool) ($row?->is_locked ?? false); } -} +} \ No newline at end of file diff --git a/app/Models/Homework.php b/app/Models/Homework.php index af827e8a..be7b8ecb 100644 --- a/app/Models/Homework.php +++ b/app/Models/Homework.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Models\BaseModel; use Illuminate\Support\Facades\DB; class Homework extends BaseModel @@ -34,20 +35,20 @@ class Homework extends BaseModel ]; protected $casts = [ - 'student_id' => 'integer', - 'school_id' => 'integer', + 'student_id' => 'integer', + 'school_id' => 'integer', 'class_section_id' => 'integer', - 'updated_by' => 'integer', - 'homework_index' => 'integer', + 'updated_by' => 'integer', + 'homework_index' => 'integer', // keep numeric; change to 'integer' if your scores are whole numbers - 'score' => 'decimal:2', - 'max_points' => 'decimal:2', - 'locked_by' => 'integer', - 'locked_at' => 'datetime', + 'score' => 'decimal:2', + 'max_points' => 'decimal:2', + 'locked_by' => 'integer', + 'locked_at' => 'datetime', - 'created_at' => 'datetime', - 'updated_at' => 'datetime', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', ]; /* Optional relationships */ @@ -92,18 +93,10 @@ class Homework extends BaseModel foreach ($rows as $r) { $score = $r->score ?? null; - if ($score === null) { - continue; - } - if (is_string($score) && trim($score) === '') { - continue; - } - if ($score === '') { - continue; - } - if (! is_numeric($score)) { - continue; - } + if ($score === null) continue; + if (is_string($score) && trim($score) === '') continue; + if ($score === '') continue; + if (!is_numeric($score)) continue; $total += (float) $score; $count++; @@ -115,4 +108,4 @@ class Homework extends BaseModel return round($total / $count, 2); } -} +} \ No newline at end of file diff --git a/app/Models/Incident.php b/app/Models/Incident.php index 5121d5d2..3ab7d922 100644 --- a/app/Models/Incident.php +++ b/app/Models/Incident.php @@ -2,6 +2,8 @@ namespace App\Models; +use App\Models\BaseModel; + class Incident extends BaseModel { protected $table = 'incident'; @@ -30,11 +32,11 @@ class Incident extends BaseModel ]; protected $casts = [ - 'student_id' => 'integer', - 'updated_by_open' => 'integer', - 'updated_by_closed' => 'integer', + 'student_id' => 'integer', + 'updated_by_open' => 'integer', + 'updated_by_closed' => 'integer', 'updated_by_canceled' => 'integer', - 'incident_datetime' => 'datetime', + 'incident_datetime' => 'datetime', ]; /* Optional relationships */ diff --git a/app/Models/InventoryCategory.php b/app/Models/InventoryCategory.php index c0901238..eb0cfbcb 100644 --- a/app/Models/InventoryCategory.php +++ b/app/Models/InventoryCategory.php @@ -2,6 +2,8 @@ namespace App\Models; +use App\Models\BaseModel; + class InventoryCategory extends BaseModel { protected $table = 'inventory_categories'; @@ -37,4 +39,4 @@ class InventoryCategory extends BaseModel ->get() ->toArray(); } -} +} \ No newline at end of file diff --git a/app/Models/InventoryItem.php b/app/Models/InventoryItem.php index 60e0af1e..51c0dbbb 100644 --- a/app/Models/InventoryItem.php +++ b/app/Models/InventoryItem.php @@ -2,6 +2,8 @@ namespace App\Models; +use App\Models\BaseModel; + class InventoryItem extends BaseModel { protected $table = 'inventory_items'; @@ -31,14 +33,14 @@ class InventoryItem extends BaseModel ]; protected $casts = [ - 'category_id' => 'integer', - 'quantity' => 'integer', - 'updated_by' => 'integer', + 'category_id' => 'integer', + 'quantity' => 'integer', + 'updated_by' => 'integer', - 'good_qty' => 'integer', - 'needs_repair_qty' => 'integer', - 'need_replace_qty' => 'integer', - 'cannot_find_qty' => 'integer', + 'good_qty' => 'integer', + 'needs_repair_qty' => 'integer', + 'need_replace_qty' => 'integer', + 'cannot_find_qty' => 'integer', ]; /* Optional relationships */ @@ -46,4 +48,4 @@ class InventoryItem extends BaseModel { return $this->belongsTo(InventoryCategory::class, 'category_id'); } -} +} \ No newline at end of file diff --git a/app/Models/InventoryMovement.php b/app/Models/InventoryMovement.php index f1c1f13a..b6bdeda9 100644 --- a/app/Models/InventoryMovement.php +++ b/app/Models/InventoryMovement.php @@ -2,6 +2,8 @@ namespace App\Models; +use App\Models\BaseModel; + class InventoryMovement extends BaseModel { protected $table = 'inventory_movements'; @@ -24,12 +26,12 @@ class InventoryMovement extends BaseModel ]; protected $casts = [ - 'item_id' => 'integer', - 'qty_change' => 'integer', - 'performed_by' => 'integer', - 'teacher_id' => 'integer', - 'student_id' => 'integer', - 'class_section_id' => 'integer', + 'item_id' => 'integer', + 'qty_change' => 'integer', + 'performed_by' => 'integer', + 'teacher_id' => 'integer', + 'student_id' => 'integer', + 'class_section_id'=> 'integer', ]; /* Optional relationships */ @@ -57,4 +59,4 @@ class InventoryMovement extends BaseModel { return $this->belongsTo(ClassSection::class, 'class_section_id'); } -} +} \ No newline at end of file diff --git a/app/Models/Invoice.php b/app/Models/Invoice.php index 38a28c37..f918522c 100644 --- a/app/Models/Invoice.php +++ b/app/Models/Invoice.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Models\BaseModel; use Illuminate\Support\Carbon; use Illuminate\Support\Facades\DB; @@ -34,19 +35,19 @@ class Invoice extends BaseModel ]; protected $casts = [ - 'parent_id' => 'integer', - 'updated_by' => 'integer', + 'parent_id' => 'integer', + 'updated_by' => 'integer', 'has_discount' => 'boolean', 'total_amount' => 'decimal:2', - 'balance' => 'decimal:2', - 'paid_amount' => 'decimal:2', + 'balance' => 'decimal:2', + 'paid_amount' => 'decimal:2', - 'issue_date' => 'datetime', - 'due_date' => 'datetime', + 'issue_date' => 'datetime', + 'due_date' => 'datetime', // created_at/updated_at exist but are DB-managed; you can still cast if you like: - 'created_at' => 'datetime', - 'updated_at' => 'datetime', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', ]; /* Optional relationships */ @@ -83,18 +84,13 @@ class Invoice extends BaseModel private function normalizeDateTimes(): void { foreach (['issue_date', 'due_date', 'created_at', 'updated_at'] as $field) { - if (! $this->isDirty($field)) { - continue; - } + if (!$this->isDirty($field)) continue; $val = $this->{$field}; if ($val === null || $val === '' || $val === '0000-00-00' || $val === '0000-00-00 00:00:00') { // keep NULL for due_date; keep other values as-is (DB defaults/triggers) - if ($field === 'due_date') { - $this->{$field} = null; - } - + if ($field === 'due_date') $this->{$field} = null; continue; } @@ -112,14 +108,11 @@ class Invoice extends BaseModel } $s = trim((string) $raw); - if ($s === '') { - return null; - } + if ($s === '') return null; // strict datetime try { $dt = Carbon::createFromFormat('Y-m-d H:i:s', $s, 'UTC'); - return $dt->utc()->format('Y-m-d H:i:s'); } catch (\Throwable $e) { // ignore @@ -128,7 +121,6 @@ class Invoice extends BaseModel // date-only sneaks in => store at 12:00:00 try { $d = Carbon::createFromFormat('Y-m-d', $s, 'UTC')->setTime(12, 0, 0); - return $d->utc()->format('Y-m-d H:i:s'); } catch (\Throwable $e) { // ignore @@ -215,7 +207,7 @@ class Invoice extends BaseModel ->selectRaw('(SELECT COALESCE(SUM(du.discount_amount),0) FROM discount_usages du WHERE du.invoice_id = i.id) AS discount') ->where('i.parent_id', $parentId); - if (! empty($schoolYear)) { + if (!empty($schoolYear)) { $q->where('i.school_year', $schoolYear); } @@ -250,12 +242,10 @@ class Invoice extends BaseModel public static function getInvoicesByUserIds(array $parentIds, string $schoolYear): array { $parentIds = array_values(array_unique(array_map('intval', array_filter($parentIds)))); - if (empty($parentIds)) { - return []; - } + if (empty($parentIds)) return []; return DB::table('invoices as i') - ->select(['i.id', 'i.parent_id', 'i.invoice_number', 'i.status', 'i.balance', 'i.total_amount', 'i.issue_date', 'i.due_date']) + ->select(['i.id','i.parent_id','i.invoice_number','i.status','i.balance','i.total_amount','i.issue_date','i.due_date']) ->selectRaw('(SELECT COALESCE(SUM(du.discount_amount),0) FROM discount_usages du WHERE du.invoice_id = i.id) AS discount') ->whereIn('i.parent_id', $parentIds) ->where('i.school_year', $schoolYear) @@ -269,12 +259,10 @@ class Invoice extends BaseModel public static function getAllInvoicesByUserIds(array $parentIds, string $schoolYear): array { $parentIds = array_values(array_unique(array_map('intval', array_filter($parentIds)))); - if (empty($parentIds)) { - return []; - } + if (empty($parentIds)) return []; return DB::table('invoices as i') - ->select(['i.id', 'i.parent_id', 'i.invoice_number', 'i.status', 'i.balance', 'i.issue_date']) + ->select(['i.id','i.parent_id','i.invoice_number','i.status','i.balance','i.issue_date']) ->selectRaw('(SELECT COALESCE(SUM(du.discount_amount),0) FROM discount_usages du WHERE du.invoice_id = i.id) AS discount') ->whereIn('i.parent_id', $parentIds) ->where('i.school_year', $schoolYear) @@ -326,10 +314,10 @@ class Invoice extends BaseModel $affected = DB::table('invoices') ->where('id', $invoiceId) ->update([ - 'total_amount' => DB::raw('GREATEST(total_amount - '.$amount.', 0)'), - 'balance' => DB::raw('GREATEST(balance - '.$amount.', 0)'), + 'total_amount' => DB::raw('GREATEST(total_amount - ' . $amount . ', 0)'), + 'balance' => DB::raw('GREATEST(balance - ' . $amount . ', 0)'), ]); return $affected >= 0; } -} +} \ No newline at end of file diff --git a/app/Models/InvoiceEvent.php b/app/Models/InvoiceEvent.php index 2ee0036a..8613000b 100644 --- a/app/Models/InvoiceEvent.php +++ b/app/Models/InvoiceEvent.php @@ -2,6 +2,8 @@ namespace App\Models; +use App\Models\BaseModel; + class InvoiceEvent extends BaseModel { protected $table = 'invoice_event'; @@ -22,9 +24,9 @@ class InvoiceEvent extends BaseModel ]; protected $casts = [ - 'invoice_id' => 'integer', - 'amount' => 'decimal:2', // adjust precision if needed - 'updated_by' => 'integer', + 'invoice_id' => 'integer', + 'amount' => 'decimal:2', // adjust precision if needed + 'updated_by' => 'integer', ]; /* Optional relationships */ @@ -37,4 +39,4 @@ class InvoiceEvent extends BaseModel { return $this->belongsTo(User::class, 'updated_by'); } -} +} \ No newline at end of file diff --git a/app/Models/InvoiceInstallment.php b/app/Models/InvoiceInstallment.php index daec4dd9..e9c0bb25 100644 --- a/app/Models/InvoiceInstallment.php +++ b/app/Models/InvoiceInstallment.php @@ -5,6 +5,5 @@ namespace App\Models; class InvoiceInstallment extends BaseModel { protected $guarded = []; - protected $table = 'invoice_installments'; } diff --git a/app/Models/InvoiceInstallmentPlan.php b/app/Models/InvoiceInstallmentPlan.php index eadc6987..610827a3 100644 --- a/app/Models/InvoiceInstallmentPlan.php +++ b/app/Models/InvoiceInstallmentPlan.php @@ -5,6 +5,5 @@ namespace App\Models; class InvoiceInstallmentPlan extends BaseModel { protected $guarded = []; - protected $table = 'invoice_installment_plans'; } diff --git a/app/Models/InvoiceStudentList.php b/app/Models/InvoiceStudentList.php index 0b2dab93..256bd20b 100644 --- a/app/Models/InvoiceStudentList.php +++ b/app/Models/InvoiceStudentList.php @@ -2,6 +2,8 @@ namespace App\Models; +use App\Models\BaseModel; + class InvoiceStudentList extends BaseModel { protected $table = 'invoice_students_list'; @@ -25,8 +27,8 @@ class InvoiceStudentList extends BaseModel protected $casts = [ 'invoice_id' => 'integer', 'student_id' => 'integer', - 'school_id' => 'integer', - 'enrolled' => 'boolean', + 'school_id' => 'integer', + 'enrolled' => 'boolean', ]; /* Optional relationships */ @@ -39,4 +41,4 @@ class InvoiceStudentList extends BaseModel { return $this->belongsTo(Student::class, 'student_id'); } -} +} \ No newline at end of file diff --git a/app/Models/IpAttempt.php b/app/Models/IpAttempt.php index ca1e6d75..b2a4175d 100644 --- a/app/Models/IpAttempt.php +++ b/app/Models/IpAttempt.php @@ -2,6 +2,8 @@ namespace App\Models; +use App\Models\BaseModel; + class IpAttempt extends BaseModel { protected $table = 'ip_attempts'; @@ -19,9 +21,9 @@ class IpAttempt extends BaseModel ]; protected $casts = [ - 'attempts' => 'integer', + 'attempts' => 'integer', 'last_attempt_at' => 'datetime', - 'blocked_until' => 'datetime', + 'blocked_until' => 'datetime', ]; /** @@ -51,7 +53,6 @@ class IpAttempt extends BaseModel public static function insertIpAttempt(array $data): int { $row = static::query()->create($data); - return (int) $row->id; } -} +} \ No newline at end of file diff --git a/app/Models/LateSlipLog.php b/app/Models/LateSlipLog.php index 85be8c58..c8103abd 100644 --- a/app/Models/LateSlipLog.php +++ b/app/Models/LateSlipLog.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Models\BaseModel; use Illuminate\Database\QueryException; use Illuminate\Support\Facades\Log; @@ -27,10 +28,10 @@ class LateSlipLog extends BaseModel protected $casts = [ 'printed_by' => 'integer', - 'slip_date' => 'date', // slip_date is Y-m-d + 'slip_date' => 'date', // slip_date is Y-m-d 'printed_at' => 'datetime', // time_in is often stored as TIME or string; keep as string unless you store as DATETIME - 'time_in' => 'string', + 'time_in' => 'string', ]; /** @@ -40,30 +41,27 @@ class LateSlipLog extends BaseModel public static function logSlip(array $data, ?int $printedBy): bool { $row = [ - 'school_year' => (string) ($data['school_year'] ?? ''), - 'semester' => (string) ($data['semester'] ?? ''), - 'student_name' => (string) ($data['student_name'] ?? ''), - 'slip_date' => $data['slip_date'] ?? null, // Y-m-d - 'time_in' => $data['time_in'] ?? null, // H:i:s - 'grade' => (string) ($data['grade'] ?? ''), - 'reason' => (string) ($data['reason'] ?? ''), - 'admin_name' => (string) ($data['admin_name'] ?? ''), - 'printed_by' => $printedBy, - 'printed_at' => now(), // use UTC if your app timezone is UTC + 'school_year' => (string)($data['school_year'] ?? ''), + 'semester' => (string)($data['semester'] ?? ''), + 'student_name' => (string)($data['student_name'] ?? ''), + 'slip_date' => $data['slip_date'] ?? null, // Y-m-d + 'time_in' => $data['time_in'] ?? null, // H:i:s + 'grade' => (string)($data['grade'] ?? ''), + 'reason' => (string)($data['reason'] ?? ''), + 'admin_name' => (string)($data['admin_name'] ?? ''), + 'printed_by' => $printedBy, + 'printed_at' => now(), // use UTC if your app timezone is UTC ]; try { // create() returns model; treat as success if created $created = static::query()->create($row); - - return ! empty($created->id); + return !empty($created->id); } catch (QueryException $e) { - Log::error('LateSlipLog::logSlip failed: '.$e->getMessage()); - + Log::error('LateSlipLog::logSlip failed: ' . $e->getMessage()); return false; } catch (\Throwable $e) { - Log::error('LateSlipLog::logSlip failed: '.$e->getMessage()); - + Log::error('LateSlipLog::logSlip failed: ' . $e->getMessage()); return false; } } @@ -73,4 +71,4 @@ class LateSlipLog extends BaseModel { return $this->belongsTo(User::class, 'printed_by'); } -} +} \ No newline at end of file diff --git a/app/Models/LoginActivity.php b/app/Models/LoginActivity.php index df5cb17e..a2995895 100644 --- a/app/Models/LoginActivity.php +++ b/app/Models/LoginActivity.php @@ -2,6 +2,8 @@ namespace App\Models; +use App\Models\BaseModel; + class LoginActivity extends BaseModel { protected $table = 'login_activity'; @@ -23,8 +25,8 @@ class LoginActivity extends BaseModel ]; protected $casts = [ - 'user_id' => 'integer', - 'login_time' => 'datetime', + 'user_id' => 'integer', + 'login_time' => 'datetime', 'logout_time' => 'datetime', ]; @@ -44,4 +46,4 @@ class LoginActivity extends BaseModel ->limit(max(1, $limit)) ->get(); } -} +} \ No newline at end of file diff --git a/app/Models/ManualPayment.php b/app/Models/ManualPayment.php index bece370d..a5edf9e9 100644 --- a/app/Models/ManualPayment.php +++ b/app/Models/ManualPayment.php @@ -2,6 +2,8 @@ namespace App\Models; +use App\Models\BaseModel; + class ManualPayment extends BaseModel { protected $table = 'manual_payments'; @@ -21,7 +23,7 @@ class ManualPayment extends BaseModel ]; protected $casts = [ - 'amount' => 'decimal:2', // adjust precision if needed + 'amount' => 'decimal:2', // adjust precision if needed 'created_at' => 'datetime', ]; @@ -30,4 +32,4 @@ class ManualPayment extends BaseModel { return $this->belongsTo(Invoice::class, 'invoice_number', 'invoice_number'); } -} +} \ No newline at end of file diff --git a/app/Models/Message.php b/app/Models/Message.php index 951a1c66..ed7dcbdc 100644 --- a/app/Models/Message.php +++ b/app/Models/Message.php @@ -2,6 +2,8 @@ namespace App\Models; +use App\Models\BaseModel; + class Message extends BaseModel { protected $table = 'messages'; @@ -26,13 +28,13 @@ class Message extends BaseModel ]; protected $casts = [ - 'sender_id' => 'integer', - 'recipient_id' => 'integer', - 'read_status' => 'boolean', + 'sender_id' => 'integer', + 'recipient_id' => 'integer', + 'read_status' => 'boolean', 'message_number' => 'integer', - 'sent_datetime' => 'datetime', - 'read_datetime' => 'datetime', + 'sent_datetime' => 'datetime', + 'read_datetime' => 'datetime', ]; /* Optional relationships */ @@ -64,7 +66,7 @@ class Message extends BaseModel $affected = static::query() ->whereKey($messageId) ->update([ - 'read_status' => 1, + 'read_status' => 1, 'read_datetime' => now(), // set app timezone (configure to UTC if desired) ]); @@ -119,7 +121,7 @@ class Message extends BaseModel return static::query() ->where(function ($q) use ($userId) { $q->where('sender_id', $userId) - ->orWhere('recipient_id', $userId); + ->orWhere('recipient_id', $userId); }) ->where('status', 'trashed') ->orderByDesc('sent_datetime') @@ -130,10 +132,10 @@ class Message extends BaseModel { $q = static::query()->where('semester', $semester); - if (! empty($schoolYear)) { + if (!empty($schoolYear)) { $q->where('school_year', $schoolYear); } return $q->orderByDesc('sent_datetime')->get(); } -} +} \ No newline at end of file diff --git a/app/Models/MidtermExam.php b/app/Models/MidtermExam.php index 5feaeb8d..9ca5eace 100644 --- a/app/Models/MidtermExam.php +++ b/app/Models/MidtermExam.php @@ -2,6 +2,8 @@ namespace App\Models; +use App\Models\BaseModel; + class MidtermExam extends BaseModel { protected $table = 'midterm_exam'; @@ -31,19 +33,19 @@ class MidtermExam extends BaseModel ]; protected $casts = [ - 'student_id' => 'integer', - 'school_id' => 'integer', + 'student_id' => 'integer', + 'school_id' => 'integer', 'class_section_id' => 'integer', - 'updated_by' => 'integer', + 'updated_by' => 'integer', // score can be decimal; change to 'integer' if it's whole number - 'score' => 'decimal:2', - 'max_points' => 'decimal:2', - 'locked_by' => 'integer', - 'locked_at' => 'datetime', + 'score' => 'decimal:2', + 'max_points' => 'decimal:2', + 'locked_by' => 'integer', + 'locked_at' => 'datetime', - 'created_at' => 'datetime', - 'updated_at' => 'datetime', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', ]; /* Optional relationships */ @@ -83,4 +85,4 @@ class MidtermExam extends BaseModel return $row?->score; } -} +} \ No newline at end of file diff --git a/app/Models/MissingScoreOverride.php b/app/Models/MissingScoreOverride.php index 0831fe47..cc0bf55e 100644 --- a/app/Models/MissingScoreOverride.php +++ b/app/Models/MissingScoreOverride.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Models\BaseModel; use Illuminate\Support\Facades\DB; class MissingScoreOverride extends BaseModel @@ -25,11 +26,11 @@ class MissingScoreOverride extends BaseModel ]; protected $casts = [ - 'student_id' => 'integer', + 'student_id' => 'integer', 'class_section_id' => 'integer', - 'item_index' => 'integer', - 'is_allowed' => 'boolean', - 'updated_by' => 'integer', + 'item_index' => 'integer', + 'is_allowed' => 'boolean', + 'updated_by' => 'integer', ]; /** @@ -100,12 +101,12 @@ class MissingScoreOverride extends BaseModel ->where('school_year', $schoolYear) ->where('item_type', $itemType); - if (! empty($studentIds)) { + if (!empty($studentIds)) { $q->whereIn('student_id', array_map('intval', $studentIds)); } if ($itemIndexes !== null) { - if (! empty($itemIndexes)) { + if (!empty($itemIndexes)) { $q->whereIn('item_index', array_map('intval', $itemIndexes)); } else { // legacy behavior: if itemIndexes is provided but empty, do nothing. @@ -127,27 +128,25 @@ class MissingScoreOverride extends BaseModel foreach ($checkedItems as $item) { $sid = (int) ($item['student_id'] ?? 0); - if ($sid <= 0) { - continue; - } + if ($sid <= 0) continue; $rows[] = [ - 'student_id' => $sid, + 'student_id' => $sid, 'class_section_id' => $classSectionId, - 'semester' => $semester, - 'school_year' => $schoolYear, - 'item_type' => $itemType, - 'item_index' => array_key_exists('item_index', $item) ? $item['item_index'] : null, - 'is_allowed' => 1, - 'updated_by' => $updatedBy, - 'created_at' => $now, - 'updated_at' => $now, + 'semester' => $semester, + 'school_year' => $schoolYear, + 'item_type' => $itemType, + 'item_index' => array_key_exists('item_index', $item) ? $item['item_index'] : null, + 'is_allowed' => 1, + 'updated_by' => $updatedBy, + 'created_at' => $now, + 'updated_at' => $now, ]; } - if (! empty($rows)) { + if (!empty($rows)) { DB::table((new static)->getTable())->insert($rows); } }); } -} +} \ No newline at end of file diff --git a/app/Models/NavItem.php b/app/Models/NavItem.php index d425fc15..deb66f32 100644 --- a/app/Models/NavItem.php +++ b/app/Models/NavItem.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Models\BaseModel; use Illuminate\Database\Eloquent\SoftDeletes; class NavItem extends BaseModel @@ -10,7 +11,6 @@ class NavItem extends BaseModel // ✅ legacy: timestamps + soft deletes use SoftDeletes; - public $timestamps = true; protected $fillable = [ @@ -25,8 +25,8 @@ class NavItem extends BaseModel protected $casts = [ 'menu_parent_id' => 'integer', - 'sort_order' => 'integer', - 'is_enabled' => 'boolean', + 'sort_order' => 'integer', + 'is_enabled' => 'boolean', ]; /* Optional relationships */ @@ -55,4 +55,4 @@ class NavItem extends BaseModel ->get() ->toArray(); } -} +} \ No newline at end of file diff --git a/app/Models/Notification.php b/app/Models/Notification.php index 056855ef..94b895b9 100644 --- a/app/Models/Notification.php +++ b/app/Models/Notification.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Models\BaseModel; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\SoftDeletes; @@ -11,7 +12,6 @@ class Notification extends BaseModel // ✅ legacy: soft deletes + timestamps use SoftDeletes; - public $timestamps = true; protected $fillable = [ @@ -73,10 +73,10 @@ class Notification extends BaseModel ->where('scheduled_at', '<=', now()) ->where(function ($w) { $w->whereNull('expires_at') - ->orWhere('expires_at', '>', now()); + ->orWhere('expires_at', '>', now()); }); - if (! empty($targetGroup)) { + if (!empty($targetGroup)) { $q->where('target_group', $targetGroup); } @@ -114,10 +114,7 @@ class Notification extends BaseModel public static function restoreNotification(int $id): bool { $row = static::withTrashed()->find($id); - if (! $row) { - return false; - } - + if (!$row) return false; return (bool) $row->restore(); } @@ -132,4 +129,4 @@ class Notification extends BaseModel ->where('expires_at', '<', now()) ->delete(); // soft delete } -} +} \ No newline at end of file diff --git a/app/Models/ParentAttendanceReport.php b/app/Models/ParentAttendanceReport.php index d4ae0341..d559d052 100644 --- a/app/Models/ParentAttendanceReport.php +++ b/app/Models/ParentAttendanceReport.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Models\BaseModel; use Illuminate\Support\Facades\DB; class ParentAttendanceReport extends BaseModel @@ -26,10 +27,10 @@ class ParentAttendanceReport extends BaseModel ]; protected $casts = [ - 'parent_id' => 'integer', - 'student_id' => 'integer', - 'class_section_id' => 'integer', - 'report_date' => 'date', + 'parent_id' => 'integer', + 'student_id' => 'integer', + 'class_section_id'=> 'integer', + 'report_date' => 'date', ]; /* Optional relationships */ @@ -67,10 +68,10 @@ class ParentAttendanceReport extends BaseModel $q->where('par.parent_id', $parentId); } - if (! empty($startDate)) { + if (!empty($startDate)) { $q->where('par.report_date', '>=', $startDate); } - if (! empty($endDate)) { + if (!empty($endDate)) { $q->where('par.report_date', '<=', $endDate); } if (is_string($schoolYear) && $schoolYear !== '') { @@ -81,12 +82,12 @@ class ParentAttendanceReport extends BaseModel } $q->leftJoin('students as s', 's.id', '=', 'par.student_id') - ->leftJoin('classSection as cs', 'cs.class_section_id', '=', 'par.class_section_id') - ->select('par.*', 's.firstname', 's.lastname', 'cs.class_section_name') - ->orderBy('par.report_date', 'asc') - ->orderBy('par.class_section_id', 'asc') - ->orderBy('par.student_id', 'asc'); + ->leftJoin('classSection as cs', 'cs.class_section_id', '=', 'par.class_section_id') + ->select('par.*', 's.firstname', 's.lastname', 'cs.class_section_name') + ->orderBy('par.report_date', 'asc') + ->orderBy('par.class_section_id', 'asc') + ->orderBy('par.student_id', 'asc'); return $q->get()->map(fn ($r) => (array) $r)->all(); } -} +} \ No newline at end of file diff --git a/app/Models/ParentMeetingSchedule.php b/app/Models/ParentMeetingSchedule.php index 6213d9e8..b0d68924 100644 --- a/app/Models/ParentMeetingSchedule.php +++ b/app/Models/ParentMeetingSchedule.php @@ -2,6 +2,8 @@ namespace App\Models; +use App\Models\BaseModel; + class ParentMeetingSchedule extends BaseModel { protected $table = 'parent_meeting_schedules'; @@ -25,14 +27,14 @@ class ParentMeetingSchedule extends BaseModel ]; protected $casts = [ - 'student_id' => 'integer', + 'student_id' => 'integer', 'parent_user_id' => 'integer', - 'created_by' => 'integer', + 'created_by' => 'integer', // if these are DATE/TIME columns: - 'date' => 'date', + 'date' => 'date', // time is typically stored as TIME or string; keep as string unless DATETIME - 'time' => 'string', + 'time' => 'string', ]; /* Optional relationships */ @@ -50,4 +52,4 @@ class ParentMeetingSchedule extends BaseModel { return $this->belongsTo(User::class, 'created_by'); } -} +} \ No newline at end of file diff --git a/app/Models/ParentModel.php b/app/Models/ParentModel.php index dbaba3fc..1313ffad 100644 --- a/app/Models/ParentModel.php +++ b/app/Models/ParentModel.php @@ -2,6 +2,8 @@ namespace App\Models; +use App\Models\BaseModel; + class ParentModel extends BaseModel { protected $table = 'parents'; @@ -29,4 +31,4 @@ class ParentModel extends BaseModel { return $this->belongsTo(User::class, 'firstparent_id'); } -} +} \ No newline at end of file diff --git a/app/Models/ParentNotification.php b/app/Models/ParentNotification.php index 2a41cc83..d09c6b94 100644 --- a/app/Models/ParentNotification.php +++ b/app/Models/ParentNotification.php @@ -2,6 +2,8 @@ namespace App\Models; +use App\Models\BaseModel; + class ParentNotification extends BaseModel { protected $table = 'parent_notifications'; @@ -23,7 +25,7 @@ class ParentNotification extends BaseModel ]; protected $casts = [ - 'student_id' => 'integer', + 'student_id' => 'integer', // incident_date is stored as Y-m-d in legacy usage 'incident_date' => 'date', ]; @@ -50,7 +52,7 @@ class ParentNotification extends BaseModel ->whereDate('incident_date', $incidentYmd) ->where('channel', $channel); - if (! empty($to)) { + if (!empty($to)) { $q->where('to_address', $to); } @@ -58,4 +60,4 @@ class ParentNotification extends BaseModel return $row && (($row->status ?? '') === 'sent'); } -} +} \ No newline at end of file diff --git a/app/Models/Participation.php b/app/Models/Participation.php index 241d278f..4abe7d62 100644 --- a/app/Models/Participation.php +++ b/app/Models/Participation.php @@ -2,6 +2,8 @@ namespace App\Models; +use App\Models\BaseModel; + class Participation extends BaseModel { protected $table = 'participation'; @@ -31,19 +33,19 @@ class Participation extends BaseModel ]; protected $casts = [ - 'student_id' => 'integer', - 'school_id' => 'integer', + 'student_id' => 'integer', + 'school_id' => 'integer', 'class_section_id' => 'integer', - 'updated_by' => 'integer', + 'updated_by' => 'integer', // score is numeric; change to 'integer' if it is whole number - 'score' => 'decimal:2', - 'max_points' => 'decimal:2', - 'locked_by' => 'integer', - 'locked_at' => 'datetime', + 'score' => 'decimal:2', + 'max_points' => 'decimal:2', + 'locked_by' => 'integer', + 'locked_at' => 'datetime', - 'created_at' => 'datetime', - 'updated_at' => 'datetime', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', ]; /* Optional relationships */ @@ -78,25 +80,15 @@ class Participation extends BaseModel } $row = $q->first(); - if (! $row) { - return null; - } + if (!$row) return null; $score = $row->score; - if ($score === null) { - return null; - } - if (is_string($score) && trim($score) === '') { - return null; - } - if ($score === '') { - return null; - } - if (! is_numeric($score)) { - return null; - } + if ($score === null) return null; + if (is_string($score) && trim($score) === '') return null; + if ($score === '') return null; + if (!is_numeric($score)) return null; return (float) $score; } -} +} \ No newline at end of file diff --git a/app/Models/PasswordReset.php b/app/Models/PasswordReset.php index ab6f1330..7857f199 100644 --- a/app/Models/PasswordReset.php +++ b/app/Models/PasswordReset.php @@ -2,6 +2,8 @@ namespace App\Models; +use App\Models\BaseModel; + class PasswordReset extends BaseModel { protected $table = 'password_resets'; @@ -21,7 +23,6 @@ class PasswordReset extends BaseModel public $timestamps = true; const CREATED_AT = 'created_at'; - const UPDATED_AT = 'expires_at'; protected $fillable = [ @@ -44,4 +45,4 @@ class PasswordReset extends BaseModel // protected $primaryKey = 'id'; // public $incrementing = true; // protected $keyType = 'int'; -} +} \ No newline at end of file diff --git a/app/Models/PasswordResetRequest.php b/app/Models/PasswordResetRequest.php index b136b6f9..f424b1a1 100644 --- a/app/Models/PasswordResetRequest.php +++ b/app/Models/PasswordResetRequest.php @@ -2,6 +2,8 @@ namespace App\Models; +use App\Models\BaseModel; + class PasswordResetRequest extends BaseModel { protected $table = 'password_reset_requests'; @@ -17,4 +19,4 @@ class PasswordResetRequest extends BaseModel protected $casts = [ 'requested_at' => 'datetime', ]; -} +} \ No newline at end of file diff --git a/app/Models/PayPalPayment.php b/app/Models/PayPalPayment.php index 1c7e56c0..25586958 100644 --- a/app/Models/PayPalPayment.php +++ b/app/Models/PayPalPayment.php @@ -2,6 +2,8 @@ namespace App\Models; +use App\Models\BaseModel; + class PayPalPayment extends BaseModel { protected $table = 'paypal_payments'; @@ -11,7 +13,6 @@ class PayPalPayment extends BaseModel * In Laravel: enable timestamps but disable UPDATED_AT. */ public $timestamps = true; - const UPDATED_AT = null; protected $fillable = [ @@ -37,10 +38,10 @@ class PayPalPayment extends BaseModel protected $casts = [ 'parent_school_id' => 'integer', - 'amount' => 'decimal:2', - 'paypal_fee' => 'decimal:2', - 'net_amount' => 'decimal:2', - 'synced' => 'boolean', - 'sync_attempts' => 'integer', + 'amount' => 'decimal:2', + 'paypal_fee' => 'decimal:2', + 'net_amount' => 'decimal:2', + 'synced' => 'boolean', + 'sync_attempts' => 'integer', ]; -} +} \ No newline at end of file diff --git a/app/Models/Payment.php b/app/Models/Payment.php index a06bb29b..e57892a2 100644 --- a/app/Models/Payment.php +++ b/app/Models/Payment.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Models\BaseModel; use Illuminate\Support\Facades\DB; class Payment extends BaseModel @@ -33,16 +34,16 @@ class Payment extends BaseModel ]; protected $casts = [ - 'parent_id' => 'integer', - 'invoice_id' => 'integer', - 'number_of_installments' => 'integer', - 'updated_by' => 'integer', + 'parent_id' => 'integer', + 'invoice_id' => 'integer', + 'number_of_installments' => 'integer', + 'updated_by' => 'integer', - 'total_amount' => 'decimal:2', - 'paid_amount' => 'decimal:2', - 'balance' => 'decimal:2', + 'total_amount' => 'decimal:2', + 'paid_amount' => 'decimal:2', + 'balance' => 'decimal:2', - 'payment_date' => 'datetime', + 'payment_date' => 'datetime', ]; /* Optional relationships */ @@ -83,20 +84,18 @@ class Payment extends BaseModel { /** @var self|null $payment */ $payment = static::query()->find($paymentId); - if (! $payment) { - return false; - } + if (!$payment) return false; - $newBalance = (float) $payment->balance - (float) $amountPaid; + $newBalance = (float)$payment->balance - (float)$amountPaid; // keep precision stable - $paidAmount = (float) $payment->paid_amount + (float) $amountPaid; + $paidAmount = (float)$payment->paid_amount + (float)$amountPaid; $affected = static::query() ->whereKey($paymentId) ->update([ 'paid_amount' => $paidAmount, - 'balance' => $newBalance, + 'balance' => $newBalance, ]); return $affected > 0; @@ -117,21 +116,21 @@ class Payment extends BaseModel public static function generateNewTransactionId(): string { $currentYear = date('Y'); - $prefix = $currentYear.'-'; + $prefix = $currentYear . '-'; $latest = static::query() - ->where('transaction_id', 'like', $prefix.'%') + ->where('transaction_id', 'like', $prefix . '%') ->orderByDesc('transaction_id') ->first(); - if ($latest && ! empty($latest->transaction_id)) { - $lastNumber = (int) str_replace($prefix, '', (string) $latest->transaction_id); + if ($latest && !empty($latest->transaction_id)) { + $lastNumber = (int) str_replace($prefix, '', (string)$latest->transaction_id); $newNumber = $lastNumber + 1; } else { $newNumber = 1; } - return $prefix.str_pad((string) $newNumber, 6, '0', STR_PAD_LEFT); + return $prefix . str_pad((string)$newNumber, 6, '0', STR_PAD_LEFT); } /** @@ -140,13 +139,11 @@ class Payment extends BaseModel */ public static function getPaymentsByInvoice($invoiceIds): array { - if (! is_array($invoiceIds)) { + if (!is_array($invoiceIds)) { $invoiceIds = [$invoiceIds]; } $invoiceIds = array_values(array_unique(array_map('intval', array_filter($invoiceIds)))); - if (empty($invoiceIds)) { - return []; - } + if (empty($invoiceIds)) return []; // Subquery: latest payment_date per invoice (Full/Partial only) $latestSub = DB::table('payments') @@ -158,7 +155,7 @@ class Payment extends BaseModel $rows = DB::table('payments as p') ->joinSub($latestSub, 'latest', function ($join) { $join->on('latest.invoice_id', '=', 'p.invoice_id') - ->on('latest.max_date', '=', 'p.payment_date'); + ->on('latest.max_date', '=', 'p.payment_date'); }) ->whereIn('p.invoice_id', $invoiceIds) ->whereIn('p.status', ['Full', 'Paid', 'Partially Paid']) @@ -172,4 +169,4 @@ class Payment extends BaseModel return $rows->map(fn ($r) => (array) $r)->all(); } -} +} \ No newline at end of file diff --git a/app/Models/PaymentCarryforwardAllocation.php b/app/Models/PaymentCarryforwardAllocation.php index dd01f8cb..0c2118f8 100644 --- a/app/Models/PaymentCarryforwardAllocation.php +++ b/app/Models/PaymentCarryforwardAllocation.php @@ -5,6 +5,5 @@ namespace App\Models; class PaymentCarryforwardAllocation extends BaseModel { protected $guarded = []; - protected $table = 'payment_carryforward_allocations'; } diff --git a/app/Models/PaymentError.php b/app/Models/PaymentError.php index ed41bbd3..753b7a34 100644 --- a/app/Models/PaymentError.php +++ b/app/Models/PaymentError.php @@ -2,6 +2,8 @@ namespace App\Models; +use App\Models\BaseModel; + class PaymentError extends BaseModel { protected $table = 'payment_error'; @@ -24,12 +26,12 @@ class PaymentError extends BaseModel ]; protected $casts = [ - 'payment_id' => 'integer', - 'invoice_id' => 'integer', - 'parent_id' => 'integer', - 'wrong_paid_amount' => 'decimal:2', // adjust precision if needed - 'logged_by' => 'integer', - 'logged_at' => 'datetime', + 'payment_id' => 'integer', + 'invoice_id' => 'integer', + 'parent_id' => 'integer', + 'wrong_paid_amount' => 'decimal:2', // adjust precision if needed + 'logged_by' => 'integer', + 'logged_at' => 'datetime', ]; /* Optional relationships */ @@ -47,4 +49,4 @@ class PaymentError extends BaseModel { return $this->belongsTo(User::class, 'logged_by'); } -} +} \ No newline at end of file diff --git a/app/Models/PaymentInstallmentAllocation.php b/app/Models/PaymentInstallmentAllocation.php index 921913eb..2ea9896c 100644 --- a/app/Models/PaymentInstallmentAllocation.php +++ b/app/Models/PaymentInstallmentAllocation.php @@ -5,6 +5,5 @@ namespace App\Models; class PaymentInstallmentAllocation extends BaseModel { protected $guarded = []; - protected $table = 'payment_installment_allocations'; } diff --git a/app/Models/PaymentNotificationLog.php b/app/Models/PaymentNotificationLog.php index c8781282..128d0738 100644 --- a/app/Models/PaymentNotificationLog.php +++ b/app/Models/PaymentNotificationLog.php @@ -2,6 +2,8 @@ namespace App\Models; +use App\Models\BaseModel; + class PaymentNotificationLog extends BaseModel { protected $table = 'payment_notification_logs'; @@ -29,14 +31,14 @@ class PaymentNotificationLog extends BaseModel ]; protected $casts = [ - 'parent_id' => 'integer', - 'invoice_id' => 'integer', - 'period_year' => 'integer', - 'period_month' => 'integer', - 'head_fa_notified' => 'boolean', - 'balance_snapshot' => 'decimal:2', // adjust precision if needed - 'created_at' => 'datetime', - 'sent_at' => 'datetime', + 'parent_id' => 'integer', + 'invoice_id' => 'integer', + 'period_year' => 'integer', + 'period_month' => 'integer', + 'head_fa_notified' => 'boolean', + 'balance_snapshot' => 'decimal:2', // adjust precision if needed + 'created_at' => 'datetime', + 'sent_at' => 'datetime', ]; /* Optional relationships */ @@ -71,16 +73,16 @@ class PaymentNotificationLog extends BaseModel { $q = static::query()->orderByDesc('sent_at'); - if (! empty($from)) { + if (!empty($from)) { $q->where('sent_at', '>=', $from); } - if (! empty($to)) { + if (!empty($to)) { $q->where('sent_at', '<=', $to); } - if (! empty($type)) { + if (!empty($type)) { $q->where('type', $type); } return $q->get()->toArray(); } -} +} \ No newline at end of file diff --git a/app/Models/PaymentTransaction.php b/app/Models/PaymentTransaction.php index a765d81e..506323b2 100644 --- a/app/Models/PaymentTransaction.php +++ b/app/Models/PaymentTransaction.php @@ -2,6 +2,9 @@ namespace App\Models; +use App\Models\BaseModel; +use Illuminate\Support\Facades\DB; + class PaymentTransaction extends BaseModel { protected $table = 'payment_transactions'; @@ -26,11 +29,11 @@ class PaymentTransaction extends BaseModel ]; protected $casts = [ - 'payment_id' => 'integer', - 'amount' => 'decimal:2', // adjust precision if needed - 'transaction_fee' => 'decimal:2', // adjust precision if needed - 'transaction_date' => 'datetime', - 'is_full_payment' => 'boolean', + 'payment_id' => 'integer', + 'amount' => 'decimal:2', // adjust precision if needed + 'transaction_fee' => 'decimal:2', // adjust precision if needed + 'transaction_date' => 'datetime', + 'is_full_payment' => 'boolean', ]; /* Optional relationship */ diff --git a/app/Models/PaypalTransaction.php b/app/Models/PaypalTransaction.php index 375f6de5..1e4b2138 100644 --- a/app/Models/PaypalTransaction.php +++ b/app/Models/PaypalTransaction.php @@ -2,6 +2,8 @@ namespace App\Models; +use App\Models\BaseModel; + class PaypalTransaction extends BaseModel { protected $table = 'paypal_transactions'; @@ -27,11 +29,11 @@ class PaypalTransaction extends BaseModel ]; protected $casts = [ - 'payment_id' => 'integer', - 'amount' => 'decimal:2', // adjust precision if needed - 'transaction_fee' => 'decimal:2', // adjust precision if needed + 'payment_id' => 'integer', + 'amount' => 'decimal:2', // adjust precision if needed + 'transaction_fee' => 'decimal:2', // adjust precision if needed // raw_data is often JSON; if it's JSON in DB, cast to array - 'raw_data' => 'array', + 'raw_data' => 'array', ]; /* Optional relationship */ @@ -39,4 +41,4 @@ class PaypalTransaction extends BaseModel { return $this->belongsTo(Payment::class, 'payment_id'); } -} +} \ No newline at end of file diff --git a/app/Models/Permission.php b/app/Models/Permission.php index c92367ea..51c6ff03 100644 --- a/app/Models/Permission.php +++ b/app/Models/Permission.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Models\BaseModel; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; @@ -31,7 +32,7 @@ class Permission extends BaseModel try { return static::query()->get(); } catch (\Throwable $e) { - Log::error('Error fetching permissions: '.$e->getMessage()); + Log::error('Error fetching permissions: ' . $e->getMessage()); throw $e; } } @@ -52,12 +53,12 @@ class Permission extends BaseModel ->map(fn ($r) => (array) $r) ->all(); - Log::info('Role permissions fetched: '.print_r($rows, true)); + Log::info('Role permissions fetched: ' . print_r($rows, true)); return $rows; } catch (\Throwable $e) { - Log::error("Error fetching role permissions for role ID {$roleId}: ".$e->getMessage()); + Log::error("Error fetching role permissions for role ID {$roleId}: " . $e->getMessage()); throw $e; } } -} +} \ No newline at end of file diff --git a/app/Models/PlacementBatch.php b/app/Models/PlacementBatch.php index 1fd0855c..1cdc517c 100644 --- a/app/Models/PlacementBatch.php +++ b/app/Models/PlacementBatch.php @@ -2,6 +2,8 @@ namespace App\Models; +use App\Models\BaseModel; + class PlacementBatch extends BaseModel { protected $table = 'placement_batches'; @@ -33,4 +35,4 @@ class PlacementBatch extends BaseModel { return $this->belongsTo(User::class, 'updated_by'); } -} +} \ No newline at end of file diff --git a/app/Models/PlacementLevel.php b/app/Models/PlacementLevel.php index a1b251bd..17155cec 100644 --- a/app/Models/PlacementLevel.php +++ b/app/Models/PlacementLevel.php @@ -2,6 +2,8 @@ namespace App\Models; +use App\Models\BaseModel; + class PlacementLevel extends BaseModel { protected $table = 'placement_levels'; @@ -40,4 +42,4 @@ class PlacementLevel extends BaseModel { return $this->belongsTo(User::class, 'updated_by'); } -} +} \ No newline at end of file diff --git a/app/Models/PlacementScore.php b/app/Models/PlacementScore.php index 0cb61c10..21f41b3f 100644 --- a/app/Models/PlacementScore.php +++ b/app/Models/PlacementScore.php @@ -2,6 +2,8 @@ namespace App\Models; +use App\Models\BaseModel; + class PlacementScore extends BaseModel { protected $table = 'placement_scores'; @@ -20,9 +22,9 @@ class PlacementScore extends BaseModel ]; protected $casts = [ - 'batch_id' => 'integer', + 'batch_id' => 'integer', 'student_id' => 'integer', - 'score' => 'decimal:2', // change to 'integer' if score is whole number + 'score' => 'decimal:2', // change to 'integer' if score is whole number 'created_by' => 'integer', 'updated_by' => 'integer', ]; @@ -47,4 +49,4 @@ class PlacementScore extends BaseModel { return $this->belongsTo(User::class, 'updated_by'); } -} +} \ No newline at end of file diff --git a/app/Models/Preferences.php b/app/Models/Preferences.php index e0434c00..4b202b82 100644 --- a/app/Models/Preferences.php +++ b/app/Models/Preferences.php @@ -2,6 +2,8 @@ namespace App\Models; +use App\Models\BaseModel; + class Preferences extends BaseModel { protected $table = 'user_preferences'; @@ -32,15 +34,15 @@ class Preferences extends BaseModel ]; protected $casts = [ - 'user_id' => 'integer', + 'user_id' => 'integer', 'receive_email_notifications' => 'boolean', - 'receive_sms_notifications' => 'boolean', - 'receive_push_notifications' => 'boolean', - 'daily_summary_email' => 'boolean', - 'privacy_mode' => 'boolean', - 'marketing_emails' => 'boolean', - 'account_activity_alerts' => 'boolean', + 'receive_sms_notifications' => 'boolean', + 'receive_push_notifications' => 'boolean', + 'daily_summary_email' => 'boolean', + 'privacy_mode' => 'boolean', + 'marketing_emails' => 'boolean', + 'account_activity_alerts' => 'boolean', ]; /* Optional relationship */ diff --git a/app/Models/PrintRequest.php b/app/Models/PrintRequest.php index afddbdca..ef860363 100644 --- a/app/Models/PrintRequest.php +++ b/app/Models/PrintRequest.php @@ -2,6 +2,8 @@ namespace App\Models; +use App\Models\BaseModel; + class PrintRequest extends BaseModel { protected $table = 'print_requests'; @@ -22,11 +24,11 @@ class PrintRequest extends BaseModel ]; protected $casts = [ - 'teacher_id' => 'integer', - 'admin_id' => 'integer', - 'class_id' => 'integer', - 'num_copies' => 'integer', - 'required_by' => 'datetime', // if stored as DATETIME + 'teacher_id' => 'integer', + 'admin_id' => 'integer', + 'class_id' => 'integer', + 'num_copies' => 'integer', + 'required_by' => 'datetime', // if stored as DATETIME ]; /* Optional relationships */ @@ -44,4 +46,4 @@ class PrintRequest extends BaseModel { return $this->belongsTo(SchoolClass::class, 'class_id'); } -} +} \ No newline at end of file diff --git a/app/Models/Project.php b/app/Models/Project.php index c0e57a25..7041a6bb 100644 --- a/app/Models/Project.php +++ b/app/Models/Project.php @@ -3,6 +3,7 @@ namespace App\Models; use Illuminate\Database\Eloquent\Builder; +use App\Models\BaseModel; use Illuminate\Database\Eloquent\Relations\BelongsTo; class Project extends BaseModel @@ -44,17 +45,17 @@ class Project extends BaseModel * Helpful casts (adjust types if your DB columns differ). */ protected $casts = [ - 'student_id' => 'integer', - 'school_id' => 'integer', - 'class_section_id' => 'integer', - 'updated_by' => 'integer', - 'project_index' => 'integer', - 'score' => 'decimal:2', - 'max_points' => 'decimal:2', - 'locked_by' => 'integer', - 'locked_at' => 'datetime', // or 'float' if you prefer - 'created_at' => 'datetime', - 'updated_at' => 'datetime', + 'student_id' => 'integer', + 'school_id' => 'integer', + 'class_section_id' => 'integer', + 'updated_by' => 'integer', + 'project_index' => 'integer', + 'score' => 'decimal:2', + 'max_points' => 'decimal:2', + 'locked_by' => 'integer', + 'locked_at' => 'datetime', // or 'float' if you prefer + 'created_at' => 'datetime', + 'updated_at' => 'datetime', ]; /* ============================================================ @@ -90,7 +91,7 @@ class Project extends BaseModel public function scopeForTerm(Builder $q, string $semester, string $schoolYear): Builder { return $q->where('semester', $semester) - ->where('school_year', $schoolYear); + ->where('school_year', $schoolYear); } public function scopeForClassSection(Builder $q, ?int $classSectionId): Builder @@ -160,4 +161,4 @@ class Project extends BaseModel // Safe default if you're unsure. Set to false if score is numeric. return true; } -} +} \ No newline at end of file diff --git a/app/Models/PromotionAuditLog.php b/app/Models/PromotionAuditLog.php index f16d4bfa..7d6b1423 100644 --- a/app/Models/PromotionAuditLog.php +++ b/app/Models/PromotionAuditLog.php @@ -16,27 +16,16 @@ class PromotionAuditLog extends BaseModel public $timestamps = true; public const ACTION_RECORD_CREATED = 'record_created'; - public const ACTION_STATUS_CHANGED = 'status_changed'; - public const ACTION_ELIGIBILITY_EVALUATED = 'eligibility_evaluated'; - public const ACTION_PARENT_NOTIFIED = 'parent_notified'; - public const ACTION_ENROLLMENT_STARTED = 'enrollment_started'; - public const ACTION_ENROLLMENT_STEP_COMPLETED = 'enrollment_step_completed'; - public const ACTION_ENROLLMENT_COMPLETED = 'enrollment_completed'; - public const ACTION_PROMOTION_FINALIZED = 'promotion_finalized'; - public const ACTION_DEADLINE_SET = 'deadline_set'; - public const ACTION_REMINDER_SENT = 'reminder_sent'; - public const ACTION_DEADLINE_EXPIRED = 'deadline_expired'; - public const ACTION_MANUAL_OVERRIDE = 'manual_override'; protected $fillable = [ diff --git a/app/Models/PromotionQueue.php b/app/Models/PromotionQueue.php index c913d34e..578799ee 100644 --- a/app/Models/PromotionQueue.php +++ b/app/Models/PromotionQueue.php @@ -3,6 +3,7 @@ namespace App\Models; use Illuminate\Database\Eloquent\Builder; +use App\Models\BaseModel; use Illuminate\Database\Eloquent\Relations\BelongsTo; class PromotionQueue extends BaseModel @@ -21,17 +22,16 @@ class PromotionQueue extends BaseModel 'updated_at', 'updated_by', ]; - public $timestamps = true; protected $casts = [ - 'student_id' => 'integer', - 'from_class_section_id' => 'integer', - 'to_class_id' => 'integer', - 'to_class_section_id' => 'integer', - 'updated_by' => 'integer', - 'created_at' => 'datetime', - 'updated_at' => 'datetime', + 'student_id' => 'integer', + 'from_class_section_id' => 'integer', + 'to_class_id' => 'integer', + 'to_class_section_id' => 'integer', + 'updated_by' => 'integer', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', ]; /* ============================================================ @@ -92,9 +92,9 @@ class PromotionQueue extends BaseModel public static function upsertQueue(array $data): ?self { $studentId = isset($data['student_id']) ? (int) $data['student_id'] : null; - $yearTo = isset($data['school_year_to']) ? (string) $data['school_year_to'] : null; + $yearTo = isset($data['school_year_to']) ? (string) $data['school_year_to'] : null; - if (! $studentId || ! $yearTo) { + if (!$studentId || !$yearTo) { return null; } @@ -114,4 +114,4 @@ class PromotionQueue extends BaseModel { return (bool) static::upsertQueue($data); } -} +} \ No newline at end of file diff --git a/app/Models/PromotionReminderLog.php b/app/Models/PromotionReminderLog.php index cbfd0c58..88973cf1 100644 --- a/app/Models/PromotionReminderLog.php +++ b/app/Models/PromotionReminderLog.php @@ -16,13 +16,9 @@ class PromotionReminderLog extends BaseModel public $timestamps = true; public const TYPE_FIRST = 'first'; - public const TYPE_HALFWAY = 'halfway'; - public const TYPE_FINAL = 'final'; - public const TYPE_EXPIRATION = 'expiration'; - public const TYPE_MANUAL = 'manual'; public const ALLOWED_TYPES = [ diff --git a/app/Models/PurchaseOrder.php b/app/Models/PurchaseOrder.php index ef898059..fbca9813 100644 --- a/app/Models/PurchaseOrder.php +++ b/app/Models/PurchaseOrder.php @@ -3,9 +3,10 @@ namespace App\Models; use Illuminate\Database\Eloquent\Builder; +use App\Models\BaseModel; +use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; -use Illuminate\Database\Eloquent\SoftDeletes; class PurchaseOrder extends BaseModel { @@ -24,19 +25,18 @@ class PurchaseOrder extends BaseModel 'total', 'notes', ]; - public $timestamps = true; protected $casts = [ - 'supplier_id' => 'integer', - 'order_date' => 'date', // or 'datetime' if your DB stores time too - 'expected_date' => 'date', - 'subtotal' => 'decimal:2', - 'tax' => 'decimal:2', - 'total' => 'decimal:2', - 'created_at' => 'datetime', - 'updated_at' => 'datetime', - 'deleted_at' => 'datetime', + 'supplier_id' => 'integer', + 'order_date' => 'date', // or 'datetime' if your DB stores time too + 'expected_date' => 'date', + 'subtotal' => 'decimal:2', + 'tax' => 'decimal:2', + 'total' => 'decimal:2', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + 'deleted_at' => 'datetime', ]; public function getFillable(): array @@ -49,12 +49,9 @@ class PurchaseOrder extends BaseModel * ============================================================ */ - public const STATUS_DRAFT = 'draft'; - - public const STATUS_ORDERED = 'ordered'; - + public const STATUS_DRAFT = 'draft'; + public const STATUS_ORDERED = 'ordered'; public const STATUS_RECEIVED = 'received'; - public const STATUS_CANCELED = 'canceled'; public static function allowedStatuses(): array @@ -119,8 +116,8 @@ class PurchaseOrder extends BaseModel $total = $subtotal + $tax; $this->subtotal = $subtotal; - $this->tax = $tax; - $this->total = $total; + $this->tax = $tax; + $this->total = $total; return $this; } @@ -128,21 +125,18 @@ class PurchaseOrder extends BaseModel public function markOrdered(): self { $this->status = self::STATUS_ORDERED; - return $this; } public function markReceived(): self { $this->status = self::STATUS_RECEIVED; - return $this; } public function markCanceled(): self { $this->status = self::STATUS_CANCELED; - return $this; } @@ -155,16 +149,16 @@ class PurchaseOrder extends BaseModel { // Mirrors legacy: unique purchase_orders.po_number except current id. return [ - 'po_number' => ['required', 'string', 'max:60', 'unique:purchase_orders,po_number,'.($id ?? 'NULL').',id'], + 'po_number' => ['required', 'string', 'max:60', 'unique:purchase_orders,po_number,' . ($id ?? 'NULL') . ',id'], 'supplier_id' => ['required', 'integer', 'min:1', 'exists:suppliers,id'], - 'status' => ['nullable', 'string', 'in:'.implode(',', self::allowedStatuses())], + 'status' => ['nullable', 'string', 'in:' . implode(',', self::allowedStatuses())], - 'order_date' => ['nullable', 'date'], - 'expected_date' => ['nullable', 'date', 'after_or_equal:order_date'], - 'subtotal' => ['nullable', 'numeric', 'min:0'], - 'tax' => ['nullable', 'numeric', 'min:0'], - 'total' => ['nullable', 'numeric', 'min:0'], - 'notes' => ['nullable', 'string', 'max:5000'], + 'order_date' => ['nullable', 'date'], + 'expected_date' => ['nullable', 'date', 'after_or_equal:order_date'], + 'subtotal' => ['nullable', 'numeric', 'min:0'], + 'tax' => ['nullable', 'numeric', 'min:0'], + 'total' => ['nullable', 'numeric', 'min:0'], + 'notes' => ['nullable', 'string', 'max:5000'], ]; } } diff --git a/app/Models/PurchaseOrderItem.php b/app/Models/PurchaseOrderItem.php index 2ef53bd9..ba44d165 100644 --- a/app/Models/PurchaseOrderItem.php +++ b/app/Models/PurchaseOrderItem.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Models\BaseModel; use Illuminate\Database\Eloquent\Relations\BelongsTo; class PurchaseOrderItem extends BaseModel @@ -16,17 +17,16 @@ class PurchaseOrderItem extends BaseModel 'received_qty', 'unit_cost', ]; - public $timestamps = true; protected $casts = [ 'purchase_order_id' => 'integer', - 'supply_id' => 'integer', - 'quantity' => 'integer', - 'received_qty' => 'integer', - 'unit_cost' => 'decimal:2', // adjust scale to match DB (e.g., decimal:4) - 'created_at' => 'datetime', - 'updated_at' => 'datetime', + 'supply_id' => 'integer', + 'quantity' => 'integer', + 'received_qty' => 'integer', + 'unit_cost' => 'decimal:2', // adjust scale to match DB (e.g., decimal:4) + 'created_at' => 'datetime', + 'updated_at' => 'datetime', ]; public function getFillable(): array @@ -57,18 +57,16 @@ class PurchaseOrderItem extends BaseModel // total ordered cost public function getLineTotalAttribute(): float { - $qty = (int) ($this->quantity ?? 0); + $qty = (int) ($this->quantity ?? 0); $cost = (float) ($this->unit_cost ?? 0); - return $qty * $cost; } // total received cost public function getReceivedLineTotalAttribute(): float { - $qty = (int) ($this->received_qty ?? 0); + $qty = (int) ($this->received_qty ?? 0); $cost = (float) ($this->unit_cost ?? 0); - return $qty * $cost; } @@ -83,11 +81,11 @@ class PurchaseOrderItem extends BaseModel // keep strict by default to match legacy behavior return [ 'purchase_order_id' => [$updating ? 'sometimes' : 'required', 'integer', 'min:1', 'exists:purchase_orders,id'], - 'supply_id' => [$updating ? 'sometimes' : 'required', 'integer', 'min:1', 'exists:supplies,id'], - 'description' => ['nullable', 'string', 'max:1000'], - 'quantity' => [$updating ? 'sometimes' : 'required', 'integer', 'min:1'], - 'received_qty' => ['nullable', 'integer', 'min:0', 'lte:quantity'], - 'unit_cost' => [$updating ? 'sometimes' : 'required', 'numeric', 'min:0'], + 'supply_id' => [$updating ? 'sometimes' : 'required', 'integer', 'min:1', 'exists:supplies,id'], + 'description' => ['nullable', 'string', 'max:1000'], + 'quantity' => [$updating ? 'sometimes' : 'required', 'integer', 'min:1'], + 'received_qty' => ['nullable', 'integer', 'min:0', 'lte:quantity'], + 'unit_cost' => [$updating ? 'sometimes' : 'required', 'numeric', 'min:0'], ]; } } diff --git a/app/Models/Quiz.php b/app/Models/Quiz.php index c131eb4c..6663fd33 100644 --- a/app/Models/Quiz.php +++ b/app/Models/Quiz.php @@ -2,12 +2,12 @@ namespace App\Models; +use App\Models\BaseModel; use Illuminate\Database\Eloquent\Relations\BelongsTo; class Quiz extends BaseModel { protected $table = 'quiz'; - public $timestamps = false; protected $fillable = [ @@ -30,17 +30,17 @@ class Quiz extends BaseModel ]; protected $casts = [ - 'student_id' => 'integer', - 'school_id' => 'integer', + 'student_id' => 'integer', + 'school_id' => 'integer', 'class_section_id' => 'integer', - 'updated_by' => 'integer', - 'quiz_index' => 'integer', - 'score' => 'decimal:2', - 'max_points' => 'decimal:2', - 'locked_by' => 'integer', - 'locked_at' => 'datetime', - 'created_at' => 'datetime', - 'updated_at' => 'datetime', + 'updated_by' => 'integer', + 'quiz_index' => 'integer', + 'score' => 'decimal:2', + 'max_points' => 'decimal:2', + 'locked_by' => 'integer', + 'locked_at' => 'datetime', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', ]; public function student(): BelongsTo diff --git a/app/Models/Refund.php b/app/Models/Refund.php index 536b5010..0755eebe 100644 --- a/app/Models/Refund.php +++ b/app/Models/Refund.php @@ -3,6 +3,7 @@ namespace App\Models; use Illuminate\Database\Eloquent\Builder; +use App\Models\BaseModel; use Illuminate\Database\Eloquent\Relations\BelongsTo; class Refund extends BaseModel @@ -30,24 +31,23 @@ class Refund extends BaseModel 'check_nbr', 'check_file', ]; - public $timestamps = true; protected $casts = [ - 'parent_id' => 'integer', - 'invoice_id' => 'integer', - 'approved_by' => 'integer', - 'updated_by' => 'integer', + 'parent_id' => 'integer', + 'invoice_id' => 'integer', + 'approved_by' => 'integer', + 'updated_by' => 'integer', - 'refund_amount' => 'decimal:2', + 'refund_amount' => 'decimal:2', 'refund_paid_amount' => 'decimal:2', - 'requested_at' => 'datetime', - 'approved_at' => 'datetime', - 'refunded_at' => 'datetime', + 'requested_at' => 'datetime', + 'approved_at' => 'datetime', + 'refunded_at' => 'datetime', - 'created_at' => 'datetime', - 'updated_at' => 'datetime', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', ]; /* ============================================================ @@ -56,15 +56,10 @@ class Refund extends BaseModel */ public const STATUS_PENDING = 'Pending'; - public const STATUS_APPROVED = 'Approved'; - public const STATUS_PARTIAL = 'Partial'; - public const STATUS_PAID = 'Paid'; - public const STATUS_REJECTED = 'Rejected'; - public const STATUS_CANCELED = 'Canceled'; public static function paidOutStatuses(): array @@ -144,18 +139,18 @@ class Refund extends BaseModel public static function rules(?int $id = null): array { return [ - 'parent_id' => ['required', 'integer', 'min:1', 'exists:parents,id'], // adjust table name - 'school_year' => ['required', 'string', 'max:20'], - 'invoice_id' => ['nullable', 'integer', 'exists:invoices,id'], + 'parent_id' => ['required', 'integer', 'min:1', 'exists:parents,id'], // adjust table name + 'school_year' => ['required', 'string', 'max:20'], + 'invoice_id' => ['nullable', 'integer', 'exists:invoices,id'], - 'refund_amount' => ['required', 'numeric', 'min:0'], + 'refund_amount' => ['required', 'numeric', 'min:0'], 'refund_paid_amount' => ['nullable', 'numeric', 'min:0'], - 'requested_at' => ['nullable', 'date'], - 'approved_at' => ['nullable', 'date', 'after_or_equal:requested_at'], - 'refunded_at' => ['nullable', 'date', 'after_or_equal:approved_at'], + 'requested_at' => ['nullable', 'date'], + 'approved_at' => ['nullable', 'date', 'after_or_equal:requested_at'], + 'refunded_at' => ['nullable', 'date', 'after_or_equal:approved_at'], - 'status' => ['required', 'string', 'in:'.implode(',', [ + 'status' => ['required', 'string', 'in:' . implode(',', [ self::STATUS_PENDING, self::STATUS_APPROVED, self::STATUS_PARTIAL, @@ -164,17 +159,17 @@ class Refund extends BaseModel self::STATUS_CANCELED, ])], - 'reason' => ['nullable', 'string', 'max:2000'], - 'request' => ['nullable', 'string'], - 'note' => ['nullable', 'string', 'max:5000'], - 'semester' => ['nullable', 'string', 'max:20'], + 'reason' => ['nullable', 'string', 'max:2000'], + 'request' => ['nullable', 'string'], + 'note' => ['nullable', 'string', 'max:5000'], + 'semester' => ['nullable', 'string', 'max:20'], - 'approved_by' => ['nullable', 'integer'], - 'updated_by' => ['nullable', 'integer'], + 'approved_by' => ['nullable', 'integer'], + 'updated_by' => ['nullable', 'integer'], 'refund_method' => ['nullable', 'string', 'max:50'], // cash/check/cc/etc. - 'check_nbr' => ['nullable', 'string', 'max:80'], - 'check_file' => ['nullable', 'string', 'max:255'], + 'check_nbr' => ['nullable', 'string', 'max:80'], + 'check_file' => ['nullable', 'string', 'max:255'], ]; } -} +} \ No newline at end of file diff --git a/app/Models/Reimbursement.php b/app/Models/Reimbursement.php index 14a07d73..d69e6a5c 100644 --- a/app/Models/Reimbursement.php +++ b/app/Models/Reimbursement.php @@ -3,6 +3,7 @@ namespace App\Models; use Illuminate\Database\Eloquent\Builder; +use App\Models\BaseModel; use Illuminate\Database\Eloquent\Relations\BelongsTo; class Reimbursement extends BaseModel @@ -32,13 +33,13 @@ class Reimbursement extends BaseModel public $timestamps = true; protected $casts = [ - 'amount' => 'decimal:2', - 'expense_id' => 'integer', - 'approved_by' => 'integer', - 'added_by' => 'integer', - 'batch_number' => 'integer', - 'created_at' => 'datetime', - 'updated_at' => 'datetime', + 'amount' => 'decimal:2', + 'expense_id' => 'integer', + 'approved_by' => 'integer', + 'added_by' => 'integer', + 'batch_number' => 'integer', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', ]; /* ============================================================ @@ -46,15 +47,11 @@ class Reimbursement extends BaseModel * ============================================================ */ - public const STATUS_DRAFT = 'draft'; - - public const STATUS_PENDING = 'pending'; - - public const STATUS_APPROVED = 'approved'; - + public const STATUS_DRAFT = 'draft'; + public const STATUS_PENDING = 'pending'; + public const STATUS_APPROVED = 'approved'; public const STATUS_REIMBURSED = 'reimbursed'; - - public const STATUS_REJECTED = 'rejected'; + public const STATUS_REJECTED = 'rejected'; public static function allowedStatuses(): array { @@ -152,22 +149,22 @@ class Reimbursement extends BaseModel public static function rules(bool $updating = false): array { return [ - 'amount' => [$updating ? 'sometimes' : 'required', 'numeric', 'min:0'], - 'reimbursed_to' => ['nullable', 'string', 'max:255'], - 'expense_id' => ['nullable', 'integer', 'exists:expenses,id'], - 'description' => ['nullable', 'string', 'max:5000'], - 'status' => ['nullable', 'string', 'in:'.implode(',', self::allowedStatuses())], - 'receipt_path' => ['nullable', 'string', 'max:255'], + 'amount' => [$updating ? 'sometimes' : 'required', 'numeric', 'min:0'], + 'reimbursed_to' => ['nullable', 'string', 'max:255'], + 'expense_id' => ['nullable', 'integer', 'exists:expenses,id'], + 'description' => ['nullable', 'string', 'max:5000'], + 'status' => ['nullable', 'string', 'in:' . implode(',', self::allowedStatuses())], + 'receipt_path' => ['nullable', 'string', 'max:255'], - 'approved_by' => ['nullable', 'integer'], - 'added_by' => ['nullable', 'integer'], + 'approved_by' => ['nullable', 'integer'], + 'added_by' => ['nullable', 'integer'], - 'school_year' => ['nullable', 'string', 'max:20'], - 'semester' => ['nullable', 'string', 'max:20'], + 'school_year' => ['nullable', 'string', 'max:20'], + 'semester' => ['nullable', 'string', 'max:20'], - 'check_number' => ['nullable', 'string', 'max:80'], + 'check_number' => ['nullable', 'string', 'max:80'], 'reimbursement_method' => ['nullable', 'string', 'max:50'], - 'batch_number' => ['nullable', 'integer', 'min:1'], + 'batch_number' => ['nullable', 'integer', 'min:1'], ]; } -} +} \ No newline at end of file diff --git a/app/Models/ReimbursementBatch.php b/app/Models/ReimbursementBatch.php index 8f866a52..02796a29 100644 --- a/app/Models/ReimbursementBatch.php +++ b/app/Models/ReimbursementBatch.php @@ -3,6 +3,7 @@ namespace App\Models; use Illuminate\Database\Eloquent\Builder; +use App\Models\BaseModel; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; @@ -29,11 +30,11 @@ class ReimbursementBatch extends BaseModel ]; protected $casts = [ - 'created_by' => 'integer', - 'closed_by' => 'integer', + 'created_by' => 'integer', + 'closed_by' => 'integer', 'yearly_batch_number' => 'integer', - 'opened_at' => 'datetime', - 'closed_at' => 'datetime', + 'opened_at' => 'datetime', + 'closed_at' => 'datetime', ]; /* ============================================================ @@ -41,11 +42,9 @@ class ReimbursementBatch extends BaseModel * ============================================================ */ - public const STATUS_OPEN = 'open'; - + public const STATUS_OPEN = 'open'; public const STATUS_CLOSED = 'closed'; - - public const STATUS_DRAFT = 'draft'; + public const STATUS_DRAFT = 'draft'; public static function allowedStatuses(): array { @@ -156,16 +155,16 @@ class ReimbursementBatch extends BaseModel public static function rules(?int $id = null): array { return [ - 'title' => ['required', 'string', 'max:255'], - 'status' => ['required', 'string', 'in:'.implode(',', self::allowedStatuses())], - 'created_by' => ['nullable', 'integer'], - 'closed_by' => ['nullable', 'integer'], - 'opened_at' => ['nullable', 'date'], - 'closed_at' => ['nullable', 'date', 'after_or_equal:opened_at'], - 'notes' => ['nullable', 'string', 'max:5000'], - 'school_year' => ['nullable', 'string', 'max:20'], - 'semester' => ['nullable', 'string', 'max:20'], + 'title' => ['required', 'string', 'max:255'], + 'status' => ['required', 'string', 'in:' . implode(',', self::allowedStatuses())], + 'created_by' => ['nullable', 'integer'], + 'closed_by' => ['nullable', 'integer'], + 'opened_at' => ['nullable', 'date'], + 'closed_at' => ['nullable', 'date', 'after_or_equal:opened_at'], + 'notes' => ['nullable', 'string', 'max:5000'], + 'school_year' => ['nullable', 'string', 'max:20'], + 'semester' => ['nullable', 'string', 'max:20'], 'yearly_batch_number' => ['nullable', 'integer', 'min:1'], ]; } -} +} \ No newline at end of file diff --git a/app/Models/ReimbursementBatchAdminFile.php b/app/Models/ReimbursementBatchAdminFile.php index 757fe6d2..f385d95d 100644 --- a/app/Models/ReimbursementBatchAdminFile.php +++ b/app/Models/ReimbursementBatchAdminFile.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Models\BaseModel; use Illuminate\Database\Eloquent\Relations\BelongsTo; class ReimbursementBatchAdminFile extends BaseModel @@ -23,10 +24,10 @@ class ReimbursementBatchAdminFile extends BaseModel ]; protected $casts = [ - 'batch_id' => 'integer', - 'admin_id' => 'integer', - 'uploaded_by' => 'integer', - 'uploaded_at' => 'datetime', + 'batch_id' => 'integer', + 'admin_id' => 'integer', + 'uploaded_by' => 'integer', + 'uploaded_at' => 'datetime', ]; /* ============================================================ @@ -74,12 +75,12 @@ class ReimbursementBatchAdminFile extends BaseModel public static function rules(bool $updating = false): array { return [ - 'batch_id' => [$updating ? 'sometimes' : 'required', 'integer', 'min:1', 'exists:reimbursement_batches,id'], - 'admin_id' => [$updating ? 'sometimes' : 'required', 'integer', 'min:1', 'exists:admins,id'], // adjust table - 'filename' => [$updating ? 'sometimes' : 'required', 'string', 'max:255'], - 'original_filename' => ['nullable', 'string', 'max:255'], - 'uploaded_at' => ['nullable', 'date'], - 'uploaded_by' => ['nullable', 'integer'], + 'batch_id' => [$updating ? 'sometimes' : 'required', 'integer', 'min:1', 'exists:reimbursement_batches,id'], + 'admin_id' => [$updating ? 'sometimes' : 'required', 'integer', 'min:1', 'exists:admins,id'], // adjust table + 'filename' => [$updating ? 'sometimes' : 'required', 'string', 'max:255'], + 'original_filename' => ['nullable', 'string', 'max:255'], + 'uploaded_at' => ['nullable', 'date'], + 'uploaded_by' => ['nullable', 'integer'], ]; } -} +} \ No newline at end of file diff --git a/app/Models/ReimbursementBatchItem.php b/app/Models/ReimbursementBatchItem.php index d10a80b3..daff3c56 100644 --- a/app/Models/ReimbursementBatchItem.php +++ b/app/Models/ReimbursementBatchItem.php @@ -3,6 +3,7 @@ namespace App\Models; use Illuminate\Database\Eloquent\Builder; +use App\Models\BaseModel; use Illuminate\Database\Eloquent\Relations\BelongsTo; class ReimbursementBatchItem extends BaseModel @@ -27,12 +28,12 @@ class ReimbursementBatchItem extends BaseModel ]; protected $casts = [ - 'batch_id' => 'integer', - 'expense_id' => 'integer', + 'batch_id' => 'integer', + 'expense_id' => 'integer', 'reimbursement_id' => 'integer', - 'admin_id' => 'integer', - 'assigned_at' => 'datetime', - 'unassigned_at' => 'datetime', + 'admin_id' => 'integer', + 'assigned_at' => 'datetime', + 'unassigned_at' => 'datetime', ]; /* ============================================================ @@ -84,14 +85,14 @@ class ReimbursementBatchItem extends BaseModel public function scopeAssigned(Builder $q): Builder { return $q->whereNotNull('admin_id') - ->whereNotNull('assigned_at') - ->whereNull('unassigned_at'); + ->whereNotNull('assigned_at') + ->whereNull('unassigned_at'); } public function scopeUnassigned(Builder $q): Builder { return $q->whereNull('admin_id') - ->whereNull('assigned_at'); + ->whereNull('assigned_at'); } /* ============================================================ @@ -136,15 +137,15 @@ class ReimbursementBatchItem extends BaseModel public static function rules(bool $updating = false): array { return [ - 'batch_id' => [$updating ? 'sometimes' : 'required', 'integer', 'min:1', 'exists:reimbursement_batches,id'], - 'expense_id' => ['nullable', 'integer', 'exists:expenses,id'], + 'batch_id' => [$updating ? 'sometimes' : 'required', 'integer', 'min:1', 'exists:reimbursement_batches,id'], + 'expense_id' => ['nullable', 'integer', 'exists:expenses,id'], 'reimbursement_id' => ['nullable', 'integer', 'exists:reimbursements,id'], - 'admin_id' => ['nullable', 'integer'], // add exists rule if you know the table - 'assigned_at' => ['nullable', 'date'], - 'unassigned_at' => ['nullable', 'date', 'after_or_equal:assigned_at'], - 'notes' => ['nullable', 'string', 'max:5000'], - 'school_year' => ['nullable', 'string', 'max:20'], - 'semester' => ['nullable', 'string', 'max:20'], + 'admin_id' => ['nullable', 'integer'], // add exists rule if you know the table + 'assigned_at' => ['nullable', 'date'], + 'unassigned_at' => ['nullable', 'date', 'after_or_equal:assigned_at'], + 'notes' => ['nullable', 'string', 'max:5000'], + 'school_year' => ['nullable', 'string', 'max:20'], + 'semester' => ['nullable', 'string', 'max:20'], ]; } -} +} \ No newline at end of file diff --git a/app/Models/ReportCardAcknowledgement.php b/app/Models/ReportCardAcknowledgement.php index 0755198c..d7f3ad90 100644 --- a/app/Models/ReportCardAcknowledgement.php +++ b/app/Models/ReportCardAcknowledgement.php @@ -3,6 +3,7 @@ namespace App\Models; use Illuminate\Database\Eloquent\Builder; +use App\Models\BaseModel; use Illuminate\Database\Eloquent\Relations\BelongsTo; class ReportCardAcknowledgement extends BaseModel @@ -21,16 +22,15 @@ class ReportCardAcknowledgement extends BaseModel 'created_at', 'updated_at', ]; - public $timestamps = true; protected $casts = [ - 'parent_id' => 'integer', - 'student_id' => 'integer', - 'viewed_at' => 'datetime', - 'signed_at' => 'datetime', - 'created_at' => 'datetime', - 'updated_at' => 'datetime', + 'parent_id' => 'integer', + 'student_id' => 'integer', + 'viewed_at' => 'datetime', + 'signed_at' => 'datetime', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', ]; /* ============================================================ @@ -67,7 +67,7 @@ class ReportCardAcknowledgement extends BaseModel public function scopeForTerm(Builder $q, string $schoolYear, string $semester): Builder { return $q->where('school_year', $schoolYear) - ->where('semester', $semester); + ->where('semester', $semester); } public function scopeSigned(Builder $q): Builder @@ -137,15 +137,15 @@ class ReportCardAcknowledgement extends BaseModel public static function rules(bool $updating = false): array { return [ - 'parent_id' => [$updating ? 'sometimes' : 'required', 'integer', 'min:1'], - 'student_id' => [$updating ? 'sometimes' : 'required', 'integer', 'min:1'], - 'school_year' => [$updating ? 'sometimes' : 'required', 'string', 'max:20'], - 'semester' => [$updating ? 'sometimes' : 'required', 'string', 'max:20'], + 'parent_id' => [$updating ? 'sometimes' : 'required', 'integer', 'min:1'], + 'student_id' => [$updating ? 'sometimes' : 'required', 'integer', 'min:1'], + 'school_year' => [$updating ? 'sometimes' : 'required', 'string', 'max:20'], + 'semester' => [$updating ? 'sometimes' : 'required', 'string', 'max:20'], - 'viewed_at' => ['nullable', 'date'], - 'signed_at' => ['nullable', 'date', 'after_or_equal:viewed_at'], - 'signed_name' => ['nullable', 'string', 'max:255'], - 'signer_ip' => ['nullable', 'string', 'max:45'], // IPv4/IPv6 + 'viewed_at' => ['nullable', 'date'], + 'signed_at' => ['nullable', 'date', 'after_or_equal:viewed_at'], + 'signed_name' => ['nullable', 'string', 'max:255'], + 'signer_ip' => ['nullable', 'string', 'max:45'], // IPv4/IPv6 ]; } -} +} \ No newline at end of file diff --git a/app/Models/Role.php b/app/Models/Role.php index e129b82e..979b0cc3 100644 --- a/app/Models/Role.php +++ b/app/Models/Role.php @@ -3,6 +3,7 @@ namespace App\Models; use Illuminate\Database\Eloquent\Builder; +use App\Models\BaseModel; class Role extends BaseModel { @@ -18,12 +19,11 @@ class Role extends BaseModel 'created_at', 'updated_at', ]; - public $timestamps = true; protected $casts = [ - 'priority' => 'integer', - 'is_active' => 'boolean', + 'priority' => 'integer', + 'is_active' => 'boolean', 'created_at' => 'datetime', 'updated_at' => 'datetime', ]; @@ -52,47 +52,43 @@ class Role extends BaseModel * Find roles by names or slugs (case-insensitive), active only, ordered by priority ASC. */ - // Put these INSIDE the Role model, replacing the current implementations +// Put these INSIDE the Role model, replacing the current implementations - public static function findByNamesOrSlugs(array $keys): array - { - $keys = array_values(array_filter(array_map('strval', $keys))); - if (empty($keys)) { - return []; - } +public static function findByNamesOrSlugs(array $keys): array +{ + $keys = array_values(array_filter(array_map('strval', $keys))); + if (empty($keys)) return []; - $lower = array_map('mb_strtolower', $keys); - $placeholders = implode(',', array_fill(0, count($lower), '?')); + $lower = array_map('mb_strtolower', $keys); + $placeholders = implode(',', array_fill(0, count($lower), '?')); - return static::query() - ->active() - ->where(function (Builder $q) use ($lower, $placeholders) { - $q->whereRaw("LOWER(name) IN ($placeholders)", $lower) - ->orWhereRaw("LOWER(slug) IN ($placeholders)", $lower); - }) - ->orderBy('priority', 'asc') - ->get() - ->all(); - } + return static::query() + ->active() + ->where(function (Builder $q) use ($lower, $placeholders) { + $q->whereRaw("LOWER(name) IN ($placeholders)", $lower) + ->orWhereRaw("LOWER(slug) IN ($placeholders)", $lower); + }) + ->orderBy('priority', 'asc') + ->get() + ->all(); +} - public static function getIdsByNames(array $names): array - { - $names = array_values(array_filter(array_map('strval', $names))); - if (empty($names)) { - return []; - } +public static function getIdsByNames(array $names): array +{ + $names = array_values(array_filter(array_map('strval', $names))); + if (empty($names)) return []; - $lower = array_map('mb_strtolower', $names); - $placeholders = implode(',', array_fill(0, count($lower), '?')); + $lower = array_map('mb_strtolower', $names); + $placeholders = implode(',', array_fill(0, count($lower), '?')); - $ids = static::query() - ->select('id') - ->whereRaw("LOWER(name) IN ($placeholders)", $lower) - ->pluck('id') - ->all(); + $ids = static::query() + ->select('id') + ->whereRaw("LOWER(name) IN ($placeholders)", $lower) + ->pluck('id') + ->all(); - return array_map('intval', $ids); - } + return array_map('intval', $ids); +} /** * Get dashboard_route by name or slug (case-insensitive), active only. @@ -100,9 +96,7 @@ class Role extends BaseModel public static function getRouteByNameOrSlug(string $key): ?string { $key = trim((string) $key); - if ($key === '') { - return null; - } + if ($key === '') return null; $lower = mb_strtolower($key); @@ -110,7 +104,7 @@ class Role extends BaseModel ->active() ->where(function (Builder $q) use ($lower) { $q->whereRaw('LOWER(name) = ?', [$lower]) - ->orWhereRaw('LOWER(slug) = ?', [$lower]); + ->orWhereRaw('LOWER(slug) = ?', [$lower]); }) ->first(); @@ -147,12 +141,12 @@ class Role extends BaseModel public static function rules(?int $id = null): array { return [ - 'name' => ['required', 'string', 'max:255'], - 'slug' => ['required', 'string', 'max:255', 'unique:roles,slug,'.($id ?? 'NULL').',id'], - 'description' => ['nullable', 'string', 'max:2000'], + 'name' => ['required', 'string', 'max:255'], + 'slug' => ['required', 'string', 'max:255', 'unique:roles,slug,' . ($id ?? 'NULL') . ',id'], + 'description' => ['nullable', 'string', 'max:2000'], 'dashboard_route' => ['nullable', 'string', 'max:255'], - 'priority' => ['nullable', 'integer', 'min:0'], - 'is_active' => ['nullable', 'boolean'], + 'priority' => ['nullable', 'integer', 'min:0'], + 'is_active' => ['nullable', 'boolean'], ]; } -} +} \ No newline at end of file diff --git a/app/Models/RoleNavItem.php b/app/Models/RoleNavItem.php index 8b1968b4..41f16cff 100644 --- a/app/Models/RoleNavItem.php +++ b/app/Models/RoleNavItem.php @@ -3,12 +3,12 @@ namespace App\Models; use Illuminate\Database\Eloquent\Builder; +use App\Models\BaseModel; use Illuminate\Database\Eloquent\Relations\BelongsTo; class RoleNavItem extends BaseModel { protected $table = 'role_nav_items'; - public $timestamps = true; protected $fillable = [ @@ -17,10 +17,10 @@ class RoleNavItem extends BaseModel ]; protected $casts = [ - 'role_id' => 'integer', + 'role_id' => 'integer', 'nav_item_id' => 'integer', - 'created_at' => 'datetime', - 'updated_at' => 'datetime', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', ]; /* ============================================================ @@ -46,7 +46,6 @@ class RoleNavItem extends BaseModel public function scopeForRoles(Builder $q, array $roleIds): Builder { $roleIds = array_values(array_unique(array_filter(array_map('intval', $roleIds)))); - return empty($roleIds) ? $q->whereRaw('1=0') : $q->whereIn('role_id', $roleIds); } @@ -61,15 +60,11 @@ class RoleNavItem extends BaseModel public static function getNavItemIdsForRoles(array $roleNames): array { $roleNames = array_values(array_filter(array_map('strval', $roleNames))); - if (empty($roleNames)) { - return []; - } + if (empty($roleNames)) return []; // Uses the Role model you converted earlier $roleIds = Role::getIdsByNames($roleNames); - if (empty($roleIds)) { - return []; - } + if (empty($roleIds)) return []; // pluck nav_item_id, unique, sorted (optional) return static::query() @@ -90,8 +85,8 @@ class RoleNavItem extends BaseModel public static function rules(bool $updating = false): array { return [ - 'role_id' => [$updating ? 'sometimes' : 'required', 'integer', 'min:1', 'exists:roles,id'], + 'role_id' => [$updating ? 'sometimes' : 'required', 'integer', 'min:1', 'exists:roles,id'], 'nav_item_id' => [$updating ? 'sometimes' : 'required', 'integer', 'min:1', 'exists:nav_items,id'], ]; } -} +} \ No newline at end of file diff --git a/app/Models/RolePermission.php b/app/Models/RolePermission.php index 2cf521f0..b39760ab 100644 --- a/app/Models/RolePermission.php +++ b/app/Models/RolePermission.php @@ -3,6 +3,7 @@ namespace App\Models; use Illuminate\Database\Eloquent\Builder; +use App\Models\BaseModel; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Support\Facades\Log; @@ -21,19 +22,18 @@ class RolePermission extends BaseModel 'created_at', 'updated_at', ]; - public $timestamps = true; protected $casts = [ - 'role_id' => 'integer', - 'permission_id' => 'integer', - 'can_create' => 'boolean', - 'can_read' => 'boolean', - 'can_update' => 'boolean', - 'can_delete' => 'boolean', - 'can_manage' => 'boolean', - 'created_at' => 'datetime', - 'updated_at' => 'datetime', + 'role_id' => 'integer', + 'permission_id' => 'integer', + 'can_create' => 'boolean', + 'can_read' => 'boolean', + 'can_update' => 'boolean', + 'can_delete' => 'boolean', + 'can_manage' => 'boolean', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', ]; /* ============================================================ @@ -117,7 +117,7 @@ class RolePermission extends BaseModel return [ 'can_create' => $manage ? true : (bool) $this->can_create, - 'can_read' => $manage ? true : (bool) $this->can_read, + 'can_read' => $manage ? true : (bool) $this->can_read, 'can_update' => $manage ? true : (bool) $this->can_update, 'can_delete' => $manage ? true : (bool) $this->can_delete, 'can_manage' => $manage, @@ -130,11 +130,10 @@ class RolePermission extends BaseModel public function setCrud(bool $create, bool $read, bool $update, bool $delete, bool $manage = false): self { $this->can_create = $create; - $this->can_read = $read; + $this->can_read = $read; $this->can_update = $update; $this->can_delete = $delete; $this->can_manage = $manage; - return $this; } @@ -146,14 +145,14 @@ class RolePermission extends BaseModel public static function rules(bool $updating = false): array { return [ - 'role_id' => [$updating ? 'sometimes' : 'required', 'integer', 'min:1', 'exists:roles,id'], + 'role_id' => [$updating ? 'sometimes' : 'required', 'integer', 'min:1', 'exists:roles,id'], 'permission_id' => [$updating ? 'sometimes' : 'required', 'integer', 'min:1', 'exists:permissions,id'], - 'can_create' => ['nullable', 'boolean'], - 'can_read' => ['nullable', 'boolean'], - 'can_update' => ['nullable', 'boolean'], - 'can_delete' => ['nullable', 'boolean'], - 'can_manage' => ['nullable', 'boolean'], + 'can_create' => ['nullable', 'boolean'], + 'can_read' => ['nullable', 'boolean'], + 'can_update' => ['nullable', 'boolean'], + 'can_delete' => ['nullable', 'boolean'], + 'can_manage' => ['nullable', 'boolean'], ]; } -} +} \ No newline at end of file diff --git a/app/Models/SchoolClass.php b/app/Models/SchoolClass.php index f42a4ca1..e5ee65e4 100644 --- a/app/Models/SchoolClass.php +++ b/app/Models/SchoolClass.php @@ -2,6 +2,8 @@ namespace App\Models; +use App\Models\BaseModel; + class SchoolClass extends BaseModel { protected $table = 'classes'; @@ -38,7 +40,6 @@ class SchoolClass extends BaseModel public static function createClass(array $data): int { $row = static::create($data); - return (int) $row->id; } @@ -50,10 +51,7 @@ class SchoolClass extends BaseModel public static function deleteClass(int $id): bool { $row = static::query()->find($id); - if (! $row) { - return false; - } - + if (!$row) return false; return (bool) $row->delete(); } -} +} \ No newline at end of file diff --git a/app/Models/SchoolYear.php b/app/Models/SchoolYear.php index 1170546f..385e4de4 100644 --- a/app/Models/SchoolYear.php +++ b/app/Models/SchoolYear.php @@ -5,11 +5,8 @@ namespace App\Models; class SchoolYear extends BaseModel { public const STATUS_DRAFT = 'draft'; - public const STATUS_ACTIVE = 'active'; - public const STATUS_CLOSED = 'closed'; - public const STATUS_ARCHIVED = 'archived'; protected $table = 'school_years'; diff --git a/app/Models/ScoreComment.php b/app/Models/ScoreComment.php index 9045017a..a4a19d12 100644 --- a/app/Models/ScoreComment.php +++ b/app/Models/ScoreComment.php @@ -3,6 +3,7 @@ namespace App\Models; use Illuminate\Database\Eloquent\Builder; +use App\Models\BaseModel; use Illuminate\Database\Eloquent\Relations\BelongsTo; class ScoreComment extends BaseModel @@ -33,14 +34,14 @@ class ScoreComment extends BaseModel ]; protected $casts = [ - 'student_id' => 'integer', + 'student_id' => 'integer', 'class_section_id' => 'integer', - 'reviewed_by' => 'integer', - 'commented_by' => 'integer', - 'created_at' => 'datetime', + 'reviewed_by' => 'integer', + 'commented_by' => 'integer', + 'created_at' => 'datetime', // If comment_review is a tinyint(1) flag in DB, boolean cast helps - 'comment_review' => 'boolean', + 'comment_review' => 'boolean', ]; /* ============================================================ @@ -88,7 +89,7 @@ class ScoreComment extends BaseModel public function scopeForTerm(Builder $q, string $schoolYear, string $semester): Builder { return $q->where('school_year', $schoolYear) - ->where('semester', $semester); + ->where('semester', $semester); } public function scopeForScoreType(Builder $q, string $scoreType): Builder @@ -101,7 +102,7 @@ class ScoreComment extends BaseModel // supports either boolean flag or reviewed_by presence return $q->where(function (Builder $w) { $w->where('comment_review', 1) - ->orWhereNotNull('reviewed_by'); + ->orWhereNotNull('reviewed_by'); }); } @@ -156,19 +157,19 @@ class ScoreComment extends BaseModel public static function rules(bool $updating = false): array { return [ - 'student_id' => [$updating ? 'sometimes' : 'required', 'integer', 'min:1'], + 'student_id' => [$updating ? 'sometimes' : 'required', 'integer', 'min:1'], 'class_section_id' => [$updating ? 'sometimes' : 'required', 'integer', 'min:1'], - 'score_type' => [$updating ? 'sometimes' : 'required', 'string', 'max:50'], - 'school_year' => [$updating ? 'sometimes' : 'required', 'string', 'max:20'], - 'semester' => [$updating ? 'sometimes' : 'required', 'string', 'max:20'], + 'score_type' => [$updating ? 'sometimes' : 'required', 'string', 'max:50'], + 'school_year' => [$updating ? 'sometimes' : 'required', 'string', 'max:20'], + 'semester' => [$updating ? 'sometimes' : 'required', 'string', 'max:20'], - 'comment' => ['nullable', 'string', 'max:5000'], - 'comment_review' => ['nullable', 'boolean'], - 'reviewed_by' => ['nullable', 'integer'], - 'commented_by' => ['nullable', 'integer'], + 'comment' => ['nullable', 'string', 'max:5000'], + 'comment_review' => ['nullable', 'boolean'], + 'reviewed_by' => ['nullable', 'integer'], + 'commented_by' => ['nullable', 'integer'], // created_at is DB default; allow client to omit it - 'created_at' => ['nullable', 'date'], + 'created_at' => ['nullable', 'date'], ]; } -} +} \ No newline at end of file diff --git a/app/Models/Section.php b/app/Models/Section.php index 66992a5d..ce3a37cc 100644 --- a/app/Models/Section.php +++ b/app/Models/Section.php @@ -3,6 +3,7 @@ namespace App\Models; use Illuminate\Database\Eloquent\Builder; +use App\Models\BaseModel; use Illuminate\Database\Eloquent\Relations\BelongsTo; class Section extends BaseModel @@ -16,7 +17,6 @@ class Section extends BaseModel 'updated_at', 'updated_by', ]; - public $timestamps = true; protected $casts = [ @@ -49,13 +49,11 @@ class Section extends BaseModel public function scopeSearch(Builder $q, string $term): Builder { $term = trim($term); - if ($term === '') { - return $q; - } + if ($term === '') return $q; return $q->where(function (Builder $w) use ($term) { $w->where('section_name', 'like', "%{$term}%") - ->orWhere('description', 'like', "%{$term}%"); + ->orWhere('description', 'like', "%{$term}%"); }); } @@ -80,9 +78,7 @@ class Section extends BaseModel public static function updateSection(int $sectionId, array $data): bool { $section = static::query()->find($sectionId); - if (! $section) { - return false; - } + if (!$section) return false; // Laravel will set updated_at automatically when saving return $section->fill($data)->save(); @@ -95,9 +91,7 @@ class Section extends BaseModel public static function deleteSection(int $sectionId): bool { $section = static::query()->find($sectionId); - if (! $section) { - return false; - } + if (!$section) return false; return (bool) $section->delete(); } @@ -110,9 +104,9 @@ class Section extends BaseModel public static function rules(?int $id = null): array { return [ - 'section_name' => ['required', 'string', 'max:255', 'unique:sections,section_name,'.($id ?? 'NULL').',id'], - 'description' => ['nullable', 'string', 'max:2000'], - 'updated_by' => ['nullable', 'integer'], + 'section_name' => ['required', 'string', 'max:255', 'unique:sections,section_name,' . ($id ?? 'NULL') . ',id'], + 'description' => ['nullable', 'string', 'max:2000'], + 'updated_by' => ['nullable', 'integer'], ]; } -} +} \ No newline at end of file diff --git a/app/Models/SectionPlacementBatch.php b/app/Models/SectionPlacementBatch.php index f9c585e1..c4a22c14 100644 --- a/app/Models/SectionPlacementBatch.php +++ b/app/Models/SectionPlacementBatch.php @@ -9,11 +9,8 @@ class SectionPlacementBatch extends BaseModel protected $table = 'section_placement_batches'; public const STATUS_DRAFT = 'draft'; - public const STATUS_FINALIZED = 'finalized'; - public const STATUS_CANCELLED = 'cancelled'; - public const STATUS_SUPERSEDED = 'superseded'; protected $fillable = [ diff --git a/app/Models/SemesterScore.php b/app/Models/SemesterScore.php index f2f46aba..d232d307 100644 --- a/app/Models/SemesterScore.php +++ b/app/Models/SemesterScore.php @@ -3,6 +3,7 @@ namespace App\Models; use Illuminate\Database\Eloquent\Builder; +use App\Models\BaseModel; use Illuminate\Database\Eloquent\Relations\BelongsTo; class SemesterScore extends BaseModel @@ -38,26 +39,26 @@ class SemesterScore extends BaseModel public $timestamps = true; protected $casts = [ - 'student_id' => 'integer', - 'school_id' => 'integer', - 'class_section_id' => 'integer', - 'updated_by' => 'integer', + 'student_id' => 'integer', + 'school_id' => 'integer', + 'class_section_id' => 'integer', + 'updated_by' => 'integer', // averages/scores: keep decimal to avoid float drift - 'homework_avg' => 'decimal:2', - 'quiz_avg' => 'decimal:2', - 'project_avg' => 'decimal:2', - 'midterm_exam_score' => 'decimal:2', - 'final_exam_score' => 'decimal:2', - 'attendance_score' => 'decimal:2', + 'homework_avg' => 'decimal:2', + 'quiz_avg' => 'decimal:2', + 'project_avg' => 'decimal:2', + 'midterm_exam_score' => 'decimal:2', + 'final_exam_score' => 'decimal:2', + 'attendance_score' => 'decimal:2', 'participation_score' => 'decimal:2', - 'ptap_score' => 'decimal:2', - 'test_avg' => 'decimal:2', - 'semester_score' => 'decimal:2', - 'snapshot_id' => 'integer', + 'ptap_score' => 'decimal:2', + 'test_avg' => 'decimal:2', + 'semester_score' => 'decimal:2', + 'snapshot_id' => 'integer', - 'created_at' => 'datetime', - 'updated_at' => 'datetime', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', ]; /* ============================================================ @@ -99,7 +100,7 @@ class SemesterScore extends BaseModel public function scopeForTerm(Builder $q, string $semester, string $schoolYear): Builder { return $q->where('semester', $semester) - ->where('school_year', $schoolYear); + ->where('school_year', $schoolYear); } public function scopeForClassSection(Builder $q, ?int $classSectionId): Builder @@ -125,8 +126,8 @@ class SemesterScore extends BaseModel } $keys = [ - 'student_id' => (int) $data['student_id'], - 'semester' => (string) $data['semester'], + 'student_id' => (int) $data['student_id'], + 'semester' => (string) $data['semester'], 'school_year' => (string) $data['school_year'], ]; @@ -154,27 +155,27 @@ class SemesterScore extends BaseModel $required = $updating ? 'sometimes' : 'required'; return [ - 'student_id' => [$required, 'integer', 'min:1'], - 'semester' => [$required, 'string', 'max:20'], - 'school_year' => [$required, 'string', 'max:20'], + 'student_id' => [$required, 'integer', 'min:1'], + 'semester' => [$required, 'string', 'max:20'], + 'school_year' => [$required, 'string', 'max:20'], - 'school_id' => ['nullable', 'integer', 'min:1'], + 'school_id' => ['nullable', 'integer', 'min:1'], 'class_section_id' => ['nullable', 'integer', 'min:1'], - 'updated_by' => ['nullable', 'integer', 'min:1'], + 'updated_by' => ['nullable', 'integer', 'min:1'], - 'homework_avg' => ['nullable', 'numeric', 'min:0'], - 'quiz_avg' => ['nullable', 'numeric', 'min:0'], - 'project_avg' => ['nullable', 'numeric', 'min:0'], - 'midterm_exam_score' => ['nullable', 'numeric', 'min:0'], - 'final_exam_score' => ['nullable', 'numeric', 'min:0'], - 'attendance_score' => ['nullable', 'numeric', 'min:0'], + 'homework_avg' => ['nullable', 'numeric', 'min:0'], + 'quiz_avg' => ['nullable', 'numeric', 'min:0'], + 'project_avg' => ['nullable', 'numeric', 'min:0'], + 'midterm_exam_score' => ['nullable', 'numeric', 'min:0'], + 'final_exam_score' => ['nullable', 'numeric', 'min:0'], + 'attendance_score' => ['nullable', 'numeric', 'min:0'], 'participation_score' => ['nullable', 'numeric', 'min:0'], - 'ptap_score' => ['nullable', 'numeric', 'min:0'], - 'test_avg' => ['nullable', 'numeric', 'min:0'], - 'semester_score' => ['nullable', 'numeric', 'min:0'], - 'calculation_mode' => ['nullable', 'string', 'in:legacy,strong'], + 'ptap_score' => ['nullable', 'numeric', 'min:0'], + 'test_avg' => ['nullable', 'numeric', 'min:0'], + 'semester_score' => ['nullable', 'numeric', 'min:0'], + 'calculation_mode' => ['nullable', 'string', 'in:legacy,strong'], 'calculation_policy_version' => ['nullable', 'string', 'max:50'], - 'snapshot_id' => ['nullable', 'integer', 'min:1'], + 'snapshot_id' => ['nullable', 'integer', 'min:1'], ]; } -} +} \ No newline at end of file diff --git a/app/Models/SemesterScoreSnapshot.php b/app/Models/SemesterScoreSnapshot.php index 317c95a6..46cc07f0 100644 --- a/app/Models/SemesterScoreSnapshot.php +++ b/app/Models/SemesterScoreSnapshot.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Models\BaseModel; use Illuminate\Database\Eloquent\Relations\BelongsTo; class SemesterScoreSnapshot extends BaseModel diff --git a/app/Models/Setting.php b/app/Models/Setting.php index 0815035d..49c2ac72 100644 --- a/app/Models/Setting.php +++ b/app/Models/Setting.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Models\BaseModel; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Support\Facades\Cache; @@ -16,7 +17,6 @@ class Setting extends BaseModel 'created_at', 'updated_at', ]; - public $timestamps = true; protected $casts = [ @@ -49,7 +49,7 @@ class Setting extends BaseModel { return static::query()->first() ?? static::query()->create([ // set safe defaults if you want - 'name' => 'App', + 'name' => 'App', 'timezone' => config('app.timezone', 'UTC'), ]); } @@ -70,7 +70,6 @@ class Setting extends BaseModel public static function updateSettings(array $data): bool { $settings = static::singleton(); - return $settings->fill($data)->save(); } @@ -105,9 +104,9 @@ class Setting extends BaseModel public static function rules(bool $updating = false): array { return [ - 'name' => ['nullable', 'string', 'max:255'], - 'timezone' => ['nullable', 'string', 'max:80'], // validate against a list if you want - 'updated_by' => ['nullable', 'integer'], + 'name' => ['nullable', 'string', 'max:255'], + 'timezone' => ['nullable', 'string', 'max:80'], // validate against a list if you want + 'updated_by'=> ['nullable', 'integer'], ]; } -} +} \ No newline at end of file diff --git a/app/Models/Staff.php b/app/Models/Staff.php index a346562d..49ed10db 100644 --- a/app/Models/Staff.php +++ b/app/Models/Staff.php @@ -3,6 +3,7 @@ namespace App\Models; use Illuminate\Database\Eloquent\Builder; +use App\Models\BaseModel; use Illuminate\Database\Eloquent\Relations\BelongsTo; class Staff extends BaseModel @@ -29,9 +30,9 @@ class Staff extends BaseModel ]; protected $casts = [ - 'user_id' => 'integer', - 'created_at' => 'datetime', - 'updated_at' => 'datetime', + 'user_id' => 'integer', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', ]; /* ============================================================ @@ -136,7 +137,7 @@ class Staff extends BaseModel // generated email. If an exact school-year row is missing but the user // already has any staff row, update that existing row instead of trying // to insert a duplicate-email record. - if (! $existing) { + if (!$existing) { $existing = static::query() ->where('user_id', $userId) ->orderByDesc('id') @@ -150,11 +151,11 @@ class Staff extends BaseModel if ($existing) { // Preserve original created_at unless explicitly provided - if (! array_key_exists('created_at', $payload) && $existing->created_at) { + if (!array_key_exists('created_at', $payload) && $existing->created_at) { $payload['created_at'] = $existing->created_at; } // Ensure updated_at exists - if (! array_key_exists('updated_at', $payload)) { + if (!array_key_exists('updated_at', $payload)) { $payload['updated_at'] = $now; } @@ -165,10 +166,10 @@ class Staff extends BaseModel } // Insert new row — set created_at/updated_at if not provided - if (! array_key_exists('created_at', $payload)) { + if (!array_key_exists('created_at', $payload)) { $payload['created_at'] = $now; } - if (! array_key_exists('updated_at', $payload)) { + if (!array_key_exists('updated_at', $payload)) { $payload['updated_at'] = $now; } @@ -193,17 +194,17 @@ class Staff extends BaseModel $req = $updating ? 'sometimes' : 'required'; return [ - 'user_id' => [$req, 'integer', 'min:1', 'exists:users,id'], - 'firstname' => ['nullable', 'string', 'max:120'], - 'lastname' => ['nullable', 'string', 'max:120'], - 'email' => ['nullable', 'email', 'max:255'], - 'phone' => ['nullable', 'string', 'max:40'], - 'role_name' => ['nullable', 'string', 'max:80'], - 'active_role' => ['nullable', 'string', 'max:80'], // includes 'inactive' - 'status' => ['nullable', 'string', 'max:40'], - 'school_year' => ['nullable', 'string', 'max:20'], - 'created_at' => ['nullable', 'date'], - 'updated_at' => ['nullable', 'date'], + 'user_id' => [$req, 'integer', 'min:1', 'exists:users,id'], + 'firstname' => ['nullable', 'string', 'max:120'], + 'lastname' => ['nullable', 'string', 'max:120'], + 'email' => ['nullable', 'email', 'max:255'], + 'phone' => ['nullable', 'string', 'max:40'], + 'role_name' => ['nullable', 'string', 'max:80'], + 'active_role' => ['nullable', 'string', 'max:80'], // includes 'inactive' + 'status' => ['nullable', 'string', 'max:40'], + 'school_year' => ['nullable', 'string', 'max:20'], + 'created_at' => ['nullable', 'date'], + 'updated_at' => ['nullable', 'date'], ]; } } diff --git a/app/Models/StaffAttendance.php b/app/Models/StaffAttendance.php index 125d8fbd..c44d94c9 100644 --- a/app/Models/StaffAttendance.php +++ b/app/Models/StaffAttendance.php @@ -3,13 +3,13 @@ namespace App\Models; use Illuminate\Database\Eloquent\Builder; +use App\Models\BaseModel; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Support\Facades\DB; class StaffAttendance extends BaseModel { protected $table = 'staff_attendance'; - public $timestamps = true; /** @@ -38,13 +38,13 @@ class StaffAttendance extends BaseModel ]; protected $casts = [ - 'user_id' => 'integer', + 'user_id' => 'integer', 'class_section_id' => 'integer', - 'created_by' => 'integer', - 'updated_by' => 'integer', - 'date' => 'date:Y-m-d', - 'created_at' => 'datetime', - 'updated_at' => 'datetime', + 'created_by' => 'integer', + 'updated_by' => 'integer', + 'date' => 'date:Y-m-d', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', ]; /* ============================================================ @@ -52,14 +52,11 @@ class StaffAttendance extends BaseModel * ============================================================ */ public const STATUS_PRESENT = 'present'; - - public const STATUS_ABSENT = 'absent'; - - public const STATUS_LATE = 'late'; + public const STATUS_ABSENT = 'absent'; + public const STATUS_LATE = 'late'; public const POS_MAIN = 'main'; - - public const POS_TA = 'ta'; + public const POS_TA = 'ta'; public static function allowedStatuses(): array { @@ -121,14 +118,12 @@ class StaffAttendance extends BaseModel protected static function normStatus(?string $status): ?string { $s = strtolower(trim((string) $status)); - return in_array($s, self::allowedStatuses(), true) ? $s : null; } protected static function isValidDate(string $d): bool { $dt = date_create_from_format('Y-m-d', $d); - return $dt && $dt->format('Y-m-d') === $d; } @@ -151,7 +146,7 @@ class StaffAttendance extends BaseModel ?string $reason, ?int $editorId = null ): ?self { - if ($userId <= 0 || ! self::isValidDate($date) || $semester === '' || $schoolYear === '') { + if ($userId <= 0 || !self::isValidDate($date) || $semester === '' || $schoolYear === '') { return null; } @@ -163,17 +158,17 @@ class StaffAttendance extends BaseModel $editorId = $editorId ?: (auth()->id() ?? null); $keys = [ - 'user_id' => $userId, - 'date' => $date, - 'semester' => $semester, + 'user_id' => $userId, + 'date' => $date, + 'semester' => $semester, 'school_year' => $schoolYear, ]; $payload = [ - 'role_name' => $roleName, - 'status' => $norm, - 'reason' => $reason ?: null, - 'updated_by' => $editorId, + 'role_name' => $roleName, + 'status' => $norm, + 'reason' => $reason ?: null, + 'updated_by' => $editorId, ]; // Ensure created_by is set on create @@ -199,7 +194,7 @@ class StaffAttendance extends BaseModel ?string $reason = null, ?int $editorId = null ): bool { - if ($userId <= 0 || ! self::isValidDate($date) || $semester === '' || $schoolYear === '') { + if ($userId <= 0 || !self::isValidDate($date) || $semester === '' || $schoolYear === '') { return false; } @@ -217,28 +212,26 @@ class StaffAttendance extends BaseModel if ($existing) { return (bool) $existing->delete(); } - return true; } $keys = [ - 'user_id' => $userId, - 'date' => $date, - 'semester' => $semester, + 'user_id' => $userId, + 'date' => $date, + 'semester' => $semester, 'school_year' => $schoolYear, ]; $payload = [ - 'role_name' => $roleName ?? ($existing->role_name ?? null), + 'role_name' => $roleName ?? ($existing->role_name ?? null), 'class_section_id' => $classSectionId, - 'position' => $position, - 'status' => $norm, - 'reason' => $reason, - 'updated_by' => $editorId, + 'position' => $position, + 'status' => $norm, + 'reason' => $reason, + 'updated_by' => $editorId, ]; static::query()->updateOrCreate($keys, $payload + ['created_by' => $editorId]); - return true; } @@ -250,7 +243,6 @@ class StaffAttendance extends BaseModel public static function deleteOne(int $userId, string $date): bool { $row = static::getOne($userId, $date); - return $row ? (bool) $row->delete() : true; } @@ -284,27 +276,27 @@ class StaffAttendance extends BaseModel ->leftJoinSub($roleSub, 'rs', fn ($j) => $j->on('rs.user_id', '=', 'u.id')) ->leftJoin('staff_attendance as sa', function ($j) use ($date, $semester, $schoolYear) { $j->on('sa.user_id', '=', 'u.id') - ->whereDate('sa.date', '=', $date) - ->where('sa.semester', '=', $semester) - ->where('sa.school_year', '=', $schoolYear); + ->whereDate('sa.date', '=', $date) + ->where('sa.semester', '=', $semester) + ->where('sa.school_year', '=', $schoolYear); }) ->whereNotIn(DB::raw('LOWER(COALESCE(rs.role_name,""))'), ['parent', 'guest']) ->orderBy('u.firstname') ->orderBy('u.lastname'); - if (! empty($excludeUserIds)) { + if (!empty($excludeUserIds)) { $q->whereNotIn('u.id', array_values(array_map('intval', $excludeUserIds))); } return $q->get()->map(function ($r) { return [ - 'user_id' => (int) $r->user_id, - 'firstname' => (string) ($r->firstname ?? ''), - 'lastname' => (string) ($r->lastname ?? ''), - 'role_name' => (string) ($r->role_name ?? ''), - 'satt_id' => $r->satt_id ? (int) $r->satt_id : null, - 'status' => $r->status ? (string) $r->status : null, - 'reason' => $r->reason, + 'user_id' => (int) $r->user_id, + 'firstname' => (string) ($r->firstname ?? ''), + 'lastname' => (string) ($r->lastname ?? ''), + 'role_name' => (string) ($r->role_name ?? ''), + 'satt_id' => $r->satt_id ? (int) $r->satt_id : null, + 'status' => $r->status ? (string) $r->status : null, + 'reason' => $r->reason, ]; })->all(); } @@ -326,10 +318,10 @@ class StaffAttendance extends BaseModel ->get() ->map(fn ($r) => [ 'class_section_id' => (int) $r->class_section_id, - 'teacher_id' => (int) $r->teacher_id, - 'position' => (string) $r->position, - 'firstname' => (string) ($r->firstname ?? ''), - 'lastname' => (string) ($r->lastname ?? ''), + 'teacher_id' => (int) $r->teacher_id, + 'position' => (string) $r->position, + 'firstname' => (string) ($r->firstname ?? ''), + 'lastname' => (string) ($r->lastname ?? ''), ])->all(); } @@ -358,20 +350,20 @@ class StaffAttendance extends BaseModel string $schoolYear ): array { return DB::table('teacher_class as tc') - ->selectRaw(' + ->selectRaw(" tc.class_section_id, tc.teacher_id as teacher_id, tc.position, u.firstname, u.lastname, t.id as satt_id, t.status, t.reason - ') + ") ->join('users as u', 'u.id', '=', 'tc.teacher_id') ->leftJoin('staff_attendance as t', function ($j) use ($date, $semester, $schoolYear) { $j->on('t.user_id', '=', 'tc.teacher_id') - ->on('t.class_section_id', '=', 'tc.class_section_id') - ->whereDate('t.date', '=', $date) - ->where('t.semester', '=', $semester) - ->where('t.school_year', '=', $schoolYear); + ->on('t.class_section_id', '=', 'tc.class_section_id') + ->whereDate('t.date', '=', $date) + ->where('t.semester', '=', $semester) + ->where('t.school_year', '=', $schoolYear); }) ->where('tc.class_section_id', $classSectionId) ->where('tc.school_year', $schoolYear) @@ -380,13 +372,13 @@ class StaffAttendance extends BaseModel ->get() ->map(fn ($r) => [ 'class_section_id' => (int) $r->class_section_id, - 'teacher_id' => (int) $r->teacher_id, - 'position' => (string) $r->position, - 'firstname' => (string) ($r->firstname ?? ''), - 'lastname' => (string) ($r->lastname ?? ''), - 'satt_id' => $r->satt_id ? (int) $r->satt_id : null, - 'status' => $r->status ? strtolower((string) $r->status) : null, - 'reason' => $r->reason, + 'teacher_id' => (int) $r->teacher_id, + 'position' => (string) $r->position, + 'firstname' => (string) ($r->firstname ?? ''), + 'lastname' => (string) ($r->lastname ?? ''), + 'satt_id' => $r->satt_id ? (int) $r->satt_id : null, + 'status' => $r->status ? strtolower((string) $r->status) : null, + 'reason' => $r->reason, ])->all(); } @@ -401,9 +393,7 @@ class StaffAttendance extends BaseModel ->where('school_year', $schoolYear) ->count(); - if ($assigned <= 0) { - return true; - } + if ($assigned <= 0) return true; $present = (int) DB::table('staff_attendance') ->where('class_section_id', $classSectionId) @@ -428,9 +418,7 @@ class StaffAttendance extends BaseModel string $schoolYear ): array { $codes = array_values(array_unique(array_filter(array_map('intval', $sectionCodes)))); - if (empty($codes)) { - return []; - } + if (empty($codes)) return []; $rows = DB::table('staff_attendance') ->selectRaw('class_section_id, user_id, DATE_FORMAT(date,"%Y-%m-%d") as d, status, reason') @@ -452,7 +440,6 @@ class StaffAttendance extends BaseModel 'reason' => $r->reason, ]; } - return $map; } @@ -465,20 +452,20 @@ class StaffAttendance extends BaseModel $req = $updating ? 'sometimes' : 'required'; return [ - 'user_id' => [$req, 'integer', 'min:1', 'exists:users,id'], - 'date' => [$req, 'date_format:Y-m-d'], - 'semester' => [$req, 'string', 'max:20'], + 'user_id' => [$req, 'integer', 'min:1', 'exists:users,id'], + 'date' => [$req, 'date_format:Y-m-d'], + 'semester' => [$req, 'string', 'max:20'], 'school_year' => [$req, 'string', 'max:20'], - 'role_name' => ['nullable', 'string', 'max:80'], - 'status' => ['nullable', 'string', 'in:'.implode(',', self::allowedStatuses())], - 'reason' => ['nullable', 'string', 'max:2000'], + 'role_name' => ['nullable', 'string', 'max:80'], + 'status' => ['nullable', 'string', 'in:' . implode(',', self::allowedStatuses())], + 'reason' => ['nullable', 'string', 'max:2000'], 'class_section_id' => ['nullable', 'integer'], - 'position' => ['nullable', 'string', 'in:'.implode(',', [self::POS_MAIN, self::POS_TA])], + 'position' => ['nullable', 'string', 'in:' . implode(',', [self::POS_MAIN, self::POS_TA])], - 'created_by' => ['nullable', 'integer'], - 'updated_by' => ['nullable', 'integer'], + 'created_by' => ['nullable', 'integer'], + 'updated_by' => ['nullable', 'integer'], ]; } -} +} \ No newline at end of file diff --git a/app/Models/Stats.php b/app/Models/Stats.php index a260abe5..c8e3bf07 100644 --- a/app/Models/Stats.php +++ b/app/Models/Stats.php @@ -2,6 +2,8 @@ namespace App\Models; +use App\Models\BaseModel; + class Stats extends BaseModel { protected $table = 'stats'; @@ -22,8 +24,8 @@ class Stats extends BaseModel protected $casts = [ 'students' => 'integer', 'teachers' => 'integer', - 'admins' => 'integer', - 'users' => 'integer', + 'admins' => 'integer', + 'users' => 'integer', ]; public function getFillable(): array @@ -49,8 +51,8 @@ class Stats extends BaseModel return static::query()->first() ?? static::query()->create([ 'students' => 0, 'teachers' => 0, - 'admins' => 0, - 'users' => 0, + 'admins' => 0, + 'users' => 0, ]); } @@ -62,7 +64,6 @@ class Stats extends BaseModel $row = static::singleton(); $row->fill($data); $row->save(); - return $row; } } diff --git a/app/Models/Student.php b/app/Models/Student.php index 2dc6ceb4..2ec699a4 100644 --- a/app/Models/Student.php +++ b/app/Models/Student.php @@ -3,6 +3,7 @@ namespace App\Models; use Illuminate\Database\Eloquent\Builder; +use App\Models\BaseModel; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; @@ -11,7 +12,6 @@ use Illuminate\Support\Facades\DB; class Student extends BaseModel { use HasFactory; - protected $table = 'students'; /** @@ -39,15 +39,15 @@ class Student extends BaseModel ]; protected $casts = [ - 'school_id' => 'string', - 'age' => 'integer', - 'photo_consent' => 'boolean', - 'is_new' => 'boolean', - 'parent_id' => 'integer', - 'tuition_paid' => 'boolean', - 'is_active' => 'boolean', - 'dob' => 'date', - 'registration_date' => 'date', + 'school_id' => 'string', + 'age' => 'integer', + 'photo_consent' => 'boolean', + 'is_new' => 'boolean', + 'parent_id' => 'integer', + 'tuition_paid' => 'boolean', + 'is_active' => 'boolean', + 'dob' => 'date', + 'registration_date' => 'date', 'year_of_registration' => 'integer', ]; @@ -97,7 +97,6 @@ class Student extends BaseModel if ($schoolYear === null || strtolower(trim($schoolYear)) === 'all' || trim($schoolYear) === '') { return $q; } - return $q->where('school_year', $schoolYear); } @@ -214,7 +213,6 @@ class Student extends BaseModel public static function getStudentSchoolIdByStudentId(int $studentId): ?string { $v = static::query()->where('id', $studentId)->value('school_id'); - return $v !== null ? (string) $v : null; } @@ -234,16 +232,12 @@ class Student extends BaseModel public static function getFullNameById($studentId): ?array { $student = static::query()->find($studentId); - if (! $student) { - return null; - } + if (!$student) return null; $first = (string) ($student->firstname ?? ''); - $last = (string) ($student->lastname ?? ''); + $last = (string) ($student->lastname ?? ''); - if ($first === '' && $last === '') { - return null; - } + if ($first === '' && $last === '') return null; return ['firstname' => $first, 'lastname' => $last]; } @@ -251,7 +245,7 @@ class Student extends BaseModel /** legacy: getFullName($student array) */ public static function getFullName(array $student): string { - return trim(($student['firstname'] ?? '').' '.($student['lastname'] ?? '')); + return trim(($student['firstname'] ?? '') . ' ' . ($student['lastname'] ?? '')); } /** legacy: getLastRegistrationDate($ParentId) */ @@ -278,9 +272,7 @@ class Student extends BaseModel ->pluck('student_id') ->all(); - if (empty($studentIds)) { - return []; - } + if (empty($studentIds)) return []; return static::query() ->whereIn('id', $studentIds) @@ -348,9 +340,7 @@ class Student extends BaseModel */ public static function getStudentInfoByClassSectionId(int $classSectionId, ?string $semester = null, ?string $schoolYear = null, ?int $actorId = null): array { - if ($classSectionId <= 0) { - return []; - } + if ($classSectionId <= 0) return []; $q = DB::table('students') ->selectRaw('students.id AS student_id, students.school_id, students.firstname, students.lastname, sc.class_section_id') @@ -385,22 +375,22 @@ class Student extends BaseModel $req = $updating ? 'sometimes' : 'required'; return [ - 'school_id' => ['nullable', 'integer', 'min:1'], - 'firstname' => [$req, 'string', 'max:120'], - 'lastname' => [$req, 'string', 'max:120'], - 'dob' => ['nullable', 'date'], - 'age' => ['nullable', 'integer', 'min:0'], - 'gender' => ['nullable', 'string', 'max:20'], - 'registration_grade' => ['nullable', 'string', 'max:50'], - 'photo_consent' => ['nullable', 'boolean'], - 'is_new' => ['nullable', 'boolean'], - 'parent_id' => ['nullable', 'integer'], - 'school_year' => ['nullable', 'string', 'max:20'], - 'registration_date' => ['nullable', 'date'], - 'tuition_paid' => ['nullable', 'boolean'], - 'year_of_registration' => ['nullable', 'integer', 'min:0'], - 'rfid_tag' => ['nullable', 'string', 'max:120'], - 'is_active' => ['nullable', 'boolean'], + 'school_id' => ['nullable', 'integer', 'min:1'], + 'firstname' => [$req, 'string', 'max:120'], + 'lastname' => [$req, 'string', 'max:120'], + 'dob' => ['nullable', 'date'], + 'age' => ['nullable', 'integer', 'min:0'], + 'gender' => ['nullable', 'string', 'max:20'], + 'registration_grade' => ['nullable', 'string', 'max:50'], + 'photo_consent' => ['nullable', 'boolean'], + 'is_new' => ['nullable', 'boolean'], + 'parent_id' => ['nullable', 'integer'], + 'school_year' => ['nullable', 'string', 'max:20'], + 'registration_date' => ['nullable', 'date'], + 'tuition_paid' => ['nullable', 'boolean'], + 'year_of_registration'=> ['nullable', 'integer', 'min:0'], + 'rfid_tag' => ['nullable', 'string', 'max:120'], + 'is_active' => ['nullable', 'boolean'], ]; } } diff --git a/app/Models/StudentAllergy.php b/app/Models/StudentAllergy.php index 11d7fc2b..83c1ea7b 100644 --- a/app/Models/StudentAllergy.php +++ b/app/Models/StudentAllergy.php @@ -3,6 +3,7 @@ namespace App\Models; use Illuminate\Database\Eloquent\Builder; +use App\Models\BaseModel; use Illuminate\Database\Eloquent\Relations\BelongsTo; class StudentAllergy extends BaseModel @@ -46,7 +47,6 @@ class StudentAllergy extends BaseModel public function scopeForStudents(Builder $q, array $studentIds): Builder { $ids = array_values(array_unique(array_filter(array_map('intval', $studentIds)))); - return empty($ids) ? $q->whereRaw('1=0') : $q->whereIn('student_id', $ids); } @@ -103,7 +103,7 @@ class StudentAllergy extends BaseModel return [ 'student_id' => [$req, 'integer', 'min:1', 'exists:students,id'], - 'allergy' => [$req, 'string', 'max:100'], + 'allergy' => [$req, 'string', 'max:100'], ]; } @@ -111,9 +111,9 @@ class StudentAllergy extends BaseModel { return [ 'student_id.required' => 'Student ID is required.', - 'student_id.min' => 'Invalid Student ID.', - 'allergy.required' => 'Allergy is required.', - 'allergy.max' => 'Allergy must be 100 characters or fewer.', + 'student_id.min' => 'Invalid Student ID.', + 'allergy.required' => 'Allergy is required.', + 'allergy.max' => 'Allergy must be 100 characters or fewer.', ]; } -} +} \ No newline at end of file diff --git a/app/Models/StudentClass.php b/app/Models/StudentClass.php index 47d3584b..70bd4d2d 100644 --- a/app/Models/StudentClass.php +++ b/app/Models/StudentClass.php @@ -3,6 +3,7 @@ namespace App\Models; use Illuminate\Database\Eloquent\Builder; +use App\Models\BaseModel; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Support\Facades\DB; @@ -22,17 +23,16 @@ class StudentClass extends BaseModel 'updated_at', 'created_at', ]; - public $timestamps = true; protected $casts = [ - 'student_id' => 'integer', - 'school_id' => 'integer', + 'student_id' => 'integer', + 'school_id' => 'integer', 'class_section_id' => 'integer', - 'is_event_only' => 'boolean', - 'updated_by' => 'integer', - 'created_at' => 'datetime', - 'updated_at' => 'datetime', + 'is_event_only' => 'boolean', + 'updated_by' => 'integer', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', ]; /* ============================================================ @@ -84,10 +84,7 @@ class StudentClass extends BaseModel public function scopeForYear(Builder $q, ?string $schoolYear): Builder { - if ($schoolYear === null || trim($schoolYear) === '') { - return $q; - } - + if ($schoolYear === null || trim($schoolYear) === '') return $q; return $q->where('student_class.school_year', $schoolYear); } @@ -137,12 +134,12 @@ class StudentClass extends BaseModel ->where('sc.school_year', $schoolYear) ->orderBy('cs.class_section_name', 'asc') ->pluck('cs.class_section_name') - ->filter(fn ($v) => $v !== null && trim((string) $v) !== '') + ->filter(fn ($v) => $v !== null && trim((string)$v) !== '') ->unique() ->values() ->all(); - return $asArray ? $names : (! empty($names) ? implode(', ', $names) : ''); + return $asArray ? $names : (!empty($names) ? implode(', ', $names) : ''); } /** @@ -165,17 +162,15 @@ class StudentClass extends BaseModel $names = []; foreach ($rows as $r) { $name = $r->class_section_name ?? null; - if (! $name) { - continue; - } + if (!$name) continue; - $isEvent = (int) ($r->is_event_only ?? 0) === 1; - $names[] = $isEvent ? ($name.' (Event)') : $name; + $isEvent = (int)($r->is_event_only ?? 0) === 1; + $names[] = $isEvent ? ($name . ' (Event)') : $name; } $names = array_values(array_unique(array_filter($names))); - return $asArray ? $names : (! empty($names) ? implode(', ', $names) : ''); + return $asArray ? $names : (!empty($names) ? implode(', ', $names) : ''); } /** @@ -205,9 +200,7 @@ class StudentClass extends BaseModel public static function getStudentsByClassSectionIds(array $classSectionIds): array { $ids = array_values(array_unique(array_filter(array_map('intval', $classSectionIds)))); - if (empty($ids)) { - return []; - } + if (empty($ids)) return []; return DB::table('student_class as sc') ->selectRaw(' @@ -241,7 +234,7 @@ class StudentClass extends BaseModel ->orderByDesc('created_at') ->first(); - if (! $sc || ! $sc->class_section_id) { + if (!$sc || !$sc->class_section_id) { return 'N/A'; } @@ -289,9 +282,7 @@ class StudentClass extends BaseModel $out = []; foreach ($rows as $r) { $cid = $r->class_section_id; - if ($cid === null || $cid === '') { - continue; - } + if ($cid === null || $cid === '') continue; $out[$cid] = (int) ($r->total ?? 0); } @@ -307,13 +298,13 @@ class StudentClass extends BaseModel $req = $updating ? 'sometimes' : 'required'; return [ - 'student_id' => [$req, 'integer', 'min:1', 'exists:students,id'], - 'school_id' => ['nullable', 'integer', 'min:1'], + 'student_id' => [$req, 'integer', 'min:1', 'exists:students,id'], + 'school_id' => ['nullable', 'integer', 'min:1'], 'class_section_id' => ['nullable', 'integer'], - 'school_year' => [$req, 'string', 'max:20'], - 'is_event_only' => ['nullable', 'boolean'], - 'description' => ['nullable', 'string', 'max:2000'], - 'updated_by' => ['nullable', 'integer'], + 'school_year' => [$req, 'string', 'max:20'], + 'is_event_only' => ['nullable', 'boolean'], + 'description' => ['nullable', 'string', 'max:2000'], + 'updated_by' => ['nullable', 'integer'], ]; } -} +} \ No newline at end of file diff --git a/app/Models/StudentMedicalCondition.php b/app/Models/StudentMedicalCondition.php index c89aefbb..2f1209ff 100644 --- a/app/Models/StudentMedicalCondition.php +++ b/app/Models/StudentMedicalCondition.php @@ -3,6 +3,7 @@ namespace App\Models; use Illuminate\Database\Eloquent\Builder; +use App\Models\BaseModel; use Illuminate\Database\Eloquent\Relations\BelongsTo; class StudentMedicalCondition extends BaseModel @@ -46,7 +47,6 @@ class StudentMedicalCondition extends BaseModel public function scopeForStudents(Builder $q, array $studentIds): Builder { $ids = array_values(array_unique(array_filter(array_map('intval', $studentIds)))); - return empty($ids) ? $q->whereRaw('1=0') : $q->whereIn('student_id', $ids); } @@ -100,7 +100,7 @@ class StudentMedicalCondition extends BaseModel $req = $updating ? 'sometimes' : 'required'; return [ - 'student_id' => [$req, 'integer', 'min:1', 'exists:students,id'], + 'student_id' => [$req, 'integer', 'min:1', 'exists:students,id'], 'condition_name' => [$req, 'string', 'max:100'], ]; } @@ -108,10 +108,10 @@ class StudentMedicalCondition extends BaseModel public static function messages(): array { return [ - 'student_id.required' => 'Student ID is required.', - 'student_id.min' => 'Invalid Student ID.', + 'student_id.required' => 'Student ID is required.', + 'student_id.min' => 'Invalid Student ID.', 'condition_name.required' => 'Condition name is required.', - 'condition_name.max' => 'Condition name must be 100 characters or fewer.', + 'condition_name.max' => 'Condition name must be 100 characters or fewer.', ]; } -} +} \ No newline at end of file diff --git a/app/Models/StudentPromotionRecord.php b/app/Models/StudentPromotionRecord.php index a978301a..1611f7ae 100644 --- a/app/Models/StudentPromotionRecord.php +++ b/app/Models/StudentPromotionRecord.php @@ -16,39 +16,25 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo; class StudentPromotionRecord extends BaseModel { protected $table = 'student_promotion_records'; - protected $primaryKey = 'promotion_id'; public $timestamps = true; public const STATUS_NOT_REVIEWED = 'not_reviewed'; - public const STATUS_ELIGIBLE = 'eligible_for_promotion'; - public const STATUS_AWAITING_PARENT = 'awaiting_parent_enrollment'; - public const STATUS_ENROLLMENT_STARTED = 'enrollment_started'; - public const STATUS_PROMOTED_AND_ENROLLED = 'promoted_and_enrolled'; - public const STATUS_CONDITIONAL = 'conditional_promotion'; - public const STATUS_REPEATED = 'repeated_level'; - public const STATUS_ON_HOLD = 'on_hold'; - public const STATUS_WITHDRAWN = 'withdrawn'; - public const STATUS_GRADUATED = 'graduated'; - public const STATUS_NOT_ENROLLED = 'not_enrolled_for_next_year'; public const ENROLLMENT_NOT_STARTED = 'not_started'; - public const ENROLLMENT_IN_PROGRESS = 'in_progress'; - public const ENROLLMENT_COMPLETED = 'completed'; - public const ENROLLMENT_EXPIRED = 'expired'; public const ALL_STATUSES = [ diff --git a/app/Models/SubjectCurriculum.php b/app/Models/SubjectCurriculum.php index 8652bda2..c0196437 100644 --- a/app/Models/SubjectCurriculum.php +++ b/app/Models/SubjectCurriculum.php @@ -3,6 +3,7 @@ namespace App\Models; use Illuminate\Database\Eloquent\Builder; +use App\Models\BaseModel; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Support\Facades\Schema; @@ -20,14 +21,13 @@ class SubjectCurriculum extends BaseModel 'created_at', 'updated_at', ]; - public $timestamps = true; protected $casts = [ - 'class_id' => 'integer', - 'unit_number' => 'integer', - 'created_at' => 'datetime', - 'updated_at' => 'datetime', + 'class_id' => 'integer', + 'unit_number' => 'integer', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', ]; /* ============================================================ @@ -59,7 +59,7 @@ class SubjectCurriculum extends BaseModel public function scopeForSchoolYear(Builder $q, ?string $schoolYear): Builder { $schoolYear = trim((string) $schoolYear); - if ($schoolYear === '' || ! Schema::hasColumn($this->getTable(), 'school_year')) { + if ($schoolYear === '' || !Schema::hasColumn($this->getTable(), 'school_year')) { return $q; } @@ -69,7 +69,7 @@ class SubjectCurriculum extends BaseModel public function scopeOrdered(Builder $q): Builder { return $q->orderBy('unit_number', 'asc') - ->orderBy('chapter_name', 'asc'); + ->orderBy('chapter_name', 'asc'); } /* ============================================================ @@ -112,7 +112,7 @@ class SubjectCurriculum extends BaseModel ->get() ->map(fn ($r) => [ 'unit_number' => (int) $r->unit_number, - 'unit_title' => (string) ($r->unit_title ?? ''), + 'unit_title' => (string) ($r->unit_title ?? ''), ]) ->all(); } @@ -131,7 +131,7 @@ class SubjectCurriculum extends BaseModel ->orderBy('chapter_name', 'asc') ->distinct() ->pluck('chapter_name') - ->filter(fn ($v) => $v !== null && trim((string) $v) !== '') + ->filter(fn ($v) => $v !== null && trim((string)$v) !== '') ->values() ->all(); } @@ -146,11 +146,11 @@ class SubjectCurriculum extends BaseModel $req = $updating ? 'sometimes' : 'required'; return [ - 'class_id' => [$req, 'integer', 'min:1'], - 'subject' => [$req, 'string', 'max:120'], - 'unit_number' => ['nullable', 'integer', 'min:0'], - 'unit_title' => ['nullable', 'string', 'max:255'], - 'chapter_name' => ['nullable', 'string', 'max:255'], + 'class_id' => [$req, 'integer', 'min:1'], + 'subject' => [$req, 'string', 'max:120'], + 'unit_number' => ['nullable', 'integer', 'min:0'], + 'unit_title' => ['nullable', 'string', 'max:255'], + 'chapter_name' => ['nullable', 'string', 'max:255'], ]; } } diff --git a/app/Models/Supplier.php b/app/Models/Supplier.php index bd9c79c7..47980b7e 100644 --- a/app/Models/Supplier.php +++ b/app/Models/Supplier.php @@ -2,10 +2,11 @@ namespace App\Models; +use App\Models\BaseModel; + class Supplier extends BaseModel { protected $table = 'suppliers'; - public $timestamps = true; protected $fillable = [ diff --git a/app/Models/Supply.php b/app/Models/Supply.php index cc0201f0..162cf2e6 100644 --- a/app/Models/Supply.php +++ b/app/Models/Supply.php @@ -2,6 +2,8 @@ namespace App\Models; +use App\Models\BaseModel; + class Supply extends BaseModel { protected $table = 'supplies'; diff --git a/app/Models/SupplyCategory.php b/app/Models/SupplyCategory.php index 00d58c46..8f422b09 100644 --- a/app/Models/SupplyCategory.php +++ b/app/Models/SupplyCategory.php @@ -2,10 +2,11 @@ namespace App\Models; +use App\Models\BaseModel; + class SupplyCategory extends BaseModel { protected $table = 'supply_categories'; - public $timestamps = true; protected $fillable = [ diff --git a/app/Models/SupplyTransaction.php b/app/Models/SupplyTransaction.php index 1d566b84..9367af3e 100644 --- a/app/Models/SupplyTransaction.php +++ b/app/Models/SupplyTransaction.php @@ -2,6 +2,8 @@ namespace App\Models; +use App\Models\BaseModel; + class SupplyTransaction extends BaseModel { protected $table = 'supply_transactions'; diff --git a/app/Models/SupportRequest.php b/app/Models/SupportRequest.php index 78234c41..7c4bb6c6 100644 --- a/app/Models/SupportRequest.php +++ b/app/Models/SupportRequest.php @@ -2,10 +2,11 @@ namespace App\Models; +use App\Models\BaseModel; + class SupportRequest extends BaseModel { protected $table = 'support_requests'; - public $timestamps = true; protected $fillable = [ diff --git a/app/Models/Teacher.php b/app/Models/Teacher.php index dd51c42c..826cac19 100644 --- a/app/Models/Teacher.php +++ b/app/Models/Teacher.php @@ -2,4 +2,6 @@ namespace App\Models; -class Teacher extends User {} +class Teacher extends User +{ +} diff --git a/app/Models/TeacherClass.php b/app/Models/TeacherClass.php index 2471096d..051f954c 100644 --- a/app/Models/TeacherClass.php +++ b/app/Models/TeacherClass.php @@ -3,6 +3,7 @@ namespace App\Models; use Illuminate\Database\Eloquent\Builder; +use App\Models\BaseModel; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Support\Facades\DB; @@ -20,30 +21,30 @@ class TeacherClass extends BaseModel 'updated_at', 'created_at', ]; - public $timestamps = true; protected $casts = [ - 'teacher_id' => 'integer', + 'teacher_id' => 'integer', 'class_section_id' => 'integer', - 'updated_by' => 'integer', - 'created_at' => 'datetime', - 'updated_at' => 'datetime', + 'updated_by' => 'integer', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', ]; + /* ============================================================ * Constants * ============================================================ */ public const POS_MAIN = 'main'; - - public const POS_TA = 'ta'; + public const POS_TA = 'ta'; public static function allowedPositions(): array { return [self::POS_MAIN, self::POS_TA]; } + /* ============================================================ * Relationships (optional) * ============================================================ @@ -137,15 +138,15 @@ class TeacherClass extends BaseModel return $rows->map(function ($r) use ($semester) { return [ - 'class_section_pk' => isset($r->class_section_pk) ? (int) $r->class_section_pk : null, - 'class_section_id' => (int) $r->class_section_id, + 'class_section_pk' => isset($r->class_section_pk) ? (int) $r->class_section_pk : null, + 'class_section_id' => (int) $r->class_section_id, 'class_section_name' => (string) $r->class_section_name, - 'class_id' => isset($r->class_id) ? (int) $r->class_id : null, - 'class_name' => $r->class_name ?? null, - 'teacher_id' => (int) $r->teacher_id, - 'teacher_role' => ucfirst((string) $r->position), // Main / Ta - 'school_year' => (string) $r->school_year, - 'semester' => $semester, + 'class_id' => isset($r->class_id) ? (int) $r->class_id : null, + 'class_name' => $r->class_name ?? null, + 'teacher_id' => (int) $r->teacher_id, + 'teacher_role' => ucfirst((string) $r->position), // Main / Ta + 'school_year' => (string) $r->school_year, + 'semester' => $semester, ]; })->all(); } @@ -213,21 +214,15 @@ class TeacherClass extends BaseModel $classSectionName = trim($classSectionName); if (str_contains($classSectionName, ',')) { $parts = array_values(array_filter(array_map('trim', explode(',', $classSectionName)))); - if (! empty($parts)) { - $classSectionName = $parts[0]; - } - } - if ($classSectionName === '') { - return null; + if (!empty($parts)) $classSectionName = $parts[0]; } + if ($classSectionName === '') return null; $sectionCode = DB::table('classSection') ->where('class_section_name', $classSectionName) ->value('class_section_id'); - if (! $sectionCode) { - return null; - } + if (!$sectionCode) return null; $teacherId = DB::table('teacher_class') ->where('class_section_id', $sectionCode) @@ -269,7 +264,7 @@ class TeacherClass extends BaseModel ?int $updatedBy = null ): bool { $position = strtolower(trim($position)); - if (! in_array($position, self::allowedPositions(), true)) { + if (!in_array($position, self::allowedPositions(), true)) { throw new \InvalidArgumentException('Position must be either "main" or "ta".'); } @@ -277,10 +272,10 @@ class TeacherClass extends BaseModel static::query()->updateOrCreate( [ - 'teacher_id' => $teacherId, + 'teacher_id' => $teacherId, 'class_section_id' => $sectionCode, - 'school_year' => $schoolYear, - 'position' => $position, + 'school_year' => $schoolYear, + 'position' => $position, ], [ 'updated_by' => $updatedBy, @@ -314,7 +309,7 @@ class TeacherClass extends BaseModel * Returns rows: class_section_id, teacher_id, position, firstname, lastname * * @param int|null $alsoSectionCode When teacher_class rows use the section PK while $sectionCode - * is the business code (or the reverse), pass the other id. + * is the business code (or the reverse), pass the other id. * @param bool $requireSemester When false, ignore semester (legacy DBs with blank/wrong tc.semester). */ public static function assignedForSectionTerm( @@ -365,7 +360,6 @@ class TeacherClass extends BaseModel foreach (static::assignedForSectionTerm($sectionCode, $semester, $schoolYear, $alsoSectionCode, $requireSemester) as $r) { $out[(int) $r['teacher_id']] = $r; } - return $out; } @@ -383,11 +377,9 @@ class TeacherClass extends BaseModel ->orderByRaw("CASE tc.position WHEN 'main' THEN 0 WHEN 'ta' THEN 1 ELSE 2 END") ->orderBy('u.firstname', 'asc'); - if (! empty($onlySectionCodes)) { + if (!empty($onlySectionCodes)) { $ids = array_values(array_unique(array_filter(array_map('intval', $onlySectionCodes)))); - if (! empty($ids)) { - $q->whereIn('tc.class_section_id', $ids); - } + if (!empty($ids)) $q->whereIn('tc.class_section_id', $ids); } $rows = $q->get()->map(fn ($r) => (array) $r)->all(); @@ -395,9 +387,7 @@ class TeacherClass extends BaseModel $out = []; foreach ($rows as $r) { $code = (int) ($r['class_section_id'] ?? 0); - if ($code <= 0) { - continue; - } + if ($code <= 0) continue; $out[$code][] = $r; } @@ -413,12 +403,12 @@ class TeacherClass extends BaseModel $req = $updating ? 'sometimes' : 'required'; return [ - 'teacher_id' => [$req, 'integer', 'min:1', 'exists:users,id'], + 'teacher_id' => [$req, 'integer', 'min:1', 'exists:users,id'], 'class_section_id' => [$req, 'integer', 'min:1'], - 'semester' => [$req, 'string', 'max:25'], - 'school_year' => [$req, 'string', 'max:9'], - 'position' => [$req, 'string', 'in:'.implode(',', self::allowedPositions())], - 'updated_by' => ['nullable', 'integer'], + 'semester' => [$req, 'string', 'max:25'], + 'school_year' => [$req, 'string', 'max:9'], + 'position' => [$req, 'string', 'in:' . implode(',', self::allowedPositions())], + 'updated_by' => ['nullable', 'integer'], ]; } } diff --git a/app/Models/TeacherSubmissionNotificationHistory.php b/app/Models/TeacherSubmissionNotificationHistory.php index 1bfe8cd6..294ac2fe 100644 --- a/app/Models/TeacherSubmissionNotificationHistory.php +++ b/app/Models/TeacherSubmissionNotificationHistory.php @@ -3,6 +3,7 @@ namespace App\Models; use Illuminate\Database\Eloquent\Builder; +use App\Models\BaseModel; use Illuminate\Database\Eloquent\Relations\BelongsTo; class TeacherSubmissionNotificationHistory extends BaseModel @@ -27,10 +28,10 @@ class TeacherSubmissionNotificationHistory extends BaseModel ]; protected $casts = [ - 'teacher_id' => 'integer', + 'teacher_id' => 'integer', 'class_section_id' => 'integer', - 'admin_id' => 'integer', - 'sent_at' => 'datetime', + 'admin_id' => 'integer', + 'sent_at' => 'datetime', ]; /* ============================================================ @@ -74,7 +75,7 @@ class TeacherSubmissionNotificationHistory extends BaseModel public function scopeForTerm(Builder $q, string $schoolYear, string $semester): Builder { return $q->where('school_year', $schoolYear) - ->where('semester', $semester); + ->where('semester', $semester); } public function scopeCategory(Builder $q, string $category): Builder @@ -112,15 +113,15 @@ class TeacherSubmissionNotificationHistory extends BaseModel $req = $updating ? 'sometimes' : 'required'; return [ - 'teacher_id' => [$req, 'integer', 'min:1', 'exists:users,id'], - 'class_section_id' => ['nullable', 'integer'], - 'admin_id' => ['nullable', 'integer', 'exists:users,id'], - 'notification_category' => [$req, 'string', 'max:120'], - 'message' => [$req, 'string', 'max:5000'], - 'status' => ['nullable', 'string', 'max:50'], - 'school_year' => ['nullable', 'string', 'max:20'], - 'semester' => ['nullable', 'string', 'max:20'], - 'sent_at' => ['nullable', 'date'], + 'teacher_id' => [$req, 'integer', 'min:1', 'exists:users,id'], + 'class_section_id' => ['nullable', 'integer'], + 'admin_id' => ['nullable', 'integer', 'exists:users,id'], + 'notification_category' => [$req, 'string', 'max:120'], + 'message' => [$req, 'string', 'max:5000'], + 'status' => ['nullable', 'string', 'max:50'], + 'school_year' => ['nullable', 'string', 'max:20'], + 'semester' => ['nullable', 'string', 'max:20'], + 'sent_at' => ['nullable', 'date'], ]; } -} +} \ No newline at end of file diff --git a/app/Models/User.php b/app/Models/User.php index 4f920ecc..2ad96b72 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -4,9 +4,9 @@ namespace App\Models; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; -use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Schema; @@ -58,14 +58,14 @@ class User extends Authenticatable implements JWTSubject ]; protected $casts = [ - 'school_id' => 'integer', - 'failed_attempts' => 'integer', - 'last_failed_at' => 'datetime', - 'accept_school_policy' => 'boolean', - 'is_suspended' => 'boolean', - 'is_verified' => 'boolean', - 'created_at' => 'datetime', - 'updated_at' => 'datetime', + 'school_id' => 'integer', + 'failed_attempts' => 'integer', + 'last_failed_at' => 'datetime', + 'accept_school_policy' => 'boolean', + 'is_suspended' => 'boolean', + 'is_verified' => 'boolean', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', ]; public function getJWTIdentifier() @@ -176,7 +176,7 @@ class User extends Authenticatable implements JWTSubject $pivotColumns[] = 'school_year'; } - if (! empty($pivotColumns)) { + if (!empty($pivotColumns)) { $relation->withPivot($pivotColumns); } @@ -218,7 +218,7 @@ class User extends Authenticatable implements JWTSubject public function scopeHasAnyRoleNames(Builder $q, array $roleNames): Builder { $names = array_values(array_unique(array_filter(array_map( - fn ($v) => strtolower(trim((string) $v)), + fn($v) => strtolower(trim((string)$v)), $roleNames )))); @@ -267,10 +267,8 @@ class User extends Authenticatable implements JWTSubject $order = strtolower($order) === 'desc' ? 'desc' : 'asc'; // whitelist to prevent SQL injection - $allowedSort = ['users.id', 'users.firstname', 'users.lastname', 'users.email', 'role']; - if (! in_array($sort, $allowedSort, true)) { - $sort = 'users.id'; - } + $allowedSort = ['users.id','users.firstname','users.lastname','users.email','role']; + if (!in_array($sort, $allowedSort, true)) $sort = 'users.id'; $q = DB::table('users') ->select('users.id', 'users.firstname', 'users.lastname', 'users.email', DB::raw('roles.name as role')) @@ -279,12 +277,12 @@ class User extends Authenticatable implements JWTSubject // sorting "role" alias requires orderByRaw if ($sort === 'role') { - $q->orderByRaw('role '.strtoupper($order)); + $q->orderByRaw('role ' . strtoupper($order)); } else { $q->orderBy($sort, $order); } - return $q->get()->map(fn ($r) => (array) $r)->all(); + return $q->get()->map(fn($r) => (array) $r)->all(); } /** @@ -314,16 +312,14 @@ class User extends Authenticatable implements JWTSubject // whitelist sort fields $allowedSort = [ - 'users.id', 'users.firstname', 'users.lastname', 'users.email', 'users.cellphone', - 'users.school_year', 'users.status', 'users.created_at', 'users.updated_at', + 'users.id','users.firstname','users.lastname','users.email','users.cellphone', + 'users.school_year','users.status','users.created_at','users.updated_at' ]; - if (! in_array($sort, $allowedSort, true)) { - $sort = 'users.id'; - } + if (!in_array($sort, $allowedSort, true)) $sort = 'users.id'; $q = static::query(); - if (! empty($conditions)) { + if (!empty($conditions)) { $q->where($conditions); } @@ -333,7 +329,7 @@ class User extends Authenticatable implements JWTSubject $q->skip($offset)->take($limit); } - return $q->get()->map(fn ($u) => $u->toArray())->all(); + return $q->get()->map(fn($u) => $u->toArray())->all(); } /** @@ -357,7 +353,6 @@ class User extends Authenticatable implements JWTSubject public static function getAllUsers(string $sort = 'users.id', string $order = 'asc') { $order = strtolower($order) === 'desc' ? 'desc' : 'asc'; - return static::query()->orderBy($sort, $order)->get(); } @@ -369,17 +364,15 @@ class User extends Authenticatable implements JWTSubject { $user = static::query() ->select([ - 'id', 'account_id', 'lastname', 'firstname', 'gender', 'cellphone', 'email', - 'address_street', 'apt', 'city', 'state', 'zip', 'accept_school_policy', - 'user_type', 'rfid_tag', 'school_id', 'failed_attempts', 'last_failed_at', - 'semester', 'school_year', 'status', 'is_suspended', 'created_at', 'updated_at', + 'id','account_id','lastname','firstname','gender','cellphone','email', + 'address_street','apt','city','state','zip','accept_school_policy', + 'user_type','rfid_tag','school_id','failed_attempts','last_failed_at', + 'semester','school_year','status','is_suspended','created_at','updated_at', ]) ->where('id', $userId) ->first(); - if (! $user) { - return null; - } + if (!$user) return null; $roles = DB::table('user_roles as ur') ->select('r.id', 'r.name', 'r.slug') @@ -387,7 +380,7 @@ class User extends Authenticatable implements JWTSubject ->where('ur.user_id', $userId) ->whereNull('ur.deleted_at') ->get() - ->map(fn ($r) => (array) $r) + ->map(fn($r) => (array) $r) ->all(); $arr = $user->toArray(); @@ -408,7 +401,7 @@ class User extends Authenticatable implements JWTSubject ->where('roles.name', $roleName) ->whereNull('user_roles.deleted_at') ->get() - ->map(fn ($r) => (array) $r) + ->map(fn($r) => (array) $r) ->all(); } @@ -425,7 +418,7 @@ class User extends Authenticatable implements JWTSubject ->where('users.school_year', $schoolYear) ->whereNull('user_roles.deleted_at') ->get() - ->map(fn ($r) => (array) $r) + ->map(fn($r) => (array) $r) ->all(); } @@ -452,7 +445,6 @@ class User extends Authenticatable implements JWTSubject public static function getSchoolIdByUserId(int $userId): ?int { $v = static::query()->where('id', $userId)->value('school_id'); - return $v === null ? null : (int) $v; } @@ -466,9 +458,7 @@ class User extends Authenticatable implements JWTSubject ->whereRaw('LOWER(name) = ?', ['parent']) ->value('id'); - if (! $parentRoleId) { - return []; - } + if (!$parentRoleId) return []; return DB::table('users') ->select('users.id', 'users.firstname', 'users.lastname', 'users.email', 'users.school_id') @@ -477,7 +467,7 @@ class User extends Authenticatable implements JWTSubject ->whereNull('user_roles.deleted_at') ->groupBy('users.id', 'users.firstname', 'users.lastname', 'users.email', 'users.school_id') ->get() - ->map(fn ($r) => (array) $r) + ->map(fn($r) => (array) $r) ->all(); } @@ -521,9 +511,7 @@ class User extends Authenticatable implements JWTSubject // whitelist sort fields $allowedSort = ['users.id', 'users.firstname', 'users.lastname', 'users.email', 'roles']; - if (! in_array($sort, $allowedSort, true)) { - $sort = 'users.lastname'; - } + if (!in_array($sort, $allowedSort, true)) $sort = 'users.lastname'; $q = DB::table('users') ->selectRaw(" @@ -538,14 +526,12 @@ class User extends Authenticatable implements JWTSubject ->whereNull('ur.deleted_at') ->whereNotIn(DB::raw('LOWER(r.name)'), ['parent', 'guest']); // drop parent/guest - if (! empty($selectedUserIds)) { + if (!empty($selectedUserIds)) { $ids = array_values(array_unique(array_filter(array_map('intval', $selectedUserIds)))); - if (! empty($ids)) { - $q->whereIn('users.id', $ids); - } + if (!empty($ids)) $q->whereIn('users.id', $ids); } - if (! empty($schoolYear)) { + if (!empty($schoolYear)) { // Prefer ur.school_year if it exists (role is year-scoped) $hasUrSchoolYear = false; try { @@ -595,7 +581,7 @@ class User extends Authenticatable implements JWTSubject $q->orderBy($sort, strtolower($order)); } - return $q->get()->map(fn ($r) => (array) $r)->all(); + return $q->get()->map(fn($r) => (array) $r)->all(); } /* ============================================================ @@ -615,7 +601,7 @@ class User extends Authenticatable implements JWTSubject ->orderBy('users.lastname', 'asc') ->orderBy('users.firstname', 'asc') ->get() - ->map(fn ($u) => $u->toArray()) + ->map(fn($u) => $u->toArray()) ->all(); } @@ -636,7 +622,7 @@ class User extends Authenticatable implements JWTSubject ->orderBy('u.lastname', 'asc') ->orderBy('u.firstname', 'asc') ->get() - ->map(fn ($r) => (array) $r) + ->map(fn($r) => (array) $r) ->all(); } @@ -678,7 +664,7 @@ class User extends Authenticatable implements JWTSubject ->orderBy('users.lastname', 'asc') ->orderBy('users.firstname', 'asc') ->get() - ->map(fn ($r) => (array) $r) + ->map(fn($r) => (array) $r) ->all(); } @@ -690,15 +676,15 @@ class User extends Authenticatable implements JWTSubject public static function registrationRules(): array { return [ - 'password' => ['required', 'string', 'min:8', 'regex:/[A-Z]/', 'regex:/[a-z]/', 'regex:/[0-9]/', 'regex:/[\W_]/'], - 'lastname' => ['required', 'string', 'max:120'], - 'firstname' => ['required', 'string', 'max:120'], - 'cellphone' => ['required', 'regex:/^\d{10}$/'], - 'email' => ['required', 'email', 'max:255'], - 'address_street' => ['required', 'string', 'max:255'], - 'city' => ['required', 'string', 'max:120'], - 'state' => ['required', 'regex:/^[A-Z]{2}$/'], - 'zip' => ['required', 'regex:/^\d{5}(-\d{4})?$/'], + 'password' => ['required', 'string', 'min:8', 'regex:/[A-Z]/', 'regex:/[a-z]/', 'regex:/[0-9]/', 'regex:/[\W_]/'], + 'lastname' => ['required', 'string', 'max:120'], + 'firstname' => ['required', 'string', 'max:120'], + 'cellphone' => ['required', 'regex:/^\d{10}$/'], + 'email' => ['required', 'email', 'max:255'], + 'address_street' => ['required', 'string', 'max:255'], + 'city' => ['required', 'string', 'max:120'], + 'state' => ['required', 'regex:/^[A-Z]{2}$/'], + 'zip' => ['required', 'regex:/^\d{5}(-\d{4})?$/'], 'accept_school_policy' => ['required', 'in:1,true,on'], ]; } @@ -720,12 +706,10 @@ class User extends Authenticatable implements JWTSubject */ public function setPasswordAttribute($value): void { - if ($value === null || $value === '') { - return; - } + if ($value === null || $value === '') return; // Avoid double-hashing - $this->attributes['password'] = str_starts_with((string) $value, '$2y$') + $this->attributes['password'] = str_starts_with((string)$value, '$2y$') ? $value : Hash::make($value); } diff --git a/app/Models/UserNotification.php b/app/Models/UserNotification.php index b2bcd8bc..bbd394a3 100644 --- a/app/Models/UserNotification.php +++ b/app/Models/UserNotification.php @@ -3,6 +3,7 @@ namespace App\Models; use Illuminate\Database\Eloquent\Builder; +use App\Models\BaseModel; use Illuminate\Database\Eloquent\Relations\BelongsTo; class UserNotification extends BaseModel @@ -24,10 +25,10 @@ class UserNotification extends BaseModel protected $casts = [ 'notification_id' => 'integer', - 'user_id' => 'integer', - 'is_read' => 'boolean', - 'delivered' => 'boolean', - 'delivered_at' => 'datetime', + 'user_id' => 'integer', + 'is_read' => 'boolean', + 'delivered' => 'boolean', + 'delivered_at' => 'datetime', ]; /* ============================================================ @@ -111,10 +112,10 @@ class UserNotification extends BaseModel return [ 'notification_id' => [$req, 'integer', 'min:1', 'exists:notifications,id'], - 'user_id' => [$req, 'integer', 'min:1', 'exists:users,id'], - 'is_read' => ['nullable', 'boolean'], - 'delivered' => ['nullable', 'boolean'], - 'delivered_at' => ['nullable', 'date'], + 'user_id' => [$req, 'integer', 'min:1', 'exists:users,id'], + 'is_read' => ['nullable', 'boolean'], + 'delivered' => ['nullable', 'boolean'], + 'delivered_at' => ['nullable', 'date'], ]; } -} +} \ No newline at end of file diff --git a/app/Models/UserRole.php b/app/Models/UserRole.php index 383157b1..0c65ccce 100644 --- a/app/Models/UserRole.php +++ b/app/Models/UserRole.php @@ -3,8 +3,9 @@ namespace App\Models; use Illuminate\Database\Eloquent\Builder; -use Illuminate\Database\Eloquent\Relations\BelongsTo; +use App\Models\BaseModel; use Illuminate\Database\Eloquent\SoftDeletes; +use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Support\Facades\DB; class UserRole extends BaseModel @@ -27,12 +28,12 @@ class UserRole extends BaseModel ]; protected $casts = [ - 'user_id' => 'integer', - 'role_id' => 'integer', - 'updated_by' => 'integer', - 'created_at' => 'datetime', - 'updated_at' => 'datetime', - 'deleted_at' => 'datetime', + 'user_id' => 'integer', + 'role_id' => 'integer', + 'updated_by' => 'integer', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + 'deleted_at' => 'datetime', ]; /* ============================================================ @@ -67,10 +68,7 @@ class UserRole extends BaseModel public function scopeForSchoolYear(Builder $q, ?string $schoolYear): Builder { - if ($schoolYear === null || trim($schoolYear) === '') { - return $q; - } - + if ($schoolYear === null || trim($schoolYear) === '') return $q; return $q->where('school_year', $schoolYear); } @@ -120,13 +118,12 @@ class UserRole extends BaseModel $existing->updated_by = $editorId; $existing->save(); } - return true; } static::query()->create([ - 'user_id' => $userId, - 'role_id' => $roleId, + 'user_id' => $userId, + 'role_id' => $roleId, 'updated_by' => $editorId, ]); @@ -173,11 +170,11 @@ class UserRole extends BaseModel $req = $updating ? 'sometimes' : 'required'; return [ - 'user_id' => [$req, 'integer', 'min:1', 'exists:users,id'], - 'role_id' => [$req, 'integer', 'min:1', 'exists:roles,id'], - 'semester' => ['nullable', 'string', 'max:20'], - 'school_year' => ['nullable', 'string', 'max:20'], + 'user_id' => [$req, 'integer', 'min:1', 'exists:users,id'], + 'role_id' => [$req, 'integer', 'min:1', 'exists:roles,id'], + 'semester' => ['nullable', 'string', 'max:20'], + 'school_year'=> ['nullable', 'string', 'max:20'], 'updated_by' => ['nullable', 'integer'], ]; } -} +} \ No newline at end of file diff --git a/app/Models/WhatsappGroupLink.php b/app/Models/WhatsappGroupLink.php index 5fea9dfb..2dcfe772 100644 --- a/app/Models/WhatsappGroupLink.php +++ b/app/Models/WhatsappGroupLink.php @@ -3,6 +3,7 @@ namespace App\Models; use Illuminate\Database\Eloquent\Builder; +use App\Models\BaseModel; class WhatsappGroupLink extends BaseModel { @@ -16,14 +17,13 @@ class WhatsappGroupLink extends BaseModel 'invite_link', 'active', ]; - public $timestamps = true; // requires created_at/updated_at protected $casts = [ 'class_section_id' => 'integer', - 'active' => 'boolean', - 'created_at' => 'datetime', - 'updated_at' => 'datetime', + 'active' => 'boolean', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', ]; /* ============================================================ @@ -52,7 +52,7 @@ class WhatsappGroupLink extends BaseModel if ($allowNullSemester) { return $q->where(function (Builder $w) use ($sem) { $w->where('semester', $sem) - ->orWhereNull('semester'); + ->orWhereNull('semester'); }); } @@ -72,7 +72,7 @@ class WhatsappGroupLink extends BaseModel public function scopeHasInviteLink(Builder $q): Builder { return $q->whereNotNull('invite_link') - ->where('invite_link', '!=', ''); + ->where('invite_link', '!=', ''); } /* ============================================================ @@ -83,11 +83,11 @@ class WhatsappGroupLink extends BaseModel /** * Get the link for a specific section in a specific term. * - * @param int $sectionId Class/section code (not PK). - * @param string $year School year, e.g. "2025-2026". - * @param string $sem Semester, e.g. "Fall". - * @param bool $onlyActive If true, require active=1. - * @param bool $allowNullSemester If true, accept rows with semester IS NULL as well. + * @param int $sectionId Class/section code (not PK). + * @param string $year School year, e.g. "2025-2026". + * @param string $sem Semester, e.g. "Fall". + * @param bool $onlyActive If true, require active=1. + * @param bool $allowNullSemester If true, accept rows with semester IS NULL as well. */ public static function getLinkForSection( int $sectionId, @@ -114,8 +114,11 @@ class WhatsappGroupLink extends BaseModel /** * Get all links for a term. * - * @param bool|null $onlyActive true: active only, false: inactive only, null: both - * @param bool $allowNullSemester If true, include rows with semester IS NULL. + * @param string $year + * @param string $sem + * @param bool|null $onlyActive true: active only, false: inactive only, null: both + * @param bool $allowNullSemester If true, include rows with semester IS NULL. + * * @return array */ public static function getAllForTerm( @@ -154,16 +157,16 @@ class WhatsappGroupLink extends BaseModel ): self { $payload = [ 'class_section_name' => $sectionName, - 'invite_link' => trim($inviteLink), - 'active' => $active ? 1 : 0, + 'invite_link' => trim($inviteLink), + 'active' => $active ? 1 : 0, ]; // NOTE: semester is part of the uniqueness key like your legacy version. return static::query()->updateOrCreate( [ 'class_section_id' => $sectionId, - 'school_year' => trim($year), - 'semester' => trim($sem), + 'school_year' => trim($year), + 'semester' => trim($sem), ], $payload ); @@ -179,12 +182,12 @@ class WhatsappGroupLink extends BaseModel $req = $updating ? 'sometimes' : 'required'; return [ - 'class_section_id' => [$req, 'integer', 'min:1'], + 'class_section_id' => [$req, 'integer', 'min:1'], 'class_section_name' => [$req, 'string', 'max:255'], - 'school_year' => [$req, 'string', 'max:20'], - 'semester' => ['nullable', 'string', 'max:20'], - 'invite_link' => [$req, 'string', 'max:2000'], - 'active' => ['nullable', 'boolean'], + 'school_year' => [$req, 'string', 'max:20'], + 'semester' => ['nullable', 'string', 'max:20'], + 'invite_link' => [$req, 'string', 'max:2000'], + 'active' => ['nullable', 'boolean'], ]; } -} +} \ No newline at end of file diff --git a/app/Models/WhatsappGroupMembership.php b/app/Models/WhatsappGroupMembership.php index 51ee49a5..6467f542 100644 --- a/app/Models/WhatsappGroupMembership.php +++ b/app/Models/WhatsappGroupMembership.php @@ -3,6 +3,7 @@ namespace App\Models; use Illuminate\Database\Eloquent\Builder; +use App\Models\BaseModel; class WhatsappGroupMembership extends BaseModel { @@ -20,17 +21,16 @@ class WhatsappGroupMembership extends BaseModel 'verified_by', 'verified_at', ]; - public $timestamps = true; // created_at, updated_at protected $casts = [ 'class_section_id' => 'integer', - 'subject_id' => 'integer', - 'is_member' => 'boolean', - 'verified_by' => 'integer', - 'verified_at' => 'datetime', - 'created_at' => 'datetime', - 'updated_at' => 'datetime', + 'subject_id' => 'integer', + 'is_member' => 'boolean', + 'verified_by' => 'integer', + 'verified_at' => 'datetime', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', ]; /* ============================================================ @@ -38,8 +38,7 @@ class WhatsappGroupMembership extends BaseModel * ============================================================ */ public const TYPE_PRIMARY = 'primary'; - - public const TYPE_SECOND = 'second'; + public const TYPE_SECOND = 'second'; public static function normType(string $type): string { @@ -54,7 +53,6 @@ class WhatsappGroupMembership extends BaseModel public function scopeForSections(Builder $q, array $sectionIds): Builder { $ids = array_values(array_unique(array_filter(array_map('intval', $sectionIds)))); - return empty($ids) ? $q->whereRaw('1=0') : $q->whereIn('class_section_id', $ids); } @@ -66,14 +64,13 @@ class WhatsappGroupMembership extends BaseModel public function scopeForSemester(Builder $q, string $sem): Builder { $sem = trim($sem); - return $sem === '' ? $q : $q->where('semester', $sem); } public function scopeForSubject(Builder $q, string $type, int $subjectId): Builder { return $q->where('subject_type', static::normType($type)) - ->where('subject_id', $subjectId); + ->where('subject_id', $subjectId); } /* ============================================================ @@ -88,12 +85,10 @@ class WhatsappGroupMembership extends BaseModel public static function getBySectionsAndTerm(array $sectionIds, string $year, string $sem): array { $ids = array_values(array_unique(array_filter(array_map('intval', $sectionIds)))); - if (empty($ids)) { - return []; - } + if (empty($ids)) return []; $year = trim($year); - $sem = trim($sem); + $sem = trim($sem); $q = static::query() ->forSections($ids) @@ -110,8 +105,8 @@ class WhatsappGroupMembership extends BaseModel $out = []; foreach ($rows as $m) { - $k = sprintf('%d:%s:%d', (int) $m->class_section_id, (string) $m->subject_type, (int) $m->subject_id); - if (! isset($out[$k])) { + $k = sprintf('%d:%s:%d', (int)$m->class_section_id, (string)$m->subject_type, (int)$m->subject_id); + if (!isset($out[$k])) { $out[$k] = $m->toArray(); } } @@ -139,15 +134,15 @@ class WhatsappGroupMembership extends BaseModel ): int { $type = static::normType($type); $year = trim($year); - $sem = trim($sem); + $sem = trim($sem); $payload = [ 'class_section_id' => $classSectionId, - 'school_year' => $year, - 'semester' => $sem, - 'subject_type' => $type, - 'subject_id' => $subjectId, - 'is_member' => $isMember ? 1 : 0, + 'school_year' => $year, + 'semester' => $sem, + 'subject_type' => $type, + 'subject_id' => $subjectId, + 'is_member' => $isMember ? 1 : 0, ]; if ($verifiedBy) { @@ -160,14 +155,13 @@ class WhatsappGroupMembership extends BaseModel $row = static::query()->updateOrCreate( [ 'class_section_id' => $classSectionId, - 'school_year' => $year, - 'semester' => $sem, - 'subject_type' => $type, - 'subject_id' => $subjectId, + 'school_year' => $year, + 'semester' => $sem, + 'subject_type' => $type, + 'subject_id' => $subjectId, ], $payload ); - return (int) $row->id; } @@ -183,7 +177,7 @@ class WhatsappGroupMembership extends BaseModel if ($existing) { // Keep semester from the existing row if present (legacy behavior). - if (! empty($existing->semester)) { + if (!empty($existing->semester)) { $payload['semester'] = (string) $existing->semester; } @@ -195,7 +189,6 @@ class WhatsappGroupMembership extends BaseModel // No existing row => create a new one (semester remains blank) $created = static::query()->create($payload); - return (int) $created->id; } @@ -210,13 +203,13 @@ class WhatsappGroupMembership extends BaseModel return [ 'class_section_id' => [$req, 'integer', 'min:1'], - 'school_year' => [$req, 'string', 'max:20'], - 'semester' => ['nullable', 'string', 'max:20'], - 'subject_type' => [$req, 'string', 'in:'.implode(',', [self::TYPE_PRIMARY, self::TYPE_SECOND])], - 'subject_id' => [$req, 'integer', 'min:1'], - 'is_member' => ['nullable', 'boolean'], - 'verified_by' => ['nullable', 'integer'], - 'verified_at' => ['nullable', 'date'], + 'school_year' => [$req, 'string', 'max:20'], + 'semester' => ['nullable', 'string', 'max:20'], + 'subject_type' => [$req, 'string', 'in:' . implode(',', [self::TYPE_PRIMARY, self::TYPE_SECOND])], + 'subject_id' => [$req, 'integer', 'min:1'], + 'is_member' => ['nullable', 'boolean'], + 'verified_by' => ['nullable', 'integer'], + 'verified_at' => ['nullable', 'date'], ]; } -} +} \ No newline at end of file diff --git a/app/Models/WhatsappInviteLog.php b/app/Models/WhatsappInviteLog.php index 76ae6705..0c966510 100644 --- a/app/Models/WhatsappInviteLog.php +++ b/app/Models/WhatsappInviteLog.php @@ -3,6 +3,7 @@ namespace App\Models; use Illuminate\Database\Eloquent\Builder; +use App\Models\BaseModel; use Illuminate\Database\Eloquent\Relations\BelongsTo; class WhatsappInviteLog extends BaseModel @@ -25,18 +26,17 @@ class WhatsappInviteLog extends BaseModel ]; protected $casts = [ - 'parent_id' => 'integer', + 'parent_id' => 'integer', 'class_section_id' => 'integer', - 'link_id' => 'integer', - 'sent_at' => 'datetime', + 'link_id' => 'integer', + 'sent_at' => 'datetime', ]; /* ============================================================ * Status constants (optional but helpful) * ============================================================ */ - public const STATUS_SENT = 'sent'; - + public const STATUS_SENT = 'sent'; public const STATUS_FAILED = 'failed'; /* ============================================================ @@ -103,13 +103,13 @@ class WhatsappInviteLog extends BaseModel ?int $linkId = null ): bool { static::query()->create([ - 'parent_id' => $parentId, - 'email' => $email, + 'parent_id' => $parentId, + 'email' => $email, 'class_section_id' => $classSectionId, - 'link_id' => $linkId, - 'status' => self::STATUS_SENT, - 'error_message' => null, - 'sent_at' => now(), + 'link_id' => $linkId, + 'status' => self::STATUS_SENT, + 'error_message' => null, + 'sent_at' => now(), ]); return true; @@ -126,13 +126,13 @@ class WhatsappInviteLog extends BaseModel ?int $linkId = null ): bool { static::query()->create([ - 'parent_id' => $parentId, - 'email' => $email, + 'parent_id' => $parentId, + 'email' => $email, 'class_section_id' => $classSectionId, - 'link_id' => $linkId, - 'status' => self::STATUS_FAILED, - 'error_message' => $error, - 'sent_at' => now(), + 'link_id' => $linkId, + 'status' => self::STATUS_FAILED, + 'error_message' => $error, + 'sent_at' => now(), ]); return true; @@ -167,13 +167,13 @@ class WhatsappInviteLog extends BaseModel $req = $updating ? 'sometimes' : 'required'; return [ - 'parent_id' => [$req, 'integer', 'min:1'], - 'email' => [$req, 'email', 'max:255'], - 'status' => [$req, 'string', 'max:20'], + 'parent_id' => [$req, 'integer', 'min:1'], + 'email' => [$req, 'email', 'max:255'], + 'status' => [$req, 'string', 'max:20'], 'class_section_id' => ['nullable', 'integer'], - 'link_id' => ['nullable', 'integer'], - 'error_message' => ['nullable', 'string', 'max:5000'], - 'sent_at' => ['nullable', 'date'], + 'link_id' => ['nullable', 'integer'], + 'error_message' => ['nullable', 'string', 'max:5000'], + 'sent_at' => ['nullable', 'date'], ]; } -} +} \ No newline at end of file diff --git a/app/Policies/NavItemPolicy.php b/app/Policies/NavItemPolicy.php index 44c57ee6..f6022037 100644 --- a/app/Policies/NavItemPolicy.php +++ b/app/Policies/NavItemPolicy.php @@ -2,6 +2,7 @@ namespace App\Policies; +use App\Models\NavItem; use App\Models\User; class NavItemPolicy @@ -9,7 +10,6 @@ class NavItemPolicy public function manage(User $user): bool { $roles = $user->roles()->pluck('roles.name')->map(fn ($name) => strtolower((string) $name))->toArray(); - return in_array('admin', $roles, true) || in_array('administrator', $roles, true); } } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 742d555a..2b03f011 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -2,42 +2,42 @@ namespace App\Providers; -use App\Models\ClassProgressReport; -use App\Models\ClassSection; -use App\Models\IpAttempt; -use App\Models\LateSlipLog; -use App\Models\Message; -use App\Models\NavItem; -use App\Models\Preferences; -use App\Models\Setting; -use App\Models\Staff; -use App\Models\SupportRequest; -use App\Policies\ClassProgressReportPolicy; -use App\Policies\ClassSectionPolicy; -use App\Policies\IpAttemptPolicy; -use App\Policies\LateSlipLogPolicy; -use App\Policies\MessagePolicy; -use App\Policies\NavItemPolicy; -use App\Policies\PreferencesPolicy; -use App\Policies\SettingPolicy; -use App\Policies\StaffPolicy; -use App\Policies\SupportRequestPolicy; -use App\Services\ApplicationUrlService; -use App\Services\Attendance\AttendanceAutoPublishService; use App\Services\Attendance\AttendancePolicyService; use App\Services\Attendance\AttendanceRecordSyncService; use App\Services\Attendance\SemesterRangeService; use App\Services\Attendance\StudentAttendanceWriterService; use App\Services\Attendance\TeacherAttendanceSubmissionService; +use App\Services\Attendance\AttendanceAutoPublishService; use App\Services\AttendanceTracking\AttendanceMailerService; use App\Services\AttendanceTracking\DefaultAttendanceMailerService; use App\Services\Invoices\InvoiceConfigService; use App\Services\Invoices\InvoiceGradeService; use App\Services\Invoices\InvoiceTuitionService; use App\Services\Staff\StaffTimeOffLinkService; +use App\Models\NavItem; +use App\Models\ClassSection; +use App\Models\IpAttempt; +use App\Models\LateSlipLog; +use App\Models\Message; +use App\Models\Preferences; +use App\Models\Staff; +use App\Models\SupportRequest; +use App\Models\Setting; +use App\Models\ClassProgressReport; +use App\Policies\NavItemPolicy; +use App\Policies\ClassSectionPolicy; +use App\Policies\IpAttemptPolicy; +use App\Policies\LateSlipLogPolicy; +use App\Policies\MessagePolicy; +use App\Policies\PreferencesPolicy; +use App\Policies\StaffPolicy; +use App\Policies\SupportRequestPolicy; +use App\Policies\SettingPolicy; +use App\Policies\ClassProgressReportPolicy; +use Illuminate\Support\Facades\Gate; use Illuminate\Foundation\Http\FormRequest; use Illuminate\Routing\Redirector; -use Illuminate\Support\Facades\Gate; +use App\Services\ApplicationUrlService; use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider @@ -58,12 +58,10 @@ class AppServiceProvider extends ServiceProvider $this->app->singleton(InvoiceConfigService::class); $this->app->bind(InvoiceGradeService::class, function ($app) { $config = $app->make(InvoiceConfigService::class); - return new InvoiceGradeService($config->getGradeFee()); }); $this->app->bind(InvoiceTuitionService::class, function ($app) { $config = $app->make(InvoiceConfigService::class); - return new InvoiceTuitionService( $app->make(InvoiceGradeService::class), $config->getGradeFee(), diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index 871de827..a7435f14 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -2,8 +2,8 @@ namespace App\Providers; -use App\Events\DeleteUnverifiedUser; use App\Events\WhatsappInvitesSend; +use App\Events\DeleteUnverifiedUser; use App\Listeners\AttendanceConsequenceListener; use App\Listeners\BelowSixtyEmailListener; use App\Listeners\DeleteUnverifiedUserListener; @@ -15,94 +15,94 @@ class EventServiceProvider extends ServiceProvider { protected $listen = [ 'attendance.follow_up' => [ - AttendanceConsequenceListener::class.'@followUp', + AttendanceConsequenceListener::class . '@followUp', ], 'attendance.final_warning' => [ - AttendanceConsequenceListener::class.'@finalWarning', + AttendanceConsequenceListener::class . '@finalWarning', ], 'attendance.dismissal' => [ - AttendanceConsequenceListener::class.'@dismissal', + AttendanceConsequenceListener::class . '@dismissal', ], 'below60.email' => [ BelowSixtyEmailListener::class, ], 'admissionUnderReview' => [ - SchoolEventListener::class.'@handleAdmissionUnderReview', + SchoolEventListener::class . '@handleAdmissionUnderReview', ], 'paymentPending' => [ - SchoolEventListener::class.'@handlePaymentPending', + SchoolEventListener::class . '@handlePaymentPending', ], 'studentEnrolled' => [ - SchoolEventListener::class.'@handleStudentEnrolled', + SchoolEventListener::class . '@handleStudentEnrolled', ], 'withdrawUnderReview' => [ - SchoolEventListener::class.'@handleWithdrawUnderReview', + SchoolEventListener::class . '@handleWithdrawUnderReview', ], 'refundPending' => [ - SchoolEventListener::class.'@handleRefundPending', + SchoolEventListener::class . '@handleRefundPending', ], 'withdrawn' => [ - SchoolEventListener::class.'@handleWithdrawn', + SchoolEventListener::class . '@handleWithdrawn', ], 'denied' => [ - SchoolEventListener::class.'@handleDenied', + SchoolEventListener::class . '@handleDenied', ], 'waitlist' => [ - SchoolEventListener::class.'@handleWaitlist', + SchoolEventListener::class . '@handleWaitlist', ], 'enrollmentStatusChanged' => [ - SchoolEventListener::class.'@handleEnrollmentStatusChanged', + SchoolEventListener::class . '@handleEnrollmentStatusChanged', ], 'paymentReceived' => [ - SchoolEventListener::class.'@handlePaymentReceived', + SchoolEventListener::class . '@handlePaymentReceived', ], 'extraCharge' => [ - SchoolEventListener::class.'@handleExtraCharge', + SchoolEventListener::class . '@handleExtraCharge', ], 'newAccountAdded' => [ - SchoolEventListener::class.'@handleNewAccountAdded', + SchoolEventListener::class . '@handleNewAccountAdded', ], 'studentRegistered' => [ - SchoolEventListener::class.'@handleStudentRegistered', + SchoolEventListener::class . '@handleStudentRegistered', ], 'delete_unverified_user' => [ - SchoolEventListener::class.'@handleDeleteUnverifiedUser', + SchoolEventListener::class . '@handleDeleteUnverifiedUser', ], 'userRegistered' => [ - SchoolEventListener::class.'@handleUserRegistered', + SchoolEventListener::class . '@handleUserRegistered', ], 'userProfileUpdated' => [ - SchoolEventListener::class.'@handleUserProfileUpdated', + SchoolEventListener::class . '@handleUserProfileUpdated', ], 'userDeactivated' => [ - SchoolEventListener::class.'@handleUserDeactivated', + SchoolEventListener::class . '@handleUserDeactivated', ], 'scoresPosted' => [ - SchoolEventListener::class.'@handleScoresPosted', + SchoolEventListener::class . '@handleScoresPosted', ], 'finalScoreReleased' => [ - SchoolEventListener::class.'@handleFinalScoreReleased', + SchoolEventListener::class . '@handleFinalScoreReleased', ], 'studentMarkedAbsent' => [ - SchoolEventListener::class.'@handleStudentMarkedAbsent', + SchoolEventListener::class . '@handleStudentMarkedAbsent', ], 'studentMarkedLate' => [ - SchoolEventListener::class.'@handleStudentMarkedLate', + SchoolEventListener::class . '@handleStudentMarkedLate', ], 'paymentMissed' => [ - SchoolEventListener::class.'@handlePaymentMissed', + SchoolEventListener::class . '@handlePaymentMissed', ], 'classScheduleUpdated' => [ - SchoolEventListener::class.'@handleClassScheduleUpdated', + SchoolEventListener::class . '@handleClassScheduleUpdated', ], 'newMessageReceived' => [ - SchoolEventListener::class.'@handleNewMessageReceived', + SchoolEventListener::class . '@handleNewMessageReceived', ], 'systemAnnouncementPosted' => [ - SchoolEventListener::class.'@handleSystemAnnouncementPosted', + SchoolEventListener::class . '@handleSystemAnnouncementPosted', ], 'customNotification' => [ - SchoolEventListener::class.'@handleCustomNotification', + SchoolEventListener::class . '@handleCustomNotification', ], DeleteUnverifiedUser::class => [ DeleteUnverifiedUserListener::class, diff --git a/app/Services/Admin/CompetitionWinners/CompetitionWinnersAdminService.php b/app/Services/Admin/CompetitionWinners/CompetitionWinnersAdminService.php index 7de223bf..f8358354 100644 --- a/app/Services/Admin/CompetitionWinners/CompetitionWinnersAdminService.php +++ b/app/Services/Admin/CompetitionWinners/CompetitionWinnersAdminService.php @@ -2,19 +2,17 @@ namespace App\Services\Admin\CompetitionWinners; -use App\Controllers\Admin\CompetitionWinnersController; use App\Models\ClassSection; use App\Models\Competition; use App\Models\CompetitionClassWinner; use App\Models\CompetitionScore; use App\Models\CompetitionWinner; use App\Models\Configuration; -use Illuminate\Support\Collection; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Schema; /** - * Administrative competition-winner workflows (ported from legacy {@see CompetitionWinnersController}). + * Administrative competition-winner workflows (ported from legacy {@see \App\Controllers\Admin\CompetitionWinnersController}). */ class CompetitionWinnersAdminService { @@ -182,7 +180,6 @@ class CompetitionWinnersAdminService if ($existing) { $existing->delete(); } - continue; } @@ -256,7 +253,7 @@ class CompetitionWinnersAdminService return $out; } - /** @return array{competitions: Collection, sectionMap: array} */ + /** @return array{competitions: \Illuminate\Support\Collection, sectionMap: array} */ public function indexData(): array { $competitions = Competition::query()->orderByDesc('id')->get(); diff --git a/app/Services/Admin/CompetitionWinners/CompetitionWinnersDomain.php b/app/Services/Admin/CompetitionWinners/CompetitionWinnersDomain.php index a895943e..f0a1922a 100644 --- a/app/Services/Admin/CompetitionWinners/CompetitionWinnersDomain.php +++ b/app/Services/Admin/CompetitionWinners/CompetitionWinnersDomain.php @@ -138,14 +138,12 @@ final class CompetitionWinnersDomain $override = $overrides[$classId] ?? null; if ($override !== null) { $map[$classId] = (int) $override; - continue; } $baseCount = $classCounts[$classId] ?? ($scoreCounts[$classId] ?? 0); if ($baseCount <= 0) { $map[$classId] = 0; - continue; } diff --git a/app/Services/Administrator/AdminNotificationSubjectService.php b/app/Services/Administrator/AdminNotificationSubjectService.php index 166a1ca4..473bd07c 100644 --- a/app/Services/Administrator/AdminNotificationSubjectService.php +++ b/app/Services/Administrator/AdminNotificationSubjectService.php @@ -9,7 +9,8 @@ class AdminNotificationSubjectService { public function __construct( protected AdminNotificationUserService $userService - ) {} + ) { + } public function subjectOptions(): array { @@ -30,7 +31,7 @@ class AdminNotificationSubjectService $assignedSubjects = []; $tableReady = DB::getSchemaBuilder()->hasTable('admin_notification_subjects'); - if ($tableReady && ! empty($admins)) { + if ($tableReady && !empty($admins)) { $adminIds = array_map('intval', array_column($admins, 'id')); $rows = AdminNotificationSubject::query() @@ -58,7 +59,7 @@ class AdminNotificationSubjectService public function save(array $posted): array { - if (! DB::getSchemaBuilder()->hasTable('admin_notification_subjects')) { + if (!DB::getSchemaBuilder()->hasTable('admin_notification_subjects')) { return [ 'success' => false, 'message' => 'Notification subject storage is missing. Run migrations first.', @@ -101,7 +102,7 @@ class AdminNotificationSubjectService if (is_array($subjectRaw)) { foreach ($subjectRaw as $key => $value) { $candidate = is_string($key) ? $key : $value; - if (! is_string($candidate)) { + if (!is_string($candidate)) { continue; } $candidate = trim($candidate); @@ -117,7 +118,7 @@ class AdminNotificationSubjectService $toDelete = array_values(array_diff($current, $selected)); $toInsert = array_values(array_diff($selected, $current)); - if (! empty($toDelete)) { + if (!empty($toDelete)) { AdminNotificationSubject::query() ->where('admin_id', $adminId) ->whereIn('subject', $toDelete) @@ -125,7 +126,7 @@ class AdminNotificationSubjectService $updates += count($toDelete); } - if (! empty($toInsert)) { + if (!empty($toInsert)) { $batch = []; foreach ($toInsert as $subject) { $batch[] = [ @@ -146,4 +147,4 @@ class AdminNotificationSubjectService 'status' => 200, ]; } -} +} \ No newline at end of file diff --git a/app/Services/Administrator/AdminNotificationUserService.php b/app/Services/Administrator/AdminNotificationUserService.php index 004216f8..4cda5390 100644 --- a/app/Services/Administrator/AdminNotificationUserService.php +++ b/app/Services/Administrator/AdminNotificationUserService.php @@ -37,7 +37,7 @@ class AdminNotificationUserService ->orderBy('u.lastname') ->orderBy('u.firstname') ->get() - ->map(fn ($r) => (array) $r) + ->map(fn($r) => (array) $r) ->all(); } -} +} \ No newline at end of file diff --git a/app/Services/Administrator/AdminPrintRecipientService.php b/app/Services/Administrator/AdminPrintRecipientService.php index 0dae8fae..da316754 100644 --- a/app/Services/Administrator/AdminPrintRecipientService.php +++ b/app/Services/Administrator/AdminPrintRecipientService.php @@ -9,7 +9,8 @@ class AdminPrintRecipientService { public function __construct( protected AdminNotificationUserService $userService - ) {} + ) { + } public function data(): array { @@ -17,7 +18,7 @@ class AdminPrintRecipientService $tableReady = DB::getSchemaBuilder()->hasTable('admin_notification_subjects'); $assigned = []; - if ($tableReady && ! empty($admins)) { + if ($tableReady && !empty($admins)) { $adminIds = array_map('intval', array_column($admins, 'id')); $rows = AdminNotificationSubject::query() @@ -43,7 +44,7 @@ class AdminPrintRecipientService public function save(array $posted): array { - if (! DB::getSchemaBuilder()->hasTable('admin_notification_subjects')) { + if (!DB::getSchemaBuilder()->hasTable('admin_notification_subjects')) { return [ 'success' => false, 'message' => 'Notification subject storage is missing. Run migrations first.', @@ -76,7 +77,7 @@ class AdminPrintRecipientService ->where('subject', 'print_requests') ->whereIn('admin_id', $adminIds) ->pluck('admin_id') - ->map(fn ($id) => (int) $id) + ->map(fn($id) => (int) $id) ->unique() ->values() ->all(); @@ -84,14 +85,14 @@ class AdminPrintRecipientService $toDelete = array_values(array_diff($current, $selected)); $toInsert = array_values(array_diff($selected, $current)); - if (! empty($toDelete)) { + if (!empty($toDelete)) { AdminNotificationSubject::query() ->where('subject', 'print_requests') ->whereIn('admin_id', $toDelete) ->delete(); } - if (! empty($toInsert)) { + if (!empty($toInsert)) { $batch = []; foreach ($toInsert as $adminId) { $batch[] = [ @@ -112,4 +113,4 @@ class AdminPrintRecipientService 'status' => 200, ]; } -} +} \ No newline at end of file diff --git a/app/Services/Administrator/AdministratorAbsenceService.php b/app/Services/Administrator/AdministratorAbsenceService.php index 87a92609..b15c51a0 100644 --- a/app/Services/Administrator/AdministratorAbsenceService.php +++ b/app/Services/Administrator/AdministratorAbsenceService.php @@ -2,12 +2,11 @@ namespace App\Services\Administrator; +use App\Services\ApplicationUrlService; +use App\Services\Staff\StaffTimeOffLinkService; use App\Models\StaffAttendance; use App\Models\User; -use App\Services\ApplicationUrlService; use App\Services\SemesterRangeService; -use App\Services\Staff\StaffTimeOffLinkService; -use Carbon\Carbon; use Illuminate\Http\Request; use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Mail; @@ -21,13 +20,14 @@ class AdministratorAbsenceService protected SemesterRangeService $semesterRangeService, protected StaffTimeOffLinkService $staffTimeOffLinkService, protected ApplicationUrlService $urls, - ) {} + ) { + } public function getAbsenceFormData(int $userId): array { $admin = $this->userModel->find($userId); $displayName = $admin - ? trim(($admin->firstname ?? '').' '.($admin->lastname ?? '')) + ? trim(($admin->firstname ?? '') . ' ' . ($admin->lastname ?? '')) : 'Administrator'; $semester = $this->shared->getSemester(); @@ -76,7 +76,7 @@ class AdministratorAbsenceService } $reasonBase = $reasonType !== '' ? strtolower($reasonType) : ''; - $reason = $reasonBase !== '' ? ($reasonBase.': '.$reasonText) : $reasonText; + $reason = $reasonBase !== '' ? ($reasonBase . ': ' . $reasonText) : $reasonText; $allowedSet = array_fill_keys($this->shared->allowedAbsenceDates(), true); @@ -97,15 +97,13 @@ class AdministratorAbsenceService } try { - $parsed = Carbon::createFromFormat('Y-m-d', $d); + $parsed = \Carbon\Carbon::createFromFormat('Y-m-d', $d); if ($parsed->format('Y-m-d') !== $d || empty($allowedSet[$d])) { $invalid[] = $d; - continue; } } catch (\Throwable) { $invalid[] = $d; - continue; } @@ -131,10 +129,10 @@ class AdministratorAbsenceService } } - if (! empty($invalid)) { + if (!empty($invalid)) { return [ 'success' => false, - 'message' => 'Invalid dates: '.implode(', ', $invalid), + 'message' => 'Invalid dates: ' . implode(', ', $invalid), 'status' => 422, ]; } @@ -152,7 +150,7 @@ class AdministratorAbsenceService return [ 'success' => true, - 'message' => $saved.' day(s) saved as absent.', + 'message' => $saved . ' day(s) saved as absent.', 'saved' => $saved, 'dates' => $savedDates, 'status' => 200, @@ -171,10 +169,10 @@ class AdministratorAbsenceService ): void { try { $user = $this->userModel->find($userId); - $fullName = trim(($user->firstname ?? '').' '.($user->lastname ?? '')) ?: 'Administrator'; + $fullName = trim(($user->firstname ?? '') . ' ' . ($user->lastname ?? '')) ?: 'Administrator'; $userEmail = $user->email ?? ''; $role = $roleName ?: 'admin'; - $dateList = ! empty($savedDates) ? implode(', ', $savedDates) : implode(', ', $dates); + $dateList = !empty($savedDates) ? implode(', ', $savedDates) : implode(', ', $dates); $subject = sprintf( 'TimeOff Request: %s (%s) — %s', @@ -186,20 +184,20 @@ class AdministratorAbsenceService $submittedAt = now()->toDateTimeString(); $body = '
' - .'

New TimeOff Request

' - .'

A staff time-off request was submitted from the administrator portal.

' - .'' - .'' - .'' - .'' - .'' - .'' - .'' - .'' - .'' - .'' - .'
Name'.e($fullName).'
Email'.e($userEmail).'
Role'.e((string) $role).'
Semester'.e($semester).'
School Year'.e($schoolYear).'
Reason Type'.e($reasonType ?: '-').'
Reason'.e($reasonText).'
Dates'.e($dateList ?: '-').'
Submitted At'.e($submittedAt).'
' - .'
'; + . '

New TimeOff Request

' + . '

A staff time-off request was submitted from the administrator portal.

' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '
Name' . e($fullName) . '
Email' . e($userEmail) . '
Role' . e((string) $role) . '
Semester' . e($semester) . '
School Year' . e($schoolYear) . '
Reason Type' . e($reasonType ?: '-') . '
Reason' . e($reasonText) . '
Dates' . e($dateList ?: '-') . '
Submitted At' . e($submittedAt) . '
' + . ''; $token = $this->staffTimeOffLinkService->createToken([ 'uid' => $userId, @@ -215,9 +213,9 @@ class AdministratorAbsenceService $notifyUrl = $this->urls->timeoffNotifyUrl($token); - $body .= '

Click Send confirmation email to ' - .e($fullName) - .' so the staff member is notified automatically. This link expires in 14 days.

'; + $body .= '

Click Send confirmation email to ' + . e($fullName) + . ' so the staff member is notified automatically. This link expires in 14 days.

'; $principalEmail = env('PRINCIPAL_EMAIL', 'principal@alrahmaisgl.org'); @@ -225,7 +223,7 @@ class AdministratorAbsenceService $message->to($principalEmail)->subject($subject); }); } catch (\Throwable $e) { - Log::error('Failed to send TimeOff email (admin): '.$e->getMessage()); + Log::error('Failed to send TimeOff email (admin): ' . $e->getMessage()); } } } diff --git a/app/Services/Administrator/AdministratorDashboardService.php b/app/Services/Administrator/AdministratorDashboardService.php index 4db14733..42261eaa 100644 --- a/app/Services/Administrator/AdministratorDashboardService.php +++ b/app/Services/Administrator/AdministratorDashboardService.php @@ -7,7 +7,8 @@ class AdministratorDashboardService public function __construct( protected AdministratorMetricsService $metricsService, protected AdministratorUserSearchService $userSearchService, - ) {} + ) { + } public function metrics(): array { @@ -18,4 +19,4 @@ class AdministratorDashboardService { return $this->userSearchService->search($query); } -} +} \ No newline at end of file diff --git a/app/Services/Administrator/AdministratorEnrollmentEventService.php b/app/Services/Administrator/AdministratorEnrollmentEventService.php index 4439f2db..957d4029 100644 --- a/app/Services/Administrator/AdministratorEnrollmentEventService.php +++ b/app/Services/Administrator/AdministratorEnrollmentEventService.php @@ -10,7 +10,8 @@ class AdministratorEnrollmentEventService { public function __construct( private ApplicationUrlService $urls, - ) {} + ) { + } public function dispatchGroupedEvents( array $groupsByParentStatus, @@ -44,7 +45,7 @@ class AdministratorEnrollmentEventService ->first(); foreach ($byStatus as $status => $studentsArr) { - if (! isset($eventMap[$status])) { + if (!isset($eventMap[$status])) { continue; } @@ -76,4 +77,4 @@ class AdministratorEnrollmentEventService } } } -} +} \ No newline at end of file diff --git a/app/Services/Administrator/AdministratorEnrollmentQueryService.php b/app/Services/Administrator/AdministratorEnrollmentQueryService.php index 2fe70265..51c0d4e7 100644 --- a/app/Services/Administrator/AdministratorEnrollmentQueryService.php +++ b/app/Services/Administrator/AdministratorEnrollmentQueryService.php @@ -17,7 +17,8 @@ class AdministratorEnrollmentQueryService protected StudentClass $studentClassModel, protected Enrollment $enrollmentModel, protected ClassSection $classSectionModel, - ) {} + ) { + } public function enrollmentWithdrawalData(?string $selectedYear = null): array { @@ -58,7 +59,7 @@ class AdministratorEnrollmentQueryService $s['student_id'] = $studentId; $s['removed_previous_year'] = isset($removedPriorIds[$studentId]) ? 'Yes' : 'No'; - if (empty($s['parent_id']) && ! empty($s['secondparent_user_id'])) { + if (empty($s['parent_id']) && !empty($s['secondparent_user_id'])) { $s['parent_id'] = (int) $s['secondparent_user_id']; } else { $s['parent_id'] = (int) ($s['parent_id'] ?? 0); @@ -67,14 +68,14 @@ class AdministratorEnrollmentQueryService $pf = trim((string) ($s['parent_firstname'] ?? '')); $pl = trim((string) ($s['parent_lastname'] ?? '')); - if ($pf === '' && $pl === '' && ! empty($s['parent_fullname'])) { + if ($pf === '' && $pl === '' && !empty($s['parent_fullname'])) { $parts = preg_split('/\s+/', trim((string) $s['parent_fullname']), 2); $pf = $parts[0] ?? ''; $pl = $parts[1] ?? ''; } - $s['parent_label'] = trim($pf.' '.$pl) ?: 'Unknown Parent'; - $s['parent_sort'] = trim(($pl !== '' ? $pl : $pf).' '.$pf) ?: 'ZZZ Unknown Parent'; + $s['parent_label'] = trim($pf . ' ' . $pl) ?: 'Unknown Parent'; + $s['parent_sort'] = trim(($pl !== '' ? $pl : $pf) . ' ' . $pf) ?: 'ZZZ Unknown Parent'; $s['is_new'] = (int) ($s['is_new'] ?? 0); $s['new_student'] = $s['is_new'] === 1 ? 'Yes' : 'No'; @@ -83,7 +84,7 @@ class AdministratorEnrollmentQueryService ? $this->enrollmentModel->getEnrollmentStatus($studentId, $selectedYear) : null; - if (! empty($statusForYear)) { + if (!empty($statusForYear)) { $s['enrollment_status'] = $statusForYear; } elseif (($s['admission_status'] ?? null) === 'denied') { $s['enrollment_status'] = 'denied'; @@ -95,7 +96,7 @@ class AdministratorEnrollmentQueryService $s['class_section'] = $className ?: 'Class not Assigned'; - $s['registration_date_order'] = ! empty($s['registration_date']) + $s['registration_date_order'] = !empty($s['registration_date']) ? Carbon::parse($s['registration_date'])->format('Y-m-d') : ''; } @@ -171,10 +172,10 @@ class AdministratorEnrollmentQueryService $r['is_new'] = (int) ($r['is_new'] ?? 0); $r['new_student'] = $r['is_new'] === 1 ? 'Yes' : 'No'; - $r['modalIdContact'] = 'contact_'.$studentId; + $r['modalIdContact'] = 'contact_' . $studentId; $r['enrollment_status'] = $enrollmentStatus; - if (! empty($r['registration_date'])) { + if (!empty($r['registration_date'])) { try { $r['registration_date'] = Carbon::parse($r['registration_date'])->format('Y-m-d'); } catch (\Throwable) { @@ -190,4 +191,4 @@ class AdministratorEnrollmentQueryService 'total_new' => count($newStudents), ]; } -} +} \ No newline at end of file diff --git a/app/Services/Administrator/AdministratorEnrollmentRefundService.php b/app/Services/Administrator/AdministratorEnrollmentRefundService.php index 6fc72392..9d36a60c 100644 --- a/app/Services/Administrator/AdministratorEnrollmentRefundService.php +++ b/app/Services/Administrator/AdministratorEnrollmentRefundService.php @@ -11,7 +11,8 @@ class AdministratorEnrollmentRefundService { public function __construct( protected FeeCalculationService $feeCalculationService - ) {} + ) { + } public function processRefunds(array $parentIds, string $schoolYear, int $editorUserId): array { @@ -35,9 +36,8 @@ class AdministratorEnrollmentRefundService ->latest('created_at') ->first(); - if (! $invoice) { + if (!$invoice) { $errors[] = "No invoice found for parent ID {$pid} (for refund calc)."; - continue; } diff --git a/app/Services/Administrator/AdministratorEnrollmentService.php b/app/Services/Administrator/AdministratorEnrollmentService.php index 89d09bb7..f2c2068f 100644 --- a/app/Services/Administrator/AdministratorEnrollmentService.php +++ b/app/Services/Administrator/AdministratorEnrollmentService.php @@ -7,7 +7,8 @@ class AdministratorEnrollmentService public function __construct( protected AdministratorEnrollmentQueryService $queryService, protected AdministratorEnrollmentStatusService $statusService, - ) {} + ) { + } public function enrollmentWithdrawalData(?string $selectedYear = null): array { diff --git a/app/Services/Administrator/AdministratorEnrollmentStatusService.php b/app/Services/Administrator/AdministratorEnrollmentStatusService.php index 90a1d5cb..cda49b10 100644 --- a/app/Services/Administrator/AdministratorEnrollmentStatusService.php +++ b/app/Services/Administrator/AdministratorEnrollmentStatusService.php @@ -15,7 +15,8 @@ class AdministratorEnrollmentStatusService protected User $userModel, protected AdministratorEnrollmentRefundService $refundService, protected AdministratorEnrollmentEventService $eventService, - ) {} + ) { + } public function updateStatuses(array $enrollmentStatuses, int $editorUserId): array { @@ -52,9 +53,8 @@ class AdministratorEnrollmentStatusService foreach ($enrollmentStatuses as $studentId => $newEnrollmentStatus) { $studentId = (int) $studentId; - if (! in_array($newEnrollmentStatus, $validStatuses, true)) { + if (!in_array($newEnrollmentStatus, $validStatuses, true)) { $errors[] = "Invalid enrollment status '{$newEnrollmentStatus}' for student ID {$studentId}."; - continue; } @@ -68,7 +68,7 @@ class AdministratorEnrollmentStatusService $refundParents ); - if (! $result['success']) { + if (!$result['success']) { $errors[] = $result['message']; } } @@ -99,7 +99,7 @@ class AdministratorEnrollmentStatusService ]; } catch (\Throwable $e) { DB::rollBack(); - Log::error('Enrollment withdrawal error: '.$e->getMessage()); + Log::error('Enrollment withdrawal error: ' . $e->getMessage()); return [ 'success' => false, @@ -129,7 +129,7 @@ class AdministratorEnrollmentStatusService ->where('school_year', $schoolYear) ->first(); - if (! $enrollmentRow) { + if (!$enrollmentRow) { return $this->createEnrollmentRow( $studentId, $newEnrollmentStatus, @@ -145,7 +145,7 @@ class AdministratorEnrollmentStatusService $oldStatus = $enrollmentRow->enrollment_status ?? null; $parentId = (int) ($enrollmentRow->parent_id ?? 0); - if (! $parentId) { + if (!$parentId) { return [ 'success' => false, 'message' => "No parent ID found for student ID {$studentId}.", @@ -165,7 +165,7 @@ class AdministratorEnrollmentStatusService 'updated_at' => now(), ]); - if (! $updated) { + if (!$updated) { return [ 'success' => false, 'message' => "Failed to update enrollment for student ID {$studentId}.", @@ -201,7 +201,7 @@ class AdministratorEnrollmentStatusService $stu = is_array($stu) ? $stu : ($stu?->toArray() ?? []); $parentId = (int) ($stu['parent_id'] ?? ($stu['secondparent_user_id'] ?? 0)); - if (! $parentId) { + if (!$parentId) { return [ 'success' => false, 'message' => "No parent ID found for student ID {$studentId}.", @@ -223,7 +223,7 @@ class AdministratorEnrollmentStatusService 'updated_at' => now(), ]); - if (! $ok) { + if (!$ok) { return [ 'success' => false, 'message' => "Failed to create enrollment for student ID {$studentId}.", @@ -250,7 +250,7 @@ class AdministratorEnrollmentStatusService $studentRow = $this->studentModel->find($studentId); $studentData = is_array($studentRow) ? $studentRow : ($studentRow?->toArray() ?? []); - return trim(($studentData['firstname'] ?? '').' '.($studentData['lastname'] ?? '')) + return trim(($studentData['firstname'] ?? '') . ' ' . ($studentData['lastname'] ?? '')) ?: "Student #{$studentId}"; } diff --git a/app/Services/Administrator/AdministratorMetricsService.php b/app/Services/Administrator/AdministratorMetricsService.php index cfa99e28..7675461d 100644 --- a/app/Services/Administrator/AdministratorMetricsService.php +++ b/app/Services/Administrator/AdministratorMetricsService.php @@ -12,7 +12,8 @@ class AdministratorMetricsService protected AdministratorSharedService $shared, protected User $userModel, protected LoginActivity $loginActivityModel, - ) {} + ) { + } public function metrics(): array { @@ -69,4 +70,4 @@ class AdministratorMetricsService ], ]; } -} +} \ No newline at end of file diff --git a/app/Services/Administrator/AdministratorNotificationService.php b/app/Services/Administrator/AdministratorNotificationService.php index 42494180..acfd591e 100644 --- a/app/Services/Administrator/AdministratorNotificationService.php +++ b/app/Services/Administrator/AdministratorNotificationService.php @@ -7,7 +7,8 @@ class AdministratorNotificationService public function __construct( protected AdminNotificationSubjectService $subjectService, protected AdminPrintRecipientService $printRecipientService, - ) {} + ) { + } public function notificationsAlertsData(): array { @@ -28,4 +29,4 @@ class AdministratorNotificationService { return $this->printRecipientService->save($posted); } -} +} \ No newline at end of file diff --git a/app/Services/Administrator/AdministratorSharedService.php b/app/Services/Administrator/AdministratorSharedService.php index f615046d..25d1f565 100644 --- a/app/Services/Administrator/AdministratorSharedService.php +++ b/app/Services/Administrator/AdministratorSharedService.php @@ -9,7 +9,8 @@ class AdministratorSharedService { public function __construct( protected Configuration $configuration - ) {} + ) { + } public function getSemester(?string $override = null): string { @@ -34,7 +35,7 @@ class AdministratorSharedService } if (preg_match('/^(\d{4})\s*-\s*(\d{4})$/', $schoolYear, $m)) { - return ((int) $m[1] - 1).'-'.((int) $m[2] - 1); + return ((int) $m[1] - 1) . '-' . ((int) $m[2] - 1); } if (preg_match('/^(\d{4})\s*-\s*(\d{2})$/', $schoolYear, $m)) { @@ -43,7 +44,6 @@ class AdministratorSharedService if ($end < 0) { $end += 100; } - return sprintf('%04d-%02d', $start, $end); } @@ -126,7 +126,7 @@ class AdministratorSharedService public function countUniqueEntities($rows): int { - if (! is_iterable($rows)) { + if (!is_iterable($rows)) { return 0; } diff --git a/app/Services/Administrator/AdministratorTeacherSubmissionService.php b/app/Services/Administrator/AdministratorTeacherSubmissionService.php index c49ba590..5ccc2af9 100644 --- a/app/Services/Administrator/AdministratorTeacherSubmissionService.php +++ b/app/Services/Administrator/AdministratorTeacherSubmissionService.php @@ -9,7 +9,8 @@ class AdministratorTeacherSubmissionService public function __construct( protected TeacherSubmissionReportService $reportService, protected TeacherSubmissionNotificationService $notificationService, - ) {} + ) { + } public function report(array $filters = []): array { diff --git a/app/Services/Administrator/AdministratorUserSearchService.php b/app/Services/Administrator/AdministratorUserSearchService.php index 06a31c50..507bc29d 100644 --- a/app/Services/Administrator/AdministratorUserSearchService.php +++ b/app/Services/Administrator/AdministratorUserSearchService.php @@ -38,23 +38,23 @@ class AdministratorUserSearchService $v[] = sprintf('(%s)-%s-%s', substr($digits, 0, 3), substr($digits, 3, 3), substr($digits, 6)); $v[] = sprintf('%s-%s-%s', substr($digits, 0, 3), substr($digits, 3, 3), substr($digits, 6)); $v[] = sprintf('%s %s %s', substr($digits, 0, 3), substr($digits, 3, 3), substr($digits, 6)); - $v[] = '1'.$digits; - $v[] = '+1'.$digits; - $v[] = '+1 '.sprintf('(%s) %s-%s', substr($digits, 0, 3), substr($digits, 3, 3), substr($digits, 6)); - $v[] = '+1-'.sprintf('%s-%s-%s', substr($digits, 0, 3), substr($digits, 3, 3), substr($digits, 6)); + $v[] = '1' . $digits; + $v[] = '+1' . $digits; + $v[] = '+1 ' . sprintf('(%s) %s-%s', substr($digits, 0, 3), substr($digits, 3, 3), substr($digits, 6)); + $v[] = '+1-' . sprintf('%s-%s-%s', substr($digits, 0, 3), substr($digits, 3, 3), substr($digits, 6)); } elseif (strlen($digits) === 11 && str_starts_with($digits, '1')) { $ten = substr($digits, 1); $v[] = $ten; $v[] = sprintf('(%s)-%s-%s', substr($ten, 0, 3), substr($ten, 3, 3), substr($ten, 6)); $v[] = sprintf('%s-%s-%s', substr($ten, 0, 3), substr($ten, 3, 3), substr($ten, 6)); $v[] = sprintf('%s %s %s', substr($ten, 0, 3), substr($ten, 3, 3), substr($ten, 6)); - $v[] = '+1'.$ten; - $v[] = '+1 '.sprintf('(%s) %s-%s', substr($ten, 0, 3), substr($ten, 3, 3), substr($ten, 6)); - $v[] = '+1-'.sprintf('%s-%s-%s', substr($ten, 0, 3), substr($ten, 3, 3), substr($ten, 6)); + $v[] = '+1' . $ten; + $v[] = '+1 ' . sprintf('(%s) %s-%s', substr($ten, 0, 3), substr($ten, 3, 3), substr($ten, 6)); + $v[] = '+1-' . sprintf('%s-%s-%s', substr($ten, 0, 3), substr($ten, 3, 3), substr($ten, 6)); } } - if (! empty($v)) { + if (!empty($v)) { $phoneMap[$t] = array_values(array_unique($v)); } } @@ -63,13 +63,13 @@ class AdministratorUserSearchService foreach ($tokens as $t) { $qb->where(function ($sub) use ($columns, $phoneCols, $phoneMap, $t) { foreach ($columns as $col) { - $sub->orWhere($col, 'like', '%'.$t.'%'); + $sub->orWhere($col, 'like', '%' . $t . '%'); } - if (! empty($phoneMap[$t]) && ! empty($phoneCols)) { + if (!empty($phoneMap[$t]) && !empty($phoneCols)) { foreach ($phoneMap[$t] as $pv) { foreach ($phoneCols as $pcol) { - $sub->orWhere($pcol, 'like', '%'.$pv.'%'); + $sub->orWhere($pcol, 'like', '%' . $pv . '%'); } } } @@ -82,12 +82,12 @@ class AdministratorUserSearchService $uQB = DB::table('users') ->select('id', 'firstname', 'lastname', 'email', 'cellphone', 'school_id', 'city', 'state', 'school_year', 'semester'); $applyMultiTokenLike($uQB, ['firstname', 'lastname', 'email', 'cellphone', 'school_id', 'city', 'state'], $tokens, ['cellphone']); - $users = $uQB->limit(150)->get()->map(fn ($r) => (array) $r)->all(); + $users = $uQB->limit(150)->get()->map(fn($r) => (array) $r)->all(); $sQB = DB::table('students') ->select('id', 'parent_id', 'school_id', 'firstname', 'lastname', 'dob', 'gender', 'school_year', 'semester', 'rfid_tag'); $applyMultiTokenLike($sQB, ['firstname', 'lastname', 'school_id', 'rfid_tag', 'dob', 'gender'], $tokens); - $students = $sQB->limit(150)->get()->map(fn ($r) => (array) $r)->all(); + $students = $sQB->limit(150)->get()->map(fn($r) => (array) $r)->all(); $pQB = DB::table('parents') ->select('id', 'firstparent_id', 'secondparent_firstname', 'secondparent_lastname', 'secondparent_email', 'secondparent_phone', 'school_year', 'semester'); @@ -99,17 +99,17 @@ class AdministratorUserSearchService } } - $parents = $pQB->limit(150)->get()->map(fn ($r) => (array) $r)->all(); + $parents = $pQB->limit(150)->get()->map(fn($r) => (array) $r)->all(); $stQB = DB::table('staff') ->select('id', 'user_id', 'firstname', 'lastname', 'email', 'phone', 'role_name', 'school_year', 'active_role'); $applyMultiTokenLike($stQB, ['firstname', 'lastname', 'email', 'role_name', 'phone'], $tokens, ['phone']); - $staff = $stQB->limit(150)->get()->map(fn ($r) => (array) $r)->all(); + $staff = $stQB->limit(150)->get()->map(fn($r) => (array) $r)->all(); $ecQB = DB::table('emergency_contacts') ->select('id', 'parent_id', 'emergency_contact_name', 'relation', 'cellphone', 'email', 'school_year', 'semester'); $applyMultiTokenLike($ecQB, ['emergency_contact_name', 'relation', 'email', 'cellphone'], $tokens, ['cellphone']); - $emergency = $ecQB->limit(150)->get()->map(fn ($r) => (array) $r)->all(); + $emergency = $ecQB->limit(150)->get()->map(fn($r) => (array) $r)->all(); return [ 'query' => $q, @@ -125,4 +125,4 @@ class AdministratorUserSearchService 'total_found' => count($users) + count($students) + count($parents) + count($staff) + count($emergency), ]; } -} +} \ No newline at end of file diff --git a/app/Services/Administrator/TeacherSubmissionNotificationService.php b/app/Services/Administrator/TeacherSubmissionNotificationService.php index debe4769..171fab54 100644 --- a/app/Services/Administrator/TeacherSubmissionNotificationService.php +++ b/app/Services/Administrator/TeacherSubmissionNotificationService.php @@ -3,10 +3,10 @@ namespace App\Services\Administrator; use App\Mail\TeacherSubmissionReminderMail; +use App\Services\ApplicationUrlService; use App\Models\ClassSection; use App\Models\TeacherSubmissionNotificationHistory; use App\Models\User; -use App\Services\ApplicationUrlService; use Illuminate\Http\Request; use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Mail; @@ -17,12 +17,13 @@ class TeacherSubmissionNotificationService protected AdministratorSharedService $shared, protected TeacherSubmissionSupportService $support, protected ApplicationUrlService $urls, - ) {} + ) { + } public function send(Request $request, int $adminId): array { $notify = $request->input('notify'); - if (! is_array($notify)) { + if (!is_array($notify)) { return [ 'success' => false, 'message' => 'Select at least one teacher to notify.', @@ -59,7 +60,7 @@ class TeacherSubmissionNotificationService ->keyBy('id'); $adminUser = User::find($adminId); - $adminName = trim(($adminUser->firstname ?? '').' '.($adminUser->lastname ?? '')) ?: 'Administrator'; + $adminName = trim(($adminUser->firstname ?? '') . ' ' . ($adminUser->lastname ?? '')) ?: 'Administrator'; $scoreUrl = $this->urls->docsHomeUrl(); $sentCount = 0; @@ -71,30 +72,30 @@ class TeacherSubmissionNotificationService $teacher = $teachers->get($teacherId); $sectionName = $classSections->get($classSectionId)?->class_section_name ?? "Section {$classSectionId}"; - $teacherName = trim(($teacher->firstname ?? '').' '.($teacher->lastname ?? '')) ?: 'Teacher'; + $teacherName = trim(($teacher->firstname ?? '') . ' ' . ($teacher->lastname ?? '')) ?: 'Teacher'; $missingItems = $this->resolveMissingItems($missingItemsPayload, $classSectionId, $teacherId); - $missingNote = ! empty($missingItems) - ? '

Outstanding items: '.e($this->support->formatMissingItemsText($missingItems)).'.

' + $missingNote = !empty($missingItems) + ? '

Outstanding items: ' . e($this->support->formatMissingItemsText($missingItems)) . '.

' : '

Our records show no outstanding submissions for this section, but please verify if anything still needs attention.

'; $subject = "Reminder: Complete submissions for {$sectionName}"; $body = "

Dear {$teacherName},

" - ."

Administration is gently reminding you to wrap up any remaining score submissions and/or comments for {$sectionName}.

" - .$missingNote - ."

Visit Teacher Score Submission to address any remaining items.

" - ."

Thank you,
{$adminName}

"; + . "

Administration is gently reminding you to wrap up any remaining score submissions and/or comments for {$sectionName}.

" + . $missingNote + . "

Visit Teacher Score Submission to address any remaining items.

" + . "

Thank you,
{$adminName}

"; $email = $teacher->email ?? ''; $status = 'failed'; - if (! empty($email) && filter_var($email, FILTER_VALIDATE_EMAIL)) { + if (!empty($email) && filter_var($email, FILTER_VALIDATE_EMAIL)) { try { Mail::to($email)->send(new TeacherSubmissionReminderMail($subject, $body)); $status = 'sent'; } catch (\Throwable $e) { - Log::error('Teacher submission notification failed: '.$e->getMessage()); + Log::error('Teacher submission notification failed: ' . $e->getMessage()); } } @@ -119,15 +120,15 @@ class TeacherSubmissionNotificationService $parts = []; if ($sentCount > 0) { - $parts[] = $sentCount.' reminder'.($sentCount === 1 ? '' : 's').' sent'; + $parts[] = $sentCount . ' reminder' . ($sentCount === 1 ? '' : 's') . ' sent'; } if ($failCount > 0) { - $parts[] = $failCount.' reminder'.($failCount === 1 ? '' : 's').' failed'; + $parts[] = $failCount . ' reminder' . ($failCount === 1 ? '' : 's') . ' failed'; } return [ 'success' => $failCount === 0, - 'message' => ! empty($parts) ? implode(' and ', $parts) : 'No notifications were sent.', + 'message' => !empty($parts) ? implode(' and ', $parts) : 'No notifications were sent.', 'sent' => $sentCount, 'failed' => $failCount, 'status' => $failCount === 0 ? 200 : 207, @@ -163,7 +164,7 @@ class TeacherSubmissionNotificationService foreach ($notify as $sectionIdRaw => $teachers) { $sectionId = (int) $sectionIdRaw; - if ($sectionId <= 0 || ! is_array($teachers)) { + if ($sectionId <= 0 || !is_array($teachers)) { continue; } @@ -196,7 +197,7 @@ class TeacherSubmissionNotificationService protected function resolveMissingItems($payload, int $classSectionId, int $teacherId): array { - if (! is_array($payload)) { + if (!is_array($payload)) { return []; } diff --git a/app/Services/Administrator/TeacherSubmissionReportService.php b/app/Services/Administrator/TeacherSubmissionReportService.php index 898004ba..9bf11f26 100644 --- a/app/Services/Administrator/TeacherSubmissionReportService.php +++ b/app/Services/Administrator/TeacherSubmissionReportService.php @@ -14,7 +14,8 @@ class TeacherSubmissionReportService public function __construct( protected AdministratorSharedService $shared, protected TeacherSubmissionSupportService $support - ) {} + ) { + } public function report(array $filters = []): array { @@ -55,14 +56,14 @@ class TeacherSubmissionReportService default => $roleKey !== '' ? ucfirst($roleKey) : 'Teacher', }; - $teacherFullName = trim(($assignment['firstname'] ?? '').' '.($assignment['lastname'] ?? '')); + $teacherFullName = trim(($assignment['firstname'] ?? '') . ' ' . ($assignment['lastname'] ?? '')); $teacherId = (int) ($assignment['teacher_id'] ?? 0); if ($teacherFullName === '' || $teacherId <= 0) { continue; } - if (! isset($teachersBySection[$sectionId])) { + if (!isset($teachersBySection[$sectionId])) { $teachersBySection[$sectionId] = [ 'class_section' => $assignment['class_section_name'] ?? "Section {$sectionId}", 'teachers' => [], @@ -112,7 +113,7 @@ class TeacherSubmissionReportService foreach ($scoreRecords as $score) { $sid = (int) ($score->student_id ?? 0); - if ($sid <= 0 || ($expected > 0 && ! in_array($sid, $studentIds, true))) { + if ($sid <= 0 || ($expected > 0 && !in_array($sid, $studentIds, true))) { continue; } @@ -128,7 +129,7 @@ class TeacherSubmissionReportService $midtermCommentStudents = []; $ptapCommentStudents = []; - if (! empty($studentIds)) { + if (!empty($studentIds)) { $comments = ScoreComment::query() ->select('student_id', 'score_type', 'comment') ->whereIn('student_id', $studentIds) @@ -176,7 +177,7 @@ class TeacherSubmissionReportService ); foreach ($teacherList as $teacherEntry) { - if (! empty($teacherEntry['id'])) { + if (!empty($teacherEntry['id'])) { $allTeacherIds[] = $teacherEntry['id']; } } @@ -263,7 +264,7 @@ class TeacherSubmissionReportService continue; } - $adminName = trim(($record->firstname ?? '').' '.($record->lastname ?? '')); + $adminName = trim(($record->firstname ?? '') . ' ' . ($record->lastname ?? '')); if ($adminName === '') { $adminName = 'Administrator'; } diff --git a/app/Services/Administrator/TeacherSubmissionSupportService.php b/app/Services/Administrator/TeacherSubmissionSupportService.php index 1ea89741..a2d242c1 100644 --- a/app/Services/Administrator/TeacherSubmissionSupportService.php +++ b/app/Services/Administrator/TeacherSubmissionSupportService.php @@ -54,7 +54,7 @@ class TeacherSubmissionSupportService $items = []; foreach ($statusMap as $key => $status) { - if (! (bool) ($status['completed'] ?? true) && isset($labels[$key])) { + if (!(bool) ($status['completed'] ?? true) && isset($labels[$key])) { $items[] = $labels[$key]; } } @@ -80,7 +80,7 @@ class TeacherSubmissionSupportService return mb_strlen($text) <= $limit ? $text - : mb_substr($text, 0, $limit).'…'; + : mb_substr($text, 0, $limit) . '…'; } public function parseMissingItemsPayload($payload): array @@ -103,7 +103,7 @@ class TeacherSubmissionSupportService } $decoded = json_decode(base64_decode($payload, true) ?: '', true); - if (! is_array($decoded)) { + if (!is_array($decoded)) { return []; } @@ -130,11 +130,10 @@ class TeacherSubmissionSupportService return $items[0]; } if ($count === 2) { - return $items[0].' and '.$items[1]; + return $items[0] . ' and ' . $items[1]; } $last = array_pop($items); - - return implode(', ', $items).' and '.$last; + return implode(', ', $items) . ' and ' . $last; } } diff --git a/app/Services/Administrator/Trophy/TrophyReportService.php b/app/Services/Administrator/Trophy/TrophyReportService.php index f3646853..84bb2825 100644 --- a/app/Services/Administrator/Trophy/TrophyReportService.php +++ b/app/Services/Administrator/Trophy/TrophyReportService.php @@ -305,7 +305,7 @@ class TrophyReportService } /** - * @return Collection> + * @return \Illuminate\Support\Collection> */ private function studentRows(string $schoolYear): Collection { @@ -393,7 +393,7 @@ class TrophyReportService } /** - * @param Collection> $rows + * @param \Illuminate\Support\Collection> $rows * @return array>}> */ private function sectionMap(Collection $rows): array diff --git a/app/Services/ApplicationUrlService.php b/app/Services/ApplicationUrlService.php index a175d085..d30ad0f5 100644 --- a/app/Services/ApplicationUrlService.php +++ b/app/Services/ApplicationUrlService.php @@ -2,7 +2,6 @@ namespace App\Services; -use App\Http\Controllers\Api\Reports\FilesController; use Illuminate\Support\Facades\Route; /** @@ -139,10 +138,10 @@ final class ApplicationUrlService return route('api.timeoff.notify', ['token' => $token]); } - /** Served by {@see FilesController::receipt} (uploads/receipts). */ + /** Served by {@see \App\Http\Controllers\Api\Reports\FilesController::receipt} (uploads/receipts). */ public function forReceiptStorage(?string $filename): ?string { - if (! $filename) { + if (!$filename) { return null; } $safe = basename(trim($filename)); @@ -150,10 +149,10 @@ final class ApplicationUrlService return $safe !== '' ? route('api.v1.files.receipt', ['name' => $safe]) : null; } - /** Served by {@see FilesController::reimb} (uploads/reimbursements). */ + /** Served by {@see \App\Http\Controllers\Api\Reports\FilesController::reimb} (uploads/reimbursements). */ public function forReimbursementStorage(?string $filename): ?string { - if (! $filename) { + if (!$filename) { return null; } $safe = basename(trim($filename)); diff --git a/app/Services/Assignment/AssignmentDataLoaderService.php b/app/Services/Assignment/AssignmentDataLoaderService.php index 7fcbdacb..aea5efed 100644 --- a/app/Services/Assignment/AssignmentDataLoaderService.php +++ b/app/Services/Assignment/AssignmentDataLoaderService.php @@ -31,7 +31,7 @@ class AssignmentDataLoaderService ->get() ->map(function ($row) { $row->teacher_full_name = trim( - ((string) optional($row->teacher)->firstname).' '. + ((string) optional($row->teacher)->firstname) . ' ' . ((string) optional($row->teacher)->lastname) ); diff --git a/app/Services/Assignment/AssignmentSectionService.php b/app/Services/Assignment/AssignmentSectionService.php index 5db2b2db..2c345fe6 100644 --- a/app/Services/Assignment/AssignmentSectionService.php +++ b/app/Services/Assignment/AssignmentSectionService.php @@ -20,7 +20,6 @@ class AssignmentSectionService ->get() ->mapWithKeys(function ($section) { $name = $section->class_section_name ?? $section->section_name ?? $section->name ?? ''; - return [(int) $section->class_section_id => (string) $name]; }) ->all(); diff --git a/app/Services/Assignment/AssignmentService.php b/app/Services/Assignment/AssignmentService.php index 67127e05..f3d59b0d 100644 --- a/app/Services/Assignment/AssignmentService.php +++ b/app/Services/Assignment/AssignmentService.php @@ -93,7 +93,7 @@ class AssignmentService ]); $students = $studentRows - ->filter(fn ($row) => ! empty($row->student)) + ->filter(fn ($row) => !empty($row->student)) ->map(function ($row) { $student = $row->student; @@ -122,10 +122,10 @@ class AssignmentService 'description' => (string) $descriptionMeta, ]; }) - ->filter() - ->sortBy('class_section_name', SORT_NATURAL | SORT_FLAG_CASE) - ->values() - ->all(); + ->filter() + ->sortBy('class_section_name', SORT_NATURAL | SORT_FLAG_CASE) + ->values() + ->all(); return [ 'classSections' => $classSections, @@ -220,7 +220,7 @@ class AssignmentService ]); $students = $studentRows - ->filter(fn ($row) => ! empty($row->student)) + ->filter(fn ($row) => !empty($row->student)) ->map(function ($row) { $student = $row->student; @@ -249,10 +249,10 @@ class AssignmentService 'description' => (string) $descriptionMeta, ]; }) - ->filter() - ->sortBy('class_section_name', SORT_NATURAL | SORT_FLAG_CASE) - ->values() - ->all(); + ->filter() + ->sortBy('class_section_name', SORT_NATURAL | SORT_FLAG_CASE) + ->values() + ->all(); return [ 'classSections' => $classSections, diff --git a/app/Services/AssignmentService.php b/app/Services/AssignmentService.php index 7b18c92b..e1f3985a 100644 --- a/app/Services/AssignmentService.php +++ b/app/Services/AssignmentService.php @@ -4,4 +4,6 @@ namespace App\Services; use App\Services\Assignment\AssignmentService as AssignmentDomainService; -class AssignmentService extends AssignmentDomainService {} +class AssignmentService extends AssignmentDomainService +{ +} diff --git a/app/Services/Attendance/AttendanceAutoPublishJobService.php b/app/Services/Attendance/AttendanceAutoPublishJobService.php index e4d23f11..88e763af 100644 --- a/app/Services/Attendance/AttendanceAutoPublishJobService.php +++ b/app/Services/Attendance/AttendanceAutoPublishJobService.php @@ -3,11 +3,14 @@ namespace App\Services\Attendance; use DateTimeImmutable; +use DateTimeZone; use Illuminate\Support\Facades\DB; class AttendanceAutoPublishJobService { - public function __construct(private AttendanceAutoPublishService $autoPublish) {} + public function __construct(private AttendanceAutoPublishService $autoPublish) + { + } public function run(?DateTimeImmutable $now = null): array { diff --git a/app/Services/Attendance/AttendanceAutoPublishService.php b/app/Services/Attendance/AttendanceAutoPublishService.php index f856bf27..c595e1e8 100644 --- a/app/Services/Attendance/AttendanceAutoPublishService.php +++ b/app/Services/Attendance/AttendanceAutoPublishService.php @@ -2,7 +2,6 @@ namespace App\Services\Attendance; -use Config\School; use DateTimeImmutable; use DateTimeZone; @@ -10,7 +9,7 @@ class AttendanceAutoPublishService { public function timezone(?string $tzFromCfg = null): DateTimeZone { - $tz = $tzFromCfg ?: (class_exists(School::class) + $tz = $tzFromCfg ?: (class_exists(\Config\School::class) ? (config('School')->attendance['timezone'] ?? null) : null); @@ -20,7 +19,7 @@ class AttendanceAutoPublishService public function secondSundayAfterEndOfDay(string $ymd, ?string $tz = null): string { $zone = $this->timezone($tz); - $day = new DateTimeImmutable($ymd.' 00:00:00', $zone); + $day = new DateTimeImmutable($ymd . ' 00:00:00', $zone); $weekday = (int) $day->format('w'); $daysToNextSunday = (7 - $weekday) % 7; diff --git a/app/Services/Attendance/AttendanceCommentService.php b/app/Services/Attendance/AttendanceCommentService.php index ad651af7..87045929 100644 --- a/app/Services/Attendance/AttendanceCommentService.php +++ b/app/Services/Attendance/AttendanceCommentService.php @@ -29,13 +29,13 @@ class AttendanceCommentService $lastChar = $name !== '' ? substr($name, -1) : ''; $namePossessive = ($name === 'This student') ? "This student's" - : ($lastChar === 's' || $lastChar === 'S' ? ($name."'") : ($name."'s")); + : ($lastChar === 's' || $lastChar === 'S' ? ($name . "'") : ($name . "'s")); if (strpos($text, '{name}') !== false) { return str_replace('{name}', $namePossessive, $text); } - return $namePossessive.' '.$text; + return $namePossessive . ' ' . $text; } public function templateForScore(float $score): ?array diff --git a/app/Services/Attendance/AttendanceConsequenceService.php b/app/Services/Attendance/AttendanceConsequenceService.php index e5feb389..6551d529 100644 --- a/app/Services/Attendance/AttendanceConsequenceService.php +++ b/app/Services/Attendance/AttendanceConsequenceService.php @@ -12,7 +12,8 @@ class AttendanceConsequenceService public function __construct( private EmailService $emailService, private UserNotificationDispatchService $notifier - ) {} + ) { + } public function sendFollowUp(array $payload): array { @@ -54,10 +55,10 @@ class AttendanceConsequenceService string $_viewName, string $logLevel ): array { - $studentName = trim((string) ($payload['student']['firstname'] ?? '').' '.(string) ($payload['student']['lastname'] ?? '')); + $studentName = trim((string) ($payload['student']['firstname'] ?? '') . ' ' . (string) ($payload['student']['lastname'] ?? '')); $parentEmail = (string) ($payload['parent']['email'] ?? ''); $parentName = (string) ($payload['parent']['name'] ?? ''); - $teacherName = trim((string) ($payload['teacher']['firstname'] ?? '').' '.(string) ($payload['teacher']['lastname'] ?? '')); + $teacherName = trim((string) ($payload['teacher']['firstname'] ?? '') . ' ' . (string) ($payload['teacher']['lastname'] ?? '')); $className = (string) ($payload['class']['class_section_name'] ?? ''); $semester = (string) ($payload['semester'] ?? ''); $schoolYear = (string) ($payload['school_year'] ?? ''); @@ -68,7 +69,6 @@ class AttendanceConsequenceService Log::warning("Attendance {$type}: missing parent email", [ 'student_id' => $payload['student_id'] ?? null, ]); - return ['ok' => false, 'message' => 'Missing parent email.']; } @@ -76,21 +76,21 @@ class AttendanceConsequenceService $html = trim((string) ($payload['html'] ?? '')); if ($html === '') { - $period = trim($semester.' '.$schoolYear); - $html = '

'.e($subject).'

' - .'

Student: '.e($studentName).'

' - .'

Parent: '.e($parentName).'

' - .'

Teacher: '.e($teacherName).'

' - .'

Class: '.e($className).'

' - .($period !== '' ? '

Term: '.e($period).'

' : '') - .'

Date: '.e($dateFmt).'

'; + $period = trim($semester . ' ' . $schoolYear); + $html = '

' . e($subject) . '

' + . '

Student: ' . e($studentName) . '

' + . '

Parent: ' . e($parentName) . '

' + . '

Teacher: ' . e($teacherName) . '

' + . '

Class: ' . e($className) . '

' + . ($period !== '' ? '

Term: ' . e($period) . '

' : '') + . '

Date: ' . e($dateFmt) . '

'; } $ok = false; try { $ok = $this->emailService->send($parentEmail, $subject, $html, 'attendance'); } catch (\Throwable $e) { - Log::error('Attendance email send failed: '.$e->getMessage()); + Log::error('Attendance email send failed: ' . $e->getMessage()); } $summary = $this->buildInAppSummary($type, $studentName, $dateFmt); @@ -106,11 +106,11 @@ class AttendanceConsequenceService ); } - if (! empty($payload['tracking_id'])) { + if (!empty($payload['tracking_id'])) { AttendanceTracking::markAsNotified((int) $payload['tracking_id']); } - Log::log($logLevel, "Attendance {$type} email ".($ok ? 'sent' : 'failed'), [ + Log::log($logLevel, "Attendance {$type} email " . ($ok ? 'sent' : 'failed'), [ 'student_id' => $payload['student_id'] ?? null, 'recipient' => $parentEmail, ]); @@ -122,8 +122,7 @@ class AttendanceConsequenceService { $name = $studentName !== '' ? " — {$studentName}" : ''; $date = $dateFmt !== '' ? " ({$dateFmt})" : ''; - - return $base.$name.$date; + return $base . $name . $date; } private function buildInAppSummary(string $type, string $studentName, string $dateFmt): string diff --git a/app/Services/Attendance/AttendanceDailySummaryService.php b/app/Services/Attendance/AttendanceDailySummaryService.php index 76879333..0a838a2c 100644 --- a/app/Services/Attendance/AttendanceDailySummaryService.php +++ b/app/Services/Attendance/AttendanceDailySummaryService.php @@ -11,19 +11,18 @@ class AttendanceDailySummaryService public function __construct( private UserNotificationDispatchService $notifier, private EmailService $emailService - ) {} + ) { + } public function sendAbsenteesSummary(): int { $rows = $this->fetchTodaySummary('absent'); - return $this->notifyParents($rows, 'absent'); } public function sendLatesSummary(): int { $rows = $this->fetchTodaySummary('late'); - return $this->notifyParents($rows, 'late'); } @@ -50,12 +49,12 @@ class AttendanceDailySummaryService if ($type === 'absent') { $query->where(function ($w) { - $w->whereIn(DB::raw('UPPER(TRIM(ad.status))'), ['ABSENT', 'ABS', 'A']) + $w->whereIn(DB::raw("UPPER(TRIM(ad.status))"), ['ABSENT', 'ABS', 'A']) ->orWhereRaw("UPPER(TRIM(ad.status)) LIKE 'ABS%'"); }); } else { $query->where(function ($w) { - $w->whereIn(DB::raw('UPPER(TRIM(ad.status))'), ['LATE', 'L']) + $w->whereIn(DB::raw("UPPER(TRIM(ad.status))"), ['LATE', 'L']) ->orWhereRaw("UPPER(TRIM(ad.status)) LIKE 'LATE%'"); }); } @@ -72,7 +71,7 @@ class AttendanceDailySummaryService continue; } - $studentName = trim((string) ($row['student_firstname'] ?? '').' '.(string) ($row['student_lastname'] ?? '')); + $studentName = trim((string) ($row['student_firstname'] ?? '') . ' ' . (string) ($row['student_lastname'] ?? '')); $date = substr((string) ($row['date'] ?? ''), 0, 10); $message = sprintf( @@ -86,7 +85,7 @@ class AttendanceDailySummaryService $email = (string) ($row['parent_email'] ?? ''); if ($email !== '') { - $body = '

'.e($message).'

'; + $body = '

' . e($message) . '

'; $this->emailService->send($email, 'Daily Attendance Update', $body, 'attendance'); } diff --git a/app/Services/Attendance/AttendancePolicyService.php b/app/Services/Attendance/AttendancePolicyService.php index 62ab27ce..af94f50f 100644 --- a/app/Services/Attendance/AttendancePolicyService.php +++ b/app/Services/Attendance/AttendancePolicyService.php @@ -3,14 +3,15 @@ namespace App\Services\Attendance; use App\Models\Role; +use Illuminate\Contracts\Auth\Authenticatable; class AttendancePolicyService { public function canEditDay(array $dayRow, array $currentUser): bool { - $status = strtolower((string) ($dayRow['status'] ?? 'draft')); - $roles = array_map([$this, 'normalizeRole'], (array) ($currentUser['roles'] ?? [])); - $permissions = array_map('strtolower', (array) ($currentUser['permissions'] ?? [])); + $status = strtolower((string)($dayRow['status'] ?? 'draft')); + $roles = array_map([$this, 'normalizeRole'], (array)($currentUser['roles'] ?? [])); + $permissions = array_map('strtolower', (array)($currentUser['permissions'] ?? [])); $isAdmin = $this->isAdminLike($roles, $permissions); $isTeacher = in_array('teacher', $roles, true) || in_array('ta', $roles, true) || in_array('teacher_assistant', $roles, true); @@ -33,7 +34,6 @@ class AttendancePolicyService public function isTeacher(array $roles): bool { $roles = array_map([$this, 'normalizeRole'], $roles); - return in_array('teacher', $roles, true) || in_array('ta', $roles, true) || in_array('teacher_assistant', $roles, true) @@ -45,7 +45,7 @@ class AttendancePolicyService $roles = array_map([$this, 'normalizeRole'], $roles); $adminTokens = $this->adminRoleTokens(); - if (! empty($adminTokens) && count(array_intersect($roles, $adminTokens)) > 0) { + if (!empty($adminTokens) && count(array_intersect($roles, $adminTokens)) > 0) { return true; } @@ -61,7 +61,7 @@ class AttendancePolicyService public function normalizeRole(?string $role): string { - return str_replace([' ', '-'], '_', strtolower(trim((string) $role))); + return str_replace([' ', '-'], '_', strtolower(trim((string)$role))); } public function canManageEarlyDismissals(array $userRoles): bool @@ -123,7 +123,6 @@ class AttendancePolicyService } $cache = array_keys($tokens); - return $cache; } } diff --git a/app/Services/Attendance/AttendanceQueryService.php b/app/Services/Attendance/AttendanceQueryService.php index 7af2d11c..0ce9ebc1 100644 --- a/app/Services/Attendance/AttendanceQueryService.php +++ b/app/Services/Attendance/AttendanceQueryService.php @@ -42,7 +42,7 @@ class AttendanceQueryService $sectionCodes = array_keys($bySection); $labels = []; - if (! empty($sectionCodes)) { + if (!empty($sectionCodes)) { $rows = $this->classSection ->query() ->select(['class_section_id', 'class_section_name']) @@ -52,13 +52,13 @@ class AttendanceQueryService ->toArray(); foreach ($rows as $row) { - $code = (int) $row['class_section_id']; - $name = trim((string) ($row['class_section_name'] ?? '')); - $labels[$code] = $name !== '' ? $name : 'Section #'.$code; + $code = (int)$row['class_section_id']; + $name = trim((string)($row['class_section_name'] ?? '')); + $labels[$code] = $name !== '' ? $name : 'Section #' . $code; } foreach ($sectionCodes as $code) { - $labels[$code] ??= 'Section #'.$code; + $labels[$code] ??= 'Section #' . $code; } } @@ -67,10 +67,10 @@ class AttendanceQueryService if ($sectionCode > 0) { $assigned = $this->teacherClass->assignedForSectionTerm($sectionCode, $semester, $schoolYear); - $teacherIds = array_map(fn ($r) => (int) $r['teacher_id'], $assigned); + $teacherIds = array_map(fn($r) => (int)$r['teacher_id'], $assigned); $statusMap = []; - if (! empty($teacherIds)) { + if (!empty($teacherIds)) { $rows = DB::table('staff_attendance as sa') ->select('sa.user_id', 'sa.status', 'sa.reason') ->where('sa.semester', $semester) @@ -80,7 +80,7 @@ class AttendanceQueryService ->get(); foreach ($rows as $row) { - $statusMap[(int) $row->user_id] = [ + $statusMap[(int)$row->user_id] = [ 'status' => $row->status, 'reason' => $row->reason, ]; @@ -88,15 +88,15 @@ class AttendanceQueryService } foreach ($assigned as $teacher) { - $teacherId = (int) $teacher['teacher_id']; - $position = strtolower((string) ($teacher['position'] ?? 'main')); + $teacherId = (int)$teacher['teacher_id']; + $position = strtolower((string)($teacher['position'] ?? 'main')); $position = in_array($position, ['main', 'ta'], true) ? $position : 'main'; - $name = trim(($teacher['firstname'] ?? '').' '.($teacher['lastname'] ?? '')); + $name = trim(($teacher['firstname'] ?? '') . ' ' . ($teacher['lastname'] ?? '')); $cell = $statusMap[$teacherId] ?? ['status' => null, 'reason' => null]; $grid[] = [ 'teacher_id' => $teacherId, - 'name' => $name !== '' ? $name : 'User#'.$teacherId, + 'name' => $name !== '' ? $name : 'User#' . $teacherId, 'position' => $position, 'status' => $cell['status'], 'reason' => $cell['reason'], @@ -125,13 +125,13 @@ class AttendanceQueryService public function teacherAttendanceFormData(?int $userId, int $requestedSectionId = 0): array { - if (! $userId) { + if (!$userId) { throw new RuntimeException('User not logged in.'); } $teacher = $this->user->find($userId); $teacherName = $teacher - ? trim(($teacher->firstname ?? '').' '.($teacher->lastname ?? '')) + ? trim(($teacher->firstname ?? '') . ' ' . ($teacher->lastname ?? '')) : 'Unknown Teacher'; $semester = $this->attendanceService->currentSemester(); @@ -145,8 +145,8 @@ class AttendanceQueryService $activeSection = null; foreach ($assignments as $assignment) { - $cid = (int) ($assignment['class_section_id'] ?? 0); - $pk = (int) ($assignment['class_section_pk'] ?? 0); + $cid = (int)($assignment['class_section_id'] ?? 0); + $pk = (int)($assignment['class_section_pk'] ?? 0); if ($requestedSectionId > 0 && ($requestedSectionId === $cid || $requestedSectionId === $pk)) { $activeSection = $assignment; @@ -156,8 +156,8 @@ class AttendanceQueryService $activeSection ??= $assignments[0]; - $classSectionCode = (int) ($activeSection['class_section_id'] ?? 0); - $classSectionPk = (int) ($activeSection['class_section_pk'] ?? 0); + $classSectionCode = (int)($activeSection['class_section_id'] ?? 0); + $classSectionPk = (int)($activeSection['class_section_pk'] ?? 0); $classSectionId = $classSectionCode > 0 ? $classSectionCode : $classSectionPk; if ($classSectionId <= 0) { @@ -183,11 +183,11 @@ class AttendanceQueryService $lockedByStudent = []; $attendanceTable = $this->attendanceData->getTable(); - $dateInSundaysSql = 'DATE(`'.$attendanceTable.'`.`date`) IN ('. - implode(',', array_fill(0, count($sundayDates), '?')).')'; + $dateInSundaysSql = 'DATE(`' . $attendanceTable . '`.`date`) IN (' . + implode(',', array_fill(0, count($sundayDates), '?')) . ')'; foreach ($students as $student) { - $studentId = (int) $student['id']; + $studentId = (int)$student['id']; $rows = AttendanceData::query() ->select(['date', 'status', 'is_reported', 'reason', 'modified_by']) @@ -202,7 +202,7 @@ class AttendanceQueryService }) ->whereRaw($dateInSundaysSql, $sundayDates) ->get() - ->map(fn ($row) => (array) $row) + ->map(fn($row) => (array)$row) ->all(); $rows = $this->attendanceService->normalizeAttendanceEntries($rows); @@ -226,8 +226,8 @@ class AttendanceQueryService $recentHistory = []; foreach ($recentRows as $row) { $recentHistory[] = [ - 'date' => substr((string) ($row['date'] ?? ''), 0, 10), - 'status' => strtolower((string) ($row['status'] ?? '')), + 'date' => substr((string)($row['date'] ?? ''), 0, 10), + 'status' => strtolower((string)($row['status'] ?? '')), ]; } @@ -253,7 +253,7 @@ class AttendanceQueryService // missed rows (SQL/driver quirks), still show statuses that match the visible columns. foreach ($recentRows as $row) { $d = $this->calendarDateKey($row['date'] ?? null); - if ($d === '' || ! in_array($d, $sundayDates, true)) { + if ($d === '' || !in_array($d, $sundayDates, true)) { continue; } $rowStatus = $row['status'] ?? null; @@ -340,13 +340,13 @@ class AttendanceQueryService $teacherRows = []; - if (! empty($assigned)) { - $teacherIds = array_map(static fn ($r) => (int) $r['teacher_id'], $assigned); + if (!empty($assigned)) { + $teacherIds = array_map(static fn($r) => (int)$r['teacher_id'], $assigned); $statusMap = []; - if (! empty($teacherIds)) { - $staffDateInSql = 'DATE(`sa`.`date`) IN ('. - implode(',', array_fill(0, count($sundayDates), '?')).')'; + if (!empty($teacherIds)) { + $staffDateInSql = 'DATE(`sa`.`date`) IN (' . + implode(',', array_fill(0, count($sundayDates), '?')) . ')'; $staffQ = DB::table('staff_attendance as sa') ->select('sa.user_id', 'sa.date', 'sa.status', 'sa.reason') ->where('sa.semester', $semester) @@ -372,7 +372,7 @@ class AttendanceQueryService if ($dk === '') { continue; } - $statusMap[(int) $row->user_id][$dk] = [ + $statusMap[(int)$row->user_id][$dk] = [ 'status' => $row->status, 'reason' => $row->reason, ]; @@ -380,10 +380,10 @@ class AttendanceQueryService } foreach ($assigned as $teacher) { - $teacherId = (int) $teacher['teacher_id']; - $position = strtolower((string) ($teacher['position'] ?? 'main')); + $teacherId = (int)$teacher['teacher_id']; + $position = strtolower((string)($teacher['position'] ?? 'main')); $position = in_array($position, ['main', 'ta'], true) ? $position : 'main'; - $name = trim(($teacher['firstname'] ?? '').' '.($teacher['lastname'] ?? '')); + $name = trim(($teacher['firstname'] ?? '') . ' ' . ($teacher['lastname'] ?? '')); $history = []; foreach ($sundayDates as $date) { @@ -400,7 +400,7 @@ class AttendanceQueryService $teacherRows[] = [ 'teacher_id' => $teacherId, 'position' => $position, - 'name' => $name !== '' ? $name : 'User#'.$teacherId, + 'name' => $name !== '' ? $name : 'User#' . $teacherId, 'sunday_statuses' => $history, 'today' => $history[$currentSunday] ?? null, 'today_reason' => $todayReason, @@ -410,8 +410,8 @@ class AttendanceQueryService $noSchoolDays = []; $calendarTable = $this->calendar->getTable(); - $calDateInSql = 'DATE(`'.$calendarTable.'`.`date`) IN ('. - implode(',', array_fill(0, count($sundayDates), '?')).')'; + $calDateInSql = 'DATE(`' . $calendarTable . '`.`date`) IN (' . + implode(',', array_fill(0, count($sundayDates), '?')) . ')'; $calendarRows = $this->calendar->query() ->where('no_school', 1) ->whereRaw($calDateInSql, $sundayDates) @@ -438,9 +438,9 @@ class AttendanceQueryService 'class_section_id' => $classSectionId, 'sunday_dates' => $sundayDates, 'current_sunday' => $currentSunday, - 'enable_attendance' => (int) $this->configuration->getConfig('enable_attendance'), - 'can_edit' => ((int) $this->configuration->getConfig('enable_attendance') === 1), - 'class_id' => (int) $this->classSection->getClassId($classSectionId), + 'enable_attendance' => (int)$this->configuration->getConfig('enable_attendance'), + 'can_edit' => ((int)$this->configuration->getConfig('enable_attendance') === 1), + 'class_id' => (int)$this->classSection->getClassId($classSectionId), 'no_school_days' => $noSchoolDays, 'today' => $currentSunday, 'locked_by_student' => $lockedByStudent, @@ -463,7 +463,7 @@ class AttendanceQueryService ->where('sc.school_year', $schoolYear) ->groupBy('classSection.id', 'classSection.class_id', 'classSection.class_section_id', 'classSection.class_section_name') ->get() - ->map(fn ($row) => $row->toArray()) + ->map(fn($row) => $row->toArray()) ->all(); }; @@ -492,32 +492,32 @@ class AttendanceQueryService $sectionCounts = $this->studentClass->getStudentCountsBySection($effectiveTermYear); foreach ($classSections as $classSection) { - $secPk = (int) ($classSection['id'] ?? 0); - $secCodeRaw = (string) ($classSection['class_section_id'] ?? ''); - $secCode = $secCodeRaw !== '' ? $secCodeRaw : (string) $secPk; - $classId = (int) ($classSection['class_id'] ?? 0); + $secPk = (int)($classSection['id'] ?? 0); + $secCodeRaw = (string)($classSection['class_section_id'] ?? ''); + $secCode = $secCodeRaw !== '' ? $secCodeRaw : (string)$secPk; + $classId = (int)($classSection['class_id'] ?? 0); $studentsBySection[$secCode] = []; $datesBySection[$secCode] = []; $attendanceData[$secCode] = []; $attendanceRecord[$secCode] = []; - $countForCode = (int) ($sectionCounts[$secCode] ?? 0); - $countForPk = (int) ($sectionCounts[$secPk] ?? 0); + $countForCode = (int)($sectionCounts[$secCode] ?? 0); + $countForPk = (int)($sectionCounts[$secPk] ?? 0); $sectionHasStudents = ($countForCode + $countForPk) > 0; $students = $this->studentClass->getClassStudents($secCode, $effectiveTermYear); - if (! $students && $secPk && (string) $secPk !== (string) $secCode) { + if (!$students && $secPk && (string)$secPk !== (string)$secCode) { $students = $this->studentClass->getClassStudents($secPk, $effectiveTermYear); } - if (! $sectionHasStudents || ! $students) { + if (!$sectionHasStudents || !$students) { continue; } // Bulk-load student profiles for the whole section in one query $studentsArray = is_array($students) ? $students : $students->toArray(); - $studentIds = array_map(fn ($sc) => (int) ($sc['student_id'] ?? $sc->student_id ?? 0), $studentsArray); + $studentIds = array_map(fn($sc) => (int)($sc['student_id'] ?? $sc->student_id ?? 0), $studentsArray); $studentProfiles = $this->student ->query() ->select(['id', 'firstname', 'lastname', 'school_id']) @@ -525,13 +525,12 @@ class AttendanceQueryService ->where('is_active', 1) ->get() ->keyBy('id') - ->map(fn ($row) => $row->toArray()) + ->map(fn($row) => $row->toArray()) ->all(); $activeIds = array_keys($studentProfiles); if (empty($activeIds)) { unset($studentsBySection[$secCode], $datesBySection[$secCode], $attendanceData[$secCode], $attendanceRecord[$secCode]); - continue; } @@ -540,17 +539,17 @@ class AttendanceQueryService ->whereIn('student_id', $activeIds) ->where(function ($q) use ($secCode, $secPk) { $q->where('class_section_id', $secCode); - if ($secPk && (string) $secPk !== (string) $secCode) { + if ($secPk && (string)$secPk !== (string)$secCode) { $q->orWhere('class_section_id', $secPk); } }) - ->when($termSemester !== '', fn ($q) => $q->where('semester', $termSemester)) - ->when($effectiveTermYear !== '', fn ($q) => $q->where('school_year', $effectiveTermYear)) + ->when($termSemester !== '', fn($q) => $q->where('semester', $termSemester)) + ->when($effectiveTermYear !== '', fn($q) => $q->where('school_year', $effectiveTermYear)) ->orderBy('date') ->get() - ->map(fn ($row) => $row->toArray()) + ->map(fn($row) => $row->toArray()) ->groupBy('student_id') - ->map(fn ($group) => $group->values()->all()) + ->map(fn($group) => $group->values()->all()) ->all(); // Bulk-load all attendance records (summaries) for the section in one query @@ -558,36 +557,36 @@ class AttendanceQueryService ->whereIn('student_id', $activeIds) ->where(function ($q) use ($secCode, $secPk) { $q->where('class_section_id', $secCode); - if ($secPk && (string) $secPk !== (string) $secCode) { + if ($secPk && (string)$secPk !== (string)$secCode) { $q->orWhere('class_section_id', $secPk); } }) - ->when($termSemester !== '', fn ($q) => $q->where('semester', $termSemester)) - ->when($effectiveTermYear !== '', fn ($q) => $q->where('school_year', $effectiveTermYear)) + ->when($termSemester !== '', fn($q) => $q->where('semester', $termSemester)) + ->when($effectiveTermYear !== '', fn($q) => $q->where('school_year', $effectiveTermYear)) ->get() ->keyBy('student_id') - ->map(fn ($row) => $row->toArray()) + ->map(fn($row) => $row->toArray()) ->all(); $hasRoster = false; foreach ($studentIds as $studentId) { $student = $studentProfiles[$studentId] ?? null; - if (! $student) { + if (!$student) { continue; } $studentsBySection[$secCode][] = $student; - $studentSchoolMap[$studentId] = (string) ($student['school_id'] ?? ''); + $studentSchoolMap[$studentId] = (string)($student['school_id'] ?? ''); $hasRoster = true; $entries = $this->attendanceService->normalizeAttendanceEntries( - array_map(fn ($r) => (array) $r, $allEntries[$studentId] ?? []) + array_map(fn($r) => (array)$r, $allEntries[$studentId] ?? []) ); $attendanceData[$secCode][$studentId] = $entries; foreach ($entries as $entry) { - $d = (string) ($entry['date'] ?? ''); + $d = (string)($entry['date'] ?? ''); if ($d !== '') { $datesBySection[$secCode][$d] = true; } @@ -601,9 +600,8 @@ class AttendanceQueryService ]; } - if (! $hasRoster) { + if (!$hasRoster) { unset($studentsBySection[$secCode], $datesBySection[$secCode], $attendanceData[$secCode], $attendanceRecord[$secCode]); - continue; } @@ -612,7 +610,7 @@ class AttendanceQueryService foreach ($attendanceData as $secId => &$studentEntries) { foreach ($studentEntries as &$entries) { - usort($entries, static fn ($a, $b) => strcmp((string) ($a['date'] ?? ''), (string) ($b['date'] ?? ''))); + usort($entries, static fn($a, $b) => strcmp((string)($a['date'] ?? ''), (string)($b['date'] ?? ''))); } } unset($studentEntries, $entries); @@ -646,9 +644,9 @@ class AttendanceQueryService ->toArray(); foreach ($events as $event) { - $d = substr((string) ($event['date'] ?? ''), 0, 10); + $d = substr((string)($event['date'] ?? ''), 0, 10); if ($d !== '') { - $noSchoolDays[$d] = trim((string) ($event['title'] ?? 'No School')) ?: 'No School'; + $noSchoolDays[$d] = trim((string)($event['title'] ?? 'No School')) ?: 'No School'; } } @@ -670,12 +668,12 @@ class AttendanceQueryService foreach ($studentEntries as $studentId => $entries) { $statusByDate = []; foreach ($entries as $entry) { - $d = substr((string) ($entry['date'] ?? ''), 0, 10); + $d = substr((string)($entry['date'] ?? ''), 0, 10); if ($d === '' || empty($passedDatesSet[$d])) { continue; } - $st = strtolower(trim((string) ($entry['status'] ?? ''))); - if (! in_array($st, ['present', 'absent', 'late'], true)) { + $st = strtolower(trim((string)($entry['status'] ?? ''))); + if (!in_array($st, ['present', 'absent', 'late'], true)) { continue; } $statusByDate[$d] = $st; @@ -697,9 +695,9 @@ class AttendanceQueryService $sum = $p + $l + $a; $record = $attendanceRecord[$secCode][$studentId] ?? []; - $storedSum = (int) ($record['total_presence'] ?? 0) - + (int) ($record['total_late'] ?? 0) - + (int) ($record['total_absence'] ?? 0); + $storedSum = (int)($record['total_presence'] ?? 0) + + (int)($record['total_late'] ?? 0) + + (int)($record['total_absence'] ?? 0); if ($storedSum !== $totalPassedDays && $storedSum !== $sum) { DB::table('attendance_record') @@ -735,7 +733,7 @@ class AttendanceQueryService if (auth()->id()) { $u = $this->user->query()->select('firstname', 'lastname')->find(auth()->id()); if ($u) { - $adminName = trim(($u->firstname ?? '').' '.($u->lastname ?? '')); + $adminName = trim(($u->firstname ?? '') . ' ' . ($u->lastname ?? '')); } } @@ -775,8 +773,8 @@ class AttendanceQueryService return []; } - $staffDateInSql = 'DATE(`sa`.`date`) IN ('. - implode(',', array_fill(0, count($sundayDates), '?')).')'; + $staffDateInSql = 'DATE(`sa`.`date`) IN (' . + implode(',', array_fill(0, count($sundayDates), '?')) . ')'; $run = static function ( bool $requireTcSemester, @@ -832,8 +830,8 @@ class AttendanceQueryService return []; } - $staffDateInSql = 'DATE(`sa`.`date`) IN ('. - implode(',', array_fill(0, count($sundayDates), '?')).')'; + $staffDateInSql = 'DATE(`sa`.`date`) IN (' . + implode(',', array_fill(0, count($sundayDates), '?')) . ')'; $ids = []; @@ -907,7 +905,7 @@ class AttendanceQueryService */ private function calendarDateKey(mixed $value): string { - $s = trim((string) ($value ?? '')); + $s = trim((string)($value ?? '')); if ($s === '') { return ''; } diff --git a/app/Services/Attendance/AttendanceRecordSyncService.php b/app/Services/Attendance/AttendanceRecordSyncService.php index 8933c88e..47771b95 100644 --- a/app/Services/Attendance/AttendanceRecordSyncService.php +++ b/app/Services/Attendance/AttendanceRecordSyncService.php @@ -25,16 +25,16 @@ class AttendanceRecordSyncService ->where('school_year', $schoolYear) ->first(); - if (! $record) { + if (!$record) { return; } $newStatus = strtolower($newStatus); - $oldStatus = strtolower((string) $oldStatus); + $oldStatus = strtolower((string)$oldStatus); - $presence = (int) ($record->total_presence ?? 0); - $absence = (int) ($record->total_absence ?? 0); - $late = (int) ($record->total_late ?? 0); + $presence = (int)($record->total_presence ?? 0); + $absence = (int)($record->total_absence ?? 0); + $late = (int)($record->total_late ?? 0); if ($oldStatus === 'present') { $presence--; @@ -80,14 +80,13 @@ class AttendanceRecordSyncService if ($record) { $record->update([ - 'total_presence' => (int) ($record->total_presence ?? 0) + ($status === 'present' ? 1 : 0), - 'total_absence' => (int) ($record->total_absence ?? 0) + ($status === 'absent' ? 1 : 0), - 'total_late' => (int) ($record->total_late ?? 0) + ($status === 'late' ? 1 : 0), - 'total_attendance' => (int) ($record->total_attendance ?? 0) + 1, + 'total_presence' => (int)($record->total_presence ?? 0) + ($status === 'present' ? 1 : 0), + 'total_absence' => (int)($record->total_absence ?? 0) + ($status === 'absent' ? 1 : 0), + 'total_late' => (int)($record->total_late ?? 0) + ($status === 'late' ? 1 : 0), + 'total_attendance' => (int)($record->total_attendance ?? 0) + 1, 'updated_at' => now(), 'modified_by' => $modifiedBy, ]); - return; } @@ -142,13 +141,13 @@ class AttendanceRecordSyncService return $record->update($payload); } - $payload['class_section_id'] = (int) $classSectionId; + $payload['class_section_id'] = (int)$classSectionId; $payload['student_id'] = $studentId; $payload['school_id'] = $schoolId; $payload['semester'] = $semester; $payload['school_year'] = $schoolYear; $payload['created_at'] = now(); - return (bool) AttendanceRecord::query()->create($payload); + return (bool)AttendanceRecord::query()->create($payload); } -} +} \ No newline at end of file diff --git a/app/Services/Attendance/AttendanceService.php b/app/Services/Attendance/AttendanceService.php index 5dc287a7..13985bc1 100644 --- a/app/Services/Attendance/AttendanceService.php +++ b/app/Services/Attendance/AttendanceService.php @@ -27,24 +27,24 @@ class AttendanceService public function currentSemester(): string { - return (string) ($this->configuration->getConfig('semester') ?? 'Fall'); + return (string)($this->configuration->getConfig('semester') ?? 'Fall'); } public function currentSchoolYear(): string { - return (string) ($this->configuration->getConfig('school_year') ?? now()->year.'-'.(now()->year + 1)); + return (string)($this->configuration->getConfig('school_year') ?? now()->year . '-' . (now()->year + 1)); } public function updateAttendanceManagement(Authenticatable $user, array $data): array { - $classSectionId = (int) $data['class_section_id']; - $studentId = (int) $data['student_id']; - $status = strtolower((string) $data['status']); - $date = (string) $data['date']; + $classSectionId = (int)$data['class_section_id']; + $studentId = (int)$data['student_id']; + $status = strtolower((string)$data['status']); + $date = (string)$data['date']; $reason = $data['reason'] ?? null; $classId = $data['class_id'] ?? null; - $semester = (string) ($data['semester'] ?? $this->currentSemester()); - $schoolYear = (string) ($data['school_year'] ?? $this->currentSchoolYear()); + $semester = (string)($data['semester'] ?? $this->currentSemester()); + $schoolYear = (string)($data['school_year'] ?? $this->currentSchoolYear()); $dayRow = AttendanceDay::query() ->where('class_section_id', $classSectionId) @@ -67,11 +67,11 @@ class AttendanceService 'permissions' => method_exists($user, 'permissions') ? $user->permissions->pluck('name')->all() : [], ]; - if (! $this->attendancePolicyService->canEditDay($dayPolicyRow, $currentUser)) { + if (!$this->attendancePolicyService->canEditDay($dayPolicyRow, $currentUser)) { throw new RuntimeException('Attendance is locked for your role.'); } - $reportedRaw = strtolower((string) ($data['is_reported'] ?? '')); + $reportedRaw = strtolower((string)($data['is_reported'] ?? '')); $isReported = $status === 'present' ? 'no' : (in_array($reportedRaw, ['1', 'yes', 'y', 'true', 'on'], true) ? 'yes' : 'no'); @@ -98,7 +98,7 @@ class AttendanceService 'semester' => $semester, 'school_year' => $schoolYear, 'date' => $date, - 'class_id' => (int) $classId, + 'class_id' => (int)$classId, 'user_id' => $user->id, ]); @@ -136,13 +136,13 @@ class AttendanceService public function adminAddEntry(Authenticatable $user, array $data): array { - $classSectionId = (int) $data['class_section_id']; - $studentId = (int) $data['student_id']; - $status = strtolower((string) $data['status']); - $date = (string) ($data['date'] ?? now()->toDateString()); + $classSectionId = (int)$data['class_section_id']; + $studentId = (int)$data['student_id']; + $status = strtolower((string)$data['status']); + $date = (string)($data['date'] ?? now()->toDateString()); $reason = $data['reason'] ?? null; - $isReported = in_array(strtolower((string) ($data['is_reported'] ?? 'no')), ['yes', 'no'], true) - ? strtolower((string) ($data['is_reported'] ?? 'no')) + $isReported = in_array(strtolower((string)($data['is_reported'] ?? 'no')), ['yes', 'no'], true) + ? strtolower((string)($data['is_reported'] ?? 'no')) : 'no'; $section = $this->classSection @@ -152,7 +152,7 @@ class AttendanceService ->orWhere('class_section_id', $classSectionId) ->first(); - $resolvedClassId = (int) ($section->class_id ?? 0); + $resolvedClassId = (int)($section->class_id ?? 0); if ($resolvedClassId <= 0) { throw new RuntimeException('Cannot resolve class_id for this section.'); @@ -210,32 +210,29 @@ class AttendanceService $reportedRaw = $row['is_reported'] ?? ($row['reported'] ?? ''); $row['is_reported'] = $this->normalizeReportedFlag($reportedRaw); } - return $entries; } public function normalizeStatus(?string $status): string { - $status = strtolower(trim((string) $status)); - + $status = strtolower(trim((string)$status)); return in_array($status, ['present', 'absent', 'late'], true) ? $status : $status; } public function normalizeReportedFlag(mixed $flag): string { - $v = strtolower(trim((string) $flag)); - + $v = strtolower(trim((string)$flag)); return in_array($v, ['yes', '1', 'true', 'y', 'on'], true) ? 'yes' : 'no'; } public function detectExternalSubmission(?array $row): array { - if (empty($row) || ! is_array($row)) { + if (empty($row) || !is_array($row)) { return ['locked' => false, 'source' => null]; } - $reportedRaw = strtolower((string) ($row['is_reported'] ?? '')); - $reason = strtolower((string) ($row['reason'] ?? '')); + $reportedRaw = strtolower((string)($row['is_reported'] ?? '')); + $reason = strtolower((string)($row['reason'] ?? '')); $isParent = in_array($reportedRaw, ['yes', '1', 'true', 'y'], true) || str_contains($reason, 'parent-reported') @@ -245,7 +242,7 @@ class AttendanceService return ['locked' => true, 'source' => 'parent']; } - $modifierId = (int) ($row['modified_by'] ?? 0); + $modifierId = (int)($row['modified_by'] ?? 0); if ($modifierId > 0 && $this->isAdminLikeUserId($modifierId)) { return ['locked' => true, 'source' => 'admin']; } @@ -267,12 +264,12 @@ class AttendanceService $excluded = ['parent', 'guest', 'teacher', 'teacher_assistant', 'assistant_teacher', 'ta']; foreach ($roles as $role) { - $name = strtolower(str_replace([' ', '-'], '_', (string) ($role['role_name'] ?? ''))); - if ($name !== '' && ! in_array($name, $excluded, true)) { + $name = strtolower(str_replace([' ', '-'], '_', (string)($role['role_name'] ?? ''))); + if ($name !== '' && !in_array($name, $excluded, true)) { return true; } } return false; } -} +} \ No newline at end of file diff --git a/app/Services/Attendance/AttendanceSummaryRebuildService.php b/app/Services/Attendance/AttendanceSummaryRebuildService.php index 5137e21a..5752aec9 100644 --- a/app/Services/Attendance/AttendanceSummaryRebuildService.php +++ b/app/Services/Attendance/AttendanceSummaryRebuildService.php @@ -29,8 +29,8 @@ class AttendanceSummaryRebuildService $semester = (string) ($row['semester'] ?? ''); $status = strtolower((string) ($row['status'] ?? '')); - $key = $studentId.'-'.$schoolYear.'-'.$semester; - if (! isset($summary[$key])) { + $key = $studentId . '-' . $schoolYear . '-' . $semester; + if (!isset($summary[$key])) { $summary[$key] = [ 'student_id' => $studentId, 'school_year' => $schoolYear, diff --git a/app/Services/Attendance/LateSlipLogQueryService.php b/app/Services/Attendance/LateSlipLogQueryService.php index f603438b..5d27271c 100644 --- a/app/Services/Attendance/LateSlipLogQueryService.php +++ b/app/Services/Attendance/LateSlipLogQueryService.php @@ -11,16 +11,16 @@ class LateSlipLogQueryService { $query = LateSlipLog::query(); - if (! empty($filters['school_year'])) { + if (!empty($filters['school_year'])) { $query->where('school_year', (string) $filters['school_year']); } - if (! empty($filters['semester'])) { + if (!empty($filters['semester'])) { $query->where('semester', (string) $filters['semester']); } - if (! empty($filters['q'])) { - $query->where('student_name', 'like', '%'.$filters['q'].'%'); + if (!empty($filters['q'])) { + $query->where('student_name', 'like', '%' . $filters['q'] . '%'); } $dateFrom = $this->toDbDate($filters['date_from'] ?? null); @@ -58,7 +58,6 @@ class LateSlipLogQueryService return null; } $ts = strtotime($value); - return $ts ? date('Y-m-d', $ts) : null; } } diff --git a/app/Services/Attendance/SemesterRangeService.php b/app/Services/Attendance/SemesterRangeService.php index 4c0870f3..79d80a27 100644 --- a/app/Services/Attendance/SemesterRangeService.php +++ b/app/Services/Attendance/SemesterRangeService.php @@ -9,7 +9,7 @@ class SemesterRangeService { public function normalizeSemester(?string $semester): string { - $value = strtolower(trim((string) $semester)); + $value = strtolower(trim((string)$semester)); return match ($value) { 'fall', 'autumn' => 'Fall', @@ -76,7 +76,7 @@ class SemesterRangeService protected function parseSchoolYear(string $schoolYear): array { if (preg_match('/^\s*(\d{4})\s*-\s*(\d{4})\s*$/', $schoolYear, $m)) { - return [(int) $m[1], (int) $m[2]]; + return [(int)$m[1], (int)$m[2]]; } $configured = trim((string) Configuration::getConfig('school_year')); @@ -85,7 +85,7 @@ class SemesterRangeService } $year = (int) date('Y'); - $derived = $year.'-'.($year + 1); + $derived = $year . '-' . ($year + 1); Configuration::setConfigValueByKey('school_year', $derived); return [$year, $year + 1]; diff --git a/app/Services/Attendance/StaffAttendanceService.php b/app/Services/Attendance/StaffAttendanceService.php index 170c8844..ea380676 100644 --- a/app/Services/Attendance/StaffAttendanceService.php +++ b/app/Services/Attendance/StaffAttendanceService.php @@ -2,14 +2,13 @@ namespace App\Services\Attendance; -use App\Controllers\View\AttendanceController; use App\Models\AttendanceDay; -use App\Models\ClassSection; use App\Models\Configuration; use App\Models\StaffAttendance; use App\Models\TeacherClass; use Carbon\Carbon; use Illuminate\Contracts\Auth\Authenticatable; +use App\Models\ClassSection; use Illuminate\Support\Facades\DB; use Symfony\Component\HttpFoundation\StreamedResponse; @@ -24,7 +23,7 @@ class StaffAttendanceService ) {} /** - * legacy {@see AttendanceController::index} — single date / section teacher grid JSON. + * legacy {@see \App\Controllers\View\AttendanceController::index} — single date / section teacher grid JSON. */ public function teacherAttendanceDayIndex(?string $semester, ?string $schoolYear, string $dateYmd, int $sectionCode): array { @@ -45,7 +44,7 @@ class StaffAttendanceService ->where('class_section_id', $code) ->value('class_section_name'); $name = trim((string) $name); - $labels[$code] = $name !== '' ? $name : ('Section #'.$code); + $labels[$code] = $name !== '' ? $name : ('Section #' . $code); } $grid = []; @@ -77,7 +76,7 @@ class StaffAttendanceService $tid = (int) ($t['teacher_id'] ?? 0); $posRaw = strtolower((string) ($t['position'] ?? 'main')); $pos = in_array($posRaw, ['main', 'ta'], true) ? $posRaw : 'main'; - $name = trim(($t['firstname'] ?? '').' '.($t['lastname'] ?? '')) ?: ('User#'.$tid); + $name = trim(($t['firstname'] ?? '') . ' ' . ($t['lastname'] ?? '')) ?: ('User#' . $tid); $cell = $statusMap[$tid] ?? ['status' => null, 'reason' => null]; @@ -109,7 +108,7 @@ class StaffAttendanceService } /** - * Bulk save for legacy {@see AttendanceController::save} (method missing in legacy; implemented here). + * Bulk save for legacy {@see \App\Controllers\View\AttendanceController::save} (method missing in legacy; implemented here). */ public function saveTeacherAttendanceBulk(Authenticatable $user, array $payload): array { @@ -182,7 +181,7 @@ class StaffAttendanceService public function monthData(?string $semester, ?string $schoolYear): array { - $semester = $semester ?: (string) $this->configuration->getConfig('semester'); + $semester = $semester ?: (string)$this->configuration->getConfig('semester'); [ 'school_year' => $schoolYear, 'current_year' => $currentYear, @@ -233,7 +232,7 @@ class StaffAttendanceService $sectionCodes = array_keys($assignedBySection); $labels = []; - if (! empty($sectionCodes)) { + if (!empty($sectionCodes)) { $rows = $this->classSection->query() ->select(['class_section_id', 'class_section_name']) ->whereIn('class_section_id', $sectionCodes) @@ -241,20 +240,20 @@ class StaffAttendanceService ->toArray(); foreach ($rows as $row) { - $labels[(int) $row['class_section_id']] = trim((string) $row['class_section_name']) ?: 'Section #'.(int) $row['class_section_id']; + $labels[(int)$row['class_section_id']] = trim((string)$row['class_section_name']) ?: 'Section #' . (int)$row['class_section_id']; } } $teacherIds = []; foreach ($assignedBySection as $rows) { foreach ($rows as $row) { - $teacherIds[(int) $row['teacher_id']] = true; + $teacherIds[(int)$row['teacher_id']] = true; } } $teacherIds = array_keys($teacherIds); $statusByTeacher = []; - if (! empty($teacherIds) && ! empty($days)) { + if (!empty($teacherIds) && !empty($days)) { $query = DB::table('staff_attendance') ->select('user_id', 'date', 'status', 'reason') ->where('school_year', $schoolYear) @@ -268,8 +267,8 @@ class StaffAttendanceService $rows = $query->get(); foreach ($rows as $row) { - $statusByTeacher[(int) $row->user_id][(string) $row->date] = [ - 'status' => strtolower((string) $row->status), + $statusByTeacher[(int)$row->user_id][(string)$row->date] = [ + 'status' => strtolower((string)$row->status), 'reason' => $row->reason, ]; } @@ -285,7 +284,7 @@ class StaffAttendanceService $noSchoolDays = []; foreach ($noSchoolRows as $row) { - $noSchoolDays[] = (string) $row->date; + $noSchoolDays[] = (string)$row->date; } $sections = []; @@ -293,9 +292,9 @@ class StaffAttendanceService $teachersPayload = []; foreach (($assignedBySection[$code] ?? []) as $row) { - $teacherId = (int) $row['teacher_id']; - $name = trim(($row['firstname'] ?? '').' '.($row['lastname'] ?? '')) ?: ('User#'.$teacherId); - $position = strtolower((string) ($row['position'] ?? 'main')); + $teacherId = (int)$row['teacher_id']; + $name = trim(($row['firstname'] ?? '') . ' ' . ($row['lastname'] ?? '')) ?: ('User#' . $teacherId); + $position = strtolower((string)($row['position'] ?? 'main')); $position = in_array($position, ['main', 'ta'], true) ? $position : 'main'; $cells = []; @@ -303,7 +302,7 @@ class StaffAttendanceService foreach ($days as $date) { $cell = $statusByTeacher[$teacherId][$date] ?? null; - if (! $cell || empty($cell['status'])) { + if (!$cell || empty($cell['status'])) { continue; } @@ -328,15 +327,15 @@ class StaffAttendanceService } $sections[] = [ - 'section_code' => (int) $code, - 'label' => $labels[$code] ?? ('Section #'.(int) $code), + 'section_code' => (int)$code, + 'label' => $labels[$code] ?? ('Section #' . (int)$code), 'teachers' => $teachersPayload, ]; } $adminStatusByUser = []; $adminIds = []; - if (! empty($days)) { + if (!empty($days)) { $query = DB::table('staff_attendance') ->select('user_id', 'date', 'status', 'reason') ->where('school_year', $schoolYear) @@ -362,7 +361,7 @@ class StaffAttendanceService $adminIds = array_keys($adminIds); $admins = []; - if (! empty($adminIds)) { + if (!empty($adminIds)) { $rolesRows = DB::table('user_roles as ur') ->select('ur.user_id', 'r.name as role_name', 'r.slug as role_slug', 'r.priority') ->join('roles as r', 'r.id', '=', 'ur.role_id') @@ -390,20 +389,20 @@ class StaffAttendanceService $namesByUser = []; foreach ($userRows as $row) { - $namesByUser[(int) $row->id] = trim(((string) ($row->firstname ?? '')).' '.((string) ($row->lastname ?? ''))); + $namesByUser[(int) $row->id] = trim(((string) ($row->firstname ?? '')) . ' ' . ((string) ($row->lastname ?? ''))); } foreach ($adminIds as $uid) { $picked = null; foreach (($rolesByUser[$uid] ?? []) as $role) { $slug = strtolower((string) ($role['role_slug'] ?? $role['role_name'] ?? '')); - if (! in_array($slug, $excludeSlugs, true)) { + if (!in_array($slug, $excludeSlugs, true)) { $picked = $role; break; } } - if (! $picked) { + if (!$picked) { continue; } @@ -411,7 +410,7 @@ class StaffAttendanceService $cells = []; foreach ($days as $date) { $cell = $adminStatusByUser[$uid][$date] ?? null; - if (! $cell || empty($cell['status'])) { + if (!$cell || empty($cell['status'])) { continue; } $cells[$date] = $cell; @@ -426,7 +425,7 @@ class StaffAttendanceService $admins[] = [ 'user_id' => $uid, - 'name' => $namesByUser[$uid] !== '' ? $namesByUser[$uid] : ('User#'.$uid), + 'name' => $namesByUser[$uid] !== '' ? $namesByUser[$uid] : ('User#' . $uid), 'role' => (string) ($picked['role_name'] ?? 'Admin'), 'cells' => $cells, 'totals' => $totals, @@ -509,7 +508,7 @@ class StaffAttendanceService } } - if ($currentYear !== '' && ! in_array($currentYear, $schoolYears, true)) { + if ($currentYear !== '' && !in_array($currentYear, $schoolYears, true)) { array_unshift($schoolYears, $currentYear); } @@ -524,7 +523,7 @@ class StaffAttendanceService ? $requestedSchoolYear : ($currentYear !== '' ? $currentYear : ($schoolYears[0] ?? '')); - if ($resolvedYear !== '' && ! in_array($resolvedYear, $schoolYears, true)) { + if ($resolvedYear !== '' && !in_array($resolvedYear, $schoolYears, true)) { array_unshift($schoolYears, $resolvedYear); } @@ -540,7 +539,7 @@ class StaffAttendanceService protected function buildRangeLabel(string $schoolYear, string $semesterNorm, string $rangeStart, string $rangeEnd): string { if ($semesterNorm === '') { - return 'School Year '.$schoolYear; + return 'School Year ' . $schoolYear; } return sprintf( @@ -554,14 +553,14 @@ class StaffAttendanceService public function adminsAttendanceData(?string $date, ?string $semester, ?string $schoolYear): array { $date = $date ?: now()->toDateString(); - $semester = $semester ?: (string) $this->configuration->getConfig('semester'); - $schoolYear = $schoolYear ?: (string) $this->configuration->getConfig('school_year'); + $semester = $semester ?: (string)$this->configuration->getConfig('semester'); + $schoolYear = $schoolYear ?: (string)$this->configuration->getConfig('school_year'); $allStaff = $this->staffAttendance->staffWithStatusForDay($date, $semester, $schoolYear, []); $userIds = []; foreach ($allStaff as $row) { - $uid = (int) ($row['user_id'] ?? 0); + $uid = (int)($row['user_id'] ?? 0); if ($uid > 0) { $userIds[$uid] = true; } @@ -578,19 +577,19 @@ class StaffAttendanceService $rolesByUser = []; foreach ($rolesRows as $row) { - $rolesByUser[(int) $row->user_id][] = (array) $row; + $rolesByUser[(int)$row->user_id][] = (array)$row; } foreach ($rolesByUser as &$list) { - usort($list, fn ($a, $b) => ((int) ($a['priority'] ?? 100)) <=> ((int) ($b['priority'] ?? 100))); + usort($list, fn($a, $b) => ((int)($a['priority'] ?? 100)) <=> ((int)($b['priority'] ?? 100))); } $excludeSlugs = ['parent', 'guest', 'teacher', 'teacher_assistant', 'ta', 'assistant_teacher']; $staffByUser = []; foreach ($allStaff as $row) { - $uid = (int) ($row['user_id'] ?? 0); - if ($uid > 0 && ! isset($staffByUser[$uid])) { + $uid = (int)($row['user_id'] ?? 0); + if ($uid > 0 && !isset($staffByUser[$uid])) { $staffByUser[$uid] = $row; } } @@ -601,14 +600,14 @@ class StaffAttendanceService $picked = null; foreach ($roles as $role) { - $slug = strtolower((string) ($role['role_slug'] ?? $role['role_name'] ?? '')); - if (! in_array($slug, $excludeSlugs, true)) { + $slug = strtolower((string)($role['role_slug'] ?? $role['role_name'] ?? '')); + if (!in_array($slug, $excludeSlugs, true)) { $picked = $role; break; } } - if (! $picked) { + if (!$picked) { continue; } @@ -619,9 +618,8 @@ class StaffAttendanceService } usort($adminsGrid, function ($a, $b) { - $an = trim(($a['firstname'] ?? '').' '.($a['lastname'] ?? '')); - $bn = trim(($b['firstname'] ?? '').' '.($b['lastname'] ?? '')); - + $an = trim(($a['firstname'] ?? '') . ' ' . ($a['lastname'] ?? '')); + $bn = trim(($b['firstname'] ?? '') . ' ' . ($b['lastname'] ?? '')); return strcasecmp($an, $bn); }); @@ -635,20 +633,20 @@ class StaffAttendanceService public function saveAdmins(Authenticatable $user, array $data): array { - $date = (string) $data['date']; - $semester = (string) $data['semester']; - $schoolYear = (string) $data['school_year']; - $admins = (array) ($data['admins'] ?? $data['staff'] ?? []); + $date = (string)$data['date']; + $semester = (string)$data['semester']; + $schoolYear = (string)$data['school_year']; + $admins = (array)($data['admins'] ?? $data['staff'] ?? []); $allowedStatuses = ['present', 'absent', 'late']; $excludedRoleSlugs = ['parent', 'guest', 'teacher', 'teacher_assistant']; DB::transaction(function () use ($admins, $date, $semester, $schoolYear, $allowedStatuses, $excludedRoleSlugs) { foreach ($admins as $uid => $row) { - $uid = (int) $uid; - $status = strtolower(trim((string) ($row['status'] ?? ''))); - $reason = trim((string) ($row['reason'] ?? '')); - $roleName = (string) ($row['role_name'] ?? ''); - $roleSlug = strtolower(str_replace(['-', ' '], '_', (string) ($row['role_slug'] ?? $roleName))); + $uid = (int)$uid; + $status = strtolower(trim((string)($row['status'] ?? ''))); + $reason = trim((string)($row['reason'] ?? '')); + $roleName = (string)($row['role_name'] ?? ''); + $roleSlug = strtolower(str_replace(['-', ' '], '_', (string)($row['role_slug'] ?? $roleName))); if (in_array($roleSlug, $excludedRoleSlugs, true)) { continue; @@ -661,11 +659,10 @@ class StaffAttendanceService ->where('semester', $semester) ->where('school_year', $schoolYear) ->delete(); - continue; } - if (! in_array($status, $allowedStatuses, true)) { + if (!in_array($status, $allowedStatuses, true)) { continue; } @@ -695,13 +692,13 @@ class StaffAttendanceService public function saveCell(Authenticatable $user, array $data): array { - $date = (string) $data['date']; - $status = (string) ($data['status'] ?? ''); - $targetId = (int) $data['user_id']; - $roleName = trim((string) ($data['role_name'] ?? '')); - $semester = (string) $data['semester']; - $schoolYear = (string) $data['school_year']; - $reason = trim((string) ($data['reason'] ?? '')); + $date = (string)$data['date']; + $status = (string)($data['status'] ?? ''); + $targetId = (int)$data['user_id']; + $roleName = trim((string)($data['role_name'] ?? '')); + $semester = (string)$data['semester']; + $schoolYear = (string)$data['school_year']; + $reason = trim((string)($data['reason'] ?? '')); if ($status === '') { DB::table('staff_attendance') @@ -735,9 +732,9 @@ class StaffAttendanceService public function monthCsv(?string $semester, ?string $schoolYear, ?string $scope): StreamedResponse { - $semester = $semester ?: (string) $this->configuration->getConfig('semester'); - $schoolYear = $schoolYear ?: (string) $this->configuration->getConfig('school_year'); - $scope = strtolower((string) $scope); + $semester = $semester ?: (string)$this->configuration->getConfig('semester'); + $schoolYear = $schoolYear ?: (string)$this->configuration->getConfig('school_year'); + $scope = strtolower((string)$scope); [$rangeStart, $rangeEnd] = $this->semesterRangeService->getSchoolYearRange($schoolYear); $semesterNorm = $this->semesterRangeService->normalizeSemester($semester); @@ -751,9 +748,9 @@ class StaffAttendanceService $daysList = $this->semesterRangeService->buildSundayList($rangeStart, $rangeEnd); $filename = ($scope === 'admins' ? 'admin_attendance_' : 'teacher_attendance_') - .$semester.'_'.str_replace('/', '-', $schoolYear).'.csv'; + . $semester . '_' . str_replace('/', '-', $schoolYear) . '.csv'; - return response()->streamDownload(function () use ($schoolYear, $scope, $semesterNorm, $rangeStart, $rangeEnd) { + return response()->streamDownload(function () use ($semester, $schoolYear, $scope, $daysList, $semesterNorm, $rangeStart, $rangeEnd) { $out = fopen('php://output', 'w'); if ($scope === 'admins') { @@ -773,13 +770,12 @@ class StaffAttendanceService fputcsv($out, [ $row->user_id, $row->date, - strtoupper(substr((string) $row->status, 0, 1)), + strtoupper(substr((string)$row->status, 0, 1)), $row->reason, ]); } fclose($out); - return; } @@ -799,7 +795,7 @@ class StaffAttendanceService fputcsv($out, [ $row->user_id, $row->date, - strtoupper(substr((string) $row->status, 0, 1)), + strtoupper(substr((string)$row->status, 0, 1)), $row->reason, ]); } diff --git a/app/Services/Attendance/StudentAttendanceWriterService.php b/app/Services/Attendance/StudentAttendanceWriterService.php index e6758416..1f81b5d9 100644 --- a/app/Services/Attendance/StudentAttendanceWriterService.php +++ b/app/Services/Attendance/StudentAttendanceWriterService.php @@ -70,7 +70,7 @@ class StudentAttendanceWriterService public function resolveStudentSchoolId(int $studentId, ?string $studentSchoolId = null): string { - $studentSchoolId = trim((string) $studentSchoolId); + $studentSchoolId = trim((string)$studentSchoolId); if ($studentSchoolId !== '') { return $studentSchoolId; @@ -78,25 +78,25 @@ class StudentAttendanceWriterService $studentRow = $this->student->query()->select('school_id')->find($studentId); - if (! $studentRow || empty($studentRow->school_id)) { + if (!$studentRow || empty($studentRow->school_id)) { throw new RuntimeException('Student school ID not found.'); } - return (string) $studentRow->school_id; + return (string)$studentRow->school_id; } public function saveOrUpdateStudentAttendance(array $payload): array { - $classSectionId = (int) $payload['class_section_id']; - $studentId = (int) $payload['student_id']; + $classSectionId = (int)$payload['class_section_id']; + $studentId = (int)$payload['student_id']; $studentSchoolId = $this->resolveStudentSchoolId($studentId, $payload['school_id'] ?? null); - $status = strtolower((string) $payload['status']); + $status = strtolower((string)$payload['status']); $reason = $payload['reason'] ?? null; $reported = $payload['is_reported'] ?? null; - $semester = (string) $payload['semester']; - $schoolYear = (string) $payload['school_year']; - $date = (string) $payload['date']; - $classId = (int) $payload['class_id']; + $semester = (string)$payload['semester']; + $schoolYear = (string)$payload['school_year']; + $date = (string)$payload['date']; + $classId = (int)$payload['class_id']; $userId = $payload['user_id'] ?? null; $existing = AttendanceData::query() @@ -110,7 +110,7 @@ class StudentAttendanceWriterService $oldStatus = $existing->status; $this->updateAttendanceData( - (int) $existing->id, + (int)$existing->id, $status, $reason, $reported, @@ -131,7 +131,7 @@ class StudentAttendanceWriterService return [ 'mode' => 'updated', - 'attendance_id' => (int) $existing->id, + 'attendance_id' => (int)$existing->id, 'old_status' => $oldStatus, 'new_status' => $status, 'school_id' => $studentSchoolId, @@ -172,10 +172,10 @@ class StudentAttendanceWriterService return [ 'mode' => 'created', - 'attendance_id' => (int) ($created->id ?? 0), + 'attendance_id' => (int)($created->id ?? 0), 'old_status' => null, 'new_status' => $status, 'school_id' => $studentSchoolId, ]; } -} +} \ No newline at end of file diff --git a/app/Services/Attendance/TeacherAttendanceSubmissionService.php b/app/Services/Attendance/TeacherAttendanceSubmissionService.php index 2b2c1ef5..48798821 100644 --- a/app/Services/Attendance/TeacherAttendanceSubmissionService.php +++ b/app/Services/Attendance/TeacherAttendanceSubmissionService.php @@ -31,7 +31,7 @@ class TeacherAttendanceSubmissionService callable $currentSemester, callable $currentSchoolYear ): array { - $rawSection = trim((string) $data['class_section_id']); + $rawSection = trim((string)$data['class_section_id']); if ($rawSection === '') { throw new RuntimeException('Missing or invalid class section.'); @@ -42,36 +42,36 @@ class TeacherAttendanceSubmissionService ->where('class_section_id', $rawSection) ->first(); - if (! $sectionRow && ctype_digit($rawSection)) { + if (!$sectionRow && ctype_digit($rawSection)) { $sectionRow = DB::table('classSection') ->select('id', 'class_id', 'class_section_id') - ->where('id', (int) $rawSection) + ->where('id', (int)$rawSection) ->first(); } - if (! $sectionRow) { + if (!$sectionRow) { throw new RuntimeException('Invalid class section.'); } - $classSectionId = (int) $sectionRow->class_section_id; - $classSectionPk = (int) ($sectionRow->id ?? 0); + $classSectionId = (int)$sectionRow->class_section_id; + $classSectionPk = (int)($sectionRow->id ?? 0); $alsoSectionId = ($classSectionPk > 0 && $classSectionPk !== $classSectionId) ? $classSectionPk : null; - $classId = (int) $sectionRow->class_id; + $classId = (int)$sectionRow->class_id; $attendanceData = $data['attendance'] ?? []; $teachersData = $data['teachers'] ?? []; - if (empty($attendanceData) || ! is_array($attendanceData)) { + if (empty($attendanceData) || !is_array($attendanceData)) { throw new RuntimeException('No student attendance data provided.'); } - if (empty($teachersData) || ! is_array($teachersData)) { + if (empty($teachersData) || !is_array($teachersData)) { throw new RuntimeException('No teacher attendance data provided.'); } $rawDate = $data['date'] ?? now()->toDateString(); $attendDate = Carbon::parse($rawDate)->toDateString(); - $semester = (string) $currentSemester(); - $schoolYear = (string) $currentSchoolYear(); + $semester = (string)$currentSemester(); + $schoolYear = (string)$currentSchoolYear(); $dayRow = AttendanceDay::query() ->where('class_section_id', $classSectionId) @@ -94,7 +94,7 @@ class TeacherAttendanceSubmissionService 'school_year' => $schoolYear, ]; - if (! $this->attendancePolicyService->canEditDay($dayPolicyRow, $currentUser)) { + if (!$this->attendancePolicyService->canEditDay($dayPolicyRow, $currentUser)) { throw new RuntimeException('Attendance is locked for your role.'); } @@ -107,7 +107,7 @@ class TeacherAttendanceSubmissionService $existingByStudent = []; foreach ($existingRows as $row) { - $existingByStudent[(int) $row->student_id] = $row->toArray(); + $existingByStudent[(int)$row->student_id] = $row->toArray(); } $allowedStatuses = ['present', 'absent', 'late']; @@ -129,10 +129,10 @@ class TeacherAttendanceSubmissionService } foreach ($assignedTeachers as $teacher) { - $teacherId = (int) $teacher['teacher_id']; - $status = strtolower((string) ($teachersData[$teacherId]['status'] ?? '')); + $teacherId = (int)$teacher['teacher_id']; + $status = strtolower((string)($teachersData[$teacherId]['status'] ?? '')); - if (! in_array($status, $allowedStatuses, true)) { + if (!in_array($status, $allowedStatuses, true)) { throw new RuntimeException('Please fill teacher attendance for all assigned teachers.'); } } @@ -168,13 +168,13 @@ class TeacherAttendanceSubmissionService ); foreach ($assignedTeachers as $teacher) { - $teacherId = (int) $teacher['teacher_id']; - $position = strtolower((string) ($teacher['position'] ?? 'main')); + $teacherId = (int)$teacher['teacher_id']; + $position = strtolower((string)($teacher['position'] ?? 'main')); $position = $position === 'ta' ? 'ta' : 'main'; $staffPayload = [ 'role_name' => 'Teacher', - 'status' => strtolower((string) ($teachersData[$teacherId]['status'] ?? 'present')), + 'status' => strtolower((string)($teachersData[$teacherId]['status'] ?? 'present')), 'reason' => ($teachersData[$teacherId]['reason'] ?? null) ?: null, 'updated_at' => now(), 'created_at' => now(), @@ -198,9 +198,9 @@ class TeacherAttendanceSubmissionService } foreach ($attendanceData as $row) { - $studentId = (int) ($row['student_id'] ?? 0); - $status = strtolower(trim((string) ($row['status'] ?? ''))); - $reason = trim((string) ($row['reason'] ?? '')); + $studentId = (int)($row['student_id'] ?? 0); + $status = strtolower(trim((string)($row['status'] ?? ''))); + $reason = trim((string)($row['reason'] ?? '')); $existing = $existingByStudent[$studentId] ?? null; if ($isTeacher && $existing) { diff --git a/app/Services/AttendanceManagement/AttendanceManagementService.php b/app/Services/AttendanceManagement/AttendanceManagementService.php index 1a73bde3..5c9226c6 100644 --- a/app/Services/AttendanceManagement/AttendanceManagementService.php +++ b/app/Services/AttendanceManagement/AttendanceManagementService.php @@ -7,21 +7,17 @@ use App\Models\LateSlipLog; use App\Models\Student; use App\Models\User; use Carbon\Carbon; +use Illuminate\Support\Arr; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Schema; class AttendanceManagementService { public const STATUS_PRESENT = 'present'; - public const STATUS_ABSENT_PENDING = 'absent_pending_verification'; - public const STATUS_ABSENT = 'absent'; - public const STATUS_LATE = 'late'; - public const STATUS_EARLY_DISMISSAL = 'early_dismissal'; - public const STATUS_LATE_WITHOUT_SLIP = 'late_without_slip'; public function dashboard(array $filters = []): array @@ -208,7 +204,6 @@ class AttendanceManagementService if ($person['type'] === 'student' && $status === self::STATUS_LATE) { $row['late_slip'] = $this->printLateSlip($row, $actor); } - return $row; } @@ -258,7 +253,6 @@ class AttendanceManagementService if (($data['exit_method'] ?? '') === 'manual_exit') { $this->recordBadgeException((int) $id, $person, $exitAt, 'manual exit entry', $badgeExceptions, $actor, $data['notes'] ?? null); } - return $row; } @@ -298,7 +292,6 @@ class AttendanceManagementService ]; $id = DB::table('attendance_management_events')->insertGetId($row); $row['id'] = $id; - return $row; } @@ -319,7 +312,6 @@ class AttendanceManagementService $update['report_status'] = $this->normalizeReportStatus($data['report_status']); } DB::table('attendance_management_events')->where('id', $id)->update($update); - return (array) DB::table('attendance_management_events')->where('id', $id)->first(); } @@ -355,7 +347,6 @@ class AttendanceManagementService 'final_decision' => 'Late slip reprinted', 'updated_at' => now(), ]); - return $row; } @@ -376,24 +367,12 @@ class AttendanceManagementService $student = null; $user = null; - if ($type === 'student' && $id > 0) { - $student = Student::query()->find($id); - } - if (in_array($type, ['staff', 'user', 'teacher', 'administrator'], true) && $id > 0) { - $user = User::query()->find($id); - } - if (! $student && $badge !== '') { - $student = Student::query()->where('rfid_tag', $badge)->first(); - } - if (! $student && ! $user && $badge !== '') { - $user = User::query()->where('rfid_tag', $badge)->first(); - } - if (! $student && ! $user && $id > 0) { - $student = Student::query()->find($id); - } - if (! $student && ! $user && $id > 0) { - $user = User::query()->find($id); - } + if ($type === 'student' && $id > 0) $student = Student::query()->find($id); + if (in_array($type, ['staff', 'user', 'teacher', 'administrator'], true) && $id > 0) $user = User::query()->find($id); + if (! $student && $badge !== '') $student = Student::query()->where('rfid_tag', $badge)->first(); + if (! $student && ! $user && $badge !== '') $user = User::query()->where('rfid_tag', $badge)->first(); + if (! $student && ! $user && $id > 0) $student = Student::query()->find($id); + if (! $student && ! $user && $id > 0) $user = User::query()->find($id); if ($student) { return [ @@ -418,7 +397,6 @@ class AttendanceManagementService if ($name === '') { throw new \InvalidArgumentException('Person could not be resolved. Provide person_id, badge_id, or person_name.'); } - return [ 'type' => $type !== '' ? $type : 'visitor', 'id' => $id > 0 ? $id : null, @@ -441,7 +419,6 @@ class AttendanceManagementService 'admin_name' => $actor ? trim(($actor->firstname ?? '').' '.($actor->lastname ?? '')) : 'System', ]; $logged = LateSlipLog::logSlip($payload, $actor?->id); - return [ 'printed' => $logged, 'slip_number' => 'AME-'.($event['id'] ?? time()), @@ -481,16 +458,9 @@ class AttendanceManagementService $absence = (clone $q)->where('attendance_status', self::STATUS_ABSENT)->count(); $late = (clone $q)->whereIn('attendance_status', [self::STATUS_LATE, self::STATUS_LATE_WITHOUT_SLIP])->count(); $ed = (clone $q)->where('attendance_status', self::STATUS_EARLY_DISMISSAL)->count(); - if ($newStatus === self::STATUS_ABSENT) { - $absence++; - } - if (in_array($newStatus, [self::STATUS_LATE, self::STATUS_LATE_WITHOUT_SLIP], true)) { - $late++; - } - if ($newStatus === self::STATUS_EARLY_DISMISSAL) { - $ed++; - } - + if ($newStatus === self::STATUS_ABSENT) $absence++; + if (in_array($newStatus, [self::STATUS_LATE, self::STATUS_LATE_WITHOUT_SLIP], true)) $late++; + if ($newStatus === self::STATUS_EARLY_DISMISSAL) $ed++; return ['absence_count' => $absence, 'late_count' => $late, 'early_dismissal_count' => $ed]; } @@ -558,7 +528,6 @@ class AttendanceManagementService private function schoolStartFor(Carbon $date): Carbon { $configured = Configuration::getConfigValueByKey('school_start_time') ?: config('attendance.school_start_time', '09:00:00'); - return Carbon::parse($date->toDateString().' '.$configured, $date->timezone); } @@ -647,20 +616,12 @@ class AttendanceManagementService })); } - private function dateTime($value): Carbon - { - return $value instanceof Carbon ? $value : Carbon::parse((string) $value); - } - - private function dateOnly($value): string - { - return Carbon::parse((string) $value)->toDateString(); - } + private function dateTime($value): Carbon { return $value instanceof Carbon ? $value : Carbon::parse((string) $value); } + private function dateOnly($value): string { return Carbon::parse((string) $value)->toDateString(); } private function normalizeReportStatus($value): string { $v = strtolower(str_replace(['-', ' '], '_', trim((string) $value))); - return in_array($v, ['reported', 'not_reported', 'pending_clarification'], true) ? $v : 'pending_clarification'; } @@ -680,61 +641,30 @@ class AttendanceManagementService private function combinationCode(int $abs, int $late, int $ed, int $badge): string { $parts = []; - if ($abs > 0) { - $parts[] = min($abs, 5).'ABS'; - } - if ($late > 0) { - $parts[] = min($late, 5).'Late'; - } - if ($ed > 0) { - $parts[] = min($ed, 3).'ED'; - } - if ($badge > 0) { - $parts[] = min($badge, 5).'Badge Exceptions'; - } - + if ($abs > 0) $parts[] = min($abs, 5).'ABS'; + if ($late > 0) $parts[] = min($late, 5).'Late'; + if ($ed > 0) $parts[] = min($ed, 3).'ED'; + if ($badge > 0) $parts[] = min($badge, 5).'Badge Exceptions'; return $parts ? implode(' + ', $parts) : 'Clear'; } private function riskLevel(int $abs, int $late, int $ed, int $badge): string { - if ($abs >= 5 && $late >= 5) { - return 'severe'; - } - if ($abs >= 5 || $late >= 5) { - return 'critical'; - } - if ($abs + $late + $ed + $badge >= 6 || $abs >= 4 || $late >= 4) { - return 'very_high'; - } - if ($abs + $late + $ed + $badge >= 4 || $abs >= 3 || $late >= 3 || $badge >= 4) { - return 'high'; - } - if ($abs + $late + $ed + $badge >= 2 || $badge >= 2) { - return 'medium'; - } - + if ($abs >= 5 && $late >= 5) return 'severe'; + if ($abs >= 5 || $late >= 5) return 'critical'; + if ($abs + $late + $ed + $badge >= 6 || $abs >= 4 || $late >= 4) return 'very_high'; + if ($abs + $late + $ed + $badge >= 4 || $abs >= 3 || $late >= 3 || $badge >= 4) return 'high'; + if ($abs + $late + $ed + $badge >= 2 || $badge >= 2) return 'medium'; return 'low'; } private function actionNeeded(string $status, string $reportStatus, int $badgeCount): string { - if ($badgeCount >= 5) { - return 'Leadership review for badge compliance'; - } - if ($badgeCount >= 4) { - return 'Formal badge follow-up required'; - } - if ($status === self::STATUS_LATE && $reportStatus !== 'reported') { - return 'Print late slip and contact parent or staff'; - } - if ($status === self::STATUS_ABSENT && $reportStatus !== 'reported') { - return 'Same-day contact required'; - } - if ($status === self::STATUS_EARLY_DISMISSAL && $reportStatus !== 'reported') { - return 'Verify authorization and contact guardian/supervisor'; - } - + if ($badgeCount >= 5) return 'Leadership review for badge compliance'; + if ($badgeCount >= 4) return 'Formal badge follow-up required'; + if ($status === self::STATUS_LATE && $reportStatus !== 'reported') return 'Print late slip and contact parent or staff'; + if ($status === self::STATUS_ABSENT && $reportStatus !== 'reported') return 'Same-day contact required'; + if ($status === self::STATUS_EARLY_DISMISSAL && $reportStatus !== 'reported') return 'Verify authorization and contact guardian/supervisor'; return 'Record and monitor'; } diff --git a/app/Services/AttendanceTracking/AttendanceCaseQueryService.php b/app/Services/AttendanceTracking/AttendanceCaseQueryService.php index a0ba324e..2afe6d83 100644 --- a/app/Services/AttendanceTracking/AttendanceCaseQueryService.php +++ b/app/Services/AttendanceTracking/AttendanceCaseQueryService.php @@ -17,7 +17,8 @@ class AttendanceCaseQueryService protected AttendanceTracking $attendanceTrackingModel, protected ViolationRuleEngineService $violationRuleEngine, protected AttendanceParentLookupService $parentLookupService - ) {} + ) { + } public function getStudentCase( int $studentId, @@ -31,16 +32,16 @@ class AttendanceCaseQueryService ?string $defaultSemester = null ): array { $student = $this->studentModel->query()->find($studentId); - if (! $student) { + if (!$student) { return [ 'success' => false, 'message' => 'Student not found', - 'status' => 404, + 'status' => 404, ]; } $student = $student->toArray(); - $student['name'] = trim(($student['firstname'] ?? '').' '.($student['lastname'] ?? '')); + $student['name'] = trim(($student['firstname'] ?? '') . ' ' . ($student['lastname'] ?? '')); $codeParam = strtoupper((string) ($codeParam ?? '')); $incidentDate = preg_match('/^\d{4}-\d{2}-\d{2}$/', (string) $incidentDate) ? (string) $incidentDate : ''; @@ -65,13 +66,13 @@ class AttendanceCaseQueryService } [$syStart, $syEnd] = $this->violationRuleEngine->deriveSchoolYearBounds($schoolYear ?: date('Y')); - $today = Carbon::today(); - $start = Carbon::parse($syStart)->startOfDay(); + $today = Carbon::today(); + $start = Carbon::parse($syStart)->startOfDay(); $rangeEnd = Carbon::parse(min($syEnd, $today->format('Y-m-d')))->startOfDay(); $windowWeeks = $this->violationRuleEngine->windowWeeksForViolationCode($codeParam); - $endYmd = $incidentDate !== '' ? $incidentDate : date('Y-m-d'); - $startYmd = $this->violationRuleEngine->activeWeekWindowStartYmd($endYmd, $windowWeeks, $schoolYear, $semester); + $endYmd = $incidentDate !== '' ? $incidentDate : date('Y-m-d'); + $startYmd = $this->violationRuleEngine->activeWeekWindowStartYmd($endYmd, $windowWeeks, $schoolYear, $semester); $studentIdValues = [(int) $studentId]; $studentCodeRaw = trim((string) ($student['student_code'] ?? $student['school_id'] ?? '')); @@ -107,7 +108,7 @@ class AttendanceCaseQueryService ]) ->where(function ($q) use ($studentIdValues, $studentCodeValues) { $q->whereIn('student_id', $studentIdValues); - if (! empty($studentCodeValues)) { + if (!empty($studentCodeValues)) { $q->orWhereIn('student_id', $studentCodeValues); } }) @@ -125,7 +126,7 @@ class AttendanceCaseQueryService if ($pendingOnly) { $this->violationRuleEngine->applyUnreportedAttendanceFilter($qb, $this->attendanceDataModel->getTable()); - if (! $includeNotified) { + if (!$includeNotified) { $qb->whereRaw("(LOWER(TRIM(COALESCE(is_notified, ''))) NOT IN ('yes','1'))"); } } @@ -141,21 +142,21 @@ class AttendanceCaseQueryService $attendanceRows = $qb->orderByDesc('date')->get()->toArray(); $data = [ - 'student' => $student, - 'attendance' => $attendanceRows, - 'violation_code' => $codeParam !== '' ? $codeParam : null, - 'violation_date' => $incidentDate, - 'violation_notified' => null, + 'student' => $student, + 'attendance' => $attendanceRows, + 'violation_code' => $codeParam !== '' ? $codeParam : null, + 'violation_date' => $incidentDate, + 'violation_notified' => null, 'show_violation_summary' => true, - 'school_year' => $schoolYear, - 'semester' => $semester, + 'school_year' => $schoolYear, + 'semester' => $semester, ]; - if ($data['violation_date'] === '' && ! empty($data['attendance'][0]['date'])) { + if ($data['violation_date'] === '' && !empty($data['attendance'][0]['date'])) { $data['violation_date'] = substr((string) $data['attendance'][0]['date'], 0, 10); } - if (! empty($data['attendance'][0]['is_notified'])) { + if (!empty($data['attendance'][0]['is_notified'])) { $data['violation_notified'] = $data['attendance'][0]['is_notified']; } @@ -171,7 +172,7 @@ class AttendanceCaseQueryService return [ 'success' => true, - 'data' => $data, + 'data' => $data, ]; } @@ -182,7 +183,7 @@ class AttendanceCaseQueryService ?string $defaultSemester = null ): array { $schoolYear = filled($schoolYearParam) ? (string) $schoolYearParam : (string) $defaultSchoolYear; - $semester = filled($semesterParam) ? (string) $semesterParam : (string) $defaultSemester; + $semester = filled($semesterParam) ? (string) $semesterParam : (string) $defaultSemester; $trackingData = $this->attendanceTrackingModel->query() ->where('school_year', $schoolYear) @@ -210,7 +211,7 @@ class AttendanceCaseQueryService if ($latest) { $schoolYear = (string) ($latest->school_year ?? $schoolYear); - $semester = (string) ($latest->semester ?? $semester); + $semester = (string) ($latest->semester ?? $semester); $trackingData = $this->attendanceTrackingModel->query() ->where('school_year', $schoolYear) @@ -228,9 +229,9 @@ class AttendanceCaseQueryService if (empty($trackingData)) { return [ - 'students' => [], - 'school_year' => $schoolYear, - 'semester' => $semester, + 'students' => [], + 'school_year' => $schoolYear, + 'semester' => $semester, 'show_actions' => false, ]; } @@ -257,9 +258,9 @@ class AttendanceCaseQueryService if (empty($trackingData)) { return [ - 'students' => [], - 'school_year' => $schoolYear, - 'semester' => $semester, + 'students' => [], + 'school_year' => $schoolYear, + 'semester' => $semester, 'show_actions' => false, ]; } @@ -288,12 +289,12 @@ class AttendanceCaseQueryService foreach ($rows as $row) { $sid = (int) ($row->student_id ?? 0); - if ($sid > 0 && ! isset($classByStudent[$sid])) { + if ($sid > 0 && !isset($classByStudent[$sid])) { $classByStudent[$sid] = [ - 'class_section_id' => $row->class_section_id ?? null, + 'class_section_id' => $row->class_section_id ?? null, 'class_section_name' => (string) ($row->class_section_name ?? ''), - 'class_id' => $row->class_id ?? null, - 'class_name' => (string) ($row->class_name ?? ''), + 'class_id' => $row->class_id ?? null, + 'class_name' => (string) ($row->class_name ?? ''), ]; } } @@ -303,7 +304,7 @@ class AttendanceCaseQueryService $mergedData = []; foreach ($trackingData as $row) { $sid = (int) ($row['student_id'] ?? 0); - if (! isset($studentMap[$sid])) { + if (!isset($studentMap[$sid])) { continue; } @@ -311,15 +312,15 @@ class AttendanceCaseQueryService if (isset($classByStudent[$sid])) { $merged['class_section_name'] = (string) ($classByStudent[$sid]['class_section_name'] ?? ''); - $merged['class_name'] = (string) ($classByStudent[$sid]['class_name'] ?? ''); - } elseif (! empty($merged['registration_grade'])) { + $merged['class_name'] = (string) ($classByStudent[$sid]['class_name'] ?? ''); + } elseif (!empty($merged['registration_grade'])) { $merged['grade'] = (string) $merged['registration_grade']; } try { $p = $this->parentLookupService->getPrimaryParentForStudent($sid); if ($p) { - $merged['parent_name'] = $p['parent_name'] ?? ''; + $merged['parent_name'] = $p['parent_name'] ?? ''; $merged['parent_email'] = $p['email'] ?? ''; $merged['parent_phone'] = $p['phone'] ?? ''; } @@ -330,9 +331,9 @@ class AttendanceCaseQueryService } return [ - 'students' => $mergedData, + 'students' => $mergedData, 'school_year' => $schoolYear, - 'semester' => $semester, + 'semester' => $semester, ]; } -} +} \ No newline at end of file diff --git a/app/Services/AttendanceTracking/AttendanceCommunicationSupportService.php b/app/Services/AttendanceTracking/AttendanceCommunicationSupportService.php index 5c475832..7e8cb2e8 100644 --- a/app/Services/AttendanceTracking/AttendanceCommunicationSupportService.php +++ b/app/Services/AttendanceTracking/AttendanceCommunicationSupportService.php @@ -10,7 +10,6 @@ use Illuminate\Support\Facades\DB; class AttendanceCommunicationSupportService { protected string $semester; - protected string $schoolYear; public function __construct( @@ -20,7 +19,7 @@ class AttendanceCommunicationSupportService protected AttendanceParentLookupService $parentLookupService, protected AttendanceEmailComposerService $emailComposerService ) { - $this->semester = (string) ($this->configModel->getConfig('semester') ?? ''); + $this->semester = (string) ($this->configModel->getConfig('semester') ?? ''); $this->schoolYear = (string) ($this->configModel->getConfig('school_year') ?? ''); } @@ -34,12 +33,12 @@ class AttendanceCommunicationSupportService return [ 'success' => false, 'message' => 'Missing student_id.', - 'status' => 422, + 'status' => 422, ]; } - $student = $this->studentModel->query()->find($studentId)?->toArray() ?? []; - $parent = $this->parentLookupService->getPrimaryParentForStudent($studentId); + $student = $this->studentModel->query()->find($studentId)?->toArray() ?? []; + $parent = $this->parentLookupService->getPrimaryParentForStudent($studentId); $secondary = $this->parentLookupService->getSecondaryParentForStudent($studentId, $this->schoolYear); $result = $this->emailComposerService->composePreview( @@ -53,7 +52,7 @@ class AttendanceCommunicationSupportService ); if (($result['success'] ?? false) && isset($result['data'])) { - $result['data']['semester'] = $this->semester; + $result['data']['semester'] = $this->semester; $result['data']['school_year'] = $this->schoolYear; } @@ -101,4 +100,4 @@ class AttendanceCommunicationSupportService ->values() ->all(); } -} +} \ No newline at end of file diff --git a/app/Services/AttendanceTracking/AttendanceEmailComposerService.php b/app/Services/AttendanceTracking/AttendanceEmailComposerService.php index 4ff13e2d..0ad29f50 100644 --- a/app/Services/AttendanceTracking/AttendanceEmailComposerService.php +++ b/app/Services/AttendanceTracking/AttendanceEmailComposerService.php @@ -4,25 +4,27 @@ namespace App\Services\AttendanceTracking; use App\Models\AttendanceEmailTemplate; use App\Support\MailHtml; +use Illuminate\Support\Str; class AttendanceEmailComposerService { public function __construct( protected AttendanceEmailTemplate $attendanceEmailTemplateModel - ) {} + ) { + } public function buildTemplateContext(array $violation, ?array $parent = null): array { $studentName = (string) ($violation['name'] ?? ''); - $incident = (string) ($violation['last_date'] ?? date('Y-m-d')); + $incident = (string) ($violation['last_date'] ?? date('Y-m-d')); return [ - '{{parent_name}}' => (string) ($parent['parent_name'] ?? 'Parent/Guardian'), - '{{student_name}}' => $studentName, - '{{incident_date}}' => $incident, - '{{school_phone}}' => '978-364-0219', + '{{parent_name}}' => (string) ($parent['parent_name'] ?? 'Parent/Guardian'), + '{{student_name}}' => $studentName, + '{{incident_date}}' => $incident, + '{{school_phone}}' => '978-364-0219', '{{voicemail_phone}}' => (string) ($parent['phone'] ?? '—'), - '{{class_time}}' => '10:00 AM', + '{{class_time}}' => '10:00 AM', ]; } @@ -40,7 +42,7 @@ class AttendanceEmailComposerService ->where('is_active', 1) ->first(); - if (! $row) { + if (!$row) { $row = $this->attendanceEmailTemplateModel->query() ->where('code', $code) ->where('variant', 'default') @@ -51,12 +53,12 @@ class AttendanceEmailComposerService $row = $row?->toArray(); } - if (! $row) { + if (!$row) { return null; } $subject = strtr($row['subject'], $context); - $body = strtr($row['body_html'], $context); + $body = strtr($row['body_html'], $context); return [$subject, $body]; } @@ -68,35 +70,35 @@ class AttendanceEmailComposerService public function pickVariant(string $code, ?string $globalVariantOverride = null): string { - if (! empty($globalVariantOverride)) { + if (!empty($globalVariantOverride)) { return $globalVariantOverride; } return match ($code) { 'ABS_2', 'ABS_2_IN3W' => 'no_answer', - default => 'default', + default => 'default', }; } public function buildFallbackAutoEmail(string $code, array $violation, string $parentName = ''): array { $subject = match ($code) { - 'ABS_1' => 'Attendance Notice: Unreported Absence', + 'ABS_1' => 'Attendance Notice: Unreported Absence', 'LATE_2' => 'Attendance Notice: Repeated Lateness', - default => 'Attendance Notice', + default => 'Attendance Notice', }; $studentName = (string) ($violation['name'] ?? ''); - $date = (string) ($violation['last_date'] ?? date('Y-m-d')); + $date = (string) ($violation['last_date'] ?? date('Y-m-d')); - $body = '

Dear '.($parentName ?: 'Parent/Guardian').'

' - ."

We'd like to inform you about your student's recent attendance:

" - .'
    ' - ."
  • Student: {$studentName}
  • " - .'
  • Issue: '.(string) ($violation['violation'] ?? '').'
  • ' - ."
  • As of: {$date}
  • " - .'
' - .'

If you have any questions, please contact the school office.

'; + $body = "

Dear " . ($parentName ?: 'Parent/Guardian') . "

" + . "

We'd like to inform you about your student's recent attendance:

" + . "
    " + . "
  • Student: {$studentName}
  • " + . "
  • Issue: " . (string) ($violation['violation'] ?? '') . "
  • " + . "
  • As of: {$date}
  • " + . "
" + . "

If you have any questions, please contact the school office.

"; return [$subject, $body]; } @@ -132,8 +134,7 @@ class AttendanceEmailComposerService static function ($m) { $url = $m[0]; $urlAttr = htmlspecialchars($url, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); - - return ''.$url.''; + return '' . $url . ''; }, $escaped ); @@ -141,9 +142,8 @@ class AttendanceEmailComposerService $paragraphs = preg_split("/\n{2,}/", $escaped); $paragraphs = array_map(static function ($p) { - $p = preg_replace("/\n/", '
', $p); - - return '

'.$p.'

'; + $p = preg_replace("/\n/", "
", $p); + return '

' . $p . '

'; }, $paragraphs); return implode("\n", $paragraphs); @@ -152,10 +152,10 @@ class AttendanceEmailComposerService public function renderAutoEmailFor(array $violation, ?array $parent = null): ?array { $code = (string) ($violation['violation_code'] ?? $violation['code'] ?? ''); - $ctx = $this->buildTemplateContext($violation, $parent); - $tpl = $this->renderTemplate($code, 'default', $ctx); + $ctx = $this->buildTemplateContext($violation, $parent); + $tpl = $this->renderTemplate($code, 'default', $ctx); - if (! $tpl) { + if (!$tpl) { return $this->buildFallbackAutoEmail($code, $violation, (string) ($parent['parent_name'] ?? '')); } @@ -176,21 +176,21 @@ class AttendanceEmailComposerService : date('Y-m-d'); $violation = [ - 'id' => $studentId, - 'name' => trim(($student['firstname'] ?? '').' '.($student['lastname'] ?? '')), - 'last_date' => $lastDate, - 'violation' => $code, + 'id' => $studentId, + 'name' => trim(($student['firstname'] ?? '') . ' ' . ($student['lastname'] ?? '')), + 'last_date' => $lastDate, + 'violation' => $code, 'violation_code' => $code, ]; $ctx = $this->buildTemplateContext($violation, $primaryParent); $rendered = $this->renderTemplate($code, $variant, $ctx); - if (! $rendered) { + if (!$rendered) { return [ 'success' => false, 'message' => "Template not found for {$code} ({$variant}).", - 'status' => 404, + 'status' => 404, ]; } @@ -198,19 +198,19 @@ class AttendanceEmailComposerService return [ 'success' => true, - 'data' => [ - 'student_id' => $studentId, - 'student_name' => $violation['name'], - 'parent_email' => (string) ($primaryParent['email'] ?? ''), - 'parent_name' => (string) ($primaryParent['parent_name'] ?? ''), + 'data' => [ + 'student_id' => $studentId, + 'student_name' => $violation['name'], + 'parent_email' => (string) ($primaryParent['email'] ?? ''), + 'parent_name' => (string) ($primaryParent['parent_name'] ?? ''), 'secondary_email' => (string) ($secondaryParent['email'] ?? ''), - 'secondary_name' => (string) ($secondaryParent['parent_name'] ?? ''), - 'code' => $code, - 'variant' => $variant, - 'subject' => $subject, - 'body_html' => $body, - 'incident_date' => $lastDate, + 'secondary_name' => (string) ($secondaryParent['parent_name'] ?? ''), + 'code' => $code, + 'variant' => $variant, + 'subject' => $subject, + 'body_html' => $body, + 'incident_date' => $lastDate, ], ]; } -} +} \ No newline at end of file diff --git a/app/Services/AttendanceTracking/AttendanceMailerService.php b/app/Services/AttendanceTracking/AttendanceMailerService.php index d574ebaf..83f4bc5a 100644 --- a/app/Services/AttendanceTracking/AttendanceMailerService.php +++ b/app/Services/AttendanceTracking/AttendanceMailerService.php @@ -7,4 +7,4 @@ interface AttendanceMailerService public function send(string $to, string $subject, string $html): bool; public function queueAttendanceEvent(array $payload): void; -} +} \ No newline at end of file diff --git a/app/Services/AttendanceTracking/AttendanceNotificationLogService.php b/app/Services/AttendanceTracking/AttendanceNotificationLogService.php index fe153019..18d40397 100644 --- a/app/Services/AttendanceTracking/AttendanceNotificationLogService.php +++ b/app/Services/AttendanceTracking/AttendanceNotificationLogService.php @@ -10,7 +10,8 @@ class AttendanceNotificationLogService { public function __construct( protected ParentNotification $notificationModel - ) {} + ) { + } public function notificationAlreadySent( int $studentId, @@ -30,14 +31,13 @@ class AttendanceNotificationLogService ->where('incident_date', $ymd) ->where('channel', $channel); - if (! empty($to)) { + if (!empty($to)) { $query->where('to_address', $to); } return $query->exists(); } catch (Throwable $e) { - Log::debug('notificationAlreadySent(): '.$e->getMessage()); - + Log::debug('notificationAlreadySent(): ' . $e->getMessage()); return false; } } @@ -61,37 +61,37 @@ class AttendanceNotificationLogService ->whereDate('incident_date', $ymd) ->where('channel', $channel); - if (! empty($to)) { + if (!empty($to)) { $existingQuery->where('to_address', $to); } $existing = $existingQuery->orderByDesc('id')->first(); - if ($existing && ! empty($existing->id)) { + if ($existing && !empty($existing->id)) { $existing->update([ - 'subject' => $subject, - 'status' => $status, - 'response' => $response, - 'semester' => $semester, - 'school_year' => $schoolYear, - 'updated_at' => now(), + 'subject' => $subject, + 'status' => $status, + 'response' => $response, + 'semester' => $semester, + 'school_year' => $schoolYear, + 'updated_at' => now(), ]); } else { $this->notificationModel->query()->create([ - 'student_id' => $studentId, - 'code' => $code, + 'student_id' => $studentId, + 'code' => $code, 'incident_date' => $ymd, - 'channel' => $channel, - 'to_address' => $to, - 'subject' => $subject, - 'status' => $status, - 'response' => $response, - 'semester' => $semester, - 'school_year' => $schoolYear, + 'channel' => $channel, + 'to_address' => $to, + 'subject' => $subject, + 'status' => $status, + 'response' => $response, + 'semester' => $semester, + 'school_year' => $schoolYear, ]); } } catch (Throwable $e) { - Log::debug('logNotification(): '.$e->getMessage()); + Log::debug('logNotification(): ' . $e->getMessage()); } } } diff --git a/app/Services/AttendanceTracking/AttendanceNotificationWorkflowService.php b/app/Services/AttendanceTracking/AttendanceNotificationWorkflowService.php index 4b81d7e6..72969ac5 100644 --- a/app/Services/AttendanceTracking/AttendanceNotificationWorkflowService.php +++ b/app/Services/AttendanceTracking/AttendanceNotificationWorkflowService.php @@ -7,6 +7,7 @@ use App\Models\AttendanceTracking; use App\Models\Configuration; use App\Models\Student; use App\Models\StudentClass; +use Carbon\Carbon; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; use Throwable; @@ -14,7 +15,6 @@ use Throwable; class AttendanceNotificationWorkflowService { protected string $semester; - protected string $schoolYear; public function __construct( @@ -29,33 +29,33 @@ class AttendanceNotificationWorkflowService protected AttendanceEmailComposerService $emailComposerService, protected AttendanceNotificationLogService $notificationLogService ) { - $this->semester = (string) ($this->configModel->getConfig('semester') ?? ''); + $this->semester = (string) ($this->configModel->getConfig('semester') ?? ''); $this->schoolYear = (string) ($this->configModel->getConfig('school_year') ?? ''); } public function record(array $data): array { - $sid = (int) $data['student_id']; - $ymd = substr((string) $data['date'], 0, 10); - $semester = $data['semester'] ?? $this->semester; + $sid = (int) $data['student_id']; + $ymd = substr((string) $data['date'], 0, 10); + $semester = $data['semester'] ?? $this->semester; $schoolYear = $data['school_year'] ?? $this->schoolYear; - $subject = $data['subject_type'] ?? 'Absent'; + $subject = $data['subject_type'] ?? 'Absent'; $student = $this->studentModel->query()->find($sid)?->toArray() ?? []; $parentEmail = $data['parent_email'] ?? ''; - $parentName = $data['parent_name'] ?? ''; + $parentName = $data['parent_name'] ?? ''; - if (! $parentEmail) { + if (!$parentEmail) { $parent = $this->parentLookupService->getPrimaryParentForStudent($sid) ?? []; $parentEmail = $parent['email'] ?? ''; - $parentName = $parent['parent_name'] ?? ''; + $parentName = $parent['parent_name'] ?? ''; } - if (! $parentEmail) { + if (!$parentEmail) { return [ 'success' => false, 'message' => 'No parent email found for this student.', - 'status' => 422, + 'status' => 422, ]; } @@ -87,37 +87,37 @@ class AttendanceNotificationWorkflowService : []; $payload = [ - 'student_id' => $sid, - 'student' => $student, - 'parent' => ['email' => $parentEmail, 'name' => $parentName], - 'semester' => $semester, + 'student_id' => $sid, + 'student' => $student, + 'parent' => ['email' => $parentEmail, 'name' => $parentName], + 'semester' => $semester, 'school_year' => $schoolYear, - 'class' => $class, - 'now' => now()->toDateTimeString(), - 'date' => $ymd, - 'subject' => $subject, + 'class' => $class, + 'now' => now()->toDateTimeString(), + 'date' => $ymd, + 'subject' => $subject, 'consequence' => $consequence ?: 'follow_up', ]; try { $this->attendanceMailerService->queueAttendanceEvent($payload); - if ($row && ! empty($row['id']) && method_exists($this->attendanceTrackingModel, 'markAsNotified')) { + if ($row && !empty($row['id']) && method_exists($this->attendanceTrackingModel, 'markAsNotified')) { $this->attendanceTrackingModel->markAsNotified((int) $row['id']); } return [ 'success' => true, 'message' => 'Notification queued.', - 'data' => $payload, + 'data' => $payload, ]; } catch (Throwable $e) { - Log::error('Attendance record queue failed: '.$e->getMessage()); + Log::error('Attendance record queue failed: ' . $e->getMessage()); return [ 'success' => false, 'message' => $e->getMessage(), - 'status' => 500, + 'status' => 500, ]; } } @@ -132,32 +132,30 @@ class AttendanceNotificationWorkflowService $details = []; foreach ($toSend as $v) { - $sid = (int) ($v['id'] ?? 0); - $code = (string) ($v['violation_code'] ?? $v['code'] ?? ''); - $date = (string) ($v['last_date'] ?? ''); - $to = trim((string) ($v['parent_email'] ?? '')); + $sid = (int) ($v['id'] ?? 0); + $code = (string) ($v['violation_code'] ?? $v['code'] ?? ''); + $date = (string) ($v['last_date'] ?? ''); + $to = trim((string) ($v['parent_email'] ?? '')); $pname = (string) ($v['parent_name'] ?? ''); - if ($sid <= 0 || ! $code || ! $date || ! $to) { + if ($sid <= 0 || !$code || !$date || !$to) { $errors++; $details[] = ['student_id' => $sid, 'code' => $code, 'date' => $date, 'result' => 'missing data']; - continue; } if ($this->notificationLogService->notificationAlreadySent($sid, $code, $date, 'email', $to)) { $skipped++; $details[] = ['student_id' => $sid, 'code' => $code, 'date' => $date, 'result' => 'duplicate']; - continue; } - $parent = $this->parentLookupService->getPrimaryParentForStudent($sid); + $parent = $this->parentLookupService->getPrimaryParentForStudent($sid); $context = $this->emailComposerService->buildTemplateContext($v, $parent); $variant = $this->emailComposerService->pickVariant($code, $variantOverride); - $tpl = $this->emailComposerService->renderTemplate($code, $variant, $context); + $tpl = $this->emailComposerService->renderTemplate($code, $variant, $context); - if (! $tpl) { + if (!$tpl) { [$subject, $body] = $this->emailComposerService->buildFallbackAutoEmail($code, $v, $pname); } else { [$subject, $body] = $tpl; @@ -214,7 +212,7 @@ class AttendanceNotificationWorkflowService $this->semester, $this->schoolYear ); - $details[] = ['student_id' => $sid, 'code' => $code, 'date' => $date, 'result' => 'error: '.$e->getMessage()]; + $details[] = ['student_id' => $sid, 'code' => $code, 'date' => $date, 'result' => 'error: ' . $e->getMessage()]; } try { @@ -262,7 +260,7 @@ class AttendanceNotificationWorkflowService } } catch (Throwable $e) { $errors++; - $details[] = ['student_id' => $sid, 'code' => $code, 'date' => $date, 'result' => 'cc_error: '.$e->getMessage()]; + $details[] = ['student_id' => $sid, 'code' => $code, 'date' => $date, 'result' => 'cc_error: ' . $e->getMessage()]; } if ($anyOk) { @@ -279,13 +277,13 @@ class AttendanceNotificationWorkflowService } $allDates = []; - $absDates = array_map(fn ($d) => substr((string) $d, 0, 10), (array) ($v['absences'] ?? [])); + $absDates = array_map(fn ($d) => substr((string) $d, 0, 10), (array) ($v['absences'] ?? [])); $lateDates = array_map(fn ($d) => substr((string) $d, 0, 10), (array) ($v['lates'] ?? [])); - if (! empty($absDates)) { + if (!empty($absDates)) { $allDates = array_merge($allDates, $absDates); } - if (! empty($lateDates)) { + if (!empty($lateDates)) { $allDates = array_merge($allDates, $lateDates); } if (empty($allDates)) { @@ -305,29 +303,29 @@ class AttendanceNotificationWorkflowService return [ 'success' => true, - 'sent' => $sent, + 'sent' => $sent, 'skipped' => $skipped, - 'errors' => $errors, + 'errors' => $errors, 'details' => $details, ]; } public function sendEmailManual(array $data, array $violationDates = []): array { - $sid = (int) $data['student_id']; - $to = trim((string) $data['to']); - $subject = (string) $data['subject']; + $sid = (int) $data['student_id']; + $to = trim((string) $data['to']); + $subject = (string) $data['subject']; $bodyInput = (string) $data['body_html']; - $code = (string) $data['code']; - $variant = (string) ($data['variant'] ?? 'default'); + $code = (string) $data['code']; + $variant = (string) ($data['variant'] ?? 'default'); $ymdRaw = (string) ($data['incident_date'] ?? ''); - $ymd = $ymdRaw !== '' ? substr($ymdRaw, 0, 10) : ''; + $ymd = $ymdRaw !== '' ? substr($ymdRaw, 0, 10) : ''; $safeHtmlBody = $this->emailComposerService->normalizeBodyToHtml($bodyInput); $wrapped = $this->emailComposerService->renderWithEmailLayout($subject, $safeHtmlBody); - $semester = $this->semester ?: (string) ($this->configModel->getConfig('semester') ?? ''); + $semester = $this->semester ?: (string) ($this->configModel->getConfig('semester') ?? ''); $schoolYear = $this->schoolYear ?: (string) ($this->configModel->getConfig('school_year') ?? ''); try { @@ -356,7 +354,7 @@ class AttendanceNotificationWorkflowService $to2 = trim((string) ($sec['email'] ?? '')); if ($to2 !== '' && strcasecmp($to2, $to) !== 0) { - if (! $this->notificationLogService->notificationAlreadySent($sid, $code, $ymd, 'email', $to2)) { + if (!$this->notificationLogService->notificationAlreadySent($sid, $code, $ymd, 'email', $to2)) { $ok2 = $this->attendanceMailerService->send($to2, $subject, $wrapped); $this->notificationLogService->logNotification( @@ -378,11 +376,11 @@ class AttendanceNotificationWorkflowService } } - if (! $anyOk) { + if (!$anyOk) { return [ 'success' => false, 'message' => 'Failed to send email.', - 'status' => 500, + 'status' => 500, ]; } @@ -396,7 +394,7 @@ class AttendanceNotificationWorkflowService ); } - if (! empty($violationDates) && method_exists($this->attendanceDataModel, 'markReportedAndNotified')) { + if (!empty($violationDates) && method_exists($this->attendanceDataModel, 'markReportedAndNotified')) { $this->attendanceDataModel->markReportedAndNotified( $sid, $violationDates, @@ -408,12 +406,12 @@ class AttendanceNotificationWorkflowService return [ 'success' => true, 'message' => 'Email sent successfully.', - 'data' => [ - 'student_id' => $sid, - 'to' => $to, - 'secondary_to' => $to2 ?? null, - 'subject' => $subject, - 'variant' => $variant, + 'data' => [ + 'student_id' => $sid, + 'to' => $to, + 'secondary_to' => $to2 ?? null, + 'subject' => $subject, + 'variant' => $variant, 'incident_date' => $ymd, ], ]; @@ -433,19 +431,19 @@ class AttendanceNotificationWorkflowService return [ 'success' => false, - 'message' => 'Error: '.$e->getMessage(), - 'status' => 500, + 'message' => 'Error: ' . $e->getMessage(), + 'status' => 500, ]; } } public function saveNotificationNote(array $data): array { - $sid = (int) $data['student_id']; - $code = (string) $data['code']; - $note = trim((string) $data['note']); + $sid = (int) $data['student_id']; + $code = (string) $data['code']; + $note = trim((string) $data['note']); $ymdRaw = (string) ($data['incident_date'] ?? ''); - $ymd = $ymdRaw !== '' ? substr($ymdRaw, 0, 10) : ''; + $ymd = $ymdRaw !== '' ? substr($ymdRaw, 0, 10) : ''; try { $day = $ymd !== '' ? $ymd : date('Y-m-d'); @@ -457,11 +455,11 @@ class AttendanceNotificationWorkflowService ->where('school_year', $this->schoolYear) ->where('date', '>=', $start) ->where('date', '<', $end) - ->where('reason', 'like', '%'.$code.'%') + ->where('reason', 'like', '%' . $code . '%') ->orderByDesc('date') ->first(); - if (! $row) { + if (!$row) { $row = $this->attendanceTrackingModel->query() ->where('student_id', $sid) ->where('semester', $this->semester) @@ -472,28 +470,28 @@ class AttendanceNotificationWorkflowService ->first(); } - if ($row && ! empty($row->id)) { + if ($row && !empty($row->id)) { DB::table($this->attendanceTrackingModel->getTable()) ->where('id', (int) $row->id) ->update([ 'notif_counter' => DB::raw('COALESCE(notif_counter,0)+1'), - 'note' => $note, - 'is_notified' => 1, - 'updated_at' => now(), + 'note' => $note, + 'is_notified' => 1, + 'updated_at' => now(), ]); } else { $this->attendanceTrackingModel->query()->create([ - 'student_id' => $sid, - 'date' => $start, - 'is_reported' => 0, - 'reason' => $code, - 'is_notified' => 1, + 'student_id' => $sid, + 'date' => $start, + 'is_reported' => 0, + 'reason' => $code, + 'is_notified' => 1, 'notif_counter' => 1, - 'semester' => $this->semester, - 'school_year' => $this->schoolYear, - 'note' => $note, - 'created_at' => now(), - 'updated_at' => now(), + 'semester' => $this->semester, + 'school_year' => $this->schoolYear, + 'note' => $note, + 'created_at' => now(), + 'updated_at' => now(), ]); } @@ -504,9 +502,9 @@ class AttendanceNotificationWorkflowService } catch (Throwable $e) { return [ 'success' => false, - 'message' => 'Failed to save note: '.$e->getMessage(), - 'status' => 500, + 'message' => 'Failed to save note: ' . $e->getMessage(), + 'status' => 500, ]; } } -} +} \ No newline at end of file diff --git a/app/Services/AttendanceTracking/AttendanceParentLookupService.php b/app/Services/AttendanceTracking/AttendanceParentLookupService.php index 60d7d1c6..dfebc7bc 100644 --- a/app/Services/AttendanceTracking/AttendanceParentLookupService.php +++ b/app/Services/AttendanceTracking/AttendanceParentLookupService.php @@ -16,15 +16,15 @@ class AttendanceParentLookupService ->where('s.id', $studentId) ->first(); - if (! $row || empty($row->user_id)) { + if (!$row || empty($row->user_id)) { return null; } return [ - 'user_id' => (int) $row->user_id, - 'email' => (string) ($row->email ?? ''), + 'user_id' => (int) $row->user_id, + 'email' => (string) ($row->email ?? ''), 'parent_name' => (string) ($row->parent_name ?? ''), - 'phone' => $row->cellphone ?? null, + 'phone' => $row->cellphone ?? null, ]; } @@ -36,11 +36,11 @@ class AttendanceParentLookupService ->where('id', $studentId) ->first(); - if (! $stu) { + if (!$stu) { return null; } - $primaryId = (int) ($stu->parent_id ?? 0); + $primaryId = (int) ($stu->parent_id ?? 0); $schoolYear = (string) ($stu->school_year ?? $fallbackSchoolYear ?? ''); $pb = DB::table('parents')->where('firstparent_id', $primaryId); @@ -49,7 +49,7 @@ class AttendanceParentLookupService } $pRow = $pb->orderByDesc('updated_at')->first(); - if (! $pRow) { + if (!$pRow) { return null; } @@ -60,12 +60,12 @@ class AttendanceParentLookupService ->where('id', $secondId) ->first(); - if ($u2 && ! empty($u2->email)) { + if ($u2 && !empty($u2->email)) { return [ - 'user_id' => (int) $u2->id, - 'email' => (string) $u2->email, - 'parent_name' => trim(($u2->firstname ?? '').' '.($u2->lastname ?? '')), - 'phone' => (string) ($u2->cellphone ?? ''), + 'user_id' => (int) $u2->id, + 'email' => (string) $u2->email, + 'parent_name' => trim(($u2->firstname ?? '') . ' ' . ($u2->lastname ?? '')), + 'phone' => (string) ($u2->cellphone ?? ''), ]; } } @@ -73,18 +73,18 @@ class AttendanceParentLookupService $fallbackEmail = (string) ($pRow->secondparent_email ?? ''); $fallbackPhone = (string) ($pRow->secondparent_phone ?? ''); $fallbackFirst = (string) ($pRow->secondparent_firstname ?? ''); - $fallbackLast = (string) ($pRow->secondparent_lastname ?? ''); + $fallbackLast = (string) ($pRow->secondparent_lastname ?? ''); if ($fallbackEmail || $fallbackPhone || $fallbackFirst || $fallbackLast) { return [ - 'user_id' => $secondId ?: null, - 'email' => $fallbackEmail, - 'parent_name' => trim($fallbackFirst.' '.$fallbackLast) ?: null, - 'phone' => $fallbackPhone, + 'user_id' => $secondId ?: null, + 'email' => $fallbackEmail, + 'parent_name' => trim($fallbackFirst . ' ' . $fallbackLast) ?: null, + 'phone' => $fallbackPhone, ]; } } catch (Throwable $e) { - Log::error('getSecondaryParentForStudent() failed: '.$e->getMessage()); + Log::error('getSecondaryParentForStudent() failed: ' . $e->getMessage()); } return null; @@ -96,7 +96,7 @@ class AttendanceParentLookupService return [ 'success' => false, 'message' => 'Missing student_id', - 'status' => 422, + 'status' => 422, ]; } @@ -106,15 +106,15 @@ class AttendanceParentLookupService ->where('id', $studentId) ->first(); - if (! $stu) { + if (!$stu) { return [ 'success' => false, 'message' => 'Student not found', - 'status' => 404, + 'status' => 404, ]; } - $primaryId = (int) $stu->parent_id; + $primaryId = (int) $stu->parent_id; $schoolYear = $stu->school_year ?: $currentSchoolYear; $u1 = DB::table('users') @@ -123,39 +123,39 @@ class AttendanceParentLookupService ->first(); $primary = $u1 ? [ - 'id' => (int) $u1->id, + 'id' => (int) $u1->id, 'firstname' => (string) $u1->firstname, - 'lastname' => (string) $u1->lastname, - 'email' => (string) $u1->email, + 'lastname' => (string) $u1->lastname, + 'email' => (string) $u1->email, 'cellphone' => (string) $u1->cellphone, ] : null; $secondaryRaw = $this->getSecondaryParentForStudent($studentId, $schoolYear); $secondary = $secondaryRaw ? [ - 'id' => $secondaryRaw['user_id'] ?? null, + 'id' => $secondaryRaw['user_id'] ?? null, 'firstname' => null, - 'lastname' => null, - 'email' => $secondaryRaw['email'] ?? '', + 'lastname' => null, + 'email' => $secondaryRaw['email'] ?? '', 'cellphone' => $secondaryRaw['phone'] ?? '', - 'name' => $secondaryRaw['parent_name'] ?? '', + 'name' => $secondaryRaw['parent_name'] ?? '', ] : null; return [ 'success' => true, - 'data' => [ - 'primary' => $primary, + 'data' => [ + 'primary' => $primary, 'secondary' => $secondary, ], ]; } catch (Throwable $e) { - Log::error('parentsInfo() failed: '.$e->getMessage()); + Log::error('parentsInfo() failed: ' . $e->getMessage()); return [ 'success' => false, 'message' => 'Server error', - 'status' => 500, + 'status' => 500, ]; } } -} +} \ No newline at end of file diff --git a/app/Services/AttendanceTracking/AttendancePendingViolationService.php b/app/Services/AttendanceTracking/AttendancePendingViolationService.php index b7c02912..47160df7 100644 --- a/app/Services/AttendanceTracking/AttendancePendingViolationService.php +++ b/app/Services/AttendanceTracking/AttendancePendingViolationService.php @@ -13,7 +13,8 @@ class AttendancePendingViolationService protected AttendanceData $attendanceDataModel, protected ViolationRuleEngineService $violationRuleEngine, protected AttendanceViolationStudentResolverService $studentResolverService - ) {} + ) { + } public function getPendingViolations( string $defaultSchoolYear, @@ -21,52 +22,52 @@ class AttendancePendingViolationService ?string $semesterParam = null ): array { $schoolYear = filled($schoolYearParam) ? (string) $schoolYearParam : (string) $defaultSchoolYear; - $semester = filled($semesterParam) ? (string) $semesterParam : null; + $semester = filled($semesterParam) ? (string) $semesterParam : null; $debugInfo = [ 'school_year_param' => $schoolYear, - 'semester_param' => $semester, - 'class_students' => 0, - 'student_ids' => 0, - 'attendance_rows' => 0, - 'filtered_rows' => 0, - 'violations' => 0, - 'active_weeks' => 0, - 'abs_last5' => 0, - 'late_last5' => 0, + 'semester_param' => $semester, + 'class_students' => 0, + 'student_ids' => 0, + 'attendance_rows' => 0, + 'filtered_rows' => 0, + 'violations' => 0, + 'active_weeks' => 0, + 'abs_last5' => 0, + 'late_last5' => 0, ]; $resolved = $this->studentResolverService->resolveForSchoolYear($schoolYear, $semester); - $schoolYear = (string) ($resolved['school_year'] ?? $schoolYear); - $semester = $resolved['semester'] ?? $semester; - $studentIds = $resolved['student_ids'] ?? []; - $studentCodes = $resolved['student_codes'] ?? []; - $students = $resolved['students'] ?? []; + $schoolYear = (string) ($resolved['school_year'] ?? $schoolYear); + $semester = $resolved['semester'] ?? $semester; + $studentIds = $resolved['student_ids'] ?? []; + $studentCodes = $resolved['student_codes'] ?? []; + $students = $resolved['students'] ?? []; $studentCodeToId = $resolved['student_code_to_id'] ?? []; - $existingIds = $resolved['existing_ids'] ?? []; - $resolverDebug = $resolved['debug'] ?? []; + $existingIds = $resolved['existing_ids'] ?? []; + $resolverDebug = $resolved['debug'] ?? []; $debugInfo['class_students'] = $resolverDebug['class_students'] ?? 0; - $debugInfo['student_ids'] = $resolverDebug['student_ids'] ?? 0; + $debugInfo['student_ids'] = $resolverDebug['student_ids'] ?? 0; if (empty($studentIds) && empty($studentCodes)) { return [ - 'students' => [], + 'students' => [], 'school_year' => $schoolYear, - 'semester' => $semester, - 'error' => 'No student identifiers found for the current school year.', - 'debug' => $debugInfo, + 'semester' => $semester, + 'error' => 'No student identifiers found for the current school year.', + 'debug' => $debugInfo, ]; } if (empty($students)) { return [ - 'students' => [], + 'students' => [], 'school_year' => $schoolYear, - 'semester' => $semester, - 'error' => 'No students found matching attendance records.', - 'debug' => $debugInfo, + 'semester' => $semester, + 'error' => 'No students found matching attendance records.', + 'debug' => $debugInfo, ]; } @@ -90,14 +91,14 @@ class AttendancePendingViolationService 'is_reported', 'is_notified', ]) - ->when(! empty($schoolYear), fn ($q) => $q->where('school_year', $schoolYear)) - ->when(! empty($semester), fn ($q) => $q->where('semester', $semester)) + ->when(!empty($schoolYear), fn ($q) => $q->where('school_year', $schoolYear)) + ->when(!empty($semester), fn ($q) => $q->where('semester', $semester)) ->where(function ($q) use ($studentIds, $studentCodes) { - if (! empty($studentIds)) { + if (!empty($studentIds)) { $q->whereIn('student_id', $studentIds); } - if (! empty($studentCodes)) { - if (! empty($studentIds)) { + if (!empty($studentCodes)) { + if (!empty($studentIds)) { $q->orWhereIn('student_id', $studentCodes); } else { $q->whereIn('student_id', $studentCodes); @@ -117,16 +118,16 @@ class AttendancePendingViolationService if (empty($termRows)) { $latestAttendanceTerm = DB::table($this->attendanceDataModel->getTable()) ->select('school_year', 'semester', 'date') - ->when(! empty($studentIds), fn ($q) => $q->whereIn('student_id', $studentIds)) + ->when(!empty($studentIds), fn ($q) => $q->whereIn('student_id', $studentIds)) ->orderByDesc('date') ->first(); if ($latestAttendanceTerm) { - $schoolYear = ! empty($latestAttendanceTerm->school_year) + $schoolYear = !empty($latestAttendanceTerm->school_year) ? (string) $latestAttendanceTerm->school_year : $schoolYear; - $semester = ! empty($latestAttendanceTerm->semester) + $semester = !empty($latestAttendanceTerm->semester) ? (string) $latestAttendanceTerm->semester : $semester; @@ -149,14 +150,14 @@ class AttendancePendingViolationService 'is_reported', 'is_notified', ]) - ->when(! empty($schoolYear), fn ($q) => $q->where('school_year', $schoolYear)) - ->when(! empty($semester), fn ($q) => $q->where('semester', $semester)) + ->when(!empty($schoolYear), fn ($q) => $q->where('school_year', $schoolYear)) + ->when(!empty($semester), fn ($q) => $q->where('semester', $semester)) ->where(function ($q) use ($studentIds, $studentCodes) { - if (! empty($studentIds)) { + if (!empty($studentIds)) { $q->whereIn('student_id', $studentIds); } - if (! empty($studentCodes)) { - if (! empty($studentIds)) { + if (!empty($studentCodes)) { + if (!empty($studentIds)) { $q->orWhereIn('student_id', $studentCodes); } else { $q->whereIn('student_id', $studentCodes); @@ -176,10 +177,10 @@ class AttendancePendingViolationService if (empty($termRows)) { return [ - 'students' => [], + 'students' => [], 'school_year' => $schoolYear, - 'semester' => $semester, - 'debug' => $debugInfo, + 'semester' => $semester, + 'debug' => $debugInfo, ]; } } @@ -187,7 +188,7 @@ class AttendancePendingViolationService $attendanceRows = []; foreach ($termRows as $row) { $dStr = $row['date'] ?? null; - if (! $dStr) { + if (!$dStr) { continue; } @@ -197,7 +198,7 @@ class AttendancePendingViolationService continue; } - if ($d->lt($start) || ! $d->lt($endEx)) { + if ($d->lt($start) || !$d->lt($endEx)) { continue; } @@ -221,15 +222,15 @@ class AttendancePendingViolationService if (empty($attendanceRows)) { return [ - 'students' => [], + 'students' => [], 'school_year' => $schoolYear, - 'semester' => $semester, - 'debug' => $debugInfo, + 'semester' => $semester, + 'debug' => $debugInfo, ]; } $debugInfo['attendance_rows'] = count($termRows); - $debugInfo['filtered_rows'] = count($attendanceRows); + $debugInfo['filtered_rows'] = count($attendanceRows); $violations = $this->violationRuleEngine->computeViolations( $students, @@ -240,17 +241,17 @@ class AttendancePendingViolationService ); $ruleDebug = $this->violationRuleEngine->getDebugCompute(); - $debugInfo['violations'] = count($violations); + $debugInfo['violations'] = count($violations); $debugInfo['active_weeks'] = $ruleDebug['active_weeks'] ?? 0; - $debugInfo['by_student'] = $ruleDebug['by_student'] ?? 0; - $debugInfo['abs_last5'] = $ruleDebug['abs_last5'] ?? 0; - $debugInfo['late_last5'] = $ruleDebug['late_last5'] ?? 0; + $debugInfo['by_student'] = $ruleDebug['by_student'] ?? 0; + $debugInfo['abs_last5'] = $ruleDebug['abs_last5'] ?? 0; + $debugInfo['late_last5'] = $ruleDebug['late_last5'] ?? 0; return [ - 'students' => $violations, + 'students' => $violations, 'school_year' => $schoolYear, - 'semester' => $semester, - 'debug' => $debugInfo, + 'semester' => $semester, + 'debug' => $debugInfo, ]; } -} +} \ No newline at end of file diff --git a/app/Services/AttendanceTracking/AttendanceTrackingService.php b/app/Services/AttendanceTracking/AttendanceTrackingService.php index bb976248..2414a8fc 100644 --- a/app/Services/AttendanceTracking/AttendanceTrackingService.php +++ b/app/Services/AttendanceTracking/AttendanceTrackingService.php @@ -2,13 +2,11 @@ namespace App\Services\AttendanceTracking; -use App\Http\Controllers\Api\AttendanceTracking\AttendanceTrackingController; use App\Models\Configuration; -use Carbon\Carbon; use Illuminate\Support\Facades\Validator; /** - * Facade used by {@see AttendanceTrackingController}. + * Facade used by {@see \App\Http\Controllers\Api\AttendanceTracking\AttendanceTrackingController}. */ class AttendanceTrackingService { @@ -17,7 +15,8 @@ class AttendanceTrackingService protected AttendanceCaseQueryService $caseQueryService, protected AttendanceNotificationWorkflowService $workflowService, protected AttendanceCommunicationSupportService $communicationSupportService, - ) {} + ) { + } protected function defaultSchoolYear(): string { @@ -153,7 +152,7 @@ class AttendanceTrackingService $ymd = null; if (! empty($data['date'])) { try { - $ymd = Carbon::parse((string) $data['date'])->format('Y-m-d'); + $ymd = \Carbon\Carbon::parse((string) $data['date'])->format('Y-m-d'); } catch (\Throwable) { $ymd = null; } diff --git a/app/Services/AttendanceTracking/AttendanceViolationStudentResolverService.php b/app/Services/AttendanceTracking/AttendanceViolationStudentResolverService.php index 28e7531a..e54bc98c 100644 --- a/app/Services/AttendanceTracking/AttendanceViolationStudentResolverService.php +++ b/app/Services/AttendanceTracking/AttendanceViolationStudentResolverService.php @@ -13,13 +13,14 @@ class AttendanceViolationStudentResolverService protected Student $studentModel, protected StudentClass $studentClassModel, protected AttendanceData $attendanceDataModel - ) {} + ) { + } public function resolveForSchoolYear(string $schoolYear, ?string $semester = null): array { $debug = [ 'class_students' => 0, - 'student_ids' => 0, + 'student_ids' => 0, ]; $classStudentsQuery = $this->studentClassModel->query(); @@ -40,9 +41,9 @@ class AttendanceViolationStudentResolverService ->orderByDesc('id') ->first(); - if ($latestTerm && ! empty($latestTerm->school_year)) { + if ($latestTerm && !empty($latestTerm->school_year)) { $schoolYear = (string) $latestTerm->school_year; - if (! empty($latestTerm->semester)) { + if (!empty($latestTerm->semester)) { $semester = (string) $latestTerm->semester; } @@ -72,14 +73,14 @@ class AttendanceViolationStudentResolverService if (empty($studentIds) && empty($studentCodes)) { return [ - 'school_year' => $schoolYear, - 'semester' => $semester, - 'student_ids' => [], - 'student_codes' => [], - 'students' => [], - 'student_code_to_id' => [], - 'existing_ids' => [], - 'debug' => $debug, + 'school_year' => $schoolYear, + 'semester' => $semester, + 'student_ids' => [], + 'student_codes' => [], + 'students' => [], + 'student_code_to_id'=> [], + 'existing_ids' => [], + 'debug' => $debug, ]; } @@ -90,14 +91,14 @@ class AttendanceViolationStudentResolverService [$studentCodeToId, $existingIds] = $this->buildLookupMaps($students); return [ - 'school_year' => $schoolYear, - 'semester' => $semester, - 'student_ids' => $studentIds, - 'student_codes' => $studentCodes, - 'students' => $students, + 'school_year' => $schoolYear, + 'semester' => $semester, + 'student_ids' => $studentIds, + 'student_codes' => $studentCodes, + 'students' => $students, 'student_code_to_id' => $studentCodeToId, - 'existing_ids' => $existingIds, - 'debug' => $debug, + 'existing_ids' => $existingIds, + 'debug' => $debug, ]; } @@ -113,12 +114,12 @@ class AttendanceViolationStudentResolverService return null; } - if (! isset($existingIds[$sid])) { + if (!isset($existingIds[$sid])) { $students[] = [ - 'id' => $sid, + 'id' => $sid, 'school_id' => (string) $sid, 'firstname' => (string) $sid, - 'lastname' => '', + 'lastname' => '', 'parent_id' => 0, ]; $existingIds[$sid] = true; @@ -138,12 +139,12 @@ class AttendanceViolationStudentResolverService $sid = abs(crc32($code)) ?: random_int(3000000, 3999999); $studentCodeToId[$code] = $sid; - if (! isset($existingIds[$sid])) { + if (!isset($existingIds[$sid])) { $students[] = [ - 'id' => $sid, + 'id' => $sid, 'school_id' => $code, 'firstname' => $code, - 'lastname' => '', + 'lastname' => '', 'parent_id' => 0, ]; $existingIds[$sid] = true; @@ -184,7 +185,7 @@ class AttendanceViolationStudentResolverService $attendanceStudents = DB::table($this->attendanceDataModel->getTable()) ->selectRaw('DISTINCT student_id') - ->when(! empty($schoolYear), fn ($q) => $q->where('school_year', $schoolYear)) + ->when(!empty($schoolYear), fn ($q) => $q->where('school_year', $schoolYear)) ->orderBy('student_id') ->get(); @@ -206,7 +207,7 @@ class AttendanceViolationStudentResolverService protected function filterInactiveIdentifiers(array $studentIds, array $studentCodes): array { - if (! empty($studentIds)) { + if (!empty($studentIds)) { $inactiveRows = $this->studentModel->query() ->select('id') ->whereIn('id', $studentIds) @@ -221,11 +222,11 @@ class AttendanceViolationStudentResolverService $studentIds = array_values(array_filter( $studentIds, - static fn ($id) => ! isset($inactiveIdSet[$id]) + static fn ($id) => !isset($inactiveIdSet[$id]) )); } - if (! empty($studentCodes)) { + if (!empty($studentCodes)) { $inactiveCodeRows = $this->studentModel->query() ->select('school_id') ->whereIn('school_id', $studentCodes) @@ -240,7 +241,7 @@ class AttendanceViolationStudentResolverService $studentCodes = array_values(array_filter( $studentCodes, - static fn ($code) => $code !== '' && ! isset($inactiveCodeSet[$code]) + static fn ($code) => $code !== '' && !isset($inactiveCodeSet[$code]) )); } @@ -251,7 +252,7 @@ class AttendanceViolationStudentResolverService { $students = []; - if (! empty($studentIds)) { + if (!empty($studentIds)) { $students = $this->studentModel->query() ->whereIn('id', $studentIds) ->where('is_active', 1) @@ -259,7 +260,7 @@ class AttendanceViolationStudentResolverService ->toArray(); } - if (! empty($studentCodes)) { + if (!empty($studentCodes)) { $byCode = $this->studentModel->query() ->whereIn('school_id', $studentCodes) ->where('is_active', 1) @@ -270,7 +271,7 @@ class AttendanceViolationStudentResolverService foreach ($byCode as $s) { $sid = (int) ($s['id'] ?? 0); - if ($sid > 0 && ! isset($seen[$sid])) { + if ($sid > 0 && !isset($seen[$sid])) { $students[] = $s; $seen[$sid] = true; } @@ -292,14 +293,14 @@ class AttendanceViolationStudentResolverService } foreach ($studentCodes as $code) { - if (! isset($knownCodes[$code])) { + if (!isset($knownCodes[$code])) { $virtualId = abs(crc32($code)) ?: random_int(1000000, 1999999); $students[] = [ - 'id' => $virtualId, - 'school_id' => $code, - 'firstname' => $code, - 'lastname' => '', - 'parent_id' => 0, + 'id' => $virtualId, + 'school_id' => $code, + 'firstname' => $code, + 'lastname' => '', + 'parent_id' => 0, 'class_name' => '', ]; } @@ -316,13 +317,13 @@ class AttendanceViolationStudentResolverService if (is_numeric($sidOrCode)) { $sid = (int) $sidOrCode; - if ($sid > 0 && ! isset($existingIds[$sid])) { + if ($sid > 0 && !isset($existingIds[$sid])) { $students[] = [ - 'id' => $sid, - 'school_id' => (string) $sid, - 'firstname' => (string) $sid, - 'lastname' => '', - 'parent_id' => 0, + 'id' => $sid, + 'school_id' => (string) $sid, + 'firstname' => (string) $sid, + 'lastname' => '', + 'parent_id' => 0, 'class_name' => '', ]; $existingIds[$sid] = true; @@ -330,14 +331,14 @@ class AttendanceViolationStudentResolverService } else { $code = (string) $sidOrCode; - if ($code !== '' && ! isset($studentCodeToId[$code])) { + if ($code !== '' && !isset($studentCodeToId[$code])) { $virtualId = abs(crc32($code)) ?: random_int(2000000, 2999999); $students[] = [ - 'id' => $virtualId, - 'school_id' => $code, - 'firstname' => $code, - 'lastname' => '', - 'parent_id' => 0, + 'id' => $virtualId, + 'school_id' => $code, + 'firstname' => $code, + 'lastname' => '', + 'parent_id' => 0, 'class_name' => '', ]; diff --git a/app/Services/AttendanceTracking/DefaultAttendanceMailerService.php b/app/Services/AttendanceTracking/DefaultAttendanceMailerService.php index e39aa0a4..0cb9946a 100644 --- a/app/Services/AttendanceTracking/DefaultAttendanceMailerService.php +++ b/app/Services/AttendanceTracking/DefaultAttendanceMailerService.php @@ -10,7 +10,8 @@ class DefaultAttendanceMailerService implements AttendanceMailerService public function __construct( protected EmailService $emailService, protected AttendanceEmailComposerService $emailComposerService - ) {} + ) { + } public function send(string $to, string $subject, string $html): bool { @@ -25,24 +26,24 @@ class DefaultAttendanceMailerService implements AttendanceMailerService } $student = (array) ($payload['student'] ?? []); - $studentName = trim((string) ($student['firstname'] ?? '').' '.(string) ($student['lastname'] ?? '')); + $studentName = trim((string) ($student['firstname'] ?? '') . ' ' . (string) ($student['lastname'] ?? '')); $subjectType = (string) ($payload['subject'] ?? 'Attendance'); $date = substr((string) ($payload['date'] ?? now()->toDateString()), 0, 10); $subject = "Attendance Notice: {$subjectType}"; - $body = '

Dear '.e((string) data_get($payload, 'parent.name', 'Parent/Guardian')).',

' - .'

This is an attendance notification for your student.

' - .'
    ' - .'
  • Student: '.e($studentName !== '' ? $studentName : 'Student').'
  • ' - .'
  • Date: '.e($date).'
  • ' - .'
  • Attendance: '.e($subjectType).'
  • ' - .'
' - .'

If you have any questions, please contact the school office.

'; + $body = '

Dear ' . e((string) data_get($payload, 'parent.name', 'Parent/Guardian')) . ',

' + . '

This is an attendance notification for your student.

' + . '
    ' + . '
  • Student: ' . e($studentName !== '' ? $studentName : 'Student') . '
  • ' + . '
  • Date: ' . e($date) . '
  • ' + . '
  • Attendance: ' . e($subjectType) . '
  • ' + . '
' + . '

If you have any questions, please contact the school office.

'; $html = $this->emailComposerService->renderWithEmailLayout($subject, $body); - if (! $this->send($to, $subject, $html)) { - Log::error('Attendance notification email failed for '.$to); + if (!$this->send($to, $subject, $html)) { + Log::error('Attendance notification email failed for ' . $to); throw new \RuntimeException('Failed to send attendance notification email.'); } } diff --git a/app/Services/AttendanceTracking/ViolationRuleEngineService.php b/app/Services/AttendanceTracking/ViolationRuleEngineService.php index 6524a29d..53cad6df 100644 --- a/app/Services/AttendanceTracking/ViolationRuleEngineService.php +++ b/app/Services/AttendanceTracking/ViolationRuleEngineService.php @@ -13,13 +13,13 @@ use Throwable; class ViolationRuleEngineService { protected array $debugCompute = []; - protected ?array $attendanceReportedColumns = null; public function __construct( protected AttendanceTracking $attendanceTrackingModel, protected AttendanceParentLookupService $parentLookupService - ) {} + ) { + } public function getDebugCompute(): array { @@ -54,10 +54,10 @@ class ViolationRuleEngineService continue; } - $ymd = substr((string) ($r['date'] ?? ''), 0, 10); + $ymd = substr((string) ($r['date'] ?? ''), 0, 10); $stat = strtolower(trim((string) ($r['status'] ?? ''))); - if (! in_array($stat, ['absent', 'late'], true)) { + if (!in_array($stat, ['absent', 'late'], true)) { continue; } @@ -69,10 +69,10 @@ class ViolationRuleEngineService [$syStart, $syEnd] = $this->deriveSchoolYearBounds($schoolYear); $weekRowsSource = null; - if (! empty($weekRowsFallback)) { + if (!empty($weekRowsFallback)) { foreach ($weekRowsFallback as $r) { $st = strtolower(trim((string) ($r['status'] ?? ''))); - if ($st !== '' && ! in_array($st, ['absent', 'late'], true)) { + if ($st !== '' && !in_array($st, ['absent', 'late'], true)) { $weekRowsSource = $weekRowsFallback; break; } @@ -86,9 +86,9 @@ class ViolationRuleEngineService return []; } - $weekIndex = array_flip($activeWeekKeys); - $currentWeekIdx = count($activeWeekKeys) - 1; - $windowStartIdx = max(0, $currentWeekIdx - 4); + $weekIndex = array_flip($activeWeekKeys); + $currentWeekIdx = count($activeWeekKeys) - 1; + $windowStartIdx = max(0, $currentWeekIdx - 4); $requiredWeeksCount = $currentWeekIdx - $windowStartIdx + 1; $keepLastWeeks = function (array $dates) use ($weekIndex, $windowStartIdx, $currentWeekIdx): array { @@ -105,18 +105,17 @@ class ViolationRuleEngineService } $out = array_values(array_unique($out)); rsort($out); - return $out; }; $classByStudent = []; try { $studentIdsForClass = array_values(array_unique(array_map( - static fn ($s) => (int) ($s['id'] ?? 0), + static fn($s) => (int) ($s['id'] ?? 0), $students ))); - if (! empty($studentIdsForClass)) { + if (!empty($studentIdsForClass)) { $rows = DB::table('student_class as sc') ->select([ 'sc.student_id', @@ -138,18 +137,18 @@ class ViolationRuleEngineService continue; } - if (! isset($classByStudent[$sid])) { + if (!isset($classByStudent[$sid])) { $classByStudent[$sid] = [ - 'class_section_id' => $row->class_section_id ?? null, + 'class_section_id' => $row->class_section_id ?? null, 'class_section_name' => (string) ($row->class_section_name ?? ''), - 'class_id' => $row->class_id ?? null, - 'class_name' => (string) ($row->class_name ?? ''), + 'class_id' => $row->class_id ?? null, + 'class_name' => (string) ($row->class_name ?? ''), ]; } } } } catch (Throwable $e) { - Log::debug('computeViolations(): class prefetch failed: '.$e->getMessage()); + Log::debug('computeViolations(): class prefetch failed: ' . $e->getMessage()); } $out = []; @@ -157,26 +156,26 @@ class ViolationRuleEngineService $lateCountLast5 = 0; foreach ($students as $stu) { - $sid = (int) ($stu['id'] ?? 0); - $name = trim(($stu['firstname'] ?? '').' '.($stu['lastname'] ?? '')); + $sid = (int) ($stu['id'] ?? 0); + $name = trim(($stu['firstname'] ?? '') . ' ' . ($stu['lastname'] ?? '')); - $absDatesAll = array_values(array_unique($byStudent[$sid]['absent'] ?? [])); + $absDatesAll = array_values(array_unique($byStudent[$sid]['absent'] ?? [])); $lateDatesAll = array_values(array_unique($byStudent[$sid]['late'] ?? [])); - $absDates = $keepLastWeeks($absDatesAll); + $absDates = $keepLastWeeks($absDatesAll); $lateDates = $keepLastWeeks($lateDatesAll); if (empty($absDates) && empty($lateDates)) { continue; } - $absWeekIdx = $this->datesToWeekIndices($absDates, $weekIndex); + $absWeekIdx = $this->datesToWeekIndices($absDates, $weekIndex); $lateWeekIdx = $this->datesToWeekIndices($lateDates, $weekIndex); - $absCountLast5 += count($absDates); + $absCountLast5 += count($absDates); $lateCountLast5 += count($lateDates); $absenceViolation = null; - if (! empty($absDates)) { + if (!empty($absDates)) { $lastAbsDate = $absDates[0]; $A2row = $this->hasNConsecutiveItems($absDates, 2, 7); @@ -189,47 +188,47 @@ class ViolationRuleEngineService if ($A4row || $A_in5w4) { $absenceViolation = [ - 'type' => 'absence', - 'code' => 'ABS_4', - 'severity' => 4, - 'action' => 'expel_notify', - 'color' => '#ff4d4f', - 'title' => '4 Absences (in a row or within last 5 active weeks)', + 'type' => 'absence', + 'code' => 'ABS_4', + 'severity' => 4, + 'action' => 'expel_notify', + 'color' => '#ff4d4f', + 'title' => '4 Absences (in a row or within last 5 active weeks)', 'description' => 'Notify team to inform parent about expulsion and send email.', - 'last_date' => $lastAbsDate, + 'last_date' => $lastAbsDate, ]; } elseif ($A3row || $A_in4w3) { $absenceViolation = [ - 'type' => 'absence', - 'code' => 'ABS_3', - 'severity' => 3, - 'action' => 'team_notify', - 'color' => '#fa8c16', - 'title' => '3 Absences (in a row or within last 4 active weeks)', + 'type' => 'absence', + 'code' => 'ABS_3', + 'severity' => 3, + 'action' => 'team_notify', + 'color' => '#fa8c16', + 'title' => '3 Absences (in a row or within last 4 active weeks)', 'description' => 'Team calls parent and sends email.', - 'last_date' => $lastAbsDate, + 'last_date' => $lastAbsDate, ]; } elseif ($A2row || $A_in3w2) { $absenceViolation = [ - 'type' => 'absence', - 'code' => 'ABS_2', - 'severity' => 2, - 'action' => 'team_notify', - 'color' => '#fadb14', - 'title' => '2 Absences (in a row or within last 3 active weeks)', + 'type' => 'absence', + 'code' => 'ABS_2', + 'severity' => 2, + 'action' => 'team_notify', + 'color' => '#fadb14', + 'title' => '2 Absences (in a row or within last 3 active weeks)', 'description' => 'Team calls parent and sends email.', - 'last_date' => $lastAbsDate, + 'last_date' => $lastAbsDate, ]; } } $lateViolation = null; - if (! empty($lateDates)) { + if (!empty($lateDates)) { $lastLateDate = $lateDates[0]; - $L2row = $this->hasNConsecutiveItems($lateDates, 2, 7); - $L3row = $this->hasNConsecutiveItems($lateDates, 3, 7); - $L4row = $this->hasNConsecutiveItems($lateDates, 4, 7); + $L2row = $this->hasNConsecutiveItems($lateDates, 2, 7); + $L3row = $this->hasNConsecutiveItems($lateDates, 3, 7); + $L4row = $this->hasNConsecutiveItems($lateDates, 4, 7); $L_in3w2 = $this->hasNInWActiveWeeks($lateWeekIdx, 2, 3); $L_in4w3 = $this->hasNInWActiveWeeks($lateWeekIdx, 3, 4); @@ -239,7 +238,7 @@ class ViolationRuleEngineService $lateWeeksSet = array_flip($lateWeekIdx); $lateCoversAllWindowWeeks = true; for ($i = $windowStartIdx; $i <= $currentWeekIdx; $i++) { - if (! isset($lateWeeksSet[$i])) { + if (!isset($lateWeeksSet[$i])) { $lateCoversAllWindowWeeks = false; break; } @@ -248,41 +247,41 @@ class ViolationRuleEngineService if ($L4row || $lateCoversAllWindowWeeks) { $lateViolation = [ - 'type' => 'late', - 'code' => 'LATE_4', - 'severity' => 4, - 'action' => 'team_notify', - 'color' => '#ff4d4f', - 'title' => '4 Lates in a row OR in each of the last 4 active weeks', + 'type' => 'late', + 'code' => 'LATE_4', + 'severity' => 4, + 'action' => 'team_notify', + 'color' => '#ff4d4f', + 'title' => '4 Lates in a row OR in each of the last 4 active weeks', 'description' => 'Team calls parent and sends email.', - 'last_date' => $lastLateDate, + 'last_date' => $lastLateDate, ]; } else { $twoLatesOneAbsWithin4 = $this->twoLatesOneAbsInWWeeks($lateWeekIdx, $absWeekIdx, 4); if ($L3row || $L_in4w3 || $twoLatesOneAbsWithin4) { $lateViolation = [ - 'type' => $twoLatesOneAbsWithin4 ? 'mix' : 'late', - 'code' => $twoLatesOneAbsWithin4 ? 'MIX_L2A1' : 'LATE_3', - 'severity' => 3, - 'action' => 'team_notify', - 'color' => '#002766', - 'title' => $twoLatesOneAbsWithin4 + 'type' => $twoLatesOneAbsWithin4 ? 'mix' : 'late', + 'code' => $twoLatesOneAbsWithin4 ? 'MIX_L2A1' : 'LATE_3', + 'severity' => 3, + 'action' => 'team_notify', + 'color' => '#002766', + 'title' => $twoLatesOneAbsWithin4 ? '2 Lates + 1 Absence (within last 4 active weeks)' : '3 Lates (in a row or within last 4 active weeks)', 'description' => 'Team calls parent and sends email.', - 'last_date' => $lastLateDate, + 'last_date' => $lastLateDate, ]; } elseif ($L2row || $L_in3w2) { $lateViolation = [ - 'type' => 'late', - 'code' => 'LATE_2', - 'severity' => 1, - 'action' => 'auto_email', - 'color' => '#bae7ff', - 'title' => '2 Lates (in a row or within last 3 active weeks)', + 'type' => 'late', + 'code' => 'LATE_2', + 'severity' => 1, + 'action' => 'auto_email', + 'color' => '#bae7ff', + 'title' => '2 Lates (in a row or within last 3 active weeks)', 'description' => 'Send automated email to parent.', - 'last_date' => $lastLateDate, + 'last_date' => $lastLateDate, ]; } } @@ -290,20 +289,20 @@ class ViolationRuleEngineService $chosen = $this->chooseHigherSeverity($absenceViolation, $lateViolation); - if (! $chosen && count($absDates) === 1) { + if (!$chosen && count($absDates) === 1) { $chosen = [ - 'type' => 'absence', - 'code' => 'ABS_1', - 'severity' => 1, - 'action' => 'auto_email', - 'color' => '#e6f7ff', - 'title' => '1 Unreported Absence', + 'type' => 'absence', + 'code' => 'ABS_1', + 'severity' => 1, + 'action' => 'auto_email', + 'color' => '#e6f7ff', + 'title' => '1 Unreported Absence', 'description' => 'Send automated email to parent.', - 'last_date' => $absDates[0], + 'last_date' => $absDates[0], ]; } - if (! $chosen) { + if (!$chosen) { continue; } @@ -316,51 +315,51 @@ class ViolationRuleEngineService ->where('school_year', $schoolYear) ->where('date', '>=', $start) ->where('date', '<', $end) - ->where('reason', 'like', '%'.$chosen['code'].'%'); + ->where('reason', 'like', '%' . $chosen['code'] . '%'); if ($semester !== null) { $trackingQ->where('semester', $semester); } - $tracking = $trackingQ->orderByDesc('date')->first(); - $tracking = $tracking?->toArray() ?? []; + $tracking = $trackingQ->orderByDesc('date')->first(); + $tracking = $tracking?->toArray() ?? []; $isNotified = (bool) ($tracking['is_notified'] ?? 0); - $notifCnt = (int) ($tracking['notif_counter'] ?? 0); + $notifCnt = (int) ($tracking['notif_counter'] ?? 0); $cls = $classByStudent[$sid] ?? []; $classLabel = (string) ($cls['class_section_name'] ?? ''); - if ($classLabel === '' && ! empty($cls['class_name'])) { + if ($classLabel === '' && !empty($cls['class_name'])) { $classLabel = (string) $cls['class_name']; } $out[] = [ - 'id' => $sid, - 'name' => $name, - 'class_name' => $classLabel, + 'id' => $sid, + 'name' => $name, + 'class_name' => $classLabel, 'class_section_name' => (string) ($cls['class_section_name'] ?? ''), - 'className' => (string) ($cls['class_name'] ?? ''), - 'class_id' => $cls['class_id'] ?? null, + 'className' => (string) ($cls['class_name'] ?? ''), + 'class_id' => $cls['class_id'] ?? null, 'class_section_id' => $cls['class_section_id'] ?? null, - 'parent_email' => $parent['email'] ?? '', - 'parent_name' => $parent['parent_name'] ?? '', - 'parent_phone' => $parent['phone'] ?? null, - 'violation' => $chosen['title'], - 'violation_code' => $chosen['code'], - 'type' => $chosen['type'], - 'severity' => $chosen['severity'], - 'action' => $chosen['action'], - 'color' => $chosen['color'], - 'last_absence' => $absDates[0] ?? null, - 'last_late' => $lateDates[0] ?? null, - 'last_date' => $chosen['last_date'], - 'is_notified' => $isNotified, - 'notif_counter' => $notifCnt, - 'absences' => $absDates, - 'lates' => $lateDates, + 'parent_email' => $parent['email'] ?? '', + 'parent_name' => $parent['parent_name'] ?? '', + 'parent_phone' => $parent['phone'] ?? null, + 'violation' => $chosen['title'], + 'violation_code' => $chosen['code'], + 'type' => $chosen['type'], + 'severity' => $chosen['severity'], + 'action' => $chosen['action'], + 'color' => $chosen['color'], + 'last_absence' => $absDates[0] ?? null, + 'last_late' => $lateDates[0] ?? null, + 'last_date' => $chosen['last_date'], + 'is_notified' => $isNotified, + 'notif_counter' => $notifCnt, + 'absences' => $absDates, + 'lates' => $lateDates, ]; } - $this->debugCompute['abs_last5'] = $absCountLast5; + $this->debugCompute['abs_last5'] = $absCountLast5; $this->debugCompute['late_last5'] = $lateCountLast5; return $out; @@ -371,10 +370,8 @@ class ViolationRuleEngineService if ($a && $b) { if ($a['severity'] === $b['severity']) { $prio = ['expel_notify' => 3, 'team_notify' => 2, 'auto_email' => 1]; - return ($prio[$a['action']] ?? 0) >= ($prio[$b['action']] ?? 0) ? $a : $b; } - return $a['severity'] > $b['severity'] ? $a : $b; } @@ -389,7 +386,7 @@ class ViolationRuleEngineService $this->attendanceReportedColumns = [ 'is_reported' => Schema::hasColumn($table, 'is_reported'), - 'reported' => Schema::hasColumn($table, 'reported'), + 'reported' => Schema::hasColumn($table, 'reported'), ]; return $this->attendanceReportedColumns; @@ -399,15 +396,15 @@ class ViolationRuleEngineService { $cols = $this->attendanceReportedColumns($table); - if (! empty($cols['is_reported'])) { + if (!empty($cols['is_reported'])) { $qb->whereRaw("(LOWER(TRIM(COALESCE(is_reported, ''))) NOT IN ('yes','1','true','y','on'))"); } - if (! empty($cols['reported'])) { + if (!empty($cols['reported'])) { $qb->whereRaw("(LOWER(TRIM(COALESCE(reported, ''))) NOT IN ('yes','1','true','y','on'))"); } $qb->whereRaw("(LOWER(COALESCE(reason, '')) NOT LIKE '%parent-reported%')") - ->whereRaw("(LOWER(COALESCE(reason, '')) NOT LIKE '%parent reported%')"); + ->whereRaw("(LOWER(COALESCE(reason, '')) NOT LIKE '%parent reported%')"); if (Schema::hasTable('parent_attendance_reports')) { $qb->whereRaw("NOT EXISTS ( @@ -421,16 +418,14 @@ class ViolationRuleEngineService public function deriveSchoolYearBounds(string $schoolYear): array { - if (! preg_match('/^(\d{4})-(\d{4})$/', $schoolYear, $m)) { - $y = (int) date('Y'); + if (!preg_match('/^(\d{4})-(\d{4})$/', $schoolYear, $m)) { + $y = (int) date('Y'); $startY = (int) (date('n') >= 8 ? $y : $y - 1); - return [sprintf('%d-08-01', $startY), sprintf('%d-07-31', $startY + 1)]; } $startY = (int) $m[1]; - $endY = (int) $m[2]; - + $endY = (int) $m[2]; return [sprintf('%d-08-01', $startY), sprintf('%d-07-31', $endY)]; } @@ -451,10 +446,9 @@ class ViolationRuleEngineService public function dayBounds(string $ymd): array { $d = substr($ymd, 0, 10); - return [ - $d.' 00:00:00', - date('Y-m-d', strtotime($d.' +1 day')).' 00:00:00', + $d . ' 00:00:00', + date('Y-m-d', strtotime($d . ' +1 day')) . ' 00:00:00', ]; } @@ -467,10 +461,10 @@ class ViolationRuleEngineService ): array { $weeks = []; - if (! empty($weekRowsFallback)) { + if (!empty($weekRowsFallback)) { foreach ($weekRowsFallback as $r) { $d = $r['date'] ?? null; - if (! $d) { + if (!$d) { continue; } $ts = strtotime($d); @@ -496,10 +490,10 @@ class ViolationRuleEngineService $q->whereNull('reason') ->orWhere(function ($q2) { $q2->where('reason', 'not like', '%break%') - ->where('reason', 'not like', '%cancel%'); + ->where('reason', 'not like', '%cancel%'); }); }) - ->when($semester !== null && $semester !== '', fn ($q) => $q->where('semester', $semester)) + ->when($semester !== null && $semester !== '', fn($q) => $q->where('semester', $semester)) ->groupBy(DB::raw('DATE(date)')) ->orderBy('ymd') ->get(); @@ -527,7 +521,6 @@ class ViolationRuleEngineService } $idx = array_keys($set); sort($idx); - return $idx; } @@ -549,16 +542,14 @@ class ViolationRuleEngineService if (Str::startsWith($code, 'ABS_4')) { return 5; } - return 5; } public function isoWeekStartYmd(string $weekKey): string { if (preg_match('/^(\d{4})-W(\d{2})$/', $weekKey, $m)) { - return date('Y-m-d', strtotime($m[1].'-W'.$m[2].'-1')); + return date('Y-m-d', strtotime($m[1] . '-W' . $m[2] . '-1')); } - return date('Y-m-d'); } @@ -580,15 +571,14 @@ class ViolationRuleEngineService if (empty($activeWeekKeys)) { $days = ($windowWeeks * 7) - 1; - - return date('Y-m-d', strtotime($endBound.' -'.$days.' days')); + return date('Y-m-d', strtotime($endBound . ' -' . $days . ' days')); } $activeWeekKeys = array_values($activeWeekKeys); $weekIndex = array_flip($activeWeekKeys); $endWeekKey = date('o-\WW', strtotime($endBound)); - if (! isset($weekIndex[$endWeekKey])) { + if (!isset($weekIndex[$endWeekKey])) { $endWeekKey = null; for ($i = count($activeWeekKeys) - 1; $i >= 0; $i--) { $wkStart = $this->isoWeekStartYmd($activeWeekKeys[$i]); @@ -672,12 +662,11 @@ class ViolationRuleEngineService while ($iL2 < count($L)) { $windowStart = $L[$iL1]; - $windowEnd = $windowStart + ($w - 1); + $windowEnd = $windowStart + ($w - 1); if ($L[$iL2] > $windowEnd) { $iL1++; $iL2 = $iL1 + 1; - continue; } @@ -698,4 +687,4 @@ class ViolationRuleEngineService return false; } -} +} \ No newline at end of file diff --git a/app/Services/Auth/ApiLoginSecurityService.php b/app/Services/Auth/ApiLoginSecurityService.php index dd9abebd..1c3b18b5 100644 --- a/app/Services/Auth/ApiLoginSecurityService.php +++ b/app/Services/Auth/ApiLoginSecurityService.php @@ -2,9 +2,9 @@ namespace App\Services\Auth; -use App\Models\Configuration; use App\Models\IpAttempt; use App\Models\LoginActivity; +use App\Models\Configuration; use App\Models\User; use Carbon\CarbonImmutable; use Illuminate\Http\Request; @@ -22,7 +22,7 @@ class ApiLoginSecurityService public function isIpBlocked(string $ip): bool { $row = IpAttempt::query()->where('ip_address', $ip)->first(); - if (! $row || ! $row->blocked_until) { + if (!$row || !$row->blocked_until) { return false; } diff --git a/app/Services/Auth/AuthSessionService.php b/app/Services/Auth/AuthSessionService.php index bc7f044f..04cda57d 100644 --- a/app/Services/Auth/AuthSessionService.php +++ b/app/Services/Auth/AuthSessionService.php @@ -3,11 +3,12 @@ namespace App\Services\Auth; use App\Models\Configuration; +use App\Services\ApplicationUrlService; use App\Models\LoginActivity; use App\Models\Preferences; use App\Models\Role; use App\Models\User; -use App\Services\ApplicationUrlService; +use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Log; @@ -20,7 +21,8 @@ class AuthSessionService public function __construct( private ApplicationUrlService $urls, - ) {} + ) { + } /** * Sanitizes login redirects to same-host relative paths only. diff --git a/app/Services/Auth/PermissionCheckService.php b/app/Services/Auth/PermissionCheckService.php index e570c625..767e52ac 100644 --- a/app/Services/Auth/PermissionCheckService.php +++ b/app/Services/Auth/PermissionCheckService.php @@ -12,7 +12,7 @@ class PermissionCheckService ->where('name', $permissionName) ->value('id'); - if (! $permissionId) { + if (!$permissionId) { return false; } diff --git a/app/Services/Auth/RegistrationCaptchaService.php b/app/Services/Auth/RegistrationCaptchaService.php index f33cbf2d..90cfc3fd 100644 --- a/app/Services/Auth/RegistrationCaptchaService.php +++ b/app/Services/Auth/RegistrationCaptchaService.php @@ -9,8 +9,7 @@ class RegistrationCaptchaService $ip = (string) (request()?->ip() ?? 'unknown'); $ua = (string) (request()?->header('User-Agent') ?? ''); $hash = substr(sha1($ua), 0, 12); - - return 'captcha:'.$ip.':'.$hash; + return 'captcha:' . $ip . ':' . $hash; } public function generate(?int $length = null): string diff --git a/app/Services/Auth/RegistrationFormatterService.php b/app/Services/Auth/RegistrationFormatterService.php index 1d6e1b48..3d00572e 100644 --- a/app/Services/Auth/RegistrationFormatterService.php +++ b/app/Services/Auth/RegistrationFormatterService.php @@ -6,7 +6,9 @@ use App\Services\PhoneFormatterService; class RegistrationFormatterService { - public function __construct(private PhoneFormatterService $phoneFormatter) {} + public function __construct(private PhoneFormatterService $phoneFormatter) + { + } public function format(array $payload): array { @@ -28,7 +30,7 @@ class RegistrationFormatterService 'accept_school_policy' => (int) ($payload['accept_school_policy'] ?? 0), ]; - $noSecondInfo = ! empty($payload['no_second_parent_info']); + $noSecondInfo = !empty($payload['no_second_parent_info']); $secondFirstname = $this->formatName($g('second_firstname')); $secondLastname = $this->formatName($g('second_lastname')); @@ -36,7 +38,7 @@ class RegistrationFormatterService $secondEmail = $this->formatEmail($g('second_email')); $secondCellphone = $this->phoneFormatter->formatPhoneNumber($g('second_cellphone')); - $secondProvided = ! $noSecondInfo && ( + $secondProvided = !$noSecondInfo && ( $secondFirstname !== '' || $secondLastname !== '' || $secondGender !== '' || @@ -62,7 +64,7 @@ class RegistrationFormatterService return ''; } - return ucwords(strtolower($name), ' -'); + return ucwords(strtolower($name), " -"); } private function formatEmail(string $email): string diff --git a/app/Services/Auth/RegistrationService.php b/app/Services/Auth/RegistrationService.php index 5192562b..94eb036c 100644 --- a/app/Services/Auth/RegistrationService.php +++ b/app/Services/Auth/RegistrationService.php @@ -20,12 +20,13 @@ class RegistrationService private RegistrationFormatterService $formatter, private RegistrationCaptchaService $captchaService, private ApplicationUrlService $urls, - ) {} + ) { + } public function register(array $payload): array { $captcha = (string) ($payload['captcha'] ?? ''); - if (! $this->captchaService->verify($captcha)) { + if (!$this->captchaService->verify($captcha)) { return [ 'ok' => false, 'code' => 'captcha_invalid', @@ -33,15 +34,15 @@ class RegistrationService ]; } - $isParent = ! empty($payload['is_parent']); - $noSecondInfo = ! empty($payload['no_second_parent_info']); + $isParent = !empty($payload['is_parent']); + $noSecondInfo = !empty($payload['no_second_parent_info']); $formatted = $this->formatter->format($payload); $email = (string) ($formatted['email'] ?? ''); $existing = User::query()->where('email', $email)->first(); if ($existing) { - $pending = ! empty($existing->token) && (int) ($existing->is_verified ?? 0) === 0; + $pending = !empty($existing->token) && (int) ($existing->is_verified ?? 0) === 0; if ($pending) { return [ 'ok' => false, @@ -59,7 +60,7 @@ class RegistrationService $roleName = $isParent ? 'parent' : 'guest'; $role = Role::query()->where('name', $roleName)->first(); - if (! $role) { + if (!$role) { return [ 'ok' => false, 'code' => 'role_missing', @@ -112,7 +113,7 @@ class RegistrationService 'created_at' => now(), ]); - if ($isParent && ! $noSecondInfo && ! empty($formatted['second_firstname'])) { + if ($isParent && !$noSecondInfo && !empty($formatted['second_firstname'])) { ParentModel::query()->create([ 'secondparent_firstname' => $formatted['second_firstname'] ?? null, 'secondparent_lastname' => $formatted['second_lastname'] ?? null, @@ -127,12 +128,12 @@ class RegistrationService }); $activationLink = $this->urls->spaRegistrationConfirmUrl($token); - $recipientName = trim((string) ($formatted['firstname'] ?? '').' '.(string) ($formatted['lastname'] ?? '')); + $recipientName = trim((string) ($formatted['firstname'] ?? '') . ' ' . (string) ($formatted['lastname'] ?? '')); - $html = '

Hello '.e($recipientName !== '' ? $recipientName : 'there').',

' - .'

Please confirm your email by clicking the link below:

' - .'

Activate your account

' - .'

Thank you.

'; + $html = '

Hello ' . e($recipientName !== '' ? $recipientName : 'there') . ',

' + . '

Please confirm your email by clicking the link below:

' + . '

Activate your account

' + . '

Thank you.

'; $sent = $this->emailService->send($email, 'Email Confirmation', $html, 'general'); $this->captchaService->clear(); diff --git a/app/Services/Auth/UserRoleService.php b/app/Services/Auth/UserRoleService.php index 1cf1a8a7..b1f8abc8 100644 --- a/app/Services/Auth/UserRoleService.php +++ b/app/Services/Auth/UserRoleService.php @@ -35,16 +35,16 @@ class UserRoleService return true; } - if ($crudAction === 'create' && ! empty($row->can_create)) { + if ($crudAction === 'create' && !empty($row->can_create)) { return true; } - if ($crudAction === 'read' && ! empty($row->can_read)) { + if ($crudAction === 'read' && !empty($row->can_read)) { return true; } - if ($crudAction === 'update' && ! empty($row->can_update)) { + if ($crudAction === 'update' && !empty($row->can_update)) { return true; } - if ($crudAction === 'delete' && ! empty($row->can_delete)) { + if ($crudAction === 'delete' && !empty($row->can_delete)) { return true; } } diff --git a/app/Services/BadgeScan/BadgeScanService.php b/app/Services/BadgeScan/BadgeScanService.php index fe137fe0..644437e8 100644 --- a/app/Services/BadgeScan/BadgeScanService.php +++ b/app/Services/BadgeScan/BadgeScanService.php @@ -2,14 +2,13 @@ namespace App\Services\BadgeScan; -use App\Controllers\View\RFIDController; use App\Models\Configuration; use App\Models\User; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Schema; /** - * legacy {@see RFIDController} parity (badge scan + log listing). + * legacy {@see \App\Controllers\View\RFIDController} parity (badge scan + log listing). */ class BadgeScanService { diff --git a/app/Services/Badges/BadgeFormDataService.php b/app/Services/Badges/BadgeFormDataService.php index 0e982b60..cd76097c 100644 --- a/app/Services/Badges/BadgeFormDataService.php +++ b/app/Services/Badges/BadgeFormDataService.php @@ -8,7 +8,8 @@ class BadgeFormDataService protected BadgeUserLookupService $lookupService, protected BadgeStudentLookupService $studentLookupService, protected BadgeTextFormatter $formatter - ) {} + ) { + } public function build( ?string $schoolYear, @@ -23,7 +24,7 @@ class BadgeFormDataService $students = $this->studentLookupService->fetchStudentList($schoolYear, $selectedStudentIds); foreach ($users as &$u) { - if (! isset($u['user_id']) && isset($u['id'])) { + if (!isset($u['user_id']) && isset($u['id'])) { $u['user_id'] = (int) $u['id']; } @@ -45,7 +46,7 @@ class BadgeFormDataService $u['class_section_id'] = $u['class_section_id'] ?? null; $u['class_section_name'] = $u['class_section_name'] ?? null; $u['entity_type'] = 'user'; - $u['row_id'] = 'user:'.(int) ($u['user_id'] ?? 0); + $u['row_id'] = 'user:' . (int) ($u['user_id'] ?? 0); $u['role_group'] = $this->detectRoleGroup( strtolower((string) ($u['roles_raw'] ?? ($u['role_name_raw'] ?? ''))) ); @@ -56,7 +57,7 @@ class BadgeFormDataService $rolesDetect = strtolower((string) ($u['roles_raw'] ?? ($u['role_name_raw'] ?? ''))); $isTeacherish = str_contains($rolesDetect, 'teacher') || preg_match('/\bta\b/', $rolesDetect); - if ($isTeacherish && ! empty($u['user_id'])) { + if ($isTeacherish && !empty($u['user_id'])) { $assign = $this->lookupService->getLatestClassForUser((int) $u['user_id'], $schoolYear); if ($assign) { $u['class_section_id'] = $assign['class_section_id'] ?? null; @@ -68,7 +69,7 @@ class BadgeFormDataService foreach ($students as &$student) { $student['entity_type'] = 'student'; - $student['row_id'] = 'student:'.(int) ($student['student_id'] ?? 0); + $student['row_id'] = 'student:' . (int) ($student['student_id'] ?? 0); $student['role_group'] = 'student'; $student['class_section_id'] = null; @@ -83,17 +84,17 @@ class BadgeFormDataService $rows = array_merge($users, $students); usort($rows, static function (array $a, array $b): int { return strcasecmp( - trim((string) ($a['lastname'] ?? '')).' '.trim((string) ($a['firstname'] ?? '')), - trim((string) ($b['lastname'] ?? '')).' '.trim((string) ($b['firstname'] ?? '')) + trim((string) ($a['lastname'] ?? '')) . ' ' . trim((string) ($a['firstname'] ?? '')), + trim((string) ($b['lastname'] ?? '')) . ' ' . trim((string) ($b['firstname'] ?? '')) ); }); $rolesTabs = [ - 'all' => 'All', + 'all' => 'All', 'teacher' => 'Teachers', - 'ta' => 'Teacher Assistants', - 'admin' => 'Admins', - 'staff' => 'Staff', + 'ta' => 'Teacher Assistants', + 'admin' => 'Admins', + 'staff' => 'Staff', 'student' => 'Students', ]; @@ -102,13 +103,13 @@ class BadgeFormDataService : 'all'; return [ - 'users' => $rows, - 'schoolYears' => $this->lookupService->getAvailableSchoolYears(), - 'selectedYear' => $schoolYear, - 'selectedUserIds' => $selectedUserIds, + 'users' => $rows, + 'schoolYears' => $this->lookupService->getAvailableSchoolYears(), + 'selectedYear' => $schoolYear, + 'selectedUserIds' => $selectedUserIds, 'selectedStudentIds' => $selectedStudentIds, - 'rolesTabs' => $rolesTabs, - 'active_role' => $activeRole, + 'rolesTabs' => $rolesTabs, + 'active_role' => $activeRole, ]; } diff --git a/app/Services/Badges/BadgePdfService.php b/app/Services/Badges/BadgePdfService.php index 761020b3..20e7111f 100644 --- a/app/Services/Badges/BadgePdfService.php +++ b/app/Services/Badges/BadgePdfService.php @@ -33,13 +33,14 @@ class BadgePdfService protected BadgeUserLookupService $userLookupService, protected BadgePrintLogService $printLogService, protected BadgeTextFormatter $formatter, - ) {} + ) { + } /** * Letter landscape PDF, 8 badges per page — students and/or staff (FPDF + QR PNG). * * @param int[] $studentIds Primary keys on `students.id` - * @param int[] $userIds Staff/admin/teacher keys on `users.id` + * @param int[] $userIds Staff/admin/teacher keys on `users.id` */ public function generate( array $studentIds, @@ -85,7 +86,7 @@ class BadgePdfService try { $qrPayload = (string) ($staffRow['user_id_for_qr'] ?? $staffRow['user_id'] ?? ''); - if ($qrPayload === '' || ! preg_match('/^[A-Za-z0-9_-]+$/', $qrPayload)) { + if ($qrPayload === '' || !preg_match('/^[A-Za-z0-9_-]+$/', $qrPayload)) { $qrPayload = (string) (int) ($staffRow['user_id'] ?? 0); } @@ -143,7 +144,7 @@ class BadgePdfService ): string { $html = $this->buildHtmlDocument($rows, $schoolName, $motto, $footerBlock); - $options = new Options; + $options = new Options(); $options->set('isHtml5ParserEnabled', true); $options->set('isRemoteEnabled', true); $options->set('defaultFont', 'DejaVu Sans'); @@ -167,7 +168,7 @@ class BadgePdfService $this->tempQrFiles = []; try { - if (! class_exists('FPDF', false)) { + if (!class_exists('FPDF', false)) { require_once base_path('app/ThirdParty/fpdf/fpdf.php'); } @@ -199,7 +200,7 @@ class BadgePdfService for ($gridCol = 0; $gridCol < 4; $gridCol++) { $idx = ($gridRow * 4) + $gridCol; $row = $slots[$idx]; - if (! is_array($row)) { + if (!is_array($row)) { continue; } @@ -261,7 +262,7 @@ class BadgePdfService array $classesMap ): ?array { $info = $this->userLookupService->getUserInfoById($userId, $schoolYear); - if (! $info || ! is_array($info)) { + if (!$info || !is_array($info)) { return null; } @@ -275,7 +276,7 @@ class BadgePdfService $roleResolved = $this->formatter->formatRole($roleResolved); $info['role_resolved'] = $roleResolved; - if (! empty($classesMap[$resolvedId])) { + if (!empty($classesMap[$resolvedId])) { $info['class_section_name'] = (string) $classesMap[$resolvedId]; } @@ -290,11 +291,11 @@ class BadgePdfService if ($isTeacherish && $class !== '') { if (strtolower($class) === 'youth') { - $display = 'Youth '.$roleResolved; + $display = 'Youth ' . $roleResolved; } elseif ($class === 'KG') { - $display = 'KG '.$roleResolved; + $display = 'KG ' . $roleResolved; } else { - $display = 'Grade '.$class.' '.$roleResolved; + $display = 'Grade ' . $class . ' ' . $roleResolved; } } elseif ($roleResolved !== '') { $display = $roleResolved; @@ -308,7 +309,7 @@ class BadgePdfService $year = $this->formatter->norm((string) ($info['school_year'] ?? ($schoolYear ?? ''))); $schoolId = trim((string) ($info['school_id'] ?? '')); - if ($schoolId === '' || ! preg_match('/^[A-Za-z0-9_-]+$/', $schoolId)) { + if ($schoolId === '' || !preg_match('/^[A-Za-z0-9_-]+$/', $schoolId)) { return null; } @@ -363,10 +364,10 @@ class BadgePdfService } return 'Badges PDF' - .$badgesHtml - .''; + . $this->badgeCss() + . '' + . $badgesHtml + . ''; } protected function buildBadgeHtml( @@ -375,7 +376,8 @@ class BadgePdfService string $motto, string $footerBlock, ?string $logoDataUri - ): string { + ): string + { $kind = (string) ($row['_badge_kind'] ?? 'student'); $idLabel = $kind === 'staff' ? 'User ID' : 'School ID'; $title = $this->escapeHtml((string) ($row['fullname'] ?? '')); @@ -402,8 +404,8 @@ class BadgePdfService
G - '.$gradeLabel.' - '.$grade.' + ' . $gradeLabel . ' + ' . $grade . '
'; } @@ -413,15 +415,15 @@ class BadgePdfService Y Academic Year - '.$year.' + ' . $year . '
ID - '.$this->escapeHtml($idLabel).' - '.$schoolId.' + ' . $this->escapeHtml($idLabel) . ' + ' . $schoolId . '
'; @@ -430,18 +432,18 @@ class BadgePdfService
' - .($logoDataUri !== null - ? 'School Logo' - : 'AS'). + . ($logoDataUri !== null + ? 'School Logo' + : 'AS') . '
-
'.$this->escapeHtml($headerMainTitle).'
- '.($schoolLevel !== '' - ? '
'.$this->escapeHtml($schoolLevel).'
' - : '').' - '.($headerMotto !== '' - ? '
'.$this->escapeHtml($headerMotto).'
' - : '').' +
' . $this->escapeHtml($headerMainTitle) . '
+ ' . ($schoolLevel !== '' + ? '
' . $this->escapeHtml($schoolLevel) . '
' + : '') . ' + ' . ($headerMotto !== '' + ? '
' . $this->escapeHtml($headerMotto) . '
' + : '') . '
@@ -449,25 +451,25 @@ class BadgePdfService
-
'.mb_strtoupper($title, 'UTF-8').'
+
' . mb_strtoupper($title, 'UTF-8') . '
-
'.mb_strtoupper($role, 'UTF-8').'
+
' . mb_strtoupper($role, 'UTF-8') . '
@@ -475,18 +477,18 @@ class BadgePdfService
- '.$infoRows.' + ' . $infoRows . '
' - .($qrSrc !== '' - ? 'QR Code' - : ''). + . ($qrSrc !== '' + ? 'QR Code' + : '') . '
Scan to verify
-
'.$this->buildBarcodeHtml($barcodeValue).'
-
'.$barcodeText.'
+
' . $this->buildBarcodeHtml($barcodeValue) . '
+
' . $barcodeText . '
'; @@ -897,7 +899,7 @@ CSS; default => '4px', }; - $bars[] = ''; + $bars[] = ''; $bars[] = ''; } @@ -915,7 +917,7 @@ CSS; return null; } - $tmpPath = sys_get_temp_dir().DIRECTORY_SEPARATOR.'badge_qr_'.bin2hex(random_bytes(8)).'.png'; + $tmpPath = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'badge_qr_' . bin2hex(random_bytes(8)) . '.png'; if (@file_put_contents($tmpPath, $png) === false) { return null; } @@ -1000,11 +1002,11 @@ CSS; { return $this->formatter->firstExisting([ public_path($filename), - public_path('badges/'.$filename), - public_path('assets/images/'.$filename), - public_path('assets/images/badges/'.$filename), - storage_path('app/public/'.$filename), - storage_path('app/public/badges/'.$filename), + public_path('badges/' . $filename), + public_path('assets/images/' . $filename), + public_path('assets/images/badges/' . $filename), + storage_path('app/public/' . $filename), + storage_path('app/public/badges/' . $filename), ]); } @@ -1085,7 +1087,7 @@ CSS; /** * Shrink text until it fits in the available width. FPDF, bravely, does not do this for us. * - * @param array{0:int,1:int,2:int} $rgb + * @param array{0:int,1:int,2:int} $rgb */ protected function fitCenteredText( \FPDF $pdf, @@ -1147,7 +1149,7 @@ CSS; } /** - * @param array{0:int,1:int,2:int} $rgb + * @param array{0:int,1:int,2:int} $rgb */ protected function fitLeftText( \FPDF $pdf, @@ -1445,7 +1447,6 @@ CSS; foreach ($segments as [$type, $segmentWidth]) { if ($type === 'gap') { $cursor += $segmentWidth; - continue; } @@ -1509,7 +1510,7 @@ CSS; protected function preparedLogoPath(): ?string { $path = $this->logoFilePath(); - if ($path === null || ! is_readable($path)) { + if ($path === null || !is_readable($path)) { return null; } @@ -1518,7 +1519,7 @@ CSS; return $path; } - $tmpPath = sys_get_temp_dir().DIRECTORY_SEPARATOR.'badge_logo_'.bin2hex(random_bytes(8)).'.png'; + $tmpPath = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'badge_logo_' . bin2hex(random_bytes(8)) . '.png'; if (@file_put_contents($tmpPath, $png) === false) { return $path; } @@ -1531,7 +1532,7 @@ CSS; protected function logoDataUri(): ?string { $path = $this->logoFilePath(); - if ($path === null || ! is_readable($path)) { + if ($path === null || !is_readable($path)) { return null; } @@ -1543,9 +1544,10 @@ CSS; return null; } - return 'data:image/png;base64,'.base64_encode($bytes); + return 'data:image/png;base64,' . base64_encode($bytes); } + protected function headerMainTitle(string $schoolName): string { $name = trim($schoolName) !== '' ? trim($schoolName) : 'Al Rahma Sunday School'; @@ -1577,7 +1579,7 @@ CSS; { $address = trim((string) config('badges.footer_address', '')); if ($address !== '') { - return $schoolName."\n".$address; + return $schoolName . "\n" . $address; } return $schoolName; @@ -1585,7 +1587,7 @@ CSS; protected function makeCircularPngBytesFromPath(string $path): ?string { - if (! function_exists('imagecreatetruecolor') || ! function_exists('imagepng')) { + if (!function_exists('imagecreatetruecolor') || !function_exists('imagepng')) { return null; } @@ -1624,7 +1626,6 @@ CSS; $circle = imagecreatetruecolor($size, $size); if ($circle === false) { imagedestroy($square); - return null; } diff --git a/app/Services/Badges/BadgePrintLogService.php b/app/Services/Badges/BadgePrintLogService.php index 25fd6da3..e820f4ad 100644 --- a/app/Services/Badges/BadgePrintLogService.php +++ b/app/Services/Badges/BadgePrintLogService.php @@ -10,7 +10,8 @@ class BadgePrintLogService public function __construct( protected BadgePrintLog $badgePrintLogModel, protected BadgeTextFormatter $formatter - ) {} + ) { + } public function getStatus(array $userIds, ?string $schoolYear): array { @@ -50,7 +51,7 @@ class BadgePrintLogService try { $this->log($userIds, $actorId, $schoolYear, $rolesMap, $classesMap, $copies); } catch (\Throwable $e) { - Log::error('Failed to log badge prints: '.$e->getMessage()); + Log::error('Failed to log badge prints: ' . $e->getMessage()); } } -} +} \ No newline at end of file diff --git a/app/Services/Badges/BadgeService.php b/app/Services/Badges/BadgeService.php index c2bb7224..fdbcfb4c 100644 --- a/app/Services/Badges/BadgeService.php +++ b/app/Services/Badges/BadgeService.php @@ -23,7 +23,8 @@ class BadgeService protected TeacherClass $teacherClassModel, protected ClassSection $classSectionModel, protected BadgePrintLog $badgePrintLogModel - ) {} + ) { + } public function generateBadgePdf( array $userIds, @@ -54,7 +55,7 @@ class BadgeService foreach ($userIds as $uid) { $info = $this->getUserInfoById($uid, $schoolYear); - if (! $info || ! is_array($info)) { + if (!$info || !is_array($info)) { continue; } @@ -69,13 +70,13 @@ class BadgeService $roleResolved = $this->formatRole($roleResolved); $info['role_resolved'] = $roleResolved; - if (! empty($classesMap[$userId])) { + if (!empty($classesMap[$userId])) { $info['class_section_name'] = (string) $classesMap[$userId]; } $class = $this->norm($info['class_section_name'] ?? ''); - $badgeKey = strtolower(trim($userId.'|'.$name.'|'.$roleResolved.'|'.$class)); + $badgeKey = strtolower(trim($userId . '|' . $name . '|' . $roleResolved . '|' . $class)); if (isset($seen[$badgeKey])) { continue; } @@ -116,7 +117,7 @@ class BadgeService 1 ); } catch (\Throwable $e) { - Log::error('Failed to log badge prints: '.$e->getMessage()); + Log::error('Failed to log badge prints: ' . $e->getMessage()); } return response($pdfString, 200, [ @@ -158,7 +159,7 @@ class BadgeService $users = $this->fetchStaffList($schoolYear, $selectedUserIds); foreach ($users as &$u) { - if (! isset($u['user_id'])) { + if (!isset($u['user_id'])) { if (isset($u['id'])) { $u['user_id'] = (int) $u['id']; } @@ -189,7 +190,7 @@ class BadgeService $rolesDetect = strtolower($u['roles_raw'] ?? ($u['role_name_raw'] ?? '')); $isTeacherish = str_contains($rolesDetect, 'teacher') || preg_match('/\bta\b/', $rolesDetect); - if ($isTeacherish && ! empty($u['user_id'])) { + if ($isTeacherish && !empty($u['user_id'])) { $assign = $this->getLatestClassForUser((int) $u['user_id'], $schoolYear); if ($assign) { $u['class_section_id'] = $assign['class_section_id'] ?? null; @@ -233,11 +234,11 @@ class BadgeService ]) ->groupBy('users.id', 'users.firstname', 'users.lastname'); - if (! empty($selectedUserIds)) { + if (!empty($selectedUserIds)) { $query->whereIn('users.id', $selectedUserIds); } - if (! empty($schoolYear) && Schema::hasColumn('user_roles', 'school_year')) { + if (!empty($schoolYear) && Schema::hasColumn('user_roles', 'school_year')) { $query->where('user_roles.school_year', $schoolYear); } @@ -254,7 +255,7 @@ class BadgeService ]) ->where('tc.teacher_id', $userId); - if (! empty($schoolYear)) { + if (!empty($schoolYear)) { $query->where('tc.school_year', $schoolYear); } @@ -264,7 +265,7 @@ class BadgeService ->orderByDesc('tc.id') ->first(); - if (! $row || empty($row->class_section_id)) { + if (!$row || empty($row->class_section_id)) { return null; } @@ -281,12 +282,12 @@ class BadgeService ->where('id', $id) ->first(); - if (! $u) { + if (!$u) { return null; } $userId = (int) $u->user_id; - $fullname = trim(($u->firstname ?? '').' '.($u->lastname ?? '')); + $fullname = trim(($u->firstname ?? '') . ' ' . ($u->lastname ?? '')); $rolesRows = DB::table('user_roles as ur') ->join('roles as r', 'r.id', '=', 'ur.role_id') @@ -309,7 +310,7 @@ class BadgeService ->select(['class_section_id', 'updated_at', 'id', 'position', 'school_year']) ->where('teacher_id', $userId); - if (! empty($schoolYear)) { + if (!empty($schoolYear)) { $tcQuery->where('school_year', $schoolYear); } @@ -321,7 +322,7 @@ class BadgeService $position = strtolower(trim((string) ($assignment->position ?? ''))); $className = null; - if (! empty($classId)) { + if (!empty($classId)) { $className = $this->resolveClassName((int) $classId); } @@ -329,7 +330,7 @@ class BadgeService $roleLabel = 'Teacher Assistant'; } elseif ($position === 'teacher' || $position === 'main') { $roleLabel = 'Teacher'; - } elseif (! empty($allRoles)) { + } elseif (!empty($allRoles)) { $roleLabel = $allRoles[0]; } else { $roleLabel = 'Staff'; @@ -346,11 +347,11 @@ class BadgeService ]); $jobTitle = ''; - if (! empty($roleLabel) && ! empty($className)) { + if (!empty($roleLabel) && !empty($className)) { $jobTitle = "{$roleLabel} - {$className}"; - } elseif (! empty($roleLabel)) { + } elseif (!empty($roleLabel)) { $jobTitle = $roleLabel; - } elseif (! empty($className)) { + } elseif (!empty($className)) { $jobTitle = $className; } @@ -378,10 +379,10 @@ class BadgeService $pdf->Rect($x, $y, $w, $h); $logoSize = 6; - if (! empty($data['school_logo']) && file_exists($data['school_logo'])) { + if (!empty($data['school_logo']) && file_exists($data['school_logo'])) { $pdf->Image($data['school_logo'], $x + $w - 6 - $logoSize, $y + 4, $logoSize + 3, $logoSize - 1); } - if (! empty($data['isgl_logo']) && file_exists($data['isgl_logo'])) { + if (!empty($data['isgl_logo']) && file_exists($data['isgl_logo'])) { $pdf->Image($data['isgl_logo'], $x + 4, $y + 4, $logoSize + 3, $logoSize - 1); } @@ -417,11 +418,11 @@ class BadgeService if ($isTeacherish && $class !== '') { if (strtolower($class) === 'youth') { - $display = 'Youth '.$roleResolved; + $display = 'Youth ' . $roleResolved; } elseif ($class === 'KG') { - $display = 'KG '.$roleResolved; + $display = 'KG ' . $roleResolved; } else { - $display = 'Grade '.$class.' '.$roleResolved; + $display = 'Grade ' . $class . ' ' . $roleResolved; } } elseif ($roleResolved !== '') { $display = $roleResolved; @@ -460,7 +461,7 @@ class BadgeService 'role', 'active_role', 'role_name', - fn ($r) => ! empty($r['roles']) + fn ($r) => !empty($r['roles']) ? (array_filter(array_map('trim', explode(',', (string) $r['roles'])))[0] ?? '') : '', 'job_title', @@ -478,7 +479,7 @@ class BadgeService return $this->norm($v); } } else { - if (! empty($info[$key])) { + if (!empty($info[$key])) { $v = $this->norm((string) $info[$key]); if ($v !== '') { return $v; @@ -578,7 +579,6 @@ class BadgeService if ($start === false) { $out[] = ucfirst(mb_strtolower($tok, 'UTF-8')); - continue; } @@ -594,12 +594,12 @@ class BadgeService $coreFmt = preg_replace_callback( '/\p{L}+/u', static fn ($m) => mb_strtoupper(mb_substr($m[0], 0, 1, 'UTF-8'), 'UTF-8') - .mb_strtolower(mb_substr($m[0], 1, null, 'UTF-8'), 'UTF-8'), + . mb_strtolower(mb_substr($m[0], 1, null, 'UTF-8'), 'UTF-8'), mb_strtolower($core, 'UTF-8') ); } - $out[] = $pre.$coreFmt.$suf; + $out[] = $pre . $coreFmt . $suf; } return implode(' ', $out); @@ -640,7 +640,7 @@ class BadgeService protected function firstExisting(array $paths): ?string { foreach ($paths as $path) { - if (! empty($path) && file_exists($path)) { + if (!empty($path) && file_exists($path)) { return $path; } } @@ -697,4 +697,4 @@ class BadgeService return $schoolYears; } -} +} \ No newline at end of file diff --git a/app/Services/Badges/BadgeStudentLookupService.php b/app/Services/Badges/BadgeStudentLookupService.php index e8c4473a..ea519b0a 100644 --- a/app/Services/Badges/BadgeStudentLookupService.php +++ b/app/Services/Badges/BadgeStudentLookupService.php @@ -12,7 +12,8 @@ class BadgeStudentLookupService { public function __construct( protected BadgeTextFormatter $formatter - ) {} + ) { + } /** * Active student roster for the badge picker. @@ -28,7 +29,7 @@ class BadgeStudentLookupService ->leftJoin('student_class as sc', function ($join) use ($schoolYear) { $join->on('sc.student_id', '=', 'students.id'); - if (! empty($schoolYear)) { + if (!empty($schoolYear)) { $join->where('sc.school_year', '=', $schoolYear); } }) @@ -54,9 +55,9 @@ class BadgeStudentLookupService ->orderBy('students.lastname') ->orderBy('students.firstname'); - if (! empty($selectedStudentIds)) { + if (!empty($selectedStudentIds)) { $query->whereIn('students.id', $selectedStudentIds); - } elseif (! empty($schoolYear)) { + } elseif (!empty($schoolYear)) { $query->where(function ($q) use ($schoolYear) { $q->where('students.school_year', $schoolYear) ->orWhereNotNull('sc.student_id'); @@ -73,7 +74,7 @@ class BadgeStudentLookupService } $schoolId = trim((string) ($row['school_id'] ?? '')); - if ($schoolId === '' || ! preg_match('/^[A-Za-z0-9_-]+$/', $schoolId)) { + if ($schoolId === '' || !preg_match('/^[A-Za-z0-9_-]+$/', $schoolId)) { continue; } @@ -116,7 +117,7 @@ class BadgeStudentLookupService $out = []; foreach ($rows as $student) { $schoolId = trim((string) ($student->school_id ?? '')); - if ($schoolId === '' || ! preg_match('/^[A-Za-z0-9_-]+$/', $schoolId)) { + if ($schoolId === '' || !preg_match('/^[A-Za-z0-9_-]+$/', $schoolId)) { continue; } diff --git a/app/Services/Badges/BadgeTextFormatter.php b/app/Services/Badges/BadgeTextFormatter.php index 1336def0..2e72731d 100644 --- a/app/Services/Badges/BadgeTextFormatter.php +++ b/app/Services/Badges/BadgeTextFormatter.php @@ -78,7 +78,7 @@ class BadgeTextFormatter } $special = [ - 'of' => 'of', + 'of' => 'of', 'head' => 'Head', ]; @@ -95,7 +95,6 @@ class BadgeTextFormatter if ($start === false) { $out[] = ucfirst(mb_strtolower($tok, 'UTF-8')); - continue; } @@ -111,12 +110,12 @@ class BadgeTextFormatter $coreFmt = preg_replace_callback( '/\p{L}+/u', static fn ($m) => mb_strtoupper(mb_substr($m[0], 0, 1, 'UTF-8'), 'UTF-8') - .mb_strtolower(mb_substr($m[0], 1, null, 'UTF-8'), 'UTF-8'), + . mb_strtolower(mb_substr($m[0], 1, null, 'UTF-8'), 'UTF-8'), mb_strtolower($core, 'UTF-8') ); } - $out[] = $pre.$coreFmt.$suf; + $out[] = $pre . $coreFmt . $suf; } return implode(' ', $out); @@ -164,7 +163,7 @@ class BadgeTextFormatter 'role', 'active_role', 'role_name', - fn ($r) => ! empty($r['roles']) + fn ($r) => !empty($r['roles']) ? (array_values(array_filter(array_map('trim', explode(',', (string) $r['roles']))))[0] ?? '') : '', 'job_title', @@ -182,7 +181,7 @@ class BadgeTextFormatter return $this->norm($v); } } else { - if (! empty($info[$key])) { + if (!empty($info[$key])) { $v = $this->norm((string) $info[$key]); if ($v !== '') { return $v; @@ -197,7 +196,7 @@ class BadgeTextFormatter public function firstExisting(array $paths): ?string { foreach ($paths as $path) { - if (! empty($path) && file_exists($path)) { + if (!empty($path) && file_exists($path)) { return $path; } } diff --git a/app/Services/Badges/BadgeUserLookupService.php b/app/Services/Badges/BadgeUserLookupService.php index 6e501b32..f9ff53e6 100644 --- a/app/Services/Badges/BadgeUserLookupService.php +++ b/app/Services/Badges/BadgeUserLookupService.php @@ -9,7 +9,8 @@ class BadgeUserLookupService { public function __construct( protected BadgeTextFormatter $formatter - ) {} + ) { + } public function fetchStaffList(?string $schoolYear, array $selectedUserIds = []): array { @@ -24,7 +25,7 @@ class BadgeUserLookupService ]) ->groupBy('users.id', 'users.firstname', 'users.lastname'); - if (! empty($selectedUserIds)) { + if (!empty($selectedUserIds)) { $query->whereIn('users.id', $selectedUserIds); } @@ -64,7 +65,7 @@ class BadgeUserLookupService ]) ->where('tc.teacher_id', $userId); - if (! empty($schoolYear)) { + if (!empty($schoolYear)) { $query->where('tc.school_year', $schoolYear); } @@ -74,12 +75,12 @@ class BadgeUserLookupService ->orderByDesc('tc.id') ->first(); - if (! $row || empty($row->class_section_id)) { + if (!$row || empty($row->class_section_id)) { return null; } return [ - 'class_section_id' => (int) $row->class_section_id, + 'class_section_id' => (int) $row->class_section_id, 'class_section_name' => $row->class_section_name ?? null, ]; } @@ -91,12 +92,12 @@ class BadgeUserLookupService ->where('id', $id) ->first(); - if (! $u) { + if (!$u) { return null; } $userId = (int) $u->user_id; - $fullname = trim(($u->firstname ?? '').' '.($u->lastname ?? '')); + $fullname = trim(($u->firstname ?? '') . ' ' . ($u->lastname ?? '')); $schoolId = trim((string) ($u->school_id ?? '')); if ($schoolId === '') { $schoolId = trim((string) ($u->account_id ?? '')); @@ -123,7 +124,7 @@ class BadgeUserLookupService ->select(['class_section_id', 'updated_at', 'id', 'position', 'school_year']) ->where('teacher_id', $userId); - if (! empty($schoolYear)) { + if (!empty($schoolYear)) { $tcQuery->where('school_year', $schoolYear); } @@ -135,7 +136,7 @@ class BadgeUserLookupService $position = strtolower(trim((string) ($assignment->position ?? ''))); $className = null; - if (! empty($classId)) { + if (!empty($classId)) { $className = $this->resolveClassName((int) $classId); } @@ -143,7 +144,7 @@ class BadgeUserLookupService $roleLabel = 'Teacher Assistant'; } elseif ($position === 'teacher' || $position === 'main') { $roleLabel = 'Teacher'; - } elseif (! empty($allRoles)) { + } elseif (!empty($allRoles)) { $roleLabel = $allRoles[0]; } else { $roleLabel = 'Staff'; @@ -160,27 +161,27 @@ class BadgeUserLookupService ]); $jobTitle = ''; - if (! empty($roleLabel) && ! empty($className)) { + if (!empty($roleLabel) && !empty($className)) { $jobTitle = "{$roleLabel} - {$className}"; - } elseif (! empty($roleLabel)) { + } elseif (!empty($roleLabel)) { $jobTitle = $roleLabel; - } elseif (! empty($className)) { + } elseif (!empty($className)) { $jobTitle = $className; } return [ - 'user_id' => $userId, - 'name' => $fullname !== '' ? $fullname : 'STAFF', - 'role' => $roleLabel, - 'roles' => $rolesCsv, - 'class_section_id' => $classId ? (int) $classId : null, - 'class_section_name' => $className, - 'job_title' => $jobTitle, - 'school_name' => 'Al Rahma Sunday School', - 'school_id' => $schoolId, - 'school_logo' => $schoolLogo, - 'isgl_logo' => $isglLogo, - 'school_year' => $assignment->school_year ?? $schoolYear, + 'user_id' => $userId, + 'name' => $fullname !== '' ? $fullname : 'STAFF', + 'role' => $roleLabel, + 'roles' => $rolesCsv, + 'class_section_id' => $classId ? (int) $classId : null, + 'class_section_name' => $className, + 'job_title' => $jobTitle, + 'school_name' => 'Al Rahma Sunday School', + 'school_id' => $schoolId, + 'school_logo' => $schoolLogo, + 'isgl_logo' => $isglLogo, + 'school_year' => $assignment->school_year ?? $schoolYear, ]; } diff --git a/app/Services/Billing/BalanceCalculationService.php b/app/Services/Billing/BalanceCalculationService.php index 2d3a4a46..46ca0a0b 100644 --- a/app/Services/Billing/BalanceCalculationService.php +++ b/app/Services/Billing/BalanceCalculationService.php @@ -21,10 +21,12 @@ class BalanceCalculationService private FeeConfigService $config, private TuitionCalculationService $tuition, private BillingTotalsService $billingTotals - ) {} + ) { + } /** - * @param array> $students + * @param array> $students + * * @return array */ public function calculateFamilyAccount(array $students, int $parentId, ?string $schoolYear = null): array @@ -96,8 +98,9 @@ class BalanceCalculationService } /** - * @param array> $tuitionStudents - * @param array $eventByStudent + * @param array> $tuitionStudents + * @param array $eventByStudent + * * @return array> */ private function buildStudentBalances( diff --git a/app/Services/Billing/BillingTotalsService.php b/app/Services/Billing/BillingTotalsService.php index b08188dc..92ce9a6c 100644 --- a/app/Services/Billing/BillingTotalsService.php +++ b/app/Services/Billing/BillingTotalsService.php @@ -96,7 +96,7 @@ class BillingTotalsService } /** - * @param array $rows + * @param array $rows */ private function sumAdditionalChargeRows(array $rows): float { diff --git a/app/Services/Billing/ChargeService.php b/app/Services/Billing/ChargeService.php index 9167a7a3..6c2125ef 100644 --- a/app/Services/Billing/ChargeService.php +++ b/app/Services/Billing/ChargeService.php @@ -26,7 +26,8 @@ class ChargeService private ExtraChargesInvoiceService $invoices, private ExtraChargesContextService $context, private InvoiceGenerationService $invoiceGeneration - ) {} + ) { + } public function createExtraCharge(array $payload): array { @@ -59,7 +60,7 @@ class ChargeService 'semester' => $payload['semester'] ?? $this->context->getSemester(), ])); - if (! ($result['ok'] ?? false)) { + if (!($result['ok'] ?? false)) { return $result; } @@ -69,14 +70,14 @@ class ChargeService 'id' => (int) ($result['id'] ?? 0), 'parent_id' => $parentId, 'invoice_id' => $result['invoice_id'] ?? null, - 'status' => ! empty($payload['invoice_id']) ? 'applied' : 'pending', + 'status' => !empty($payload['invoice_id']) ? 'applied' : 'pending', ]; } public function cancelExtraCharge(int $chargeId): array { $charge = AdditionalCharge::query()->find($chargeId); - if (! $charge) { + if (!$charge) { return ['ok' => false, 'message' => 'Extra charge not found.']; } @@ -98,7 +99,7 @@ class ChargeService public function applyExtraCharge(int $chargeId, int $invoiceId): array { $charge = AdditionalCharge::query()->find($chargeId); - if (! $charge) { + if (!$charge) { return ['ok' => false, 'message' => 'Extra charge not found.']; } @@ -164,7 +165,7 @@ class ChargeService $amount = isset($payload['amount']) ? (float) $payload['amount'] : null; if ($eventId > 0) { $event = Event::getEvent($eventId, $schoolYear); - if (! $event) { + if (!$event) { return ['ok' => false, 'message' => 'Event not found.']; } $eventName = (string) ($event->event_name ?? $eventName); @@ -224,7 +225,7 @@ class ChargeService public function cancelEventCharge(int $chargeId, bool $issueCredit = true): array { $charge = EventCharges::query()->find($chargeId); - if (! $charge) { + if (!$charge) { return ['ok' => false, 'message' => 'Event charge not found.']; } @@ -251,8 +252,8 @@ class ChargeService if ($issueCredit && $amount > 0 && $parentId > 0) { $credit = $this->createExtraCharge([ 'parent_id' => $parentId, - 'title' => 'Event credit: '.$eventName, - 'description' => 'Account credit for canceled event charge #'.$chargeId, + 'title' => 'Event credit: ' . $eventName, + 'description' => 'Account credit for canceled event charge #' . $chargeId, 'amount' => $amount, 'charge_type' => 'deduct', 'school_year' => $schoolYear, @@ -286,7 +287,7 @@ class ChargeService public function markEventChargePaid(int $chargeId, bool $paid, ?int $paymentId = null): array { $charge = EventCharges::query()->find($chargeId); - if (! $charge) { + if (!$charge) { return ['ok' => false, 'message' => 'Event charge not found.']; } @@ -388,7 +389,7 @@ class ChargeService private function resolveEventName(EventCharges $charge): string { - if (! empty($charge->event_name)) { + if (!empty($charge->event_name)) { return (string) $charge->event_name; } @@ -425,7 +426,7 @@ class ChargeService } /** - * @param array $row + * @param array $row */ private function formatEventCharge(array $row): array { diff --git a/app/Services/BroadcastEmail/BroadcastEmailComposerService.php b/app/Services/BroadcastEmail/BroadcastEmailComposerService.php index cc58abcb..a0361dd3 100644 --- a/app/Services/BroadcastEmail/BroadcastEmailComposerService.php +++ b/app/Services/BroadcastEmail/BroadcastEmailComposerService.php @@ -11,7 +11,6 @@ class BroadcastEmailComposerService $html = preg_replace('#<(script|iframe)[^>]*>.*?#is', '', $html); $html = preg_replace('/\\son\\w+="[^"]*"/i', '', $html); $html = preg_replace("/\\son\\w+='[^']*'/i", '', $html); - return $html ?? ''; } @@ -27,7 +26,7 @@ class BroadcastEmailComposerService ): string { $content = $personalize ? str_replace('{{name}}', $recipientName, $body) : $body; - if (! $wrapLayout) { + if (!$wrapLayout) { return $content; } diff --git a/app/Services/BroadcastEmail/BroadcastEmailDispatchService.php b/app/Services/BroadcastEmail/BroadcastEmailDispatchService.php index a7f2c3f9..5962317c 100644 --- a/app/Services/BroadcastEmail/BroadcastEmailDispatchService.php +++ b/app/Services/BroadcastEmail/BroadcastEmailDispatchService.php @@ -7,7 +7,6 @@ use App\Services\EmailService; class BroadcastEmailDispatchService { private EmailService $mailer; - private BroadcastEmailComposerService $composer; public function __construct(EmailService $mailer, BroadcastEmailComposerService $composer) @@ -31,7 +30,7 @@ class BroadcastEmailDispatchService $ok = $this->mailer->send( $payload['test_email'], - '[TEST] '.$payload['subject'], + '[TEST] ' . $payload['subject'], $html, $payload['from_key'] ); diff --git a/app/Services/BroadcastEmail/BroadcastEmailImageService.php b/app/Services/BroadcastEmail/BroadcastEmailImageService.php index 255f2026..eddac183 100644 --- a/app/Services/BroadcastEmail/BroadcastEmailImageService.php +++ b/app/Services/BroadcastEmail/BroadcastEmailImageService.php @@ -26,7 +26,7 @@ class BroadcastEmailImageService } $mime = strtolower((string) $file->getMimeType()); - if (! isset($this->allowedTypes[$mime])) { + if (!isset($this->allowedTypes[$mime])) { return [ 'ok' => false, 'status' => 415, @@ -43,17 +43,17 @@ class BroadcastEmailImageService } $targetDir = public_path('uploads/email'); - if (! is_dir($targetDir)) { + if (!is_dir($targetDir)) { @mkdir($targetDir, 0755, true); } $ext = $this->allowedTypes[$mime]; - $newName = 'em_'.bin2hex(random_bytes(16)).'.'.$ext; + $newName = 'em_' . bin2hex(random_bytes(16)) . '.' . $ext; $file->move($targetDir, $newName); return [ 'ok' => true, - 'url' => asset('uploads/email/'.$newName), + 'url' => asset('uploads/email/' . $newName), ]; } } diff --git a/app/Services/BroadcastEmail/BroadcastEmailRecipientService.php b/app/Services/BroadcastEmail/BroadcastEmailRecipientService.php index 10cd1630..0649ef0a 100644 --- a/app/Services/BroadcastEmail/BroadcastEmailRecipientService.php +++ b/app/Services/BroadcastEmail/BroadcastEmailRecipientService.php @@ -13,7 +13,6 @@ class BroadcastEmailRecipientService return array_values(array_filter($parents, static function ($parent) { $email = $parent['email'] ?? ''; - return is_string($email) && trim($email) !== ''; })); } diff --git a/app/Services/BroadcastEmail/BroadcastEmailSenderOptionsService.php b/app/Services/BroadcastEmail/BroadcastEmailSenderOptionsService.php index dde00441..294458b8 100644 --- a/app/Services/BroadcastEmail/BroadcastEmailSenderOptionsService.php +++ b/app/Services/BroadcastEmail/BroadcastEmailSenderOptionsService.php @@ -9,13 +9,12 @@ class BroadcastEmailSenderOptionsService $json = getenv('MAIL_SENDERS') ?: env('MAIL_SENDERS', '{}'); $arr = json_decode($json, true); - if (! is_array($arr) || empty($arr)) { + if (!is_array($arr) || empty($arr)) { $smtpUser = getenv('SMTP_USER') ?: ''; $name = 'Al Rahma Sunday School'; - return [[ 'key' => 'general', - 'label' => $name.($smtpUser ? " <{$smtpUser}>" : ''), + 'label' => $name . ($smtpUser ? " <{$smtpUser}>" : ''), ]]; } @@ -25,7 +24,7 @@ class BroadcastEmailSenderOptionsService $email = $info['email'] ?? ''; $out[] = [ 'key' => (string) $key, - 'label' => trim($name.($email ? " <{$email}>" : '')), + 'label' => trim($name . ($email ? " <{$email}>" : '')), ]; } diff --git a/app/Services/Certificates/CertificateAdminService.php b/app/Services/Certificates/CertificateAdminService.php index 82540f38..52e8668d 100644 --- a/app/Services/Certificates/CertificateAdminService.php +++ b/app/Services/Certificates/CertificateAdminService.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace App\Services\Certificates; +use App\Models\CertificateRecord; use App\Models\Configuration; use App\Models\StudentDecision; use Illuminate\Support\Facades\DB; @@ -640,7 +641,6 @@ class CertificateAdminService if (preg_match('/grade\s*(\d+)/i', $name, $matches) === 1) { $grade = (int) $matches[1]; - return [ 'Grade '.$grade, '1_'.str_pad((string) $grade, 3, '0', STR_PAD_LEFT), diff --git a/app/Services/Certificates/CertificatePdfService.php b/app/Services/Certificates/CertificatePdfService.php index c07690f9..8bc5cce2 100644 --- a/app/Services/Certificates/CertificatePdfService.php +++ b/app/Services/Certificates/CertificatePdfService.php @@ -7,13 +7,12 @@ namespace App\Services\Certificates; class CertificatePdfService { private string $fontDir; - private string $imgDir; public function __construct() { - $this->fontDir = public_path('assets/certificates/fonts').DIRECTORY_SEPARATOR; - $this->imgDir = public_path('assets/certificates/images').DIRECTORY_SEPARATOR; + $this->fontDir = public_path('assets/certificates/fonts') . DIRECTORY_SEPARATOR; + $this->imgDir = public_path('assets/certificates/images') . DIRECTORY_SEPARATOR; } /** @@ -21,9 +20,9 @@ class CertificatePdfService */ public function generate(array $students, string $certDate): string { - $edwardianFont = \TCPDF_FONTS::addTTFfont($this->fontDir.'Edwardian Script ITC Regular.ttf', 'TrueTypeUnicode', '', 32); - $garamondBold = \TCPDF_FONTS::addTTFfont($this->fontDir.'Garamond Bold.ttf', 'TrueTypeUnicode', '', 32); - $ebGaramond = \TCPDF_FONTS::addTTFfont($this->fontDir.'EBGaramond-Regular.ttf', 'TrueTypeUnicode', '', 32); + $edwardianFont = \TCPDF_FONTS::addTTFfont($this->fontDir . 'Edwardian Script ITC Regular.ttf', 'TrueTypeUnicode', '', 32); + $garamondBold = \TCPDF_FONTS::addTTFfont($this->fontDir . 'Garamond Bold.ttf', 'TrueTypeUnicode', '', 32); + $ebGaramond = \TCPDF_FONTS::addTTFfont($this->fontDir . 'EBGaramond-Regular.ttf', 'TrueTypeUnicode', '', 32); $pdf = new \TCPDF('L', 'pt', 'A4', true, 'UTF-8', false); $pdf->SetCreator('Al Rahma Sunday School'); @@ -40,7 +39,7 @@ class CertificatePdfService foreach ($students as $student) { $pdf->AddPage(); - $name = $student['firstname'].' '.$student['lastname']; + $name = $student['firstname'] . ' ' . $student['lastname']; $grade = $this->formatGrade((string) ($student['grade'] ?? '')); $certNumber = (string) ($student['cert_number'] ?? ''); $verifyUrl = is_string($student['verify_url'] ?? null) ? $student['verify_url'] : null; @@ -65,8 +64,8 @@ class CertificatePdfService private function drawCertificate( \TCPDF $pdf, - float $W, - float $H, + float $W, + float $H, string $name, string $grade, string $certDate, @@ -76,9 +75,9 @@ class CertificatePdfService string $garamondBold, string $ebGaramond ): void { - $pdf->Image($this->imgDir.'title.png', 126, 0, 600); - $pdf->Image($this->imgDir.'background.png', 280, 176, 291, 340); - $pdf->Image($this->imgDir.'signature.png', 140, 410, 90, 80); + $pdf->Image($this->imgDir . 'title.png', 126, 0, 600); + $pdf->Image($this->imgDir . 'background.png', 280, 176, 291, 340); + $pdf->Image($this->imgDir . 'signature.png', 140, 410, 90, 80); $pdf->SetFont('times', 'B', 24); $pdf->SetTextColor(0, 0, 0); @@ -136,7 +135,7 @@ class CertificatePdfService $pdf->SetFont('helvetica', '', 8); $pdf->SetTextColor(150, 150, 150); $pdf->SetXY(70.86, $H - 39.68); - $pdf->Cell(200, 12, 'Certificate No. '.$certNumber, 0, 0, 'L'); + $pdf->Cell(200, 12, 'Certificate No. ' . $certNumber, 0, 0, 'L'); $pdf->SetTextColor(0, 0, 0); } } @@ -162,7 +161,7 @@ class CertificatePdfService $lower = strtolower($clean); if (preg_match('/^\d+$/', $clean) && (int) $clean >= 1 && (int) $clean <= 9) { - return 'Grade '.$clean; + return 'Grade ' . $clean; } if ($lower === 'youth') { return 'Youth'; diff --git a/app/Services/ClassPreparation/ClassPreparationAdjustmentWriterService.php b/app/Services/ClassPreparation/ClassPreparationAdjustmentWriterService.php index 83d75c7e..fb2b0942 100644 --- a/app/Services/ClassPreparation/ClassPreparationAdjustmentWriterService.php +++ b/app/Services/ClassPreparation/ClassPreparationAdjustmentWriterService.php @@ -16,7 +16,7 @@ class ClassPreparationAdjustmentWriterService foreach ($adjustments as $itemName => $adjustment) { $item = (string) $itemName; - if ($item === '' || ! isset($allowed[$item])) { + if ($item === '' || !isset($allowed[$item])) { continue; } @@ -32,7 +32,6 @@ class ClassPreparationAdjustmentWriterService 'adjustable' => 1, ]); $count++; - continue; } diff --git a/app/Services/ClassPreparation/ClassPreparationCalculatorService.php b/app/Services/ClassPreparation/ClassPreparationCalculatorService.php index 96ff72dc..8b23c58e 100644 --- a/app/Services/ClassPreparation/ClassPreparationCalculatorService.php +++ b/app/Services/ClassPreparation/ClassPreparationCalculatorService.php @@ -37,7 +37,6 @@ class ClassPreparationCalculatorService if ($numValue > 0 && $numValue < 30) { $firstDigit = (int) substr((string) $numValue, 0, 1); - return $firstDigit === 1 ? 1 : ($firstDigit === 2 ? 2 : 3); } @@ -70,12 +69,10 @@ class ClassPreparationCalculatorService if ($gradeMin !== null && $classLevel < (int) $gradeMin) { $items[$name] = 0; - continue; } if ($gradeMax !== null && $classLevel > (int) $gradeMax) { $items[$name] = 0; - continue; } diff --git a/app/Services/ClassPreparation/ClassPreparationContextService.php b/app/Services/ClassPreparation/ClassPreparationContextService.php index 84591cdc..a3d80999 100644 --- a/app/Services/ClassPreparation/ClassPreparationContextService.php +++ b/app/Services/ClassPreparation/ClassPreparationContextService.php @@ -39,7 +39,6 @@ class ClassPreparationContextService ->count(); $this->rosterPresenceCache[$key] = $cnt > 0; - return $this->rosterPresenceCache[$key]; } } diff --git a/app/Services/ClassPreparation/ClassPreparationInventoryService.php b/app/Services/ClassPreparation/ClassPreparationInventoryService.php index 2f443429..c28c847a 100644 --- a/app/Services/ClassPreparation/ClassPreparationInventoryService.php +++ b/app/Services/ClassPreparation/ClassPreparationInventoryService.php @@ -50,7 +50,7 @@ class ClassPreparationInventoryService } $needsFallback = array_filter($inventoryMap, static fn ($v) => (int) $v === 0); - if (! empty($needsFallback)) { + if (!empty($needsFallback)) { $fallbackRows = DB::table('inventory_items') ->select('name AS item_name, COALESCE(SUM(CASE WHEN good_qty IS NOT NULL AND good_qty > 0 THEN good_qty WHEN `condition`=\"good\" THEN quantity ELSE quantity END),0) AS available') ->where('type', 'classroom') @@ -79,7 +79,6 @@ class ClassPreparationInventoryService if ($goodQty > 0) { $inventoryMap[$name] = $goodQty; - continue; } diff --git a/app/Services/ClassPreparation/ClassPreparationLogService.php b/app/Services/ClassPreparation/ClassPreparationLogService.php index 42ff6c25..23d51b96 100644 --- a/app/Services/ClassPreparation/ClassPreparationLogService.php +++ b/app/Services/ClassPreparation/ClassPreparationLogService.php @@ -33,7 +33,6 @@ class ClassPreparationLogService 'prep_data' => json_encode($items), 'created_at' => $createdAt, ]); - return true; } catch (\Throwable $e) { return false; diff --git a/app/Services/ClassPreparation/ClassPreparationPrintService.php b/app/Services/ClassPreparation/ClassPreparationPrintService.php index d1e06c45..ae9128f2 100644 --- a/app/Services/ClassPreparation/ClassPreparationPrintService.php +++ b/app/Services/ClassPreparation/ClassPreparationPrintService.php @@ -13,7 +13,8 @@ class ClassPreparationPrintService private ClassPreparationCalculatorService $calculator, private ClassPreparationAdjustmentService $adjustments, private ClassPreparationLogService $logs - ) {} + ) { + } public function printPrep(string $classSectionId, string $schoolYear, ?string $semester = null): array { @@ -30,7 +31,7 @@ class ClassPreparationPrintService $printedAt = $this->utcNow(); $ok = $this->logs->createLog($classSectionId, $className, $schoolYear, $items, $printedAt); - if (! $ok) { + if (!$ok) { Log::warning('Failed to store class preparation log.', [ 'class_section_id' => $classSectionId, 'school_year' => $schoolYear, @@ -54,7 +55,6 @@ class ClassPreparationPrintService if (function_exists('utc_now')) { return (string) utc_now(); } - return now('UTC')->toDateTimeString(); } } diff --git a/app/Services/ClassPreparation/ClassPreparationRosterService.php b/app/Services/ClassPreparation/ClassPreparationRosterService.php index 16c16a12..3332d00b 100644 --- a/app/Services/ClassPreparation/ClassPreparationRosterService.php +++ b/app/Services/ClassPreparation/ClassPreparationRosterService.php @@ -35,7 +35,6 @@ class ClassPreparationRosterService } $row = $query->first(); - return (int) ($row->cnt ?? 0); } } diff --git a/app/Services/ClassPreparation/ClassPreparationService.php b/app/Services/ClassPreparation/ClassPreparationService.php index 3a047a14..d24e210e 100644 --- a/app/Services/ClassPreparation/ClassPreparationService.php +++ b/app/Services/ClassPreparation/ClassPreparationService.php @@ -7,19 +7,12 @@ use App\Models\ClassSection; class ClassPreparationService { private ClassPreparationContextService $context; - private ClassPreparationRosterService $roster; - private ClassPreparationInventoryService $inventory; - private ClassPreparationCalculatorService $calculator; - private ClassPreparationAdjustmentService $adjustments; - private ClassPreparationLogService $logs; - private ClassPreparationAdjustmentWriterService $adjustmentWriter; - private ClassPreparationPrintService $printService; public function __construct( @@ -73,14 +66,14 @@ class ClassPreparationService $hasChanged = $this->logs->hasPrepChanged($baseItems, $oldPrep); $results[] = [ - 'class_section' => $className, + 'class_section' => $className, 'class_section_id' => $classSectionId, - 'student_count' => $studentCount, - 'class_level' => $classLevel, - 'prep_items' => $baseItems, - 'needs_print' => $hasChanged, - 'last_printed_at' => $oldSnap?->created_at, - 'adjustments' => $adjMap, + 'student_count' => $studentCount, + 'class_level' => $classLevel, + 'prep_items' => $baseItems, + 'needs_print' => $hasChanged, + 'last_printed_at' => $oldSnap?->created_at, + 'adjustments' => $adjMap, ]; } @@ -146,7 +139,6 @@ class ClassPreparationService if (function_exists('utc_now')) { return (string) utc_now(); } - return now('UTC')->toDateTimeString(); } } diff --git a/app/Services/ClassProgress/ClassProgressAttachmentService.php b/app/Services/ClassProgress/ClassProgressAttachmentService.php index bf849b42..b5b8559c 100644 --- a/app/Services/ClassProgress/ClassProgressAttachmentService.php +++ b/app/Services/ClassProgress/ClassProgressAttachmentService.php @@ -5,6 +5,7 @@ namespace App\Services\ClassProgress; use App\Models\ClassProgressAttachment; use Illuminate\Http\UploadedFile; use Illuminate\Support\Facades\Log; +use Illuminate\Support\Facades\Storage; class ClassProgressAttachmentService { @@ -16,7 +17,7 @@ class ClassProgressAttachmentService $stored = []; foreach ($files as $file) { - if (! $file instanceof UploadedFile || ! $file->isValid()) { + if (!$file instanceof UploadedFile || !$file->isValid()) { continue; } @@ -48,17 +49,17 @@ class ClassProgressAttachmentService $candidates = []; if (str_starts_with($path, 'storage/')) { $relative = ltrim(substr($path, strlen('storage/')), '/'); - $candidates[] = storage_path('app/public/'.$relative); + $candidates[] = storage_path('app/public/' . $relative); } if (str_starts_with($path, 'writable/uploads/')) { $relative = ltrim(substr($path, strlen('writable/uploads/')), '/'); - $candidates[] = storage_path('app/'.$relative); - $candidates[] = storage_path('app/public/'.$relative); + $candidates[] = storage_path('app/' . $relative); + $candidates[] = storage_path('app/public/' . $relative); } - $candidates[] = storage_path('app/'.ltrim($path, '/')); - $candidates[] = storage_path('app/public/'.ltrim($path, '/')); + $candidates[] = storage_path('app/' . ltrim($path, '/')); + $candidates[] = storage_path('app/public/' . ltrim($path, '/')); foreach ($candidates as $candidate) { if (is_file($candidate)) { @@ -83,6 +84,6 @@ class ClassProgressAttachmentService throw $e; } - return 'storage/'.ltrim($path, '/'); + return 'storage/' . ltrim($path, '/'); } } diff --git a/app/Services/ClassProgress/ClassProgressMetaService.php b/app/Services/ClassProgress/ClassProgressMetaService.php index bc04e930..8b016aeb 100644 --- a/app/Services/ClassProgress/ClassProgressMetaService.php +++ b/app/Services/ClassProgress/ClassProgressMetaService.php @@ -140,7 +140,7 @@ class ClassProgressMetaService public function curriculumOptions(?int $classId, ?string $schoolYear = null): array { - if (! $classId) { + if (!$classId) { return []; } diff --git a/app/Services/ClassProgress/ClassProgressMutationService.php b/app/Services/ClassProgress/ClassProgressMutationService.php index 7bcaf3e6..86602d0f 100644 --- a/app/Services/ClassProgress/ClassProgressMutationService.php +++ b/app/Services/ClassProgress/ClassProgressMutationService.php @@ -7,11 +7,10 @@ use App\Models\ClassProgressReport; use App\Models\TeacherClass; use App\Models\User; use App\Services\SchoolYears\SchoolYearContextService; -use Carbon\CarbonInterface; +use Illuminate\Support\Facades\Schema; use Illuminate\Support\Collection; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; -use Illuminate\Support\Facades\Schema; class ClassProgressMutationService { @@ -35,11 +34,11 @@ class ClassProgressMutationService $weekStart = (string) ($payload['week_start'] ?? ''); $weekEnd = $this->rules->ensureWeekEnd($weekStart, $payload['week_end'] ?? null); - if (! $this->rules->isWeekEndValid($weekStart, $weekEnd)) { + if (!$this->rules->isWeekEndValid($weekStart, $weekEnd)) { throw new \InvalidArgumentException('Week end must be the same as or after the week start.'); } - if (! $this->rules->hasIslamicUnitSelection($payload)) { + if (!$this->rules->hasIslamicUnitSelection($payload)) { throw new \InvalidArgumentException('Please select at least one Islamic Studies unit.'); } @@ -54,7 +53,7 @@ class ClassProgressMutationService ->values() ->all(); - if ($existingIds !== [] && ! $confirmOverwrite) { + if ($existingIds !== [] && !$confirmOverwrite) { throw new \InvalidArgumentException('CONFIRM_OVERWRITE'); } @@ -151,7 +150,7 @@ class ClassProgressMutationService throw new \InvalidArgumentException('Please select at least one Islamic Studies unit.'); } - $originalWeekStart = $anchor->week_start instanceof CarbonInterface + $originalWeekStart = $anchor->week_start instanceof \Carbon\CarbonInterface ? $anchor->week_start->format('Y-m-d') : (string) $anchor->week_start; @@ -296,7 +295,7 @@ class ClassProgressMutationService return DB::transaction(function () use ($report, $payload, $attachments) { $weekStart = $payload['week_start'] ?? $report->week_start?->format('Y-m-d'); $weekEnd = $this->rules->ensureWeekEnd((string) $weekStart, $payload['week_end'] ?? null); - if ($weekStart && $weekEnd && ! $this->rules->isWeekEndValid((string) $weekStart, (string) $weekEnd)) { + if ($weekStart && $weekEnd && !$this->rules->isWeekEndValid((string) $weekStart, (string) $weekEnd)) { throw new \InvalidArgumentException('Week end must be the same as or after the week start.'); } @@ -365,7 +364,7 @@ class ClassProgressMutationService ->where('class_section_id', $classSectionId) ->exists(); - if (! $assigned) { + if (!$assigned) { throw new \InvalidArgumentException('No class assignment found for this report.'); } } diff --git a/app/Services/ClassProgress/ClassProgressQueryService.php b/app/Services/ClassProgress/ClassProgressQueryService.php index 24c9ded0..c3d4aa50 100644 --- a/app/Services/ClassProgress/ClassProgressQueryService.php +++ b/app/Services/ClassProgress/ClassProgressQueryService.php @@ -8,8 +8,8 @@ use App\Models\Configuration; use App\Models\TeacherClass; use App\Models\User; use App\Services\SchoolYears\SchoolYearContextService; -use Illuminate\Contracts\Pagination\LengthAwarePaginator; use Illuminate\Support\Facades\Schema; +use Illuminate\Contracts\Pagination\LengthAwarePaginator; class ClassProgressQueryService { @@ -28,7 +28,7 @@ class ClassProgressQueryService ->with(['classSection', 'teacher']) ->orderBy($filters['sort_by'] ?? 'week_start', $filters['sort_dir'] ?? 'desc'); - if (! $this->isAdmin($user)) { + if (!$this->isAdmin($user)) { $relaxedSectionIds = TeacherClass::distinctSectionIdsForTeacher((int) $user->id); if (! empty($filters['class_section_id'])) { @@ -50,31 +50,31 @@ class ClassProgressQueryService $query->where('teacher_id', (int) $filters['teacher_id']); } - if (! empty($filters['class_section_id'])) { + if (!empty($filters['class_section_id'])) { $query->where('class_section_id', (int) $filters['class_section_id']); } - if (! empty($filters['subject'])) { + if (!empty($filters['subject'])) { $query->where('subject', (string) $filters['subject']); } - if (! empty($filters['status'])) { + if (!empty($filters['status'])) { $query->where('status', (string) $filters['status']); } - if (! empty($filters['school_year']) && Schema::hasColumn('class_progress_reports', 'school_year')) { + if (!empty($filters['school_year']) && Schema::hasColumn('class_progress_reports', 'school_year')) { $query->where('school_year', (string) $filters['school_year']); } - if (! empty($filters['semester']) && Schema::hasColumn('class_progress_reports', 'semester')) { + if (!empty($filters['semester']) && Schema::hasColumn('class_progress_reports', 'semester')) { $query->where('semester', (string) $filters['semester']); } - if (! empty($filters['week_start'])) { + if (!empty($filters['week_start'])) { $query->whereDate('week_start', '>=', $filters['week_start']); } - if (! empty($filters['week_end'])) { + if (!empty($filters['week_end'])) { $query->whereDate('week_end', '<=', $filters['week_end']); } @@ -114,7 +114,6 @@ class ClassProgressQueryService return $rows->map(function (ClassProgressReport $row) use ($attachments) { $row->setAttribute('status_label', $this->statusLabel($row->status)); $row->setAttribute('attachments', $attachments[$row->id] ?? []); - return $row; })->all(); } @@ -160,7 +159,6 @@ class ClassProgressQueryService private function statusLabel(?string $status): string { $options = (array) config('progress.status_options', []); - return $options[$status] ?? 'Unknown'; } diff --git a/app/Services/ClassProgress/ClassProgressRuleService.php b/app/Services/ClassProgress/ClassProgressRuleService.php index 3ca3c9ad..99e9c154 100644 --- a/app/Services/ClassProgress/ClassProgressRuleService.php +++ b/app/Services/ClassProgress/ClassProgressRuleService.php @@ -15,7 +15,6 @@ class ClassProgressRuleService public function normalizeFlags($flags): ?array { $values = array_values(array_filter((array) $flags, static fn ($item) => $item !== '' && $item !== null)); - return $values === [] ? null : $values; } @@ -81,7 +80,6 @@ class ClassProgressRuleService try { $dt = new \DateTime($weekStart); $dt->modify('+6 days'); - return $dt->format('Y-m-d'); } catch (\Exception $e) { return $weekStart; diff --git a/app/Services/ClassSections/ClassAttendanceService.php b/app/Services/ClassSections/ClassAttendanceService.php index 3b09af7b..46e28922 100644 --- a/app/Services/ClassSections/ClassAttendanceService.php +++ b/app/Services/ClassSections/ClassAttendanceService.php @@ -15,7 +15,7 @@ class ClassAttendanceService $semester = $semester ?: (string) (Configuration::getConfig('semester') ?? ''); $teacher = User::query()->find($teacherId); - $teacherName = trim((string) ($teacher?->firstname ?? '').' '.(string) ($teacher?->lastname ?? '')); + $teacherName = trim((string) ($teacher?->firstname ?? '') . ' ' . (string) ($teacher?->lastname ?? '')); $className = ClassSection::getClassSectionNameBySectionId($classSectionId) ?? (string) $classSectionId; $students = Student::getStudentInfoByClassSectionId($classSectionId, $semester, $schoolYear, $teacherId); diff --git a/app/Services/ClassSections/ClassSectionQueryService.php b/app/Services/ClassSections/ClassSectionQueryService.php index 52e43933..1fc9e901 100644 --- a/app/Services/ClassSections/ClassSectionQueryService.php +++ b/app/Services/ClassSections/ClassSectionQueryService.php @@ -13,27 +13,27 @@ class ClassSectionQueryService ->leftJoin('classes', 'classSection.class_id', '=', 'classes.id') ->select('classSection.*', 'classes.class_name'); - if (! empty($filters['search'])) { - $term = '%'.strtolower((string) $filters['search']).'%'; + if (!empty($filters['search'])) { + $term = '%' . strtolower((string) $filters['search']) . '%'; $query->where(function ($q) use ($term) { $q->whereRaw('LOWER(classSection.class_section_name) LIKE ?', [$term]) ->orWhereRaw('LOWER(classes.class_name) LIKE ?', [$term]); }); } - if (! empty($filters['class_id'])) { + if (!empty($filters['class_id'])) { $query->where('classSection.class_id', (int) $filters['class_id']); } - if (! empty($filters['school_year'])) { + if (!empty($filters['school_year'])) { $query->where('classSection.school_year', (string) $filters['school_year']); } - if (! empty($filters['semester'])) { + if (!empty($filters['semester'])) { $query->where('classSection.semester', (string) $filters['semester']); } - if (! empty($filters['with_students'])) { + if (!empty($filters['with_students'])) { $query->whereExists(function ($sub) use ($filters) { $sub->selectRaw('1') ->from('student_class as sc') @@ -41,7 +41,7 @@ class ClassSectionQueryService ->whereColumn('sc.class_section_id', 'classSection.class_section_id') ->where('s.is_active', 1); - if (! empty($filters['school_year'])) { + if (!empty($filters['school_year'])) { $sub->where('sc.school_year', (string) $filters['school_year']); } }); diff --git a/app/Services/Communication/CommunicationFamilyService.php b/app/Services/Communication/CommunicationFamilyService.php index 18bec187..af849bb0 100644 --- a/app/Services/Communication/CommunicationFamilyService.php +++ b/app/Services/Communication/CommunicationFamilyService.php @@ -51,7 +51,7 @@ class CommunicationFamilyService } $names = array_map(static function ($row) { - return trim(($row['firstname'] ?? '').' '.($row['lastname'] ?? '')); + return trim(($row['firstname'] ?? '') . ' ' . ($row['lastname'] ?? '')); }, $rows); return implode(' & ', $names); diff --git a/app/Services/Communication/CommunicationLogService.php b/app/Services/Communication/CommunicationLogService.php index 4ff6f3a3..f6bca614 100644 --- a/app/Services/Communication/CommunicationLogService.php +++ b/app/Services/Communication/CommunicationLogService.php @@ -9,7 +9,7 @@ class CommunicationLogService { public function log(array $payload): void { - if (! Schema::hasTable('communication_logs')) { + if (!Schema::hasTable('communication_logs')) { return; } diff --git a/app/Services/Communication/CommunicationPreviewService.php b/app/Services/Communication/CommunicationPreviewService.php index f6838f58..0539b745 100644 --- a/app/Services/Communication/CommunicationPreviewService.php +++ b/app/Services/Communication/CommunicationPreviewService.php @@ -5,9 +5,7 @@ namespace App\Services\Communication; class CommunicationPreviewService { private CommunicationTemplateService $templates; - private CommunicationStudentService $students; - private CommunicationFamilyService $families; public function __construct( @@ -28,7 +26,7 @@ class CommunicationPreviewService string $teacherName ): array { $template = $this->templates->findTemplateByKey($templateKey); - if (! $template) { + if (!$template) { return [ 'ok' => false, 'status' => 404, @@ -37,7 +35,7 @@ class CommunicationPreviewService } $student = $this->students->getStudentBasic($studentId); - if (! $student) { + if (!$student) { return [ 'ok' => false, 'status' => 404, @@ -48,7 +46,7 @@ class CommunicationPreviewService $salutation = $this->families->guardianSalutation($familyId); $autoVars = [ - 'student_fullname' => trim(($student['firstname'] ?? '').' '.($student['lastname'] ?? '')), + 'student_fullname' => trim(($student['firstname'] ?? '') . ' ' . ($student['lastname'] ?? '')), 'student_grade' => $student['grade'] ?? '', 'parent_salutation' => $salutation, 'date' => $this->formatLocalDate('Y-m-d'), @@ -72,7 +70,6 @@ class CommunicationPreviewService { return (string) preg_replace_callback('/\\{\\{\\s*([a-zA-Z0-9_\\.]+)\\s*\\}\\}/', function ($m) use ($vars) { $key = $m[1]; - return htmlspecialchars((string) ($vars[$key] ?? ''), ENT_QUOTES, 'UTF-8'); }, $template); } @@ -82,7 +79,6 @@ class CommunicationPreviewService if (function_exists('local_date') && function_exists('utc_now')) { return (string) local_date(utc_now(), $format); } - return now()->format($format); } } diff --git a/app/Services/Communication/CommunicationSendService.php b/app/Services/Communication/CommunicationSendService.php index 9041e053..c045a9a8 100644 --- a/app/Services/Communication/CommunicationSendService.php +++ b/app/Services/Communication/CommunicationSendService.php @@ -7,7 +7,6 @@ use App\Services\EmailService; class CommunicationSendService { private EmailService $mailer; - private CommunicationLogService $logService; public function __construct(EmailService $mailer, CommunicationLogService $logService) @@ -62,7 +61,7 @@ class CommunicationSendService $sendOk = false; } - if (! $sendOk && $error === null) { + if (!$sendOk && $error === null) { $error = 'Mailer returned false'; } @@ -91,7 +90,6 @@ class CommunicationSendService private function normalizeEmails(array $values): array { $out = array_values(array_unique(array_filter(array_map('strval', $values)))); - return array_values(array_filter($out, static function ($email) { return $email !== ''; })); diff --git a/app/Services/Communication/CommunicationStudentService.php b/app/Services/Communication/CommunicationStudentService.php index 164d8e6e..00c59790 100644 --- a/app/Services/Communication/CommunicationStudentService.php +++ b/app/Services/Communication/CommunicationStudentService.php @@ -33,13 +33,13 @@ class CommunicationStudentService ->orderBy('sc.id', 'desc') ->first(); - if (! $row) { + if (!$row) { return null; } $row = (array) $row; $grade = $row['registration_grade'] ?? ''; - if (! $grade && ! empty($row['class_section_name'])) { + if (!$grade && !empty($row['class_section_name'])) { $grade = $row['class_section_name']; } diff --git a/app/Services/Communication/CommunicationTemplateService.php b/app/Services/Communication/CommunicationTemplateService.php index 7f1663f8..372c8da6 100644 --- a/app/Services/Communication/CommunicationTemplateService.php +++ b/app/Services/Communication/CommunicationTemplateService.php @@ -17,7 +17,6 @@ class CommunicationTemplateService ->get() ->map(function ($row) use ($keyColumn, $bodyColumn) { $row = (array) $row; - return [ 'template_key' => (string) ($row[$keyColumn] ?? ''), 'subject' => (string) ($row['subject'] ?? ''), @@ -36,7 +35,7 @@ class CommunicationTemplateService ->where('is_active', 1) ->first(); - if (! $row) { + if (!$row) { return null; } @@ -53,7 +52,6 @@ class CommunicationTemplateService { $keyColumn = Schema::hasColumn('email_templates', 'template_key') ? 'template_key' : 'code'; $bodyColumn = Schema::hasColumn('email_templates', 'body') ? 'body' : 'body_html'; - return [$keyColumn, $bodyColumn]; } } diff --git a/app/Services/CompetitionScores/CompetitionScoresQueryService.php b/app/Services/CompetitionScores/CompetitionScoresQueryService.php index 6cc18268..e59d0adf 100644 --- a/app/Services/CompetitionScores/CompetitionScoresQueryService.php +++ b/app/Services/CompetitionScores/CompetitionScoresQueryService.php @@ -42,7 +42,7 @@ class CompetitionScoresQueryService public function getQuestionCounts(array $competitionIds, int $classSectionId): array { - if (! $this->hasQuestionCount() || empty($competitionIds)) { + if (!$this->hasQuestionCount() || empty($competitionIds)) { return []; } @@ -93,7 +93,7 @@ class CompetitionScoresQueryService public function getQuestionCountForCompetition(int $competitionId, int $classSectionId): ?int { - if (! $this->hasQuestionCount()) { + if (!$this->hasQuestionCount()) { return null; } diff --git a/app/Services/CompetitionScores/CompetitionScoresRosterService.php b/app/Services/CompetitionScores/CompetitionScoresRosterService.php index 4c849c48..06727500 100644 --- a/app/Services/CompetitionScores/CompetitionScoresRosterService.php +++ b/app/Services/CompetitionScores/CompetitionScoresRosterService.php @@ -35,7 +35,6 @@ class CompetitionScoresRosterService } $row = (array) $builder->first(); - return (int) ($row['total'] ?? 0); } @@ -47,7 +46,7 @@ class CompetitionScoresRosterService $builder = DB::table('students as s') ->select('s.id', 's.school_id', 's.firstname', 's.lastname') - ->join($this->classStudentTable.' as cs', 'cs.student_id', '=', 's.id') + ->join($this->classStudentTable . ' as cs', 'cs.student_id', '=', 's.id') ->where('cs.class_section_id', $classSectionId); if (Schema::hasColumn($this->classStudentTable, 'school_year')) { diff --git a/app/Services/CompetitionScores/CompetitionScoresSaveService.php b/app/Services/CompetitionScores/CompetitionScoresSaveService.php index f27ed494..5d0bf68a 100644 --- a/app/Services/CompetitionScores/CompetitionScoresSaveService.php +++ b/app/Services/CompetitionScores/CompetitionScoresSaveService.php @@ -16,9 +16,8 @@ class CompetitionScoresSaveService if ($scoreValue === '') { continue; } - if (! preg_match('/^\\d+$/', $scoreValue)) { + if (!preg_match('/^\\d+$/', $scoreValue)) { $invalidScores[] = (int) $studentId; - continue; } $cleanScores[(int) $studentId] = (int) $scoreValue; diff --git a/app/Services/Dashboard/DashboardRouteService.php b/app/Services/Dashboard/DashboardRouteService.php index beba360c..e5074d16 100644 --- a/app/Services/Dashboard/DashboardRouteService.php +++ b/app/Services/Dashboard/DashboardRouteService.php @@ -12,9 +12,8 @@ class DashboardRouteService public function resolveForUser(?User $user): array { - if (! $user) { + if (!$user) { Log::warning('Dashboard route requested without an authenticated user.'); - return [ 'route' => self::FALLBACK_ROUTE, 'role' => null, @@ -49,7 +48,7 @@ class DashboardRouteService $rows = Role::findByNamesOrSlugs($lookupRoles); - if (! empty($rows)) { + if (!empty($rows)) { $role = $rows[0]; $route = $role->dashboard_route ?: self::FALLBACK_ROUTE; diff --git a/app/Services/Discounts/DiscountApplyService.php b/app/Services/Discounts/DiscountApplyService.php index 3917d6e9..08f7d9dd 100644 --- a/app/Services/Discounts/DiscountApplyService.php +++ b/app/Services/Discounts/DiscountApplyService.php @@ -10,7 +10,6 @@ use Illuminate\Support\Facades\Log; class DiscountApplyService { private DiscountContextService $context; - private DiscountInvoiceService $invoiceService; public function __construct(DiscountContextService $context, DiscountInvoiceService $invoiceService) @@ -25,7 +24,7 @@ class DiscountApplyService $semester = $this->context->getSemester(); $voucher = DiscountVoucher::query()->find($voucherId); - if (! $voucher) { + if (!$voucher) { return ['ok' => false, 'message' => 'Voucher not found.']; } @@ -44,7 +43,7 @@ class DiscountApplyService return ['ok' => false, 'message' => 'This voucher has reached its maximum allowed uses.']; } - if (! $allowAdditional) { + if (!$allowAdditional) { $rows = DB::table('discount_usages as du') ->select('i.parent_id') ->join('invoices as i', 'i.id', '=', 'du.invoice_id') @@ -56,7 +55,7 @@ class DiscountApplyService ->all(); $blockedParentIds = array_map(static fn ($r) => (int) ($r['parent_id'] ?? 0), $rows); - if (! empty($blockedParentIds)) { + if (!empty($blockedParentIds)) { $parentIds = array_values(array_diff(array_map('intval', $parentIds), $blockedParentIds)); } if (empty($parentIds)) { @@ -82,7 +81,6 @@ class DiscountApplyService 'parent_id' => (int) $parentId, 'voucher_id' => $voucherId, ]); - continue; } @@ -101,7 +99,7 @@ class DiscountApplyService continue; } - if (! $allowAdditional) { + if (!$allowAdditional) { $exists = DB::table('discount_usages as du') ->join('invoices as i', 'du.invoice_id', '=', 'i.id') ->where('du.voucher_id', $voucherId) @@ -199,7 +197,6 @@ class DiscountApplyService DB::commit(); } catch (\Throwable $e) { DB::rollBack(); - return ['ok' => false, 'message' => 'Voucher application failed. Transaction rolled back.']; } @@ -289,7 +286,7 @@ class DiscountApplyService event('paymentReceived', [$eventData, $studentData]); } } catch (\Throwable $e) { - Log::warning('paymentReceived hook failed: '.$e->getMessage()); + Log::warning('paymentReceived hook failed: ' . $e->getMessage()); } } } diff --git a/app/Services/Discounts/DiscountInvoiceService.php b/app/Services/Discounts/DiscountInvoiceService.php index d5c42a85..12bec3d5 100644 --- a/app/Services/Discounts/DiscountInvoiceService.php +++ b/app/Services/Discounts/DiscountInvoiceService.php @@ -17,12 +17,13 @@ class DiscountInvoiceService { public function __construct( private ApplicationUrlService $urls, - ) {} + ) { + } public function getCurrentInvoiceBalance(int $invoiceId, string $schoolYear): float { $invoice = Invoice::query()->find($invoiceId); - if (! $invoice) { + if (!$invoice) { return 0.0; } @@ -65,14 +66,13 @@ class DiscountInvoiceService $totalRefundPaid = (float) ($rowRefund['total_refund_paid'] ?? 0); $total = (float) ($invoice->total_amount ?? 0); - return max(0.0, round($total - $totalPaid - $totalDisc - $totalRefundPaid, 2)); } public function recalculateInvoice(int $invoiceId, string $schoolYear): void { $invoice = Invoice::query()->find($invoiceId); - if (! $invoice) { + if (!$invoice) { return; } @@ -118,7 +118,7 @@ class DiscountInvoiceService } $tuitionStudents = $registered; - if (! $refundAllowed) { + if (!$refundAllowed) { $tuitionStudents = array_merge($tuitionStudents, $withdrawn); } @@ -129,7 +129,7 @@ class DiscountInvoiceService foreach ($tuitionStudents as &$s) { $name = null; - if (! empty($s['class_section_id'])) { + if (!empty($s['class_section_id'])) { $name = ClassSection::getClassSectionNameBySectionId($s['class_section_id']); } $s['grade_name'] = is_string($name) ? strtoupper(trim($name)) : 'N/A'; @@ -236,13 +236,13 @@ class DiscountInvoiceService public function updateEnrollmentStatusIfPaid(int $invoiceId, string $schoolYear): int { $invoice = Invoice::query()->find($invoiceId); - if (! $invoice) { + if (!$invoice) { return 0; } $total = (float) ($invoice->total_amount ?? 0); $balance = (float) ($invoice->balance ?? 0); - if (! ($total > 0 && $balance < $total)) { + if (!($total > 0 && $balance < $total)) { return 0; } @@ -281,7 +281,7 @@ class DiscountInvoiceService ->orWhereRaw("LOWER(TRIM(REPLACE(enrollment_status, CHAR(160), ' '))) = 'payment pending'"); }); - if (! empty($paidEnrollmentIds)) { + if (!empty($paidEnrollmentIds)) { $query->whereIn('id', array_values(array_unique($paidEnrollmentIds))); } @@ -314,7 +314,7 @@ class DiscountInvoiceService string $semester ): array { $invoice = Invoice::query()->find($invoiceId); - if (! $invoice) { + if (!$invoice) { throw new \RuntimeException("Invoice {$invoiceId} not found"); } @@ -380,7 +380,6 @@ class DiscountInvoiceService if (preg_match('/^Y(?:OUTH)?\\s*(\\d+)?$/', $g, $m)) { $n = isset($m[1]) && $m[1] !== '' ? max(1, (int) $m[1]) : 1; - return $gradeFee + $n; } @@ -396,7 +395,6 @@ class DiscountInvoiceService if (function_exists('local_date') && function_exists('utc_now')) { return (string) local_date(utc_now(), $format); } - return now()->format($format); } } diff --git a/app/Services/Discounts/DiscountVoucherService.php b/app/Services/Discounts/DiscountVoucherService.php index d8f02426..068919f3 100644 --- a/app/Services/Discounts/DiscountVoucherService.php +++ b/app/Services/Discounts/DiscountVoucherService.php @@ -33,7 +33,6 @@ class DiscountVoucherService $voucher = DiscountVoucher::query()->findOrFail($id); $voucher->fill($payload); $voucher->save(); - return $voucher; } diff --git a/app/Services/Docs/ApiDocsService.php b/app/Services/Docs/ApiDocsService.php index 6055fa5c..425b8130 100644 --- a/app/Services/Docs/ApiDocsService.php +++ b/app/Services/Docs/ApiDocsService.php @@ -9,7 +9,8 @@ class ApiDocsService { public function __construct( private ApplicationUrlService $urls, - ) {} + ) { + } /** * Bootstrap payload for the documentation SPA (replaces legacy swagger_ui / swagger_ui_public views). diff --git a/app/Services/Docs/DocsCatalogService.php b/app/Services/Docs/DocsCatalogService.php index 17c1bcba..adc9265e 100644 --- a/app/Services/Docs/DocsCatalogService.php +++ b/app/Services/Docs/DocsCatalogService.php @@ -2,18 +2,18 @@ namespace App\Services\Docs; -use App\Controllers\DocsController; use App\Services\ApplicationUrlService; /** - * legacy parity: {@see DocsController::index} and + * legacy parity: {@see \App\Controllers\DocsController::index} and * {@see \App\Controllers\View\DocsController::swagger} (multi-spec Swagger UI payload). */ class DocsCatalogService { public function __construct( private ApplicationUrlService $urls, - ) {} + ) { + } /** * Hub metadata for the docs landing route (replaces Blade `docs/index`). diff --git a/app/Services/Docs/OpenApiRouteExporter.php b/app/Services/Docs/OpenApiRouteExporter.php index 3d2a0fba..82dec7ed 100644 --- a/app/Services/Docs/OpenApiRouteExporter.php +++ b/app/Services/Docs/OpenApiRouteExporter.php @@ -29,22 +29,22 @@ class OpenApiRouteExporter private function mergeRoute(array &$paths, Route $route): void { - if (! $this->isDocumentableRoute($route)) { + if (!$this->isDocumentableRoute($route)) { return; } $uriRaw = ltrim($route->uri(), '/'); if (str_starts_with($uriRaw, 'api/')) { - $uri = '/'.$uriRaw; + $uri = '/' . $uriRaw; } elseif (str_starts_with($uriRaw, 'v1/')) { - $uri = '/api/'.$uriRaw; + $uri = '/api/' . $uriRaw; } elseif ($uriRaw === 'v1') { $uri = '/api/v1'; } else { return; } - if (! str_starts_with($uri, '/api/v1')) { + if (!str_starts_with($uri, '/api/v1')) { return; } @@ -56,7 +56,7 @@ class OpenApiRouteExporter $tag = $this->resolveTag($uri); foreach ($methods as $method) { $lower = strtolower($method); - if (! isset($paths[$uri][$lower])) { + if (!isset($paths[$uri][$lower])) { $paths[$uri][$lower] = [ 'tags' => [$tag], 'summary' => $this->summaryFromAction($route), @@ -73,12 +73,12 @@ class OpenApiRouteExporter private function isDocumentableRoute(Route $route): bool { $uri = ltrim($route->uri(), '/'); - if (! str_starts_with($uri, 'api/v1') && ! str_starts_with($uri, 'v1/')) { + if (!str_starts_with($uri, 'api/v1') && !str_starts_with($uri, 'v1/')) { return false; } $action = $route->getActionName(); - if (! is_string($action)) { + if (!is_string($action)) { return false; } @@ -120,13 +120,11 @@ class OpenApiRouteExporter if (is_string($action) && str_contains($action, '@')) { [$controller, $method] = explode('@', $action, 2); $base = class_basename($controller); - - return $base.'::'.$method; + return $base . '::' . $method; } if (is_string($action)) { return $action; } - return 'Endpoint'; } } diff --git a/app/Services/Email/EmailAttachmentService.php b/app/Services/Email/EmailAttachmentService.php index a330d6da..7ba2c497 100644 --- a/app/Services/Email/EmailAttachmentService.php +++ b/app/Services/Email/EmailAttachmentService.php @@ -11,17 +11,17 @@ class EmailAttachmentService if ($files instanceof UploadedFile) { $files = [$files]; } - if (! is_array($files)) { + if (!is_array($files)) { return []; } $out = []; foreach ($files as $file) { - if (! $file instanceof UploadedFile || ! $file->isValid()) { + if (!$file instanceof UploadedFile || !$file->isValid()) { continue; } $path = $file->getRealPath(); - if (! $path || ! is_file($path)) { + if (!$path || !is_file($path)) { continue; } $out[] = [ diff --git a/app/Services/Email/EmailDispatchService.php b/app/Services/Email/EmailDispatchService.php index b65eb143..1a0b55b4 100644 --- a/app/Services/Email/EmailDispatchService.php +++ b/app/Services/Email/EmailDispatchService.php @@ -7,7 +7,9 @@ use PHPMailer\PHPMailer\PHPMailer; class EmailDispatchService { - public function __construct(private EmailProfileService $profiles) {} + public function __construct(private EmailProfileService $profiles) + { + } public function send( string $recipient, @@ -23,7 +25,6 @@ class EmailDispatchService if (empty($cfg['host']) || empty($cfg['user']) || $cfg['pass'] === '') { logger()->error("[mail:$profile] Missing SMTP config (host/user/pass). Check MAIL_{$this->profiles->envKeyFromProfile($profile)}_* or MAIL_DEFAULT_*/MAIL_*."); - return false; } @@ -60,7 +61,7 @@ class EmailDispatchService 'ssl' => [ 'verify_peer' => $verifyPeer, 'verify_peer_name' => $verifyPeer, - 'allow_self_signed' => ! $verifyPeer, + 'allow_self_signed' => !$verifyPeer, ], ]; @@ -68,17 +69,17 @@ class EmailDispatchService $mail->Debugoutput = 'error_log'; $mail->setFrom($cfg['fromEmail'], $cfg['fromName']); - if (! empty($cfg['returnPath'])) { + if (!empty($cfg['returnPath'])) { $mail->Sender = $cfg['returnPath']; } $mail->clearReplyTos(); $rtEmail = env('MAIL_DEFAULT_REPLY_TO'); $rtName = env('MAIL_DEFAULT_REPLY_TO_NAME'); - if (! $rtEmail || ! filter_var($rtEmail, FILTER_VALIDATE_EMAIL)) { + if (!$rtEmail || !filter_var($rtEmail, FILTER_VALIDATE_EMAIL)) { $rtEmail = $replyToEmail ?: ($cfg['replyTo'] ?: $cfg['fromEmail']); } - if (! $rtName) { + if (!$rtName) { $rtName = $replyToName ?: ($cfg['replyToName'] ?: $cfg['fromName']); } $rtName = $this->profiles->sanitizeReplyToName($rtName, $cfg['fromName']); @@ -86,7 +87,7 @@ class EmailDispatchService $mail->addReplyTo($rtEmail, $rtName); } - if (! empty($cfg['dkim']['domain']) && ! empty($cfg['dkim']['private']) && ! empty($cfg['dkim']['selector'])) { + if (!empty($cfg['dkim']['domain']) && !empty($cfg['dkim']['private']) && !empty($cfg['dkim']['selector'])) { $mail->DKIM_domain = $cfg['dkim']['domain']; $mail->DKIM_private = $cfg['dkim']['private']; $mail->DKIM_selector = $cfg['dkim']['selector']; @@ -96,7 +97,7 @@ class EmailDispatchService $mail->addAddress($recipient); foreach ($attachments as $att) { - if (! empty($att['path']) && is_file($att['path'])) { + if (!empty($att['path']) && is_file($att['path'])) { $mail->addAttachment($att['path'], $att['name'] ?? ''); } } @@ -110,17 +111,14 @@ class EmailDispatchService if ($ok) { logger()->info("[mail:$profile] Sent to {$recipient}, subj='{$subject}' via {$cfg['host']}:{$cfg['port']}/{$cfg['encryption']}"); - return true; } logger()->error("[mail:$profile] Failed: {$mail->ErrorInfo}. Debug: {$dbg}"); - return false; } catch (MailerException $e) { $dbg = $debugEnabled ? (ob_get_clean() ?: '') : ''; logger()->error("[mail:$profile] Exception: {$e->getMessage()} | PHPMailer: {$mail->ErrorInfo} | Debug: {$dbg}"); - return false; } } diff --git a/app/Services/Email/EmailExtractorService.php b/app/Services/Email/EmailExtractorService.php index 39924558..9219605e 100644 --- a/app/Services/Email/EmailExtractorService.php +++ b/app/Services/Email/EmailExtractorService.php @@ -57,7 +57,7 @@ class EmailExtractorService $needToAdd = []; foreach ($dbSet as $email => $_) { - if (! isset($csvSet[$email])) { + if (!isset($csvSet[$email])) { $needToAdd[] = $email; } } @@ -84,7 +84,6 @@ class EmailExtractorService $out[] = $email; } } - return array_values(array_unique($out)); } @@ -93,7 +92,6 @@ class EmailExtractorService $pattern = '/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/i'; preg_match_all($pattern, $text, $matches); $emails = $matches[0] ?? []; - return $this->normalizeEmailArray($emails); } } diff --git a/app/Services/Email/EmailProfileService.php b/app/Services/Email/EmailProfileService.php index fa6638ee..14b76eab 100644 --- a/app/Services/Email/EmailProfileService.php +++ b/app/Services/Email/EmailProfileService.php @@ -37,7 +37,6 @@ class EmailProfileService return $v; } } - return $default; }; @@ -91,7 +90,6 @@ class EmailProfileService if ($trimmed === '' || preg_match('/^no[- ]?repl(?:y|ay)$/i', $trimmed)) { return $fallback; } - return $trimmed; } } diff --git a/app/Services/Email/EmailSenderOptionsService.php b/app/Services/Email/EmailSenderOptionsService.php index 3f1a0324..de6e4990 100644 --- a/app/Services/Email/EmailSenderOptionsService.php +++ b/app/Services/Email/EmailSenderOptionsService.php @@ -9,7 +9,7 @@ class EmailSenderOptionsService $json = env('MAIL_SENDERS', '{}'); $arr = json_decode($json, true); - if (! is_array($arr) || empty($arr)) { + if (!is_array($arr) || empty($arr)) { $smtpUser = getenv('SMTP_USER') ?: ''; $name = 'Al Rahma Sunday School'; @@ -17,7 +17,7 @@ class EmailSenderOptionsService 'key' => 'general', 'name' => $name, 'email' => $smtpUser, - 'label' => trim($name.($smtpUser ? " <{$smtpUser}>" : '')), + 'label' => trim($name . ($smtpUser ? " <{$smtpUser}>" : '')), ]]; } @@ -29,7 +29,7 @@ class EmailSenderOptionsService 'key' => (string) $key, 'name' => (string) $name, 'email' => (string) $email, - 'label' => trim($name.($email ? " <{$email}>" : '')), + 'label' => trim($name . ($email ? " <{$email}>" : '')), ]; } diff --git a/app/Services/EmailService.php b/app/Services/EmailService.php index 81cbd8a2..6acf3238 100644 --- a/app/Services/EmailService.php +++ b/app/Services/EmailService.php @@ -14,7 +14,8 @@ class EmailService string $fromKey = 'general', array $cc = [], array $bcc = [] - ): bool { + ): bool + { $sender = $this->resolveSender($fromKey); try { @@ -23,18 +24,16 @@ class EmailService if ($sender['email'] !== '') { $message->from($sender['email'], $sender['name']); } - if (! empty($cc)) { + if (!empty($cc)) { $message->cc($cc); } - if (! empty($bcc)) { + if (!empty($bcc)) { $message->bcc($bcc); } }); - return true; } catch (\Throwable $e) { - Log::error('EmailService send failed: '.$e->getMessage()); - + Log::error('EmailService send failed: ' . $e->getMessage()); return false; } } @@ -47,7 +46,6 @@ class EmailService if (is_array($senders) && isset($senders[$fromKey])) { $info = $senders[$fromKey]; - return [ 'name' => (string) ($info['name'] ?? 'Sender'), 'email' => (string) ($info['email'] ?? ''), diff --git a/app/Services/EmergencyContacts/EmergencyContactCrudService.php b/app/Services/EmergencyContacts/EmergencyContactCrudService.php index 2a40c62e..044749e6 100644 --- a/app/Services/EmergencyContacts/EmergencyContactCrudService.php +++ b/app/Services/EmergencyContacts/EmergencyContactCrudService.php @@ -9,7 +9,8 @@ class EmergencyContactCrudService { public function __construct( private PhoneFormatterService $phoneFormatter - ) {} + ) { + } public function findForParent(int $contactId, int $parentId): array { diff --git a/app/Services/EmergencyContacts/EmergencyContactDirectoryService.php b/app/Services/EmergencyContacts/EmergencyContactDirectoryService.php index 666f0012..89d8c706 100644 --- a/app/Services/EmergencyContacts/EmergencyContactDirectoryService.php +++ b/app/Services/EmergencyContacts/EmergencyContactDirectoryService.php @@ -13,7 +13,7 @@ class EmergencyContactDirectoryService { public function groups(?array $parentIds = null): array { - $parentIds = ! empty($parentIds) + $parentIds = !empty($parentIds) ? array_values(array_filter(array_map('intval', $parentIds), static fn ($id) => $id > 0)) : EmergencyContact::query() ->distinct() @@ -28,7 +28,7 @@ class EmergencyContactDirectoryService $parent = User::query()->find($parentId); $parentName = $parent - ? trim((string) $parent->firstname.' '.(string) $parent->lastname) + ? trim((string) $parent->firstname . ' ' . (string) $parent->lastname) : 'Unknown Parent'; if ($parentName === '') { $parentName = 'Unknown Parent'; @@ -67,11 +67,11 @@ class EmergencyContactDirectoryService private function secondParentPhone(int $parentId): string { try { - if (! Schema::hasTable('parents')) { + if (!Schema::hasTable('parents')) { return ''; } - if (! Schema::hasColumn('parents', 'secondparent_phone')) { + if (!Schema::hasColumn('parents', 'secondparent_phone')) { return ''; } @@ -82,8 +82,7 @@ class EmergencyContactDirectoryService return $phone ? (string) $phone : ''; } catch (\Throwable $e) { - Log::debug('EmergencyContactDirectoryService: could not load second parent phone: '.$e->getMessage()); - + Log::debug('EmergencyContactDirectoryService: could not load second parent phone: ' . $e->getMessage()); return ''; } } diff --git a/app/Services/Events/EventChargeQueryService.php b/app/Services/Events/EventChargeQueryService.php index 1a973208..754d6854 100644 --- a/app/Services/Events/EventChargeQueryService.php +++ b/app/Services/Events/EventChargeQueryService.php @@ -4,6 +4,7 @@ namespace App\Services\Events; use App\Models\EventCharges; use Illuminate\Contracts\Pagination\LengthAwarePaginator; +use Illuminate\Support\Facades\DB; class EventChargeQueryService { @@ -25,16 +26,16 @@ class EventChargeQueryService ->leftJoin('students', 'students.id', '=', 'event_charges.student_id') ->leftJoin('events', 'events.id', '=', 'event_charges.event_id'); - if (! empty($filters['school_year'])) { + if (!empty($filters['school_year'])) { $query->where('event_charges.school_year', (string) $filters['school_year']); } - if (! empty($filters['semester'])) { + if (!empty($filters['semester'])) { $query->where('event_charges.semester', (string) $filters['semester']); } - if (! empty($filters['parent_id'])) { + if (!empty($filters['parent_id'])) { $query->where('event_charges.parent_id', (int) $filters['parent_id']); } - if (! empty($filters['event_id'])) { + if (!empty($filters['event_id'])) { $query->where('event_charges.event_id', (int) $filters['event_id']); } diff --git a/app/Services/Events/EventChargeService.php b/app/Services/Events/EventChargeService.php index d9f1370d..cac21166 100644 --- a/app/Services/Events/EventChargeService.php +++ b/app/Services/Events/EventChargeService.php @@ -9,7 +9,9 @@ use App\Services\Invoices\InvoiceGenerationService; class EventChargeService { - public function __construct(private InvoiceGenerationService $invoiceGeneration) {} + public function __construct(private InvoiceGenerationService $invoiceGeneration) + { + } public function seedChargesForEvent(Event $event, string $schoolYear, string $semester, float $amount, int $actorId): array { @@ -80,7 +82,7 @@ class EventChargeService int $actorId ): array { $event = Event::getEvent($eventId, $schoolYear); - if (! $event) { + if (!$event) { return ['ok' => false, 'message' => 'Event not found.']; } @@ -107,7 +109,6 @@ class EventChargeService $existing->delete(); $deleted++; } - continue; } diff --git a/app/Services/Events/EventListService.php b/app/Services/Events/EventListService.php index 996e3590..cd2283c9 100644 --- a/app/Services/Events/EventListService.php +++ b/app/Services/Events/EventListService.php @@ -16,19 +16,19 @@ class EventListService $activeOnly = $filters['active'] ?? null; $allowedSorts = ['created_at', 'expiration_date', 'event_name']; - if (! in_array($sortBy, $allowedSorts, true)) { + if (!in_array($sortBy, $allowedSorts, true)) { $sortBy = 'created_at'; } $query = Event::query(); - if (! empty($filters['school_year'])) { + if (!empty($filters['school_year'])) { $query->where('school_year', (string) $filters['school_year']); } - if (! empty($filters['semester'])) { + if (!empty($filters['semester'])) { $query->where('semester', (string) $filters['semester']); } - if (! empty($filters['q'])) { + if (!empty($filters['q'])) { $term = trim((string) $filters['q']); $query->where('event_name', 'like', "%{$term}%"); } @@ -67,10 +67,10 @@ class EventListService private function activeCount(array $filters): int { $query = Event::query()->whereDate('expiration_date', '>=', now()->toDateString()); - if (! empty($filters['school_year'])) { + if (!empty($filters['school_year'])) { $query->where('school_year', (string) $filters['school_year']); } - if (! empty($filters['semester'])) { + if (!empty($filters['semester'])) { $query->where('semester', (string) $filters['semester']); } diff --git a/app/Services/Events/EventManagementService.php b/app/Services/Events/EventManagementService.php index aa1fcd41..ac327586 100644 --- a/app/Services/Events/EventManagementService.php +++ b/app/Services/Events/EventManagementService.php @@ -13,7 +13,8 @@ class EventManagementService public function __construct( private EventChargeService $charges, private InvoiceGenerationService $invoiceGeneration - ) {} + ) { + } public function create(array $payload, ?UploadedFile $flyer, int $actorId): array { @@ -53,7 +54,7 @@ class EventManagementService public function update(int $eventId, array $payload, ?UploadedFile $flyer): array { $event = Event::query()->find($eventId); - if (! $event) { + if (!$event) { return ['ok' => false, 'message' => 'Event not found.']; } @@ -80,7 +81,7 @@ class EventManagementService public function delete(int $eventId, int $actorId): array { $event = Event::query()->find($eventId); - if (! $event) { + if (!$event) { return ['ok' => false, 'message' => 'Event not found.']; } @@ -98,7 +99,7 @@ class EventManagementService private function storeFlyer(?UploadedFile $file): ?string { - if (! $file || ! $file->isValid()) { + if (!$file || !$file->isValid()) { return null; } @@ -119,13 +120,13 @@ class EventManagementService } $dir = public_path('uploads/event_flyers'); - if (! File::exists($dir)) { + if (!File::exists($dir)) { File::makeDirectory($dir, 0755, true); } - $name = bin2hex(random_bytes(16)).'.'.$allowed[$mime]; + $name = bin2hex(random_bytes(16)) . '.' . $allowed[$mime]; $file->move($dir, $name); - return 'event_flyers/'.$name; + return 'event_flyers/' . $name; } } diff --git a/app/Services/Events/EventStudentChargeService.php b/app/Services/Events/EventStudentChargeService.php index c3d8790d..26a2ca6c 100644 --- a/app/Services/Events/EventStudentChargeService.php +++ b/app/Services/Events/EventStudentChargeService.php @@ -30,7 +30,7 @@ class EventStudentChargeService foreach ($students as $student) { $data[] = [ 'id' => (int) ($student['id'] ?? 0), - 'name' => trim(($student['firstname'] ?? '').' '.($student['lastname'] ?? '')), + 'name' => trim(($student['firstname'] ?? '') . ' ' . ($student['lastname'] ?? '')), 'charged' => isset($chargedMap[(int) ($student['id'] ?? 0)]), ]; } diff --git a/app/Services/Exams/ExamDraftAdminService.php b/app/Services/Exams/ExamDraftAdminService.php index 13dff9ae..9f22c66a 100644 --- a/app/Services/Exams/ExamDraftAdminService.php +++ b/app/Services/Exams/ExamDraftAdminService.php @@ -4,7 +4,6 @@ namespace App\Services\Exams; use App\Models\ClassSection; use App\Models\ExamDraft; -use Illuminate\Http\UploadedFile; use Illuminate\Support\Facades\DB; class ExamDraftAdminService @@ -12,7 +11,8 @@ class ExamDraftAdminService public function __construct( private ExamDraftConfigService $configService, private ExamDraftFileService $fileService - ) {} + ) { + } public function list(): array { @@ -29,7 +29,7 @@ class ExamDraftAdminService if ($teacherUserColumn !== '') { $draftsQuery - ->leftJoin('users as u', 'u.id', '=', "ed.{$teacherUserColumn}") + ->leftJoin('users as u', "u.id", '=', "ed.{$teacherUserColumn}") ->addSelect( 'u.firstname as teacher_first', 'u.lastname as teacher_last' @@ -82,15 +82,15 @@ class ExamDraftAdminService $legacyByClass = []; if ($context['has_legacy'] ?? false) { foreach ($drafts as $row) { - $isLegacy = ! empty($row['is_legacy']); - if (! $isLegacy) { + $isLegacy = !empty($row['is_legacy']); + if (!$isLegacy) { continue; } $cid = (int) ($row['class_section_id'] ?? 0); - if (! isset($legacyByClass[$cid])) { + if (!isset($legacyByClass[$cid])) { $legacyByClass[$cid] = [ 'class_section_id' => $cid, - 'class_section_name' => $row['class_section_name'] ?? 'Class '.$cid, + 'class_section_name' => $row['class_section_name'] ?? 'Class ' . $cid, 'items' => [], ]; } @@ -111,7 +111,7 @@ class ExamDraftAdminService ]; } - public function uploadLegacy(int $adminId, array $payload, UploadedFile $file): array + public function uploadLegacy(int $adminId, array $payload, \Illuminate\Http\UploadedFile $file): array { $context = $this->configService->context(); $adminUserColumn = (string) ($context['admin_user_column'] ?? ''); @@ -148,7 +148,7 @@ class ExamDraftAdminService $finalFilename = $stored['extension'] === 'pdf' ? $stored['original'] - : (pathinfo($stored['original'], PATHINFO_FILENAME).'.pdf'); + : (pathinfo($stored['original'], PATHINFO_FILENAME) . '.pdf'); $payload = [ 'class_section_id' => $classSectionId, @@ -193,7 +193,7 @@ class ExamDraftAdminService return ['ok' => true, 'draft' => $draft->toArray()]; } - public function review(int $adminId, array $payload, ?UploadedFile $file): array + public function review(int $adminId, array $payload, ?\Illuminate\Http\UploadedFile $file): array { $draftId = (int) ($payload['draft_id'] ?? 0); if ($draftId <= 0) { @@ -201,7 +201,7 @@ class ExamDraftAdminService } $draft = ExamDraft::query()->find($draftId); - if (! $draft) { + if (!$draft) { return ['ok' => false, 'message' => 'Submission not found.']; } @@ -209,7 +209,7 @@ class ExamDraftAdminService $statusInput = trim((string) ($payload['review_status'] ?? '')); $currentStatus = (string) ($draft->status ?? 'draft'); $status = $this->normalizeStatus($statusInput !== '' ? $statusInput : $currentStatus, $currentStatus); - if (! in_array($status, ExamDraftConfigService::STATUS_OPTIONS, true)) { + if (!in_array($status, ExamDraftConfigService::STATUS_OPTIONS, true)) { $status = 'reviewed'; } @@ -255,7 +255,7 @@ class ExamDraftAdminService if ($pdfName !== null && ($this->configService->context()['has_final_pdf'] ?? false)) { $update['final_pdf_file'] = $pdfName; } - } elseif ($status === 'finalized' && ! empty($draft->teacher_file ?: $draft->author_file)) { + } elseif ($status === 'finalized' && !empty($draft->teacher_file ?: $draft->author_file)) { $copied = $this->fileService->copyDraftToFinal( (string) ($draft->teacher_file ?? $draft->author_file), ExamDraftConfigService::TEACHER_UPLOAD_DIR, @@ -297,7 +297,6 @@ class ExamDraftAdminService if (isset($aliases[$input])) { $input = $aliases[$input]; } - return in_array($input, ExamDraftConfigService::STATUS_OPTIONS, true) ? $input : $default; } } diff --git a/app/Services/Exams/ExamDraftConfigService.php b/app/Services/Exams/ExamDraftConfigService.php index 3bfcbdf8..b7d0f68e 100644 --- a/app/Services/Exams/ExamDraftConfigService.php +++ b/app/Services/Exams/ExamDraftConfigService.php @@ -8,13 +8,9 @@ use Illuminate\Support\Facades\Schema; class ExamDraftConfigService { public const TEACHER_UPLOAD_DIR = 'exams/drafts'; - public const FINAL_UPLOAD_DIR = 'exams/finals'; - public const MAX_UPLOAD_BYTES = 12 * 1024 * 1024; - public const ALLOWED_EXTENSIONS = ['doc', 'docx']; - public const ADMIN_ALLOWED_EXTENSIONS = ['doc', 'docx', 'pdf']; public const EXAM_TYPES = [ diff --git a/app/Services/Exams/ExamDraftFileService.php b/app/Services/Exams/ExamDraftFileService.php index b880103e..d75a74e9 100644 --- a/app/Services/Exams/ExamDraftFileService.php +++ b/app/Services/Exams/ExamDraftFileService.php @@ -9,22 +9,21 @@ class ExamDraftFileService public function storeUploadedFile(UploadedFile $file, string $subdir, array $allowedExtensions, int $maxBytes): ?array { $ext = strtolower($file->getClientOriginalExtension()); - if (! in_array($ext, $allowedExtensions, true)) { + if (!in_array($ext, $allowedExtensions, true)) { return null; } if ($file->getSize() > $maxBytes) { return null; } - $destination = storage_path('uploads/'.trim($subdir, '/')); - if (! is_dir($destination)) { + $destination = storage_path('uploads/' . trim($subdir, '/')); + if (!is_dir($destination)) { mkdir($destination, 0755, true); } - $filename = uniqid('exam_', true).'.'.$ext; + $filename = uniqid('exam_', true) . '.' . $ext; try { $file->move($destination, $filename); - return [ 'stored' => $filename, 'original' => $file->getClientOriginalName(), @@ -44,49 +43,46 @@ class ExamDraftFileService $ext = strtolower((string) $originalExt); if ($ext === 'pdf') { $path = $this->fullUploadPath($subdir, $finalFilename); - return is_file($path) ? $finalFilename : null; } - return $this->convertDocToPdf($this->fullUploadPath($subdir, $finalFilename), $subdir); } public function copyDraftToFinal(string $draftFilename, string $sourceDir, string $targetDir): ?string { $source = $this->fullUploadPath($sourceDir, $draftFilename); - if (! is_file($source)) { + if (!is_file($source)) { return null; } - $destinationDir = storage_path('uploads/'.trim($targetDir, '/')); - if (! is_dir($destinationDir)) { + $destinationDir = storage_path('uploads/' . trim($targetDir, '/')); + if (!is_dir($destinationDir)) { mkdir($destinationDir, 0755, true); } $ext = pathinfo($draftFilename, PATHINFO_EXTENSION); - $destName = uniqid('final_', true).'.'.$ext; - $destPath = $destinationDir.'/'.$destName; - if (! @copy($source, $destPath)) { + $destName = uniqid('final_', true) . '.' . $ext; + $destPath = $destinationDir . '/' . $destName; + if (!@copy($source, $destPath)) { return null; } - return $destName; } private function convertDocToPdf(string $sourcePath, string $targetSubdir): ?string { $ext = strtolower(pathinfo($sourcePath, PATHINFO_EXTENSION)); - if (! in_array($ext, ['doc', 'docx'], true)) { + if (!in_array($ext, ['doc', 'docx'], true)) { return null; } - $targetDir = storage_path('uploads/'.trim($targetSubdir, '/')); - if (! is_dir($targetDir)) { + $targetDir = storage_path('uploads/' . trim($targetSubdir, '/')); + if (!is_dir($targetDir)) { mkdir($targetDir, 0755, true); } $base = pathinfo($sourcePath, PATHINFO_FILENAME); - $targetPath = $targetDir.'/'.$base.'.pdf'; + $targetPath = $targetDir . '/' . $base . '.pdf'; - $cmd = 'soffice --headless --convert-to pdf --outdir '.escapeshellarg($targetDir).' '.escapeshellarg($sourcePath).' 2>/dev/null'; + $cmd = 'soffice --headless --convert-to pdf --outdir ' . escapeshellarg($targetDir) . ' ' . escapeshellarg($sourcePath) . ' 2>/dev/null'; @exec($cmd); return is_file($targetPath) ? basename($targetPath) : null; @@ -100,13 +96,12 @@ class ExamDraftFileService $path = $this->fullUploadPath($subdir, $filename); $base = pathinfo($path, PATHINFO_FILENAME); $dir = pathinfo($path, PATHINFO_DIRNAME); - $pdfPath = $dir.'/'.$base.'.pdf'; - + $pdfPath = $dir . '/' . $base . '.pdf'; return is_file($pdfPath) ? basename($pdfPath) : null; } private function fullUploadPath(string $subdir, string $filename): string { - return storage_path('uploads/'.trim($subdir, '/').'/'.$filename); + return storage_path('uploads/' . trim($subdir, '/') . '/' . $filename); } } diff --git a/app/Services/Exams/ExamDraftTeacherService.php b/app/Services/Exams/ExamDraftTeacherService.php index d099d67b..fa28bdf7 100644 --- a/app/Services/Exams/ExamDraftTeacherService.php +++ b/app/Services/Exams/ExamDraftTeacherService.php @@ -4,7 +4,6 @@ namespace App\Services\Exams; use App\Models\ExamDraft; use App\Models\TeacherClass; -use Illuminate\Http\UploadedFile; use Illuminate\Support\Facades\DB; class ExamDraftTeacherService @@ -12,7 +11,8 @@ class ExamDraftTeacherService public function __construct( private ExamDraftConfigService $configService, private ExamDraftFileService $fileService - ) {} + ) { + } public function listForTeacher(int $teacherId): array { @@ -33,7 +33,7 @@ class ExamDraftTeacherService } $legacyExams = []; - if (($context['has_legacy'] ?? false) && ! empty($classSectionIds)) { + if (($context['has_legacy'] ?? false) && !empty($classSectionIds)) { $legacyExams = DB::table('exam_drafts as ed') ->select('ed.*', 'cs.class_section_name') ->leftJoin('classSection as cs', 'cs.class_section_id', '=', 'ed.class_section_id') @@ -58,7 +58,7 @@ class ExamDraftTeacherService ]; } - public function store(int $teacherId, array $payload, ?UploadedFile $file): array + public function store(int $teacherId, array $payload, ?\Illuminate\Http\UploadedFile $file): array { $context = $this->configService->context(); $schoolYear = (string) ($context['school_year'] ?? ''); @@ -77,7 +77,7 @@ class ExamDraftTeacherService ->where('class_section_id', $classSectionId) ->first(); - if (! $assignment) { + if (!$assignment) { return ['ok' => false, 'message' => 'You are not assigned to the selected class section.']; } diff --git a/app/Services/Expenses/ExpenseReceiptService.php b/app/Services/Expenses/ExpenseReceiptService.php index f748bcdf..e0720a5c 100644 --- a/app/Services/Expenses/ExpenseReceiptService.php +++ b/app/Services/Expenses/ExpenseReceiptService.php @@ -9,7 +9,8 @@ class ExpenseReceiptService { public function __construct( private ApplicationUrlService $urls - ) {} + ) { + } public function storeReceipt(UploadedFile $file): string { @@ -33,7 +34,7 @@ class ExpenseReceiptService throw new \InvalidArgumentException('Receipt file too large. Max 5MB.'); } - $filename = bin2hex(random_bytes(16)).'.'.$allowed[$mime]; + $filename = bin2hex(random_bytes(16)) . '.' . $allowed[$mime]; $file->storeAs('receipts', $filename); return $filename; diff --git a/app/Services/Expenses/ExpenseStaffService.php b/app/Services/Expenses/ExpenseStaffService.php index 16fb4a30..0a204b67 100644 --- a/app/Services/Expenses/ExpenseStaffService.php +++ b/app/Services/Expenses/ExpenseStaffService.php @@ -26,7 +26,7 @@ class ExpenseStaffService if ($id <= 0 || in_array($roleName, $excludedRoles, true)) { continue; } - if (! isset($staff[$id])) { + if (!isset($staff[$id])) { $staff[$id] = [ 'id' => $id, 'firstname' => $row['firstname'] ?? '', @@ -36,9 +36,8 @@ class ExpenseStaffService } uasort($staff, static function ($a, $b) { - $nameA = trim(($a['firstname'] ?? '').' '.($a['lastname'] ?? '')); - $nameB = trim(($b['firstname'] ?? '').' '.($b['lastname'] ?? '')); - + $nameA = trim(($a['firstname'] ?? '') . ' ' . ($a['lastname'] ?? '')); + $nameB = trim(($b['firstname'] ?? '') . ' ' . ($b['lastname'] ?? '')); return strcasecmp($nameA, $nameB); }); diff --git a/app/Services/Expenses/ExpenseWriteService.php b/app/Services/Expenses/ExpenseWriteService.php index a35d8eec..0bea865d 100644 --- a/app/Services/Expenses/ExpenseWriteService.php +++ b/app/Services/Expenses/ExpenseWriteService.php @@ -36,7 +36,6 @@ class ExpenseWriteService { $expense->fill($payload); $expense->save(); - return $expense; } } diff --git a/app/Services/ExtraCharges/ExtraChargesChargeService.php b/app/Services/ExtraCharges/ExtraChargesChargeService.php index af1e25bf..2bf33682 100644 --- a/app/Services/ExtraCharges/ExtraChargesChargeService.php +++ b/app/Services/ExtraCharges/ExtraChargesChargeService.php @@ -58,8 +58,7 @@ class ExtraChargesChargeService DB::commit(); } catch (\Throwable $e) { DB::rollBack(); - Log::error('Extra charge create failed: '.$e->getMessage()); - + Log::error('Extra charge create failed: ' . $e->getMessage()); return ['ok' => false, 'message' => 'Failed to save charge.']; } @@ -94,17 +93,15 @@ class ExtraChargesChargeService } $charge->save(); - if ($charge->status === 'applied' && ! empty($charge->invoice_id) && abs($delta) > 0.00001) { + if ($charge->status === 'applied' && !empty($charge->invoice_id) && abs($delta) > 0.00001) { $this->invoiceService->adjustDelta((int) $charge->invoice_id, $delta); } DB::commit(); - return true; } catch (\Throwable $e) { DB::rollBack(); - Log::error('Extra charge update failed: '.$e->getMessage()); - + Log::error('Extra charge update failed: ' . $e->getMessage()); return false; } } @@ -127,12 +124,10 @@ class ExtraChargesChargeService $charge->save(); DB::commit(); - return true; } catch (\Throwable $e) { DB::rollBack(); - Log::error('Extra charge void failed: '.$e->getMessage()); - + Log::error('Extra charge void failed: ' . $e->getMessage()); return false; } } @@ -158,25 +153,23 @@ class ExtraChargesChargeService $charge->save(); DB::commit(); - return true; } catch (\Throwable $e) { DB::rollBack(); - Log::error('Extra charge reverse failed: '.$e->getMessage()); - + Log::error('Extra charge reverse failed: ' . $e->getMessage()); return false; } } private function triggerExtraChargeEvent(?array $parentUser, array $payload, int $chargeId, ?int $invoiceId, array $invoiceBefore, array $invoiceAfter): void { - if (! $parentUser) { + if (!$parentUser) { return; } $first = $parentUser['first_name'] ?? $parentUser['firstname'] ?? ''; $last = $parentUser['last_name'] ?? $parentUser['lastname'] ?? ''; - $parentName = trim($first.' '.$last); + $parentName = trim($first . ' ' . $last); $before = $this->normalizeRow($invoiceBefore); $after = $this->normalizeRow($invoiceAfter); @@ -194,7 +187,7 @@ class ExtraChargesChargeService 'school_year' => $payload['school_year'], 'semester' => $payload['semester'], 'invoice_id' => $invoiceId, - 'invoice_number' => $after['invoice_number'] ?? ($invoiceId ? 'INV-'.$invoiceId : null), + 'invoice_number' => $after['invoice_number'] ?? ($invoiceId ? 'INV-' . $invoiceId : null), 'charge_id' => $chargeId, 'charge_title' => $payload['title'], 'charge_desc' => $payload['description'], @@ -219,19 +212,18 @@ class ExtraChargesChargeService event('extraCharge', [$eventData, $students]); } } catch (\Throwable $e) { - Log::warning('extraCharge event failed: '.$e->getMessage()); + Log::warning('extraCharge event failed: ' . $e->getMessage()); } } private function normalizeRow($row): ?array { - if (! $row) { + if (!$row) { return null; } if (is_array($row) && isset($row[0]) && is_array($row[0])) { return $row[0]; } - return is_array($row) ? $row : null; } @@ -240,7 +232,6 @@ class ExtraChargesChargeService if (function_exists('utc_now')) { return (string) utc_now(); } - return now('UTC')->toDateTimeString(); } } diff --git a/app/Services/ExtraCharges/ExtraChargesInvoiceService.php b/app/Services/ExtraCharges/ExtraChargesInvoiceService.php index 11465f0a..ddb6b12b 100644 --- a/app/Services/ExtraCharges/ExtraChargesInvoiceService.php +++ b/app/Services/ExtraCharges/ExtraChargesInvoiceService.php @@ -16,7 +16,7 @@ class ExtraChargesInvoiceService return array_map(function ($inv) { $id = (int) ($inv['id'] ?? 0); - $num = (string) ($inv['invoice_number'] ?? ('INV-'.$id)); + $num = (string) ($inv['invoice_number'] ?? ('INV-' . $id)); $status = strtolower((string) ($inv['status'] ?? '')); $balance = (float) ($inv['balance'] ?? 0); $issue = $inv['issue_date'] ?? null; @@ -28,8 +28,8 @@ class ExtraChargesInvoiceService 'balance' => $balance, 'issue_date' => $issue, 'text' => $num - .($status ? ' — '.ucfirst($status) : '') - .' (Bal: $'.number_format($balance, 2).')', + . ($status ? ' — ' . ucfirst($status) : '') + . ' (Bal: $' . number_format($balance, 2) . ')', ]; }, $rows); } diff --git a/app/Services/ExtraCharges/ExtraChargesMetaService.php b/app/Services/ExtraCharges/ExtraChargesMetaService.php index 0109c183..ae429e6a 100644 --- a/app/Services/ExtraCharges/ExtraChargesMetaService.php +++ b/app/Services/ExtraCharges/ExtraChargesMetaService.php @@ -21,7 +21,7 @@ class ExtraChargesMetaService foreach ($rows as $row) { $val = (string) ($row['school_year'] ?? ''); - if ($val !== '' && ! in_array($val, $schoolYears, true)) { + if ($val !== '' && !in_array($val, $schoolYears, true)) { $schoolYears[] = $val; } } @@ -39,7 +39,7 @@ class ExtraChargesMetaService foreach ($rows as $row) { $val = (string) ($row['school_year'] ?? ''); - if ($val !== '' && ! in_array($val, $schoolYears, true)) { + if ($val !== '' && !in_array($val, $schoolYears, true)) { $schoolYears[] = $val; } } @@ -53,7 +53,7 @@ class ExtraChargesMetaService $start = (int) $start; if ($start > 0) { for ($i = 1; $i <= 3; $i++) { - $schoolYears[] = ($start - $i).'-'.(($start - $i) + 1); + $schoolYears[] = ($start - $i) . '-' . (($start - $i) + 1); } } } diff --git a/app/Services/ExtraCharges/ExtraChargesParentService.php b/app/Services/ExtraCharges/ExtraChargesParentService.php index c5593ed7..d9b3ad37 100644 --- a/app/Services/ExtraCharges/ExtraChargesParentService.php +++ b/app/Services/ExtraCharges/ExtraChargesParentService.php @@ -32,14 +32,13 @@ class ExtraChargesParentService ->all(); return array_map(function ($row) { - $name = trim(($row['lastname'] ?? '').', '.($row['firstname'] ?? '')); + $name = trim(($row['lastname'] ?? '') . ', ' . ($row['firstname'] ?? '')); $email = $row['email'] ?? ''; - $label = $name !== ',' ? $name : ('User #'.$row['id']); + $label = $name !== ',' ? $name : ('User #' . $row['id']); if ($email) { - $label .= ' — '.$email; + $label .= ' — ' . $email; } - $label .= ' (ID: '.$row['id'].')'; - + $label .= ' (ID: ' . $row['id'] . ')'; return [ 'id' => (int) $row['id'], 'text' => $label, diff --git a/app/Services/Families/FamilyMutationService.php b/app/Services/Families/FamilyMutationService.php index 78dd2fb4..78f188ab 100644 --- a/app/Services/Families/FamilyMutationService.php +++ b/app/Services/Families/FamilyMutationService.php @@ -67,7 +67,7 @@ class FamilyMutationService } $familyId = $this->familyIdFromStudentPrimary($studentId); - if (! $familyId) { + if (!$familyId) { return ['ok' => false, 'message' => 'No primary family found for this student']; } @@ -83,13 +83,13 @@ class FamilyMutationService } $familyId = $this->familyIdFromStudentPrimary($studentId); - if (! $familyId) { + if (!$familyId) { return ['ok' => false, 'message' => 'No primary family found for this student']; } return DB::transaction(function () use ($email, $firstname, $lastname, $relation, $familyId) { $user = User::query()->where('email', $email)->first(); - if (! $user) { + if (!$user) { $user = User::query()->create([ 'firstname' => (string) ($firstname ?? ''), 'lastname' => (string) ($lastname ?? ''), @@ -167,7 +167,7 @@ class FamilyMutationService { $legacyTable = 'parents'; $this->ensureTablesExist(['families', 'family_students', 'family_guardians']); - if (! Schema::hasTable($legacyTable)) { + if (!Schema::hasTable($legacyTable)) { return ['ok' => false, 'message' => "Legacy table '{$legacyTable}' not found"]; } @@ -192,7 +192,6 @@ class FamilyMutationService if ($firstId <= 0 || ($secondId <= 0 && $email === '')) { $skipped++; - continue; } @@ -204,7 +203,7 @@ class FamilyMutationService $userId = $secondId; } else { $user = User::query()->where('email', $email)->first(); - if (! $user) { + if (!$user) { $user = User::query()->create([ 'firstname' => (string) ($r['firstname'] ?? ''), 'lastname' => (string) ($r['lastname'] ?? ''), @@ -252,7 +251,7 @@ class FamilyMutationService private function ensureFamilyForPrimaryParent(int $primaryUserId, int &$createdCounter): int { - $code = 'FAM-'.$primaryUserId; + $code = 'FAM-' . $primaryUserId; $row = Family::query()->where('family_code', $code)->first(); if ($row) { return (int) $row->id; @@ -260,7 +259,7 @@ class FamilyMutationService $family = Family::query()->create([ 'family_code' => $code, - 'household_name' => 'Family of User '.$primaryUserId, + 'household_name' => 'Family of User ' . $primaryUserId, 'is_active' => 1, ]); $createdCounter++; @@ -310,11 +309,11 @@ class FamilyMutationService private function familyIdFromStudentPrimary(int $studentId): ?int { $student = Student::query()->select('parent_id')->find($studentId); - if (! $student || empty($student->parent_id)) { + if (!$student || empty($student->parent_id)) { return null; } - $code = 'FAM-'.(int) $student->parent_id; + $code = 'FAM-' . (int) $student->parent_id; $row = Family::query()->select('id')->where('family_code', $code)->first(); if ($row) { return (int) $row->id; @@ -332,7 +331,7 @@ class FamilyMutationService private function ensureTablesExist(array $tables): void { foreach ($tables as $table) { - if (! Schema::hasTable($table)) { + if (!Schema::hasTable($table)) { Log::warning('Family tables missing', ['table' => $table]); throw new \RuntimeException("Missing required table '{$table}'. Run migrations."); } diff --git a/app/Services/Families/FamilyNotificationService.php b/app/Services/Families/FamilyNotificationService.php index 119dd496..7b31cbc4 100644 --- a/app/Services/Families/FamilyNotificationService.php +++ b/app/Services/Families/FamilyNotificationService.php @@ -6,7 +6,9 @@ use App\Services\Email\EmailDispatchService; class FamilyNotificationService { - public function __construct(private EmailDispatchService $dispatch) {} + public function __construct(private EmailDispatchService $dispatch) + { + } public function sendComposeEmail( string $recipient, diff --git a/app/Services/Families/FamilyQueryService.php b/app/Services/Families/FamilyQueryService.php index cb0ac923..528bb667 100644 --- a/app/Services/Families/FamilyQueryService.php +++ b/app/Services/Families/FamilyQueryService.php @@ -5,11 +5,14 @@ namespace App\Services\Families; use App\Models\Family; use App\Models\Student; use App\Models\StudentClass; +use App\Models\User; use Illuminate\Support\Facades\DB; class FamilyQueryService { - public function __construct(private FamilyFinanceService $finance) {} + public function __construct(private FamilyFinanceService $finance) + { + } public function listStudentsForSelect(): array { @@ -29,7 +32,7 @@ class FamilyQueryService return []; } - $qs = '%'.str_replace(['%', '_'], ['\\%', '\\_'], $query).'%'; + $qs = '%' . str_replace(['%', '_'], ['\\%', '\\_'], $query) . '%'; $students = DB::table('students') ->select('id', 'firstname', 'lastname') @@ -60,7 +63,7 @@ class FamilyQueryService $items[] = [ 'type' => 'student', 'id' => (int) ($s['id'] ?? 0), - 'label' => trim(($s['firstname'] ?? '').' '.($s['lastname'] ?? '')), + 'label' => trim(($s['firstname'] ?? '') . ' ' . ($s['lastname'] ?? '')), 'sub' => 'Student', ]; } @@ -68,8 +71,8 @@ class FamilyQueryService $items[] = [ 'type' => 'guardian', 'id' => (int) ($g['id'] ?? 0), - 'label' => trim(($g['firstname'] ?? '').' '.($g['lastname'] ?? '')), - 'sub' => trim(($g['email'] ?? '').' '.($g['cellphone'] ?? '')), + 'label' => trim(($g['firstname'] ?? '') . ' ' . ($g['lastname'] ?? '')), + 'sub' => trim(($g['email'] ?? '') . ' ' . ($g['cellphone'] ?? '')), ]; } @@ -90,7 +93,7 @@ class FamilyQueryService ->orderBy('s.firstname') ->select('s.id as student_id') ->first(); - if ($row && ! empty($row->student_id)) { + if ($row && !empty($row->student_id)) { return (int) $row->student_id; } @@ -233,7 +236,7 @@ class FamilyQueryService unset($fam); $data['families'] = $families; - if (! empty($families)) { + if (!empty($families)) { $familyId = (int) ($families[0]['id'] ?? 0); $data['guardians'] = $familyId ? $this->listGuardiansByFamily($familyId) : []; } @@ -303,7 +306,7 @@ class FamilyQueryService ) ->find($familyId); - if (! $family) { + if (!$family) { return null; } @@ -361,7 +364,7 @@ class FamilyQueryService foreach ($guardians as $g) { $gid = (int) ($g['user_id'] ?? 0); if ($gid > 0) { - $guardianMap[$gid] = trim(($g['firstname'] ?? '').' '.($g['lastname'] ?? '')); + $guardianMap[$gid] = trim(($g['firstname'] ?? '') . ' ' . ($g['lastname'] ?? '')); } } @@ -375,7 +378,7 @@ class FamilyQueryService foreach ($rows as &$row) { $pid = (int) ($row['parent_id'] ?? 0); - $row['parent_label'] = $guardianMap[$pid] ?? ('Parent #'.$pid); + $row['parent_label'] = $guardianMap[$pid] ?? ('Parent #' . $pid); } unset($row); diff --git a/app/Services/FeeCalculationService.php b/app/Services/FeeCalculationService.php index ed9505bf..619a7d74 100644 --- a/app/Services/FeeCalculationService.php +++ b/app/Services/FeeCalculationService.php @@ -8,12 +8,12 @@ class FeeCalculationService { public function __construct( private FeeRefundCalculatorService $refundCalculator - ) {} + ) { + } public function calculateRefund(array $students, int $parentId): float { $result = $this->refundCalculator->calculateRefund($students, $parentId); - return (float) ($result['refund_amount'] ?? 0); } } diff --git a/app/Services/Fees/FeeGradeService.php b/app/Services/Fees/FeeGradeService.php index 9505b346..5933fd76 100644 --- a/app/Services/Fees/FeeGradeService.php +++ b/app/Services/Fees/FeeGradeService.php @@ -7,7 +7,6 @@ class FeeGradeService public function normalizeGrade(?string $grade): string { $normalized = strtoupper(trim((string) $grade)); - return str_replace([' ', '-'], '', $normalized); } diff --git a/app/Services/Fees/FeeRefundCalculatorService.php b/app/Services/Fees/FeeRefundCalculatorService.php index 2ba2391e..65cc299e 100644 --- a/app/Services/Fees/FeeRefundCalculatorService.php +++ b/app/Services/Fees/FeeRefundCalculatorService.php @@ -10,7 +10,8 @@ class FeeRefundCalculatorService public function __construct( private FeeConfigService $config, private FeeStudentFeeService $studentFees - ) {} + ) { + } public function calculateRefund(array $students, int $parentId): array { @@ -25,7 +26,6 @@ class FeeRefundCalculatorService 'parent_id' => $parentId, 'school_year' => $schoolYear, ]); - return $this->resultPayload(0.0, $totalPaid, $refundDeadline, $schoolEndDate, $weeksOfStudy, 0, []); } @@ -36,7 +36,6 @@ class FeeRefundCalculatorService 'parent_id' => $parentId, 'registered_count' => count($registered), ]); - return $this->resultPayload(0.0, $totalPaid, $refundDeadline, $schoolEndDate, $weeksOfStudy, 0, []); } @@ -58,7 +57,7 @@ class FeeRefundCalculatorService $withdrawnWithFees = array_values(array_filter( $allStudents, - fn (array $student): bool => ! empty($student['_is_withdrawn']) + fn (array $student): bool => !empty($student['_is_withdrawn']) )); $refundAmount = 0.0; @@ -83,32 +82,29 @@ class FeeRefundCalculatorService 'reason' => null, ]; - if (! $withdrawalDate) { + if (!$withdrawalDate) { $entry['reason'] = 'missing_withdrawal_date'; Log::warning('Refund skipped for student: missing withdrawal date', [ 'parent_id' => $parentId, 'student_id' => $studentId, ]); $studentBreakdown[] = $entry; - continue; } - if (! $refundDeadline || strtotime($withdrawalDate) > strtotime($refundDeadline)) { + if (!$refundDeadline || strtotime($withdrawalDate) > strtotime($refundDeadline)) { $entry['reason'] = 'after_refund_deadline'; $studentBreakdown[] = $entry; - continue; } - if (! $schoolEndDate) { + if (!$schoolEndDate) { $entry['reason'] = 'missing_school_end_date'; Log::warning('Refund skipped for student: missing school end date', [ 'parent_id' => $parentId, 'student_id' => $studentId, ]); $studentBreakdown[] = $entry; - continue; } @@ -121,7 +117,6 @@ class FeeRefundCalculatorService 'weeks_of_study' => $weeksOfStudy, ]); $studentBreakdown[] = $entry; - continue; } @@ -158,7 +153,7 @@ class FeeRefundCalculatorService if ($uncappedRefund > 0) { $ratio = $totalPaid / $uncappedRefund; foreach ($studentBreakdown as &$row) { - if (! empty($row['eligible'])) { + if (!empty($row['eligible'])) { $row['refund_amount'] = round($row['refund_amount'] * $ratio, 2); } } @@ -197,7 +192,6 @@ class FeeRefundCalculatorService foreach ($students as $student) { if ($this->studentFees->isWithdrawn($student)) { $withdrawn[] = $student; - continue; } @@ -219,7 +213,6 @@ class FeeRefundCalculatorService } $daysRemaining = $withdrawDateObj->diff($schoolEndDateObj)->days; - return (int) min($weeksOfStudy, max(0, (int) ceil($daysRemaining / 7))); } diff --git a/app/Services/Fees/FeeRefundDetailService.php b/app/Services/Fees/FeeRefundDetailService.php index ecb89d93..2affe056 100644 --- a/app/Services/Fees/FeeRefundDetailService.php +++ b/app/Services/Fees/FeeRefundDetailService.php @@ -37,12 +37,11 @@ class FeeRefundDetailService $tuitionStudents = $registeredKids; foreach ($withdrawnKids as $kid) { $rawDate = $kid['withdrawal_date'] ?? null; - if (empty($rawDate) || ! $deadline) { + if (empty($rawDate) || !$deadline) { Log::warning('Missing withdrawal date or deadline; billing withdrawn student.', [ 'student_id' => $kid['student_id'] ?? null, ]); $tuitionStudents[] = $kid; - continue; } @@ -53,7 +52,6 @@ class FeeRefundDetailService 'student_id' => $kid['student_id'] ?? null, ]); $tuitionStudents[] = $kid; - continue; } @@ -64,13 +62,12 @@ class FeeRefundDetailService $tuitionStudents = array_values(array_filter($tuitionStudents, function ($student) use ($schoolYear) { $sid = (int) ($student['student_id'] ?? 0); - return $sid > 0 && StudentClass::hasNonEventAssignment($sid, $schoolYear); })); foreach ($tuitionStudents as &$student) { $name = null; - if (! empty($student['class_section_id'])) { + if (!empty($student['class_section_id'])) { $name = ClassSection::getClassSectionNameBySectionId($student['class_section_id']); } $student['grade_name'] = is_string($name) ? strtoupper(trim($name)) : 'N/A'; @@ -132,7 +129,7 @@ class FeeRefundDetailService if ($bookPriceRaw === null || $bookPriceRaw === '') { return ['ok' => false, 'error' => 'Book price is not configured.']; } - if (! is_numeric($bookPriceRaw)) { + if (!is_numeric($bookPriceRaw)) { return ['ok' => false, 'error' => 'Book price must be numeric.']; } $bookPrice = round((float) $bookPriceRaw, 2); @@ -149,10 +146,9 @@ class FeeRefundDetailService $status = (string) ($student['enrollment_status'] ?? ''); if (in_array($status, ['enrolled', 'payment pending'], true)) { $registered[] = $student; - continue; } - if (! in_array($status, ['withdrawn', 'refund pending', 'withdraw under review'], true)) { + if (!in_array($status, ['withdrawn', 'refund pending', 'withdraw under review'], true)) { continue; } $hasWithdrawn = true; @@ -173,7 +169,7 @@ class FeeRefundDetailService } } - if (! $hasWithdrawn) { + if (!$hasWithdrawn) { return ['ok' => false, 'error' => 'No withdrawn students found for refund evaluation.']; } @@ -207,7 +203,7 @@ class FeeRefundDetailService } } - $eligible = ! empty($withdrawnEligible); + $eligible = !empty($withdrawnEligible); $refundAmount = 0.0; $debitAmount = 0.0; @@ -234,9 +230,9 @@ class FeeRefundDetailService $refundableCash = min($totalPaid, $refundableTuition); $raw = round($refundableCash - $bookPrice, 2); - if ($raw > 0 && ! $fullDiscount) { + if ($raw > 0 && !$fullDiscount) { $refundAmount = $raw; - } elseif ($raw < 0 && ! $fullDiscount) { + } elseif ($raw < 0 && !$fullDiscount) { $debitAmount = abs($raw); } } @@ -263,7 +259,6 @@ class FeeRefundDetailService public function calculateRefund(array $students, int $parentId): float { $result = $this->calculateRefundDetails($students, $parentId); - return (float) ($result['refund_amount'] ?? 0.0); } @@ -271,7 +266,6 @@ class FeeRefundDetailService { $resolvedGradeFee = $gradeFee ?? (int) (Configuration::getConfig('grade_fee') ?? 9); $grades = new InvoiceGradeService($resolvedGradeFee); - return $grades->getGradeLevel($grade); } @@ -279,7 +273,6 @@ class FeeRefundDetailService { $resolvedGradeFee = $gradeFee ?? (int) (Configuration::getConfig('grade_fee') ?? 9); $grades = new InvoiceGradeService($resolvedGradeFee); - return $grades->compareGrades((string) $gradeA, (string) $gradeB); } diff --git a/app/Services/Fees/FeeStudentFeeService.php b/app/Services/Fees/FeeStudentFeeService.php index 7c6b1a14..546d92e1 100644 --- a/app/Services/Fees/FeeStudentFeeService.php +++ b/app/Services/Fees/FeeStudentFeeService.php @@ -8,23 +8,19 @@ use Illuminate\Support\Facades\Log; class FeeStudentFeeService { public const CATEGORY_FIRST_REGULAR = 'first_regular'; - public const CATEGORY_ADDITIONAL_REGULAR = 'additional_regular'; - public const CATEGORY_YOUTH = 'youth'; - public const CATEGORY_NOT_BILLABLE = 'not_billable'; private const BILLABLE_ENROLLMENT_STATUSES = ['enrolled', 'payment pending']; - private const WITHDRAWN_ENROLLMENT_STATUSES = ['withdrawn', 'refund pending', 'withdraw under review']; - private const ACCEPTED_ADMISSION_STATUS = 'accepted'; public function __construct( private FeeGradeService $grades, private FeeConfigService $config - ) {} + ) { + } /** * Normalize grades, sort by grade level, and assign a tuition fee to each @@ -134,7 +130,6 @@ class FeeStudentFeeService $nonBillable = array_map(function (array $student): array { $student['fee_category'] = self::CATEGORY_NOT_BILLABLE; $student['tuition_fee'] = 0.0; - return $student; }, $nonBillable); @@ -153,7 +148,7 @@ class FeeStudentFeeService $studentRows[] = [ 'student_id' => $student['student_id'] ?? null, - 'student_name' => trim(($student['firstname'] ?? '').' '.($student['lastname'] ?? '')), + 'student_name' => trim(($student['firstname'] ?? '') . ' ' . ($student['lastname'] ?? '')), 'grade' => $student['grade'] ?? null, 'grade_level' => $student['grade_level'] ?? null, 'enrollment_status' => $student['enrollment_status'] ?? null, @@ -197,7 +192,6 @@ class FeeStudentFeeService public function isWithdrawn(array $student): bool { $status = strtolower((string) ($student['enrollment_status'] ?? '')); - return in_array($status, self::WITHDRAWN_ENROLLMENT_STATUSES, true); } diff --git a/app/Services/Fees/TuitionCalculationService.php b/app/Services/Fees/TuitionCalculationService.php index 18d6d640..cfeceb33 100644 --- a/app/Services/Fees/TuitionCalculationService.php +++ b/app/Services/Fees/TuitionCalculationService.php @@ -23,14 +23,16 @@ class TuitionCalculationService public function __construct( private FeeConfigService $config, private FeeStudentFeeService $studentFees - ) {} + ) { + } /** * Produce a family + per-student tuition calculation. * - * @param array $students Each item may include: student_id, firstname, - * lastname, grade, class_section_id, enrollment_status, - * admission_status, discount_amount. + * @param array $students Each item may include: student_id, firstname, + * lastname, grade, class_section_id, enrollment_status, + * admission_status, discount_amount. + * * @return array{ * school_year: string, * fees: array, diff --git a/app/Services/Files/ExamDraftDownloadNameService.php b/app/Services/Files/ExamDraftDownloadNameService.php index ca156993..e08fd3a8 100644 --- a/app/Services/Files/ExamDraftDownloadNameService.php +++ b/app/Services/Files/ExamDraftDownloadNameService.php @@ -13,15 +13,15 @@ class ExamDraftDownloadNameService $row = DB::table('exam_drafts as ed') ->select('ed.version', 'ed.exam_type', 'ed.class_section_id', 'cs.class_section_name') ->leftJoin('classSection as cs', 'cs.class_section_id', '=', 'ed.class_section_id') - ->where('ed.'.$column, $filename) + ->where('ed.' . $column, $filename) ->limit(1) ->first(); $row = $row ? (array) $row : []; - $classLabel = trim((string) ($row['class_section_name'] ?? ('Class'.($row['class_section_id'] ?? '0')))); + $classLabel = trim((string) ($row['class_section_name'] ?? ('Class' . ($row['class_section_id'] ?? '0')))); $typeLabel = trim((string) ($row['exam_type'] ?? 'Exam')); - $version = 'v'.max(1, (int) ($row['version'] ?? 1)); + $version = 'v' . max(1, (int) ($row['version'] ?? 1)); $parts = array_filter([ $this->slugify($classLabel), @@ -40,7 +40,6 @@ class ExamDraftDownloadNameService if ($value === '') { return 'Exam'; } - return mb_strtolower($value); } } diff --git a/app/Services/Files/FileServeService.php b/app/Services/Files/FileServeService.php index e0424468..fb39b566 100644 --- a/app/Services/Files/FileServeService.php +++ b/app/Services/Files/FileServeService.php @@ -35,7 +35,7 @@ class FileServeService $headers = [ 'Content-Type' => $meta['mime'], - 'Content-Disposition' => 'inline; filename="'.$meta['download_name'].'"', + 'Content-Disposition' => 'inline; filename="' . $meta['download_name'] . '"', 'Content-Length' => (string) $meta['size'], 'ETag' => $meta['etag'], 'Last-Modified' => $meta['last_modified'], @@ -53,20 +53,20 @@ class FileServeService } $ext = strtolower(pathinfo($name, PATHINFO_EXTENSION)); - if (! in_array($ext, $allowedExtensions, true)) { + if (!in_array($ext, $allowedExtensions, true)) { throw new HttpException(404, 'File not found'); } - $path = rtrim($baseDir, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR.$name; - if (! is_file($path)) { + $path = rtrim($baseDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $name; + if (!is_file($path)) { throw new HttpException(404, 'File not found'); } $mime = $this->detectMime($path); $mtime = filemtime($path) ?: time(); $size = filesize($path) ?: 0; - $etag = md5($name.'|'.$mtime.'|'.$size); - $downloadName = $downloadName ? $downloadName.'.'.$ext : $name; + $etag = md5($name . '|' . $mtime . '|' . $size); + $downloadName = $downloadName ? $downloadName . '.' . $ext : $name; return [ 'name' => $name, @@ -76,7 +76,7 @@ class FileServeService 'size' => $size, 'etag' => $etag, 'download_name' => $downloadName, - 'last_modified' => gmdate('D, d M Y H:i:s', $mtime).' GMT', + 'last_modified' => gmdate('D, d M Y H:i:s', $mtime) . ' GMT', ]; } diff --git a/app/Services/Finance/FinanceNotificationLogService.php b/app/Services/Finance/FinanceNotificationLogService.php index 5a4531be..33229701 100644 --- a/app/Services/Finance/FinanceNotificationLogService.php +++ b/app/Services/Finance/FinanceNotificationLogService.php @@ -16,36 +16,25 @@ class FinanceNotificationLogService 'failed_at' => $status === 'failed' ? now() : null, 'created_by' => $actorId, ])); - return $log->toArray(); } public function list(array $filters): array { $q = FinanceNotificationLog::query(); - foreach (['status', 'notification_type', 'parent_id'] as $field) { - if (! empty($filters[$field])) { - $q->where($field, $filters[$field]); - } - } - if (! empty($filters['date_from'])) { - $q->whereDate('created_at', '>=', $filters['date_from']); - } - if (! empty($filters['date_to'])) { - $q->whereDate('created_at', '<=', $filters['date_to']); - } - - return ['rows' => $q->orderByDesc('id')->paginate((int) ($filters['per_page'] ?? 25))]; + foreach (['status','notification_type','parent_id'] as $field) if (!empty($filters[$field])) $q->where($field, $filters[$field]); + if (!empty($filters['date_from'])) $q->whereDate('created_at', '>=', $filters['date_from']); + if (!empty($filters['date_to'])) $q->whereDate('created_at', '<=', $filters['date_to']); + return ['rows' => $q->orderByDesc('id')->paginate((int)($filters['per_page'] ?? 25))]; } public function nextReceiptNumber(string $schoolYear): string { return DB::transaction(function () use ($schoolYear) { $seq = FinanceReceiptSequence::query()->lockForUpdate()->firstOrCreate(['school_year' => $schoolYear], ['prefix' => 'R', 'next_number' => 1]); - $number = $seq->prefix.'-'.$schoolYear.'-'.str_pad((string) $seq->next_number, 6, '0', STR_PAD_LEFT); + $number = $seq->prefix . '-' . $schoolYear . '-' . str_pad((string) $seq->next_number, 6, '0', STR_PAD_LEFT); $seq->next_number = $seq->next_number + 1; $seq->save(); - return $number; }); } diff --git a/app/Services/Finance/FinancialChartService.php b/app/Services/Finance/FinancialChartService.php index c2f7609c..8beea7a1 100644 --- a/app/Services/Finance/FinancialChartService.php +++ b/app/Services/Finance/FinancialChartService.php @@ -60,9 +60,9 @@ class FinancialChartService private function downloadChart(array $chartData, string $filename): ?string { $baseUrl = 'https://quickchart.io/chart?c='; - $url = $baseUrl.urlencode(json_encode($chartData)); + $url = $baseUrl . urlencode(json_encode($chartData)); $targetDir = storage_path('app/reports'); - if (! is_dir($targetDir) && ! mkdir($targetDir, 0775, true) && ! is_dir($targetDir)) { + if (!is_dir($targetDir) && !mkdir($targetDir, 0775, true) && !is_dir($targetDir)) { return null; } @@ -71,9 +71,8 @@ class FinancialChartService if ($contents === false) { return null; } - $path = $targetDir.DIRECTORY_SEPARATOR.$filename; + $path = $targetDir . DIRECTORY_SEPARATOR . $filename; file_put_contents($path, $contents); - return $path; } catch (\Throwable $e) { return null; diff --git a/app/Services/Finance/FinancialPaymentService.php b/app/Services/Finance/FinancialPaymentService.php index c22a7b6d..5f55bdf7 100644 --- a/app/Services/Finance/FinancialPaymentService.php +++ b/app/Services/Finance/FinancialPaymentService.php @@ -50,7 +50,7 @@ class FinancialPaymentService } $method = (string) ($row['method'] ?? ''); $amount = (float) ($row['amount'] ?? 0); - if (! isset($breakdown[$invoiceId])) { + if (!isset($breakdown[$invoiceId])) { $breakdown[$invoiceId] = []; } $breakdown[$invoiceId][$method] = $amount; @@ -85,13 +85,13 @@ class FinancialPaymentService { $query = DB::table('payments'); - if (! empty($schoolYear)) { + if (!empty($schoolYear)) { $query->where('school_year', $schoolYear); } - if (! empty($dateFrom)) { + if (!empty($dateFrom)) { $query->whereRaw('DATE(payment_date) >= ?', [$dateFrom]); } - if (! empty($dateTo)) { + if (!empty($dateTo)) { $query->whereRaw('DATE(payment_date) <= ?', [$dateTo]); } diff --git a/app/Services/Finance/FinancialPdfReportService.php b/app/Services/Finance/FinancialPdfReportService.php index bd1c3be9..0f7c28b1 100644 --- a/app/Services/Finance/FinancialPdfReportService.php +++ b/app/Services/Finance/FinancialPdfReportService.php @@ -4,14 +4,16 @@ namespace App\Services\Finance; class FinancialPdfReportService { - public function __construct(private FinancialChartService $charts) {} + public function __construct(private FinancialChartService $charts) + { + } public function buildPdf(array $summary): string { - $pdf = new \FPDF; + $pdf = new \FPDF(); $pdf->AddPage(); $pdf->SetFont('Arial', 'B', 16); - $pdf->Cell(0, 10, 'Financial Report for School Year: '.($summary['schoolYear'] ?? ''), 0, 1, 'C'); + $pdf->Cell(0, 10, 'Financial Report for School Year: ' . ($summary['schoolYear'] ?? ''), 0, 1, 'C'); $pdf->Ln(10); $pdf->SetFont('Arial', 'B', 12); @@ -35,12 +37,12 @@ class FinancialPdfReportService $pdf->SetFont('Arial', '', 11); foreach ($rows as $label => $amount) { $pdf->Cell(130, 10, $label, 1); - $pdf->Cell(60, 10, '$'.number_format((float) $amount, 2), 1); + $pdf->Cell(60, 10, '$' . number_format((float) $amount, 2), 1); $pdf->Ln(); } $barChart = $this->charts->generateBarChart($summary); - if (! empty($barChart) && file_exists($barChart)) { + if (!empty($barChart) && file_exists($barChart)) { $pdf->Ln(10); $pdf->SetFont('Arial', 'B', 12); $pdf->Cell(0, 10, 'Summary Graph', 0, 1); @@ -48,7 +50,7 @@ class FinancialPdfReportService } $pieChart = $this->charts->generatePieChart($summary); - if (! empty($pieChart) && file_exists($pieChart)) { + if (!empty($pieChart) && file_exists($pieChart)) { $pdf->Ln(10); $pdf->SetFont('Arial', 'B', 12); $pdf->Cell(0, 10, 'Expense Breakdown', 0, 1); diff --git a/app/Services/Finance/FinancialReportService.php b/app/Services/Finance/FinancialReportService.php index 1fe0c70c..16ab30ee 100644 --- a/app/Services/Finance/FinancialReportService.php +++ b/app/Services/Finance/FinancialReportService.php @@ -9,7 +9,8 @@ class FinancialReportService public function __construct( private FinancialPaymentService $payments, private FinancialSchoolYearService $schoolYears - ) {} + ) { + } public function getReport(?string $dateFrom, ?string $dateTo, ?string $schoolYear): array { @@ -22,7 +23,7 @@ class FinancialReportService $expenseQuery = DB::table('expenses'); $reimbQuery = DB::table('reimbursements'); - if (! empty($schoolYear)) { + if (!empty($schoolYear)) { $invoiceQuery->where('invoices.school_year', $schoolYear); $refundQuery->where('school_year', $schoolYear); $discountQuery->where('school_year', $schoolYear); @@ -30,7 +31,7 @@ class FinancialReportService $reimbQuery->where('school_year', $schoolYear); } - if (! empty($dateFrom)) { + if (!empty($dateFrom)) { $invoiceQuery->whereRaw('DATE(COALESCE(invoices.issue_date, invoices.created_at)) >= ?', [$dateFrom]); $refundQuery->whereRaw('DATE(COALESCE(refunded_at, created_at)) >= ?', [$dateFrom]); $discountQuery->whereRaw('DATE(COALESCE(used_at, created_at)) >= ?', [$dateFrom]); @@ -38,7 +39,7 @@ class FinancialReportService $reimbQuery->whereRaw('DATE(created_at) >= ?', [$dateFrom]); } - if (! empty($dateTo)) { + if (!empty($dateTo)) { $invoiceQuery->whereRaw('DATE(COALESCE(invoices.issue_date, invoices.created_at)) <= ?', [$dateTo]); $refundQuery->whereRaw('DATE(COALESCE(refunded_at, created_at)) <= ?', [$dateTo]); $discountQuery->whereRaw('DATE(COALESCE(used_at, created_at)) <= ?', [$dateTo]); diff --git a/app/Services/Finance/FinancialSchoolYearService.php b/app/Services/Finance/FinancialSchoolYearService.php index 04038ef4..2cb61a51 100644 --- a/app/Services/Finance/FinancialSchoolYearService.php +++ b/app/Services/Finance/FinancialSchoolYearService.php @@ -19,7 +19,7 @@ class FinancialSchoolYearService ->values() ->all(); - if (empty($years) && ! empty($fallbackYear)) { + if (empty($years) && !empty($fallbackYear)) { $years[] = (string) $fallbackYear; } diff --git a/app/Services/Finance/FinancialSummaryService.php b/app/Services/Finance/FinancialSummaryService.php index a5980ce1..c0df7c95 100644 --- a/app/Services/Finance/FinancialSummaryService.php +++ b/app/Services/Finance/FinancialSummaryService.php @@ -10,7 +10,8 @@ class FinancialSummaryService { public function __construct( private FinancialDonationRecipientService $donationRecipients - ) {} + ) { + } public function getSummary(?string $dateFrom, ?string $dateTo, ?string $schoolYear): array { @@ -18,10 +19,10 @@ class FinancialSummaryService $derivedDateFrom = null; $derivedDateTo = null; - if (! empty($schoolYear) && empty($dateFrom) && empty($dateTo)) { + if (!empty($schoolYear) && empty($dateFrom) && empty($dateTo)) { if (preg_match('/^(\\d{4})[^\\d]?(\\d{4})$/', $schoolYear, $m)) { - $derivedDateFrom = $m[1].'-01-01'; - $derivedDateTo = $m[2].'-12-31'; + $derivedDateFrom = $m[1] . '-01-01'; + $derivedDateTo = $m[2] . '-12-31'; } } @@ -29,10 +30,10 @@ class FinancialSummaryService $invoiceDateTo = $dateTo ?: $derivedDateTo; $invoiceQuery = DB::table('invoices')->where('school_year', $schoolYear); - if (! empty($invoiceDateFrom)) { + if (!empty($invoiceDateFrom)) { $invoiceQuery->whereRaw('DATE(COALESCE(issue_date, created_at)) >= ?', [$invoiceDateFrom]); } - if (! empty($invoiceDateTo)) { + if (!empty($invoiceDateTo)) { $invoiceQuery->whereRaw('DATE(COALESCE(issue_date, created_at)) <= ?', [$invoiceDateTo]); } $invoices = $invoiceQuery->get()->map(fn ($row) => (array) $row)->all(); @@ -40,7 +41,6 @@ class FinancialSummaryService $invoiceIds = array_values(array_unique(array_filter(array_map(static function ($row) { $id = (int) ($row['id'] ?? 0); - return $id > 0 ? $id : null; }, $invoices)))); @@ -54,7 +54,7 @@ class FinancialSummaryService $paidByInvoice = []; $discountByInvoice = []; $refundByInvoice = []; - if (! empty($invoiceIds)) { + if (!empty($invoiceIds)) { $paidByInvoice = $this->paymentsByInvoiceIds($invoiceIds, $schoolYear, $invoiceDateFrom, $invoiceDateTo); $discountByInvoice = $this->discountsByInvoiceIds($invoiceIds, $dateFrom, $dateTo); $refundByInvoice = $this->refundsByInvoiceIds($invoiceIds, $dateFrom, $dateTo); @@ -99,15 +99,14 @@ class FinancialSummaryService ->where('ac.school_year', $schoolYear) ->where('ac.status', '!=', 'void'); - if (! empty($dateFrom)) { + if (!empty($dateFrom)) { $query->whereRaw('DATE(ac.due_date) >= ?', [$dateFrom]); } - if (! empty($dateTo)) { + if (!empty($dateTo)) { $query->whereRaw('DATE(ac.due_date) <= ?', [$dateTo]); } $row = (array) $query->first(); - return (float) ($row['amount'] ?? 0); } @@ -124,10 +123,10 @@ class FinancialSummaryService }) ->groupBy('ac.parent_id'); - if (! empty($dateFrom)) { + if (!empty($dateFrom)) { $query->whereRaw('DATE(ac.due_date) >= ?', [$dateFrom]); } - if (! empty($dateTo)) { + if (!empty($dateTo)) { $query->whereRaw('DATE(ac.due_date) <= ?', [$dateTo]); } @@ -147,10 +146,10 @@ class FinancialSummaryService private function sumPayments(string $schoolYear, ?string $dateFrom, ?string $dateTo): float { $query = DB::table('payments')->where('school_year', $schoolYear); - if (! empty($dateFrom)) { + if (!empty($dateFrom)) { $query->whereRaw('DATE(payment_date) >= ?', [$dateFrom]); } - if (! empty($dateTo)) { + if (!empty($dateTo)) { $query->whereRaw('DATE(payment_date) <= ?', [$dateTo]); } @@ -167,7 +166,6 @@ class FinancialSummaryService } $row = (array) $query->selectRaw('COALESCE(SUM(paid_amount), 0) AS paid_amount')->first(); - return (float) ($row['paid_amount'] ?? 0); } @@ -178,10 +176,10 @@ class FinancialSummaryService ->whereIn('invoice_id', $invoiceIds) ->where('school_year', $schoolYear); - if (! empty($dateFrom)) { + if (!empty($dateFrom)) { $query->whereRaw('DATE(payment_date) >= ?', [$dateFrom]); } - if (! empty($dateTo)) { + if (!empty($dateTo)) { $query->whereRaw('DATE(payment_date) <= ?', [$dateTo]); } @@ -205,7 +203,6 @@ class FinancialSummaryService $map[$invoiceId] = (float) ($row['total_paid'] ?? 0); } } - return $map; } @@ -215,10 +212,10 @@ class FinancialSummaryService ->selectRaw('invoice_id, COALESCE(SUM(discount_amount),0) AS total_disc') ->whereIn('invoice_id', $invoiceIds); - if (! empty($dateFrom)) { + if (!empty($dateFrom)) { $query->whereRaw('DATE(COALESCE(used_at, created_at)) >= ?', [$dateFrom]); } - if (! empty($dateTo)) { + if (!empty($dateTo)) { $query->whereRaw('DATE(COALESCE(used_at, created_at)) <= ?', [$dateTo]); } @@ -230,7 +227,6 @@ class FinancialSummaryService $map[$invoiceId] = (float) ($row['total_disc'] ?? 0); } } - return $map; } @@ -241,10 +237,10 @@ class FinancialSummaryService ->whereIn('invoice_id', $invoiceIds) ->whereIn('status', ['Partial', 'Paid']); - if (! empty($dateFrom)) { + if (!empty($dateFrom)) { $query->whereRaw('DATE(COALESCE(refunded_at, created_at)) >= ?', [$dateFrom]); } - if (! empty($dateTo)) { + if (!empty($dateTo)) { $query->whereRaw('DATE(COALESCE(refunded_at, created_at)) <= ?', [$dateTo]); } @@ -256,22 +252,20 @@ class FinancialSummaryService $map[$invoiceId] = (float) ($row['total_refund'] ?? 0); } } - return $map; } private function sumExpenses(string $schoolYear, ?string $dateFrom, ?string $dateTo): float { $query = DB::table('expenses')->where('school_year', $schoolYear); - if (! empty($dateFrom)) { + if (!empty($dateFrom)) { $query->whereRaw('DATE(created_at) >= ?', [$dateFrom]); } - if (! empty($dateTo)) { + if (!empty($dateTo)) { $query->whereRaw('DATE(created_at) <= ?', [$dateTo]); } $row = (array) $query->selectRaw('COALESCE(SUM(amount), 0) AS amount')->first(); - return (float) ($row['amount'] ?? 0); } @@ -283,7 +277,7 @@ class FinancialSummaryService ->selectRaw('COALESCE(SUM(r.amount),0) AS amount') ->leftJoin('expenses as e', 'e.id', '=', 'r.expense_id'); - if (! empty($schoolYear)) { + if (!empty($schoolYear)) { $query->where(function ($q) use ($schoolYear, $normalizedYear) { $q->where(function ($q2) use ($schoolYear, $normalizedYear) { $q2->where('r.school_year', $schoolYear) @@ -304,10 +298,10 @@ class FinancialSummaryService }); } - if (! empty($dateFrom)) { + if (!empty($dateFrom)) { $query->whereRaw('DATE(COALESCE(r.created_at, e.created_at)) >= ?', [$dateFrom]); } - if (! empty($dateTo)) { + if (!empty($dateTo)) { $query->whereRaw('DATE(COALESCE(r.created_at, e.created_at)) <= ?', [$dateTo]); } @@ -323,7 +317,7 @@ class FinancialSummaryService ->whereNull('bi.unassigned_at') ->whereNull('r.id'); - if (! empty($schoolYear)) { + if (!empty($schoolYear)) { $batchFallback->where(function ($q) use ($schoolYear, $normalizedYear) { $q->where('e.school_year', $schoolYear) ->orWhereRaw( @@ -333,10 +327,10 @@ class FinancialSummaryService }); } - if (! empty($dateFrom)) { + if (!empty($dateFrom)) { $batchFallback->whereRaw('DATE(COALESCE(e.created_at, b.closed_at)) >= ?', [$dateFrom]); } - if (! empty($dateTo)) { + if (!empty($dateTo)) { $batchFallback->whereRaw('DATE(COALESCE(e.created_at, b.closed_at)) <= ?', [$dateTo]); } @@ -352,10 +346,10 @@ class FinancialSummaryService ->where('school_year', $schoolYear) ->where('category', 'Donation'); - if (! empty($dateFrom)) { + if (!empty($dateFrom)) { $expenseQuery->whereRaw('DATE(created_at) >= ?', [$dateFrom]); } - if (! empty($dateTo)) { + if (!empty($dateTo)) { $expenseQuery->whereRaw('DATE(created_at) <= ?', [$dateTo]); } @@ -364,14 +358,14 @@ class FinancialSummaryService $donationReimb = 0.0; $recipientIds = $this->donationRecipients->recipientIds(); - if (! empty($recipientIds)) { + if (!empty($recipientIds)) { $normalizedYear = preg_replace('/[^0-9]/', '', (string) $schoolYear); $donationQuery = DB::table('reimbursements as r') ->selectRaw('COALESCE(SUM(r.amount),0) AS amount') ->leftJoin('expenses as e', 'e.id', '=', 'r.expense_id') ->whereIn('r.reimbursed_to', $recipientIds); - if (! empty($schoolYear)) { + if (!empty($schoolYear)) { $donationQuery->where(function ($q) use ($schoolYear, $normalizedYear) { $q->where(function ($q2) use ($schoolYear, $normalizedYear) { $q2->where('r.school_year', $schoolYear) @@ -392,10 +386,10 @@ class FinancialSummaryService }); } - if (! empty($dateFrom)) { + if (!empty($dateFrom)) { $donationQuery->whereRaw('DATE(COALESCE(r.created_at, e.created_at)) >= ?', [$dateFrom]); } - if (! empty($dateTo)) { + if (!empty($dateTo)) { $donationQuery->whereRaw('DATE(COALESCE(r.created_at, e.created_at)) <= ?', [$dateTo]); } @@ -416,15 +410,14 @@ class FinancialSummaryService ->whereIn('status', ['Partial', 'Paid']) ->whereNotNull('refund_paid_amount'); - if (! empty($dateFrom)) { + if (!empty($dateFrom)) { $query->whereRaw('DATE(COALESCE(refunded_at, created_at)) >= ?', [$dateFrom]); } - if (! empty($dateTo)) { + if (!empty($dateTo)) { $query->whereRaw('DATE(COALESCE(refunded_at, created_at)) <= ?', [$dateTo]); } $row = (array) $query->selectRaw('COALESCE(SUM(refund_paid_amount),0) AS refund_paid_amount')->first(); - return (float) ($row['refund_paid_amount'] ?? 0); } @@ -434,15 +427,14 @@ class FinancialSummaryService ->join('invoices as i', 'i.id', '=', 'du.invoice_id') ->where('i.school_year', $schoolYear); - if (! empty($dateFrom)) { + if (!empty($dateFrom)) { $query->whereRaw('DATE(COALESCE(du.used_at, du.created_at)) >= ?', [$dateFrom]); } - if (! empty($dateTo)) { + if (!empty($dateTo)) { $query->whereRaw('DATE(COALESCE(du.used_at, du.created_at)) <= ?', [$dateTo]); } $row = (array) $query->selectRaw('COALESCE(SUM(du.discount_amount),0) AS discount_amount')->first(); - return (float) ($row['discount_amount'] ?? 0); } @@ -462,14 +454,14 @@ class FinancialSummaryService $ref = (float) ($refundByInvoice[$invoiceId] ?? 0); $balance = max(0.0, round($total - $disc - $paid - $ref, 2)); - if (! isset($parentBalances[$parentId])) { + if (!isset($parentBalances[$parentId])) { $parentBalances[$parentId] = 0.0; } $parentBalances[$parentId] += $balance; } foreach ($extraByParent as $parentId => $extra) { - if (! isset($parentBalances[$parentId])) { + if (!isset($parentBalances[$parentId])) { $parentBalances[$parentId] = 0.0; } $parentBalances[$parentId] += (float) $extra; diff --git a/app/Services/Finance/FinancialUnpaidParentsService.php b/app/Services/Finance/FinancialUnpaidParentsService.php index 356f2955..fddd9a56 100644 --- a/app/Services/Finance/FinancialUnpaidParentsService.php +++ b/app/Services/Finance/FinancialUnpaidParentsService.php @@ -8,7 +8,9 @@ use Illuminate\Support\Facades\Schema; class FinancialUnpaidParentsService { - public function __construct(private FinancialSchoolYearService $schoolYears) {} + public function __construct(private FinancialSchoolYearService $schoolYears) + { + } public function getUnpaidParents(?string $schoolYear): array { @@ -74,7 +76,7 @@ class FinancialUnpaidParentsService $total = (float) ($row['total_amount'] ?? 0); $balance = max(0.0, round($total - $discSum - $paidSum - $refSum, 2)); - if (! isset($byParent[$parentId])) { + if (!isset($byParent[$parentId])) { $byParent[$parentId] = [ 'parent_id' => $parentId, 'firstname' => (string) ($row['firstname'] ?? ''), @@ -94,7 +96,7 @@ class FinancialUnpaidParentsService } $extraRows = DB::table('additional_charges as ac') - ->selectRaw('ac.parent_id, u.firstname, u.lastname, u.email, COALESCE(SUM(ac.amount), 0) AS total_extra') + ->selectRaw("ac.parent_id, u.firstname, u.lastname, u.email, COALESCE(SUM(ac.amount), 0) AS total_extra") ->leftJoin('users as u', 'u.id', '=', 'ac.parent_id') ->where('ac.school_year', $schoolYear) ->where('ac.status', '!=', 'void') @@ -117,7 +119,7 @@ class FinancialUnpaidParentsService if (abs($extra) < 0.00001) { continue; } - if (! isset($byParent[$parentId])) { + if (!isset($byParent[$parentId])) { $byParent[$parentId] = [ 'parent_id' => $parentId, 'firstname' => (string) ($row['firstname'] ?? ''), @@ -148,8 +150,8 @@ class FinancialUnpaidParentsService } } - $monthsUntil = static function (?\DateTimeImmutable $end) use ($today): int { - if (! $end) { + $monthsUntil = static function (? \DateTimeImmutable $end) use ($today): int { + if (!$end) { return 0; } $y = (int) $end->format('Y') - (int) $today->format('Y'); @@ -158,7 +160,6 @@ class FinancialUnpaidParentsService if ((int) $end->format('j') > (int) $today->format('j')) { $months += 1; } - return max(0, $months); }; @@ -194,7 +195,7 @@ class FinancialUnpaidParentsService $hasPayments = []; $paidTotals = []; $paymentCounts = []; - if (! empty($parentIds)) { + if (!empty($parentIds)) { $pRows = DB::table('payments') ->selectRaw('parent_id, SUM(paid_amount) AS total_paid, COUNT(*) AS payment_count') ->whereIn('parent_id', $parentIds) @@ -216,8 +217,7 @@ class FinancialUnpaidParentsService $results = array_map(function (array $row) use ($hasPayments, $paidTotals, $paymentCounts, $nextInstallment) { $parentId = (int) ($row['parent_id'] ?? 0); - $name = trim((string) ($row['firstname'] ?? '').' '.(string) ($row['lastname'] ?? '')); - + $name = trim((string) ($row['firstname'] ?? '') . ' ' . (string) ($row['lastname'] ?? '')); return [ 'parent_id' => $parentId, 'parent_name' => $name, diff --git a/app/Services/Finance/InstallmentPlanService.php b/app/Services/Finance/InstallmentPlanService.php index 216628aa..aa949ca9 100644 --- a/app/Services/Finance/InstallmentPlanService.php +++ b/app/Services/Finance/InstallmentPlanService.php @@ -7,33 +7,31 @@ use App\Models\InvoiceInstallmentPlan; use App\Models\PaymentInstallmentAllocation; use Carbon\Carbon; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Schema; class InstallmentPlanService { public function list(array $filters): array { $q = InvoiceInstallmentPlan::query(); - foreach (['invoice_id', 'parent_id', 'school_year', 'status'] as $field) { - if (! empty($filters[$field])) { - $q->where($field, $filters[$field]); - } + foreach (['invoice_id','parent_id','school_year','status'] as $field) { + if (!empty($filters[$field])) $q->where($field, $filters[$field]); } - - return ['rows' => $q->orderByDesc('id')->paginate((int) ($filters['per_page'] ?? 25))]; + return ['rows' => $q->orderByDesc('id')->paginate((int)($filters['per_page'] ?? 25))]; } public function createForInvoice(int $invoiceId, array $payload, ?int $actorId): array { return DB::transaction(function () use ($invoiceId, $payload, $actorId) { $invoice = DB::table('invoices')->where('id', $invoiceId)->first(); - abort_if(! $invoice, 404, 'Invoice not found.'); + abort_if(!$invoice, 404, 'Invoice not found.'); $total = (float) ($payload['total_amount'] ?? ($invoice->balance ?? $invoice->total_amount ?? 0)); $count = (int) $payload['number_of_installments']; $plan = InvoiceInstallmentPlan::query()->create([ 'invoice_id' => $invoiceId, 'parent_id' => $invoice->parent_id, 'school_year' => $invoice->school_year ?? null, - 'plan_name' => $payload['plan_name'] ?? ('Invoice #'.$invoiceId.' installment plan'), + 'plan_name' => $payload['plan_name'] ?? ('Invoice #' . $invoiceId . ' installment plan'), 'total_amount' => $total, 'number_of_installments' => $count, 'status' => 'draft', @@ -67,7 +65,6 @@ class InstallmentPlanService { $plan = InvoiceInstallmentPlan::query()->findOrFail($id)->toArray(); $plan['installments'] = InvoiceInstallment::query()->where('installment_plan_id', $id)->orderBy('sequence')->get()->toArray(); - return $plan; } @@ -76,7 +73,6 @@ class InstallmentPlanService $plan = InvoiceInstallmentPlan::query()->findOrFail($id); $plan->fill(['status' => 'active', 'approved_by' => $actorId])->save(); InvoiceInstallment::query()->where('installment_plan_id', $id)->where('status', 'pending')->update(['status' => 'due']); - return $this->show($id); } @@ -84,7 +80,6 @@ class InstallmentPlanService { InvoiceInstallmentPlan::query()->whereKey($id)->update(['status' => 'cancelled']); InvoiceInstallment::query()->where('installment_plan_id', $id)->whereNotIn('status', ['paid'])->update(['status' => 'cancelled']); - return $this->show($id); } @@ -92,16 +87,14 @@ class InstallmentPlanService { return DB::transaction(function () use ($paymentId, $payload) { $payment = DB::table('payments')->where('id', $paymentId)->first(); - abort_if(! $payment, 404, 'Payment not found.'); + abort_if(!$payment, 404, 'Payment not found.'); $allocations = $payload['installments'] ?? null; - if (! $allocations) { + if (!$allocations) { $remaining = (float) ($payment->paid_amount ?? 0); $installments = InvoiceInstallment::query()->where('invoice_id', $payment->invoice_id)->where('balance', '>', 0)->orderBy('due_date')->lockForUpdate()->get(); $allocations = []; foreach ($installments as $inst) { - if ($remaining <= 0) { - break; - } + if ($remaining <= 0) break; $amount = min($remaining, (float) $inst->balance); $allocations[] = ['installment_id' => $inst->id, 'amount' => $amount]; $remaining -= $amount; @@ -112,9 +105,7 @@ class InstallmentPlanService foreach ($allocations as $a) { $inst = InvoiceInstallment::query()->lockForUpdate()->findOrFail($a['installment_id']); $amount = min((float) $a['amount'], (float) $inst->balance); - if ($amount <= 0) { - continue; - } + if ($amount <= 0) continue; $created[] = PaymentInstallmentAllocation::query()->create([ 'payment_id' => $paymentId, 'invoice_id' => $inst->invoice_id, @@ -127,7 +118,6 @@ class InstallmentPlanService $inst->paid_at = $inst->balance <= 0 ? now() : null; $inst->save(); } - return ['allocations' => $created]; }); } @@ -136,12 +126,11 @@ class InstallmentPlanService { $q = InvoiceInstallment::query()->where('balance', '>', 0); $overdue ? $q->whereDate('due_date', '<', now()->toDateString()) : $q->whereDate('due_date', '>=', now()->toDateString()); - return ['rows' => $q->orderBy('due_date')->get()->toArray()]; } public function markOverdue(): int { - return InvoiceInstallment::query()->where('balance', '>', 0)->whereDate('due_date', '<', now()->toDateString())->whereNotIn('status', ['paid', 'cancelled', 'waived'])->update(['status' => 'overdue']); + return InvoiceInstallment::query()->where('balance', '>', 0)->whereDate('due_date', '<', now()->toDateString())->whereNotIn('status', ['paid','cancelled','waived'])->update(['status' => 'overdue']); } } diff --git a/app/Services/Finance/ParentPaymentFollowUpService.php b/app/Services/Finance/ParentPaymentFollowUpService.php index 630729bb..fc084be1 100644 --- a/app/Services/Finance/ParentPaymentFollowUpService.php +++ b/app/Services/Finance/ParentPaymentFollowUpService.php @@ -4,7 +4,6 @@ namespace App\Services\Finance; use App\Models\Configuration; use App\Models\FinanceFollowUpNote; -use Carbon\Carbon; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Schema; @@ -36,11 +35,11 @@ class ParentPaymentFollowUpService $discounted = (float) ($discountByInvoice[$invoiceId] ?? 0); $refunded = (float) ($refundByInvoice[$invoiceId] ?? 0); $open = max(0, $total - $paid - $discounted - $refunded); - if (! $includePaid && $open <= 0) { + if (!$includePaid && $open <= 0) { continue; } - if (! isset($parents[$parentId])) { + if (!isset($parents[$parentId])) { $parents[$parentId] = [ 'parent_id' => $parentId, 'parent_name' => $invoice->parent_name ?? null, @@ -72,7 +71,7 @@ class ParentPaymentFollowUpService ]; } - $row = &$parents[$parentId]; + $row =& $parents[$parentId]; $row['invoice_count']++; $row['total_invoiced'] += $total; $row['total_paid'] += $paid; @@ -100,7 +99,7 @@ class ParentPaymentFollowUpService $row['promise_to_pay_amount'] = $note->promise_to_pay_amount !== null ? (float) $note->promise_to_pay_amount : null; $row['escalation_level'] = (int) ($note->escalation_level ?? 0); } - $row['notes_count'] = FinanceFollowUpNote::query()->where('parent_id', $parentId)->when($schoolYear, fn ($q) => $q->where('school_year', $schoolYear))->count(); + $row['notes_count'] = FinanceFollowUpNote::query()->where('parent_id', $parentId)->when($schoolYear, fn($q) => $q->where('school_year', $schoolYear))->count(); $row['days_overdue'] = $this->daysOverdue($row['oldest_unpaid_invoice_date']); $row['aging_bucket'] = $this->agingBucket($row['days_overdue']); $row['risk_flags'] = $this->riskFlags($row); @@ -108,26 +107,15 @@ class ParentPaymentFollowUpService unset($row); $rows = array_values(array_filter($parents, function (array $row) use ($filters, $minimumBalance) { - if ($row['open_balance'] < $minimumBalance) { - return false; - } - if (! empty($filters['aging_bucket']) && $row['aging_bucket'] !== $filters['aging_bucket']) { - return false; - } - if (! empty($filters['follow_up_status']) && $row['follow_up_status'] !== $filters['follow_up_status']) { - return false; - } - if (! empty($filters['next_follow_up_due_before']) && (empty($row['next_follow_up_date']) || strtotime($row['next_follow_up_date']) > strtotime($filters['next_follow_up_due_before']))) { - return false; - } - if (! empty($filters['promise_to_pay_due_before']) && (empty($row['promise_to_pay_date']) || strtotime($row['promise_to_pay_date']) > strtotime($filters['promise_to_pay_due_before']))) { - return false; - } - + if ($row['open_balance'] < $minimumBalance) return false; + if (!empty($filters['aging_bucket']) && $row['aging_bucket'] !== $filters['aging_bucket']) return false; + if (!empty($filters['follow_up_status']) && $row['follow_up_status'] !== $filters['follow_up_status']) return false; + if (!empty($filters['next_follow_up_due_before']) && (empty($row['next_follow_up_date']) || strtotime($row['next_follow_up_date']) > strtotime($filters['next_follow_up_due_before']))) return false; + if (!empty($filters['promise_to_pay_due_before']) && (empty($row['promise_to_pay_date']) || strtotime($row['promise_to_pay_date']) > strtotime($filters['promise_to_pay_due_before']))) return false; return true; })); - usort($rows, fn ($a, $b) => [$b['open_balance'], $b['days_overdue']] <=> [$a['open_balance'], $a['days_overdue']]); + usort($rows, fn($a, $b) => [$b['open_balance'], $b['days_overdue']] <=> [$a['open_balance'], $a['days_overdue']]); return ['schoolYear' => $schoolYear, 'generatedAt' => now()->toISOString(), 'results' => $rows]; } @@ -153,78 +141,56 @@ class ParentPaymentFollowUpService public function csvRows(array $report): array { - $rows = [['Parent ID', 'Parent', 'Email', 'Phone', 'Students', 'School Year', 'Invoices', 'Open Invoices', 'Total Invoiced', 'Total Paid', 'Open Balance', 'Oldest Unpaid', 'Days Overdue', 'Aging', 'Status', 'Next Follow-Up', 'Promise Date', 'Promise Amount', 'Risk Flags']]; + $rows = [['Parent ID','Parent','Email','Phone','Students','School Year','Invoices','Open Invoices','Total Invoiced','Total Paid','Open Balance','Oldest Unpaid','Days Overdue','Aging','Status','Next Follow-Up','Promise Date','Promise Amount','Risk Flags']]; foreach ($report['results'] as $r) { $rows[] = [ $r['parent_id'], $r['parent_name'], $r['parent_email'], $r['parent_phone'], implode('; ', $r['student_names'] ?? []), $r['school_year'], $r['invoice_count'], $r['open_invoice_count'], $r['total_invoiced'], $r['total_paid'], $r['open_balance'], $r['oldest_unpaid_invoice_date'], $r['days_overdue'], $r['aging_bucket'], $r['follow_up_status'], $r['next_follow_up_date'], $r['promise_to_pay_date'], $r['promise_to_pay_amount'], implode('; ', $r['risk_flags'] ?? []), ]; } - return $rows; } private function invoiceRows(string $schoolYear, array $filters) { $q = DB::table('invoices')->where('invoices.school_year', $schoolYear); - if (! empty($filters['parent_id'])) { - $q->where('invoices.parent_id', $filters['parent_id']); - } - if (! empty($filters['date_from'])) { - $q->whereDate('invoices.created_at', '>=', $filters['date_from']); - } - if (! empty($filters['date_to'])) { - $q->whereDate('invoices.created_at', '<=', $filters['date_to']); - } + if (!empty($filters['parent_id'])) $q->where('invoices.parent_id', $filters['parent_id']); + if (!empty($filters['date_from'])) $q->whereDate('invoices.created_at', '>=', $filters['date_from']); + if (!empty($filters['date_to'])) $q->whereDate('invoices.created_at', '<=', $filters['date_to']); if (Schema::hasTable('users')) { $name = $this->nameExpression('users'); $q->leftJoin('users', 'users.id', '=', 'invoices.parent_id') - ->addSelect('invoices.*') - ->addSelect(DB::raw($name.' as parent_name')) - ->addSelect('users.email as parent_email'); - if (Schema::hasColumn('users', 'phone')) { - $q->addSelect('users.phone as parent_phone'); - } else { - $q->addSelect(DB::raw('NULL as parent_phone')); - } + ->addSelect('invoices.*') + ->addSelect(DB::raw($name . ' as parent_name')) + ->addSelect('users.email as parent_email'); + if (Schema::hasColumn('users', 'phone')) $q->addSelect('users.phone as parent_phone'); + else $q->addSelect(DB::raw('NULL as parent_phone')); } else { $q->select('invoices.*', DB::raw('NULL as parent_name'), DB::raw('NULL as parent_email'), DB::raw('NULL as parent_phone')); } - return $q->get(); } private function paymentTotalsByInvoice(string $schoolYear): array { - if (! Schema::hasTable('payments')) { - return []; - } - + if (!Schema::hasTable('payments')) return []; return DB::table('payments')->select('invoice_id', DB::raw('COALESCE(SUM(paid_amount),0) total'), DB::raw('MAX(payment_date) last_payment_date')) - ->where('school_year', $schoolYear)->whereNotIn('status', ['void', 'voided', 'failed', 'cancelled', 'canceled', 'reversed']) - ->groupBy('invoice_id')->pluck('total', 'invoice_id')->map(fn ($v) => (float) $v)->all(); + ->where('school_year', $schoolYear)->whereNotIn('status', ['void','voided','failed','cancelled','canceled','reversed']) + ->groupBy('invoice_id')->pluck('total', 'invoice_id')->map(fn($v) => (float) $v)->all(); } private function discountTotalsByInvoice(): array { - if (! Schema::hasTable('discount_usages')) { - return []; - } + if (!Schema::hasTable('discount_usages')) return []; $col = Schema::hasColumn('discount_usages', 'discount_amount') ? 'discount_amount' : 'amount'; - - return DB::table('discount_usages')->select('invoice_id', DB::raw('COALESCE(SUM('.$col.'),0) total'))->groupBy('invoice_id')->pluck('total', 'invoice_id')->map(fn ($v) => (float) $v)->all(); + return DB::table('discount_usages')->select('invoice_id', DB::raw('COALESCE(SUM('.$col.'),0) total'))->groupBy('invoice_id')->pluck('total','invoice_id')->map(fn($v)=>(float)$v)->all(); } private function refundTotalsByInvoice(): array { - if (! Schema::hasTable('refunds')) { - return []; - } + if (!Schema::hasTable('refunds')) return []; $col = Schema::hasColumn('refunds', 'refund_amount') ? 'refund_amount' : (Schema::hasColumn('refunds', 'amount') ? 'amount' : null); - if (! $col) { - return []; - } - - return DB::table('refunds')->select('invoice_id', DB::raw('COALESCE(SUM('.$col.'),0) total'))->whereNotIn('status', ['void', 'voided', 'cancelled', 'canceled'])->groupBy('invoice_id')->pluck('total', 'invoice_id')->map(fn ($v) => (float) $v)->all(); + if (!$col) return []; + return DB::table('refunds')->select('invoice_id', DB::raw('COALESCE(SUM('.$col.'),0) total'))->whereNotIn('status', ['void','voided','cancelled','canceled'])->groupBy('invoice_id')->pluck('total','invoice_id')->map(fn($v)=>(float)$v)->all(); } private function latestNotesByParent(string $schoolYear): array @@ -234,66 +200,40 @@ class ParentPaymentFollowUpService private function studentsByParent(): array { - if (! Schema::hasTable('students') || ! Schema::hasColumn('students', 'parent_id')) { - return []; - } + if (!Schema::hasTable('students') || !Schema::hasColumn('students', 'parent_id')) return []; $name = $this->nameExpression('students'); - - return DB::table('students')->select('parent_id', DB::raw($name.' as student_name'))->whereNotNull('parent_id')->get()->groupBy('parent_id')->map(fn ($rows) => $rows->pluck('student_name')->filter()->values()->all())->all(); + return DB::table('students')->select('parent_id', DB::raw($name.' as student_name'))->whereNotNull('parent_id')->get()->groupBy('parent_id')->map(fn($rows) => $rows->pluck('student_name')->filter()->values()->all())->all(); } private function nameExpression(string $table): string { - if (Schema::hasColumn($table, 'name')) { - return $table.'.name'; - } - if (Schema::hasColumn($table, 'full_name')) { - return $table.'.full_name'; - } + if (Schema::hasColumn($table, 'name')) return $table.'.name'; + if (Schema::hasColumn($table, 'full_name')) return $table.'.full_name'; $driver = DB::connection()->getDriverName(); $concat = $driver === 'sqlite' ? '%s || \' \' || %s' : "CONCAT(%s, ' ', %s)"; - if (Schema::hasColumn($table, 'firstname') && Schema::hasColumn($table, 'lastname')) { - return sprintf($concat, $table.'.firstname', $table.'.lastname'); - } - if (Schema::hasColumn($table, 'first_name') && Schema::hasColumn($table, 'last_name')) { - return sprintf($concat, $table.'.first_name', $table.'.last_name'); - } - + if (Schema::hasColumn($table, 'firstname') && Schema::hasColumn($table, 'lastname')) return sprintf($concat, $table.'.firstname', $table.'.lastname'); + if (Schema::hasColumn($table, 'first_name') && Schema::hasColumn($table, 'last_name')) return sprintf($concat, $table.'.first_name', $table.'.last_name'); return "CAST($table.id AS CHAR)"; } private function daysOverdue(?string $date): int { - if (! $date) { - return 0; - } - - return max(0, now()->startOfDay()->diffInDays(Carbon::parse($date)->startOfDay(), false) * -1); + if (!$date) return 0; + return max(0, now()->startOfDay()->diffInDays(\Carbon\Carbon::parse($date)->startOfDay(), false) * -1); } private function agingBucket(int $days): string { - return match (true) { - $days <= 0 => 'current', $days <= 30 => '1-30 days overdue', $days <= 60 => '31-60 days overdue', $days <= 90 => '61-90 days overdue', default => '90+ days overdue' - }; + return match (true) { $days <= 0 => 'current', $days <= 30 => '1-30 days overdue', $days <= 60 => '31-60 days overdue', $days <= 90 => '61-90 days overdue', default => '90+ days overdue' }; } private function riskFlags(array $row): array { $flags = []; - if ($row['days_overdue'] > 90) { - $flags[] = '90+ days overdue'; - } - if ($row['open_balance'] > 1000) { - $flags[] = 'high balance'; - } - if ($row['promise_to_pay_date'] && strtotime($row['promise_to_pay_date']) < strtotime(date('Y-m-d'))) { - $flags[] = 'missed promise-to-pay'; - } - if (! $row['parent_email'] && ! $row['parent_phone']) { - $flags[] = 'missing contact info'; - } - + if ($row['days_overdue'] > 90) $flags[] = '90+ days overdue'; + if ($row['open_balance'] > 1000) $flags[] = 'high balance'; + if ($row['promise_to_pay_date'] && strtotime($row['promise_to_pay_date']) < strtotime(date('Y-m-d'))) $flags[] = 'missed promise-to-pay'; + if (!$row['parent_email'] && !$row['parent_phone']) $flags[] = 'missing contact info'; return $flags; } } diff --git a/app/Services/Finance/PriorYearBalanceCarryforwardService.php b/app/Services/Finance/PriorYearBalanceCarryforwardService.php index e9ead37f..66b4a53b 100644 --- a/app/Services/Finance/PriorYearBalanceCarryforwardService.php +++ b/app/Services/Finance/PriorYearBalanceCarryforwardService.php @@ -20,20 +20,16 @@ class PriorYearBalanceCarryforwardService $refunds = $this->refundTotals(); $q = DB::table('invoices')->where('school_year', $from); - if ($parentId) { - $q->where('parent_id', $parentId); - } + if ($parentId) $q->where('parent_id', $parentId); $invoices = $q->get(); $byParent = []; foreach ($invoices as $invoice) { $invoiceId = (int) $invoice->id; $balance = max(0, (float) ($invoice->total_amount ?? 0) - (float) ($payments[$invoiceId] ?? 0) - (float) ($discounts[$invoiceId] ?? 0) - (float) ($refunds[$invoiceId] ?? 0)); - if ($balance <= 0) { - continue; - } + if ($balance <= 0) continue; $pid = (int) $invoice->parent_id; - if (! isset($byParent[$pid])) { + if (!isset($byParent[$pid])) { $existing = FinanceBalanceCarryforward::query()->where('parent_id', $pid)->where('from_school_year', $from)->where('to_school_year', $to)->first(); $byParent[$pid] = [ 'parent_id' => $pid, @@ -45,17 +41,15 @@ class PriorYearBalanceCarryforwardService 'existing_carryforward_id' => $existing?->id, 'warnings' => [], ]; - if ($existing) { - $byParent[$pid]['warnings'][] = 'source invoice already carried forward'; - } + if ($existing) $byParent[$pid]['warnings'][] = 'source invoice already carried forward'; } $byParent[$pid]['source_invoice_ids'][] = $invoiceId; $byParent[$pid]['source_balance_amount'] += $balance; $byParent[$pid]['proposed_carryforward_amount'] += $balance; } - $rows = array_values(array_filter($byParent, fn ($r) => $r['source_balance_amount'] >= $minimum)); - usort($rows, fn ($a, $b) => $b['source_balance_amount'] <=> $a['source_balance_amount']); + $rows = array_values(array_filter($byParent, fn($r) => $r['source_balance_amount'] >= $minimum)); + usort($rows, fn($a, $b) => $b['source_balance_amount'] <=> $a['source_balance_amount']); return ['fromSchoolYear' => $from, 'toSchoolYear' => $to, 'generatedAt' => now()->toISOString(), 'rows' => $rows]; } @@ -65,9 +59,7 @@ class PriorYearBalanceCarryforwardService $preview = $this->preview($filters); $created = []; foreach ($preview['rows'] as $row) { - if (! empty($row['existing_carryforward_id'])) { - continue; - } + if (!empty($row['existing_carryforward_id'])) continue; $created[] = FinanceBalanceCarryforward::query()->create([ 'parent_id' => $row['parent_id'], 'from_school_year' => $row['from_school_year'], @@ -82,7 +74,6 @@ class PriorYearBalanceCarryforwardService 'metadata_json' => ['warnings' => $row['warnings']], ])->toArray(); } - return ['created' => $created, 'skipped' => count($preview['rows']) - count($created)]; } @@ -90,7 +81,6 @@ class PriorYearBalanceCarryforwardService { $cf = FinanceBalanceCarryforward::query()->findOrFail($id); $cf->fill(['status' => 'approved', 'approved_by' => $actorId, 'approved_at' => now()])->save(); - return $cf->fresh()->toArray(); } @@ -104,7 +94,6 @@ class PriorYearBalanceCarryforwardService $cf->reason = $reason ?: $cf->reason; $cf->approved_by = $cf->approved_by ?: $actorId; $cf->save(); - return $cf->fresh()->toArray(); } @@ -115,7 +104,6 @@ class PriorYearBalanceCarryforwardService $cf->remaining_amount = max(0, (float) $cf->carryforward_amount - (float) $cf->waived_amount + $amount); $cf->reason = $reason ?: $cf->reason; $cf->save(); - return $cf->fresh()->toArray(); } @@ -123,7 +111,7 @@ class PriorYearBalanceCarryforwardService { return DB::transaction(function () use ($id, $actorId) { $cf = FinanceBalanceCarryforward::query()->lockForUpdate()->findOrFail($id); - if (! in_array($cf->status, ['approved', 'draft', 'pending_review'], true)) { + if (!in_array($cf->status, ['approved','draft','pending_review'], true)) { return $cf->toArray() + ['warning' => 'Carryforward is not in a postable status.']; } @@ -132,14 +120,14 @@ class PriorYearBalanceCarryforwardService if (Schema::hasTable('invoices')) { $invoiceId = DB::table('invoices')->insertGetId([ 'parent_id' => $cf->parent_id, - 'invoice_number' => 'CF-'.$cf->to_school_year.'-'.$cf->id, + 'invoice_number' => 'CF-' . $cf->to_school_year . '-' . $cf->id, 'total_amount' => $amount, 'balance' => $amount, 'paid_amount' => 0, 'school_year' => $cf->to_school_year, 'issue_date' => now()->toDateString(), 'status' => $amount > 0 ? 'unpaid' : 'paid', - 'description' => 'Previous year balance from '.$cf->from_school_year, + 'description' => 'Previous year balance from ' . $cf->from_school_year, 'created_at' => now(), 'updated_at' => now(), 'updated_by' => $actorId, @@ -149,7 +137,6 @@ class PriorYearBalanceCarryforwardService $cf->status = 'posted_to_new_year'; $cf->remaining_amount = $amount; $cf->save(); - return $cf->fresh()->toArray(); }); } @@ -157,54 +144,37 @@ class PriorYearBalanceCarryforwardService public function report(array $filters): array { $q = FinanceBalanceCarryforward::query(); - foreach (['from_school_year', 'to_school_year', 'parent_id', 'status'] as $field) { - if (! empty($filters[$field])) { - $q->where($field, $filters[$field]); - } + foreach (['from_school_year','to_school_year','parent_id','status'] as $field) { + if (!empty($filters[$field])) $q->where($field, $filters[$field]); } - return ['generatedAt' => now()->toISOString(), 'rows' => $q->orderByDesc('remaining_amount')->get()->toArray()]; } public function csvRows(array $report): array { - $rows = [['ID', 'Parent ID', 'From Year', 'To Year', 'Source Invoices', 'Source Balance', 'Carryforward', 'Waived', 'Adjusted', 'Remaining', 'Status', 'Posted Invoice']]; + $rows = [['ID','Parent ID','From Year','To Year','Source Invoices','Source Balance','Carryforward','Waived','Adjusted','Remaining','Status','Posted Invoice']]; foreach ($report['rows'] as $r) { - $rows[] = [$r['id'], $r['parent_id'], $r['from_school_year'], $r['to_school_year'], json_encode($r['source_invoice_ids_json'] ?? []), $r['source_balance_amount'], $r['carryforward_amount'], $r['waived_amount'], $r['adjusted_amount'], $r['remaining_amount'], $r['status'], $r['posted_invoice_id'] ?? null]; + $rows[] = [$r['id'],$r['parent_id'],$r['from_school_year'],$r['to_school_year'],json_encode($r['source_invoice_ids_json'] ?? []),$r['source_balance_amount'],$r['carryforward_amount'],$r['waived_amount'],$r['adjusted_amount'],$r['remaining_amount'],$r['status'],$r['posted_invoice_id'] ?? null]; } - return $rows; } private function paymentTotals(string $schoolYear): array { - if (! Schema::hasTable('payments')) { - return []; - } - - return DB::table('payments')->where('school_year', $schoolYear)->whereNotIn('status', ['void', 'voided', 'failed', 'cancelled', 'canceled', 'reversed'])->select('invoice_id', DB::raw('COALESCE(SUM(paid_amount),0) total'))->groupBy('invoice_id')->pluck('total', 'invoice_id')->map(fn ($v) => (float) $v)->all(); + if (!Schema::hasTable('payments')) return []; + return DB::table('payments')->where('school_year', $schoolYear)->whereNotIn('status', ['void','voided','failed','cancelled','canceled','reversed'])->select('invoice_id', DB::raw('COALESCE(SUM(paid_amount),0) total'))->groupBy('invoice_id')->pluck('total','invoice_id')->map(fn($v)=>(float)$v)->all(); } - private function discountTotals(): array { - if (! Schema::hasTable('discount_usages')) { - return []; - } - $col = Schema::hasColumn('discount_usages', 'discount_amount') ? 'discount_amount' : 'amount'; - - return DB::table('discount_usages')->select('invoice_id', DB::raw('COALESCE(SUM('.$col.'),0) total'))->groupBy('invoice_id')->pluck('total', 'invoice_id')->map(fn ($v) => (float) $v)->all(); + if (!Schema::hasTable('discount_usages')) return []; + $col = Schema::hasColumn('discount_usages','discount_amount') ? 'discount_amount' : 'amount'; + return DB::table('discount_usages')->select('invoice_id', DB::raw('COALESCE(SUM('.$col.'),0) total'))->groupBy('invoice_id')->pluck('total','invoice_id')->map(fn($v)=>(float)$v)->all(); } - private function refundTotals(): array { - if (! Schema::hasTable('refunds')) { - return []; - } - $col = Schema::hasColumn('refunds', 'refund_amount') ? 'refund_amount' : (Schema::hasColumn('refunds', 'amount') ? 'amount' : null); - if (! $col) { - return []; - } - - return DB::table('refunds')->whereNotIn('status', ['void', 'voided', 'cancelled', 'canceled'])->select('invoice_id', DB::raw('COALESCE(SUM('.$col.'),0) total'))->groupBy('invoice_id')->pluck('total', 'invoice_id')->map(fn ($v) => (float) $v)->all(); + if (!Schema::hasTable('refunds')) return []; + $col = Schema::hasColumn('refunds','refund_amount') ? 'refund_amount' : (Schema::hasColumn('refunds','amount') ? 'amount' : null); + if (!$col) return []; + return DB::table('refunds')->whereNotIn('status', ['void','voided','cancelled','canceled'])->select('invoice_id', DB::raw('COALESCE(SUM('.$col.'),0) total'))->groupBy('invoice_id')->pluck('total','invoice_id')->map(fn($v)=>(float)$v)->all(); } } diff --git a/app/Services/Finance/StakeholderFinancialAnalysisService.php b/app/Services/Finance/StakeholderFinancialAnalysisService.php index 98f9c754..ab9415ef 100644 --- a/app/Services/Finance/StakeholderFinancialAnalysisService.php +++ b/app/Services/Finance/StakeholderFinancialAnalysisService.php @@ -3,7 +3,6 @@ namespace App\Services\Finance; use App\Models\Configuration; -use Illuminate\Support\Carbon; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Schema; @@ -22,7 +21,7 @@ class StakeholderFinancialAnalysisService $current = $this->periodMetrics($schoolYear, $rangeFrom, $rangeTo); $comparison = null; - if (! empty($compareSchoolYear)) { + if (!empty($compareSchoolYear)) { [$compareFrom, $compareTo] = $this->resolveDateRange($compareSchoolYear, null, null); $comparison = $this->periodMetrics($compareSchoolYear, $compareFrom, $compareTo); } @@ -71,7 +70,6 @@ class StakeholderFinancialAnalysisService foreach ($value as $bucket => $amount) { $rows[] = ['Receivables Aging', $bucket, $amount]; } - continue; } $rows[] = ['Receivables', $key, is_scalar($value) ? $value : json_encode($value)]; @@ -83,15 +81,14 @@ class StakeholderFinancialAnalysisService $rows[] = ['Payment Method Mix', $method, $amount]; } foreach (($analysis['monthlyTrend'] ?? []) as $month => $data) { - $rows[] = ['Monthly Trend', $month.' charges', $data['charges'] ?? 0]; - $rows[] = ['Monthly Trend', $month.' collections', $data['collections'] ?? 0]; - $rows[] = ['Monthly Trend', $month.' expenses', $data['expenses'] ?? 0]; - $rows[] = ['Monthly Trend', $month.' netCash', $data['netCash'] ?? 0]; + $rows[] = ['Monthly Trend', $month . ' charges', $data['charges'] ?? 0]; + $rows[] = ['Monthly Trend', $month . ' collections', $data['collections'] ?? 0]; + $rows[] = ['Monthly Trend', $month . ' expenses', $data['expenses'] ?? 0]; + $rows[] = ['Monthly Trend', $month . ' netCash', $data['netCash'] ?? 0]; } foreach (($analysis['riskFlags'] ?? []) as $flag) { $rows[] = ['Risk Flag', $flag['level'] ?? '', $flag['message'] ?? '']; } - return $rows; } @@ -172,19 +169,18 @@ class StakeholderFinancialAnalysisService private function invoiceTotals(string $schoolYear, ?string $dateFrom, ?string $dateTo): array { - if (! Schema::hasTable('invoices')) { + if (!Schema::hasTable('invoices')) { return ['total' => 0.0, 'count' => 0]; } $q = DB::table('invoices')->where('school_year', $schoolYear); $this->applyDateRange($q, $this->dateColumn('invoices', ['issue_date', 'created_at']), $dateFrom, $dateTo); $row = (array) $q->selectRaw('COALESCE(SUM(total_amount),0) as total, COUNT(*) as count')->first(); - return ['total' => (float) ($row['total'] ?? 0), 'count' => (int) ($row['count'] ?? 0)]; } private function paymentTotals(string $schoolYear, ?string $dateFrom, ?string $dateTo): array { - if (! Schema::hasTable('payments')) { + if (!Schema::hasTable('payments')) { return ['total' => 0.0, 'familyCount' => 0, 'byMethod' => []]; } $base = DB::table('payments')->where('school_year', $schoolYear); @@ -202,13 +198,12 @@ class StakeholderFinancialAnalysisService $arr = (array) $row; $byMethod[(string) ($arr['method'] ?? 'unknown')] = (float) ($arr['total'] ?? 0); } - return ['total' => $total, 'familyCount' => $familyCount, 'byMethod' => $byMethod]; } private function refundTotals(string $schoolYear, ?string $dateFrom, ?string $dateTo): array { - if (! Schema::hasTable('refunds')) { + if (!Schema::hasTable('refunds')) { return ['total' => 0.0]; } $q = DB::table('refunds')->where('school_year', $schoolYear); @@ -217,14 +212,13 @@ class StakeholderFinancialAnalysisService } $this->applyDateRange($q, $this->dateColumn('refunds', ['refunded_at', 'approved_at', 'created_at']), $dateFrom, $dateTo); $amountColumn = Schema::hasColumn('refunds', 'refund_paid_amount') ? 'refund_paid_amount' : 'refund_amount'; - $row = (array) $q->selectRaw('COALESCE(SUM('.$amountColumn.'),0) as total')->first(); - + $row = (array) $q->selectRaw('COALESCE(SUM(' . $amountColumn . '),0) as total')->first(); return ['total' => (float) ($row['total'] ?? 0)]; } private function discountTotals(string $schoolYear, ?string $dateFrom, ?string $dateTo): array { - if (! Schema::hasTable('discount_usages')) { + if (!Schema::hasTable('discount_usages')) { return ['total' => 0.0]; } $q = DB::table('discount_usages as du'); @@ -234,21 +228,19 @@ class StakeholderFinancialAnalysisService $this->excludeStatuses($q, 'discount_usages', $this->excludedChargeStatuses, 'du.status'); $this->applyDateRange($q, Schema::hasColumn('discount_usages', 'created_at') ? 'du.created_at' : null, $dateFrom, $dateTo); $amountColumn = Schema::hasColumn('discount_usages', 'discount_amount') ? 'discount_amount' : 'amount'; - $row = (array) $q->selectRaw('COALESCE(SUM(du.'.$amountColumn.'),0) as total')->first(); - + $row = (array) $q->selectRaw('COALESCE(SUM(du.' . $amountColumn . '),0) as total')->first(); return ['total' => (float) ($row['total'] ?? 0)]; } private function additionalChargeTotals(string $schoolYear, ?string $dateFrom, ?string $dateTo): array { - if (! Schema::hasTable('additional_charges')) { + if (!Schema::hasTable('additional_charges')) { return ['total' => 0.0]; } $q = DB::table('additional_charges')->where('school_year', $schoolYear); $this->excludeStatuses($q, 'additional_charges', $this->excludedChargeStatuses); $this->applyDateRange($q, $this->dateColumn('additional_charges', ['due_date', 'created_at']), $dateFrom, $dateTo); $row = (array) $q->selectRaw('COALESCE(SUM(amount),0) as total')->first(); - return ['total' => (float) ($row['total'] ?? 0)]; } @@ -268,21 +260,20 @@ class StakeholderFinancialAnalysisService return ['total' => 0.0]; } $this->applyDateRange($q, $this->dateColumn($table, ['charge_date', 'created_at']), $dateFrom, $dateTo); - $row = (array) $q->selectRaw('COALESCE(SUM('.$amountColumn.'),0) as total')->first(); - + $row = (array) $q->selectRaw('COALESCE(SUM(' . $amountColumn . '),0) as total')->first(); return ['total' => (float) ($row['total'] ?? 0)]; } private function expenseTotals(string $schoolYear, ?string $dateFrom, ?string $dateTo): array { - if (! Schema::hasTable('expenses')) { + if (!Schema::hasTable('expenses')) { return ['total' => 0.0, 'byCategory' => []]; } $q = DB::table('expenses')->where('school_year', $schoolYear); $this->excludeStatuses($q, 'expenses', ['void', 'voided', 'rejected', 'cancelled', 'canceled']); $this->applyDateRange($q, $this->dateColumn('expenses', ['expense_date', 'date', 'created_at']), $dateFrom, $dateTo); $amountColumn = Schema::hasColumn('expenses', 'amount') ? 'amount' : 'total_amount'; - $total = (float) ((array) (clone $q)->selectRaw('COALESCE(SUM('.$amountColumn.'),0) as total')->first())['total']; + $total = (float) ((array) (clone $q)->selectRaw('COALESCE(SUM(' . $amountColumn . '),0) as total')->first())['total']; $categoryColumn = Schema::hasColumn('expenses', 'category') ? 'category' : (Schema::hasColumn('expenses', 'expense_category') ? 'expense_category' : null); $byCategory = []; if ($categoryColumn) { @@ -291,13 +282,12 @@ class StakeholderFinancialAnalysisService $byCategory[(string) ($arr['category'] ?? 'uncategorized')] = (float) ($arr['total'] ?? 0); } } - return ['total' => $total, 'byCategory' => $byCategory]; } private function reimbursementTotals(string $schoolYear, ?string $dateFrom, ?string $dateTo): array { - if (! Schema::hasTable('reimbursements')) { + if (!Schema::hasTable('reimbursements')) { return ['total' => 0.0]; } $q = DB::table('reimbursements')->where('school_year', $schoolYear); @@ -306,8 +296,7 @@ class StakeholderFinancialAnalysisService } $this->applyDateRange($q, $this->dateColumn('reimbursements', ['reimbursed_at', 'created_at']), $dateFrom, $dateTo); $amountColumn = Schema::hasColumn('reimbursements', 'amount') ? 'amount' : (Schema::hasColumn('reimbursements', 'total_amount') ? 'total_amount' : 'reimbursement_amount'); - $row = (array) $q->selectRaw('COALESCE(SUM('.$amountColumn.'),0) as total')->first(); - + $row = (array) $q->selectRaw('COALESCE(SUM(' . $amountColumn . '),0) as total')->first(); return ['total' => (float) ($row['total'] ?? 0)]; } @@ -315,7 +304,7 @@ class StakeholderFinancialAnalysisService { $buckets = ['current' => 0.0, '1_30_days' => 0.0, '31_60_days' => 0.0, '61_90_days' => 0.0, 'over_90_days' => 0.0]; $largest = []; - if (! Schema::hasTable('invoices')) { + if (!Schema::hasTable('invoices')) { return ['total' => 0.0, 'buckets' => $buckets, 'largestOpenInvoices' => []]; } $q = DB::table('invoices')->where('school_year', $schoolYear); @@ -338,8 +327,8 @@ class StakeholderFinancialAnalysisService $dueRaw = $arr['due_date'] ?? $arr['created_at'] ?? null; $days = 0; try { - if (! empty($dueRaw)) { - $days = max(0, (int) Carbon::parse($dueRaw)->startOfDay()->diffInDays($today, false)); + if (!empty($dueRaw)) { + $days = max(0, (int) \Illuminate\Support\Carbon::parse($dueRaw)->startOfDay()->diffInDays($today, false)); } } catch (\Throwable $e) { $days = 0; @@ -355,7 +344,6 @@ class StakeholderFinancialAnalysisService ]; } usort($largest, fn ($a, $b) => (float) $b['balance'] <=> (float) $a['balance']); - return ['total' => $total, 'buckets' => $buckets, 'largestOpenInvoices' => array_slice($largest, 0, 10)]; } @@ -379,13 +367,12 @@ class StakeholderFinancialAnalysisService ]; } ksort($months); - return $months; } private function monthlySum(string $table, string $amountColumn, string $schoolYear, ?string $dateColumn, ?string $dateFrom, ?string $dateTo, array $excludedStatuses = []): array { - if (! Schema::hasTable($table) || ! Schema::hasColumn($table, $amountColumn) || $dateColumn === null) { + if (!Schema::hasTable($table) || !Schema::hasColumn($table, $amountColumn) || $dateColumn === null) { return []; } $q = DB::table($table); @@ -398,18 +385,17 @@ class StakeholderFinancialAnalysisService $monthExpr = $driver === 'sqlite' ? "strftime('%Y-%m', $dateColumn)" : "DATE_FORMAT($dateColumn, '%Y-%m')"; - $rows = $q->selectRaw($monthExpr.' as month, COALESCE(SUM('.$amountColumn.'),0) as total') + $rows = $q->selectRaw($monthExpr . ' as month, COALESCE(SUM(' . $amountColumn . '),0) as total') ->groupBy('month') ->orderBy('month') ->get(); $out = []; foreach ($rows as $row) { $arr = (array) $row; - if (! empty($arr['month'])) { + if (!empty($arr['month'])) { $out[(string) $arr['month']] = (float) ($arr['total'] ?? 0); } } - return $out; } @@ -424,7 +410,6 @@ class StakeholderFinancialAnalysisService if ($comparison) { $summary['comparison'] = $this->deltas($current['keyMetrics'], $comparison['keyMetrics']); } - return $summary; } @@ -446,7 +431,6 @@ class StakeholderFinancialAnalysisService if (empty($flags)) { $flags[] = ['level' => 'ok', 'message' => 'No automatic risk flags were triggered for the selected period.']; } - return $flags; } @@ -464,17 +448,15 @@ class StakeholderFinancialAnalysisService 'changePct' => $prev != 0.0 ? $this->percent((($cur - $prev) / abs($prev)) * 100) : null, ]; } - return $out; } private function resolveDateRange(string $schoolYear, ?string $dateFrom, ?string $dateTo): array { - if ((! $dateFrom || ! $dateTo) && preg_match('/^(\d{4})\D?(\d{4})$/', $schoolYear, $m)) { - $dateFrom = $dateFrom ?: $m[1].'-01-01'; - $dateTo = $dateTo ?: $m[2].'-12-31'; + if ((!$dateFrom || !$dateTo) && preg_match('/^(\d{4})\D?(\d{4})$/', $schoolYear, $m)) { + $dateFrom = $dateFrom ?: $m[1] . '-01-01'; + $dateTo = $dateTo ?: $m[2] . '-12-31'; } - return [$dateFrom, $dateTo]; } @@ -483,17 +465,17 @@ class StakeholderFinancialAnalysisService if ($column === null) { return; } - if (! empty($dateFrom)) { - $query->whereRaw('DATE('.$column.') >= ?', [$dateFrom]); + if (!empty($dateFrom)) { + $query->whereRaw('DATE(' . $column . ') >= ?', [$dateFrom]); } - if (! empty($dateTo)) { - $query->whereRaw('DATE('.$column.') <= ?', [$dateTo]); + if (!empty($dateTo)) { + $query->whereRaw('DATE(' . $column . ') <= ?', [$dateTo]); } } private function excludeStatuses($query, string $table, array $statuses, ?string $qualifiedColumn = null): void { - if (empty($statuses) || ! Schema::hasColumn($table, 'status')) { + if (empty($statuses) || !Schema::hasColumn($table, 'status')) { return; } $column = $qualifiedColumn ?: 'status'; @@ -504,7 +486,7 @@ class StakeholderFinancialAnalysisService private function dateColumn(string $table, array $candidates): ?string { - if (! Schema::hasTable($table)) { + if (!Schema::hasTable($table)) { return null; } foreach ($candidates as $column) { @@ -512,7 +494,6 @@ class StakeholderFinancialAnalysisService return $column; } } - return null; } diff --git a/app/Services/Frontend/ContactSubmissionService.php b/app/Services/Frontend/ContactSubmissionService.php index a0ee0228..71ce244f 100644 --- a/app/Services/Frontend/ContactSubmissionService.php +++ b/app/Services/Frontend/ContactSubmissionService.php @@ -2,8 +2,8 @@ namespace App\Services\Frontend; -use App\Models\Configuration; use App\Models\ContactUs; +use App\Models\Configuration; use App\Services\Email\EmailDispatchService; use App\Services\System\GlobalConfigService; use Illuminate\Support\Facades\Log; @@ -13,7 +13,8 @@ class ContactSubmissionService public function __construct( private EmailDispatchService $emailService, private GlobalConfigService $configService - ) {} + ) { + } public function submit(array $payload): array { @@ -35,20 +36,20 @@ class ContactSubmissionService 'school_year' => $schoolYear ?: '2025-2026', ]); } catch (\Throwable $e) { - Log::error('Contact submission save failed: '.$e->getMessage()); + Log::error('Contact submission save failed: ' . $e->getMessage()); } $recipient = Configuration::getConfig('administrator_email') ?: (string) config('mail.from.address', '') ?: 'alrahma.isgl@gmail.com'; - $htmlMessage = nl2br('You have received a new message from '.$email.":\n\n".$message); + $htmlMessage = nl2br('You have received a new message from ' . $email . ":\n\n" . $message); $emailSent = false; - if (! app()->runningUnitTests()) { + if (!app()->runningUnitTests()) { $emailSent = $this->emailService->send($recipient, $subject, $htmlMessage, null, $email, $email); - if (! $emailSent) { - Log::error('Contact submission email failed for '.$email); + if (!$emailSent) { + Log::error('Contact submission email failed for ' . $email); } } diff --git a/app/Services/Frontend/LandingPageParentDashboardService.php b/app/Services/Frontend/LandingPageParentDashboardService.php index 27e39fef..67aee08d 100644 --- a/app/Services/Frontend/LandingPageParentDashboardService.php +++ b/app/Services/Frontend/LandingPageParentDashboardService.php @@ -21,7 +21,7 @@ class LandingPageParentDashboardService $semester = (string) ($context['semester'] ?? ''); $parentId = $this->primaryParentResolver->resolveFromCredentials($parentUserId, $userType); - if (! $parentId) { + if (!$parentId) { return [ 'ok' => false, 'message' => 'Unable to retrieve student data. Please contact support.', @@ -38,7 +38,6 @@ class LandingPageParentDashboardService $classSection = StudentClass::getClassSectionsByStudentId($student['id'], $schoolYear); $student['class_section'] = $classSection; $student['grade'] = $classSection; - return $student; }) ->all(); diff --git a/app/Services/Frontend/LandingPageRoleService.php b/app/Services/Frontend/LandingPageRoleService.php index c9785853..54f82436 100644 --- a/app/Services/Frontend/LandingPageRoleService.php +++ b/app/Services/Frontend/LandingPageRoleService.php @@ -9,7 +9,7 @@ class LandingPageRoleService { public function resolveRole(?User $user): string { - if (! $user) { + if (!$user) { return 'guest'; } @@ -22,7 +22,7 @@ class LandingPageRoleService ->map(fn ($r) => strtolower((string) $r)) ->all(); - if (! empty($roles)) { + if (!empty($roles)) { return $roles[0]; } diff --git a/app/Services/Frontend/LandingPageService.php b/app/Services/Frontend/LandingPageService.php index 0a31e4e7..7bdb6376 100644 --- a/app/Services/Frontend/LandingPageService.php +++ b/app/Services/Frontend/LandingPageService.php @@ -11,7 +11,8 @@ class LandingPageService private LandingPageContextService $contextService, private LandingPageTeacherSummaryService $teacherSummaryService, private LandingPageParentDashboardService $parentDashboardService - ) {} + ) { + } public function dashboardForUser(?User $user, ?int $classSectionId = null): array { @@ -45,7 +46,7 @@ class LandingPageService public function teacher(?User $user, ?int $classSectionId = null): array { - if (! $user) { + if (!$user) { return ['role' => 'teacher', 'dashboard' => 'teacher', 'summary' => []]; } @@ -66,7 +67,7 @@ class LandingPageService public function parent(?User $user): array { - if (! $user) { + if (!$user) { return ['role' => 'parent', 'dashboard' => 'parent', 'summary' => []]; } diff --git a/app/Services/Frontend/LandingPageTeacherSummaryService.php b/app/Services/Frontend/LandingPageTeacherSummaryService.php index 38ce2a95..bd2fe95a 100644 --- a/app/Services/Frontend/LandingPageTeacherSummaryService.php +++ b/app/Services/Frontend/LandingPageTeacherSummaryService.php @@ -8,6 +8,7 @@ use App\Models\Calendar; use App\Models\ClassSection; use App\Models\ScoreComment; use App\Models\SemesterScore; +use App\Models\StudentClass; use App\Models\TeacherClass; use DateTimeImmutable; use DateTimeZone; @@ -15,7 +16,9 @@ use Illuminate\Support\Facades\DB; class LandingPageTeacherSummaryService { - public function __construct(private LandingPageContextService $context) {} + public function __construct(private LandingPageContextService $context) + { + } public function summary(int $teacherId, ?int $requestedClassSectionId = null): array { @@ -32,7 +35,7 @@ class LandingPageTeacherSummaryService $activeId = null; if ($requestedClassSectionId && in_array($requestedClassSectionId, $availableIds, true)) { $activeId = $requestedClassSectionId; - } elseif (! empty($availableIds)) { + } elseif (!empty($availableIds)) { $activeId = $availableIds[0]; } @@ -45,7 +48,7 @@ class LandingPageTeacherSummaryService } $normalizedSemester = ucfirst(strtolower(trim((string) $semester))); - if (! in_array($normalizedSemester, ['Fall', 'Spring'], true)) { + if (!in_array($normalizedSemester, ['Fall', 'Spring'], true)) { $normalizedSemester = 'Fall'; } @@ -111,7 +114,7 @@ class LandingPageTeacherSummaryService ]; $uniqueStudentIds = []; - if (! empty($classSectionIds)) { + if (!empty($classSectionIds)) { $studentRows = DB::table('student_class as sc') ->select('sc.student_id', 'sc.class_section_id') ->join('students as s', 's.id', '=', 'sc.student_id') @@ -133,7 +136,7 @@ class LandingPageTeacherSummaryService $summary['attendanceSummary']['students'] = $totalStudents; $scoreRows = []; - if (! empty($classSectionIds)) { + if (!empty($classSectionIds)) { $scoreRows = SemesterScore::query() ->select(['student_id', 'class_section_id', 'midterm_exam_score', 'final_exam_score', 'participation_score']) ->whereIn('class_section_id', $classSectionIds) @@ -211,7 +214,7 @@ class LandingPageTeacherSummaryService ]; $commentRows = []; - if (! empty($uniqueStudentIds)) { + if (!empty($uniqueStudentIds)) { $commentRows = ScoreComment::query() ->select(['student_id', 'score_type', 'comment', 'comment_review']) ->whereIn('student_id', $uniqueStudentIds) @@ -235,7 +238,7 @@ class LandingPageTeacherSummaryService if ($type === '') { $type = 'general'; } - if (! isset($commentStats[$type])) { + if (!isset($commentStats[$type])) { $commentStats[$type] = ['pending' => 0, 'reviewed' => 0]; } @@ -258,7 +261,7 @@ class LandingPageTeacherSummaryService } foreach ($expectedCommentTypes as $type) { - if (! isset($commentStats[$type])) { + if (!isset($commentStats[$type])) { $commentStats[$type] = ['pending' => 0, 'reviewed' => 0]; } } @@ -282,7 +285,7 @@ class LandingPageTeacherSummaryService $completionPct = $totalStudents > 0 ? (int) round(min(100, ($filled / $totalStudents) * 100)) : 0; $commentTypes[$type] = [ - 'label' => ucfirst($type).' comments', + 'label' => ucfirst($type) . ' comments', 'pending' => $stats['pending'], 'reviewed' => $stats['reviewed'], 'filled' => $filled, @@ -309,7 +312,7 @@ class LandingPageTeacherSummaryService ]; $attendanceRows = []; - if (! empty($classSectionIds)) { + if (!empty($classSectionIds)) { $attendanceRows = AttendanceRecord::query() ->select(['student_id', 'class_section_id', 'total_absence']) ->whereIn('class_section_id', $classSectionIds) @@ -359,7 +362,7 @@ class LandingPageTeacherSummaryService ->get() ->toArray(); - if ($attendanceStatus['submitted'] === false && ! empty($classSectionIds)) { + if ($attendanceStatus['submitted'] === false && !empty($classSectionIds)) { $summary['attendanceSummary']['completionPct'] = 0; $summary['attendanceSummary']['recorded'] = 0; } @@ -374,22 +377,20 @@ class LandingPageTeacherSummaryService $notifications = []; $buildScoreLink = static function (?string $focus): string { $base = '/teacher/scores'; - - return $focus ? $base.'?focus='.urlencode($focus) : $base; + return $focus ? $base . '?focus=' . urlencode($focus) : $base; }; $determineFocus = static function (array $event): string { $eventTypeText = strtolower(trim((string) ($event['event_type'] ?? ''))); if ($eventTypeText !== '' && str_contains($eventTypeText, 'draft')) { return 'comments'; } - $text = strtolower(trim(($event['title'] ?? '').' '.($event['description'] ?? ''))); + $text = strtolower(trim(($event['title'] ?? '') . ' ' . ($event['description'] ?? ''))); if ($text === '') { return 'scores'; } if (str_contains($text, 'comment') || str_contains($text, 'ptap')) { return 'comments'; } - return 'scores'; }; $calendarLink = '/teacher/calendar'; @@ -442,13 +443,12 @@ class LandingPageTeacherSummaryService $notifications[] = [ 'type' => 'danger', 'message' => sprintf( - 'Deadline today: %s is due. Submit the remaining entries before the day ends.', + "Deadline today: %s is due. Submit the remaining entries before the day ends.", $matchedType ), 'link' => $buildScoreLink($eventFocus), 'linkText' => 'Submit now', ]; - continue; } @@ -460,7 +460,7 @@ class LandingPageTeacherSummaryService $notifications[] = [ 'type' => 'warning', 'message' => sprintf( - '%s is due on %s%s. Review it on the calendar so you don’t miss it.', + "%s is due on %s%s. Review it on the calendar so you don’t miss it.", $matchedType, $eventDateTime->format('M j, Y'), $relativeText @@ -470,13 +470,13 @@ class LandingPageTeacherSummaryService ]; } - if (! $activeDeadline) { + if (!$activeDeadline) { $scoreLabel = $summary['scoreSummary']['fieldLabel'] ?? 'Score'; - if (! empty($summary['scoreSummary']['missing'] ?? 0)) { + if (!empty($summary['scoreSummary']['missing'] ?? 0)) { $notifications[] = [ 'type' => 'warning', 'message' => sprintf( - 'You have %s %s entries missing on the scores page. Please finish submitting them.', + "You have %s %s entries missing on the scores page. Please finish submitting them.", (int) $summary['scoreSummary']['missing'], $scoreLabel ), @@ -493,20 +493,20 @@ class LandingPageTeacherSummaryService $notifications[] = [ 'type' => 'warning', 'message' => sprintf( - '%s comment missing for %s student(s). Leave comments on the scores page.', + "%s comment missing for %s student(s). Leave comments on the scores page.", ucfirst($type), $count ), 'link' => $buildScoreLink('comments'), - 'linkText' => ucfirst($type).' comments', + 'linkText' => ucfirst($type) . ' comments', ]; } - if (! empty($summary['participationSummary']['missing'] ?? 0)) { + if (!empty($summary['participationSummary']['missing'] ?? 0)) { $notifications[] = [ 'type' => 'warning', 'message' => sprintf( - 'Participation entries are still missing for %s student(s). Please add them from the scores page.', + "Participation entries are still missing for %s student(s). Please add them from the scores page.", (int) $summary['participationSummary']['missing'] ), 'link' => $buildScoreLink('scores'), @@ -515,11 +515,11 @@ class LandingPageTeacherSummaryService } } - if (! empty($summary['attendanceStatus']['missingSections'] ?? [])) { + if (!empty($summary['attendanceStatus']['missingSections'] ?? [])) { $missingNames = $summary['attendanceStatus']['missingNames'] ?? []; $label = ''; - if (! empty($missingNames)) { - $label = ' ('.implode(', ', array_slice($missingNames, 0, 3)).(count($missingNames) > 3 ? '…' : '').')'; + if (!empty($missingNames)) { + $label = ' (' . implode(', ', array_slice($missingNames, 0, 3)) . (count($missingNames) > 3 ? '…' : '') . ')'; } $notifications[] = [ 'type' => 'danger', @@ -564,13 +564,13 @@ class LandingPageTeacherSummaryService $missingSections = []; foreach ($classSectionIds as $classSectionId) { $status = $statusMap[$classSectionId] ?? ''; - if (! in_array($status, ['submitted', 'published', 'finalized'], true)) { + if (!in_array($status, ['submitted', 'published', 'finalized'], true)) { $missingSections[] = $classSectionId; } } $missingNames = []; - if (! empty($missingSections)) { + if (!empty($missingSections)) { $sectionRows = ClassSection::query() ->select(['class_section_id', 'class_section_name']) ->whereIn('class_section_id', $missingSections) diff --git a/app/Services/Frontend/ProfileIconService.php b/app/Services/Frontend/ProfileIconService.php index b58c44ef..97be1848 100644 --- a/app/Services/Frontend/ProfileIconService.php +++ b/app/Services/Frontend/ProfileIconService.php @@ -9,7 +9,7 @@ class ProfileIconService public function buildForUser(int $userId): array { $user = User::query()->find($userId); - if (! $user) { + if (!$user) { return [ 'userName' => 'User not found', 'userInitials' => '??', @@ -28,7 +28,7 @@ class ProfileIconService } return [ - 'userName' => trim($firstname.' '.$lastname), + 'userName' => trim($firstname . ' ' . $lastname), 'userInitials' => $initials !== '' ? $initials : '??', ]; } diff --git a/app/Services/Frontend/StaticPageService.php b/app/Services/Frontend/StaticPageService.php index 3edc2387..eea1eca6 100644 --- a/app/Services/Frontend/StaticPageService.php +++ b/app/Services/Frontend/StaticPageService.php @@ -7,9 +7,9 @@ class StaticPageService public function load(string $filename, string $type): array { $safeName = basename($filename); - $path = public_path('html/'.$safeName); + $path = public_path('html/' . $safeName); - if (! is_file($path) || ! is_readable($path)) { + if (!is_file($path) || !is_readable($path)) { return [ 'ok' => false, 'error' => 'Page content not available.', diff --git a/app/Services/Grading/BelowSixtyEmailService.php b/app/Services/Grading/BelowSixtyEmailService.php index 37357e66..906c2a55 100644 --- a/app/Services/Grading/BelowSixtyEmailService.php +++ b/app/Services/Grading/BelowSixtyEmailService.php @@ -2,14 +2,16 @@ namespace App\Services\Grading; -use App\Services\EmailService; use App\Support\MailHtml; +use App\Services\EmailService; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; class BelowSixtyEmailService { - public function __construct(private EmailService $emailService) {} + public function __construct(private EmailService $emailService) + { + } public function send(array $payload): array { @@ -23,7 +25,6 @@ class BelowSixtyEmailService if ($studentId <= 0) { Log::warning('Below60Email: missing student_id'); - return ['ok' => false, 'message' => 'Missing student id.']; } @@ -41,7 +42,6 @@ class BelowSixtyEmailService if (empty($rows)) { Log::warning('Below60Email: no guardian emails', ['student_id' => $studentId]); - return ['ok' => false, 'message' => 'No guardian email addresses.']; } @@ -57,17 +57,17 @@ class BelowSixtyEmailService $primary = $rows[0] ?? null; $parentName = ''; if ($primary) { - $parentName = trim((string) ($primary->firstname ?? '').' '.(string) ($primary->lastname ?? '')); + $parentName = trim((string) ($primary->firstname ?? '') . ' ' . (string) ($primary->lastname ?? '')); } $subject = (string) ($payload['subject'] ?? ''); if ($subject === '') { $subject = 'Student Performance Alert'; if ($studentName !== '') { - $subject .= ' — '.$studentName; + $subject .= ' — ' . $studentName; } if ($semester !== '' || $schoolYear !== '') { - $subject .= ' ('.trim($semester.' '.$schoolYear).')'; + $subject .= ' (' . trim($semester . ' ' . $schoolYear) . ')'; } } diff --git a/app/Services/Grading/GradingBelowSixtyService.php b/app/Services/Grading/GradingBelowSixtyService.php index c804e95b..6ceb5fa5 100644 --- a/app/Services/Grading/GradingBelowSixtyService.php +++ b/app/Services/Grading/GradingBelowSixtyService.php @@ -6,6 +6,7 @@ use App\Models\BelowSixtyDecision; use App\Models\CurrentFlag; use App\Models\ParentMeetingSchedule; use App\Models\ScoreComment; +use App\Models\Student; use App\Models\StudentDecision; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Event; @@ -57,7 +58,7 @@ class GradingBelowSixtyService $studentIds = array_values(array_filter($studentIds, static fn ($id) => $id > 0)); $commentMap = []; - if (! empty($studentIds)) { + if (!empty($studentIds)) { $commentRows = ScoreComment::query() ->select('student_id', 'comment', 'created_at') ->where('score_type', 'general') @@ -68,14 +69,14 @@ class GradingBelowSixtyService ->get(); foreach ($commentRows as $row) { $sid = (int) $row->student_id; - if ($sid > 0 && ! isset($commentMap[$sid])) { + if ($sid > 0 && !isset($commentMap[$sid])) { $commentMap[$sid] = (string) ($row->comment ?? ''); } } } $statusMap = []; - if (! empty($studentIds)) { + if (!empty($studentIds)) { $flagRows = CurrentFlag::query() ->select('student_id', 'flag_state') ->where('flag', 'grade') @@ -109,7 +110,7 @@ class GradingBelowSixtyService throw new RuntimeException('Student record not found for the selected term.'); } - $studentName = trim((string) ($row['firstname'] ?? '').' '.(string) ($row['lastname'] ?? '')); + $studentName = trim((string) ($row['firstname'] ?? '') . ' ' . (string) ($row['lastname'] ?? '')); $subject = $this->buildBelowSixtySubject($studentName, $semester, $schoolYear); $parentName = $this->fetchBelowSixtyParentName($studentId); @@ -145,7 +146,7 @@ class GradingBelowSixtyService public function updateStatus(int $studentId, string $semester, string $schoolYear, string $status, string $note, ?int $userId): void { $status = ucfirst(strtolower($status)); - if (! in_array($status, ['Open', 'Closed'], true)) { + if (!in_array($status, ['Open', 'Closed'], true)) { throw new RuntimeException('Invalid status.'); } @@ -169,22 +170,21 @@ class GradingBelowSixtyService $data['updated_by_open'] = $userId; if ($note !== '') { $prev = (string) ($existing->open_description ?? ''); - $data['open_description'] = trim($prev.PHP_EOL.$note); + $data['open_description'] = trim($prev . PHP_EOL . $note); } } else { $data['updated_by_closed'] = $userId; if ($note !== '') { $prev = (string) ($existing->close_description ?? ''); - $data['close_description'] = trim($prev.PHP_EOL.$note); + $data['close_description'] = trim($prev . PHP_EOL . $note); } } $existing->update($data); - return; } $row = $this->fetchBelowSixtyEmailRow($studentId, $schoolYear, $semester); - $studentName = trim((string) ($row['firstname'] ?? '').' '.(string) ($row['lastname'] ?? '')); + $studentName = trim((string) ($row['firstname'] ?? '') . ' ' . (string) ($row['lastname'] ?? '')); $grade = (string) ($row['class_section_name'] ?? ''); $data = [ 'student_id' => $studentId, @@ -298,7 +298,7 @@ class GradingBelowSixtyService continue; } - if (! isset($studentMap[$studentId])) { + if (!isset($studentMap[$studentId])) { $studentMap[$studentId] = [ 'student_id' => $studentId, 'school_id' => $row->school_id, @@ -355,7 +355,7 @@ class GradingBelowSixtyService $certificateMap = []; foreach ($certificateRows as $row) { $studentId = (int) ($row->student_id ?? 0); - if ($studentId > 0 && ! isset($certificateMap[$studentId])) { + if ($studentId > 0 && !isset($certificateMap[$studentId])) { $certificateMap[$studentId] = (string) ($row->certificate_number ?? ''); } } @@ -522,8 +522,8 @@ class GradingBelowSixtyService } $subject = 'Whole Year Academic Decision — ' - .(string) ($context['student_name'] ?? 'Student') - .' ('.$schoolYear.')'; + . (string) ($context['student_name'] ?? 'Student') + . ' (' . $schoolYear . ')'; return [ 'student_id' => $studentId, @@ -553,8 +553,8 @@ class GradingBelowSixtyService $resolvedSubject = trim((string) $subject); if ($resolvedSubject === '') { $resolvedSubject = 'Whole Year Academic Decision — ' - .(string) ($context['student_name'] ?? 'Student') - .' ('.$schoolYear.')'; + . (string) ($context['student_name'] ?? 'Student') + . ' (' . $schoolYear . ')'; } $payload = [ @@ -628,7 +628,7 @@ class GradingBelowSixtyService continue; } - if (! isset($studentMap[$studentId])) { + if (!isset($studentMap[$studentId])) { $studentMap[$studentId] = [ 'school_id' => $row->school_id, 'firstname' => $row->firstname, @@ -661,7 +661,7 @@ class GradingBelowSixtyService if ($studentId <= 0) { continue; } - if (! isset($belowMap[$studentId]) || trim((string) ($belowMap[$studentId]->decision ?? '')) === '') { + if (!isset($belowMap[$studentId]) || trim((string) ($belowMap[$studentId]->decision ?? '')) === '') { $belowMap[$studentId] = $row; } } @@ -754,7 +754,7 @@ class GradingBelowSixtyService return [ 'rows' => $rows, - 'generated' => ! empty($savedMap), + 'generated' => !empty($savedMap), 'school_year' => $schoolYear, ]; } @@ -789,7 +789,7 @@ class GradingBelowSixtyService continue; } - if (! isset($studentMap[$studentId])) { + if (!isset($studentMap[$studentId])) { $studentMap[$studentId] = [ 'firstname' => $row->firstname, 'lastname' => $row->lastname, @@ -818,7 +818,7 @@ class GradingBelowSixtyService if ($studentId <= 0) { continue; } - if (! isset($belowMap[$studentId]) || trim((string) ($belowMap[$studentId]->decision ?? '')) === '') { + if (!isset($belowMap[$studentId]) || trim((string) ($belowMap[$studentId]->decision ?? '')) === '') { $belowMap[$studentId] = $row; } } @@ -912,7 +912,7 @@ class GradingBelowSixtyService ->whereRaw('LOWER(TRIM(ss.semester)) = ?', [$semesterKey]) ->first(); - if (! $row) { + if (!$row) { return []; } @@ -972,7 +972,7 @@ class GradingBelowSixtyService ]; } - if (! empty($semesters)) { + if (!empty($semesters)) { $commentRows = DB::table('score_comments') ->select('semester', 'score_type', 'comment', 'created_at') ->where('student_id', $studentId) @@ -987,7 +987,7 @@ class GradingBelowSixtyService foreach ($commentRows as $row) { $semester = ucfirst(strtolower(trim((string) ($row->semester ?? '')))); $type = strtolower(trim((string) ($row->score_type ?? 'general'))); - if (isset($semesters[$semester]) && ! isset($semesters[$semester]['comments'][$type])) { + if (isset($semesters[$semester]) && !isset($semesters[$semester]['comments'][$type])) { $semesters[$semester]['comments'][$type] = (string) ($row->comment ?? ''); } } @@ -1001,24 +1001,23 @@ class GradingBelowSixtyService $parentName = 'Parent/Guardian'; try { $rows = DB::select( - 'SELECT u.firstname, u.lastname + "SELECT u.firstname, u.lastname FROM family_students fs JOIN family_guardians fg ON fg.family_id = fs.family_id JOIN users u ON u.id = fg.user_id WHERE fs.student_id = ? ORDER BY fg.is_primary DESC, u.lastname, u.firstname - LIMIT 1', + LIMIT 1", [$studentId] ); - if (! empty($rows[0])) { - $candidate = trim((string) ($rows[0]->firstname ?? '').' '.(string) ($rows[0]->lastname ?? '')); + if (!empty($rows[0])) { + $candidate = trim((string) ($rows[0]->firstname ?? '') . ' ' . (string) ($rows[0]->lastname ?? '')); if ($candidate !== '') { $parentName = $candidate; } } } catch (\Throwable $e) { } - return $parentName; } @@ -1042,7 +1041,7 @@ class GradingBelowSixtyService ]; } - $studentName = trim((string) ($student->firstname ?? '').' '.(string) ($student->lastname ?? '')); + $studentName = trim((string) ($student->firstname ?? '') . ' ' . (string) ($student->lastname ?? '')); if ($studentName === '') { $studentName = 'Student'; } @@ -1141,22 +1140,22 @@ class GradingBelowSixtyService $html = '

Dear Parent/Guardian,

- This message is regarding '.e($studentName).' - for the '.e($schoolYear).' school year. + This message is regarding ' . e($studentName) . ' + for the ' . e($schoolYear) . ' school year.

- Class: '.e($classSectionName !== '' ? $classSectionName : 'N/A').'
- Fall Score: '.e($fallText).'
- Spring Score: '.e($springText).'
- Whole-Year Score: '.e($yearText).'
- Decision: '.e($decision).' + Class: ' . e($classSectionName !== '' ? $classSectionName : 'N/A') . '
+ Fall Score: ' . e($fallText) . '
+ Spring Score: ' . e($springText) . '
+ Whole-Year Score: ' . e($yearText) . '
+ Decision: ' . e($decision) . '

'; if ($notes !== '') { $html .= '

Decision Notes:
- '.nl2br(e($notes)).' + ' . nl2br(e($notes)) . '

'; } @@ -1313,9 +1312,9 @@ class GradingBelowSixtyService $classSectionName = trim((string) ($semester['class_section_name'] ?? '')); $html .= '

- '.e($semesterName).' Semester'; + ' . e($semesterName) . ' Semester'; if ($classSectionName !== '') { - $html .= ' — '.e($classSectionName); + $html .= ' — ' . e($classSectionName); } $html .= '

@@ -1330,7 +1329,7 @@ class GradingBelowSixtyService $hasScoreRow = false; foreach ($scoreLabels as $key => $label) { - if (! array_key_exists($key, $semester)) { + if (!array_key_exists($key, $semester)) { continue; } $value = $semester[$key]; @@ -1341,13 +1340,13 @@ class GradingBelowSixtyService $fontWeight = $key === 'semester_score' ? 'font-weight:bold;' : ''; $html .= ' - '.e($label).' - '.e($scoreText).' + ' . e($label) . ' + ' . e($scoreText) . ' '; $hasScoreRow = true; } - if (! $hasScoreRow) { + if (!$hasScoreRow) { $html .= ' No scores recorded. @@ -1359,7 +1358,7 @@ class GradingBelowSixtyService '; $comments = is_array($semester['comments'] ?? null) ? $semester['comments'] : []; - if (! empty($comments)) { + if (!empty($comments)) { $deduped = []; $seen = []; foreach ($comments as $type => $text) { @@ -1368,7 +1367,7 @@ class GradingBelowSixtyService continue; } $label = $commentTypeLabels[$type] ?? (string) $type; - $hash = $label.'|'.$text; + $hash = $label . '|' . $text; if (isset($seen[$hash])) { continue; } @@ -1376,13 +1375,13 @@ class GradingBelowSixtyService $deduped[] = ['label' => $label, 'text' => $text]; } - if (! empty($deduped)) { + if (!empty($deduped)) { $html .= '

Comments

'; foreach ($deduped as $comment) { $html .= '

- '.e($comment['label']).':
- '.nl2br(e($comment['text'])).' + ' . e($comment['label']) . ':
+ ' . nl2br(e($comment['text'])) . '

'; } } @@ -1396,12 +1395,11 @@ class GradingBelowSixtyService { $subject = 'Student Performance Alert'; if ($studentName !== '') { - $subject .= ' — '.$studentName; + $subject .= ' — ' . $studentName; } if ($semester !== '' || $schoolYear !== '') { - $subject .= ' ('.trim($semester.' '.$schoolYear).')'; + $subject .= ' (' . trim($semester . ' ' . $schoolYear) . ')'; } - return $subject; } @@ -1416,18 +1414,18 @@ class GradingBelowSixtyService $parentUserId = null; try { $pRows = DB::select( - 'SELECT u.id AS user_id, u.firstname, u.lastname + "SELECT u.id AS user_id, u.firstname, u.lastname FROM family_students fs JOIN family_guardians fg ON fg.family_id = fs.family_id JOIN users u ON u.id = fg.user_id WHERE fs.student_id = ? ORDER BY fg.is_primary DESC, u.lastname, u.firstname - LIMIT 1', + LIMIT 1", [$studentId] ); - if (! empty($pRows[0])) { + if (!empty($pRows[0])) { $parentUserId = (int) ($pRows[0]->user_id ?? 0) ?: null; - $parentName = trim((string) ($pRows[0]->firstname ?? '').' '.(string) ($pRows[0]->lastname ?? '')); + $parentName = trim((string) ($pRows[0]->firstname ?? '') . ' ' . (string) ($pRows[0]->lastname ?? '')); if ($parentName === '') { $parentName = 'Parent/Guardian'; } @@ -1438,16 +1436,16 @@ class GradingBelowSixtyService if ($parentUserId === null) { try { $srow = DB::selectOne( - 'SELECT s.parent_id, u.firstname, u.lastname + "SELECT s.parent_id, u.firstname, u.lastname FROM students s LEFT JOIN users u ON u.id = s.parent_id WHERE s.id = ? - LIMIT 1', + LIMIT 1", [$studentId] ); - if (! empty($srow)) { + if (!empty($srow)) { $parentUserId = (int) ($srow->parent_id ?? 0) ?: null; - $fallbackName = trim((string) ($srow->firstname ?? '').' '.(string) ($srow->lastname ?? '')); + $fallbackName = trim((string) ($srow->firstname ?? '') . ' ' . (string) ($srow->lastname ?? '')); if ($fallbackName !== '') { $parentName = $fallbackName; } @@ -1456,7 +1454,7 @@ class GradingBelowSixtyService } } - $studentName = trim((string) ($row['firstname'] ?? '').' '.(string) ($row['lastname'] ?? '')); + $studentName = trim((string) ($row['firstname'] ?? '') . ' ' . (string) ($row['lastname'] ?? '')); return [ 'student_name' => $studentName !== '' ? $studentName : 'Student', diff --git a/app/Services/Grading/GradingLockService.php b/app/Services/Grading/GradingLockService.php index e343df9b..c5ba695e 100644 --- a/app/Services/Grading/GradingLockService.php +++ b/app/Services/Grading/GradingLockService.php @@ -2,11 +2,11 @@ namespace App\Services\Grading; -use App\Models\ClassSection; use App\Models\GradingLock; +use App\Models\ClassSection; +use RuntimeException; use App\Services\Grading\Policy\GradingPolicyResolver; use App\Services\Grading\Validation\GradebookFinalizationValidator; -use RuntimeException; class GradingLockService { @@ -14,8 +14,8 @@ class GradingLockService private ?GradingPolicyResolver $policyResolver = null, private ?GradebookFinalizationValidator $finalizationValidator = null ) { - $this->policyResolver = $this->policyResolver ?? new GradingPolicyResolver; - $this->finalizationValidator = $this->finalizationValidator ?? new GradebookFinalizationValidator; + $this->policyResolver = $this->policyResolver ?? new GradingPolicyResolver(); + $this->finalizationValidator = $this->finalizationValidator ?? new GradebookFinalizationValidator(); } public function toggle(int $classSectionId, string $semester, string $schoolYear, ?int $userId): bool @@ -32,7 +32,6 @@ class GradingLockService 'locked_by' => null, 'locked_at' => null, ]); - return false; } @@ -98,7 +97,7 @@ class GradingLockService foreach ($sectionIds as $sid) { $this->assertCanLock($sid, $semester, $schoolYear); - if (! empty($existingBySection[$sid])) { + if (!empty($existingBySection[$sid])) { if ($existingBySection[$sid]->is_locked) { continue; } @@ -108,7 +107,6 @@ class GradingLockService 'locked_at' => $now, ]); $count++; - continue; } @@ -125,7 +123,7 @@ class GradingLockService $count++; } - if (! empty($insertRows)) { + if (!empty($insertRows)) { GradingLock::query()->insert($insertRows); } @@ -140,7 +138,7 @@ class GradingLockService } $result = $this->finalizationValidator->validateClassSectionBeforeLock($classSectionId, $semester, $schoolYear); - if (! $result['valid']) { + if (!$result['valid']) { $count = count($result['errors']); throw new RuntimeException("Cannot lock class section {$classSectionId}; strong grading validation failed with {$count} error(s)."); } diff --git a/app/Services/Grading/GradingOverviewService.php b/app/Services/Grading/GradingOverviewService.php index 72b67bd6..3bfb14d9 100644 --- a/app/Services/Grading/GradingOverviewService.php +++ b/app/Services/Grading/GradingOverviewService.php @@ -8,7 +8,9 @@ use Illuminate\Support\Facades\DB; class GradingOverviewService { - public function __construct() {} + public function __construct() + { + } public function overview(?int $classId, ?string $semester, ?string $schoolYear): array { @@ -36,7 +38,7 @@ class GradingOverviewService continue; } - if (! isset($grades[$classIdValue])) { + if (!isset($grades[$classIdValue])) { $grades[$classIdValue] = []; } $exists = false; @@ -46,7 +48,7 @@ class GradingOverviewService break; } } - if (! $exists) { + if (!$exists) { $grades[$classIdValue][] = [ 'class_section_id' => $sectionId, 'class_section_name' => $sectionName, @@ -246,7 +248,6 @@ class GradingOverviewService if ($value === null || $value === '') { return null; } - return round((float) $value, 2); } @@ -259,7 +260,6 @@ class GradingOverviewService return null; } } - return $score; } @@ -271,7 +271,6 @@ class GradingOverviewService return $option; } } - return $requestedSemester; } if ($fallbackSemester !== null && $fallbackSemester !== '') { @@ -280,10 +279,8 @@ class GradingOverviewService return $option; } } - return $fallbackSemester; } - return $semesterOptions[0] ?? ''; } @@ -309,7 +306,7 @@ class GradingOverviewService $semesters = ['Fall', 'Spring']; } - if ($fallbackSemester !== null && $fallbackSemester !== '' && ! in_array($fallbackSemester, $semesters, true)) { + if ($fallbackSemester !== null && $fallbackSemester !== '' && !in_array($fallbackSemester, $semesters, true)) { array_unshift($semesters, $fallbackSemester); } @@ -325,7 +322,6 @@ class GradingOverviewService if ($norm === 'spring') { return 'parent_scores_released_spring'; } - return null; } @@ -334,14 +330,13 @@ class GradingOverviewService $key = $this->getParentReleaseKey($semester); $raw = $key ? Configuration::getConfig($key) : null; $raw = (string) ($raw ?? ''); - return in_array(strtolower(trim($raw)), ['1', 'true', 'yes', 'y', 'on'], true); } private function ensureParentReleaseKeyExists(string $semester): void { $key = $this->getParentReleaseKey($semester); - if (! $key) { + if (!$key) { return; } diff --git a/app/Services/Grading/GradingPlacementService.php b/app/Services/Grading/GradingPlacementService.php index b0edfcf6..63a36d2f 100644 --- a/app/Services/Grading/GradingPlacementService.php +++ b/app/Services/Grading/GradingPlacementService.php @@ -14,7 +14,7 @@ class GradingPlacementService { public function placementContext(?int $classSectionId, string $schoolYear, ?string $placementTest, ?string $open): array { - if (! $classSectionId) { + if (!$classSectionId) { $showStudents = ($placementTest !== '' && $open === '1'); $students = $showStudents ? $this->fetchActiveStudentsWithSection($schoolYear) : []; $batches = $this->fetchPlacementBatches($schoolYear); @@ -40,7 +40,7 @@ class GradingPlacementService ), static fn ($id) => $id > 0)); $levels = []; - if (! empty($studentIds)) { + if (!empty($studentIds)) { $rows = PlacementLevel::query() ->whereIn('student_id', $studentIds) ->where('school_year', $schoolYear) @@ -67,7 +67,7 @@ class GradingPlacementService } $level = $level === '' || $level === null ? null : (int) $level; - if ($level !== null && ! in_array($level, [1, 2, 3], true)) { + if ($level !== null && !in_array($level, [1, 2, 3], true)) { throw new RuntimeException('Invalid placement level.'); } @@ -80,7 +80,6 @@ class GradingPlacementService if ($existing) { $existing->delete(); } - return; } @@ -113,7 +112,7 @@ class GradingPlacementService $validSet = array_flip($validIds); $existingRows = []; - if (! empty($validIds)) { + if (!empty($validIds)) { $rows = PlacementLevel::query() ->whereIn('student_id', $validIds) ->where('school_year', $schoolYear) @@ -125,12 +124,12 @@ class GradingPlacementService foreach ($levels as $studentIdRaw => $levelRaw) { $studentId = (int) $studentIdRaw; - if (! isset($validSet[$studentId])) { + if (!isset($validSet[$studentId])) { continue; } $levelRaw = trim((string) $levelRaw); $level = $levelRaw === '' ? null : (int) $levelRaw; - if ($level !== null && ! in_array($level, [1, 2, 3], true)) { + if ($level !== null && !in_array($level, [1, 2, 3], true)) { continue; } @@ -138,7 +137,6 @@ class GradingPlacementService if (isset($existingRows[$studentId])) { $existingRows[$studentId]->delete(); } - continue; } @@ -181,7 +179,7 @@ class GradingPlacementService $savedCount = 0; foreach ($levels as $studentIdRaw => $levelRaw) { $studentId = (int) $studentIdRaw; - if (! isset($validSet[$studentId])) { + if (!isset($validSet[$studentId])) { continue; } $levelRaw = trim((string) $levelRaw); @@ -214,7 +212,7 @@ class GradingPlacementService public function getPlacementBatch(int $batchId): array { $batch = PlacementBatch::query()->find($batchId); - if (! $batch) { + if (!$batch) { throw new RuntimeException('Placement batch not found.'); } @@ -232,7 +230,7 @@ class GradingPlacementService public function updatePlacementBatch(int $batchId, string $schoolYear, array $levels, ?int $userId): void { $batch = PlacementBatch::query()->find($batchId); - if (! $batch) { + if (!$batch) { throw new RuntimeException('Placement batch not found.'); } @@ -247,7 +245,7 @@ class GradingPlacementService foreach ($levels as $studentIdRaw => $scoreRaw) { $studentId = (int) $studentIdRaw; - if (! isset($validSet[$studentId])) { + if (!isset($validSet[$studentId])) { continue; } $scoreRaw = trim((string) $scoreRaw); @@ -255,7 +253,6 @@ class GradingPlacementService if (isset($existing[$studentId])) { PlacementScore::query()->whereKey($existing[$studentId]['id'])->delete(); } - continue; } $score = (int) $scoreRaw; diff --git a/app/Services/Grading/GradingRefreshService.php b/app/Services/Grading/GradingRefreshService.php index d8b19ce3..d43c3589 100644 --- a/app/Services/Grading/GradingRefreshService.php +++ b/app/Services/Grading/GradingRefreshService.php @@ -10,7 +10,9 @@ use RuntimeException; class GradingRefreshService { - public function __construct(private SemesterScoreService $semesterScoreService) {} + public function __construct(private SemesterScoreService $semesterScoreService) + { + } public function refreshSemesterScores(int $classSectionId, string $semester, string $schoolYear): void { @@ -33,7 +35,7 @@ class GradingRefreshService private function refreshAttendanceComments(int $classSectionId, string $semester, string $schoolYear): void { - if (! function_exists('attendance_comment_from_score')) { + if (!function_exists('attendance_comment_from_score')) { return; } diff --git a/app/Services/Grading/GradingScoreService.php b/app/Services/Grading/GradingScoreService.php index 2b52548f..7896c093 100644 --- a/app/Services/Grading/GradingScoreService.php +++ b/app/Services/Grading/GradingScoreService.php @@ -2,7 +2,6 @@ namespace App\Services\Grading; -use App\Models\ClassSection; use App\Models\FinalExam; use App\Models\GradingLock; use App\Models\Homework; @@ -12,20 +11,23 @@ use App\Models\Quiz; use App\Models\ScoreComment; use App\Models\SemesterScore; use App\Models\Student; +use App\Models\ClassSection; use App\Services\Grading\Validation\ScoreValueValidator; use App\Services\Scores\SemesterScoreService; use RuntimeException; class GradingScoreService { - public function __construct(private SemesterScoreService $semesterScoreService) {} + public function __construct(private SemesterScoreService $semesterScoreService) + { + } public function show(string $type, int $classSectionId, int $studentId, string $semester, string $schoolYear): array { $model = $this->resolveModel($type); $student = Student::query()->find($studentId); - if (! $student) { + if (!$student) { throw new RuntimeException('Student not found.'); } @@ -71,9 +73,9 @@ class GradingScoreService $scores = $payload['scores'] ?? []; $comments = $payload['comments'] ?? []; - $validator = new ScoreValueValidator; + $validator = new ScoreValueValidator(); foreach ($scoreIds as $i => $id) { - $normalizedScore = $validator->normalizeNullable($scores[$i] ?? null, ScoreValueValidator::DEFAULT_MAX_POINTS, ucfirst($type).' score'); + $normalizedScore = $validator->normalizeNullable($scores[$i] ?? null, ScoreValueValidator::DEFAULT_MAX_POINTS, ucfirst($type) . ' score'); $model->newQuery()->whereKey($id)->update([ 'score' => $normalizedScore, 'max_points' => ScoreValueValidator::DEFAULT_MAX_POINTS, @@ -84,8 +86,8 @@ class GradingScoreService } } elseif (in_array($type, ['midterm', 'final', 'test'], true)) { $score = $payload['score'] ?? null; - $validator = new ScoreValueValidator; - $normalizedScore = $validator->normalizeNullable($score, ScoreValueValidator::DEFAULT_MAX_POINTS, ucfirst($type).' score'); + $validator = new ScoreValueValidator(); + $normalizedScore = $validator->normalizeNullable($score, ScoreValueValidator::DEFAULT_MAX_POINTS, ucfirst($type) . ' score'); $data = [ 'score' => $normalizedScore, @@ -133,7 +135,7 @@ class GradingScoreService } $studentTeacherInfo = Student::getStudentInfoByClassSectionId($classSectionId, $semester, $schoolYear, $updatedBy); - if (! empty($studentTeacherInfo)) { + if (!empty($studentTeacherInfo)) { try { $this->semesterScoreService->updateScoresForStudents($studentTeacherInfo, $semester, $schoolYear); } catch (RuntimeException $e) { @@ -187,13 +189,13 @@ class GradingScoreService private function resolveModel(string $type) { return match ($type) { - 'homework' => new Homework, - 'quiz' => new Quiz, - 'project' => new Project, - 'midterm' => new MidtermExam, - 'final' => new FinalExam, - 'test' => new SemesterScore, - 'comments' => new ScoreComment, + 'homework' => new Homework(), + 'quiz' => new Quiz(), + 'project' => new Project(), + 'midterm' => new MidtermExam(), + 'final' => new FinalExam(), + 'test' => new SemesterScore(), + 'comments' => new ScoreComment(), default => throw new RuntimeException("Invalid type: {$type}"), }; } diff --git a/app/Services/Grading/HomeworkTrackingCalendarService.php b/app/Services/Grading/HomeworkTrackingCalendarService.php index 4a5f2f7a..bc3568e1 100644 --- a/app/Services/Grading/HomeworkTrackingCalendarService.php +++ b/app/Services/Grading/HomeworkTrackingCalendarService.php @@ -15,7 +15,6 @@ class HomeworkTrackingCalendarService } if ($startYear === null) { $today = new DateTime('today'); - return [$today, $today]; } @@ -32,7 +31,6 @@ class HomeworkTrackingCalendarService } } catch (\Throwable $e) { $today = new DateTime('today'); - return [$today, $today]; } @@ -75,7 +73,7 @@ class HomeworkTrackingCalendarService $dateToIndex = []; $idx = 0; foreach ($sundays as $ymd) { - if (! isset($eventDays[$ymd])) { + if (!isset($eventDays[$ymd])) { $idx++; $dateToIndex[$ymd] = $idx; } else { diff --git a/app/Services/Grading/HomeworkTrackingService.php b/app/Services/Grading/HomeworkTrackingService.php index 94516870..d92690ca 100644 --- a/app/Services/Grading/HomeworkTrackingService.php +++ b/app/Services/Grading/HomeworkTrackingService.php @@ -9,7 +9,9 @@ class HomeworkTrackingService { private array $teacherAssignmentCache = []; - public function __construct(private HomeworkTrackingCalendarService $calendarService) {} + public function __construct(private HomeworkTrackingCalendarService $calendarService) + { + } public function report(?string $semester = null, ?string $schoolYear = null, int $page = 1): array { @@ -120,7 +122,7 @@ class HomeworkTrackingService $date = substr((string) ($row->hw_date ?? ''), 0, 10); $cnt = (int) $row->cnt; $firstCreated = substr((string) ($row->first_created ?? ''), 0, 10); - if ($csid <= 0 || ! $date || $cnt <= 0) { + if ($csid <= 0 || !$date || $cnt <= 0) { continue; } @@ -136,7 +138,7 @@ class HomeworkTrackingService } $j = $baseIndex; - while ($j >= 0 && ! empty($eventDays[$sundays[$j]])) { + while ($j >= 0 && !empty($eventDays[$sundays[$j]])) { $j--; } if ($j < 0) { @@ -175,7 +177,7 @@ class HomeworkTrackingService if ($sid <= 0) { continue; } - if (! isset($bySection[$sid])) { + if (!isset($bySection[$sid])) { $bySection[$sid] = [ 'class_section_id' => $sid, 'class_id' => (int) ($row->class_id ?? 0), @@ -184,7 +186,7 @@ class HomeworkTrackingService 'tas' => [], ]; } - $name = trim((string) ($row->firstname ?? '').' '.(string) ($row->lastname ?? '')); + $name = trim((string) ($row->firstname ?? '') . ' ' . (string) ($row->lastname ?? '')); $pos = strtolower((string) ($row->position ?? '')); if ($name !== '') { if ($pos === 'main') { @@ -200,23 +202,15 @@ class HomeworkTrackingService $ai = (int) ($a['class_id'] ?? 0); $bi = (int) ($b['class_id'] ?? 0); $order = function (int $cid): int { - if ($cid === 13) { - return 0; - } - if ($cid >= 1 && $cid <= 11) { - return $cid; - } - if ($cid === 12) { - return 99; - } - + if ($cid === 13) return 0; + if ($cid >= 1 && $cid <= 11) return $cid; + if ($cid === 12) return 99; return 200 + $cid; }; $cmp = $order($ai) <=> $order($bi); if ($cmp !== 0) { return $cmp; } - return strnatcasecmp($a['class_section_name'] ?? '', $b['class_section_name'] ?? ''); }); diff --git a/app/Services/Grading/Policy/GradingPolicyResolver.php b/app/Services/Grading/Policy/GradingPolicyResolver.php index bf7b3ad6..a4b6501f 100644 --- a/app/Services/Grading/Policy/GradingPolicyResolver.php +++ b/app/Services/Grading/Policy/GradingPolicyResolver.php @@ -7,17 +7,14 @@ use App\Models\Configuration; class GradingPolicyResolver { public const LEGACY_MODE = 'legacy'; - public const STRONG_MODE = 'strong'; - public const LEGACY_VERSION = 'legacy_v1'; - public const STRONG_VERSION = 'strong_v1'; public function calculationMode(?int $classSectionId = null, ?string $semester = null, ?string $schoolYear = null): string { $enabled = strtolower((string) (Configuration::getConfig('strong_grading_enabled') ?? '')); - if (! in_array($enabled, ['1', 'true', 'yes', 'on'], true)) { + if (!in_array($enabled, ['1', 'true', 'yes', 'on'], true)) { return self::LEGACY_MODE; } @@ -27,7 +24,6 @@ class GradingPolicyResolver } $ids = array_filter(array_map('trim', explode(',', $allowedSections)), fn ($v) => $v !== ''); - return $classSectionId !== null && in_array((string) $classSectionId, $ids, true) ? self::STRONG_MODE : self::LEGACY_MODE; diff --git a/app/Services/Grading/StrongCategoryAverageCalculator.php b/app/Services/Grading/StrongCategoryAverageCalculator.php index 54f2bddf..4b390d29 100644 --- a/app/Services/Grading/StrongCategoryAverageCalculator.php +++ b/app/Services/Grading/StrongCategoryAverageCalculator.php @@ -31,19 +31,16 @@ class StrongCategoryAverageCalculator if ($status === 'pending' || $status === '') { $pending[] = $row; - continue; } if (in_array($status, ['excused', 'not_assigned'], true)) { $excluded[] = $row; - continue; } if ($status === 'missing') { $included[] = 0.0; - continue; } diff --git a/app/Services/Grading/Validation/GradebookFinalizationValidator.php b/app/Services/Grading/Validation/GradebookFinalizationValidator.php index 776cf6d6..f9510a66 100644 --- a/app/Services/Grading/Validation/GradebookFinalizationValidator.php +++ b/app/Services/Grading/Validation/GradebookFinalizationValidator.php @@ -15,7 +15,7 @@ class GradebookFinalizationValidator $errors = []; foreach ($this->scoreTables() as $table => $meta) { - if (! DB::getSchemaBuilder()->hasTable($table)) { + if (!DB::getSchemaBuilder()->hasTable($table)) { continue; } diff --git a/app/Services/Grading/Validation/ScoreValueValidator.php b/app/Services/Grading/Validation/ScoreValueValidator.php index 1c72c6f3..2141fb16 100644 --- a/app/Services/Grading/Validation/ScoreValueValidator.php +++ b/app/Services/Grading/Validation/ScoreValueValidator.php @@ -16,7 +16,7 @@ class ScoreValueValidator $max = $this->normalizeMaxPoints($maxPoints, $label); - if (! is_numeric($value)) { + if (!is_numeric($value)) { throw new InvalidArgumentException("{$label} must be numeric."); } @@ -34,7 +34,6 @@ class ScoreValueValidator if ($max <= 0) { throw new InvalidArgumentException("{$label} max points must be greater than 0."); } - return $max; } diff --git a/app/Services/Incidents/CurrentIncidentService.php b/app/Services/Incidents/CurrentIncidentService.php index ee0f08ef..9d1261f6 100644 --- a/app/Services/Incidents/CurrentIncidentService.php +++ b/app/Services/Incidents/CurrentIncidentService.php @@ -11,7 +11,9 @@ use Illuminate\Support\Facades\DB; class CurrentIncidentService { - public function __construct(private IncidentLookupService $lookup) {} + public function __construct(private IncidentLookupService $lookup) + { + } public function listCurrent(): array { @@ -21,7 +23,7 @@ class CurrentIncidentService $updaterIds = []; foreach ($incidents as $incident) { foreach (['updated_by_open', 'updated_by_closed', 'updated_by_canceled'] as $key) { - if (! empty($incident[$key])) { + if (!empty($incident[$key])) { $updaterIds[] = (int) $incident[$key]; } } @@ -31,7 +33,7 @@ class CurrentIncidentService foreach ($incidents as &$incident) { foreach (['updated_by_open', 'updated_by_closed', 'updated_by_canceled'] as $key) { $id = (int) ($incident[$key] ?? 0); - $incident[$key.'_name'] = $id > 0 ? ($updaterMap[$id] ?? null) : null; + $incident[$key . '_name'] = $id > 0 ? ($updaterMap[$id] ?? null) : null; } } unset($incident); @@ -60,7 +62,7 @@ class CurrentIncidentService ->get() ->map(fn ($student) => [ 'id' => (int) $student->id, - 'name' => trim((string) $student->firstname.' '.(string) $student->lastname), + 'name' => trim((string) $student->firstname . ' ' . (string) $student->lastname), ]) ->all(); } @@ -75,7 +77,7 @@ class CurrentIncidentService $grade = (string) $payload['grade']; $student = Student::query()->find($studentId); - if (! $student) { + if (!$student) { return [ 'ok' => false, 'message' => 'Student not found.', @@ -119,7 +121,7 @@ class CurrentIncidentService $incident = CurrentIncident::query()->create([ 'student_id' => $student->id, - 'student_name' => trim((string) $student->firstname.' '.(string) $student->lastname), + 'student_name' => trim((string) $student->firstname . ' ' . (string) $student->lastname), 'grade' => $grade, 'incident' => $incidentType, 'incident_datetime' => $currentDateTime, @@ -147,7 +149,7 @@ class CurrentIncidentService public function closeIncident(int $incidentId, string $closeDescription, ?string $actionTaken, int $userId): array { $incident = CurrentIncident::query()->find($incidentId); - if (! $incident) { + if (!$incident) { return ['ok' => false, 'message' => 'Incident not found.']; } @@ -169,7 +171,7 @@ class CurrentIncidentService public function cancelIncident(int $incidentId, ?string $cancelDescription, ?string $actionTaken, int $userId): array { $incident = CurrentIncident::query()->find($incidentId); - if (! $incident) { + if (!$incident) { return ['ok' => false, 'message' => 'Incident not found.']; } @@ -226,6 +228,6 @@ class CurrentIncidentService return $append; } - return $existing.PHP_EOL.$append; + return $existing . PHP_EOL . $append; } } diff --git a/app/Services/Incidents/IncidentAnalysisService.php b/app/Services/Incidents/IncidentAnalysisService.php index 48d8aef8..768b93e7 100644 --- a/app/Services/Incidents/IncidentAnalysisService.php +++ b/app/Services/Incidents/IncidentAnalysisService.php @@ -6,7 +6,9 @@ use App\Models\Incident; class IncidentAnalysisService { - public function __construct(private IncidentLookupService $lookup) {} + public function __construct(private IncidentLookupService $lookup) + { + } public function analyze(?string $schoolYear, ?string $semester): array { @@ -29,7 +31,7 @@ class IncidentAnalysisService foreach ($incidents as $incident) { $studentId = (string) ($incident['student_id'] ?? ''); $studentName = (string) ($incident['student_name'] ?? ''); - $studentKey = $studentId !== '' ? 'id:'.$studentId : 'name:'.strtolower(trim($studentName)); + $studentKey = $studentId !== '' ? 'id:' . $studentId : 'name:' . strtolower(trim($studentName)); $gradeValue = $incident['grade'] ?? ''; $gradeLabel = $gradeValue; @@ -37,7 +39,7 @@ class IncidentAnalysisService $gradeLabel = $gradeMap[(int) $gradeValue] ?? (string) $gradeValue; } - if (! isset($students[$studentKey])) { + if (!isset($students[$studentKey])) { $students[$studentKey] = [ 'student_id' => $studentId, 'student_name' => $studentName, @@ -82,7 +84,6 @@ class IncidentAnalysisService if ($a['total'] === $b['total']) { return strcasecmp($a['student_name'], $b['student_name']); } - return $b['total'] <=> $a['total']; }); diff --git a/app/Services/Incidents/IncidentHistoryService.php b/app/Services/Incidents/IncidentHistoryService.php index f22f7779..36af2499 100644 --- a/app/Services/Incidents/IncidentHistoryService.php +++ b/app/Services/Incidents/IncidentHistoryService.php @@ -6,7 +6,9 @@ use App\Models\Incident; class IncidentHistoryService { - public function __construct(private IncidentLookupService $lookup) {} + public function __construct(private IncidentLookupService $lookup) + { + } public function history(?string $schoolYear, ?string $semester): array { @@ -33,7 +35,7 @@ class IncidentHistoryService $updaterIds = []; foreach ($incidents as $incident) { foreach (['updated_by_open', 'updated_by_closed', 'updated_by_canceled'] as $key) { - if (! empty($incident[$key])) { + if (!empty($incident[$key])) { $updaterIds[] = (int) $incident[$key]; } } @@ -44,7 +46,7 @@ class IncidentHistoryService foreach ($incidents as &$incident) { foreach (['updated_by_open', 'updated_by_closed', 'updated_by_canceled'] as $key) { $id = (int) ($incident[$key] ?? 0); - $incident[$key.'_name'] = $id > 0 ? ($updaterMap[$id] ?? null) : null; + $incident[$key . '_name'] = $id > 0 ? ($updaterMap[$id] ?? null) : null; } $gradeValue = $incident['grade'] ?? ''; diff --git a/app/Services/Incidents/IncidentLookupService.php b/app/Services/Incidents/IncidentLookupService.php index ca572ef4..81119202 100644 --- a/app/Services/Incidents/IncidentLookupService.php +++ b/app/Services/Incidents/IncidentLookupService.php @@ -4,7 +4,6 @@ namespace App\Services\Incidents; use App\Models\ClassSection; use App\Models\StudentClass; -use Illuminate\Contracts\Support\Arrayable; use Illuminate\Support\Facades\DB; class IncidentLookupService @@ -73,7 +72,7 @@ class IncidentLookupService ->select('id', 'firstname', 'lastname') ->whereIn('id', $userIds) ->get() - ->map(fn ($row) => $row instanceof Arrayable ? $row->toArray() : (array) $row) + ->map(fn ($row) => $row instanceof \Illuminate\Contracts\Support\Arrayable ? $row->toArray() : (array) $row) ->all(); $map = []; @@ -82,8 +81,8 @@ class IncidentLookupService if ($id === null) { continue; } - $name = trim((string) ($row['firstname'] ?? '').' '.(string) ($row['lastname'] ?? '')); - $map[(int) $id] = $name !== '' ? $name : ('User #'.$id); + $name = trim((string) ($row['firstname'] ?? '') . ' ' . (string) ($row['lastname'] ?? '')); + $map[(int) $id] = $name !== '' ? $name : ('User #' . $id); } return $map; diff --git a/app/Services/Inventory/InventoryCategoryService.php b/app/Services/Inventory/InventoryCategoryService.php index 69a513b5..96d9f2b1 100644 --- a/app/Services/Inventory/InventoryCategoryService.php +++ b/app/Services/Inventory/InventoryCategoryService.php @@ -3,6 +3,7 @@ namespace App\Services\Inventory; use App\Models\InventoryCategory; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; class InventoryCategoryService @@ -15,7 +16,6 @@ class InventoryCategoryService if ($type) { $query->where('type', $this->normalizeType($type)); } - return $query->get()->toArray(); } @@ -37,8 +37,7 @@ class InventoryCategoryService try { $category = InventoryCategory::query()->create($payload); } catch (\Throwable $e) { - Log::error('Inventory category create failed: '.$e->getMessage()); - + Log::error('Inventory category create failed: ' . $e->getMessage()); return ['ok' => false, 'message' => 'Failed to save category.']; } @@ -48,7 +47,7 @@ class InventoryCategoryService public function update(int $id, array $data): array { $category = InventoryCategory::query()->find($id); - if (! $category) { + if (!$category) { return ['ok' => false, 'message' => 'Category not found.']; } @@ -69,8 +68,7 @@ class InventoryCategoryService try { $category->update($payload); } catch (\Throwable $e) { - Log::error('Inventory category update failed: '.$e->getMessage()); - + Log::error('Inventory category update failed: ' . $e->getMessage()); return ['ok' => false, 'message' => 'Failed to save category.']; } @@ -80,15 +78,14 @@ class InventoryCategoryService public function delete(int $id): array { $category = InventoryCategory::query()->find($id); - if (! $category) { + if (!$category) { return ['ok' => false, 'message' => 'Category not found.']; } try { $category->delete(); } catch (\Throwable $e) { - Log::error('Inventory category delete failed: '.$e->getMessage()); - + Log::error('Inventory category delete failed: ' . $e->getMessage()); return ['ok' => false, 'message' => 'Failed to delete category.']; } @@ -132,7 +129,6 @@ class InventoryCategoryService private function normalizeType(?string $type): string { $type = strtolower((string) $type); - return in_array($type, $this->types, true) ? $type : 'office'; } } diff --git a/app/Services/Inventory/InventoryItemService.php b/app/Services/Inventory/InventoryItemService.php index 9805872b..aaa14c20 100644 --- a/app/Services/Inventory/InventoryItemService.php +++ b/app/Services/Inventory/InventoryItemService.php @@ -2,8 +2,8 @@ namespace App\Services\Inventory; -use App\Models\InventoryCategory; use App\Models\InventoryItem; +use App\Models\InventoryCategory; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; @@ -14,7 +14,8 @@ class InventoryItemService public function __construct( private InventoryContextService $context, private InventoryMovementService $movementService - ) {} + ) { + } public function list(array $filters = []): array { @@ -31,7 +32,7 @@ class InventoryItemService $builder->where('semester', $semester); } if ($q !== '') { - $builder->where('name', 'like', '%'.$q.'%'); + $builder->where('name', 'like', '%' . $q . '%'); } $items = $builder->orderBy('name')->get()->toArray(); @@ -61,7 +62,6 @@ class InventoryItemService public function find(int $id): ?array { $item = InventoryItem::query()->find($id); - return $item ? $item->toArray() : null; } @@ -78,12 +78,10 @@ class InventoryItemService } else { $this->movementService->recalcQuantity((int) $item->id); } - return ['ok' => true, 'item' => $item->toArray()]; }); } catch (\Throwable $e) { - Log::error('Inventory item create failed: '.$e->getMessage()); - + Log::error('Inventory item create failed: ' . $e->getMessage()); return ['ok' => false, 'message' => 'Failed to create item.']; } } @@ -91,7 +89,7 @@ class InventoryItemService public function update(int $id, array $data, ?int $userId = null): array { $item = InventoryItem::query()->find($id); - if (! $item) { + if (!$item) { return ['ok' => false, 'message' => 'Item not found.']; } @@ -101,8 +99,7 @@ class InventoryItemService try { $item->update($payload); } catch (\Throwable $e) { - Log::error('Inventory item update failed: '.$e->getMessage()); - + Log::error('Inventory item update failed: ' . $e->getMessage()); return ['ok' => false, 'message' => 'Failed to update item.']; } @@ -112,15 +109,14 @@ class InventoryItemService public function delete(int $id): array { $item = InventoryItem::query()->find($id); - if (! $item) { + if (!$item) { return ['ok' => false, 'message' => 'Item not found.']; } try { $item->delete(); } catch (\Throwable $e) { - Log::error('Inventory item delete failed: '.$e->getMessage()); - + Log::error('Inventory item delete failed: ' . $e->getMessage()); return ['ok' => false, 'message' => 'Failed to delete item.']; } @@ -130,7 +126,7 @@ class InventoryItemService public function auditClassroom(int $itemId, array $data, ?int $userId = null): array { $item = InventoryItem::query()->find($itemId); - if (! $item || $item->type !== 'classroom') { + if (!$item || $item->type !== 'classroom') { return ['ok' => false, 'message' => 'Invalid classroom item.']; } @@ -163,7 +159,7 @@ class InventoryItemService $totalIn += -$deltaMiss; } - if ($totalOut > 0 && ! $this->movementService->recordMovement($itemId, -$totalOut, 'out', 'Audit: loss/missing increased')) { + if ($totalOut > 0 && !$this->movementService->recordMovement($itemId, -$totalOut, 'out', 'Audit: loss/missing increased')) { return ['ok' => false, 'message' => 'Not enough stock to apply audit.']; } if ($totalIn > 0) { @@ -186,7 +182,7 @@ class InventoryItemService public function adjustStock(int $itemId, array $data, ?int $userId = null): array { $item = InventoryItem::query()->find($itemId); - if (! $item) { + if (!$item) { return ['ok' => false, 'message' => 'Item not found.']; } @@ -203,7 +199,7 @@ class InventoryItemService $type = ($mode === 'out') ? 'out' : (($mode === 'in') ? 'in' : 'adjust'); $ok = $this->movementService->recordMovement($itemId, $delta, $type, $reason, $note, null, null, $userId); - if (! $ok) { + if (!$ok) { return ['ok' => false, 'message' => 'Not enough stock to apply adjustment.']; } @@ -246,7 +242,6 @@ class InventoryItemService private function normalizeType(?string $type): string { $type = strtolower((string) $type); - return in_array($type, $this->types, true) ? $type : 'classroom'; } @@ -272,9 +267,8 @@ class InventoryItemService } else { $out = []; foreach ($ids as $id) { - $out[$id] = 'User #'.$id; + $out[$id] = 'User #' . $id; } - return $out; } @@ -282,9 +276,8 @@ class InventoryItemService $map = []; foreach ($rows as $r) { $disp = trim((string) ($r->display_name ?? '')); - $map[(int) $r->id] = $disp !== '' ? $disp : ('User #'.$r->id); + $map[(int) $r->id] = $disp !== '' ? $disp : ('User #' . $r->id); } - return $map; } @@ -304,7 +297,7 @@ class InventoryItemService ->all(); $currentYear = $this->context->schoolYear(); - if ($currentYear && ! in_array($currentYear, $years, true)) { + if ($currentYear && !in_array($currentYear, $years, true)) { array_unshift($years, $currentYear); } diff --git a/app/Services/Inventory/InventoryMovementService.php b/app/Services/Inventory/InventoryMovementService.php index 7bc55fa2..daa9e524 100644 --- a/app/Services/Inventory/InventoryMovementService.php +++ b/app/Services/Inventory/InventoryMovementService.php @@ -9,7 +9,9 @@ use Illuminate\Support\Facades\Log; class InventoryMovementService { - public function __construct(private InventoryContextService $context) {} + public function __construct(private InventoryContextService $context) + { + } public function list(array $filters = []): array { @@ -49,7 +51,7 @@ class InventoryMovementService $qtyChange = (int) ($data['qty_change'] ?? 0); $item = InventoryItem::query()->find($itemId); - if (! $item) { + if (!$item) { return ['ok' => false, 'message' => 'Selected item not found.']; } @@ -76,8 +78,7 @@ class InventoryMovementService InventoryMovement::query()->create($payload); $this->recalcQuantity($itemId); } catch (\Throwable $e) { - Log::error('Inventory movement create failed: '.$e->getMessage()); - + Log::error('Inventory movement create failed: ' . $e->getMessage()); return ['ok' => false, 'message' => 'Could not save movement.']; } @@ -87,7 +88,7 @@ class InventoryMovementService public function update(int $id, array $data): array { $movement = InventoryMovement::query()->find($id); - if (! $movement) { + if (!$movement) { return ['ok' => false, 'message' => 'Movement not found.']; } @@ -117,8 +118,7 @@ class InventoryMovementService $movement->update($payload); $this->recalcQuantity($itemId); } catch (\Throwable $e) { - Log::error('Inventory movement update failed: '.$e->getMessage()); - + Log::error('Inventory movement update failed: ' . $e->getMessage()); return ['ok' => false, 'message' => 'Could not update movement.']; } @@ -128,7 +128,7 @@ class InventoryMovementService public function delete(int $id): array { $movement = InventoryMovement::query()->find($id); - if (! $movement) { + if (!$movement) { return ['ok' => false, 'message' => 'Movement not found.']; } @@ -139,8 +139,7 @@ class InventoryMovementService $this->recalcQuantity($itemId); } } catch (\Throwable $e) { - Log::error('Inventory movement delete failed: '.$e->getMessage()); - + Log::error('Inventory movement delete failed: ' . $e->getMessage()); return ['ok' => false, 'message' => 'Could not delete movement.']; } @@ -170,8 +169,7 @@ class InventoryMovementService $this->recalcQuantity($itemId); } } catch (\Throwable $e) { - Log::error('Inventory movement bulk delete failed: '.$e->getMessage()); - + Log::error('Inventory movement bulk delete failed: ' . $e->getMessage()); return ['ok' => false, 'message' => 'Bulk delete failed.']; } @@ -190,7 +188,7 @@ class InventoryMovementService ): bool { $isInitial = ($type === 'initial'); - if (! $isInitial) { + if (!$isInitial) { $this->ensureInitialMovement($itemId); $this->ensureInitialMovementForYear($itemId, $this->context->schoolYear()); $this->recalcQuantity($itemId); @@ -219,7 +217,6 @@ class InventoryMovementService ]); $this->recalcQuantity($itemId); - return true; } @@ -236,7 +233,7 @@ class InventoryMovementService } $item = InventoryItem::query()->select('id', 'type', 'quantity', 'needs_repair_qty', 'good_qty')->find($itemId); - if (! $item) { + if (!$item) { return; } @@ -253,7 +250,7 @@ class InventoryMovementService } } - if (! empty($updates)) { + if (!empty($updates)) { $item->update($updates); } } @@ -284,7 +281,7 @@ class InventoryMovementService ->where('school_year', $schoolYear) ->exists(); - if (! $hasAnyThisYear) { + if (!$hasAnyThisYear) { $onHand = (int) (InventoryItem::query()->where('id', $itemId)->value('quantity') ?? 0); InventoryMovement::query()->create([ 'item_id' => $itemId, @@ -293,7 +290,6 @@ class InventoryMovementService 'school_year' => $schoolYear, 'semester' => $this->context->semester() ?: null, ]); - return; } @@ -303,7 +299,7 @@ class InventoryMovementService ->where('movement_type', 'initial') ->exists(); - if (! $hasInitial) { + if (!$hasInitial) { $onHand = (int) (InventoryItem::query()->where('id', $itemId)->value('quantity') ?? 0); $netYear = (int) (InventoryMovement::query() ->where('item_id', $itemId) @@ -326,14 +322,12 @@ class InventoryMovementService if (in_array($movementType, ['out', 'distribution'], true) && $qtyChange > 0) { return -abs($qtyChange); } - return $qtyChange; } private function wouldGoNegative(int $itemId, int $delta): bool { $onHand = (int) (InventoryItem::query()->where('id', $itemId)->value('quantity') ?? 0); - return $onHand + $delta < 0; } @@ -342,7 +336,6 @@ class InventoryMovementService if ($val === null || $val === '' || $val === '0') { return null; } - return (int) $val; } } diff --git a/app/Services/Inventory/InventorySummaryService.php b/app/Services/Inventory/InventorySummaryService.php index 5254f847..6d91e5e9 100644 --- a/app/Services/Inventory/InventorySummaryService.php +++ b/app/Services/Inventory/InventorySummaryService.php @@ -7,7 +7,9 @@ use Illuminate\Support\Facades\DB; class InventorySummaryService { - public function __construct(private InventoryContextService $context) {} + public function __construct(private InventoryContextService $context) + { + } public function summary(string $type, ?string $schoolYear): array { @@ -22,7 +24,7 @@ class InventorySummaryService $itemIds = array_map(static fn ($i) => (int) ($i['id'] ?? 0), $items); $agg = []; - if (! empty($itemIds)) { + if (!empty($itemIds)) { $qb = DB::table('inventory_movements') ->selectRaw('item_id, SUM(CASE WHEN qty_change>0 THEN qty_change ELSE 0 END) AS received, @@ -63,7 +65,7 @@ class InventorySummaryService ->leftJoin('users as u', 'u.id', '=', 'i.updated_by') ->orderBy('i.name'); - if (! $isAllYears) { + if (!$isAllYears) { $qbItems->where('i.school_year', $selectedYear); } @@ -85,7 +87,7 @@ class InventorySummaryService 'variance' => 0, ]; - if (! empty($items)) { + if (!empty($items)) { $itemIds = array_map('intval', array_column($items, 'id')); $aggById = []; $qb = DB::table('inventory_movements as m') @@ -100,7 +102,7 @@ class InventorySummaryService ->whereIn('m.item_id', $itemIds) ->groupBy('m.item_id'); - if (! $isAllYears) { + if (!$isAllYears) { $qb->where('m.school_year', $selectedYear); } @@ -180,7 +182,7 @@ class InventorySummaryService ->all(); $currentYear = $this->context->schoolYear(); - if ($currentYear && ! in_array($currentYear, $years, true)) { + if ($currentYear && !in_array($currentYear, $years, true)) { array_unshift($years, $currentYear); } diff --git a/app/Services/Inventory/InventoryTeacherDistributionService.php b/app/Services/Inventory/InventoryTeacherDistributionService.php index 25501c9c..e5b76634 100644 --- a/app/Services/Inventory/InventoryTeacherDistributionService.php +++ b/app/Services/Inventory/InventoryTeacherDistributionService.php @@ -9,18 +9,20 @@ use App\Models\StudentClass; use App\Models\TeacherClass; use App\Models\User; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Log; class InventoryTeacherDistributionService { public function __construct( private InventoryContextService $context, private InventoryMovementService $movementService - ) {} + ) { + } public function formData(int $userId, ?int $classSectionId, ?int $itemId): array { $ctx = $this->buildTeacherClassContext($userId); - if (! $ctx['ok']) { + if (!$ctx['ok']) { return $ctx; } @@ -31,7 +33,7 @@ class InventoryTeacherDistributionService : (int) ($classSectionId ?? ($ctx['defaultClassSectionId'] ?? 0)); $classID = null; - if (! empty($selectedClassId)) { + if (!empty($selectedClassId)) { $classID = ClassSection::getClassId($selectedClassId); $classID = $classID !== null ? (int) $classID : null; } @@ -41,7 +43,7 @@ class InventoryTeacherDistributionService ->leftJoin('inventory_categories as c', 'c.id', '=', 'i.category_id') ->where('i.type', 'book'); - if (! empty($classID)) { + if (!empty($classID)) { $builder->where(function ($q) use ($classID) { $q->where(function ($inner) use ($classID) { $inner->whereNotNull('c.grade_min') @@ -66,34 +68,27 @@ class InventoryTeacherDistributionService $name = $r['category_name'] ?? 'Uncategorized'; $gmin = $r['grade_min'] ?? null; $gmax = $r['grade_max'] ?? null; - if ($gmin === null && $gmax === null) { - return $name; - } - if ($gmin !== null && $gmax !== null) { - return $name.' (G'.(int) $gmin.'–G'.(int) $gmax.')'; - } - if ($gmin !== null) { - return $name.' (G'.(int) $gmin.'+)'; - } - - return $name.' (≤G'.(int) $gmax.')'; + if ($gmin === null && $gmax === null) return $name; + if ($gmin !== null && $gmax !== null) return $name . ' (G' . (int) $gmin . '–G' . (int) $gmax . ')'; + if ($gmin !== null) return $name . ' (G' . (int) $gmin . '+)'; + return $name . ' (≤G' . (int) $gmax . ')'; }; $booksGrouped = []; foreach ($rows as $r) { $catId = (int) ($r['category_id'] ?? 0); - if (! isset($booksGrouped[$catId])) { + if (!isset($booksGrouped[$catId])) { $booksGrouped[$catId] = [ 'label' => $labelFor($r), 'items' => [], ]; } $display = $r['name']; - if (! empty($r['isbn'])) { - $display .= ' — ISBN '.$r['isbn']; + if (!empty($r['isbn'])) { + $display .= ' — ISBN ' . $r['isbn']; } - if (! empty($r['edition'])) { - $display .= ' ('.$r['edition'].')'; + if (!empty($r['edition'])) { + $display .= ' (' . $r['edition'] . ')'; } $booksGrouped[$catId]['items'][] = [ @@ -128,9 +123,7 @@ class InventoryTeacherDistributionService ->all(); foreach ($rowsHist as $r) { $sid = (int) ($r['student_id'] ?? 0); - if ($sid) { - $already[$sid] = (int) ($r['qty'] ?? 0); - } + if ($sid) $already[$sid] = (int) ($r['qty'] ?? 0); } } @@ -161,7 +154,7 @@ class InventoryTeacherDistributionService public function distribute(int $userId, array $payload): array { $ctx = $this->buildTeacherClassContext($userId); - if (! $ctx['ok']) { + if (!$ctx['ok']) { return $ctx; } @@ -175,12 +168,12 @@ class InventoryTeacherDistributionService ? (int) $classIds[0] : (in_array($postedClassId, $classIds, true) ? $postedClassId : 0); - if (! $classSectionId) { + if (!$classSectionId) { return ['ok' => false, 'message' => 'Invalid class selection.']; } $book = InventoryItem::query()->find($itemId); - if (! $book || $book->type !== 'book') { + if (!$book || $book->type !== 'book') { return ['ok' => false, 'message' => 'Invalid book.']; } @@ -188,9 +181,7 @@ class InventoryTeacherDistributionService $validIds = []; foreach ($students as $st) { $sid = $st['student_id'] ?? ($st['id'] ?? null); - if ($sid) { - $validIds[(int) $sid] = true; - } + if ($sid) $validIds[(int) $sid] = true; } $selectedIds = array_values(array_filter($selectedIds, fn ($sid) => isset($validIds[$sid]))); @@ -209,9 +200,7 @@ class InventoryTeacherDistributionService ->all(); foreach ($rows as $r) { $sid = (int) ($r['student_id'] ?? 0); - if ($sid) { - $already[$sid] = (int) ($r['qty'] ?? 0); - } + if ($sid) $already[$sid] = (int) ($r['qty'] ?? 0); } $toGive = []; @@ -254,7 +243,7 @@ class InventoryTeacherDistributionService } $user = User::query()->find($userId); - if (! $user) { + if (!$user) { return ['ok' => false, 'message' => 'User not found.']; } @@ -264,13 +253,13 @@ class InventoryTeacherDistributionService ->pluck('r.name') ->toArray(); - if (! in_array('teacher', $roles, true) && ! in_array('teacher_assistant', $roles, true)) { + if (!in_array('teacher', $roles, true) && !in_array('teacher_assistant', $roles, true)) { return ['ok' => false, 'message' => 'Access denied.']; } $classSectionIds = array_column($classes, 'class_section_id'); $studentsData = []; - if (! empty($classSectionIds)) { + if (!empty($classSectionIds)) { $students = StudentClass::getStudentsByClassSectionIds($classSectionIds); foreach ($students as $student) { $studentsData[$student['class_section_id']][] = $student; @@ -289,7 +278,7 @@ class InventoryTeacherDistributionService $taUser = User::query()->find($ta['teacher_id'] ?? 0); if ($taUser) { $csid = $ta['class_section_id']; - $taNames[$csid][] = trim(($taUser->firstname ?? '').' '.($taUser->lastname ?? '')); + $taNames[$csid][] = trim(($taUser->firstname ?? '') . ' ' . ($taUser->lastname ?? '')); } } diff --git a/app/Services/Inventory/SupplierService.php b/app/Services/Inventory/SupplierService.php index 1e6a8fde..a852d638 100644 --- a/app/Services/Inventory/SupplierService.php +++ b/app/Services/Inventory/SupplierService.php @@ -13,9 +13,9 @@ class SupplierService $query = Supplier::query(); if ($q !== '') { $query->where(function ($builder) use ($q) { - $builder->where('name', 'like', '%'.$q.'%') - ->orWhere('email', 'like', '%'.$q.'%') - ->orWhere('phone', 'like', '%'.$q.'%'); + $builder->where('name', 'like', '%' . $q . '%') + ->orWhere('email', 'like', '%' . $q . '%') + ->orWhere('phone', 'like', '%' . $q . '%'); }); } @@ -37,8 +37,7 @@ class SupplierService try { $supplier = Supplier::query()->create($this->payload($data)); } catch (\Throwable $e) { - Log::error('Supplier create failed: '.$e->getMessage()); - + Log::error('Supplier create failed: ' . $e->getMessage()); return ['ok' => false, 'message' => 'Failed to save supplier.']; } @@ -48,15 +47,14 @@ class SupplierService public function update(int $id, array $data): array { $supplier = Supplier::query()->find($id); - if (! $supplier) { + if (!$supplier) { return ['ok' => false, 'message' => 'Supplier not found.']; } try { $supplier->update($this->payload($data)); } catch (\Throwable $e) { - Log::error('Supplier update failed: '.$e->getMessage()); - + Log::error('Supplier update failed: ' . $e->getMessage()); return ['ok' => false, 'message' => 'Failed to update supplier.']; } @@ -66,15 +64,14 @@ class SupplierService public function delete(int $id): array { $supplier = Supplier::query()->find($id); - if (! $supplier) { + if (!$supplier) { return ['ok' => false, 'message' => 'Supplier not found.']; } try { $supplier->delete(); } catch (\Throwable $e) { - Log::error('Supplier delete failed: '.$e->getMessage()); - + Log::error('Supplier delete failed: ' . $e->getMessage()); return ['ok' => false, 'message' => 'Failed to delete supplier.']; } diff --git a/app/Services/Inventory/SupplyCategoryService.php b/app/Services/Inventory/SupplyCategoryService.php index 273553a3..b435ea67 100644 --- a/app/Services/Inventory/SupplyCategoryService.php +++ b/app/Services/Inventory/SupplyCategoryService.php @@ -19,8 +19,7 @@ class SupplyCategoryService 'name' => (string) ($data['name'] ?? ''), ]); } catch (\Throwable $e) { - Log::error('Supply category create failed: '.$e->getMessage()); - + Log::error('Supply category create failed: ' . $e->getMessage()); return ['ok' => false, 'message' => 'Failed to save category.']; } @@ -30,15 +29,14 @@ class SupplyCategoryService public function update(int $id, array $data): array { $category = SupplyCategory::query()->find($id); - if (! $category) { + if (!$category) { return ['ok' => false, 'message' => 'Category not found.']; } try { $category->update(['name' => (string) ($data['name'] ?? '')]); } catch (\Throwable $e) { - Log::error('Supply category update failed: '.$e->getMessage()); - + Log::error('Supply category update failed: ' . $e->getMessage()); return ['ok' => false, 'message' => 'Failed to update category.']; } @@ -48,15 +46,14 @@ class SupplyCategoryService public function delete(int $id): array { $category = SupplyCategory::query()->find($id); - if (! $category) { + if (!$category) { return ['ok' => false, 'message' => 'Category not found.']; } try { $category->delete(); } catch (\Throwable $e) { - Log::error('Supply category delete failed: '.$e->getMessage()); - + Log::error('Supply category delete failed: ' . $e->getMessage()); return ['ok' => false, 'message' => 'Failed to delete category.']; } diff --git a/app/Services/Invoices/InvoiceConfigService.php b/app/Services/Invoices/InvoiceConfigService.php index cff9bca1..1ccb9cc7 100644 --- a/app/Services/Invoices/InvoiceConfigService.php +++ b/app/Services/Invoices/InvoiceConfigService.php @@ -19,7 +19,6 @@ class InvoiceConfigService public function getDueDate(): ?string { $value = Configuration::getConfig('due_date'); - return $value ? (string) $value : null; } @@ -46,7 +45,7 @@ class InvoiceConfigService public function getRefundDeadline(): ?string { $value = Configuration::getConfig('refund_deadline'); - if (! $value) { + if (!$value) { return null; } diff --git a/app/Services/Invoices/InvoiceGenerationService.php b/app/Services/Invoices/InvoiceGenerationService.php index 7ed4f7dd..ddc5c5db 100644 --- a/app/Services/Invoices/InvoiceGenerationService.php +++ b/app/Services/Invoices/InvoiceGenerationService.php @@ -18,7 +18,8 @@ class InvoiceGenerationService public function __construct( private InvoiceConfigService $config, private InvoiceTuitionService $tuitionService - ) {} + ) { + } public function generateInvoice(int $parentId, ?string $schoolYear = null): array { @@ -63,13 +64,11 @@ class InvoiceGenerationService $registeredKids = array_values(array_filter($registeredKids, function ($student) use ($schoolYear) { $sid = (int) ($student['student_id'] ?? 0); - return $sid > 0 && StudentClass::hasNonEventAssignment($sid, $schoolYear); })); $withdrawnKids = array_values(array_filter($withdrawnKids, function ($student) use ($schoolYear) { $sid = (int) ($student['student_id'] ?? 0); - return $sid > 0 && StudentClass::hasNonEventAssignment($sid, $schoolYear); })); @@ -84,7 +83,7 @@ class InvoiceGenerationService $this->recalculateAndUpdateDiscount($parentId, $schoolYear, $tuitionFee, $enrollments); - $refundPaid = (new Refund)->getTotalApprovedRefundByParentIdAndSchoolYear($parentId, $schoolYear); + $refundPaid = (new Refund())->getTotalApprovedRefundByParentIdAndSchoolYear($parentId, $schoolYear); $totalPaid = Payment::getTotalPaidByParentId($parentId, $schoolYear); $totalAmount = max(0.0, (float) $tuitionFee) + (float) $eventChargeTotal; @@ -94,9 +93,9 @@ class InvoiceGenerationService $updatedIds = []; $insertId = null; - if (! empty($existingInvoices)) { + if (!empty($existingInvoices)) { foreach ($existingInvoices as $invoice) { - if (! isset($invoice['id'])) { + if (!isset($invoice['id'])) { continue; } @@ -128,14 +127,14 @@ class InvoiceGenerationService } else { $schoolId = User::getSchoolIdByUserId($parentId); $invoiceNumber = $schoolId - ? ('INV-'.$schoolId.'-'.uniqid()) + ? ('INV-' . $schoolId . '-' . uniqid()) : uniqid('INV-'); $issueUtc = (new DateTime('now', new DateTimeZone('UTC')))->format('Y-m-d H:i:s'); $dueUtc = null; $dueDate = $this->config->getDueDate(); - if (! empty($dueDate)) { - $dueLocal = new DateTime($dueDate.' 19:59:59', new DateTimeZone($this->config->getTimezone())); + if (!empty($dueDate)) { + $dueLocal = new DateTime($dueDate . ' 19:59:59', new DateTimeZone($this->config->getTimezone())); $dueLocal->setTimezone(new DateTimeZone('UTC')); $dueUtc = $dueLocal->format('Y-m-d H:i:s'); } @@ -181,7 +180,7 @@ class InvoiceGenerationService } foreach ($invoices as $invoice) { - if (! isset($invoice['id'])) { + if (!isset($invoice['id'])) { continue; } @@ -194,7 +193,7 @@ class InvoiceGenerationService ->where('i.school_year', $schoolYear) ->first(); - if (! $discountUsage) { + if (!$discountUsage) { continue; } diff --git a/app/Services/Invoices/InvoiceGradeService.php b/app/Services/Invoices/InvoiceGradeService.php index 7609503a..41d54c1e 100644 --- a/app/Services/Invoices/InvoiceGradeService.php +++ b/app/Services/Invoices/InvoiceGradeService.php @@ -4,12 +4,13 @@ namespace App\Services\Invoices; class InvoiceGradeService { - public function __construct(private int $gradeFee) {} + public function __construct(private int $gradeFee) + { + } public function isKindergarten(string $grade): bool { $g = strtolower(trim($grade)); - return in_array($g, ['KG', 'kg', 'k', 'kindergarten', 'k-g', 'k.g', 'k g'], true); } @@ -28,7 +29,6 @@ class InvoiceGradeService public function gradeLevelInt(string $grade): int { $gl = $this->getGradeLevel($grade); - return (int) ($gl['level'] ?? 0); } @@ -39,11 +39,10 @@ class InvoiceGradeService if ($num === 13) { return ['level' => 1, 'suffix' => '', 'classId' => 13]; } - return ['level' => $num, 'suffix' => '', 'classId' => null]; } - if (! is_string($grade)) { + if (!is_string($grade)) { return ['level' => 999, 'suffix' => '', 'classId' => null]; } @@ -64,7 +63,6 @@ class InvoiceGradeService if (preg_match('/^Y(?:OUTH)?(?:\s*(\d+))?$/', $g, $m)) { $n = isset($m[1]) && $m[1] !== '' ? max(1, (int) $m[1]) : 1; - return [ 'level' => $this->gradeFee + $n, 'suffix' => '', diff --git a/app/Services/Invoices/InvoiceManagementService.php b/app/Services/Invoices/InvoiceManagementService.php index d81a0886..b410ec7c 100644 --- a/app/Services/Invoices/InvoiceManagementService.php +++ b/app/Services/Invoices/InvoiceManagementService.php @@ -5,12 +5,15 @@ namespace App\Services\Invoices; use App\Models\Enrollment; use App\Models\Invoice; use App\Models\Student; +use App\Models\StudentClass; use App\Models\User; use Illuminate\Support\Facades\DB; class InvoiceManagementService { - public function __construct(private InvoiceConfigService $config) {} + public function __construct(private InvoiceConfigService $config) + { + } public function getManagementData(?string $schoolYear): array { @@ -44,7 +47,7 @@ class InvoiceManagementService ->all(); $parentData = [ - 'parent_name' => trim(($parent['firstname'] ?? '').' '.($parent['lastname'] ?? '')), + 'parent_name' => trim(($parent['firstname'] ?? '') . ' ' . ($parent['lastname'] ?? '')), 'parent_id' => (int) $parent['id'], 'enrolledKids' => [], 'withdrawnKids' => [], @@ -57,7 +60,7 @@ class InvoiceManagementService $invoices = Invoice::getInvoicesByParentId((int) $parent['id'], $schoolYear); foreach ($invoices as $invoice) { - if (! $invoice) { + if (!$invoice) { continue; } @@ -65,13 +68,13 @@ class InvoiceManagementService $parentData['last_updated'] = $invoice['updated_at'] ?? null; $parentData['invoice_id'] = $invoice['id'] ?? null; - if (! empty($invoice['issue_date'])) { + if (!empty($invoice['issue_date'])) { $tzName = $this->config->getTimezone(); $parentData['invoice_date'] = (new \DateTimeImmutable($invoice['issue_date'], new \DateTimeZone('UTC'))) ->setTimezone(new \DateTimeZone($tzName)) ->format('Y-m-d H:i:s'); } else { - $parentData['invoice_date'] = ! empty($invoice['updated_at']) + $parentData['invoice_date'] = !empty($invoice['updated_at']) ? date('Y-m-d H:i:s', strtotime($invoice['updated_at'])) : null; } @@ -112,7 +115,7 @@ class InvoiceManagementService foreach ($enrollments as $enrollment) { $kid = [ - 'name' => trim(($student['firstname'] ?? '').' '.($student['lastname'] ?? '')), + 'name' => trim(($student['firstname'] ?? '') . ' ' . ($student['lastname'] ?? '')), 'grade' => $grade, 'tuition_fee' => (float) ($enrollment['tuition_fee'] ?? 0), ]; @@ -133,7 +136,7 @@ class InvoiceManagementService } } - if (! empty($parentData['enrolledKids']) || ! empty($parentData['withdrawnKids'])) { + if (!empty($parentData['enrolledKids']) || !empty($parentData['withdrawnKids'])) { $invoiceData[] = $parentData; } } diff --git a/app/Services/Invoices/InvoicePaymentService.php b/app/Services/Invoices/InvoicePaymentService.php index 51bb2275..ec1ad56c 100644 --- a/app/Services/Invoices/InvoicePaymentService.php +++ b/app/Services/Invoices/InvoicePaymentService.php @@ -8,7 +8,9 @@ use Illuminate\Support\Facades\DB; class InvoicePaymentService { - public function __construct(private InvoiceConfigService $config) {} + public function __construct(private InvoiceConfigService $config) + { + } public function getParentInvoiceSummary(int $parentId, ?string $schoolYear): array { @@ -24,9 +26,9 @@ class InvoicePaymentService ->all(); $selectedYear = $schoolYear ?: null; - if (! $selectedYear) { + if (!$selectedYear) { $hasCurrentYear = in_array($currentSchoolYear, array_column($schoolYears, 'school_year'), true); - $selectedYear = $hasCurrentYear ? $currentSchoolYear : (! empty($schoolYears) ? $schoolYears[0]['school_year'] : null); + $selectedYear = $hasCurrentYear ? $currentSchoolYear : (!empty($schoolYears) ? $schoolYears[0]['school_year'] : null); } $invoices = []; @@ -36,7 +38,7 @@ class InvoicePaymentService $invoiceIds = array_column($invoices, 'id'); $refunds = []; - if (! empty($invoiceIds)) { + if (!empty($invoiceIds)) { $refundResults = DB::table('refunds') ->selectRaw('invoice_id, COALESCE(SUM(refund_paid_amount),0) as refund_paid_amount') ->whereIn('invoice_id', $invoiceIds) @@ -52,7 +54,7 @@ class InvoicePaymentService } $lastPayments = []; - if (! empty($invoiceIds)) { + if (!empty($invoiceIds)) { $paymentResults = Payment::getPaymentsByInvoice($invoiceIds); foreach ($paymentResults as $payment) { $lastPayments[$payment['invoice_id']] = [ diff --git a/app/Services/Invoices/InvoicePdfService.php b/app/Services/Invoices/InvoicePdfService.php index a69482f1..ab47d5d6 100644 --- a/app/Services/Invoices/InvoicePdfService.php +++ b/app/Services/Invoices/InvoicePdfService.php @@ -9,6 +9,7 @@ use App\Models\Enrollment; use App\Models\EventCharges; use App\Models\Invoice; use App\Models\Payment; +use App\Models\Refund; use App\Models\Student; use App\Models\StudentClass; use App\Models\User; @@ -20,7 +21,8 @@ class InvoicePdfService public function __construct( private InvoiceConfigService $config, private InvoiceGradeService $grades - ) {} + ) { + } public function buildPdf(int $invoiceId): string { @@ -40,7 +42,7 @@ class InvoicePdfService private function prepareInvoiceData(int $invoiceId): array { $invoice = Invoice::query()->find($invoiceId); - if (! $invoice) { + if (!$invoice) { return ['error' => 'No invoice was generated. Please contact the school administration.']; } @@ -67,7 +69,7 @@ class InvoicePdfService $payments = $paymentsQuery->get()->map(fn ($r) => $r->toArray())->all(); $parent = User::query()->find($parentId); - if (! $parent) { + if (!$parent) { return ['error' => 'Parent associated with the invoice was not found.']; } @@ -87,7 +89,7 @@ class InvoicePdfService foreach ($enrollments as $enrollment) { $student = Student::query()->find($enrollment['student_id']); - if (! $student) { + if (!$student) { continue; } @@ -110,13 +112,11 @@ class InvoicePdfService $registeredKids = array_values(array_filter($registeredKids, function ($student) use ($schoolYear) { $sid = (int) ($student['student_id'] ?? 0); - return $sid > 0 && StudentClass::hasNonEventAssignment($sid, $schoolYear); })); $withdrawnKids = array_values(array_filter($withdrawnKids, function ($student) use ($schoolYear) { $sid = (int) ($student['student_id'] ?? 0); - return $sid > 0 && StudentClass::hasNonEventAssignment($sid, $schoolYear); })); @@ -148,10 +148,8 @@ class InvoicePdfService if ($isRegular) { $fee = $regularCount === 0 ? $firstFee : $secondFee; $regularCount++; - return $fee; } - return $youthFee; }; @@ -162,7 +160,7 @@ class InvoicePdfService $studentCharges[$student['student_id']] = ['unit_fee' => $unitFee, 'refund' => 0]; } - if (! $refundAllowed) { + if (!$refundAllowed) { foreach ($withdrawnKids as $student) { $gradeName = (string) ($student['grade'] ?? ''); $gradeLevel = $this->grades->gradeLevelInt($gradeName); @@ -175,7 +173,7 @@ class InvoicePdfService $studentIds = array_values(array_unique(array_map(static fn ($r) => (int) ($r['student_id'] ?? 0), array_merge($registeredKids, $withdrawnKids)))); $schoolIdMap = []; - if (! empty($studentIds)) { + if (!empty($studentIds)) { $rows = Student::query()->whereIn('id', $studentIds)->get(['id', 'school_id'])->all(); foreach ($rows as $row) { $schoolIdMap[(int) $row->id] = $row->school_id ?? 'N/A'; @@ -224,9 +222,9 @@ class InvoicePdfService $signed = abs($signed); } - $lineDate = ! empty($ac['created_at']) + $lineDate = !empty($ac['created_at']) ? date('Y-m-d', strtotime($ac['created_at'])) - : (! empty($invoice->created_at) ? local_date($invoice->created_at, 'Y-m-d') : local_date(utc_now(), 'Y-m-d')); + : (!empty($invoice->created_at) ? local_date($invoice->created_at, 'Y-m-d') : local_date(utc_now(), 'Y-m-d')); $desc = $ac['description'] ?? ''; if ($desc === '') { @@ -272,7 +270,7 @@ class InvoicePdfService $additionalChargesTotal = (float) ($data['additionalChargesTotal'] ?? 0); $refundsPaidTotal = (float) ($data['refundsPaidTotal'] ?? 0); - $pdf = new \FPDF; + $pdf = new \FPDF(); $pdf->AddPage(); $pdf->SetFont('Arial', 'B', 16); $pdf->Cell(0, 10, 'Invoice', 0, 1, 'C'); @@ -281,7 +279,7 @@ class InvoicePdfService $pdf->SetFont('Arial', 'B', 12); $pdf->Cell(40, 6, 'Parent Name:', 0, 0, 'L'); $pdf->SetFont('Arial', '', 12); - $pdf->Cell(0, 6, ($parent['firstname'] ?? '').' '.($parent['lastname'] ?? ''), 0, 1, 'L'); + $pdf->Cell(0, 6, ($parent['firstname'] ?? '') . ' ' . ($parent['lastname'] ?? ''), 0, 1, 'L'); $pdf->SetFont('Arial', 'B', 12); $pdf->Cell(40, 6, 'Invoice Number:', 0, 0, 'L'); @@ -308,7 +306,7 @@ class InvoicePdfService $tz = new \DateTimeZone($this->config->getTimezone()); $toLocal = function (?string $raw) use ($tz): \DateTimeImmutable { - if (! $raw) { + if (!$raw) { return new \DateTimeImmutable('now', $tz); } try { @@ -321,24 +319,24 @@ class InvoicePdfService foreach ($registeredKids as $student) { $id = $student['student_id']; $unit = (float) ($studentCharges[$id]['unit_fee'] ?? 0.0); - $name = $student['student_firstname'].' '.$student['student_lastname']; + $name = $student['student_firstname'] . ' ' . $student['student_lastname']; $gradeName = ClassSection::getClassSectionNameByClassId($student['grade']) ?? $student['grade']; - $push($toLocal($invoice['created_at'] ?? null), 'Registration of "'.$name.'" in '.$gradeName, $unit, 'registration'); + $push($toLocal($invoice['created_at'] ?? null), 'Registration of "' . $name . '" in ' . $gradeName, $unit, 'registration'); } foreach ($events as $event) { $studentName = 'N/A'; - if (! empty($event['student_id'])) { + if (!empty($event['student_id'])) { foreach ($students as $st) { if (($st['student_id'] ?? null) == $event['student_id']) { - $studentName = $st['student_firstname'].' '.$st['student_lastname']; + $studentName = $st['student_firstname'] . ' ' . $st['student_lastname']; break; } } } $amount = (float) ($event['charged'] ?? 0.0); $eventName = $event['event_name'] ?? 'Event'; - $push($toLocal($event['created_at'] ?? null), 'Event '.$eventName.' for "'.$studentName.'"', $amount, 'event'); + $push($toLocal($event['created_at'] ?? null), 'Event ' . $eventName . ' for "' . $studentName . '"', $amount, 'event'); } foreach ($additionalChargeLines as $line) { @@ -349,7 +347,7 @@ class InvoicePdfService foreach ($payments as $payment) { $amount = (float) ($payment['paid_amount'] ?? 0.0); $totalPaid += $amount; - $push($toLocal($payment['payment_date'] ?? null), 'Payment ('.($payment['payment_method'] ?? 'Payment').')', -$amount, 'payment'); + $push($toLocal($payment['payment_date'] ?? null), 'Payment (' . ($payment['payment_method'] ?? 'Payment') . ')', -$amount, 'payment'); } $totalDiscount = 0.0; @@ -379,7 +377,6 @@ class InvoicePdfService return $pa <=> $pb; } $cmp = $a['dt'] <=> $b['dt']; - return $cmp !== 0 ? $cmp : ($a['seq'] <=> $b['seq']); }); @@ -387,7 +384,7 @@ class InvoicePdfService foreach ($transactions as $t) { $pdf->Cell(30, 7, $t['dt']->format('m-d-Y'), 1); $pdf->Cell(130, 7, $t['description'], 1); - $pdf->Cell(30, 7, ($t['amount'] < 0 ? '-$' : '$').number_format(abs($t['amount']), 2), 1, 1, 'R'); + $pdf->Cell(30, 7, ($t['amount'] < 0 ? '-$' : '$') . number_format(abs($t['amount']), 2), 1, 1, 'R'); } $tuitionSubtotal = 0.0; @@ -409,38 +406,38 @@ class InvoicePdfService $pdf->SetFont('Arial', 'B', 12); $pdf->Cell(160, 7, 'Total Charges:', 0, 0, 'R'); $pdf->SetFont('Arial', '', 12); - $pdf->Cell(30, 7, '$'.number_format($totalAmount, 2), 0, 1, 'R'); + $pdf->Cell(30, 7, '$' . number_format($totalAmount, 2), 0, 1, 'R'); $pdf->SetFont('Arial', 'B', 12); $pdf->Cell(160, 7, 'Total Paid:', 0, 0, 'R'); $pdf->SetFont('Arial', '', 12); - $pdf->Cell(30, 7, '$'.number_format($totalPaid, 2), 0, 1, 'R'); + $pdf->Cell(30, 7, '$' . number_format($totalPaid, 2), 0, 1, 'R'); if ($totalDiscount > 0) { $pdf->SetFont('Arial', 'B', 12); $pdf->Cell(160, 7, 'Total Discount:', 0, 0, 'R'); $pdf->SetFont('Arial', '', 12); - $pdf->Cell(30, 7, '$'.number_format($totalDiscount, 2), 0, 1, 'R'); + $pdf->Cell(30, 7, '$' . number_format($totalDiscount, 2), 0, 1, 'R'); } if ($refundsPaidTotal > 0) { $pdf->SetFont('Arial', 'B', 12); $pdf->Cell(160, 7, 'Refunds Paid:', 0, 0, 'R'); $pdf->SetFont('Arial', '', 12); - $pdf->Cell(30, 7, '$'.number_format($refundsPaidTotal, 2), 0, 1, 'R'); + $pdf->Cell(30, 7, '$' . number_format($refundsPaidTotal, 2), 0, 1, 'R'); } $pdf->SetFont('Arial', 'B', 12); $pdf->Cell(160, 7, 'Balance Due:', 0, 0, 'R'); $pdf->SetFont('Arial', '', 12); - $pdf->Cell(30, 7, '$'.number_format($balance, 2), 0, 1, 'R'); + $pdf->Cell(30, 7, '$' . number_format($balance, 2), 0, 1, 'R'); return $pdf->Output('S'); } private function renderErrorPdf(string $message): string { - $pdf = new \FPDF; + $pdf = new \FPDF(); $pdf->AddPage(); $pdf->SetFont('Arial', 'B', 16); $pdf->Cell(0, 10, 'Error', 0, 1, 'C'); diff --git a/app/Services/Invoices/InvoiceService.php b/app/Services/Invoices/InvoiceService.php index 0d6b03c5..2325ab9d 100644 --- a/app/Services/Invoices/InvoiceService.php +++ b/app/Services/Invoices/InvoiceService.php @@ -13,7 +13,8 @@ class InvoiceService private InvoiceGenerationService $generation, private InvoiceTuitionService $tuition, private InvoiceGradeService $grades - ) {} + ) { + } public function getDefaultSchoolYear(): string { diff --git a/app/Services/Invoices/InvoiceTuitionService.php b/app/Services/Invoices/InvoiceTuitionService.php index b3df5523..967c579e 100644 --- a/app/Services/Invoices/InvoiceTuitionService.php +++ b/app/Services/Invoices/InvoiceTuitionService.php @@ -13,7 +13,8 @@ class InvoiceTuitionService private float $secondStudentFee, private float $youthFee, private string $timezone - ) {} + ) { + } public function calculateTuitionFee(array $registeredKids, array $withdrawnKids, ?string $refundDeadline): float { @@ -60,7 +61,7 @@ class InvoiceTuitionService private function isRefundAllowed(?string $refundDeadline): bool { - if (! $refundDeadline) { + if (!$refundDeadline) { return true; } @@ -68,7 +69,6 @@ class InvoiceTuitionService $tz = new \DateTimeZone($this->timezone); $today = new \DateTimeImmutable('today', $tz); $deadline = new \DateTimeImmutable($refundDeadline, $tz); - return $today <= $deadline; } catch (\Throwable $e) { return true; diff --git a/app/Services/Messaging/MessageCommandService.php b/app/Services/Messaging/MessageCommandService.php index 28ff9ab9..d4a8b54a 100644 --- a/app/Services/Messaging/MessageCommandService.php +++ b/app/Services/Messaging/MessageCommandService.php @@ -10,16 +10,18 @@ use RuntimeException; class MessageCommandService { - public function __construct(private GlobalConfigService $configService) {} + public function __construct(private GlobalConfigService $configService) + { + } public function create(int $senderId, array $payload, ?UploadedFile $attachment = null): Message { $recipientId = $payload['recipient_id'] ?? null; - if (! $recipientId && ! empty($payload['recipient_role'])) { + if (!$recipientId && !empty($payload['recipient_role'])) { $recipientId = $this->recipientIdFromRole($payload['recipient_role']); } - if (! $recipientId) { + if (!$recipientId) { throw new RuntimeException('Recipient is required.'); } @@ -40,7 +42,7 @@ class MessageCommandService 'message' => (string) $payload['message'], 'sent_datetime' => now(), 'read_status' => 0, - 'message_number' => 'MSG-'.now()->format('YmdHis'), + 'message_number' => 'MSG-' . now()->format('YmdHis'), 'priority' => $payload['priority'] ?? 'normal', 'attachment' => $path, 'status' => $payload['status'] ?? 'sent', @@ -48,7 +50,7 @@ class MessageCommandService 'school_year' => $payload['school_year'] ?? $this->configService->getSchoolYear() ?? '', ]); - if (! $message) { + if (!$message) { throw new RuntimeException('Failed to send message.'); } @@ -74,7 +76,7 @@ class MessageCommandService } } - if (! empty($updates)) { + if (!empty($updates)) { $message->fill($updates); $message->save(); } @@ -87,7 +89,6 @@ class MessageCommandService { return DB::transaction(function () use ($message) { $message->status = 'trashed'; - return (bool) $message->save(); }); } @@ -116,7 +117,7 @@ class MessageCommandService ]; $key = strtolower(trim($roleName)); - if (! isset($map[$key])) { + if (!isset($map[$key])) { throw new RuntimeException('Invalid recipient role.'); } diff --git a/app/Services/Messaging/MessageQueryService.php b/app/Services/Messaging/MessageQueryService.php index 37008828..ed90bfb0 100644 --- a/app/Services/Messaging/MessageQueryService.php +++ b/app/Services/Messaging/MessageQueryService.php @@ -11,12 +11,11 @@ class MessageQueryService public function getUserRoleName(int $userId): ?string { $user = User::query()->with('roles')->find($userId); - if (! $user) { + if (!$user) { return null; } $role = $user->roles->sortBy('id')->first(); - return $role ? (string) $role->name : null; } @@ -68,7 +67,7 @@ class MessageQueryService ->pluck('id') ->all(); - if (! empty($ids)) { + if (!empty($ids)) { Message::query() ->whereIn('id', $ids) ->update([ @@ -91,7 +90,7 @@ class MessageQueryService { $query = Message::query()->with(['sender', 'recipient']); - if (! empty($filters['priority'])) { + if (!empty($filters['priority'])) { $query->where('priority', $filters['priority']); } @@ -99,8 +98,8 @@ class MessageQueryService $query->where('read_status', (int) $filters['read_status']); } - if (! empty($filters['search'])) { - $term = '%'.$filters['search'].'%'; + if (!empty($filters['search'])) { + $term = '%' . $filters['search'] . '%'; $query->where(function ($q) use ($term) { $q->where('subject', 'like', $term) ->orWhere('message', 'like', $term); diff --git a/app/Services/Messaging/MessageRecipientService.php b/app/Services/Messaging/MessageRecipientService.php index 99fcc905..f75c6cdf 100644 --- a/app/Services/Messaging/MessageRecipientService.php +++ b/app/Services/Messaging/MessageRecipientService.php @@ -22,7 +22,7 @@ class MessageRecipientService return $rows ->map(fn ($row) => [ 'id' => (int) $row->teacher_id, - 'name' => trim($row->firstname.' '.$row->lastname), + 'name' => trim($row->firstname . ' ' . $row->lastname), ]) ->unique('id') ->values() @@ -81,7 +81,7 @@ class MessageRecipientService return $users ->map(fn ($row) => [ 'id' => (int) $row->id, - 'name' => trim($row->firstname.' '.$row->lastname), + 'name' => trim($row->firstname . ' ' . $row->lastname), ]) ->unique('id') ->values() diff --git a/app/Services/Navigation/NavBuilderService.php b/app/Services/Navigation/NavBuilderService.php index 4d8c18f8..93617a4d 100644 --- a/app/Services/Navigation/NavBuilderService.php +++ b/app/Services/Navigation/NavBuilderService.php @@ -6,10 +6,13 @@ use App\Models\NavItem; use App\Models\Role; use App\Models\RoleNavItem; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Log; class NavBuilderService { - public function __construct(private NavbarService $navbarService) {} + public function __construct(private NavbarService $navbarService) + { + } public function getAdminPayload(): array { @@ -74,7 +77,7 @@ class NavBuilderService $menuParentId = null; } - if ($menuParentId !== null && ! NavItem::query()->whereKey($menuParentId)->exists()) { + if ($menuParentId !== null && !NavItem::query()->whereKey($menuParentId)->exists()) { return ['ok' => false, 'message' => 'Selected parent does not exist.']; } @@ -85,7 +88,7 @@ class NavBuilderService 'icon_class' => trim((string) ($payload['icon_class'] ?? '')) ?: null, 'target' => trim((string) ($payload['target'] ?? '')) ?: null, 'sort_order' => (int) ($payload['sort_order'] ?? 0), - 'is_enabled' => ! empty($payload['is_enabled']) ? 1 : 0, + 'is_enabled' => !empty($payload['is_enabled']) ? 1 : 0, ]; $roleIds = array_values(array_unique(array_filter( @@ -187,7 +190,7 @@ class NavBuilderService } $roleId = (int) ($row['role_id'] ?? 0); - $roleName = $row['role_name'] ?? ($roleId ? ('#'.$roleId) : null); + $roleName = $row['role_name'] ?? ($roleId ? ('#' . $roleId) : null); if ($roleId > 0) { $roleAssignments[$navId]['ids'][] = $roleId; @@ -223,7 +226,7 @@ class NavBuilderService ], ]; - if (! empty($node['children'])) { + if (!empty($node['children'])) { $output = array_merge($output, $this->flattenTreeForResponse($node['children'], $roleAssignments, $depth + 1)); } } @@ -236,7 +239,6 @@ class NavBuilderService $s = trim(preg_replace('/\s+/', ' ', $s)); $s = preg_replace('/^[^[:alnum:]]+/u', '', $s); $s = preg_replace('/^(?i)(the|an|a)\s+/', '', $s); - return mb_strtolower($s, 'UTF-8'); } @@ -248,7 +250,7 @@ class NavBuilderService return $result !== 0 ? $result : ((int) ($a['id'] ?? 0) <=> (int) ($b['id'] ?? 0)); }); foreach ($nodes as &$node) { - if (! empty($node['children'])) { + if (!empty($node['children'])) { $this->sortTreeAlpha($node['children']); } } diff --git a/app/Services/Navigation/NavbarService.php b/app/Services/Navigation/NavbarService.php index 6936396a..cf9d6778 100644 --- a/app/Services/Navigation/NavbarService.php +++ b/app/Services/Navigation/NavbarService.php @@ -11,7 +11,7 @@ class NavbarService public function getMenuForRoles(array $roles): array { $roles = array_values(array_filter(array_map(fn ($r) => strtolower(trim((string) $r)), $roles))); - $cacheKey = 'navbar_'.md5(json_encode($roles)); + $cacheKey = 'navbar_' . md5(json_encode($roles)); return Cache::remember($cacheKey, 300, function () use ($roles) { $allowedIds = RoleNavItem::getNavItemIdsForRoles($roles); @@ -75,7 +75,7 @@ class NavbarService }); foreach ($nodes as &$node) { - if (! empty($node['children'])) { + if (!empty($node['children'])) { $this->sortTreeAlpha($node['children']); } } diff --git a/app/Services/Notifications/NotificationManagementService.php b/app/Services/Notifications/NotificationManagementService.php index 9bacc69d..d692725b 100644 --- a/app/Services/Notifications/NotificationManagementService.php +++ b/app/Services/Notifications/NotificationManagementService.php @@ -9,7 +9,7 @@ class NotificationManagementService public function update(int $notificationId, array $payload): array { $notification = Notification::query()->find($notificationId); - if (! $notification) { + if (!$notification) { return ['ok' => false, 'message' => 'Notification not found.']; } @@ -33,7 +33,7 @@ class NotificationManagementService public function delete(int $notificationId): bool { $notification = Notification::query()->find($notificationId); - if (! $notification) { + if (!$notification) { return false; } diff --git a/app/Services/Notifications/NotificationReadService.php b/app/Services/Notifications/NotificationReadService.php index 595ec09c..a8aef10a 100644 --- a/app/Services/Notifications/NotificationReadService.php +++ b/app/Services/Notifications/NotificationReadService.php @@ -13,7 +13,7 @@ class NotificationReadService ->where('notification_id', $notificationId) ->first(); - if (! $row) { + if (!$row) { return false; } diff --git a/app/Services/Notifications/NotificationRecipientService.php b/app/Services/Notifications/NotificationRecipientService.php index 9a06446b..a857e941 100644 --- a/app/Services/Notifications/NotificationRecipientService.php +++ b/app/Services/Notifications/NotificationRecipientService.php @@ -9,7 +9,6 @@ class NotificationRecipientService public function getRecipients(string $targetGroup): array { $rows = User::getUsersByRole($targetGroup); - return is_array($rows) ? $rows : []; } } diff --git a/app/Services/Notifications/NotificationSendService.php b/app/Services/Notifications/NotificationSendService.php index cc701c64..5fd6dc1d 100644 --- a/app/Services/Notifications/NotificationSendService.php +++ b/app/Services/Notifications/NotificationSendService.php @@ -12,7 +12,8 @@ class NotificationSendService public function __construct( private NotificationRecipientService $recipients, private NotificationDispatchService $dispatcher - ) {} + ) { + } public function send(array $payload, int $actorId): array { @@ -56,11 +57,11 @@ class NotificationSendService ]); $created++; - if (in_array('email', $channels, true) && ! empty($user['email'])) { + if (in_array('email', $channels, true) && !empty($user['email'])) { $this->dispatcher->sendEmail((string) $user['email'], (string) $payload['title'], (string) $payload['message']); } - if (in_array('sms', $channels, true) && ! empty($user['phone'])) { + if (in_array('sms', $channels, true) && !empty($user['phone'])) { $this->dispatcher->sendSms((string) $user['phone'], (string) $payload['message']); } } diff --git a/app/Services/Notifications/NotificationUserListService.php b/app/Services/Notifications/NotificationUserListService.php index 0c9d3f0b..65cdd23b 100644 --- a/app/Services/Notifications/NotificationUserListService.php +++ b/app/Services/Notifications/NotificationUserListService.php @@ -15,7 +15,7 @@ class NotificationUserListService $sortDir = strtolower((string) ($filters['sort_dir'] ?? 'desc')) === 'asc' ? 'asc' : 'desc'; $allowedSorts = ['created_at', 'scheduled_at', 'priority']; - if (! in_array($sortBy, $allowedSorts, true)) { + if (!in_array($sortBy, $allowedSorts, true)) { $sortBy = 'created_at'; } @@ -40,7 +40,7 @@ class NotificationUserListService } $paginator = $query - ->orderBy('notifications.'.$sortBy, $sortDir) + ->orderBy('notifications.' . $sortBy, $sortDir) ->paginate($perPage, ['*'], 'page', $page); return [ diff --git a/app/Services/Parents/AuthorizedUsersManagementService.php b/app/Services/Parents/AuthorizedUsersManagementService.php index 569a7d9e..e34a6a23 100644 --- a/app/Services/Parents/AuthorizedUsersManagementService.php +++ b/app/Services/Parents/AuthorizedUsersManagementService.php @@ -2,7 +2,6 @@ namespace App\Services\Parents; -use App\Controllers\View\AuthorizedUsersController; use App\Models\AuthorizedUser; use App\Models\User; use App\Services\ApplicationUrlService; @@ -10,7 +9,7 @@ use App\Services\Email\EmailDispatchService; use Illuminate\Support\Facades\DB; /** - * legacy {@see AuthorizedUsersController} parity. + * legacy {@see \App\Controllers\View\AuthorizedUsersController} parity. */ class AuthorizedUsersManagementService { diff --git a/app/Services/Parents/ParentAttendanceReportCalendarService.php b/app/Services/Parents/ParentAttendanceReportCalendarService.php index 7e5428fd..f7f03c12 100644 --- a/app/Services/Parents/ParentAttendanceReportCalendarService.php +++ b/app/Services/Parents/ParentAttendanceReportCalendarService.php @@ -6,7 +6,9 @@ use Illuminate\Support\Facades\DB; class ParentAttendanceReportCalendarService { - public function __construct(private ParentConfigService $configService) {} + public function __construct(private ParentConfigService $configService) + { + } /** * @return array{0: array, 1: string} @@ -31,7 +33,7 @@ class ParentAttendanceReportCalendarService $dates = []; for ($i = $weeksBefore; $i >= 1; $i--) { - $dates[] = $thisSunday->copy()->modify('-'.$i.' week'); + $dates[] = $thisSunday->copy()->modify('-' . $i . ' week'); } $cursor = $thisSunday->copy(); @@ -70,13 +72,13 @@ class ParentAttendanceReportCalendarService $filtered = []; foreach ($dates as $d) { $ymd = $d->format('Y-m-d'); - if (! isset($noSchool[$ymd]) && $d >= $today && $d <= $cap) { + if (!isset($noSchool[$ymd]) && $d >= $today && $d <= $cap) { $filtered[] = $d; } } $defaultYmd = $default->format('Y-m-d'); - if (! empty($filtered)) { + if (!empty($filtered)) { $defaultYmd = $filtered[0]->format('Y-m-d'); } @@ -99,10 +101,8 @@ class ParentAttendanceReportCalendarService } if (preg_match('/^(\\d{1,2}):(\\d{2})$/', $raw, $m)) { $hh = str_pad((string) ((int) $m[1]), 2, '0', STR_PAD_LEFT); - - return $hh.':'.$m[2]; + return $hh . ':' . $m[2]; } - return $default; } diff --git a/app/Services/Parents/ParentAttendanceReportService.php b/app/Services/Parents/ParentAttendanceReportService.php index 99a70ddc..19a6812e 100644 --- a/app/Services/Parents/ParentAttendanceReportService.php +++ b/app/Services/Parents/ParentAttendanceReportService.php @@ -2,14 +2,12 @@ namespace App\Services\Parents; -use App\Controllers\View\ParentAttendanceReportController; -use App\Http\Controllers\Api\Reports\FilesController; use App\Models\AttendanceData; use App\Models\AttendanceRecord; -use App\Models\ClassSection; use App\Models\EarlyDismissalSignature; use App\Models\ParentAttendanceReport; use App\Models\Student; +use App\Models\ClassSection; use App\Services\SemesterRangeService; use Illuminate\Http\UploadedFile; use Illuminate\Support\Facades\DB; @@ -20,7 +18,8 @@ class ParentAttendanceReportService private ParentConfigService $configService, private ParentAttendanceReportCalendarService $calendarService, private SemesterRangeService $semesterRangeService - ) {} + ) { + } public function formData(int $parentId): array { @@ -91,7 +90,7 @@ class ParentAttendanceReportService } $type = (string) ($payload['type'] ?? ''); - if (! in_array($type, ['absent', 'late', 'early_dismissal'], true)) { + if (!in_array($type, ['absent', 'late', 'early_dismissal'], true)) { throw new \RuntimeException('Invalid report type.'); } @@ -105,8 +104,8 @@ class ParentAttendanceReportService foreach ($rawDates as $entry) { $dt = \DateTime::createFromFormat('Y-m-d', $entry); - if (! $dt) { - throw new \RuntimeException('Invalid date selected: '.$entry); + if (!$dt) { + throw new \RuntimeException('Invalid date selected: ' . $entry); } if ((int) $dt->format('w') !== 0) { throw new \RuntimeException('Please select Sunday dates only.'); @@ -120,10 +119,10 @@ class ParentAttendanceReportService $validDates[$entry] = $dt; } - if ($type === 'late' && (! is_string($arrivalTime) || $arrivalTime === '')) { + if ($type === 'late' && (!is_string($arrivalTime) || $arrivalTime === '')) { throw new \RuntimeException('Please provide an expected arrival time for late reporting.'); } - if ($type === 'early_dismissal' && (! is_string($dismissTime) || $dismissTime === '')) { + if ($type === 'early_dismissal' && (!is_string($dismissTime) || $dismissTime === '')) { throw new \RuntimeException('Please provide a dismissal time for early dismissal.'); } if ($type !== 'early_dismissal') { @@ -148,7 +147,7 @@ class ParentAttendanceReportService ->first(); $classSectionId = $sectionRow->class_section_id ?? null; $student = Student::query()->select('firstname', 'lastname')->find($sid); - $studentName = $student ? trim($student->firstname.' '.$student->lastname) : ('Student #'.$sid); + $studentName = $student ? trim($student->firstname . ' ' . $student->lastname) : ('Student #' . $sid); $classLabel = $this->resolveClassSectionLabel($classSectionId); foreach ($validDates as $reportDate => $_dt) { @@ -164,7 +163,7 @@ class ParentAttendanceReportService ->whereIn('type', ['absent', 'late']) ->first(); if ($existingAL) { - $blocked[] = $studentName.' ('.$reportDate.')'; + $blocked[] = $studentName . ' (' . $reportDate . ')'; } else { $existingED = (clone $qb)->where('type', 'early_dismissal')->first(); if ($existingED) { @@ -196,7 +195,7 @@ class ParentAttendanceReportService ->whereIn('type', ['absent', 'late', 'early_dismissal']) ->first(); if ($existingAny) { - $blocked[] = $studentName.' ('.$reportDate.')'; + $blocked[] = $studentName . ' (' . $reportDate . ')'; } else { ParentAttendanceReport::query()->create([ 'parent_id' => $parentId, @@ -243,8 +242,8 @@ class ParentAttendanceReportService } if ($inserted <= 0) { - $err = ! empty($blocked) - ? ('Already submitted and cannot be changed for: '.implode(', ', array_slice($blocked, 0, 5)).(count($blocked) > 5 ? '…' : '')) + $err = !empty($blocked) + ? ('Already submitted and cannot be changed for: ' . implode(', ', array_slice($blocked, 0, 5)) . (count($blocked) > 5 ? '…' : '')) : 'Could not save your report. Please try again.'; throw new \RuntimeException($err); } @@ -279,7 +278,7 @@ class ParentAttendanceReportService }))); $type = (string) ($payload['type'] ?? ''); - if (empty($dateValues) || ! in_array($type, ['absent', 'late', 'early_dismissal'], true)) { + if (empty($dateValues) || !in_array($type, ['absent', 'late', 'early_dismissal'], true)) { throw new \RuntimeException('Invalid date or type.'); } @@ -313,7 +312,7 @@ class ParentAttendanceReportService if ($sid <= 0) { continue; } - if (! isset($map[$sid])) { + if (!isset($map[$sid])) { $map[$sid] = [ 'student_id' => $sid, 'firstname' => (string) ($row['firstname'] ?? ''), @@ -327,7 +326,7 @@ class ParentAttendanceReportService } $map[$sid]['dates'][$dVal] = $map[$sid]['dates'][$dVal] ?? []; $t = (string) ($row['type'] ?? ''); - if ($t !== '' && ! in_array($t, $map[$sid]['dates'][$dVal], true)) { + if ($t !== '' && !in_array($t, $map[$sid]['dates'][$dVal], true)) { $map[$sid]['dates'][$dVal][] = $t; } } @@ -338,7 +337,7 @@ class ParentAttendanceReportService public function updateReport(int $parentId, int $reportId, array $payload): void { $row = ParentAttendanceReport::query()->find($reportId); - if (! $row || (int) $row->parent_id !== $parentId) { + if (!$row || (int) $row->parent_id !== $parentId) { throw new \RuntimeException('Report not found.'); } @@ -348,7 +347,7 @@ class ParentAttendanceReportService $tzName = (string) (config('School')->attendance['timezone'] ?? 'UTC'); $tz = new \DateTimeZone($tzName ?: 'UTC'); - $cutoff = new \DateTime($row->report_date.' 09:00:00', $tz); + $cutoff = new \DateTime($row->report_date . ' 09:00:00', $tz); $now = new \DateTime('now', $tz); if ($now >= $cutoff) { throw new \RuntimeException('Editing window closed at 9:00 AM on the report date.'); @@ -364,13 +363,13 @@ class ParentAttendanceReportService ]; if ($type === 'late' && $arrival !== '') { - if (! preg_match('/^\\d{2}:\\d{2}(:\\d{2})?$/', $arrival)) { + if (!preg_match('/^\\d{2}:\\d{2}(:\\d{2})?$/', $arrival)) { throw new \RuntimeException('Invalid arrival time format.'); } $update['arrival_time'] = $arrival; } if ($type === 'early_dismissal' && $dismiss !== '') { - if (! preg_match('/^\\d{2}:\\d{2}(:\\d{2})?$/', $dismiss)) { + if (!preg_match('/^\\d{2}:\\d{2}(:\\d{2})?$/', $dismiss)) { throw new \RuntimeException('Invalid dismissal time format.'); } $update['dismiss_time'] = $dismiss; @@ -386,7 +385,7 @@ class ParentAttendanceReportService /** * Staff index: early dismissal rows grouped by date (+ signature metadata per date). * - * Parity with legacy {@see ParentAttendanceReportController::earlyDismissals}. + * Parity with legacy {@see \App\Controllers\View\ParentAttendanceReportController::earlyDismissals}. * * @return array{groups: array>>, school_year: string, semester: string, signature_by_date: array>} */ @@ -468,7 +467,7 @@ class ParentAttendanceReportService } /** - * Parity with legacy {@see ParentAttendanceReportController::addEarlyDismissalForm}. + * Parity with legacy {@see \App\Controllers\View\ParentAttendanceReportController::addEarlyDismissalForm}. * * @return array{students: array>, default_date: string, school_year: string, semester: string} */ @@ -506,7 +505,7 @@ class ParentAttendanceReportService } /** - * Parity with legacy {@see ParentAttendanceReportController::saveEarlyDismissal}. + * Parity with legacy {@see \App\Controllers\View\ParentAttendanceReportController::saveEarlyDismissal}. */ public function saveStaffEarlyDismissal(int $studentId, string $reportDate, string $dismissTime, string $reason): void { @@ -574,8 +573,8 @@ class ParentAttendanceReportService } /** - * Parity with legacy {@see ParentAttendanceReportController::uploadEarlyDismissalSignature}. - * Files live under {@see storage_path('uploads/early_dismissal_signatures')} (same as {@see FilesController::earlyDismissalSignature}). + * Parity with legacy {@see \App\Controllers\View\ParentAttendanceReportController::uploadEarlyDismissalSignature}. + * Files live under {@see storage_path('uploads/early_dismissal_signatures')} (same as {@see \App\Http\Controllers\Api\Reports\FilesController::earlyDismissalSignature}). */ public function storeEarlyDismissalSignature( UploadedFile $file, @@ -645,7 +644,7 @@ class ParentAttendanceReportService $oldFile = (string) ($existing->filename ?? ''); $existing->update($payload); if ($oldFile !== '' && $oldFile !== $filename) { - @unlink($targetDir.DIRECTORY_SEPARATOR.$oldFile); + @unlink($targetDir . DIRECTORY_SEPARATOR . $oldFile); } } else { EarlyDismissalSignature::query()->create($payload); @@ -681,7 +680,7 @@ class ParentAttendanceReportService return; } - if (! $classSectionId) { + if (!$classSectionId) { $row = DB::table('student_class') ->select('class_section_id') ->where('student_id', $studentId) @@ -704,19 +703,19 @@ class ParentAttendanceReportService $reasonParts = ['Parent-reported']; if ($type === 'late' && $arrivalTime) { - $reasonParts[] = 'ETA '.substr($arrivalTime, 0, 5); + $reasonParts[] = 'ETA ' . substr($arrivalTime, 0, 5); } - if (! empty($reason)) { + if (!empty($reason)) { $reasonParts[] = trim($reason); } $reasonText = implode(' — ', $reasonParts); - if (! $existing) { + if (!$existing) { $classId = ClassSection::query() ->where('class_section_id', $classSectionId) ->value('class_id'); $schoolId = Student::query()->whereKey($studentId)->value('school_id'); - if (! $classId || ! $schoolId) { + if (!$classId || !$schoolId) { return; } AttendanceData::query()->create([ @@ -731,7 +730,6 @@ class ParentAttendanceReportService 'school_year' => $schoolYear, ]); $this->bumpAttendanceRecord($studentId, $classSectionId, $schoolId, $status, $semester, $schoolYear); - return; } @@ -773,7 +771,6 @@ class ParentAttendanceReportService $updates['total_late']++; } $record->update($updates); - return; } diff --git a/app/Services/Parents/ParentAttendanceService.php b/app/Services/Parents/ParentAttendanceService.php index 09cf93f5..9bf02154 100644 --- a/app/Services/Parents/ParentAttendanceService.php +++ b/app/Services/Parents/ParentAttendanceService.php @@ -6,7 +6,9 @@ use Illuminate\Support\Facades\DB; class ParentAttendanceService { - public function __construct(private ParentConfigService $configService) {} + public function __construct(private ParentConfigService $configService) + { + } public function listAttendance(int $parentId, ?string $schoolYear = null): array { diff --git a/app/Services/Parents/ParentEmergencyContactService.php b/app/Services/Parents/ParentEmergencyContactService.php index 1baa6ab1..c42d29b5 100644 --- a/app/Services/Parents/ParentEmergencyContactService.php +++ b/app/Services/Parents/ParentEmergencyContactService.php @@ -10,7 +10,8 @@ class ParentEmergencyContactService public function __construct( private ParentConfigService $configService, private PhoneFormatterService $phoneFormatter - ) {} + ) { + } public function list(int $parentId): array { diff --git a/app/Services/Parents/ParentEnrollmentService.php b/app/Services/Parents/ParentEnrollmentService.php index b08fa238..7b5021ac 100644 --- a/app/Services/Parents/ParentEnrollmentService.php +++ b/app/Services/Parents/ParentEnrollmentService.php @@ -10,7 +10,9 @@ use Illuminate\Support\Facades\DB; class ParentEnrollmentService { - public function __construct(private ParentConfigService $configService) {} + public function __construct(private ParentConfigService $configService) + { + } public function overview(int $parentId, ?string $schoolYear = null): array { @@ -24,11 +26,11 @@ class ParentEnrollmentService ->get() ->map(function ($student) use ($selectedYear) { $classSections = StudentClass::getClassSectionsByStudentId($student->id, $selectedYear, true); - $student->class_section = ! empty($classSections) + $student->class_section = !empty($classSections) ? implode(', ', $classSections) : 'Class not Assigned'; - $isArabicClass = ! empty($classSections) && array_reduce( + $isArabicClass = !empty($classSections) && array_reduce( $classSections, static fn ($carry, $name) => $carry || (is_string($name) && stripos($name, 'arabic') === 0), false @@ -179,7 +181,7 @@ class ParentEnrollmentService ->where('is_withdrawn', 0) ->first(); - if (! $enrollment) { + if (!$enrollment) { continue; } @@ -204,7 +206,7 @@ class ParentEnrollmentService if ($refund) { $refund->update([ - 'reason' => 'Withdrawal under review for student ID '.$studentId, + 'reason' => 'Withdrawal under review for student ID ' . $studentId, 'note' => null, 'updated_by' => $parentId, ]); @@ -215,7 +217,7 @@ class ParentEnrollmentService 'requested_at' => now(), 'school_year' => $schoolYear, 'status' => Refund::STATUS_PENDING, - 'reason' => 'Withdrawal under review for student ID '.$studentId, + 'reason' => 'Withdrawal under review for student ID ' . $studentId, 'request' => 'new', 'semester' => $semester, 'refund_paid_amount' => 0.0, diff --git a/app/Services/Parents/ParentEventParticipationService.php b/app/Services/Parents/ParentEventParticipationService.php index 7298cd05..e1bb7b57 100644 --- a/app/Services/Parents/ParentEventParticipationService.php +++ b/app/Services/Parents/ParentEventParticipationService.php @@ -2,9 +2,9 @@ namespace App\Services\Parents; -use App\Models\Enrollment; use App\Models\Event; use App\Models\EventCharges; +use App\Models\Enrollment; use App\Models\Student; use App\Services\Invoices\InvoiceGenerationService; @@ -13,7 +13,8 @@ class ParentEventParticipationService public function __construct( private ParentConfigService $configService, private InvoiceGenerationService $invoiceGeneration - ) {} + ) { + } public function overview(int $parentId): array { @@ -26,7 +27,7 @@ class ParentEventParticipationService $charges = []; foreach ($chargesList as $charge) { - $key = $charge['student_id'].':'.$charge['event_id']; + $key = $charge['student_id'] . ':' . $charge['event_id']; $charges[$key] = [ 'participation' => $charge['participation'], 'date' => $charge['updated_at'] ?? $charge['created_at'], @@ -106,7 +107,6 @@ class ParentEventParticipationService if ($existing) { $existing->delete(); } - continue; } diff --git a/app/Services/Parents/ParentProfileService.php b/app/Services/Parents/ParentProfileService.php index 1ce98558..a04e231f 100644 --- a/app/Services/Parents/ParentProfileService.php +++ b/app/Services/Parents/ParentProfileService.php @@ -9,7 +9,6 @@ class ParentProfileService public function getProfile(int $parentId): ?array { $user = User::query()->find($parentId); - return $user ? $user->toArray() : null; } diff --git a/app/Services/Parents/ParentProgressQueryService.php b/app/Services/Parents/ParentProgressQueryService.php index 8773cc86..2dcbfa7b 100644 --- a/app/Services/Parents/ParentProgressQueryService.php +++ b/app/Services/Parents/ParentProgressQueryService.php @@ -2,16 +2,13 @@ namespace App\Services\Parents; -use App\Controllers\ParentProgressController; -use App\Http\Resources\ClassProgress\ClassProgressReportResource; use App\Models\ClassProgressReport; use App\Services\ClassProgress\ClassProgressQueryService; -use Carbon\CarbonInterface; use Illuminate\Support\Collection; use Illuminate\Support\Facades\DB; /** - * legacy {@see ParentProgressController} read-side logic. + * legacy {@see \App\Controllers\ParentProgressController} read-side logic. */ class ParentProgressQueryService { @@ -136,7 +133,7 @@ class ParentProgressQueryService $statusLabel = $statusOptions[$row->status] ?? 'Unknown'; $row->setAttribute('status_label', $statusLabel); - $weekStart = $row->week_start instanceof CarbonInterface + $weekStart = $row->week_start instanceof \Carbon\CarbonInterface ? $row->week_start->format('Y-m-d') : (string) $row->week_start; $sectionId = (int) $row->class_section_id; @@ -148,7 +145,7 @@ class ParentProgressQueryService if (! isset($reportGroups[$key])) { $reportGroups[$key] = [ 'week_start' => $weekStart, - 'week_end' => $row->week_end instanceof CarbonInterface + 'week_end' => $row->week_end instanceof \Carbon\CarbonInterface ? $row->week_end->format('Y-m-d') : (string) ($row->week_end ?? ''), 'class_section_name' => $row->class_section_name ?? '', @@ -162,7 +159,7 @@ class ParentProgressQueryService continue; } - $reportGroups[$key]['reports'][$subject] = (new ClassProgressReportResource($row))->toArray(request()); + $reportGroups[$key]['reports'][$subject] = (new \App\Http\Resources\ClassProgress\ClassProgressReportResource($row))->toArray(request()); } return $reportGroups; diff --git a/app/Services/Parents/ParentRegistrationService.php b/app/Services/Parents/ParentRegistrationService.php index a675a49f..ba0a7a7f 100644 --- a/app/Services/Parents/ParentRegistrationService.php +++ b/app/Services/Parents/ParentRegistrationService.php @@ -15,7 +15,8 @@ class ParentRegistrationService public function __construct( private ParentConfigService $configService, private SchoolIdService $schoolIdService - ) {} + ) { + } public function overview(int $parentId): array { @@ -38,7 +39,6 @@ class ParentRegistrationService $kids = $kids->map(function ($kid) use ($allergies, $conditions) { $kid->allergies = ($allergies[$kid->id] ?? collect())->pluck('allergy')->all(); $kid->medical_conditions = ($conditions[$kid->id] ?? collect())->pluck('condition_name')->all(); - return $kid->toArray(); })->all(); @@ -105,7 +105,6 @@ class ParentRegistrationService 'dob' => $student['dob'], 'reason' => 'already_registered', ]; - continue; } @@ -116,7 +115,7 @@ class ParentRegistrationService 'age' => $this->calculateAge($student['dob'], $ageReference), 'gender' => $student['gender'], 'registration_grade' => $student['registration_grade'] ?? null, - 'photo_consent' => ! empty($student['photo_consent']) ? 1 : 0, + 'photo_consent' => !empty($student['photo_consent']) ? 1 : 0, 'parent_id' => $parentId, 'year_of_registration' => (string) date('Y'), 'school_year' => $schoolYear, @@ -187,7 +186,7 @@ class ParentRegistrationService 'age' => $this->calculateAge($payload['dob'], $ageReference), 'gender' => $payload['gender'], 'registration_grade' => $payload['registration_grade'] ?? null, - 'photo_consent' => ! empty($payload['photo_consent']) ? 1 : 0, + 'photo_consent' => !empty($payload['photo_consent']) ? 1 : 0, ]); StudentMedicalCondition::query()->where('student_id', $studentId)->delete(); @@ -235,7 +234,6 @@ class ParentRegistrationService try { $dobObj = new \DateTimeImmutable($dob); $refObj = new \DateTimeImmutable($referenceDate); - return (int) $dobObj->diff($refObj)->y; } catch (\Throwable $e) { return 0; diff --git a/app/Services/Parents/ParentReportCardService.php b/app/Services/Parents/ParentReportCardService.php index 0972e662..e1ab3167 100644 --- a/app/Services/Parents/ParentReportCardService.php +++ b/app/Services/Parents/ParentReportCardService.php @@ -2,15 +2,14 @@ namespace App\Services\Parents; -use App\Controllers\ParentReportCardController; use App\Models\Configuration; +use App\Services\ApplicationUrlService; use App\Models\ReportCardAcknowledgement; use App\Models\User; -use App\Services\ApplicationUrlService; use Illuminate\Support\Facades\DB; /** - * legacy {@see ParentReportCardController} parity (read + acknowledgement). + * legacy {@see \App\Controllers\ParentReportCardController} parity (read + acknowledgement). */ class ParentReportCardService { @@ -86,7 +85,7 @@ class ParentReportCardService } /** - * @param list $studentIds + * @param list $studentIds * @return array> */ public function acknowledgementMap(int $primaryParentUserId, array $studentIds, string $schoolYear, string $semester): array @@ -118,7 +117,7 @@ class ParentReportCardService } /** - * @param array $values viewed_at, signed_at, signed_name, signer_ip, … + * @param array $values viewed_at, signed_at, signed_name, signer_ip, … */ public function touchAcknowledgement( int $primaryParentUserId, diff --git a/app/Services/Parents/PrimaryParentUserResolver.php b/app/Services/Parents/PrimaryParentUserResolver.php index ade3ba7c..42cf1b8d 100644 --- a/app/Services/Parents/PrimaryParentUserResolver.php +++ b/app/Services/Parents/PrimaryParentUserResolver.php @@ -2,15 +2,13 @@ namespace App\Services\Parents; -use App\Controllers\ParentReportCardController; -use App\Models\Student; use App\Models\User; use Illuminate\Support\Facades\DB; /** * Maps the logged-in parent-family account to the primary parent's user id - * (stored on {@see Student::$parent_id}), matching legacy - * {@see ParentReportCardController::resolvePrimaryParentId}. + * (stored on {@see \App\Models\Student::$parent_id}), matching legacy + * {@see \App\Controllers\ParentReportCardController::resolvePrimaryParentId}. */ final class PrimaryParentUserResolver { @@ -24,7 +22,7 @@ final class PrimaryParentUserResolver } /** - * @param int $userId Session user id (may be secondary/tertiary login). + * @param int $userId Session user id (may be secondary/tertiary login). */ public function resolveFromCredentials(int $userId, string $userType): ?int { diff --git a/app/Services/Payments/PaymentEnrollmentEventService.php b/app/Services/Payments/PaymentEnrollmentEventService.php index 8881e570..ad162049 100644 --- a/app/Services/Payments/PaymentEnrollmentEventService.php +++ b/app/Services/Payments/PaymentEnrollmentEventService.php @@ -10,7 +10,8 @@ class PaymentEnrollmentEventService { public function __construct( private ApplicationUrlService $urls, - ) {} + ) { + } public function buildEventData( int $parentId, @@ -28,7 +29,7 @@ class PaymentEnrollmentEventService ->where('id', $parentId) ->first(); - if (! $parent) { + if (!$parent) { throw new \RuntimeException("Parent user #{$parentId} not found"); } @@ -103,7 +104,7 @@ class PaymentEnrollmentEventService } $seen[$sid] = true; - $teacherFull = trim(trim((string) ($row['t_firstname'] ?? '')).' '.trim((string) ($row['t_lastname'] ?? ''))); + $teacherFull = trim(trim((string) ($row['t_firstname'] ?? '')) . ' ' . trim((string) ($row['t_lastname'] ?? ''))); $teacherFull = $teacherFull !== '' ? $teacherFull : null; $students[] = [ @@ -111,7 +112,7 @@ class PaymentEnrollmentEventService 'student_id' => $sid, 'firstname' => (string) ($row['s_firstname'] ?? ''), 'lastname' => (string) ($row['s_lastname'] ?? ''), - 'name' => trim(((string) ($row['s_firstname'] ?? '')).' '.((string) ($row['s_lastname'] ?? ''))), + 'name' => trim(((string) ($row['s_firstname'] ?? '')) . ' ' . ((string) ($row['s_lastname'] ?? ''))), 'registration_grade' => (string) ($row['registration_grade'] ?? ''), 'class_section_id' => isset($row['class_section_id']) ? (int) $row['class_section_id'] : null, 'class_section_name' => $row['class_section_name'] ?? null, diff --git a/app/Services/Payments/PaymentEventChargesService.php b/app/Services/Payments/PaymentEventChargesService.php index 2fd3b49e..838da34a 100644 --- a/app/Services/Payments/PaymentEventChargesService.php +++ b/app/Services/Payments/PaymentEventChargesService.php @@ -11,7 +11,9 @@ use Illuminate\Support\Facades\DB; class PaymentEventChargesService { - public function __construct(private ChargeService $chargeService) {} + public function __construct(private ChargeService $chargeService) + { + } public function listCharges(?string $schoolYear, ?string $semester): array { @@ -54,7 +56,7 @@ class PaymentEventChargesService $count = 0; - if (! empty($studentIds)) { + if (!empty($studentIds)) { foreach ($studentIds as $studentId) { $result = $this->chargeService->createEventCharge([ 'parent_id' => $parentId, @@ -71,7 +73,7 @@ class PaymentEventChargesService $count++; } } - } elseif (! empty($payload['student_id'])) { + } elseif (!empty($payload['student_id'])) { $result = $this->chargeService->createEventCharge([ 'parent_id' => $parentId, 'student_id' => (int) $payload['student_id'], diff --git a/app/Services/Payments/PaymentManualService.php b/app/Services/Payments/PaymentManualService.php index 0a975af3..72c6788b 100644 --- a/app/Services/Payments/PaymentManualService.php +++ b/app/Services/Payments/PaymentManualService.php @@ -17,7 +17,8 @@ class PaymentManualService public function __construct( private DiscountInvoiceService $invoiceService, private PaymentEnrollmentEventService $enrollmentEventService - ) {} + ) { + } public function search(string $searchTerm, ?string $schoolYear = null): array { @@ -47,9 +48,9 @@ class PaymentManualService $builder->where(function ($q) use ($searchTerm, $searchClean, $searchFormatted) { $q->where('email', $searchTerm); - if (! empty($searchClean)) { + if (!empty($searchClean)) { if (strlen($searchClean) === 10) { - $formattedPhone = substr($searchClean, 0, 3).'-'.substr($searchClean, 3, 3).'-'.substr($searchClean, 6, 4); + $formattedPhone = substr($searchClean, 0, 3) . '-' . substr($searchClean, 3, 3) . '-' . substr($searchClean, 6, 4); $q->orWhere('cellphone', $formattedPhone); } @@ -59,13 +60,13 @@ class PaymentManualService ); } - if (! empty($searchFormatted)) { + if (!empty($searchFormatted)) { $q->orWhere('cellphone', $searchFormatted); } }); $parent = (array) $builder->first(); - if (! empty($parent)) { + if (!empty($parent)) { unset($parent['password']); $parentData = $parent; $parentId = (int) ($parent['id'] ?? 0); @@ -126,23 +127,23 @@ class PaymentManualService OR email LIKE ? OR cellphone LIKE ?"; - $params = ['%'.$q.'%', '%'.$q.'%', '%'.$q.'%']; + $params = ['%' . $q . '%', '%' . $q . '%', '%' . $q . '%']; if ($digits !== '') { $sql .= " OR REPLACE(REPLACE(REPLACE(REPLACE(cellphone, '(', ''), ')', ''), '-', ''), ' ', '') LIKE ?"; - $params[] = '%'.$digits.'%'; + $params[] = '%' . $digits . '%'; } - $sql .= ') ORDER BY lastname, firstname LIMIT 20'; + $sql .= ") ORDER BY lastname, firstname LIMIT 20"; $rows = DB::select($sql, $params); $items = []; foreach ($rows as $row) { - $label = trim(($row->firstname ?? '').' '.($row->lastname ?? '')); + $label = trim(($row->firstname ?? '') . ' ' . ($row->lastname ?? '')); $email = trim((string) ($row->email ?? '')); $phone = trim((string) ($row->cellphone ?? '')); - $sub = trim($email.' '.$phone); + $sub = trim($email . ' ' . $phone); $value = $email !== '' ? $email : ($phone !== '' ? $phone : $label); $items[] = [ @@ -171,7 +172,7 @@ class PaymentManualService $paymentMethod = strtolower(trim($paymentMethod)); $allowedMethods = ['cash', 'check', 'card']; - if (! in_array($paymentMethod, $allowedMethods, true)) { + if (!in_array($paymentMethod, $allowedMethods, true)) { throw new \InvalidArgumentException('Invalid payment method.'); } @@ -184,7 +185,7 @@ class PaymentManualService } $invoice = Invoice::query()->find($invoiceId); - if (! $invoice) { + if (!$invoice) { throw new \RuntimeException('Invoice not found.'); } @@ -208,7 +209,7 @@ class PaymentManualService try { $locked = DB::table('invoices')->where('id', $invoiceId)->lockForUpdate()->first(); - if (! $locked) { + if (!$locked) { DB::rollBack(); throw new \RuntimeException('Invoice not found.'); } @@ -229,7 +230,7 @@ class PaymentManualService $paidCount = $this->getSuccessfulPaymentCount($invoiceId); $installmentSeq = $paidCount + 1; - $transactionId = 'INV-'.$invoiceId.'-'.str_replace('.', '', (string) microtime(true)); + $transactionId = 'INV-' . $invoiceId . '-' . str_replace('.', '', (string) microtime(true)); $ok = $this->processPayment( $invoiceId, @@ -245,7 +246,7 @@ class PaymentManualService $userId ); - if (! $ok) { + if (!$ok) { DB::rollBack(); throw new \RuntimeException('Failed to record payment.'); } @@ -307,7 +308,7 @@ class PaymentManualService ]; } catch (\Throwable $e) { DB::rollBack(); - Log::error('[manualPayUpdate] '.$e->getMessage()); + Log::error('[manualPayUpdate] ' . $e->getMessage()); throw $e; } } @@ -327,12 +328,12 @@ class PaymentManualService } $payment = Payment::query()->find($paymentId); - if (! $payment) { + if (!$payment) { throw new \RuntimeException('Original payment not found.'); } $invoice = Invoice::query()->find((int) $payment->invoice_id); - if (! $invoice) { + if (!$invoice) { throw new \RuntimeException('Linked invoice not found.'); } @@ -366,10 +367,10 @@ class PaymentManualService { $filename = basename($filename); $roots = [ - storage_path('app/uploads/checks/'.$filename), - storage_path('app/uploads/cards/'.$filename), - storage_path('app/uploads/misc/'.$filename), - storage_path('app/uploads/'.$filename), + storage_path('app/uploads/checks/' . $filename), + storage_path('app/uploads/cards/' . $filename), + storage_path('app/uploads/misc/' . $filename), + storage_path('app/uploads/' . $filename), ]; $path = null; @@ -380,14 +381,14 @@ class PaymentManualService } } - if (! $path) { + if (!$path) { abort(404, 'Payment file not found.'); } if ($mode === 'inline') { return response()->file($path, [ 'Content-Type' => mime_content_type($path) ?: 'application/octet-stream', - 'Content-Disposition' => 'inline; filename="'.$filename.'"', + 'Content-Disposition' => 'inline; filename="' . $filename . '"', ]); } @@ -408,7 +409,7 @@ class PaymentManualService ?int $userId ): bool { $invoice = Invoice::query()->find($invoiceId); - if (! $invoice) { + if (!$invoice) { return false; } @@ -432,7 +433,7 @@ class PaymentManualService 'updated_by' => $userId, ]; - if (! Invoice::query()->whereKey($invoiceId)->update($invoiceUpdateData)) { + if (!Invoice::query()->whereKey($invoiceId)->update($invoiceUpdateData)) { return false; } @@ -465,7 +466,7 @@ class PaymentManualService private function getCurrentInvoiceBalanceExcludingPayment(int $invoiceId, int $excludePaymentId): float { $invoice = Invoice::query()->find($invoiceId); - if (! $invoice) { + if (!$invoice) { return 0.0; } @@ -529,7 +530,7 @@ class PaymentManualService $discountByInvoice = []; $refundByInvoice = []; - if (! empty($invoiceIds)) { + if (!empty($invoiceIds)) { $rows = Payment::query() ->selectRaw('invoice_id, COALESCE(SUM(paid_amount),0) AS total_paid') ->whereIn('invoice_id', $invoiceIds) @@ -580,7 +581,7 @@ class PaymentManualService $issueYmd = ''; foreach (['issue_date', 'start_date', 'created_at'] as $k) { - if (! empty($inv[$k])) { + if (!empty($inv[$k])) { $its = strtotime((string) $inv[$k]); if ($its !== false) { $issueYmd = date('Y-m-d', $its); @@ -612,7 +613,7 @@ class PaymentManualService $okTypes = ['image/jpeg', 'image/png', 'application/pdf']; $okExts = ['jpg', 'jpeg', 'png', 'pdf']; - if (! in_array($mime, $okTypes, true) || ! in_array($ext, $okExts, true)) { + if (!in_array($mime, $okTypes, true) || !in_array($ext, $okExts, true)) { throw new \InvalidArgumentException('Unsupported file type. Use JPG, PNG, or PDF.'); } @@ -622,9 +623,9 @@ class PaymentManualService $fileName = $file->hashName(); $subdir = $paymentMethod === 'check' ? 'checks' : ($paymentMethod === 'card' ? 'cards' : 'misc'); - $targetDir = storage_path('app/uploads/'.$subdir); + $targetDir = storage_path('app/uploads/' . $subdir); - if (! is_dir($targetDir)) { + if (!is_dir($targetDir)) { mkdir($targetDir, 0775, true); } @@ -640,7 +641,7 @@ class PaymentManualService event($name, $payload); } } catch (\Throwable $e) { - Log::warning($name.' hook failed: '.$e->getMessage()); + Log::warning($name . ' hook failed: ' . $e->getMessage()); } } } diff --git a/app/Services/Payments/PaymentMissedCheckService.php b/app/Services/Payments/PaymentMissedCheckService.php index fc9fa0a6..1ae3e5b7 100644 --- a/app/Services/Payments/PaymentMissedCheckService.php +++ b/app/Services/Payments/PaymentMissedCheckService.php @@ -11,7 +11,8 @@ class PaymentMissedCheckService public function __construct( private UserNotificationDispatchService $notifier, private EmailService $emailService - ) {} + ) { + } public function findUsersWithMissedPayments(): array { @@ -45,7 +46,7 @@ class PaymentMissedCheckService foreach ($users as $user) { $userId = (int) ($user['user_id'] ?? 0); $email = (string) ($user['email'] ?? ''); - $name = trim((string) ($user['firstname'] ?? '').' '.(string) ($user['lastname'] ?? '')); + $name = trim((string) ($user['firstname'] ?? '') . ' ' . (string) ($user['lastname'] ?? '')); $this->notifier->notifyUser( $userId, @@ -58,8 +59,8 @@ class PaymentMissedCheckService if ($email !== '') { $subject = 'Payment Missed'; - $body = '

Dear '.e($name !== '' ? $name : 'Parent').',

' - .'

You have a missed payment. Please pay to avoid penalty.

'; + $body = '

Dear ' . e($name !== '' ? $name : 'Parent') . ',

' + . '

You have a missed payment. Please pay to avoid penalty.

'; $ok = $this->emailService->send($email, $subject, $body, 'finance'); $ok ? $sent++ : $failed++; } diff --git a/app/Services/Payments/PaymentNotificationDispatchService.php b/app/Services/Payments/PaymentNotificationDispatchService.php index 534f58db..7bfa3d85 100644 --- a/app/Services/Payments/PaymentNotificationDispatchService.php +++ b/app/Services/Payments/PaymentNotificationDispatchService.php @@ -2,9 +2,9 @@ namespace App\Services\Payments; -use App\Models\Configuration; use App\Models\Notification; use App\Models\UserNotification; +use App\Models\Configuration; class PaymentNotificationDispatchService { diff --git a/app/Services/Payments/PaymentNotificationService.php b/app/Services/Payments/PaymentNotificationService.php index 3797cab6..935f88f1 100644 --- a/app/Services/Payments/PaymentNotificationService.php +++ b/app/Services/Payments/PaymentNotificationService.php @@ -15,19 +15,20 @@ class PaymentNotificationService public function __construct( private EmailService $emailService, private PaymentNotificationDispatchService $dispatchService - ) {} + ) { + } public function listLogs(?string $from, ?string $to, ?string $type): array { $query = PaymentNotificationLog::query()->with('parent')->orderByDesc('sent_at'); - if (! empty($from)) { + if (!empty($from)) { $query->where('sent_at', '>=', $from); } - if (! empty($to)) { + if (!empty($to)) { $query->where('sent_at', '<=', $to); } - if (! empty($type)) { + if (!empty($type)) { $query->where('type', $type); } @@ -84,17 +85,15 @@ class PaymentNotificationService $type = $this->resolveType($parentId, $schoolYear, $typeInput); - if (! $force && PaymentNotificationLog::existsForPeriod($parentId, $year, $month, $type)) { + if (!$force && PaymentNotificationLog::existsForPeriod($parentId, $year, $month, $type)) { $results['skipped']++; $results['details'][] = ['parent_id' => $parentId, 'result' => 'already_sent']; - return $results; } - if ($balance <= 0 && ! $force) { + if ($balance <= 0 && !$force) { $results['skipped']++; $results['details'][] = ['parent_id' => $parentId, 'result' => 'no_balance']; - return $results; } @@ -229,13 +228,13 @@ class PaymentNotificationService private function composeMessage(int $parentId, float $balance, string $schoolYear, \DateTime $now): array { $user = User::query()->find($parentId); - $parentName = $user ? trim(($user->firstname ?? '').' '.($user->lastname ?? '')) : ''; + $parentName = $user ? trim(($user->firstname ?? '') . ' ' . ($user->lastname ?? '')) : ''; $greeting = $parentName !== '' ? "Dear {$parentName}," : 'Dear Parent,'; $monthYear = $now->format('F Y'); $subject = sprintf('Monthly Tuition Reminder — %s', $schoolYear); - $balanceFmt = '$'.number_format($balance, 2); - $intro = 'This is your monthly installment reminder for '.$schoolYear.'.'; + $balanceFmt = '$' . number_format($balance, 2); + $intro = 'This is your monthly installment reminder for ' . $schoolYear . '.'; $installmentInfo = $this->computeInstallmentSuggestion($balance); @@ -299,14 +298,14 @@ class PaymentNotificationService 'remaining_installments' => $remainingInstallments, 'max_installments' => $maxInstallments, 'installment_due' => round($installmentDue, 2), - 'installment_due_fmt' => '$'.number_format($installmentDue, 2), + 'installment_due_fmt' => '$' . number_format($installmentDue, 2), ]; } private function getSecondaryGuardianEmail(int $primaryUserId): ?string { $row = DB::table('family_guardians')->where('user_id', $primaryUserId)->first(); - if (! $row || empty($row->family_id)) { + if (!$row || empty($row->family_id)) { return null; } @@ -331,10 +330,9 @@ class PaymentNotificationService private function getUserEmail(int $userId): ?string { $email = (string) (DB::table('users')->where('id', $userId)->value('email') ?? ''); - if ($email === '' || ! filter_var($email, FILTER_VALIDATE_EMAIL)) { + if ($email === '' || !filter_var($email, FILTER_VALIDATE_EMAIL)) { return null; } - return $email; } @@ -355,7 +353,7 @@ class PaymentNotificationService ); } } catch (\Throwable $e) { - Log::warning('Unable to notify heads of finance: '.$e->getMessage()); + Log::warning('Unable to notify heads of finance: ' . $e->getMessage()); } } @@ -364,10 +362,9 @@ class PaymentNotificationService $base['sent'] += $update['sent'] ?? 0; $base['skipped'] += $update['skipped'] ?? 0; $base['failed'] += $update['failed'] ?? 0; - if (! empty($update['details'])) { + if (!empty($update['details'])) { $base['details'] = array_merge($base['details'], $update['details']); } - return $base; } @@ -388,7 +385,6 @@ class PaymentNotificationService if (function_exists('user_timezone')) { return (string) user_timezone(); } - return (string) (config('app.timezone') ?: 'UTC'); } } diff --git a/app/Services/Payments/PaymentTestNotificationService.php b/app/Services/Payments/PaymentTestNotificationService.php index 0d9ac2e9..1ab86176 100644 --- a/app/Services/Payments/PaymentTestNotificationService.php +++ b/app/Services/Payments/PaymentTestNotificationService.php @@ -14,12 +14,14 @@ use DateTimeZone; class PaymentTestNotificationService { - public function __construct(private EmailService $emailService) {} + public function __construct(private EmailService $emailService) + { + } public function send(string $email, string $type = 'no_payment'): array { $email = trim($email); - if ($email === '' || ! filter_var($email, FILTER_VALIDATE_EMAIL)) { + if ($email === '' || !filter_var($email, FILTER_VALIDATE_EMAIL)) { return ['ok' => false, 'message' => 'Invalid email address.']; } @@ -51,11 +53,11 @@ class PaymentTestNotificationService $ccEmail = $this->findSecondaryGuardianEmail($parentId, $email); - $parentName = $user ? trim(($user->firstname ?? '').' '.($user->lastname ?? '')) : ''; + $parentName = $user ? trim(($user->firstname ?? '') . ' ' . ($user->lastname ?? '')) : ''; $monthYear = $now->format('F Y'); $subject = sprintf('Monthly Tuition Reminder — %s', $schoolYear); $greeting = $parentName !== '' ? "Dear {$parentName}," : 'Dear Parent,'; - $balanceFmt = '$'.number_format($totalBalance, 2); + $balanceFmt = '$' . number_format($totalBalance, 2); $intro = ($type === 'no_payment') ? "We noticed there are outstanding tuition charges for {$schoolYear} but no payment has been recorded yet." : "This is your monthly installment reminder for {$schoolYear}."; @@ -77,11 +79,11 @@ class PaymentTestNotificationService HTML; - $bodyHtml .= '
    ' - ."
  • Remaining installments: {$installmentInfo['remaining_installments']}
  • " - ."
  • Suggested installment this month: {$installmentInfo['installment_due_fmt']}
  • " - ."
  • Maximum installments available: {$installmentInfo['max_installments']}
  • " - .'
'; + $bodyHtml .= "
    " + . "
  • Remaining installments: {$installmentInfo['remaining_installments']}
  • " + . "
  • Suggested installment this month: {$installmentInfo['installment_due_fmt']}
  • " + . "
  • Maximum installments available: {$installmentInfo['max_installments']}
  • " + . "
"; $body = MailHtml::document('Monthly Tuition Reminder', $bodyHtml); @@ -166,7 +168,7 @@ class PaymentTestNotificationService 'remaining_installments' => $remainingInstallments, 'max_installments' => $maxInstallments, 'installment_due' => round($installmentDue, 2), - 'installment_due_fmt' => '$'.number_format($installmentDue, 2), + 'installment_due_fmt' => '$' . number_format($installmentDue, 2), ]; } @@ -177,7 +179,7 @@ class PaymentTestNotificationService } $row = FamilyGuardian::query()->where('user_id', $parentId)->first(); - if (! $row || empty($row->family_id)) { + if (!$row || empty($row->family_id)) { return null; } diff --git a/app/Services/Payments/PaymentTransactionService.php b/app/Services/Payments/PaymentTransactionService.php index 5138045a..0c011e0e 100644 --- a/app/Services/Payments/PaymentTransactionService.php +++ b/app/Services/Payments/PaymentTransactionService.php @@ -17,7 +17,7 @@ class PaymentTransactionService 'payment_status' => $payload['payment_status'] ?? 'Pending', 'transaction_fee' => $payload['transaction_fee'] ?? null, 'payment_reference' => $payload['payment_reference'] ?? null, - 'is_full_payment' => ! empty($payload['is_full_payment']), + 'is_full_payment' => !empty($payload['is_full_payment']), 'school_year' => $payload['school_year'] ?? null, 'semester' => $payload['semester'] ?? null, ]; diff --git a/app/Services/Payments/PaypalPaymentService.php b/app/Services/Payments/PaypalPaymentService.php index ea6bae73..359d573b 100644 --- a/app/Services/Payments/PaypalPaymentService.php +++ b/app/Services/Payments/PaypalPaymentService.php @@ -6,19 +6,13 @@ use App\Models\Payment; use App\Services\ApplicationUrlService; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Log; -use PayPal\Api\Amount; -use PayPal\Api\Payer; -use PayPal\Api\PaymentExecution; -use PayPal\Api\RedirectUrls; -use PayPal\Api\Transaction; -use PayPal\Auth\OAuthTokenCredential; -use PayPal\Rest\ApiContext; class PaypalPaymentService { public function __construct( private ApplicationUrlService $urls, - ) {} + ) { + } public function getRedirectUrl(): string { @@ -30,11 +24,11 @@ class PaypalPaymentService public function createPayment(int $paymentId): array { $payment = Payment::query()->find($paymentId); - if (! $payment) { + if (!$payment) { throw new \RuntimeException('Payment not found.'); } - if (! $this->sdkAvailable()) { + if (!$this->sdkAvailable()) { return [ 'redirect_url' => $this->getRedirectUrl(), 'mode' => 'hosted', @@ -45,23 +39,23 @@ class PaypalPaymentService try { $apiContext = $this->buildApiContext(); - $payer = new Payer; + $payer = new \PayPal\Api\Payer(); $payer->setPaymentMethod('paypal'); - $amount = new Amount; + $amount = new \PayPal\Api\Amount(); $amount->setCurrency('USD')->setTotal($payment->balance ?? $payment->total_amount ?? 0); - $transaction = new Transaction; + $transaction = new \PayPal\Api\Transaction(); $transaction->setAmount($amount) ->setDescription('Payment for school fees') ->setInvoiceNumber(uniqid('inv_', true)); - $paypalPayment = new \PayPal\Api\Payment; + $paypalPayment = new \PayPal\Api\Payment(); $paypalPayment->setIntent('sale') ->setPayer($payer) ->setTransactions([$transaction]); - $redirectUrls = new RedirectUrls; + $redirectUrls = new \PayPal\Api\RedirectUrls(); $redirectUrls->setReturnUrl($this->urls->paypalExecuteReturnUrl()) ->setCancelUrl($this->urls->paypalCancelUrl()); @@ -77,14 +71,14 @@ class PaypalPaymentService 'payment_id' => $paymentId, ]; } catch (\Throwable $e) { - Log::error('PayPal create payment failed: '.$e->getMessage()); + Log::error('PayPal create payment failed: ' . $e->getMessage()); throw new \RuntimeException('Unable to create PayPal payment.'); } } public function executePayment(string $payerId, ?string $paypalPaymentId = null, ?int $paymentId = null): void { - if (! $this->sdkAvailable()) { + if (!$this->sdkAvailable()) { throw new \RuntimeException('PayPal SDK not installed.'); } @@ -95,13 +89,13 @@ class PaypalPaymentService $apiContext = $this->buildApiContext(); $payment = \PayPal\Api\Payment::get($paypalId, $apiContext); - $execution = new PaymentExecution; + $execution = new \PayPal\Api\PaymentExecution(); $execution->setPayerId($payerId); $payment->execute($execution, $apiContext); $internalPaymentId = $paymentId; - if (! $internalPaymentId && $paypalId !== '') { + if (!$internalPaymentId && $paypalId !== '') { $internalPaymentId = (int) Cache::get($this->cacheKey($paypalId)); } if ($internalPaymentId) { @@ -119,18 +113,18 @@ class PaypalPaymentService private function sdkAvailable(): bool { - return class_exists(ApiContext::class) - && class_exists(OAuthTokenCredential::class); + return class_exists(\PayPal\Rest\ApiContext::class) + && class_exists(\PayPal\Auth\OAuthTokenCredential::class); } - private function buildApiContext(): ApiContext + private function buildApiContext(): \PayPal\Rest\ApiContext { $clientId = (string) (config('services.paypal.client_id') ?: env('PAYPAL_CLIENT_ID')); $secret = (string) (config('services.paypal.secret') ?: env('PAYPAL_SECRET')); $mode = (string) (config('services.paypal.mode') ?: env('PAYPAL_MODE') ?: 'sandbox'); - $apiContext = new ApiContext( - new OAuthTokenCredential($clientId, $secret) + $apiContext = new \PayPal\Rest\ApiContext( + new \PayPal\Auth\OAuthTokenCredential($clientId, $secret) ); $apiContext->setConfig([ @@ -143,6 +137,6 @@ class PaypalPaymentService private function cacheKey(string $paypalPaymentId): string { - return 'paypal_payment:'.$paypalPaymentId; + return 'paypal_payment:' . $paypalPaymentId; } } diff --git a/app/Services/Payments/PaypalPaymentSyncService.php b/app/Services/Payments/PaypalPaymentSyncService.php index 65a1266a..01b9bcbf 100644 --- a/app/Services/Payments/PaypalPaymentSyncService.php +++ b/app/Services/Payments/PaypalPaymentSyncService.php @@ -5,8 +5,8 @@ namespace App\Services\Payments; use App\Models\Configuration; use App\Models\Enrollment; use App\Models\Invoice; -use App\Models\Payment; use App\Models\PayPalPayment; +use App\Models\Payment; use App\Models\Student; use App\Models\User; use App\Services\EmailService; @@ -14,7 +14,9 @@ use Illuminate\Support\Facades\Log; class PaypalPaymentSyncService { - public function __construct(private EmailService $emailService) {} + public function __construct(private EmailService $emailService) + { + } public function sync(bool $dryRun = false, bool $reportOnly = false): array { @@ -34,16 +36,15 @@ class PaypalPaymentSyncService $failed = []; foreach ($entries as $entry) { - if (! $reportOnly) { + if (!$reportOnly) { $entry->sync_attempts = (int) $entry->sync_attempts + 1; $entry->save(); } $user = User::query()->where('school_id', $entry->parent_school_id)->first(); - if (! $user) { + if (!$user) { $failed[] = (string) $entry->transaction_id; Log::error('PayPal sync: no user for parent_school_id', ['parent_school_id' => $entry->parent_school_id]); - continue; } @@ -53,12 +54,11 @@ class PaypalPaymentSyncService ->latest('created_at') ->first(); - if (! $reportOnly && ! $dryRun) { + if (!$reportOnly && !$dryRun) { if ($invoice) { $ok = $this->applyPayment($invoice, $entry->amount, (string) $entry->transaction_id, $schoolYear, $semester, $entry->created_at); - if (! $ok) { + if (!$ok) { $failed[] = (string) $entry->transaction_id; - continue; } } else { @@ -106,7 +106,7 @@ class PaypalPaymentSyncService $invoice->status = 'Paid'; } - if (! $invoice->save()) { + if (!$invoice->save()) { return false; } @@ -135,7 +135,7 @@ class PaypalPaymentSyncService private function updateEnrollmentStatusIfPaid(int $invoiceId, string $schoolYear): void { $invoice = Invoice::query()->find($invoiceId); - if (! $invoice || (float) $invoice->balance > 0) { + if (!$invoice || (float) $invoice->balance > 0) { return; } @@ -155,16 +155,15 @@ class PaypalPaymentSyncService { if ($processed === 0 && empty($failed)) { Log::info("[{$mode}] No PayPal sync updates."); - return; } - $subject = "[{$mode}] PayPal Sync Report - ".now()->format('Y-m-d H:i'); + $subject = "[{$mode}] PayPal Sync Report - " . now()->format('Y-m-d H:i'); $body = "PayPal Sync Mode: {$mode}\n\n"; $body .= "{$processed} PayPal payments processed.\n\n"; - if (! empty($failed)) { - $body .= count($failed)." failed transactions:\n".implode("\n", $failed); + if (!empty($failed)) { + $body .= count($failed) . " failed transactions:\n" . implode("\n", $failed); } else { $body .= "No failed transactions.\n"; } diff --git a/app/Services/Payments/PaypalTransactionsService.php b/app/Services/Payments/PaypalTransactionsService.php index 9e0fd78e..096678a1 100644 --- a/app/Services/Payments/PaypalTransactionsService.php +++ b/app/Services/Payments/PaypalTransactionsService.php @@ -12,11 +12,11 @@ class PaypalTransactionsService if ($keyword) { $query->where(function ($q) use ($keyword) { - $q->where('transaction_id', 'like', '%'.$keyword.'%') - ->orWhere('payer_email', 'like', '%'.$keyword.'%') - ->orWhere('event_type', 'like', '%'.$keyword.'%') - ->orWhere('order_id', 'like', '%'.$keyword.'%') - ->orWhere('parent_school_id', 'like', '%'.$keyword.'%'); + $q->where('transaction_id', 'like', '%' . $keyword . '%') + ->orWhere('payer_email', 'like', '%' . $keyword . '%') + ->orWhere('event_type', 'like', '%' . $keyword . '%') + ->orWhere('order_id', 'like', '%' . $keyword . '%') + ->orWhere('parent_school_id', 'like', '%' . $keyword . '%'); }); } @@ -29,11 +29,11 @@ class PaypalTransactionsService if ($keyword) { $query->where(function ($q) use ($keyword) { - $q->where('transaction_id', 'like', '%'.$keyword.'%') - ->orWhere('payer_email', 'like', '%'.$keyword.'%') - ->orWhere('event_type', 'like', '%'.$keyword.'%') - ->orWhere('order_id', 'like', '%'.$keyword.'%') - ->orWhere('parent_school_id', 'like', '%'.$keyword.'%'); + $q->where('transaction_id', 'like', '%' . $keyword . '%') + ->orWhere('payer_email', 'like', '%' . $keyword . '%') + ->orWhere('event_type', 'like', '%' . $keyword . '%') + ->orWhere('order_id', 'like', '%' . $keyword . '%') + ->orWhere('parent_school_id', 'like', '%' . $keyword . '%'); }); } diff --git a/app/Services/Policy/PolicyContentService.php b/app/Services/Policy/PolicyContentService.php index 4d7cee33..365b1e0f 100644 --- a/app/Services/Policy/PolicyContentService.php +++ b/app/Services/Policy/PolicyContentService.php @@ -14,16 +14,15 @@ class PolicyContentService public function getPolicy(string $type): array { $type = strtolower(trim($type)); - if (! isset(self::TYPES[$type])) { + if (!isset(self::TYPES[$type])) { throw new \InvalidArgumentException('Unsupported policy type.'); } $filename = self::TYPES[$type]; - $path = resource_path('policies/'.$filename); + $path = resource_path('policies/' . $filename); - if (! is_file($path)) { + if (!is_file($path)) { Log::warning('Policy file missing.', ['type' => $type, 'path' => $path]); - return [ 'type' => $type, 'title' => $this->titleFor($type), diff --git a/app/Services/Preferences/PreferencesCommandService.php b/app/Services/Preferences/PreferencesCommandService.php index 11ac3e57..ff629909 100644 --- a/app/Services/Preferences/PreferencesCommandService.php +++ b/app/Services/Preferences/PreferencesCommandService.php @@ -7,7 +7,9 @@ use Illuminate\Support\Facades\DB; class PreferencesCommandService { - public function __construct(private PreferencesOptionsService $options) {} + public function __construct(private PreferencesOptionsService $options) + { + } public function upsert(int $userId, array $payload): Preferences { @@ -20,7 +22,6 @@ class PreferencesCommandService if ($existing) { $existing->fill($data); $existing->save(); - return $existing->refresh(); } diff --git a/app/Services/Preferences/PreferencesOptionsService.php b/app/Services/Preferences/PreferencesOptionsService.php index 9b4fee2c..09cf286f 100644 --- a/app/Services/Preferences/PreferencesOptionsService.php +++ b/app/Services/Preferences/PreferencesOptionsService.php @@ -28,23 +28,21 @@ class PreferencesOptionsService public function normalizeStyle(?string $style): ?string { - if (! $style) { + if (!$style) { return null; } $style = trim($style); - return in_array($style, $this->styleOptions(), true) ? $style : null; } public function normalizeMenu(?string $menu): ?string { - if (! $menu) { + if (!$menu) { return null; } $menu = trim($menu); - return in_array($menu, $this->menuOptions(), true) ? $menu : null; } } diff --git a/app/Services/Preferences/PreferencesQueryService.php b/app/Services/Preferences/PreferencesQueryService.php index 34b7ed74..bd546226 100644 --- a/app/Services/Preferences/PreferencesQueryService.php +++ b/app/Services/Preferences/PreferencesQueryService.php @@ -7,14 +7,16 @@ use Illuminate\Contracts\Pagination\LengthAwarePaginator; class PreferencesQueryService { - public function __construct(private PreferencesOptionsService $options) {} + public function __construct(private PreferencesOptionsService $options) + { + } public function getForUser(int $userId): array { $row = Preferences::query()->where('user_id', $userId)->first(); $base = $this->options->defaultPreferences(); - if (! $row) { + if (!$row) { return [ 'preferences' => $base, 'options' => $this->optionsPayload(), @@ -33,12 +35,12 @@ class PreferencesQueryService ->leftJoin('users', 'users.id', '=', 'user_preferences.user_id') ->select('user_preferences.*', 'users.firstname', 'users.lastname', 'users.email'); - if (! empty($filters['user_id'])) { + if (!empty($filters['user_id'])) { $query->where('user_preferences.user_id', (int) $filters['user_id']); } - if (! empty($filters['q'])) { - $term = '%'.strtolower(trim((string) $filters['q'])).'%'; + if (!empty($filters['q'])) { + $term = '%' . strtolower(trim((string) $filters['q'])) . '%'; $query->where(function ($q) use ($term) { $q->whereRaw('LOWER(users.firstname) LIKE ?', [$term]) ->orWhereRaw('LOWER(users.lastname) LIKE ?', [$term]) @@ -51,7 +53,7 @@ class PreferencesQueryService $allowedSorts = ['updated_at', 'created_at', 'user_id']; if (in_array($sortBy, $allowedSorts, true)) { - $query->orderBy('user_preferences.'.$sortBy, $sortDir); + $query->orderBy('user_preferences.' . $sortBy, $sortDir); } else { $query->orderBy('user_preferences.updated_at', 'desc'); } diff --git a/app/Services/PrintRequests/PrintRequestsPortalService.php b/app/Services/PrintRequests/PrintRequestsPortalService.php index a1b23838..504a6ddb 100644 --- a/app/Services/PrintRequests/PrintRequestsPortalService.php +++ b/app/Services/PrintRequests/PrintRequestsPortalService.php @@ -2,7 +2,6 @@ namespace App\Services\PrintRequests; -use App\Controllers\PrintRequests; use App\Models\AdminNotificationSubject; use App\Models\ClassSection; use App\Models\Configuration; @@ -17,7 +16,7 @@ use Illuminate\Support\Facades\Schema; use Illuminate\Support\Str; /** - * legacy {@see PrintRequests} parity (SPA JSON + uploads). + * legacy {@see \App\Controllers\PrintRequests} parity (SPA JSON + uploads). */ class PrintRequestsPortalService { @@ -166,7 +165,7 @@ class PrintRequestsPortalService } /** - * @param array $requestData merged row after update for notifications + * @param array $requestData merged row after update for notifications */ public function notifyAdminsForPrintRequest(int $printRequestId, array $requestData, string $event = 'created'): void { @@ -324,7 +323,7 @@ class PrintRequestsPortalService } /** - * @param array $row + * @param array $row * @return array */ public function normalizePrintRequestRow(array $row): array diff --git a/app/Services/Promotions/LevelProgressionService.php b/app/Services/Promotions/LevelProgressionService.php index c85e75f7..60b42182 100644 --- a/app/Services/Promotions/LevelProgressionService.php +++ b/app/Services/Promotions/LevelProgressionService.php @@ -5,7 +5,6 @@ namespace App\Services\Promotions; use App\Models\ClassSection; use App\Models\LevelProgression; use App\Models\SchoolClass; -use App\Services\School\AccountEventService; use Illuminate\Support\Collection; /** @@ -13,7 +12,7 @@ use Illuminate\Support\Collection; * `level_progressions` table (plan section 10). * * Falls back to `class_id + 1` only if no mapping is found, matching - * the legacy behaviour in {@see AccountEventService}. + * the legacy behaviour in {@see \App\Services\School\AccountEventService}. */ class LevelProgressionService { @@ -24,15 +23,15 @@ class LevelProgressionService public const DEFAULT_MAP = [ ['current' => 'KG1', 'next' => 'KG2', 'order' => 10, 'terminal' => false], ['current' => 'KG2', 'next' => 'Grade 1', 'order' => 20, 'terminal' => false], - ['current' => 'Grade 1', 'next' => 'Grade 2', 'order' => 30, 'terminal' => false], - ['current' => 'Grade 2', 'next' => 'Grade 3', 'order' => 40, 'terminal' => false], - ['current' => 'Grade 3', 'next' => 'Grade 4', 'order' => 50, 'terminal' => false], - ['current' => 'Grade 4', 'next' => 'Grade 5', 'order' => 60, 'terminal' => false], - ['current' => 'Grade 5', 'next' => 'Grade 6', 'order' => 70, 'terminal' => false], - ['current' => 'Grade 6', 'next' => 'Grade 7', 'order' => 80, 'terminal' => false], - ['current' => 'Grade 7', 'next' => 'Grade 8', 'order' => 90, 'terminal' => false], - ['current' => 'Grade 8', 'next' => 'Grade 9', 'order' => 100, 'terminal' => false], - ['current' => 'Grade 9', 'next' => 'Youth', 'order' => 110, 'terminal' => false], + ['current' => 'Grade 1','next' => 'Grade 2', 'order' => 30, 'terminal' => false], + ['current' => 'Grade 2','next' => 'Grade 3', 'order' => 40, 'terminal' => false], + ['current' => 'Grade 3','next' => 'Grade 4', 'order' => 50, 'terminal' => false], + ['current' => 'Grade 4','next' => 'Grade 5', 'order' => 60, 'terminal' => false], + ['current' => 'Grade 5','next' => 'Grade 6', 'order' => 70, 'terminal' => false], + ['current' => 'Grade 6','next' => 'Grade 7', 'order' => 80, 'terminal' => false], + ['current' => 'Grade 7','next' => 'Grade 8', 'order' => 90, 'terminal' => false], + ['current' => 'Grade 8','next' => 'Grade 9', 'order' => 100, 'terminal' => false], + ['current' => 'Grade 9','next' => 'Youth', 'order' => 110, 'terminal' => false], ['current' => 'Youth', 'next' => null, 'order' => 120, 'terminal' => true], ]; @@ -78,7 +77,7 @@ class LevelProgressionService } $current = SchoolClass::query()->find($currentClassId); - if (! $current) { + if (!$current) { return null; } $currentName = (string) ($current->class_name ?? ''); @@ -119,10 +118,9 @@ class LevelProgressionService return null; } $classId = ClassSection::getClassId($classSectionId); - if (! $classId) { + if (!$classId) { return null; } - return $this->resolveByCurrentClassId((int) $classId); } @@ -183,7 +181,6 @@ class LevelProgressionService if ($existing) { $existing->fill($data); $existing->save(); - return $existing; } diff --git a/app/Services/Promotions/Placement/BalancedSectionPlacementService.php b/app/Services/Promotions/Placement/BalancedSectionPlacementService.php index 0bb8e3d6..9e193d74 100644 --- a/app/Services/Promotions/Placement/BalancedSectionPlacementService.php +++ b/app/Services/Promotions/Placement/BalancedSectionPlacementService.php @@ -45,7 +45,6 @@ class BalancedSectionPlacementService return $scoreB <=> $scoreA; } $name = strcmp((string) ($a['student_name'] ?? ''), (string) ($b['student_name'] ?? '')); - return $name !== 0 ? $name : ((int) $a['student_id'] <=> (int) $b['student_id']); }); } @@ -80,7 +79,6 @@ class BalancedSectionPlacementService for ($i = 1; $i <= $sectionCount; $i++) { $sizes[$i] = $base + ($i <= $remainder ? 1 : 0); } - return $sizes; } @@ -100,7 +98,6 @@ class BalancedSectionPlacementService if ($cmp !== 0) { return $cmp; } - return $a['section_index'] <=> $b['section_index']; }); diff --git a/app/Services/Promotions/Placement/PlacementPoolBuilder.php b/app/Services/Promotions/Placement/PlacementPoolBuilder.php index 9e5818fe..03755292 100644 --- a/app/Services/Promotions/Placement/PlacementPoolBuilder.php +++ b/app/Services/Promotions/Placement/PlacementPoolBuilder.php @@ -7,7 +7,9 @@ use Illuminate\Support\Facades\DB; class PlacementPoolBuilder { - public function __construct(private ScoreBandClassifier $bands) {} + public function __construct(private ScoreBandClassifier $bands) + { + } public function build(string $fromSchoolYear, string $toSchoolYear, ?int $toGradeLevelId = null): array { @@ -36,39 +38,34 @@ class PlacementPoolBuilder $studentId = (int) $row->student_id; if ((int) ($row->is_active ?? 1) === 0) { $exceptions['withdrawn_or_inactive_student'][] = $studentId; - continue; } if ($this->alreadyPlaced($studentId, $toSchoolYear)) { $exceptions['duplicate_target_year_placement'][] = $studentId; - continue; } - if (! $this->isPromotedDecision((string) ($row->decision ?? ''))) { + if (!$this->isPromotedDecision((string) ($row->decision ?? ''))) { $bucket = $row->decision === null ? 'missing_student_decision' : 'decision_conflicts_with_enrollment_request'; $exceptions[$bucket][] = $studentId; - continue; } $score = $row->year_score !== null ? (float) $row->year_score : ($row->final_average !== null ? (float) $row->final_average : null); if ($score === null) { $exceptions['missing_student_decision'][] = $studentId; - continue; } try { $band = $this->bands->fromScore($score, true); } catch (\RuntimeException) { $exceptions['failed_retained'][] = $studentId; - continue; } $pool[] = [ 'student_id' => $studentId, 'student_type' => 'returning', - 'student_name' => trim((string) $row->lastname.', '.(string) $row->firstname), + 'student_name' => trim((string) $row->lastname . ', ' . (string) $row->firstname), 'source_decision_id' => $row->decision_id ? (int) $row->decision_id : null, 'source_enrollment_id' => $row->enrollment_id ? (int) $row->enrollment_id : null, 'final_score' => $score, @@ -101,19 +98,17 @@ class PlacementPoolBuilder $studentId = (int) $row->student_id; if ((int) ($row->is_active ?? 1) === 0) { $exceptions['withdrawn_or_inactive_student'][] = $studentId; - continue; } if ($this->alreadyPlaced($studentId, $toSchoolYear)) { $exceptions['duplicate_target_year_placement'][] = $studentId; - continue; } $pool[] = [ 'student_id' => $studentId, 'student_type' => 'new', - 'student_name' => trim((string) $row->lastname.', '.(string) $row->firstname), + 'student_name' => trim((string) $row->lastname . ', ' . (string) $row->firstname), 'source_decision_id' => null, 'source_enrollment_id' => (int) $row->enrollment_id, 'final_score' => null, @@ -182,7 +177,6 @@ class PlacementPoolBuilder private function isPromotedDecision(string $decision): bool { $normalized = strtolower(trim($decision)); - return in_array($normalized, ['passed', 'pass', 'promoted', 'promote', 'eligible_to_continue'], true); } } diff --git a/app/Services/Promotions/Placement/PromotionSectionCapacityService.php b/app/Services/Promotions/Placement/PromotionSectionCapacityService.php index 412caf47..62770bd9 100644 --- a/app/Services/Promotions/Placement/PromotionSectionCapacityService.php +++ b/app/Services/Promotions/Placement/PromotionSectionCapacityService.php @@ -9,7 +9,6 @@ use RuntimeException; class PromotionSectionCapacityService { public const CONFIG_KEY = 'promotion.section_capacity'; - public const DEFAULT_CAPACITY = 20; public function capacity(): array @@ -24,7 +23,7 @@ class PromotionSectionCapacityService 'fallback' => self::DEFAULT_CAPACITY, ]); $value = self::DEFAULT_CAPACITY; - } elseif (! ctype_digit((string) $raw)) { + } elseif (!ctype_digit((string) $raw)) { throw new RuntimeException('promotion.section_capacity must be a positive integer.'); } else { $value = (int) $raw; diff --git a/app/Services/Promotions/Placement/SectionPlacementPreviewService.php b/app/Services/Promotions/Placement/SectionPlacementPreviewService.php index 555cfea9..daa402ac 100644 --- a/app/Services/Promotions/Placement/SectionPlacementPreviewService.php +++ b/app/Services/Promotions/Placement/SectionPlacementPreviewService.php @@ -14,7 +14,8 @@ class SectionPlacementPreviewService private PromotionSectionCapacityService $capacityService, private PlacementPoolBuilder $poolBuilder, private BalancedSectionPlacementService $placementService - ) {} + ) { + } public function createDraft( string $fromSchoolYear, @@ -118,12 +119,12 @@ class SectionPlacementPreviewService ->where('is_event_only', 0) ->exists(); if ($duplicate) { - throw new RuntimeException('Student '.$studentId.' already has a target-year class placement.'); + throw new RuntimeException('Student ' . $studentId . ' already has a target-year class placement.'); } $sectionId = $sectionMap[(int) $student->planned_section_index] ?? null; if ($sectionId === null) { - throw new RuntimeException('Missing target section for planned section '.$student->planned_section_index.'.'); + throw new RuntimeException('Missing target section for planned section ' . $student->planned_section_index . '.'); } DB::table('student_class')->insert([ @@ -132,7 +133,7 @@ class SectionPlacementPreviewService 'is_event_only' => 0, 'semester' => $semester, 'school_year' => $batch->to_school_year, - 'description' => 'Finalized from section placement batch #'.$batch->getKey(), + 'description' => 'Finalized from section placement batch #' . $batch->getKey(), 'created_at' => now(), 'updated_at' => now(), 'updated_by' => $userId, @@ -153,7 +154,7 @@ class SectionPlacementPreviewService private function ensureSections(SectionPlacementBatch $batch, string $semester): array { - if (! $batch->to_grade_level_id) { + if (!$batch->to_grade_level_id) { throw new RuntimeException('to_grade_level_id is required to finalize placement sections.'); } @@ -175,7 +176,6 @@ class SectionPlacementPreviewService if ($existing) { $map[$i] = (int) $existing; - continue; } @@ -198,8 +198,7 @@ class SectionPlacementPreviewService private function sectionName(int $classId, string $suffix, string $schoolYear): string { $className = DB::table('classes')->where('id', $classId)->value('class_name'); - $base = $className ? (string) $className : ('Class '.$classId); - - return $base.'-'.$suffix.' '.$schoolYear; + $base = $className ? (string) $className : ('Class ' . $classId); + return $base . '-' . $suffix . ' ' . $schoolYear; } } diff --git a/app/Services/Promotions/PromotionEligibilityService.php b/app/Services/Promotions/PromotionEligibilityService.php index 3d2d169e..29845c89 100644 --- a/app/Services/Promotions/PromotionEligibilityService.php +++ b/app/Services/Promotions/PromotionEligibilityService.php @@ -24,12 +24,13 @@ class PromotionEligibilityService private LevelProgressionService $progression, private PromotionStatusService $statusService, private PromotionAuditService $audit - ) {} + ) { + } /** * Evaluate eligibility for a single student. * - * @param int|null $userId actor id for audit + * @param int|null $userId actor id for audit */ public function evaluateStudent( int $studentId, @@ -37,7 +38,7 @@ class PromotionEligibilityService ?int $userId = null ): ?StudentPromotionRecord { $student = Student::query()->find($studentId); - if (! $student) { + if (!$student) { return null; } @@ -82,7 +83,7 @@ class PromotionEligibilityService $summary = $this->emptySummary(); foreach ($studentIds as $studentId) { $student = Student::query()->find($studentId); - if (! $student) { + if (!$student) { continue; } try { @@ -131,7 +132,7 @@ class PromotionEligibilityService $summary = $this->emptySummary(); foreach ($studentIds as $studentId) { $student = Student::query()->find($studentId); - if (! $student) { + if (!$student) { continue; } try { @@ -213,7 +214,7 @@ class PromotionEligibilityService ->first(); $isNew = $existing === null; - $record = $existing ?: new StudentPromotionRecord; + $record = $existing ?: new StudentPromotionRecord(); $record->fill([ 'student_id' => $studentId, 'parent_id' => (int) ($student->parent_id ?? 0) ?: null, @@ -287,7 +288,7 @@ class PromotionEligibilityService ->orderByDesc('id') ->first(); - if (! $decision) { + if (!$decision) { return [ 'passed' => null, 'final_average' => null, @@ -331,14 +332,14 @@ class PromotionEligibilityService return [ 'passed' => null, 'final_average' => $score, - 'notes' => 'student_decisions has unrecognized decision: '.$rawDecision, + 'notes' => 'student_decisions has unrecognized decision: ' . $rawDecision, 'has_data' => false, ]; } private function resolveStatusFromEvaluation(?bool $passed, bool $terminal, bool $hasData): string { - if (! $hasData) { + if (!$hasData) { return StudentPromotionRecord::STATUS_ON_HOLD; } if ($passed === false) { @@ -347,7 +348,6 @@ class PromotionEligibilityService if ($terminal) { return StudentPromotionRecord::STATUS_GRADUATED; } - return StudentPromotionRecord::STATUS_AWAITING_PARENT; } @@ -380,17 +380,15 @@ class PromotionEligibilityService private function resolveCurrentSchoolYear(): ?string { $year = (string) (Configuration::getConfigValueByKey('school_year') ?? ''); - return $year !== '' ? $year : null; } private function nextSchoolYear(string $current): ?string { - if (! preg_match('/^(\d{4})-(\d{4})$/', $current, $m)) { + if (!preg_match('/^(\d{4})-(\d{4})$/', $current, $m)) { return null; } - - return ((int) $m[1] + 1).'-'.((int) $m[2] + 1); + return ((int) $m[1] + 1) . '-' . ((int) $m[2] + 1); } private function passThreshold(): float @@ -399,7 +397,6 @@ class PromotionEligibilityService if ($configured !== null && is_numeric($configured)) { return (float) $configured; } - return self::DEFAULT_PASS_THRESHOLD; } } diff --git a/app/Services/Promotions/PromotionEnrollmentService.php b/app/Services/Promotions/PromotionEnrollmentService.php index 2ad07f09..b6a3c54c 100644 --- a/app/Services/Promotions/PromotionEnrollmentService.php +++ b/app/Services/Promotions/PromotionEnrollmentService.php @@ -28,7 +28,8 @@ class PromotionEnrollmentService public function __construct( private PromotionStatusService $statusService, private PromotionAuditService $audit - ) {} + ) { + } /** * Lists actionable promotion records for a parent (the parent @@ -53,9 +54,9 @@ class PromotionEnrollmentService $studentIds = $records->pluck('student_id')->unique()->all(); /** @var EloquentCollection $studentsCollection */ - $studentsCollection = ! empty($studentIds) + $studentsCollection = !empty($studentIds) ? Student::query()->whereIn('id', $studentIds)->get() - : new EloquentCollection; + : new EloquentCollection(); $studentsById = $studentsCollection->keyBy('id'); $payload = $records->map(function (StudentPromotionRecord $record) use ($studentsById) { @@ -79,7 +80,7 @@ class PromotionEnrollmentService $this->assertActionable($record); return DB::transaction(function () use ($record, $parentId, $userId) { - if (! $record->enrollment_started_at) { + if (!$record->enrollment_started_at) { $record->enrollment_started_at = now(); } $record->enrollment_status = StudentPromotionRecord::ENROLLMENT_IN_PROGRESS; @@ -116,7 +117,7 @@ class PromotionEnrollmentService $sanitized = []; foreach ($steps as $field => $value) { - if (! in_array($field, self::ALLOWED_STEPS, true)) { + if (!in_array($field, self::ALLOWED_STEPS, true)) { continue; } $sanitized[$field] = (bool) $value; @@ -138,7 +139,7 @@ class PromotionEnrollmentService return $record; } - if (! $record->enrollment_started_at) { + if (!$record->enrollment_started_at) { $record->enrollment_started_at = now(); } $record->enrollment_status = StudentPromotionRecord::ENROLLMENT_IN_PROGRESS; @@ -184,7 +185,7 @@ class PromotionEnrollmentService $this->assertParentOwns($record, $parentId); $this->assertActionable($record); - if (! $record->enrollmentChecklistComplete()) { + if (!$record->enrollmentChecklistComplete()) { throw new RuntimeException('Enrollment checklist is incomplete.'); } @@ -293,12 +294,10 @@ class PromotionEnrollmentService if ($existing) { $existing->fill($payload); $existing->save(); - return (int) $existing->getKey(); } $created = Enrollment::query()->create($payload); - return $created ? (int) $created->getKey() : null; } @@ -308,7 +307,7 @@ class PromotionEnrollmentService 'promotion_id' => (int) $record->getKey(), 'student_id' => (int) $record->student_id, 'student_name' => $student - ? trim((string) $student->firstname.' '.(string) $student->lastname) + ? trim((string) $student->firstname . ' ' . (string) $student->lastname) : null, 'current_level' => $record->current_level_name, 'promoted_level' => $record->promoted_level_name, @@ -344,13 +343,12 @@ class PromotionEnrollmentService true ); }); - if (! $actionable) { + if (!$actionable) { return null; } $level = $actionable->promoted_level_name ?: 'the next level'; $deadline = $actionable->enrollment_deadline?->toDateString(); - $deadlineText = $deadline ? 'by '.$deadline : 'as soon as possible'; - + $deadlineText = $deadline ? 'by ' . $deadline : 'as soon as possible'; return sprintf( 'Your child is eligible for promotion to %s. Please complete enrollment for the new school year %s.', $level, @@ -374,7 +372,7 @@ class PromotionEnrollmentService private function assertActionable(StudentPromotionRecord $record): void { $actionable = StudentPromotionRecord::parentActionableStatuses(); - if (! in_array($record->promotion_status, $actionable, true)) { + if (!in_array($record->promotion_status, $actionable, true)) { throw new RuntimeException(sprintf( 'Promotion record is not actionable in status "%s".', $record->promotion_status @@ -385,10 +383,9 @@ class PromotionEnrollmentService private function guessNextSchoolYear(): ?string { $current = (string) (Configuration::getConfigValueByKey('school_year') ?? ''); - if (! preg_match('/^(\d{4})-(\d{4})$/', $current, $m)) { + if (!preg_match('/^(\d{4})-(\d{4})$/', $current, $m)) { return null; } - - return ((int) $m[1] + 1).'-'.((int) $m[2] + 1); + return ((int) $m[1] + 1) . '-' . ((int) $m[2] + 1); } } diff --git a/app/Services/Promotions/PromotionQueryService.php b/app/Services/Promotions/PromotionQueryService.php index c4b99455..8cd3eb31 100644 --- a/app/Services/Promotions/PromotionQueryService.php +++ b/app/Services/Promotions/PromotionQueryService.php @@ -42,6 +42,7 @@ class PromotionQueryService * parent_action_required?: bool, * per_page?: int|null, * } $filters + * * @return array{ * data: array>, * total: int, @@ -67,12 +68,12 @@ class PromotionQueryService $studentIds = $records->pluck('student_id')->unique()->values()->all(); $parentIds = $records->pluck('parent_id')->filter()->unique()->values()->all(); - $studentsById = ! empty($studentIds) + $studentsById = !empty($studentIds) ? Student::query()->whereIn('id', $studentIds)->get()->keyBy('id') - : new EloquentCollection; - $parentsById = ! empty($parentIds) + : new EloquentCollection(); + $parentsById = !empty($parentIds) ? User::query()->whereIn('id', $parentIds)->get()->keyBy('id') - : new EloquentCollection; + : new EloquentCollection(); $rows = $records->map(function (StudentPromotionRecord $record) use ($studentsById, $parentsById) { return $this->presentAdminRow( @@ -116,7 +117,6 @@ class PromotionQueryService foreach ($rows as $row) { $counts[(string) $row->promotion_status] = (int) $row->total; } - return $counts; } @@ -133,34 +133,34 @@ class PromotionQueryService } /** - * @param Builder $query + * @param Builder $query */ private function applyFilters(Builder $query, array $filters): void { - if (! empty($filters['status'])) { + if (!empty($filters['status'])) { $statuses = is_array($filters['status']) ? $filters['status'] : [$filters['status']]; $statuses = array_values(array_filter($statuses, static fn ($s) => is_string($s) && $s !== '')); - if (! empty($statuses)) { + if (!empty($statuses)) { $query->whereIn('promotion_status', $statuses); } } - if (! empty($filters['parent_action_required'])) { + if (!empty($filters['parent_action_required'])) { $query->whereIn('promotion_status', StudentPromotionRecord::parentActionableStatuses()); } - if (! empty($filters['next_school_year'])) { + if (!empty($filters['next_school_year'])) { $query->where('next_school_year', $filters['next_school_year']); } - if (! empty($filters['current_school_year'])) { + if (!empty($filters['current_school_year'])) { $query->where('current_school_year', $filters['current_school_year']); } - if (! empty($filters['parent_id'])) { + if (!empty($filters['parent_id'])) { $query->where('parent_id', (int) $filters['parent_id']); } - if (! empty($filters['student_id'])) { + if (!empty($filters['student_id'])) { $query->where('student_id', (int) $filters['student_id']); } - if (! empty($filters['search'])) { - $search = '%'.strtolower((string) $filters['search']).'%'; + if (!empty($filters['search'])) { + $search = '%' . strtolower((string) $filters['search']) . '%'; $query->whereIn('student_id', function ($sub) use ($search) { $sub->select('id') ->from('students') @@ -182,12 +182,12 @@ class PromotionQueryService 'promotion_id' => (int) $record->getKey(), 'student_id' => (int) $record->student_id, 'student_name' => $student - ? trim((string) $student->firstname.' '.(string) $student->lastname) + ? trim((string) $student->firstname . ' ' . (string) $student->lastname) : null, 'school_id' => $student->school_id ?? null, 'parent_id' => $record->parent_id ? (int) $record->parent_id : null, 'parent_name' => $parent - ? trim((string) $parent->firstname.' '.(string) $parent->lastname) + ? trim((string) $parent->firstname . ' ' . (string) $parent->lastname) : null, 'parent_email' => $parent->email ?? null, 'current_school_year' => $record->current_school_year, diff --git a/app/Services/Promotions/PromotionReminderService.php b/app/Services/Promotions/PromotionReminderService.php index 64e030b9..5227946b 100644 --- a/app/Services/Promotions/PromotionReminderService.php +++ b/app/Services/Promotions/PromotionReminderService.php @@ -19,7 +19,7 @@ use Illuminate\Support\Facades\Log; */ class PromotionReminderService { - /** @var (callable(int, string, string, array):void)|null */ + /** @var (callable(int $userId, string $title, string $body, array $channels):void)|null */ private $dispatcher; public function __construct( @@ -40,7 +40,7 @@ class PromotionReminderService ?string $message = null, array $channels = ['in_app', 'email'] ): PromotionReminderLog { - if (! in_array($reminderType, PromotionReminderLog::ALLOWED_TYPES, true)) { + if (!in_array($reminderType, PromotionReminderLog::ALLOWED_TYPES, true)) { $reminderType = PromotionReminderLog::TYPE_MANUAL; } @@ -53,7 +53,7 @@ class PromotionReminderService '%s is eligible for promotion to %s.%s Please complete enrollment as soon as possible.', $studentName ?: 'Your child', $level, - $deadline ? ' Deadline: '.$deadline.'.' : '' + $deadline ? ' Deadline: ' . $deadline . '.' : '' ); $finalSubject = $subject ?: $defaultSubject; @@ -83,7 +83,7 @@ class PromotionReminderService $this->dispatch((int) $record->parent_id, $finalSubject, $finalMessage, $channels); } - if (! $record->parent_notified_at) { + if (!$record->parent_notified_at) { $record->parent_notified_at = now(); $record->save(); $this->audit->logParentNotified($record, $userId, $reminderType); @@ -118,7 +118,6 @@ class PromotionReminderService $type = $this->resolveReminderType($record, $now); if ($type === null) { $skipped++; - continue; } $alreadySent = PromotionReminderLog::query() @@ -127,7 +126,6 @@ class PromotionReminderService ->exists(); if ($alreadySent) { $skipped++; - continue; } try { @@ -179,24 +177,22 @@ class PromotionReminderService if ($elapsedSeconds * 2 >= $totalSeconds) { return PromotionReminderLog::TYPE_HALFWAY; } - return PromotionReminderLog::TYPE_FIRST; } private function studentName(StudentPromotionRecord $record): ?string { $student = Student::query()->find($record->student_id); - if (! $student) { + if (!$student) { return null; } - $name = trim((string) ($student->firstname ?? '').' '.(string) ($student->lastname ?? '')); - + $name = trim((string) ($student->firstname ?? '') . ' ' . (string) ($student->lastname ?? '')); return $name !== '' ? $name : null; } private function dispatch(int $userId, string $subject, string $message, array $channels): void { - if (! $this->dispatcher) { + if (!$this->dispatcher) { return; } try { diff --git a/app/Services/Promotions/PromotionStatusService.php b/app/Services/Promotions/PromotionStatusService.php index 1b61de3d..6399a451 100644 --- a/app/Services/Promotions/PromotionStatusService.php +++ b/app/Services/Promotions/PromotionStatusService.php @@ -15,7 +15,9 @@ use RuntimeException; */ class PromotionStatusService { - public function __construct(private PromotionAuditService $audit) {} + public function __construct(private PromotionAuditService $audit) + { + } /** * Map of allowed transitions from current → set of next statuses. @@ -78,11 +80,10 @@ class PromotionStatusService if ($from === $to) { return true; } - if (! in_array($to, StudentPromotionRecord::ALL_STATUSES, true)) { + if (!in_array($to, StudentPromotionRecord::ALL_STATUSES, true)) { return false; } $allowed = self::ALLOWED_TRANSITIONS[$from] ?? []; - return in_array($to, $allowed, true); } @@ -97,7 +98,7 @@ class PromotionStatusService ?int $userId = null, ?string $notes = null ): StudentPromotionRecord { - if (! in_array($newStatus, StudentPromotionRecord::ALL_STATUSES, true)) { + if (!in_array($newStatus, StudentPromotionRecord::ALL_STATUSES, true)) { throw new InvalidArgumentException(sprintf('Unknown promotion status "%s".', $newStatus)); } @@ -105,7 +106,7 @@ class PromotionStatusService if ($oldStatus === $newStatus) { return $record; } - if (! $this->canTransition($oldStatus, $newStatus)) { + if (!$this->canTransition($oldStatus, $newStatus)) { throw new RuntimeException(sprintf( 'Transition from "%s" to "%s" is not allowed.', $oldStatus, @@ -134,7 +135,7 @@ class PromotionStatusService ?int $userId = null, ?string $notes = null ): StudentPromotionRecord { - if (! in_array($newStatus, StudentPromotionRecord::ALL_STATUSES, true)) { + if (!in_array($newStatus, StudentPromotionRecord::ALL_STATUSES, true)) { throw new InvalidArgumentException(sprintf('Unknown promotion status "%s".', $newStatus)); } diff --git a/app/Services/PublicWinners/PublicWinnersPortalService.php b/app/Services/PublicWinners/PublicWinnersPortalService.php index 31484b33..95954a7c 100644 --- a/app/Services/PublicWinners/PublicWinnersPortalService.php +++ b/app/Services/PublicWinners/PublicWinnersPortalService.php @@ -2,14 +2,12 @@ namespace App\Services\PublicWinners; -use App\Controllers\WinnersController; use App\Models\ClassSection; use App\Models\Competition; -use Carbon\CarbonInterface; use Illuminate\Support\Facades\DB; /** - * legacy {@see WinnersController} — public published winners pages. + * legacy {@see \App\Controllers\WinnersController} — public published winners pages. */ class PublicWinnersPortalService { @@ -106,7 +104,7 @@ class PublicWinnersPortalService { $arr = $c->toArray(); foreach ($arr as $key => $value) { - if ($value instanceof CarbonInterface) { + if ($value instanceof \Carbon\CarbonInterface) { $arr[$key] = $value->format('Y-m-d H:i:s'); } } diff --git a/app/Services/PurchaseOrders/PurchaseOrderCreateService.php b/app/Services/PurchaseOrders/PurchaseOrderCreateService.php index 1729b15c..c7776451 100644 --- a/app/Services/PurchaseOrders/PurchaseOrderCreateService.php +++ b/app/Services/PurchaseOrders/PurchaseOrderCreateService.php @@ -12,7 +12,7 @@ class PurchaseOrderCreateService public function create(array $payload): PurchaseOrder { $itemsPayload = $payload['items'] ?? []; - if (empty($itemsPayload) || ! is_array($itemsPayload)) { + if (empty($itemsPayload) || !is_array($itemsPayload)) { throw new RuntimeException('Add at least one line item.'); } diff --git a/app/Services/PurchaseOrders/PurchaseOrderQueryService.php b/app/Services/PurchaseOrders/PurchaseOrderQueryService.php index 22145d57..7bdab684 100644 --- a/app/Services/PurchaseOrders/PurchaseOrderQueryService.php +++ b/app/Services/PurchaseOrders/PurchaseOrderQueryService.php @@ -45,7 +45,7 @@ class PurchaseOrderQueryService ->where('purchase_orders.id', $id) ->first(); - if (! $po) { + if (!$po) { return null; } diff --git a/app/Services/PurchaseOrders/PurchaseOrderReceiveService.php b/app/Services/PurchaseOrders/PurchaseOrderReceiveService.php index b919b5a3..7539f623 100644 --- a/app/Services/PurchaseOrders/PurchaseOrderReceiveService.php +++ b/app/Services/PurchaseOrders/PurchaseOrderReceiveService.php @@ -14,7 +14,7 @@ class PurchaseOrderReceiveService public function receive(int $poId, array $received, string $issuedBy): array { $po = PurchaseOrder::query()->find($poId); - if (! $po || in_array($po->status, [PurchaseOrder::STATUS_CANCELED, PurchaseOrder::STATUS_RECEIVED], true)) { + if (!$po || in_array($po->status, [PurchaseOrder::STATUS_CANCELED, PurchaseOrder::STATUS_RECEIVED], true)) { throw new RuntimeException('PO not receivable.'); } @@ -37,9 +37,8 @@ class PurchaseOrderReceiveService ->where('id', $itemId) ->first(); - if (! $poItem) { + if (!$poItem) { $completed = false; - continue; } @@ -53,9 +52,8 @@ class PurchaseOrderReceiveService $poItem->save(); $supply = Supply::query()->find($poItem->supply_id); - if (! $supply) { + if (!$supply) { $completed = false; - continue; } @@ -66,7 +64,7 @@ class PurchaseOrderReceiveService 'supply_id' => $supply->id, 'type' => 'in', 'quantity' => $toReceive, - 'ref' => 'PO '.($po->po_number ?? $po->id), + 'ref' => 'PO ' . ($po->po_number ?? $po->id), 'issued_to' => 'Inventory', 'issued_by' => $issuedBy, 'notes' => 'Received against PO', diff --git a/app/Services/PurchaseOrders/PurchaseOrderStatusService.php b/app/Services/PurchaseOrders/PurchaseOrderStatusService.php index 5eeae0d0..3a417589 100644 --- a/app/Services/PurchaseOrders/PurchaseOrderStatusService.php +++ b/app/Services/PurchaseOrders/PurchaseOrderStatusService.php @@ -10,7 +10,7 @@ class PurchaseOrderStatusService public function cancel(int $poId): PurchaseOrder { $po = PurchaseOrder::query()->find($poId); - if (! $po || $po->status === PurchaseOrder::STATUS_RECEIVED) { + if (!$po || $po->status === PurchaseOrder::STATUS_RECEIVED) { throw new RuntimeException('Cannot cancel this PO.'); } diff --git a/app/Services/Refunds/RefundDecisionService.php b/app/Services/Refunds/RefundDecisionService.php index 75808864..c57baddf 100644 --- a/app/Services/Refunds/RefundDecisionService.php +++ b/app/Services/Refunds/RefundDecisionService.php @@ -9,11 +9,11 @@ class RefundDecisionService public function updateDecision(int $refundId, string $status, string $reason, int $actorId): array { $refund = Refund::query()->find($refundId); - if (! $refund) { + if (!$refund) { return ['ok' => false, 'message' => 'Refund not found.']; } - if (! in_array($status, [Refund::STATUS_APPROVED, Refund::STATUS_REJECTED], true)) { + if (!in_array($status, [Refund::STATUS_APPROVED, Refund::STATUS_REJECTED], true)) { return ['ok' => false, 'message' => 'Invalid refund status.']; } @@ -46,7 +46,7 @@ class RefundDecisionService public function cancel(int $refundId, int $actorId): array { $refund = Refund::query()->find($refundId); - if (! $refund) { + if (!$refund) { return ['ok' => false, 'message' => 'Refund not found.']; } diff --git a/app/Services/Refunds/RefundInvoiceAdjustmentService.php b/app/Services/Refunds/RefundInvoiceAdjustmentService.php index c5dd0345..ecccc8ae 100644 --- a/app/Services/Refunds/RefundInvoiceAdjustmentService.php +++ b/app/Services/Refunds/RefundInvoiceAdjustmentService.php @@ -8,7 +8,9 @@ use App\Services\Discounts\DiscountInvoiceService; class RefundInvoiceAdjustmentService { - public function __construct(private DiscountInvoiceService $invoiceService) {} + public function __construct(private DiscountInvoiceService $invoiceService) + { + } public function applyBookChargeAndUpdateInvoice( Invoice $invoice, @@ -21,7 +23,7 @@ class RefundInvoiceAdjustmentService $bookChargeDesc = 'Book fee retained for withdrawals before the refund deadline.'; $bookPrice = (float) ($refundDetails['book_price'] ?? 0.0); - $eligible = ! empty($refundDetails['eligible']); + $eligible = !empty($refundDetails['eligible']); $bookChargeDelta = 0.0; $bookChargeAction = 'none'; diff --git a/app/Services/Refunds/RefundOverpaymentService.php b/app/Services/Refunds/RefundOverpaymentService.php index f3e452df..5b875617 100644 --- a/app/Services/Refunds/RefundOverpaymentService.php +++ b/app/Services/Refunds/RefundOverpaymentService.php @@ -15,7 +15,8 @@ class RefundOverpaymentService public function __construct( private RefundNotificationService $notifications, private ApplicationUrlService $urls, - ) {} + ) { + } public function recalculate(bool $triggerEvents = false, ?string $invoiceNumber = null, ?int $actorId = null): array { @@ -76,7 +77,7 @@ class RefundOverpaymentService private function recalculateForInvoice(string $invoiceNumber, bool $triggerEvents, ?int $actorId): array { $invoice = Invoice::query()->where('invoice_number', $invoiceNumber)->first(); - if (! $invoice) { + if (!$invoice) { return ['ok' => false, 'message' => 'Invoice not found.']; } @@ -120,7 +121,6 @@ class RefundOverpaymentService 'updated_at' => utc_now(), 'updated_by' => $actorId, ]); - return ['ok' => true, 'message' => 'Overpayment updated for invoice.']; } @@ -204,7 +204,6 @@ class RefundOverpaymentService 'updated_by' => $actorId, ]); $updated[] = (int) $openRow->id; - continue; } @@ -225,7 +224,6 @@ class RefundOverpaymentService 'updated_by' => $actorId, ]); $updated[] = (int) $parentLevelOpen->id; - continue; } @@ -236,7 +234,7 @@ class RefundOverpaymentService ->orderByDesc('id') ->first(); - if (! $existing || $existing->status === Refund::STATUS_PAID) { + if (!$existing || $existing->status === Refund::STATUS_PAID) { $refund = Refund::query()->create([ 'parent_id' => $pid, 'school_year' => $schoolYear, diff --git a/app/Services/Refunds/RefundPayoutService.php b/app/Services/Refunds/RefundPayoutService.php index 0a6ec0e3..33a687c1 100644 --- a/app/Services/Refunds/RefundPayoutService.php +++ b/app/Services/Refunds/RefundPayoutService.php @@ -14,11 +14,11 @@ class RefundPayoutService public function recordPayment(int $refundId, array $payload, int $actorId): array { $refund = Refund::query()->find($refundId); - if (! $refund) { + if (!$refund) { return ['ok' => false, 'message' => 'Refund not found.']; } - if (! in_array($refund->status, [Refund::STATUS_APPROVED, Refund::STATUS_PARTIAL], true)) { + if (!in_array($refund->status, [Refund::STATUS_APPROVED, Refund::STATUS_PARTIAL], true)) { return ['ok' => false, 'message' => 'Refund must be Approved or Partial to pay.']; } @@ -126,7 +126,6 @@ class RefundPayoutService return $latest ? (int) $latest->id : null; } catch (\Throwable $e) { Log::warning('Refund invoice assignment failed.', ['error' => $e->getMessage()]); - return null; } } diff --git a/app/Services/Refunds/RefundPolicyService.php b/app/Services/Refunds/RefundPolicyService.php index 62aa28c8..2e0f4757 100644 --- a/app/Services/Refunds/RefundPolicyService.php +++ b/app/Services/Refunds/RefundPolicyService.php @@ -14,7 +14,8 @@ class RefundPolicyService public function __construct( private FeeRefundDetailService $refundDetails, private RefundInvoiceAdjustmentService $invoiceAdjustments - ) {} + ) { + } public function processWithdrawalRefund( int $parentId, @@ -38,7 +39,7 @@ class RefundPolicyService ->orderByDesc('created_at') ->first(); - if (! $invoice) { + if (!$invoice) { return ['ok' => false, 'error' => 'No invoice found for refund processing.']; } @@ -115,14 +116,14 @@ class RefundPolicyService $refundAmount = (float) ($details['refund_amount'] ?? 0.0); $debitAmount = (float) ($details['debit_amount'] ?? 0.0); - $eligible = ! empty($details['eligible']); + $eligible = !empty($details['eligible']); $status = $refundAmount > 0 ? Refund::STATUS_PENDING : Refund::STATUS_REJECTED; $reason = null; if ($refundAmount <= 0) { - if (! $eligible) { + if (!$eligible) { $reason = 'No refund due to late withdrawal.'; - } elseif (! empty($details['full_discount'])) { + } elseif (!empty($details['full_discount'])) { $reason = 'No refund due to full discount.'; } elseif ($debitAmount > 0) { $reason = 'No refund due; book fee balance remains.'; @@ -188,7 +189,7 @@ class RefundPolicyService 'updated_by' => $actorId, ]); - if (! $created) { + if (!$created) { return ['ok' => false, 'error' => 'Failed to create refund record.']; } diff --git a/app/Services/Refunds/RefundQueryService.php b/app/Services/Refunds/RefundQueryService.php index 1289e832..cf3fb261 100644 --- a/app/Services/Refunds/RefundQueryService.php +++ b/app/Services/Refunds/RefundQueryService.php @@ -16,7 +16,7 @@ class RefundQueryService $sortDir = strtolower((string) ($filters['sort_dir'] ?? 'desc')) === 'asc' ? 'asc' : 'desc'; $allowedSorts = ['created_at', 'updated_at', 'refund_amount', 'status', 'parent_id', 'invoice_id']; - if (! in_array($sortBy, $allowedSorts, true)) { + if (!in_array($sortBy, $allowedSorts, true)) { $sortBy = 'created_at'; } @@ -29,25 +29,25 @@ class RefundQueryService ->leftJoin('users as parents', 'parents.id', '=', 'refunds.parent_id') ->leftJoin('users as approvers', 'approvers.id', '=', 'refunds.approved_by'); - if (! empty($filters['status'])) { + if (!empty($filters['status'])) { $query->where('refunds.status', $filters['status']); } - if (! empty($filters['request'])) { + if (!empty($filters['request'])) { $query->where('refunds.request', $filters['request']); } - if (! empty($filters['parent_id'])) { + if (!empty($filters['parent_id'])) { $query->where('refunds.parent_id', (int) $filters['parent_id']); } - if (! empty($filters['invoice_id'])) { + if (!empty($filters['invoice_id'])) { $query->where('refunds.invoice_id', (int) $filters['invoice_id']); } - if (! empty($filters['school_year'])) { + if (!empty($filters['school_year'])) { $query->where('refunds.school_year', (string) $filters['school_year']); } - if (! empty($filters['semester'])) { + if (!empty($filters['semester'])) { $query->where('refunds.semester', (string) $filters['semester']); } - if (! empty($filters['q'])) { + if (!empty($filters['q'])) { $term = trim((string) $filters['q']); $query->where(function ($where) use ($term) { $where->where('refunds.reason', 'like', "%{$term}%") @@ -57,7 +57,7 @@ class RefundQueryService }); } - $paginator = $query->orderBy('refunds.'.$sortBy, $sortDir) + $paginator = $query->orderBy('refunds.' . $sortBy, $sortDir) ->paginate($perPage, ['*'], 'page', $page); return [ diff --git a/app/Services/Refunds/RefundRequestService.php b/app/Services/Refunds/RefundRequestService.php index aeb3aaad..fbc37bb9 100644 --- a/app/Services/Refunds/RefundRequestService.php +++ b/app/Services/Refunds/RefundRequestService.php @@ -18,7 +18,8 @@ class RefundRequestService private RefundSummaryService $summaryService, private RefundNotificationService $notifications, private ApplicationUrlService $urls, - ) {} + ) { + } public function requestRefund(array $payload, int $actorId): array { @@ -29,12 +30,12 @@ class RefundRequestService $paymentId = isset($payload['payment_id']) ? (int) $payload['payment_id'] : null; $reason = $payload['reason'] ?? null; - if (! in_array($requestType, self::REQUEST_TYPES, true)) { + if (!in_array($requestType, self::REQUEST_TYPES, true)) { return ['ok' => false, 'message' => 'Invalid refund request type.']; } $parent = User::query()->find($parentId); - if (! $parent) { + if (!$parent) { return ['ok' => false, 'message' => 'Parent not found.']; } @@ -100,19 +101,18 @@ class RefundRequestService 'status' => Refund::STATUS_PENDING, 'reason' => $reason, 'request' => $requestType, - 'note' => $paymentId ? ('duplicate-of-payment#'.$paymentId) : null, + 'note' => $paymentId ? ('duplicate-of-payment#' . $paymentId) : null, 'requested_at' => utc_now(), 'created_at' => utc_now(), 'updated_at' => utc_now(), 'updated_by' => $actorId, ]); - if (! $refund) { + if (!$refund) { Log::error('Failed to create refund request.', [ 'parent_id' => $parentId, 'type' => $requestType, ]); - return ['ok' => false, 'message' => 'Failed to create refund request.']; } diff --git a/app/Services/Reimbursements/ReimbursementBatchAssignmentService.php b/app/Services/Reimbursements/ReimbursementBatchAssignmentService.php index 29cbeec6..a56d19fb 100644 --- a/app/Services/Reimbursements/ReimbursementBatchAssignmentService.php +++ b/app/Services/Reimbursements/ReimbursementBatchAssignmentService.php @@ -9,7 +9,9 @@ use RuntimeException; class ReimbursementBatchAssignmentService { - public function __construct(private ReimbursementLookupService $lookupService) {} + public function __construct(private ReimbursementLookupService $lookupService) + { + } public function updateAssignment( int $expenseId, @@ -33,7 +35,7 @@ class ReimbursementBatchAssignmentService if ($batchId <= 0) { if ($activeItem) { $activeItem->update(['unassigned_at' => $now]); - if (! $reimbursementId && ! empty($activeItem->reimbursement_id)) { + if (!$reimbursementId && !empty($activeItem->reimbursement_id)) { $reimbursementId = (int) $activeItem->reimbursement_id; } } @@ -46,7 +48,7 @@ class ReimbursementBatchAssignmentService } $batch = ReimbursementBatch::query()->find($batchId); - if (! $batch || strtolower((string) $batch->status) !== ReimbursementBatch::STATUS_OPEN) { + if (!$batch || strtolower((string) $batch->status) !== ReimbursementBatch::STATUS_OPEN) { throw new RuntimeException('Batch not found or already closed.'); } @@ -61,7 +63,7 @@ class ReimbursementBatchAssignmentService } if ($activeSameBatch) { - if (! $reimbursementId) { + if (!$reimbursementId) { $reimbursementId = $activeItem->reimbursement_id ? (int) $activeItem->reimbursement_id : $this->lookupService->findReimbursementIdForExpense($expenseId); @@ -83,12 +85,12 @@ class ReimbursementBatchAssignmentService if ($activeItem) { $activeItem->update(['unassigned_at' => $now]); - if (! $reimbursementId && ! empty($activeItem->reimbursement_id)) { + if (!$reimbursementId && !empty($activeItem->reimbursement_id)) { $reimbursementId = (int) $activeItem->reimbursement_id; } } - if (! $reimbursementId) { + if (!$reimbursementId) { $reimbursementId = $this->lookupService->findReimbursementIdForExpense($expenseId); } @@ -103,7 +105,7 @@ class ReimbursementBatchAssignmentService 'semester' => $semester, ]); } catch (\Throwable $e) { - Log::error('Failed to assign expense to batch: '.$e->getMessage(), [ + Log::error('Failed to assign expense to batch: ' . $e->getMessage(), [ 'expense_id' => $expenseId, 'batch_id' => $batchId, ]); diff --git a/app/Services/Reimbursements/ReimbursementBatchService.php b/app/Services/Reimbursements/ReimbursementBatchService.php index 23829d16..698db538 100644 --- a/app/Services/Reimbursements/ReimbursementBatchService.php +++ b/app/Services/Reimbursements/ReimbursementBatchService.php @@ -12,7 +12,9 @@ use RuntimeException; class ReimbursementBatchService { - public function __construct(private ReimbursementLookupService $lookupService) {} + public function __construct(private ReimbursementLookupService $lookupService) + { + } public function createBatch(?string $title, int $userId, string $schoolYear, string $semester): array { @@ -29,7 +31,7 @@ class ReimbursementBatchService 'yearly_batch_number' => $sequence, ]); - $label = trim((string) $batch->title) !== '' ? (string) $batch->title : ('Batch #'.$sequence); + $label = trim((string) $batch->title) !== '' ? (string) $batch->title : ('Batch #' . $sequence); if (trim((string) $batch->title) === '') { $batch->title = $label; $batch->save(); @@ -45,7 +47,7 @@ class ReimbursementBatchService public function lockBatch(int $batchId, int $userId, string $schoolYear, string $semester): void { $batch = ReimbursementBatch::query()->find($batchId); - if (! $batch || strtolower((string) $batch->status) !== ReimbursementBatch::STATUS_OPEN) { + if (!$batch || strtolower((string) $batch->status) !== ReimbursementBatch::STATUS_OPEN) { throw new RuntimeException('Batch not found or already closed.'); } @@ -58,7 +60,7 @@ class ReimbursementBatchService $adminIds = array_values(array_unique($adminRows->pluck('admin_id')->map(fn ($id) => (int) $id)->all())); $requiredAdmins = array_values(array_filter($adminIds, static fn ($adminId) => $adminId > 0)); - if (! empty($requiredAdmins)) { + if (!empty($requiredAdmins)) { $uploadedAdminIds = ReimbursementBatchAdminFile::query() ->where('batch_id', $batchId) ->whereIn('admin_id', $requiredAdmins) @@ -69,7 +71,7 @@ class ReimbursementBatchService ->all(); foreach ($requiredAdmins as $adminId) { - if (! in_array($adminId, $uploadedAdminIds, true)) { + if (!in_array($adminId, $uploadedAdminIds, true)) { throw new RuntimeException('All admin sections must have a check file before submitting the batch.'); } } @@ -92,7 +94,7 @@ class ReimbursementBatchService $reimbId = $item->batch_reimb_id ?: ($item->expense_reimb_id ?: $this->lookupService->findReimbursementIdForExpense($expenseId)); - if (! $reimbId) { + if (!$reimbId) { $payload = [ 'expense_id' => $expenseId, 'amount' => (float) ($item->amount ?? 0), @@ -120,7 +122,7 @@ class ReimbursementBatchService ]); Expense::query()->where('id', $expenseId)->update(['reimbursement_id' => $reimbId]); - if (! empty($item->batch_item_id)) { + if (!empty($item->batch_item_id)) { ReimbursementBatchItem::query()->where('id', (int) $item->batch_item_id)->update([ 'reimbursement_id' => $reimbId, ]); diff --git a/app/Services/Reimbursements/ReimbursementDonationService.php b/app/Services/Reimbursements/ReimbursementDonationService.php index 26d25327..6413fee3 100644 --- a/app/Services/Reimbursements/ReimbursementDonationService.php +++ b/app/Services/Reimbursements/ReimbursementDonationService.php @@ -17,11 +17,11 @@ class ReimbursementDonationService } $expense = Expense::query()->find($expenseId); - if (! $expense) { + if (!$expense) { throw new RuntimeException('Expense not found.'); } - if (! empty($expense->reimbursement_id)) { + if (!empty($expense->reimbursement_id)) { throw new RuntimeException('Expense is already tied to a reimbursement.'); } @@ -46,7 +46,7 @@ class ReimbursementDonationService DB::commit(); } catch (\Throwable $e) { DB::rollBack(); - Log::error('Failed to mark donation: '.$e->getMessage(), ['expense_id' => $expenseId]); + Log::error('Failed to mark donation: ' . $e->getMessage(), ['expense_id' => $expenseId]); throw new RuntimeException('Unable to mark donation right now.'); } } diff --git a/app/Services/Reimbursements/ReimbursementEmailService.php b/app/Services/Reimbursements/ReimbursementEmailService.php index dd8ec02d..31adbcf6 100644 --- a/app/Services/Reimbursements/ReimbursementEmailService.php +++ b/app/Services/Reimbursements/ReimbursementEmailService.php @@ -11,7 +11,9 @@ use ZipArchive; class ReimbursementEmailService { - public function __construct(private ReimbursementRecipientService $recipients) {} + public function __construct(private ReimbursementRecipientService $recipients) + { + } public function sendBatchEmail( int $batchId, @@ -21,14 +23,14 @@ class ReimbursementEmailService array $checkIds ): bool { $batch = ReimbursementBatch::query()->find($batchId); - if (! $batch) { + if (!$batch) { throw new \RuntimeException('Requested batch was not found.'); } - $receiptRows = ! empty($receiptIds) ? $this->fetchBatchReceiptRows($batchId, $receiptIds) : []; + $receiptRows = !empty($receiptIds) ? $this->fetchBatchReceiptRows($batchId, $receiptIds) : []; $checkRows = []; - if (! empty($checkIds)) { + if (!empty($checkIds)) { $checkRows = ReimbursementBatchAdminFile::query() ->select('id as file_id', 'admin_id', 'filename', 'original_filename', 'uploaded_at') ->where('batch_id', $batchId) @@ -38,29 +40,29 @@ class ReimbursementEmailService $adminLookup = []; foreach ($this->recipients->adminRoster() as $admin) { - $adminLookup[(int) ($admin['id'] ?? 0)] = trim((string) (($admin['firstname'] ?? '').' '.($admin['lastname'] ?? ''))); + $adminLookup[(int) ($admin['id'] ?? 0)] = trim((string) (($admin['firstname'] ?? '') . ' ' . ($admin['lastname'] ?? ''))); } foreach ($checkRows as &$row) { $aid = (int) ($row['admin_id'] ?? 0); - $row['admin'] = $adminLookup[$aid] ?? ($aid > 0 ? 'Admin #'.$aid : 'Admin'); + $row['admin'] = $adminLookup[$aid] ?? ($aid > 0 ? 'Admin #' . $aid : 'Admin'); } unset($row); } $tempDir = storage_path('tmp'); - if (! is_dir($tempDir)) { + if (!is_dir($tempDir)) { mkdir($tempDir, 0755, true); } $cleanupPaths = []; - $batchLabel = trim((string) ($batch->title ?? '')) ?: ('Batch #'.((int) ($batch->yearly_batch_number ?? $batchId))); + $batchLabel = trim((string) ($batch->title ?? '')) ?: ('Batch #' . ((int) ($batch->yearly_batch_number ?? $batchId))); $batchFileLabel = $this->sanitizeAttachmentComponent($batchLabel); $csvPath = tempnam($tempDir, 'batch_csv_'); $cleanupPaths[] = $csvPath; $csvHandle = fopen($csvPath, 'w'); - if (! $csvHandle) { + if (!$csvHandle) { if (is_file($csvPath)) { @unlink($csvPath); } @@ -72,31 +74,31 @@ class ReimbursementEmailService $this->writeBatchCsvSummary($csvHandle, $receiptRows, $batchClosedDate); fclose($csvHandle); - $csvAttachmentName = $batchFileLabel.'_summary.csv'; + $csvAttachmentName = $batchFileLabel . '_summary.csv'; $receiptsArchivePath = null; $receiptZipCreated = false; - if (! empty($receiptRows)) { - $archivePath = $tempDir.'/batch_receipts_'.$batchId.'_'.date('Ymd_His').'_'.bin2hex(random_bytes(4)).'.zip'; - $zip = new ZipArchive; + if (!empty($receiptRows)) { + $archivePath = $tempDir . '/batch_receipts_' . $batchId . '_' . date('Ymd_His') . '_' . bin2hex(random_bytes(4)) . '.zip'; + $zip = new ZipArchive(); $added = 0; $usedNames = []; if ($zip->open($archivePath, ZipArchive::CREATE | ZipArchive::OVERWRITE) === true) { foreach ($receiptRows as $row) { - $sourcePath = storage_path('uploads/receipts/'.basename((string) ($row['receipt_filename'] ?? ''))); - if (! is_file($sourcePath)) { + $sourcePath = storage_path('uploads/receipts/' . basename((string) ($row['receipt_filename'] ?? ''))); + if (!is_file($sourcePath)) { continue; } $firstName = $row['purchaser_firstname'] ?? ''; - if (trim($firstName) === '' && ! empty($row['purchaser_lastname'] ?? '')) { + if (trim($firstName) === '' && !empty($row['purchaser_lastname'] ?? '')) { $firstName = $row['purchaser_lastname']; } $firstName = $this->sanitizeAttachmentComponent($firstName); $vendorPart = $this->sanitizeAttachmentComponent($row['retailor'] ?? ''); $datePart = $this->formatAttachmentDate($row['purchase_date'] ?? ''); $extension = pathinfo($sourcePath, PATHINFO_EXTENSION); - $extPart = $extension !== '' ? ('.'.strtolower($extension)) : ''; - $baseName = $firstName.'_'.$vendorPart.'_'.$datePart; + $extPart = $extension !== '' ? ('.' . strtolower($extension)) : ''; + $baseName = $firstName . '_' . $vendorPart . '_' . $datePart; $entryName = $this->uniqueZipEntryName($baseName, $extPart, $usedNames); if ($zip->addFile($sourcePath, $entryName)) { $added++; @@ -115,21 +117,21 @@ class ReimbursementEmailService $checksArchivePath = null; $checksAdded = 0; - if (! empty($checkRows)) { - $archivePath = $tempDir.'/batch_checks_'.$batchId.'_'.date('Ymd_His').'_'.bin2hex(random_bytes(4)).'.zip'; - $zip = new ZipArchive; + if (!empty($checkRows)) { + $archivePath = $tempDir . '/batch_checks_' . $batchId . '_' . date('Ymd_His') . '_' . bin2hex(random_bytes(4)) . '.zip'; + $zip = new ZipArchive(); $usedNames = []; if ($zip->open($archivePath, ZipArchive::CREATE | ZipArchive::OVERWRITE) === true) { foreach ($checkRows as $row) { - $sourcePath = storage_path('uploads/reimbursements/'.basename((string) ($row['filename'] ?? ''))); - if (! is_file($sourcePath)) { + $sourcePath = storage_path('uploads/reimbursements/' . basename((string) ($row['filename'] ?? ''))); + if (!is_file($sourcePath)) { continue; } $adminLabel = $row['admin'] ?? 'Admin'; $firstToken = strtok($adminLabel, ' '); $namePart = $this->sanitizeAttachmentComponent($firstToken ?: $adminLabel); $extension = pathinfo($sourcePath, PATHINFO_EXTENSION); - $extPart = $extension !== '' ? ('.'.strtolower($extension)) : ''; + $extPart = $extension !== '' ? ('.' . strtolower($extension)) : ''; $entryName = $this->uniqueZipEntryName($namePart, $extPart, $usedNames); if ($zip->addFile($sourcePath, $entryName)) { $checksAdded++; @@ -153,14 +155,14 @@ class ReimbursementEmailService $attachmentSummary[] = 'Check Scans'; } - $subject = 'Reimbursement Batch Export: '.$batchLabel; + $subject = 'Reimbursement Batch Export: ' . $batchLabel; $bodyParts = [ - '

Please find attached the export for '.htmlspecialchars($batchLabel, ENT_QUOTES, 'UTF-8').'.

', + '

Please find attached the export for ' . htmlspecialchars($batchLabel, ENT_QUOTES, 'UTF-8') . '.

', ]; if (trim($message) !== '') { - $bodyParts[] = '

Message:
'.nl2br(htmlspecialchars($message, ENT_QUOTES, 'UTF-8')).'

'; + $bodyParts[] = '

Message:
' . nl2br(htmlspecialchars($message, ENT_QUOTES, 'UTF-8')) . '

'; } - $bodyParts[] = '

Attachments: '.htmlspecialchars(implode(', ', $attachmentSummary), ENT_QUOTES, 'UTF-8').'.

'; + $bodyParts[] = '

Attachments: ' . htmlspecialchars(implode(', ', $attachmentSummary), ENT_QUOTES, 'UTF-8') . '.

'; $bodyParts[] = '

Regards,
Al Rahma Sunday School

'; $htmlBody = implode("\n", $bodyParts); @@ -168,10 +170,10 @@ class ReimbursementEmailService ['path' => $csvPath, 'name' => $csvAttachmentName], ]; if ($receiptsArchivePath) { - $attachments[] = ['path' => $receiptsArchivePath, 'name' => $batchFileLabel.'_receipts.zip']; + $attachments[] = ['path' => $receiptsArchivePath, 'name' => $batchFileLabel . '_receipts.zip']; } if ($checksArchivePath) { - $attachments[] = ['path' => $checksArchivePath, 'name' => $batchFileLabel.'_checks.zip']; + $attachments[] = ['path' => $checksArchivePath, 'name' => $batchFileLabel . '_checks.zip']; } $sender = $this->resolveSender('general'); @@ -179,11 +181,11 @@ class ReimbursementEmailService try { Mail::html($htmlBody, function ($message) use ($recipientEmail, $subject, $sender, $attachments) { $message->to($recipientEmail)->subject($subject); - if (! empty($sender['email'])) { + if (!empty($sender['email'])) { $message->from($sender['email'], $sender['name']); } foreach ($attachments as $file) { - if (! empty($file['path']) && is_file($file['path'])) { + if (!empty($file['path']) && is_file($file['path'])) { $message->attach($file['path'], ['as' => $file['name'] ?? basename($file['path'])]); } } @@ -191,8 +193,7 @@ class ReimbursementEmailService return true; } catch (\Throwable $e) { - Log::error('Failed to send batch export email: '.$e->getMessage()); - + Log::error('Failed to send batch export email: ' . $e->getMessage()); return false; } finally { foreach ($cleanupPaths as $path) { @@ -211,7 +212,7 @@ class ReimbursementEmailService ->leftJoin('users as u', 'u.id', '=', 'e.purchased_by') ->where('bi.batch_id', $batchId); - if (! empty($receiptIds)) { + if (!empty($receiptIds)) { $builder->whereIn('e.id', $receiptIds); } @@ -229,7 +230,7 @@ class ReimbursementEmailService 'purchaser_lastname' => $row->purchaser_lastname ?? null, ]; }, $rows), static function ($row) { - return ! empty($row['receipt_filename']); + return !empty($row['receipt_filename']); })); } @@ -246,7 +247,7 @@ class ReimbursementEmailService { $receiptTotalsByRecipient = []; foreach ($receiptRows as $row) { - $recipientName = trim(($row['purchaser_firstname'] ?? '').' '.($row['purchaser_lastname'] ?? '')); + $recipientName = trim(($row['purchaser_firstname'] ?? '') . ' ' . ($row['purchaser_lastname'] ?? '')); if ($recipientName === '') { $recipientName = 'Unknown'; } @@ -270,7 +271,7 @@ class ReimbursementEmailService implode(' | ', $descriptionParts), ]); } - if (! empty($receiptTotalsByRecipient)) { + if (!empty($receiptTotalsByRecipient)) { fputcsv($handle, []); fputcsv($handle, ['Totals by Recipient', '', '', '', '', '', '', '']); foreach ($receiptTotalsByRecipient as $recipient => $total) { @@ -293,46 +294,42 @@ class ReimbursementEmailService { $clean = preg_replace('/[^A-Za-z0-9]+/', '_', trim((string) $value)); $clean = trim($clean, '_'); - return $clean === '' ? 'unknown' : $clean; } private function formatAttachmentDate(?string $value): string { - if (! $value) { + if (!$value) { return 'unknown'; } $timestamp = strtotime($value); if ($timestamp === false) { return 'unknown'; } - return date('Ymd', $timestamp); } private function uniqueZipEntryName(string $base, string $extension, array &$used): string { - $candidate = $base.$extension; + $candidate = $base . $extension; $counter = 1; while (isset($used[$candidate])) { - $candidate = $base.'_'.$counter.$extension; + $candidate = $base . '_' . $counter . $extension; $counter++; } $used[$candidate] = true; - return $candidate; } private function formatCsvDate(?string $value): string { - if (! $value) { + if (!$value) { return ''; } $timestamp = strtotime($value); if ($timestamp === false) { return ''; } - return date('m-d-Y', $timestamp); } @@ -344,7 +341,6 @@ class ReimbursementEmailService if (is_array($senders) && isset($senders[$fromKey])) { $info = $senders[$fromKey]; - return [ 'name' => (string) ($info['name'] ?? 'Sender'), 'email' => (string) ($info['email'] ?? ''), diff --git a/app/Services/Reimbursements/ReimbursementExportService.php b/app/Services/Reimbursements/ReimbursementExportService.php index 7a297cc7..f2c4d8f6 100644 --- a/app/Services/Reimbursements/ReimbursementExportService.php +++ b/app/Services/Reimbursements/ReimbursementExportService.php @@ -8,7 +8,9 @@ use RuntimeException; class ReimbursementExportService { - public function __construct(private ReimbursementRecipientService $recipients) {} + public function __construct(private ReimbursementRecipientService $recipients) + { + } public function buildUnderProcessingCsv(): array { @@ -25,12 +27,12 @@ class ReimbursementExportService foreach ($records as $row) { $personId = (int) ($row->purchased_by ?? 0); - $name = trim(($row->purchaser_firstname ?? '').' '.($row->purchaser_lastname ?? '')); + $name = trim(($row->purchaser_firstname ?? '') . ' ' . ($row->purchaser_lastname ?? '')); if ($name === '') { $name = $row->purchaser_firstname ?? $row->purchaser_lastname ?? 'Unknown'; } - $key = $personId > 0 ? 'id_'.$personId : 'name_'.strtolower($name); - if (! isset($grouped[$key])) { + $key = $personId > 0 ? 'id_' . $personId : 'name_' . strtolower($name); + if (!isset($grouped[$key])) { $grouped[$key] = [ 'name' => $name ?: 'Unknown', 'total' => 0.0, @@ -43,12 +45,12 @@ class ReimbursementExportService $grouped[$key]['total'] += $amount; $vendor = trim((string) ($row->retailor ?? '')); - if ($vendor !== '' && ! in_array($vendor, $grouped[$key]['vendors'], true)) { + if ($vendor !== '' && !in_array($vendor, $grouped[$key]['vendors'], true)) { $grouped[$key]['vendors'][] = $vendor; } $desc = trim((string) ($row->description ?? '')); - if ($desc !== '' && ! in_array($desc, $grouped[$key]['descriptions'], true)) { + if ($desc !== '' && !in_array($desc, $grouped[$key]['descriptions'], true)) { $grouped[$key]['descriptions'][] = $desc; } } @@ -76,16 +78,16 @@ class ReimbursementExportService ->select('reimbursements.*', 'u.firstname as reimb_firstname', 'u.lastname as reimb_lastname') ->leftJoin('users as u', 'u.id', '=', 'reimbursements.reimbursed_to'); - if (! empty($filters['semester'])) { + if (!empty($filters['semester'])) { $builder->where('reimbursements.semester', $filters['semester']); } - if (! empty($filters['status'])) { + if (!empty($filters['status'])) { $builder->where('reimbursements.status', $filters['status']); } - if (! empty($filters['user_id'])) { + if (!empty($filters['user_id'])) { $builder->where('reimbursements.reimbursed_to', $filters['user_id']); } - if (! empty($filters['school_year'])) { + if (!empty($filters['school_year'])) { $builder->where('reimbursements.school_year', $filters['school_year']); } @@ -94,8 +96,8 @@ class ReimbursementExportService $rows[] = ['Amount', 'Reimbursed To', 'Status', 'Method', 'Check #', 'School Year', 'Semester', 'Date']; foreach ($records as $row) { - $hasName = trim(($row->reimb_firstname ?? '').' '.($row->reimb_lastname ?? '')) !== ''; - if (! $hasName) { + $hasName = trim(($row->reimb_firstname ?? '') . ' ' . ($row->reimb_lastname ?? '')) !== ''; + if (!$hasName) { $label = $this->recipients->specialRecipientLabel($row->reimbursed_to ?? null); if ($label !== null) { $row->reimb_firstname = $label; @@ -105,7 +107,7 @@ class ReimbursementExportService $rows[] = [ $row->amount, - trim(($row->reimb_firstname ?? '').' '.($row->reimb_lastname ?? '')), + trim(($row->reimb_firstname ?? '') . ' ' . ($row->reimb_lastname ?? '')), $row->status, $row->reimbursement_method ?? '-', $row->check_number ?? '-', @@ -124,11 +126,11 @@ class ReimbursementExportService public function buildBatchCsv(int $batchId): array { $batch = ReimbursementBatch::query()->find($batchId); - if (! $batch) { + if (!$batch) { throw new RuntimeException('Batch not found.'); } - $batchLabel = trim((string) ($batch->title ?? '')) ?: ('Batch #'.$batchId); + $batchLabel = trim((string) ($batch->title ?? '')) ?: ('Batch #' . $batchId); $receiptRows = $this->fetchBatchReceiptRows($batchId); $rows = []; @@ -149,14 +151,14 @@ class ReimbursementExportService ->leftJoin('users as u', 'u.id', '=', 'e.purchased_by') ->where('bi.batch_id', $batchId); - if (! empty($receiptIds)) { + if (!empty($receiptIds)) { $builder->whereIn('e.id', $receiptIds); } $rows = $builder->get()->toArray(); return array_values(array_filter($rows, static function ($row) { - return ! empty($row->receipt_filename); + return !empty($row->receipt_filename); })); } @@ -173,7 +175,7 @@ class ReimbursementExportService { $receiptTotalsByRecipient = []; foreach ($receiptRows as $row) { - $recipientName = trim(($row->purchaser_firstname ?? '').' '.($row->purchaser_lastname ?? '')); + $recipientName = trim(($row->purchaser_firstname ?? '') . ' ' . ($row->purchaser_lastname ?? '')); if ($recipientName === '') { $recipientName = 'Unknown'; } @@ -198,7 +200,7 @@ class ReimbursementExportService ]; } - if (! empty($receiptTotalsByRecipient)) { + if (!empty($receiptTotalsByRecipient)) { $rows[] = []; $rows[] = ['Totals by Recipient', '', '', '', '', '', '', '']; foreach ($receiptTotalsByRecipient as $recipient => $total) { @@ -219,14 +221,13 @@ class ReimbursementExportService private function formatCsvDate(?string $value): string { - if (! $value) { + if (!$value) { return ''; } $timestamp = strtotime($value); if ($timestamp === false) { return ''; } - return date('m-d-Y', $timestamp); } } diff --git a/app/Services/Reimbursements/ReimbursementFileService.php b/app/Services/Reimbursements/ReimbursementFileService.php index ec42d495..a842e351 100644 --- a/app/Services/Reimbursements/ReimbursementFileService.php +++ b/app/Services/Reimbursements/ReimbursementFileService.php @@ -12,7 +12,8 @@ class ReimbursementFileService { public function __construct( private ApplicationUrlService $urls - ) {} + ) { + } public function receiptUrl(?string $filename): ?string { @@ -31,7 +32,7 @@ class ReimbursementFileService public function storeReceipt(?UploadedFile $file): ?string { - if (! $file || ! $file->isValid()) { + if (!$file || !$file->isValid()) { return null; } @@ -41,10 +42,10 @@ class ReimbursementFileService $dir = $this->ensureUploadDir(); $extension = $file->getClientOriginalExtension(); - $extension = $extension !== '' ? '.'.strtolower($extension) : ''; - $filename = 'reimb_'.date('Ymd_His').'_'.Str::random(8).$extension; + $extension = $extension !== '' ? '.' . strtolower($extension) : ''; + $filename = 'reimb_' . date('Ymd_His') . '_' . Str::random(8) . $extension; - if (! $file->move($dir, $filename)) { + if (!$file->move($dir, $filename)) { throw new RuntimeException('Unable to save uploaded file.'); } @@ -55,10 +56,10 @@ class ReimbursementFileService { $dir = $this->ensureUploadDir(); $extension = $file->getClientOriginalExtension(); - $extension = $extension !== '' ? '.'.strtolower($extension) : ''; - $filename = 'batch_check_'.date('Ymd_His').'_'.Str::random(8).$extension; + $extension = $extension !== '' ? '.' . strtolower($extension) : ''; + $filename = 'batch_check_' . date('Ymd_His') . '_' . Str::random(8) . $extension; - if (! $file->move($dir, $filename)) { + if (!$file->move($dir, $filename)) { throw new RuntimeException('Unable to save uploaded file.'); } @@ -67,8 +68,8 @@ class ReimbursementFileService ->where('admin_id', $adminId) ->first(); - if ($existing && ! empty($existing->filename)) { - $oldPath = $dir.DIRECTORY_SEPARATOR.$existing->filename; + if ($existing && !empty($existing->filename)) { + $oldPath = $dir . DIRECTORY_SEPARATOR . $existing->filename; if (is_file($oldPath)) { @unlink($oldPath); } @@ -101,13 +102,13 @@ class ReimbursementFileService { $dir = storage_path('uploads/reimbursements'); - if (! is_dir($dir)) { - if (! @mkdir($dir, 0755, true) && ! is_dir($dir)) { + if (!is_dir($dir)) { + if (!@mkdir($dir, 0755, true) && !is_dir($dir)) { throw new RuntimeException('Upload directory is not available.'); } } - if (! is_writable($dir)) { + if (!is_writable($dir)) { throw new RuntimeException('Upload directory is not writable.'); } diff --git a/app/Services/Reimbursements/ReimbursementQueryService.php b/app/Services/Reimbursements/ReimbursementQueryService.php index 9d65a734..66b2d40f 100644 --- a/app/Services/Reimbursements/ReimbursementQueryService.php +++ b/app/Services/Reimbursements/ReimbursementQueryService.php @@ -11,7 +11,8 @@ class ReimbursementQueryService public function __construct( private ReimbursementRecipientService $recipients, private ReimbursementFileService $files - ) {} + ) { + } public function underProcessing(): array { @@ -44,7 +45,7 @@ class ReimbursementQueryService } $receiptUrl = $this->files->expenseReceiptUrl($row->expense_receipt ?? null); - $fullName = trim(($row->purchaser_firstname ?? '').' '.($row->purchaser_lastname ?? '')) ?: 'Unknown'; + $fullName = trim(($row->purchaser_firstname ?? '') . ' ' . ($row->purchaser_lastname ?? '')) ?: 'Unknown'; $item = [ 'id' => $expenseId, @@ -95,16 +96,16 @@ class ReimbursementQueryService } $receiptUrl = $this->files->expenseReceiptUrl($row->expense_receipt ?? null); - $purchaser = trim(($row->purchaser_firstname ?? '').' '.($row->purchaser_lastname ?? '')) ?: 'Unknown'; + $purchaser = trim(($row->purchaser_firstname ?? '') . ' ' . ($row->purchaser_lastname ?? '')) ?: 'Unknown'; $yearlyNumber = (int) ($row->batch_year_number ?? 0); $batchLabel = trim((string) ($row->batch_title ?? '')); if ($batchLabel === '') { $fallbackNumber = $yearlyNumber > 0 ? $yearlyNumber : $batchId; - $batchLabel = 'Batch #'.$fallbackNumber; + $batchLabel = 'Batch #' . $fallbackNumber; } $adminId = $row->admin_id ? (int) $row->admin_id : 0; $adminName = $adminId > 0 - ? (trim(($row->admin_firstname ?? '').' '.($row->admin_lastname ?? '')) ?: 'Admin') + ? (trim(($row->admin_firstname ?? '') . ' ' . ($row->admin_lastname ?? '')) ?: 'Admin') : 'Unassigned'; $batchStatus = strtolower(trim((string) ($row->batch_status ?? ''))) ?: 'open'; @@ -126,7 +127,7 @@ class ReimbursementQueryService $itemsMap[$expenseId] = $item; - if (! isset($batchStructures[$batchId])) { + if (!isset($batchStructures[$batchId])) { $batchStructures[$batchId] = [ 'batchId' => $batchId, 'label' => $batchLabel, @@ -142,7 +143,7 @@ class ReimbursementQueryService } $slotKey = $adminId ?: 0; - if (! isset($batchStructures[$batchId]['slots'][$slotKey])) { + if (!isset($batchStructures[$batchId]['slots'][$slotKey])) { $batchStructures[$batchId]['slots'][$slotKey] = [ 'admin_id' => $adminId ?: 0, 'admin_name' => $adminId > 0 ? $adminName : 'Unassigned', @@ -154,7 +155,7 @@ class ReimbursementQueryService } $adminFileMap = []; - if (! empty($batchStructures)) { + if (!empty($batchStructures)) { $batchIds = array_keys($batchStructures); $files = ReimbursementBatchAdminFile::query() ->select('batch_id', 'admin_id', 'filename', 'original_filename') @@ -187,7 +188,6 @@ class ReimbursementQueryService $existingBatches = array_map(static function (array $batch) { $normalizedSlots = array_map(static function (array $slot) { $slot['items'] = array_values(array_map('intval', $slot['items'])); - return $slot; }, array_values($batch['slots'])); @@ -238,13 +238,13 @@ class ReimbursementQueryService ->where('b.status', 'closed') ->whereNull('bi.unassigned_at'); - if (! empty($filters['school_year'])) { + if (!empty($filters['school_year'])) { $batchQuery->where(function ($q) use ($filters) { $q->where('b.school_year', $filters['school_year']) ->orWhere('e.school_year', $filters['school_year']); }); } - if (! empty($filters['user_id'])) { + if (!empty($filters['user_id'])) { $batchQuery->where('e.purchased_by', $filters['user_id']); } @@ -255,10 +255,10 @@ class ReimbursementQueryService continue; } $labelNumber = (int) ($row->yearly_batch_number ?? $bid); - if (! isset($batchSummaries[$bid])) { + if (!isset($batchSummaries[$bid])) { $batchSummaries[$bid] = [ 'batch_id' => $bid, - 'title' => trim((string) ($row->batch_title ?? '')) ?: ('Batch #'.$labelNumber), + 'title' => trim((string) ($row->batch_title ?? '')) ?: ('Batch #' . $labelNumber), 'sequence' => $labelNumber, 'closed_at' => $row->closed_at ?? null, 'amount' => 0.0, @@ -269,7 +269,7 @@ class ReimbursementQueryService $batchSummaries[$bid]['amount'] += (float) $amount; $batchSummaries[$bid]['items'] += 1; - if (! isset($batchDetails[$bid])) { + if (!isset($batchDetails[$bid])) { $batchDetails[$bid] = [ 'title' => $batchSummaries[$bid]['title'], 'sequence' => $labelNumber, @@ -278,7 +278,7 @@ class ReimbursementQueryService 'checks' => [], ]; } - if (! isset($batchAttachments[$bid])) { + if (!isset($batchAttachments[$bid])) { $batchAttachments[$bid] = [ 'receipts' => [], 'checks' => [], @@ -289,7 +289,7 @@ class ReimbursementQueryService 'amount' => (float) $amount, 'description' => $row->description ?? '', 'vendor' => $row->retailor ?? '', - 'purchased_by' => trim(($row->purchaser_firstname ?? '').' '.($row->purchaser_lastname ?? '')), + 'purchased_by' => trim(($row->purchaser_firstname ?? '') . ' ' . ($row->purchaser_lastname ?? '')), 'receipt_url' => $this->files->expenseReceiptUrl($row->expense_receipt ?? null), ]; @@ -309,7 +309,7 @@ class ReimbursementQueryService } } - if (! empty($batchSummaries)) { + if (!empty($batchSummaries)) { $batchIds = array_keys($batchSummaries); $files = ReimbursementBatchAdminFile::query() ->select('id as file_id', 'batch_id', 'admin_id', 'filename', 'original_filename', 'uploaded_at') @@ -319,22 +319,22 @@ class ReimbursementQueryService $adminNames = $this->recipients->adminRoster(); $adminNameMap = []; foreach ($adminNames as $admin) { - $adminNameMap[(int) $admin['id']] = trim(($admin['firstname'] ?? '').' '.($admin['lastname'] ?? '')); + $adminNameMap[(int) $admin['id']] = trim(($admin['firstname'] ?? '') . ' ' . ($admin['lastname'] ?? '')); } foreach ($files as $file) { $bid = (int) ($file->batch_id ?? 0); - if ($bid <= 0 || ! isset($batchDetails[$bid])) { + if ($bid <= 0 || !isset($batchDetails[$bid])) { continue; } $aid = (int) ($file->admin_id ?? 0); $batchDetails[$bid]['checks'][] = [ - 'admin' => $adminNameMap[$aid] ?? ($aid > 0 ? 'Admin #'.$aid : 'Unassigned'), + 'admin' => $adminNameMap[$aid] ?? ($aid > 0 ? 'Admin #' . $aid : 'Unassigned'), 'filename' => $file->filename, 'original' => $file->original_filename ?? $file->filename, 'url' => $this->files->adminFileUrl($file->filename ?? '', 'inline'), ]; - if (! isset($batchAttachments[$bid])) { + if (!isset($batchAttachments[$bid])) { $batchAttachments[$bid] = [ 'receipts' => [], 'checks' => [], @@ -344,19 +344,18 @@ class ReimbursementQueryService 'file_id' => (int) ($file->file_id ?? 0), 'filename' => $file->filename, 'original_filename' => $file->original_filename ?? $file->filename, - 'admin' => $adminNameMap[$aid] ?? ($aid > 0 ? 'Admin #'.$aid : 'Unassigned'), + 'admin' => $adminNameMap[$aid] ?? ($aid > 0 ? 'Admin #' . $aid : 'Unassigned'), 'url' => $this->files->adminFileUrl($file->filename ?? '', 'inline'), 'uploaded_at' => $file->uploaded_at ?? null, ]; } } - if (! empty($batchSummaries)) { + if (!empty($batchSummaries)) { $batchSummaries = array_values($batchSummaries); usort($batchSummaries, static function ($a, $b) { $aSeq = (int) ($a['sequence'] ?? 0); $bSeq = (int) ($b['sequence'] ?? 0); - return $bSeq <=> $aSeq; }); } @@ -372,16 +371,16 @@ class ReimbursementQueryService ->leftJoin('users as u', 'u.id', '=', 'e.purchased_by') ->where('e.category', 'Donation'); - if (! empty($filters['school_year'])) { + if (!empty($filters['school_year'])) { $donationQuery->where('e.school_year', $filters['school_year']); } - if (! empty($filters['semester'])) { + if (!empty($filters['semester'])) { $donationQuery->where('e.semester', $filters['semester']); } - if (! empty($filters['user_id'])) { + if (!empty($filters['user_id'])) { $donationQuery->where('e.purchased_by', $filters['user_id']); } - if (! empty($filters['status'])) { + if (!empty($filters['status'])) { $donationQuery->where(function ($q) use ($filters) { $q->where('e.status', $filters['status']) ->orWhere('e.status', strtolower((string) $filters['status'])) @@ -395,7 +394,7 @@ class ReimbursementQueryService foreach ($donationRows as $row) { $amount = (float) ($row->expense_amount ?? 0); $donationTotal += $amount; - $purchaser = trim(($row->purchaser_firstname ?? '').' '.($row->purchaser_lastname ?? '')); + $purchaser = trim(($row->purchaser_firstname ?? '') . ' ' . ($row->purchaser_lastname ?? '')); if ($purchaser === '') { $purchaser = 'Unknown'; } @@ -429,8 +428,8 @@ class ReimbursementQueryService } $recipientId = $e['reimb_recipient_id'] ?? ($e['reimbursed_to'] ?? null); - $hasName = trim(($e['reimb_firstname'] ?? '').' '.($e['reimb_lastname'] ?? '')) !== ''; - if (! $hasName) { + $hasName = trim(($e['reimb_firstname'] ?? '') . ' ' . ($e['reimb_lastname'] ?? '')) !== ''; + if (!$hasName) { $label = $this->recipients->specialRecipientLabel($recipientId); if ($label !== null) { $e['reimb_firstname'] = $label; diff --git a/app/Services/Reimbursements/ReimbursementRecipientService.php b/app/Services/Reimbursements/ReimbursementRecipientService.php index eeef0bed..d614b514 100644 --- a/app/Services/Reimbursements/ReimbursementRecipientService.php +++ b/app/Services/Reimbursements/ReimbursementRecipientService.php @@ -29,7 +29,7 @@ class ReimbursementRecipientService if ($id <= 0 || in_array($roleName, $excludedRoles, true)) { continue; } - if (! isset($staff[$id])) { + if (!isset($staff[$id])) { $staff[$id] = [ 'id' => $id, 'firstname' => $row->firstname ?? '', @@ -39,9 +39,8 @@ class ReimbursementRecipientService } uasort($staff, static function (array $a, array $b) { - $nameA = trim(($a['firstname'] ?? '').' '.($a['lastname'] ?? '')); - $nameB = trim(($b['firstname'] ?? '').' '.($b['lastname'] ?? '')); - + $nameA = trim(($a['firstname'] ?? '') . ' ' . ($a['lastname'] ?? '')); + $nameB = trim(($b['firstname'] ?? '') . ' ' . ($b['lastname'] ?? '')); return strcasecmp($nameA, $nameB); }); @@ -56,7 +55,7 @@ class ReimbursementRecipientService public function adminRoster(): array { $admins = $this->staffUsers(); - if (! empty($admins)) { + if (!empty($admins)) { return $admins; } @@ -65,12 +64,11 @@ class ReimbursementRecipientService public function specialRecipientLabel($value): ?string { - if ($value === null || ! is_numeric($value)) { + if ($value === null || !is_numeric($value)) { return null; } $id = (int) $value; - return self::SPECIAL_RECIPIENTS[$id] ?? null; } @@ -81,7 +79,7 @@ class ReimbursementRecipientService }, $users); foreach (self::SPECIAL_RECIPIENTS as $id => $label) { - if (! in_array($id, $existingIds, true)) { + if (!in_array($id, $existingIds, true)) { $users[] = [ 'id' => $id, 'firstname' => $label, diff --git a/app/Services/Reports/ReportCards/ReportCardService.php b/app/Services/Reports/ReportCards/ReportCardService.php index 2ecc82db..b2098603 100644 --- a/app/Services/Reports/ReportCards/ReportCardService.php +++ b/app/Services/Reports/ReportCards/ReportCardService.php @@ -2,9 +2,10 @@ namespace App\Services\Reports\ReportCards; +use App\Models\Calendar; use App\Models\AttendanceData; use App\Models\AttendanceRecord; -use App\Models\Calendar; +use App\Models\ClassSection; use App\Models\Configuration; use App\Models\ReportCardAcknowledgement; use App\Models\ScoreComment; @@ -12,12 +13,13 @@ use App\Models\SemesterScore; use App\Models\Student; use App\Models\StudentClass; use App\Models\TeacherClass; +use App\Models\User; +use Illuminate\Database\Query\Builder; use Illuminate\Support\Facades\DB; class ReportCardService { private string $schoolYear; - private string $semester; public function __construct() @@ -54,7 +56,7 @@ class ReportCardService ->get(); foreach ($q2->all() as $r) { $val = (string) ($r->school_year ?? ''); - if ($val !== '' && ! in_array($val, $schoolYears, true)) { + if ($val !== '' && !in_array($val, $schoolYears, true)) { $schoolYears[] = $val; } } @@ -82,7 +84,7 @@ class ReportCardService ->get(); foreach ($rs2->all() as $r) { $val = (string) ($r->semester ?? ''); - if ($val !== '' && ! in_array($val, $semesters, true)) { + if ($val !== '' && !in_array($val, $semesters, true)) { $semesters[] = $val; } } @@ -91,14 +93,14 @@ class ReportCardService $defaults = array_values(array_filter([(string) ($this->semester ?? ''), 'Fall', 'Spring'], static fn ($v) => $v !== '')); foreach ($defaults as $d) { - if (! in_array($d, $semesters, true)) { + if (!in_array($d, $semesters, true)) { $semesters[] = $d; } } $semesters = array_values(array_unique($semesters)); - if ($sem === '' && ! empty($semesters)) { + if ($sem === '' && !empty($semesters)) { $sem = $semesters[0]; - } elseif ($sem !== '' && ! in_array($sem, $semesters, true)) { + } elseif ($sem !== '' && !in_array($sem, $semesters, true)) { array_unshift($semesters, $sem); $semesters = array_values(array_unique($semesters)); } @@ -126,7 +128,7 @@ class ReportCardService ->get() ->map(fn ($r) => (array) $r) ->all(); - if (! empty($stuRows)) { + if (!empty($stuRows)) { $students = $stuRows; } } catch (\Throwable $e) { @@ -252,7 +254,7 @@ class ReportCardService $students = $this->fetchStudentsByClass($sectionId, $year); } $rosterSemester = ''; - $usedFallback = ! empty($students); + $usedFallback = !empty($students); } if (empty($students)) { @@ -299,7 +301,7 @@ class ReportCardService $scoreByStudent = []; foreach ($scoreRows as $row) { $sid = (int) ($row['student_id'] ?? 0); - if ($sid > 0 && ! isset($scoreByStudent[$sid])) { + if ($sid > 0 && !isset($scoreByStudent[$sid])) { $scoreByStudent[$sid] = $row; } } @@ -324,7 +326,7 @@ class ReportCardService ->all(); foreach ($otherRows as $row) { $sid = (int) ($row['student_id'] ?? 0); - if ($sid > 0 && ! isset($firstScoreByStudent[$sid])) { + if ($sid > 0 && !isset($firstScoreByStudent[$sid])) { $firstScoreByStudent[$sid] = $row; } } @@ -387,7 +389,7 @@ class ReportCardService ->all(); foreach ($ackRows as $row) { $sid = (int) ($row['student_id'] ?? 0); - if ($sid > 0 && ! isset($ackMap[$sid])) { + if ($sid > 0 && !isset($ackMap[$sid])) { $ackMap[$sid] = $row; } } @@ -400,7 +402,7 @@ class ReportCardService $warningCount = 0; foreach ($students as $student) { $sid = (int) ($student['id'] ?? 0); - $name = trim(((string) ($student['firstname'] ?? '')).' '.((string) ($student['lastname'] ?? ''))); + $name = trim(((string) ($student['firstname'] ?? '')) . ' ' . ((string) ($student['lastname'] ?? ''))); $missing = []; $warnings = []; @@ -415,7 +417,7 @@ class ReportCardService $semesterScore = null; $hasAllComponents = false; - if (! $score) { + if (!$score) { $missing[] = 'Score record'; } else { $attendanceScore = $score['attendance_score'] ?? null; @@ -428,11 +430,11 @@ class ReportCardService $score['project_avg'] ?? null, $score['test_avg'] ?? null, ]; - $hasAllComponents = ! in_array(false, array_map($isNumeric, $components), true); + $hasAllComponents = !in_array(false, array_map($isNumeric, $components), true); if ($isSecond) { $examScore = $score['final_exam_score'] ?? null; - if (! $isNumeric($examScore) && $isNumeric($score['midterm_exam_score'] ?? null)) { + if (!$isNumeric($examScore) && $isNumeric($score['midterm_exam_score'] ?? null)) { $examScore = $score['midterm_exam_score']; $warnings[] = 'Final exam stored as midterm'; } @@ -442,13 +444,13 @@ class ReportCardService $examScore = $score['final_exam_score'] ?? ($score['midterm_exam_score'] ?? null); } - if (! $isNumeric($examScore)) { + if (!$isNumeric($examScore)) { $missing[] = $isSecond ? 'Final exam score' : ($isFirst ? 'Midterm exam score' : 'Exam score'); } - if (! $isNumeric($attendanceScore)) { + if (!$isNumeric($attendanceScore)) { $missing[] = 'Attendance score'; } - if (! $isNumeric($ptapScore)) { + if (!$isNumeric($ptapScore)) { if ($hasAllComponents) { $warnings[] = 'PTAP computed from components'; } else { @@ -457,7 +459,7 @@ class ReportCardService } $canComputeSemester = $isNumeric($examScore) && $isNumeric($attendanceScore) && ($isNumeric($ptapScore) || $hasAllComponents); - if (! $isNumeric($semesterScore)) { + if (!$isNumeric($semesterScore)) { if ($canComputeSemester) { $warnings[] = 'Semester score computed'; } else { @@ -467,7 +469,7 @@ class ReportCardService if ($isSecond) { $firstScore = $firstScoreByStudent[$sid]['semester_score'] ?? null; - if (! $isNumeric($firstScore)) { + if (!$isNumeric($firstScore)) { $missing[] = 'First semester score'; } } @@ -480,7 +482,7 @@ class ReportCardService ? ($commentSet[$commentKey] ?? '') : ($commentSet['final'] ?? ($commentSet['midterm'] ?? '')); if (trim((string) $examComment) === '') { - $missing[] = $commentLabel.' comment'; + $missing[] = $commentLabel . ' comment'; } if (trim((string) ($commentSet['ptap'] ?? '')) === '') { $missing[] = 'PTAP comment'; @@ -493,7 +495,7 @@ class ReportCardService if ($isComplete) { $completeCount++; } - if (! empty($warnings)) { + if (!empty($warnings)) { $warningCount++; } @@ -562,7 +564,7 @@ class ReportCardService $reportDate = $this->sanitizeReportDate((string) ($params['report_date'] ?? '')); $data = $this->prepareStudentReportData($studentId, $schoolYear, $semester); - if (! $data) { + if (!$data) { return ['ok' => false, 'message' => 'Student not found or missing scores.']; } @@ -572,7 +574,7 @@ class ReportCardService $pdf = new \FPDF('P', 'mm', 'Letter'); $this->formatReportPDF($pdf, $data); - $filename = 'ReportCard_'.($data['student']['lastname'] ?? 'student').'.pdf'; + $filename = 'ReportCard_' . ($data['student']['lastname'] ?? 'student') . '.pdf'; $pdfContent = $pdf->Output('S'); @@ -594,10 +596,10 @@ class ReportCardService ->join('students as s', 's.id', '=', 'semester_scores.student_id') ->where('s.is_active', 1) ->where('semester_scores.class_section_id', $classSectionId); - if (! empty($schoolYear)) { + if (!empty($schoolYear)) { $q->where('semester_scores.school_year', $schoolYear); } - if (! empty($semester)) { + if (!empty($semester)) { $this->applySemesterFilter($q, $semester, 'semester_scores.semester'); } $scores = $q @@ -607,7 +609,7 @@ class ReportCardService ->map(fn ($r) => $r->toArray()) ->all(); - if (! $scores) { + if (!$scores) { return ['ok' => false, 'message' => 'No students found for this class section.']; } @@ -623,7 +625,7 @@ class ReportCardService } } - $filename = 'ClassReport_Section_'.$classSectionId.'.pdf'; + $filename = 'ClassReport_Section_' . $classSectionId . '.pdf'; $pdfContent = $pdf->Output('S'); return [ @@ -665,7 +667,7 @@ class ReportCardService ->orderBy('s.firstname', 'ASC') ->orderBy('s.lastname', 'ASC'); - if (! empty($schoolYear)) { + if (!empty($schoolYear)) { $query->where('student_class.school_year', $schoolYear); } @@ -681,7 +683,6 @@ class ReportCardService if ($s === 'spring' || $s === 'second' || str_contains($s, 'spring') || str_contains($s, '2')) { return 'spring'; } - return ''; } @@ -708,7 +709,7 @@ class ReportCardService private function applySemesterFilter($builder, ?string $semester, string $field = 'semester'): void { $vals = $this->semesterVariants($semester); - if (! empty($vals)) { + if (!empty($vals)) { $builder->whereIn($field, $vals); } } @@ -716,7 +717,7 @@ class ReportCardService private function applySemesterExclusion($builder, ?string $semester, string $field = 'semester'): void { $vals = $this->semesterVariants($semester); - if (! empty($vals)) { + if (!empty($vals)) { $builder->whereNotIn($field, $vals); } } @@ -760,7 +761,6 @@ class ReportCardService if ($weekday === 0) { return $now->format('Y-m-d'); } - return $now->modify('next sunday')->format('Y-m-d'); } @@ -788,14 +788,13 @@ class ReportCardService $sundays[] = $cursor->format('Y-m-d'); $cursor = $cursor->modify('+7 days'); } - return $sundays; } private function prepareStudentReportData(int $studentId, ?string $schoolYear = null, ?string $semester = null) { $student = Student::query()->find($studentId); - if (! $student || (int) ($student->is_active ?? 1) !== 1) { + if (!$student || (int) ($student->is_active ?? 1) !== 1) { return null; } @@ -816,7 +815,7 @@ class ReportCardService $this->applySemesterFilter($qb, $refSemester, 'semester'); } $score = $qb->orderBy('updated_at', 'DESC')->orderBy('id', 'DESC')->first(); - if (! $score) { + if (!$score) { return null; } $score = $score->toArray(); @@ -846,7 +845,7 @@ class ReportCardService } } - if (! $sectionId || $sectionCode <= 0) { + if (!$sectionId || $sectionCode <= 0) { try { $scRow = DB::table('student_class') ->select('class_section_id') @@ -855,7 +854,7 @@ class ReportCardService ->orderByRaw('COALESCE(updated_at, created_at) DESC') ->limit(1) ->first(); - if (! empty($scRow->class_section_id)) { + if (!empty($scRow->class_section_id)) { $sectionCode = (int) $scRow->class_section_id; $row = DB::table('classSection') ->select('id') @@ -888,7 +887,7 @@ class ReportCardService if (empty($taNames)) { $taNames = $names['ta_names']; } - if (empty($teacherNamesList) && ! empty($names['teacher_names'])) { + if (empty($teacherNamesList) && !empty($names['teacher_names'])) { $teacherNamesList = $names['teacher_names']; } } @@ -916,22 +915,22 @@ class ReportCardService if ($commentVal === '') { continue; } - if (! isset($commentMap[$typeRaw])) { + if (!isset($commentMap[$typeRaw])) { $commentMap[$typeRaw] = $commentVal; } } - if (isset($commentMap['attendance']) && ! isset($score['attendance_comment'])) { + if (isset($commentMap['attendance']) && !isset($score['attendance_comment'])) { $score['attendance_comment'] = $commentMap['attendance']; } $att = $score['attendance_score'] ?? null; $attendanceCommentVal = $commentMap['attendance'] ?? $score['attendance_comment'] ?? null; - if ((! is_string($attendanceCommentVal) || trim($attendanceCommentVal) === '') && is_numeric($att) && function_exists('attendance_comment_from_score')) { + if ((!is_string($attendanceCommentVal) || trim($attendanceCommentVal) === '') && is_numeric($att) && function_exists('attendance_comment_from_score')) { $autoAttendance = attendance_comment_from_score((float) $att, (string) ($student->firstname ?? '')); if (is_string($autoAttendance) && $autoAttendance !== '') { $commentMap['attendance'] = $autoAttendance; - if (! isset($score['attendance_comment'])) { + if (!isset($score['attendance_comment'])) { $score['attendance_comment'] = $autoAttendance; } } @@ -943,7 +942,7 @@ class ReportCardService if ($normSemester !== '') { $events = Calendar::getEventsBySchoolYearAndSemester($refYear, $normSemester); $dates = array_values(array_filter(array_map(static fn ($r) => (string) ($r['date'] ?? ''), $events))); - if (! empty($dates)) { + if (!empty($dates)) { $start = min($dates); $end = max($dates); $sundays = $this->listSundays($start, $end); @@ -968,7 +967,7 @@ class ReportCardService static fn ($r) => isset($r['total_attendance']) ? (int) $r['total_attendance'] : null, $attRows ), static fn ($v) => $v !== null)); - if (! empty($vals)) { + if (!empty($vals)) { $totalSemesterDays = max($vals); } } catch (\Throwable $e) { @@ -1025,8 +1024,7 @@ class ReportCardService $withPrefix = static function (array $row): string { $first = trim((string) ($row['firstname'] ?? '')); $last = trim((string) ($row['lastname'] ?? '')); - $name = trim($last.', '.$first, ' ,'); - + $name = trim($last . ', ' . $first, ' ,'); return $name !== '' ? $name : 'N/A'; }; $normalizePos = static function (?string $pos): string { @@ -1034,7 +1032,6 @@ class ReportCardService if ($pos === 'ta' || $pos === 'assistant') { return 'ta'; } - return 'main'; }; @@ -1068,7 +1065,7 @@ class ReportCardService $ids = [$sectionIdOrCode]; $rows = $runner($ids, (string) $schoolYear, (string) $semester); - if (! empty($rows)) { + if (!empty($rows)) { $consume($rows); } @@ -1123,7 +1120,6 @@ class ReportCardService if (is_numeric($val)) { return number_format((float) $val, 1); } - return (string) $val; }; $splitText = static function ($text, int $limit = 55): array { @@ -1133,7 +1129,6 @@ class ReportCardService } $lines = explode("\n", wordwrap($text, $limit)); $lines[] = ''; - return $lines; }; $drawLabeledCell = static function (\FPDF $pdf, string $label, string $value, float $width = 95) { @@ -1148,7 +1143,6 @@ class ReportCardService return $item; } } - return null; }; $wrapLinesToWidth = static function (\FPDF $pdf, string $text, float $width) { @@ -1159,19 +1153,18 @@ class ReportCardService if ($width <= 0) { return [$text]; } - $segments = preg_split('/\\r?\\n+/', $text); + $segments = preg_split("/\\r?\\n+/", $text); $lines = []; foreach ($segments as $seg) { $seg = trim($seg); if ($seg === '') { $lines[] = ' '; - continue; } $words = preg_split('/\\s+/u', $seg); $current = ''; foreach ($words as $word) { - $trial = trim($current === '' ? $word : ($current.' '.$word)); + $trial = trim($current === '' ? $word : ($current . ' ' . $word)); if ($pdf->GetStringWidth($trial) <= $width) { $current = $trial; } else { @@ -1185,7 +1178,6 @@ class ReportCardService $lines[] = $current; } } - return $lines; }; $draw3ColumnRow = static function (\FPDF $pdf, string $label, $score, $feedback) use ($wrapLinesToWidth) { @@ -1243,13 +1235,12 @@ class ReportCardService $seg = trim($seg); if ($seg === '') { $out[] = ' '; - continue; } $words = preg_split('/\\s+/u', $seg); $current = ''; foreach ($words as $word) { - $trial = $current === '' ? $word : ($current.' '.$word); + $trial = $current === '' ? $word : ($current . ' ' . $word); if ($pdf->GetStringWidth($trial) <= ($width - 2)) { $current = $trial; } else { @@ -1263,15 +1254,14 @@ class ReportCardService $out[] = $current; } } - - return ! empty($out) ? $out : [' ']; + return !empty($out) ? $out : [' ']; }; if ($startY !== null) { $pdf->SetY($startY); } foreach ($rows as $row) { - if (! is_array($row) || count($row) < 4) { + if (!is_array($row) || count($row) < 4) { continue; } [$labelLeft, $valueLeft, $labelRight, $valueRight] = $row; @@ -1326,7 +1316,6 @@ class ReportCardService $currentY += $rowHeight; $pdf->SetY($currentY); } - return $currentY; }; @@ -1343,7 +1332,7 @@ class ReportCardService } $teacherNamesList = $data['teacher_names'] ?? []; - if (empty($teacherNamesList) && ! empty($data['teacher_name'])) { + if (empty($teacherNamesList) && !empty($data['teacher_name'])) { $teacherNamesList = [trim((string) $data['teacher_name'])]; } $teacherNamesList = array_values(array_filter( @@ -1376,9 +1365,9 @@ class ReportCardService $schoolYear = (string) ($data['score']['school_year'] ?? ($data['score']['year'] ?? 'N/A')); if (preg_match('/^(\\d{4})-(\\d{4})$/', $schoolYear, $m)) { - $schoolYear = $m[1].'/'.$m[2]; + $schoolYear = $m[1] . '/' . $m[2]; } - $studentName = trim(($data['student']['firstname'] ?? '').' '.($data['student']['lastname'] ?? '')); + $studentName = trim(($data['student']['firstname'] ?? '') . ' ' . ($data['student']['lastname'] ?? '')); $gradeLabel = (string) ($data['grade'] ?? ($data['class_section_name'] ?? 'N/A')); $today = (string) ($data['report_date_display'] ?? date('m-d-Y')); $firstSemScore = $data['first_semester_score'] ?? null; @@ -1407,9 +1396,9 @@ class ReportCardService }; $afterInfoY = $drawInfoRows($pdf, [ - ["Teachers' Names: ", ' '.($teacherNames !== '' ? $teacherNames : 'N/A'), 'School Year: ', ' '.$schoolYear], - ['Student Name: ', ' '.($studentName !== '' ? $studentName : 'N/A'), 'Grade: ', ' '.($gradeLabel !== '' ? $gradeLabel : 'N/A')], - ['Term: ', ' '.$semFriendly, 'Date: ', ' '.$today], + ["Teachers' Names: ", ' ' . ($teacherNames !== '' ? $teacherNames : 'N/A'), 'School Year: ', ' ' . $schoolYear], + ['Student Name: ', ' ' . ($studentName !== '' ? $studentName : 'N/A'), 'Grade: ', ' ' . ($gradeLabel !== '' ? $gradeLabel : 'N/A')], + ['Term: ', ' ' . $semFriendly, 'Date: ', ' ' . $today], ]); $pdf->SetY($afterInfoY); @@ -1486,7 +1475,7 @@ class ReportCardService $pdf->Rect($x, $startY, $statW, $statH); $pdf->SetXY($x + 2, $startY + 3); $pdf->SetFont('Helvetica', 'B', 11); - $pdf->Write(5, $lbl.' '); + $pdf->Write(5, $lbl . ' '); $pdf->SetFont('Helvetica', '', 11); $pdf->Write(5, $val); } @@ -1494,7 +1483,7 @@ class ReportCardService $effectiveSecond = $secondSemScore ?? $data['score']['semester_score'] ?? $data['total_score'] ?? null; $finalScoreVal = $finalAverage; - if (! is_numeric($finalScoreVal)) { + if (!is_numeric($finalScoreVal)) { $finalScoreVal = (is_numeric($firstSemScore) && is_numeric($effectiveSecond)) ? number_format(((float) $firstSemScore + (float) $effectiveSecond) / 2, 1) : $numFmt($effectiveSecond); @@ -1509,8 +1498,8 @@ class ReportCardService $pdf->Rect($x, $y, $w, $h); $pdf->SetXY($x + $pad, $y + 3); $pdf->SetFont('Helvetica', 'B', 11); - $pdf->Write(5, $label.' '); - $labelWidth = $pdf->GetStringWidth($label.' '); + $pdf->Write(5, $label . ' '); + $labelWidth = $pdf->GetStringWidth($label . ' '); $pdf->SetFont('Helvetica', '', 12); $pdf->SetXY($x + 2 + $labelWidth, $y + 3); $pdf->Write(5, $value); @@ -1518,14 +1507,14 @@ class ReportCardService if ($semNum === 2) { $firstLabel = ' 1st Semester Grade:'; - $firstValue = $numFmt($firstSemScore).'/100'; + $firstValue = $numFmt($firstSemScore) . '/100'; $secondLabel = ' 2nd Semester Grade:'; - $secondValue = $numFmt($effectiveSecond).'/100'; + $secondValue = $numFmt($effectiveSecond) . '/100'; $drawGradeCell($pdf, $gradeX, $gradeY, $gradeCellWidth, $gradeCellHeight, $firstLabel, $firstValue, $gradePad); $drawGradeCell($pdf, $gradeX + $gradeCellWidth, $gradeY, $gradeCellWidth, $gradeCellHeight, $secondLabel, $secondValue, $gradePad); } else { $gradeLabel = ' 1st Semester Grade:'; - $gradeValue = $numFmt($effectiveSecond).'/100'; + $gradeValue = $numFmt($effectiveSecond) . '/100'; $drawGradeCell($pdf, $gradeX, $gradeY, $gradeCellWidth, $gradeCellHeight, $gradeLabel, $gradeValue, $gradePad); } $pdf->SetY($gradeY + $gradeCellHeight); @@ -1534,7 +1523,7 @@ class ReportCardService $finalScoreEndY = null; if ($semNum === 2) { $finalLabel = 'Final Score****:'; - $finalValue = (is_string($finalScoreVal) ? $finalScoreVal : $numFmt($finalScoreVal)).'/100'; + $finalValue = (is_string($finalScoreVal) ? $finalScoreVal : $numFmt($finalScoreVal)) . '/100'; $finalCellWidth = 65; $finalCellHeight = $finalScoreRowHeight; $finalX = $pdf->GetX(); @@ -1543,8 +1532,8 @@ class ReportCardService $finalPad = max(0, 2 - (4 * 0.3528)); $pdf->SetXY($finalX + $finalPad, $finalY + 3); $pdf->SetFont('Helvetica', 'B', 11); - $pdf->Write(5, ' '.$finalLabel.' '); - $finalLabelWidth = $pdf->GetStringWidth(' '.$finalLabel.' '); + $pdf->Write(5, ' ' . $finalLabel . ' '); + $finalLabelWidth = $pdf->GetStringWidth(' ' . $finalLabel . ' '); $pdf->SetFont('Helvetica', '', 12); $pdf->SetXY($finalX + 2 + $finalLabelWidth, $finalY + 3); $pdf->Write(5, $finalValue); @@ -1556,11 +1545,11 @@ class ReportCardService $examLegendLabel = ($semNum === 1) ? 'Midterm Exam' : 'Final Exam'; $legendLines = [ "(*) {$examLegendLabel} (Weight: 60% of semester score)", - '(**) PTAP - Participation, Tests, Assignments, Projects (Weight: 20%)', + "(**) PTAP - Participation, Tests, Assignments, Projects (Weight: 20%)", "(***) Attendance = ({$totalDays} - Total Absences + One Sick Day) / {$totalDays} ) x 100 (Weight: 20%)", ]; if ($semNum === 2) { - $legendLines[] = '(****) Final Score = (1st Semester Score + 2nd Semester Score) / 2'; + $legendLines[] = "(****) Final Score = (1st Semester Score + 2nd Semester Score) / 2"; } $legendLines[] = ['text' => 'If you have any questions, please contact the school via email at alrahma.isgl@gmail.com', 'bold' => true]; $lineHeight = 4; @@ -1619,7 +1608,6 @@ class ReportCardService return $path; } } - return null; } } diff --git a/app/Services/Reports/SlipPrinterConfigService.php b/app/Services/Reports/SlipPrinterConfigService.php index 8700234a..3bed6d80 100644 --- a/app/Services/Reports/SlipPrinterConfigService.php +++ b/app/Services/Reports/SlipPrinterConfigService.php @@ -20,7 +20,7 @@ class SlipPrinterConfigService if ($userId && $userId > 0) { $user = User::query()->select('firstname', 'lastname')->find($userId); if ($user) { - $full = trim(($user->firstname ?? '').' '.($user->lastname ?? '')); + $full = trim(($user->firstname ?? '') . ' ' . ($user->lastname ?? '')); if ($full !== '') { return $full; } @@ -40,7 +40,6 @@ class SlipPrinterConfigService return $val; } } - return $default; }; diff --git a/app/Services/Reports/SlipPrinterFormatterService.php b/app/Services/Reports/SlipPrinterFormatterService.php index b2ed7d5f..fee957c4 100644 --- a/app/Services/Reports/SlipPrinterFormatterService.php +++ b/app/Services/Reports/SlipPrinterFormatterService.php @@ -15,16 +15,13 @@ class SlipPrinterFormatterService } if (preg_match('/^\d{1,2}\/\d{1,2}\/\d{4}$/', $value)) { $dt = \DateTime::createFromFormat('!m/d/Y', $value); - return $dt ? $dt->format('Y-m-d') : null; } if (preg_match('/^\d{1,2}-\d{1,2}-\d{4}$/', $value)) { $dt = \DateTime::createFromFormat('!m-d-Y', $value); - return $dt ? $dt->format('Y-m-d') : null; } $ts = strtotime($value); - return $ts ? date('Y-m-d', $ts) : null; } @@ -35,7 +32,6 @@ class SlipPrinterFormatterService return null; } $ts = strtotime($value); - return $ts ? date('H:i:s', $ts) : null; } @@ -46,7 +42,6 @@ class SlipPrinterFormatterService return ''; } $ts = strtotime($value); - return $ts ? date('m/d/Y', $ts) : ''; } @@ -58,9 +53,8 @@ class SlipPrinterFormatterService } $ts = strtotime($value); if ($ts === false) { - $ts = strtotime('today '.$value); + $ts = strtotime('today ' . $value); } - return $ts ? date('h:i A', $ts) : ''; } @@ -70,19 +64,17 @@ class SlipPrinterFormatterService if (strlen($line) > $width) { return substr($line, 0, $width); } - - return $line.str_repeat(' ', $width - strlen($line)); + return $line . str_repeat(' ', $width - strlen($line)); } public function buildSlipLines(array $data, int $width): array { $lines = []; - $lines[] = $this->fitLine('Student Name: '.(string) ($data['student_name'] ?? ''), $width); - $lines[] = $this->fitLine('Date: '.(string) ($data['date'] ?? '').' Time In: '.(string) ($data['time_in'] ?? ''), $width); - $lines[] = $this->fitLine('Grade: '.(string) ($data['grade'] ?? ''), $width); - $lines[] = $this->fitLine('Reason: '.(string) ($data['reason'] ?? ''), $width); - $lines[] = $this->fitLine('Admin: '.(string) ($data['admin_name'] ?? ''), $width); - + $lines[] = $this->fitLine('Student Name: ' . (string) ($data['student_name'] ?? ''), $width); + $lines[] = $this->fitLine('Date: ' . (string) ($data['date'] ?? '') . ' Time In: ' . (string) ($data['time_in'] ?? ''), $width); + $lines[] = $this->fitLine('Grade: ' . (string) ($data['grade'] ?? ''), $width); + $lines[] = $this->fitLine('Reason: ' . (string) ($data['reason'] ?? ''), $width); + $lines[] = $this->fitLine('Admin: ' . (string) ($data['admin_name'] ?? ''), $width); return $lines; } } diff --git a/app/Services/Reports/SlipPrinterPdfService.php b/app/Services/Reports/SlipPrinterPdfService.php index 3c5a7380..e2bdd688 100644 --- a/app/Services/Reports/SlipPrinterPdfService.php +++ b/app/Services/Reports/SlipPrinterPdfService.php @@ -8,7 +8,9 @@ use Dompdf\Options; class SlipPrinterPdfService { - public function __construct(private SlipPrinterConfigService $configService) {} + public function __construct(private SlipPrinterConfigService $configService) + { + } public function render(array $data, array $lines, array $options = []): array { @@ -61,7 +63,7 @@ class SlipPrinterPdfService 'h_mm' => $hMm, ]); - $optionsObj = new Options; + $optionsObj = new Options(); $optionsObj->set('isRemoteEnabled', true); $optionsObj->set('defaultFont', 'Courier'); @@ -74,7 +76,7 @@ class SlipPrinterPdfService $dompdf->setPaper([0, 0, $wPt, $hPt]); $dompdf->render(); - $filename = 'LateSlip_'.preg_replace('/[^A-Za-z0-9]+/', '_', (string) ($data['student_name'] ?? 'slip')).'_'.date('Ymd_His').'.pdf'; + $filename = 'LateSlip_' . preg_replace('/[^A-Za-z0-9]+/', '_', (string) ($data['student_name'] ?? 'slip')) . '_' . date('Ymd_His') . '.pdf'; return [ 'content' => $dompdf->output(), diff --git a/app/Services/Reports/SlipPrinterService.php b/app/Services/Reports/SlipPrinterService.php index 518ffc03..553e72b0 100644 --- a/app/Services/Reports/SlipPrinterService.php +++ b/app/Services/Reports/SlipPrinterService.php @@ -10,7 +10,8 @@ class SlipPrinterService private SlipPrinterConfigService $configService, private SlipPrinterFormatterService $formatter, private SlipPrinterPdfService $pdfService - ) {} + ) { + } public function print(array $input, ?int $userId): array { @@ -44,7 +45,7 @@ class SlipPrinterService return [ 'ok' => true, - 'text' => $header.$body.$footer, + 'text' => $header . $body . $footer, 'width' => $width, ]; } @@ -96,7 +97,7 @@ class SlipPrinterService public function reprint(int $id, ?int $userId): array { $row = LateSlipLog::query()->find($id); - if (! $row) { + if (!$row) { return ['ok' => false, 'message' => 'Slip not found.']; } @@ -172,7 +173,6 @@ class SlipPrinterService { $cfg = $this->configService->printerConfig(); $width = (int) ($cfg['chars_per_line'] ?? 48); - return $width > 0 ? $width : 48; } diff --git a/app/Services/Reports/Stickers/StickerPresetService.php b/app/Services/Reports/Stickers/StickerPresetService.php index 7e0093b3..c1a84c65 100644 --- a/app/Services/Reports/Stickers/StickerPresetService.php +++ b/app/Services/Reports/Stickers/StickerPresetService.php @@ -14,10 +14,9 @@ class StickerPresetService ]; return array_map(static function (array $row): array { - $row['title'] = $row['title'] ?? ($row['label'] ?? ($row['value'].' mm')); + $row['title'] = $row['title'] ?? ($row['label'] ?? ($row['value'] . ' mm')); $row['text'] = $row['text'] ?? $row['title']; $row['per_page'] = $row['per_page'] ?? ($row['ppg'] ?? null); - return $row; }, $rows); } diff --git a/app/Services/Reports/Stickers/StickerPrintService.php b/app/Services/Reports/Stickers/StickerPrintService.php index 74278420..f4b0ebf9 100644 --- a/app/Services/Reports/Stickers/StickerPrintService.php +++ b/app/Services/Reports/Stickers/StickerPrintService.php @@ -6,7 +6,9 @@ use Illuminate\Support\Facades\Log; class StickerPrintService { - public function __construct(private StickerQueryService $queryService) {} + public function __construct(private StickerQueryService $queryService) + { + } public function generate(array $payload): array { @@ -21,7 +23,7 @@ class StickerPrintService if ($studentId > 0) { $student = $this->queryService->findStudent($studentId); - if (! $student) { + if (!$student) { return ['ok' => false, 'message' => 'Student not found.']; } $students = [$student]; @@ -100,7 +102,7 @@ class StickerPrintService $minPt = 8; $hPad = 2; - $fullName = trim(($student['firstname'] ?? '').' '.($student['lastname'] ?? '')) ?: 'Student'; + $fullName = trim(($student['firstname'] ?? '') . ' ' . ($student['lastname'] ?? '')) ?: 'Student'; $bestPt = $maxPt; $textW = 0.0; @@ -156,7 +158,6 @@ class StickerPrintService for ($k = 0; $k < $qty; $k++) { $printList[] = $students[0]; } - return $printList; } @@ -220,19 +221,19 @@ class StickerPrintService || (abs($stickerWidth - 100.0) < 0.6 && abs($stickerHeight - 24.5) < 0.6); if ($isXSmallPreset) { - if (! $marginLeftProvided) { + if (!$marginLeftProvided) { $marginLeft = 4.0; - if (! $marginRightProvided) { + if (!$marginRightProvided) { $marginRight = 4.0; } } - if (! $marginRightProvided && $marginRight < 4.0) { + if (!$marginRightProvided && $marginRight < 4.0) { $marginRight = 4.0; } - if (! $gapXProvided) { + if (!$gapXProvided) { $gapX = 0.0; } - if (! $gapYProvided) { + if (!$gapYProvided) { $gapY = 0.0; } } diff --git a/app/Services/Reports/Stickers/StickerQueryService.php b/app/Services/Reports/Stickers/StickerQueryService.php index 68a37376..a363bacc 100644 --- a/app/Services/Reports/Stickers/StickerQueryService.php +++ b/app/Services/Reports/Stickers/StickerQueryService.php @@ -6,7 +6,9 @@ use Illuminate\Support\Facades\DB; class StickerQueryService { - public function __construct(private StickerContextService $context) {} + public function __construct(private StickerContextService $context) + { + } public function resolveSchoolYear(?string $schoolYear): string { @@ -106,8 +108,7 @@ class StickerQueryService if ($sectionName === '') { return false; } - - return ! preg_match('/^(youth|kg)(?:\b|-)/i', $sectionName); + return !preg_match('/^(youth|kg)(?:\b|-)/i', $sectionName); })); } } diff --git a/app/Services/Roles/PermissionCrudService.php b/app/Services/Roles/PermissionCrudService.php index 5fa38fe1..6c57bdb4 100644 --- a/app/Services/Roles/PermissionCrudService.php +++ b/app/Services/Roles/PermissionCrudService.php @@ -17,7 +17,7 @@ class PermissionCrudService return Permission::query()->create($data); }); } catch (\Throwable $e) { - Log::error('Permission create failed: '.$e->getMessage()); + Log::error('Permission create failed: ' . $e->getMessage()); throw $e; } } @@ -30,11 +30,10 @@ class PermissionCrudService return DB::transaction(function () use ($permission, $data) { $permission->fill($data); $permission->save(); - return $permission; }); } catch (\Throwable $e) { - Log::error('Permission update failed: '.$e->getMessage()); + Log::error('Permission update failed: ' . $e->getMessage()); throw $e; } } @@ -46,7 +45,7 @@ class PermissionCrudService $permission->delete(); }); } catch (\Throwable $e) { - Log::error('Permission delete failed: '.$e->getMessage()); + Log::error('Permission delete failed: ' . $e->getMessage()); throw $e; } } diff --git a/app/Services/Roles/RoleAssignmentService.php b/app/Services/Roles/RoleAssignmentService.php index 6268e57f..8c55dd8e 100644 --- a/app/Services/Roles/RoleAssignmentService.php +++ b/app/Services/Roles/RoleAssignmentService.php @@ -3,6 +3,7 @@ namespace App\Services\Roles; use App\Models\Role; +use App\Models\Staff; use App\Models\User; use App\Models\UserRole; use Illuminate\Support\Facades\DB; @@ -10,7 +11,9 @@ use Illuminate\Support\Facades\Log; class RoleAssignmentService { - public function __construct(private RoleStaffService $staffService) {} + public function __construct(private RoleStaffService $staffService) + { + } /** @param int[] $roleIds */ public function assignRoles(int $userId, array $roleIds, ?int $adminId = null): array @@ -18,7 +21,7 @@ class RoleAssignmentService $roleIds = array_values(array_unique(array_filter(array_map('intval', $roleIds), fn ($id) => $id > 0))); $user = User::query()->find($userId); - if (! $user) { + if (!$user) { return ['ok' => false, 'message' => 'User not found.']; } @@ -35,7 +38,7 @@ class RoleAssignmentService } }); } catch (\Throwable $e) { - Log::error('Role assignment failed: '.$e->getMessage()); + Log::error('Role assignment failed: ' . $e->getMessage()); throw $e; } diff --git a/app/Services/Roles/RoleCrudService.php b/app/Services/Roles/RoleCrudService.php index 4a629871..a11b0570 100644 --- a/app/Services/Roles/RoleCrudService.php +++ b/app/Services/Roles/RoleCrudService.php @@ -17,7 +17,7 @@ class RoleCrudService return Role::query()->create($data); }); } catch (\Throwable $e) { - Log::error('Role create failed: '.$e->getMessage()); + Log::error('Role create failed: ' . $e->getMessage()); throw $e; } } @@ -30,11 +30,10 @@ class RoleCrudService return DB::transaction(function () use ($role, $data) { $role->fill($data); $role->save(); - return $role; }); } catch (\Throwable $e) { - Log::error('Role update failed: '.$e->getMessage()); + Log::error('Role update failed: ' . $e->getMessage()); throw $e; } } @@ -46,7 +45,7 @@ class RoleCrudService $role->delete(); }); } catch (\Throwable $e) { - Log::error('Role delete failed: '.$e->getMessage()); + Log::error('Role delete failed: ' . $e->getMessage()); throw $e; } } @@ -58,7 +57,7 @@ class RoleCrudService $route = trim((string) ($payload['dashboard_route'] ?? '')); $desc = trim((string) ($payload['description'] ?? '')); $priority = (int) ($payload['priority'] ?? 0); - $isActive = ! empty($payload['is_active']) ? 1 : 0; + $isActive = !empty($payload['is_active']) ? 1 : 0; $name = strtolower($rawName); $dashboardRoute = strtolower($route); @@ -93,11 +92,11 @@ class RoleCrudService $query->where('id', '!=', $ignoreId); } - if (! $query->exists()) { + if (!$query->exists()) { return $slug; } - $slug = $baseSlug.'_'.$i; + $slug = $baseSlug . '_' . $i; $i++; if ($i > 100000) { diff --git a/app/Services/Roles/RoleDashboardService.php b/app/Services/Roles/RoleDashboardService.php index 74406ff6..f5a6f119 100644 --- a/app/Services/Roles/RoleDashboardService.php +++ b/app/Services/Roles/RoleDashboardService.php @@ -10,7 +10,7 @@ class RoleDashboardService public function bestDashboardRouteFor(array $roleKeys): string { $rows = Role::findByNamesOrSlugs($roleKeys); - if (! empty($rows)) { + if (!empty($rows)) { return $rows[0]->dashboard_route ?? '/landing_page/guest_dashboard'; } diff --git a/app/Services/Roles/RolePermissionService.php b/app/Services/Roles/RolePermissionService.php index 4d01060d..c299adc3 100644 --- a/app/Services/Roles/RolePermissionService.php +++ b/app/Services/Roles/RolePermissionService.php @@ -58,11 +58,11 @@ class RolePermissionService } $data = [ - 'can_create' => ! empty($crudValues['create']) ? 1 : 0, - 'can_read' => ! empty($crudValues['read']) ? 1 : 0, - 'can_update' => ! empty($crudValues['update']) ? 1 : 0, - 'can_delete' => ! empty($crudValues['delete']) ? 1 : 0, - 'can_manage' => ! empty($crudValues['manage']) ? 1 : 0, + 'can_create' => !empty($crudValues['create']) ? 1 : 0, + 'can_read' => !empty($crudValues['read']) ? 1 : 0, + 'can_update' => !empty($crudValues['update']) ? 1 : 0, + 'can_delete' => !empty($crudValues['delete']) ? 1 : 0, + 'can_manage' => !empty($crudValues['manage']) ? 1 : 0, ]; if ($existing->has($permissionId)) { @@ -79,7 +79,7 @@ class RolePermissionService } foreach ($existing as $permissionId => $row) { - if (! array_key_exists((string) $permissionId, $permissions) && ! array_key_exists((int) $permissionId, $permissions)) { + if (!array_key_exists((string) $permissionId, $permissions) && !array_key_exists((int) $permissionId, $permissions)) { RolePermission::query() ->where('role_id', $roleId) ->where('permission_id', $permissionId) @@ -88,7 +88,7 @@ class RolePermissionService } }); } catch (\Throwable $e) { - Log::error('Role permission save failed: '.$e->getMessage()); + Log::error('Role permission save failed: ' . $e->getMessage()); throw $e; } } diff --git a/app/Services/Roles/RoleQueryService.php b/app/Services/Roles/RoleQueryService.php index b4a2c60f..aba59176 100644 --- a/app/Services/Roles/RoleQueryService.php +++ b/app/Services/Roles/RoleQueryService.php @@ -40,19 +40,19 @@ class RoleQueryService 'roles.updated_at', ]); - if (! empty($filters['search'])) { + if (!empty($filters['search'])) { $search = trim((string) $filters['search']); $query->where(function (Builder $q) use ($search) { - $q->where('roles.name', 'like', '%'.$search.'%') - ->orWhere('roles.slug', 'like', '%'.$search.'%') - ->orWhere('roles.description', 'like', '%'.$search.'%'); + $q->where('roles.name', 'like', '%' . $search . '%') + ->orWhere('roles.slug', 'like', '%' . $search . '%') + ->orWhere('roles.description', 'like', '%' . $search . '%'); }); } $sortBy = (string) ($filters['sort_by'] ?? 'name'); $sortDir = strtolower((string) ($filters['sort_dir'] ?? 'asc')) === 'desc' ? 'desc' : 'asc'; $allowed = ['name', 'slug', 'priority', 'is_active', 'created_at']; - if (! in_array($sortBy, $allowed, true)) { + if (!in_array($sortBy, $allowed, true)) { $sortBy = 'name'; } @@ -80,26 +80,26 @@ class RoleQueryService ) ->groupBy('users.id', 'users.firstname', 'users.lastname', 'users.email', 'users.account_id'); - if (! empty($filters['search'])) { + if (!empty($filters['search'])) { $search = trim((string) $filters['search']); $query->where(function ($q) use ($search) { - $q->where('users.firstname', 'like', '%'.$search.'%') - ->orWhere('users.lastname', 'like', '%'.$search.'%') - ->orWhere('users.email', 'like', '%'.$search.'%') - ->orWhere('users.account_id', 'like', '%'.$search.'%'); + $q->where('users.firstname', 'like', '%' . $search . '%') + ->orWhere('users.lastname', 'like', '%' . $search . '%') + ->orWhere('users.email', 'like', '%' . $search . '%') + ->orWhere('users.account_id', 'like', '%' . $search . '%'); }); } $sortBy = (string) ($filters['sort_by'] ?? 'lastname'); $sortDir = strtolower((string) ($filters['sort_dir'] ?? 'asc')) === 'desc' ? 'desc' : 'asc'; $allowed = ['firstname', 'lastname', 'email', 'account_id']; - if (! in_array($sortBy, $allowed, true)) { + if (!in_array($sortBy, $allowed, true)) { $sortBy = 'lastname'; } $perPage = (int) ($filters['per_page'] ?? 20); $perPage = $perPage > 0 ? $perPage : 20; - return $query->orderBy('users.'.$sortBy, $sortDir)->paginate($perPage); + return $query->orderBy('users.' . $sortBy, $sortDir)->paginate($perPage); } } diff --git a/app/Services/Roles/RoleStaffService.php b/app/Services/Roles/RoleStaffService.php index abf1d0d1..17473e0b 100644 --- a/app/Services/Roles/RoleStaffService.php +++ b/app/Services/Roles/RoleStaffService.php @@ -16,9 +16,9 @@ class RoleStaffService static fn ($role) => trim((string) $role), $newRoleNames ), - static fn ($role) => $role !== '' && ! in_array(strtolower($role), $excludedRoles, true) + static fn ($role) => $role !== '' && !in_array(strtolower($role), $excludedRoles, true) ))); - $activeRole = ! empty($staffRoles) ? strtolower((string) $staffRoles[0]) : 'inactive'; + $activeRole = !empty($staffRoles) ? strtolower((string) $staffRoles[0]) : 'inactive'; $email = $this->generateUniqueStaffEmail($user->firstname ?? '', $user->lastname ?? '', (int) $user->id); @@ -34,7 +34,7 @@ class RoleStaffService ]; $saved = Staff::upsertStaff($payload); - if (! $saved) { + if (!$saved) { Log::warning('Failed to upsert staff record.', ['user_id' => $user->id]); } } @@ -50,7 +50,7 @@ class RoleStaffService $out = ''; foreach ($roles as $role) { - $next = $out === '' ? $role : $out.', '.$role; + $next = $out === '' ? $role : $out . ', ' . $role; if (strlen($next) > $maxLength) { break; } @@ -73,18 +73,18 @@ class RoleStaffService $email = ''; for ($i = $min; $i <= $max; $i++) { - $base = substr($firstname, 0, $i).$lastname; - $email = $base.$domain; + $base = substr($firstname, 0, $i) . $lastname; + $email = $base . $domain; $exists = Staff::query() ->where('email', $email) ->where('user_id', '!=', $userId) ->exists(); - if (! $exists) { + if (!$exists) { return $email; } } - return $base.$userId.$domain; + return $base . $userId . $domain; } } diff --git a/app/Services/Roles/RoleSwitchService.php b/app/Services/Roles/RoleSwitchService.php index 52fdbb32..da2c05bf 100644 --- a/app/Services/Roles/RoleSwitchService.php +++ b/app/Services/Roles/RoleSwitchService.php @@ -8,12 +8,13 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; class RoleSwitchService { - public function __construct(private RoleDashboardService $dashboardService) {} + public function __construct(private RoleDashboardService $dashboardService) + { + } public function getUserRoleNames(int $userId): array { $roles = UserRole::getRolesByUserId($userId); - return array_values(array_filter(array_map(fn ($r) => (string) ($r['role_name'] ?? ''), $roles))); } diff --git a/app/Services/School/AccountEventService.php b/app/Services/School/AccountEventService.php index 0cc05b98..36f4f24e 100644 --- a/app/Services/School/AccountEventService.php +++ b/app/Services/School/AccountEventService.php @@ -20,7 +20,8 @@ class AccountEventService public function __construct( private EmailService $emailService, private UserNotificationDispatchService $notifier - ) {} + ) { + } public function newAccountAdded(array $userData): array { @@ -28,7 +29,7 @@ class AccountEventService $message = $this->renderEmail('emails/welcome_user', ['user' => $userData], $subject); $sent = false; - if (! empty($userData['email'])) { + if (!empty($userData['email'])) { $sent = $this->emailService->send( (string) $userData['email'], $subject, @@ -40,7 +41,7 @@ class AccountEventService $supportMessage = $this->renderEmail('emails/support_new_account', ['user' => $userData], $subject); $this->emailService->send( 'support@alrahmaisgl.org', - 'New Account Created: '.trim((string) ($userData['firstname'] ?? '').' '.(string) ($userData['lastname'] ?? '')), + 'New Account Created: ' . trim((string) ($userData['firstname'] ?? '') . ' ' . (string) ($userData['lastname'] ?? '')), $supportMessage, 'notifications' ); @@ -52,14 +53,13 @@ class AccountEventService public function studentRegistered(array $data): array { - if (empty($data['parents']) || ! is_array($data['parents'])) { + if (empty($data['parents']) || !is_array($data['parents'])) { Log::warning('StudentRegistered: missing parent data'); - return ['ok' => false, 'message' => 'Missing parent data.']; } - $studentFullName = trim((string) ($data['firstname'] ?? '').' '.(string) ($data['lastname'] ?? '')); - if (! empty($data['parents']['user_id'])) { + $studentFullName = trim((string) ($data['firstname'] ?? '') . ' ' . (string) ($data['lastname'] ?? '')); + if (!empty($data['parents']['user_id'])) { $title = 'Your child has been registered'; $msg = "{$studentFullName} has been successfully registered to Al Rahma Sunday School."; $this->notifier->notifyUser((int) $data['parents']['user_id'], $title, $msg, ['in_app', 'email'], 'registration', 'parent'); @@ -100,21 +100,18 @@ class AccountEventService public function userRegistered(array $user): array { $this->notifier->notifyUser((int) ($user['id'] ?? 0), 'Welcome!', 'Thanks for registering with our school system.', ['in_app', 'email'], 'notifications', 'parent'); - return ['ok' => true]; } public function userProfileUpdated(array $user): array { $this->notifier->notifyUser((int) ($user['id'] ?? 0), 'Profile Updated', 'Your profile information was successfully updated.', ['in_app'], 'notifications', 'parent'); - return ['ok' => true]; } public function userDeactivated(array $user): array { $this->notifier->notifyUser((int) ($user['id'] ?? 0), 'Account Deactivated', 'Your account has been deactivated by the administrator.', ['in_app', 'email'], 'notifications', 'parent'); - return ['ok' => true]; } @@ -123,7 +120,6 @@ class AccountEventService foreach ((array) ($data['students'] ?? []) as $student) { $this->notifier->notifyUser((int) ($student['id'] ?? 0), 'New Score Available', 'A new score has been posted.', ['in_app', 'email'], 'registration', 'student'); } - return ['ok' => true]; } @@ -140,24 +136,21 @@ class AccountEventService public function studentMarkedAbsent(array $record): array { - $msg = (string) ($record['student_name'] ?? 'Student').' was absent on '.(string) ($record['date'] ?? ''); + $msg = (string) ($record['student_name'] ?? 'Student') . ' was absent on ' . (string) ($record['date'] ?? ''); $this->notifier->notifyUser((int) ($record['parent_id'] ?? 0), 'Absence Notification', $msg, ['in_app', 'email'], 'registration', 'parent'); - return ['ok' => true]; } public function studentMarkedLate(array $record): array { - $msg = (string) ($record['student_name'] ?? 'Student').' was late on '.(string) ($record['date'] ?? ''); + $msg = (string) ($record['student_name'] ?? 'Student') . ' was late on ' . (string) ($record['date'] ?? ''); $this->notifier->notifyUser((int) ($record['parent_id'] ?? 0), 'Late Notification', $msg, ['in_app', 'email'], 'registration', 'parent'); - return ['ok' => true]; } public function paymentMissed(array $user): array { $this->notifier->notifyUser((int) ($user['id'] ?? 0), 'Payment Missed', 'You have missed a scheduled payment. Please settle your balance.', ['in_app', 'email'], 'payment', 'parent'); - return ['ok' => true]; } @@ -166,15 +159,13 @@ class AccountEventService foreach ((array) ($data['students'] ?? []) as $student) { $this->notifier->notifyUser((int) ($student['id'] ?? 0), 'Schedule Update', 'Your class schedule has been updated.', ['in_app'], 'notifications', 'student'); } - return ['ok' => true]; } public function newMessageReceived(array $data): array { - $msg = 'You have a new message from '.(string) ($data['sender_name'] ?? ''); + $msg = 'You have a new message from ' . (string) ($data['sender_name'] ?? ''); $this->notifier->notifyUser((int) ($data['recipient_id'] ?? 0), 'New Message Received', $msg, ['in_app'], 'notifications', 'parent'); - return ['ok' => true]; } @@ -189,7 +180,6 @@ class AccountEventService foreach ($users as $user) { $this->notifier->notifyUser((int) ($user['id'] ?? 0), (string) ($data['title'] ?? ''), (string) ($data['message'] ?? ''), ['in_app'], 'notifications', $targetGroup); } - return ['ok' => true]; } @@ -215,10 +205,10 @@ class AccountEventService } DB::transaction(function () use ($userData, $userId): void { - $familyCode = 'FAM-'.$userId; + $familyCode = 'FAM-' . $userId; $family = Family::query()->where('family_code', $familyCode)->first(); - if (! $family) { - $householdName = trim('Family of '.((string) ($userData['firstname'] ?? '').' '.(string) ($userData['lastname'] ?? ''))); + if (!$family) { + $householdName = trim('Family of ' . ((string) ($userData['firstname'] ?? '') . ' ' . (string) ($userData['lastname'] ?? ''))); $family = Family::query()->create([ 'family_code' => $familyCode, 'household_name' => $householdName, @@ -262,12 +252,12 @@ class AccountEventService { try { $currentYear = (string) Configuration::getConfigValueByKey('school_year'); - if (! preg_match('/^(\d{4})-(\d{4})$/', $currentYear, $m)) { + if (!preg_match('/^(\d{4})-(\d{4})$/', $currentYear, $m)) { return; } $y1 = (int) $m[1]; $y2 = (int) $m[2]; - $nextYear = ($y1 + 1).'-'.($y2 + 1); + $nextYear = ($y1 + 1) . '-' . ($y2 + 1); $rows = DB::table('students as s') ->select('s.id as student_id', 'sc.class_section_id', 'fall.semester_score as fall_score', 'spring.semester_score as spring_score') @@ -319,7 +309,7 @@ class AccountEventService ]); } } catch (\Throwable $e) { - Log::error('Promotion queue build failed: '.$e->getMessage()); + Log::error('Promotion queue build failed: ' . $e->getMessage()); } } @@ -327,6 +317,6 @@ class AccountEventService { Log::debug('Skipping Blade email template', ['view' => $viewName]); - return '

'.e($fallbackTitle).'

'; + return '

' . e($fallbackTitle) . '

'; } } diff --git a/app/Services/School/EnrollmentEventService.php b/app/Services/School/EnrollmentEventService.php index 8ef92418..b61719cb 100644 --- a/app/Services/School/EnrollmentEventService.php +++ b/app/Services/School/EnrollmentEventService.php @@ -13,7 +13,8 @@ class EnrollmentEventService private EmailService $emailService, private UserNotificationDispatchService $notifier, private ApplicationUrlService $urls, - ) {} + ) { + } public function admissionUnderReview(array $parentData, array $students): array { @@ -178,7 +179,7 @@ class EnrollmentEventService 'parentName' => $parentName, 'studentName' => $studentName, 'withdrawalDate' => $parentData['withdrawal_date'] ?? ($parentData['date'] ?? ''), - 'withdrawals' => ! empty($withdrawals) ? $withdrawals : null, + 'withdrawals' => !empty($withdrawals) ? $withdrawals : null, 'portalLink' => $parentData['portalLink'] ?? $this->urls->webLoginUrl(), 'title' => 'Withdrawal Confirmed', ]; @@ -277,16 +278,16 @@ class EnrollmentEventService private function parentName(array $parentData): string { - return trim((string) ($parentData['firstname'] ?? '').' '.(string) ($parentData['lastname'] ?? '')); + return trim((string) ($parentData['firstname'] ?? '') . ' ' . (string) ($parentData['lastname'] ?? '')); } private function studentNameData(array $studentdata, array $parentData) { $names = []; foreach ($studentdata as $s) { - $nm = ! empty($s['name']) + $nm = !empty($s['name']) ? trim((string) $s['name']) - : trim((string) ($s['firstname'] ?? '').' '.(string) ($s['lastname'] ?? '')); + : trim((string) ($s['firstname'] ?? '') . ' ' . (string) ($s['lastname'] ?? '')); if ($nm !== '') { $names[] = $nm; } @@ -298,7 +299,6 @@ class EnrollmentEventService if (count($names) === 1) { return $names[0]; } - return $names; } @@ -306,9 +306,9 @@ class EnrollmentEventService { $names = []; foreach ($studentdata as $s) { - $nm = ! empty($s['name']) + $nm = !empty($s['name']) ? trim((string) $s['name']) - : trim((string) ($s['firstname'] ?? '').' '.(string) ($s['lastname'] ?? '')); + : trim((string) ($s['firstname'] ?? '') . ' ' . (string) ($s['lastname'] ?? '')); if ($nm !== '') { $names[] = $nm; } @@ -321,20 +321,19 @@ class EnrollmentEventService return $names[0]; } if (count($names) === 2) { - return $names[0].' and '.$names[1]; + return $names[0] . ' and ' . $names[1]; } $last = array_pop($names); - - return implode(', ', $names).', and '.$last; + return implode(', ', $names) . ', and ' . $last; } private function studentDetails(array $studentdata): array { $details = []; foreach ($studentdata as $s) { - $nm = ! empty($s['name']) + $nm = !empty($s['name']) ? trim((string) $s['name']) - : trim((string) ($s['firstname'] ?? '').' '.(string) ($s['lastname'] ?? '')); + : trim((string) ($s['firstname'] ?? '') . ' ' . (string) ($s['lastname'] ?? '')); if ($nm === '') { continue; } @@ -346,7 +345,6 @@ class EnrollmentEventService 'startDate' => $s['start_date'] ?? '', ]; } - return $details; } @@ -354,9 +352,9 @@ class EnrollmentEventService { $withdrawals = []; foreach ($studentdata as $s) { - $nm = ! empty($s['name']) + $nm = !empty($s['name']) ? trim((string) $s['name']) - : trim((string) ($s['firstname'] ?? '').' '.(string) ($s['lastname'] ?? '')); + : trim((string) ($s['firstname'] ?? '') . ' ' . (string) ($s['lastname'] ?? '')); if ($nm === '') { continue; } @@ -368,7 +366,6 @@ class EnrollmentEventService ]; } } - return $withdrawals; } @@ -376,9 +373,9 @@ class EnrollmentEventService { $rows = []; foreach ($studentdata as $s) { - $nm = ! empty($s['name']) + $nm = !empty($s['name']) ? trim((string) $s['name']) - : trim((string) ($s['firstname'] ?? '').' '.(string) ($s['lastname'] ?? '')); + : trim((string) ($s['firstname'] ?? '') . ' ' . (string) ($s['lastname'] ?? '')); if ($nm === '') { continue; } @@ -387,7 +384,6 @@ class EnrollmentEventService 'reason' => $s['reason'] ?? null, ]; } - return $rows; } @@ -403,15 +399,13 @@ class EnrollmentEventService { if ($to === '') { Log::warning('Enrollment event: missing recipient email', ['subject' => $subject]); - return ['ok' => false, 'message' => 'Missing recipient email.']; } $ok = $this->emailService->send($to, $subject, $html, $fromKey); - if (! $ok) { + if (!$ok) { Log::error('Enrollment event email failed', ['to' => $to, 'subject' => $subject]); } - return ['ok' => $ok]; } @@ -419,6 +413,6 @@ class EnrollmentEventService { Log::debug('Skipping Blade email template', ['view' => $viewName]); - return '

'.e($fallbackTitle).'

'; + return '

' . e($fallbackTitle) . '

'; } } diff --git a/app/Services/School/PaymentEventService.php b/app/Services/School/PaymentEventService.php index eb23383c..23e84fd4 100644 --- a/app/Services/School/PaymentEventService.php +++ b/app/Services/School/PaymentEventService.php @@ -13,21 +13,21 @@ class PaymentEventService private EmailService $emailService, private UserNotificationDispatchService $notifier, private ApplicationUrlService $urls, - ) {} + ) { + } public function paymentReceived(array $data, array $studentdata = []): array { - $parentName = trim((string) ($data['firstname'] ?? '').' '.(string) ($data['lastname'] ?? '')); + $parentName = trim((string) ($data['firstname'] ?? '') . ' ' . (string) ($data['lastname'] ?? '')); - $fmtMoney = static fn ($v) => '$'.number_format((float) $v, 2); + $fmtMoney = static fn ($v) => '$' . number_format((float) $v, 2); $fmtDateTz = static function (?string $s): string { - if (! $s) { + if (!$s) { return ''; } $dt = new \DateTime($s, new \DateTimeZone('UTC')); $tzName = (string) (config('School')->attendance['timezone'] ?? user_timezone()); $dt->setTimezone(new \DateTimeZone($tzName ?: 'UTC')); - return $dt->format('m-d-Y h:i A'); }; @@ -94,33 +94,30 @@ class PaymentEventService public function extraCharge(array $data, array $studentdata = []): array { - $fmtMoney = static fn ($v) => '$'.number_format((float) $v, 2); + $fmtMoney = static fn ($v) => '$' . number_format((float) $v, 2); $fmtFromUtc = static function (?string $s, string $fmt = 'm-d-Y h:i A'): string { - if (! $s) { + if (!$s) { return ''; } try { $dt = new \DateTime($s, new \DateTimeZone('UTC')); $tzName = (string) (config('School')->attendance['timezone'] ?? user_timezone()); $dt->setTimezone(new \DateTimeZone($tzName ?: 'UTC')); - return $dt->format($fmt); } catch (\Throwable $e) { - Log::error('extraCharge date error: '.$e->getMessage()); - + Log::error('extraCharge date error: ' . $e->getMessage()); return ''; } }; $fmtDueLocal = static function (?string $s): string { - if (! $s) { + if (!$s) { return ''; } try { if (strlen($s) <= 10) { $tzName = (string) (config('School')->attendance['timezone'] ?? user_timezone()); $dt = \DateTime::createFromFormat('Y-m-d', $s, new \DateTimeZone($tzName ?: 'UTC')); - return $dt ? $dt->format('m-d-Y') : ''; } @@ -128,22 +125,19 @@ class PaymentEventService $dt = new \DateTime($s); $tzName = (string) (config('School')->attendance['timezone'] ?? user_timezone()); $dt->setTimezone(new \DateTimeZone($tzName ?: 'UTC')); - return $dt->format('m-d-Y h:i A'); } $tzName = (string) (config('School')->attendance['timezone'] ?? user_timezone()); $dt = new \DateTime($s, new \DateTimeZone($tzName ?: 'UTC')); - return $dt->format('m-d-Y h:i A'); } catch (\Throwable $e) { - Log::error('extraCharge due date error: '.$e->getMessage()); - + Log::error('extraCharge due date error: ' . $e->getMessage()); return ''; } }; - $parentName = trim((string) ($data['firstname'] ?? '').' '.(string) ($data['lastname'] ?? '')); + $parentName = trim((string) ($data['firstname'] ?? '') . ' ' . (string) ($data['lastname'] ?? '')); $amountSigned = (float) ($data['amount_signed'] ?? 0); $amountAbs = (float) ($data['amount_abs'] ?? abs($amountSigned)); @@ -195,7 +189,6 @@ class PaymentEventService ); $body = $this->renderEmail('emails/extra_charge_notice', $emailData, 'Account Charge Update'); - return $this->sendEmail((string) ($data['email'] ?? ''), $subject, $body, 'billing'); } @@ -211,15 +204,13 @@ class PaymentEventService { if ($to === '') { Log::warning('Payment event missing recipient email', ['subject' => $subject]); - return ['ok' => false, 'message' => 'Missing recipient email.']; } $ok = $this->emailService->send($to, $subject, $html, $fromKey); - if (! $ok) { + if (!$ok) { Log::error('Payment event email failed', ['to' => $to, 'subject' => $subject]); } - return ['ok' => $ok]; } @@ -227,6 +218,6 @@ class PaymentEventService { Log::debug('Skipping Blade email template', ['view' => $viewName]); - return '

'.e($fallbackTitle).'

'; + return '

' . e($fallbackTitle) . '

'; } } diff --git a/app/Services/SchoolIdService.php b/app/Services/SchoolIdService.php index 32af8e4b..390f1233 100644 --- a/app/Services/SchoolIdService.php +++ b/app/Services/SchoolIdService.php @@ -10,7 +10,7 @@ class SchoolIdService public function assignSchoolIdToUser(int $userId): ?string { $user = User::query()->find($userId); - if (! $user) { + if (!$user) { return null; } @@ -36,7 +36,7 @@ class SchoolIdService ->where('school_id', 'like', "STU{$year}%") ->first(); - if (! empty($row?->max_id)) { + if (!empty($row?->max_id)) { $numericPart = substr((string) $row->max_id, 5); $maxId = (int) $numericPart; } @@ -57,7 +57,7 @@ class SchoolIdService ->where('school_id', 'like', "{$year}%") ->first(); - if (! empty($row?->max_id)) { + if (!empty($row?->max_id)) { $numericPart = substr((string) $row->max_id, 2); $maxId = max($maxId, (int) $numericPart); } diff --git a/app/Services/SchoolIds/SchoolIdAssignmentService.php b/app/Services/SchoolIds/SchoolIdAssignmentService.php index 951bd5e5..c0faaba3 100644 --- a/app/Services/SchoolIds/SchoolIdAssignmentService.php +++ b/app/Services/SchoolIds/SchoolIdAssignmentService.php @@ -6,7 +6,9 @@ use App\Services\SchoolIdService; class SchoolIdAssignmentService { - public function __construct(private SchoolIdService $schoolIdService) {} + public function __construct(private SchoolIdService $schoolIdService) + { + } public function assignToUser(int $userId): ?string { diff --git a/app/Services/SchoolIds/SchoolIdGenerationService.php b/app/Services/SchoolIds/SchoolIdGenerationService.php index 3f1be635..79a02e11 100644 --- a/app/Services/SchoolIds/SchoolIdGenerationService.php +++ b/app/Services/SchoolIds/SchoolIdGenerationService.php @@ -6,7 +6,9 @@ use App\Services\SchoolIdService; class SchoolIdGenerationService { - public function __construct(private SchoolIdService $schoolIdService) {} + public function __construct(private SchoolIdService $schoolIdService) + { + } public function generateStudentId(): string { diff --git a/app/Services/SchoolYears/SchoolYearClosureService.php b/app/Services/SchoolYears/SchoolYearClosureService.php index d494acbf..6c10dc74 100644 --- a/app/Services/SchoolYears/SchoolYearClosureService.php +++ b/app/Services/SchoolYears/SchoolYearClosureService.php @@ -8,6 +8,7 @@ use App\Models\ParentAccount; use App\Models\ParentBalanceTransfer; use App\Models\SchoolYear; use App\Services\Promotions\LevelProgressionService; +use Illuminate\Support\Collection; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Schema; use Illuminate\Validation\ValidationException; @@ -18,7 +19,8 @@ class SchoolYearClosureService public function __construct( private SchoolYearResolver $resolver, private LevelProgressionService $progression - ) {} + ) { + } public function list(): array { @@ -233,7 +235,7 @@ class SchoolYearClosureService { $year = $this->findOrFail($id); $validation = $this->validateClose($id, $payload); - if (! $validation['can_close']) { + if (!$validation['can_close']) { throw ValidationException::withMessages(['school_year' => $validation['errors']]); } @@ -247,7 +249,7 @@ class SchoolYearClosureService $newYearPayload = $payload['new_school_year']; $promotionPreview = $this->buildPromotionPreview($year->name, $newYearPayload['name']); - $balances = ! empty($payload['transfer_unpaid_balances']) + $balances = !empty($payload['transfer_unpaid_balances']) ? $this->buildParentBalanceRows($year->name, $newYearPayload['name']) : ['rows' => [], 'summary' => ['net_balance_to_transfer' => 0]]; @@ -289,7 +291,7 @@ class SchoolYearClosureService Configuration::setConfigValueByKey('school_year', $newYear->name); $promotionCounts = $this->applyPromotions($promotionPreview['rows'], $newYear->name, $newYear->start_date, $actorId); - $balanceCounts = ! empty($payload['transfer_unpaid_balances']) + $balanceCounts = !empty($payload['transfer_unpaid_balances']) ? $this->applyBalanceTransfers($balances['rows'], $year->name, $newYear->name, $actorId) : [ 'transferred_parents' => 0, @@ -394,9 +396,8 @@ class SchoolYearClosureService $errors[] = 'New school year name is required.'; } - if (! $startDate || ! $endDate) { + if (!$startDate || !$endDate) { $errors[] = 'New school year start_date and end_date are required.'; - return $errors; } @@ -456,7 +457,7 @@ class SchoolYearClosureService ]; foreach ($tables as $mapping) { - if (! Schema::hasTable($mapping['table'])) { + if (!Schema::hasTable($mapping['table'])) { continue; } if (DB::table($mapping['table'])->where($mapping['column'], $schoolYear)->exists()) { @@ -489,7 +490,7 @@ class SchoolYearClosureService $assignmentByStudent = []; foreach ($assignments as $assignment) { $studentId = (int) $assignment->student_id; - if (! isset($assignmentByStudent[$studentId])) { + if (!isset($assignmentByStudent[$studentId])) { $assignmentByStudent[$studentId] = $assignment; } } @@ -505,7 +506,7 @@ class SchoolYearClosureService $enrollmentByStudent = []; foreach ($enrollments as $enrollment) { $studentId = (int) $enrollment->student_id; - if (! isset($enrollmentByStudent[$studentId])) { + if (!isset($enrollmentByStudent[$studentId])) { $enrollmentByStudent[$studentId] = $enrollment; } } @@ -520,7 +521,7 @@ class SchoolYearClosureService $decisionByStudent = []; foreach ($decisions as $decision) { $studentId = (int) $decision->student_id; - if (! isset($decisionByStudent[$studentId])) { + if (!isset($decisionByStudent[$studentId])) { $decisionByStudent[$studentId] = $decision; } } @@ -538,7 +539,7 @@ class SchoolYearClosureService foreach ($studentIds as $studentId) { $student = $students->get($studentId); - if (! $student) { + if (!$student) { continue; } @@ -598,7 +599,7 @@ class SchoolYearClosureService $row = [ 'student_id' => $studentId, - 'student_name' => trim((string) ($student->firstname ?? '').' '.(string) ($student->lastname ?? '')), + 'student_name' => trim((string) ($student->firstname ?? '') . ' ' . (string) ($student->lastname ?? '')), 'parent_id' => (int) ($student->parent_id ?? 0) ?: null, 'current_grade' => $progression['current_level_name'] ?? ($currentClass->class_name ?? null), 'current_class' => $section->class_section_name ?? null, @@ -644,7 +645,7 @@ class SchoolYearClosureService ? (float) $invoice->balance : (float) ($invoice->total_amount ?? 0) - (float) ($invoice->paid_amount ?? 0); - if (! isset($byParent[$parentId])) { + if (!isset($byParent[$parentId])) { $existing = ParentBalanceTransfer::query() ->where('parent_id', $parentId) ->where('from_school_year', $fromSchoolYear) @@ -736,7 +737,7 @@ class SchoolYearClosureService ->orderBy('class_section_id') ->first(); - if (! $section) { + if (!$section) { return [null, null]; } @@ -756,13 +757,13 @@ class SchoolYearClosureService foreach ($rows as $row) { $action = $row['promotion_action']; - if (! isset($counts[$action])) { + if (!isset($counts[$action])) { continue; } $counts[$action]++; - if (! in_array($action, ['promote', 'repeat'], true)) { + if (!in_array($action, ['promote', 'repeat'], true)) { $this->logAudit( $actorId, 'school_year_promotion_decision_applied', @@ -772,7 +773,6 @@ class SchoolYearClosureService null, ['action' => $action, 'new_school_year' => $newSchoolYear] ); - continue; } @@ -781,7 +781,7 @@ class SchoolYearClosureService ->where('school_year', $newSchoolYear) ->first(); - if (! $existingEnrollment) { + if (!$existingEnrollment) { DB::table('enrollments')->insert([ 'student_id' => $row['student_id'], 'class_section_id' => $row['target_class_section_id'], @@ -805,7 +805,7 @@ class SchoolYearClosureService ->where('class_section_id', $row['target_class_section_id']) ->exists(); - if (! $existingAssignment) { + if (!$existingAssignment) { DB::table('student_class')->insert([ 'student_id' => $row['student_id'], 'class_section_id' => $row['target_class_section_id'], @@ -950,7 +950,7 @@ class SchoolYearClosureService private function countPendingPayments(string $schoolYear): int { - if (! Schema::hasTable('payments')) { + if (!Schema::hasTable('payments')) { return 0; } @@ -978,7 +978,7 @@ class SchoolYearClosureService private function countDuplicateParentAccounts(): int { - if (! Schema::hasTable('parent_accounts')) { + if (!Schema::hasTable('parent_accounts')) { return 0; } @@ -992,11 +992,11 @@ class SchoolYearClosureService private function resolveParentName(int $parentId): ?string { $row = DB::table('users')->where('id', $parentId)->first(); - if (! $row) { + if (!$row) { return null; } - return trim((string) ($row->firstname ?? '').' '.(string) ($row->lastname ?? '')); + return trim((string) ($row->firstname ?? '') . ' ' . (string) ($row->lastname ?? '')); } private function resolveStudentNames(int $parentId, string $schoolYear): array @@ -1015,7 +1015,7 @@ class SchoolYearClosureService return DB::table('students') ->whereIn('id', $studentIds) ->get() - ->map(fn ($student) => trim((string) ($student->firstname ?? '').' '.(string) ($student->lastname ?? ''))) + ->map(fn ($student) => trim((string) ($student->firstname ?? '') . ' ' . (string) ($student->lastname ?? ''))) ->filter() ->values() ->all(); @@ -1023,7 +1023,7 @@ class SchoolYearClosureService private function presentSchoolYear(?SchoolYear $year): ?array { - if (! $year) { + if (!$year) { return null; } @@ -1048,7 +1048,7 @@ class SchoolYearClosureService ?array $newValue, ?array $metadata = null ): void { - if (! Schema::hasTable('audit_logs')) { + if (!Schema::hasTable('audit_logs')) { return; } diff --git a/app/Services/SchoolYears/SchoolYearContextService.php b/app/Services/SchoolYears/SchoolYearContextService.php index 3094b0d8..bbaf6f4b 100644 --- a/app/Services/SchoolYears/SchoolYearContextService.php +++ b/app/Services/SchoolYears/SchoolYearContextService.php @@ -9,7 +9,9 @@ use Illuminate\Support\Facades\Schema; class SchoolYearContextService { - public function __construct(private SchoolYearResolver $resolver) {} + public function __construct(private SchoolYearResolver $resolver) + { + } public function currentSchoolYear(?string $requested = null): string { @@ -93,7 +95,7 @@ class SchoolYearContextService } foreach ($this->legacySchoolYearNames() as $name) { - if (! isset($seen[$name])) { + if (!isset($seen[$name])) { $years[] = [ 'id' => null, 'name' => $name, @@ -109,7 +111,7 @@ class SchoolYearContextService } $include = trim((string) $include); - if ($include !== '' && ! isset($seen[$include])) { + if ($include !== '' && !isset($seen[$include])) { array_unshift($years, [ 'id' => null, 'name' => $include, @@ -129,7 +131,7 @@ class SchoolYearContextService { $names = ['Fall', 'Spring']; $current = trim((string) $include); - if ($current !== '' && ! in_array($current, $names, true)) { + if ($current !== '' && !in_array($current, $names, true)) { array_unshift($names, $current); } @@ -151,7 +153,7 @@ class SchoolYearContextService $names = []; foreach ($tables as $table) { - if (! Schema::hasTable($table) || ! Schema::hasColumn($table, 'school_year')) { + if (!Schema::hasTable($table) || !Schema::hasColumn($table, 'school_year')) { continue; } diff --git a/app/Services/SchoolYears/SchoolYearResolver.php b/app/Services/SchoolYears/SchoolYearResolver.php index 510d1f96..ffc07421 100644 --- a/app/Services/SchoolYears/SchoolYearResolver.php +++ b/app/Services/SchoolYears/SchoolYearResolver.php @@ -16,7 +16,7 @@ class SchoolYearResolver $existing = SchoolYear::query()->where('name', $current)->first(); if ($existing) { - if ($existing->status !== SchoolYear::STATUS_ACTIVE || ! $existing->is_current) { + if ($existing->status !== SchoolYear::STATUS_ACTIVE || !$existing->is_current) { SchoolYear::query()->where('id', '!=', $existing->id)->where('is_current', true)->update([ 'is_current' => false, 'updated_at' => now(), diff --git a/app/Services/SchoolYears/SchoolYearWriteGuard.php b/app/Services/SchoolYears/SchoolYearWriteGuard.php index cd86fafa..c44632d7 100644 --- a/app/Services/SchoolYears/SchoolYearWriteGuard.php +++ b/app/Services/SchoolYears/SchoolYearWriteGuard.php @@ -10,7 +10,7 @@ class SchoolYearWriteGuard { public function assertEditable(array $schoolYears): void { - if (! Schema::hasTable('school_years')) { + if (!Schema::hasTable('school_years')) { return; } diff --git a/app/Services/Scores/AttendanceCalculator.php b/app/Services/Scores/AttendanceCalculator.php index f5d45b20..f97b3120 100644 --- a/app/Services/Scores/AttendanceCalculator.php +++ b/app/Services/Scores/AttendanceCalculator.php @@ -14,7 +14,8 @@ class AttendanceCalculator implements ScoreCalculatorInterface private AttendanceRecord $attendanceModel, private Configuration $configModel, private CalendarEvent $calendarModel - ) {} + ) { + } public function calculate(int $studentId, string $semester, string $schoolYear, ?int $classSectionId = null): array { @@ -29,7 +30,7 @@ class AttendanceCalculator implements ScoreCalculatorInterface $events = CalendarEvent::getEventsBySchoolYearAndSemester($schoolYear, $normSemester); $noSchoolSundays = 0; foreach ($events as $event) { - if (! empty($event['no_school'])) { + if (!empty($event['no_school'])) { $date = new \DateTime($event['date']); if ($date->format('N') == 7) { if ($date->format('Y-m-d') >= $semRange[0] && $date->format('Y-m-d') <= $semRange[1]) { @@ -74,21 +75,16 @@ class AttendanceCalculator implements ScoreCalculatorInterface protected function normalizeSemester(?string $input): string { $s = strtolower(trim((string) $input)); - if ($s === 'fall' || str_contains($s, '1')) { - return 'fall'; - } - if ($s === 'spring' || str_contains($s, '2')) { - return 'spring'; - } - + if ($s === 'fall' || str_contains($s, '1')) return 'fall'; + if ($s === 'spring' || str_contains($s, '2')) return 'spring'; return ''; } protected function getSemesterRange(string $schoolYear, string $semester): ?array { $norm = $this->normalizeSemester($semester); - if ($norm === '' || ! preg_match('/^(\d{4})-(\d{4})$/', trim($schoolYear), $m)) { - if ($norm === '' || ! preg_match('/^(\d{4})/', trim($schoolYear), $m)) { + if ($norm === '' || !preg_match('/^(\d{4})-(\d{4})$/', trim($schoolYear), $m)) { + if ($norm === '' || !preg_match('/^(\d{4})/', trim($schoolYear), $m)) { return null; } $y1 = (int) $m[1]; @@ -106,14 +102,12 @@ class AttendanceCalculator implements ScoreCalculatorInterface if ($norm === 'fall') { $start = ($fallStartCfg !== '') ? sprintf('%04d-%s', $y1, date('m-d', strtotime($fallStartCfg))) : "{$y1}-09-01"; $end = ($fallEndCfg !== '') ? sprintf('%04d-%s', $y2, date('m-d', strtotime($fallEndCfg))) : "{$y2}-01-15"; - return [$start, $end]; } if ($norm === 'spring') { $start = ($springStartCfg !== '') ? sprintf('%04d-%s', $y2, date('m-d', strtotime($springStartCfg))) : "{$y2}-01-16"; $end = ($springEndCfg !== '') ? sprintf('%04d-%s', $y2, date('m-d', strtotime($springEndCfg))) : "{$y2}-06-30"; - return [$start, $end]; } @@ -133,7 +127,6 @@ class AttendanceCalculator implements ScoreCalculatorInterface $sundays++; } } - return $sundays; } @@ -146,12 +139,11 @@ class AttendanceCalculator implements ScoreCalculatorInterface $configKey = $norm === 'fall' ? 'total_semester1_days' : 'total_semester2_days'; $value = Configuration::getConfig($configKey); - if (! is_numeric($value)) { + if (!is_numeric($value)) { return null; } $length = (int) $value; - return $length > 0 ? $length : null; } } diff --git a/app/Services/Scores/ExamScoreService.php b/app/Services/Scores/ExamScoreService.php index f4b0a123..66746cda 100644 --- a/app/Services/Scores/ExamScoreService.php +++ b/app/Services/Scores/ExamScoreService.php @@ -2,17 +2,19 @@ namespace App\Services\Scores; -use App\Models\ClassSection; use App\Models\GradingLock; use App\Models\MissingScoreOverride; use App\Models\Student; use App\Models\TeacherClass; +use App\Models\ClassSection; use App\Services\Grading\Validation\ScoreValueValidator; use Illuminate\Support\Facades\DB; class ExamScoreService { - public function __construct(private ScoreTermService $term) {} + public function __construct(private ScoreTermService $term) + { + } public function resolveClassSectionIdForTeacher(int $teacherId, ?int $requestedClassSectionId, ?string $schoolYearInput): ?int { @@ -70,7 +72,7 @@ class ExamScoreService ->where('sc.school_year', '=', $schoolYear); }) ->leftJoinSub($latestSub, 'latest', 'latest.student_id', '=', 's.id') - ->leftJoin($table.' as ex', 'ex.id', '=', 'latest.max_id') + ->leftJoin($table . ' as ex', 'ex.id', '=', 'latest.max_id') ->orderBy('s.lastname') ->orderBy('s.firstname') ->get() @@ -123,17 +125,17 @@ class ExamScoreService $count = 0; foreach ($scores as $studentId => $data) { - if (! is_numeric($studentId)) { + if (!is_numeric($studentId)) { continue; } $student = Student::query()->find((int) $studentId); - if (! $student) { + if (!$student) { continue; } $rawScore = is_array($data) ? ($data['score'] ?? null) : $data; - $validator = new ScoreValueValidator; + $validator = new ScoreValueValidator(); $normalizedScore = $validator->normalizeNullable($rawScore, ScoreValueValidator::DEFAULT_MAX_POINTS, 'Exam score'); - $missingAllowed = ! empty($missingOk[$studentId]); + $missingAllowed = !empty($missingOk[$studentId]); $existing = $builder ->where('student_id', $studentId) @@ -176,7 +178,7 @@ class ExamScoreService $schoolYearVariants = [$schoolYear]; } - $rows = DB::table($table.' as ex') + $rows = DB::table($table . ' as ex') ->select([ 'ex.*', 's.firstname as student_firstname', diff --git a/app/Services/Scores/HomeworkCalculator.php b/app/Services/Scores/HomeworkCalculator.php index 18eabce4..96a63d1d 100644 --- a/app/Services/Scores/HomeworkCalculator.php +++ b/app/Services/Scores/HomeworkCalculator.php @@ -7,12 +7,13 @@ use App\Models\Homework; class HomeworkCalculator implements ScoreCalculatorInterface { - public function __construct(private Homework $homeworkModel) {} + public function __construct(private Homework $homeworkModel) + { + } public function calculate(int $studentId, string $semester, string $schoolYear, ?int $classSectionId = null): array { $average = Homework::getAverageHomeworkScore($studentId, $semester, $schoolYear, $classSectionId); - return ['homework_avg' => $average !== null ? (float) $average : null]; } } diff --git a/app/Services/Scores/HomeworkScoreService.php b/app/Services/Scores/HomeworkScoreService.php index 2c95a63e..13475f69 100644 --- a/app/Services/Scores/HomeworkScoreService.php +++ b/app/Services/Scores/HomeworkScoreService.php @@ -2,18 +2,20 @@ namespace App\Services\Scores; -use App\Models\ClassSection; use App\Models\GradingLock; use App\Models\Homework; use App\Models\MissingScoreOverride; use App\Models\Student; use App\Models\StudentClass; use App\Models\TeacherClass; +use App\Models\ClassSection; use App\Services\Grading\Validation\ScoreValueValidator; class HomeworkScoreService { - public function __construct(private ScoreTermService $term) {} + public function __construct(private ScoreTermService $term) + { + } /** * When the client omits class_section_id (legacy teacher portal), use the teacher's assignment @@ -172,7 +174,6 @@ class HomeworkScoreService return true; } } - return false; } @@ -180,11 +181,10 @@ class HomeworkScoreService { foreach ($schoolYearVariants as $year) { $map = MissingScoreOverride::getOverridesMap($classSectionId, $semester, $year, 'homework'); - if (! empty($map)) { + if (!empty($map)) { return $map; } } - return []; } @@ -197,7 +197,7 @@ class HomeworkScoreService $studentIds = array_keys($scores); $indexSet = []; foreach ($scores as $hwData) { - if (! is_array($hwData)) { + if (!is_array($hwData)) { continue; } foreach ($hwData as $index => $_) { @@ -209,11 +209,11 @@ class HomeworkScoreService $checkedItems = []; foreach ($missingOk as $studentId => $indexes) { - if (! is_array($indexes)) { + if (!is_array($indexes)) { continue; } foreach ($indexes as $index => $flag) { - if (! is_numeric($index)) { + if (!is_numeric($index)) { continue; } $checkedItems[] = [ @@ -236,22 +236,22 @@ class HomeworkScoreService $count = 0; foreach ($scores as $studentId => $hwData) { - if (! is_numeric($studentId) || ! is_array($hwData)) { + if (!is_numeric($studentId) || !is_array($hwData)) { continue; } $student = Student::query()->find((int) $studentId); - if (! $student) { + if (!$student) { continue; } foreach ($hwData as $index => $score) { - if (! is_numeric($index)) { + if (!is_numeric($index)) { continue; } $rawScore = $score ?? null; - $validator = new ScoreValueValidator; + $validator = new ScoreValueValidator(); $normalizedScore = $validator->normalizeNullable($rawScore, ScoreValueValidator::DEFAULT_MAX_POINTS, 'Homework score'); - $missingAllowed = ! empty($missingOk[$studentId][$index]); + $missingAllowed = !empty($missingOk[$studentId][$index]); $existing = Homework::query() ->where('student_id', $studentId) @@ -306,7 +306,7 @@ class HomeworkScoreService foreach ($students as $student) { $studentRow = Student::query()->find((int) $student->student_id); - if (! $studentRow) { + if (!$studentRow) { continue; } $exists = Homework::query() diff --git a/app/Services/Scores/ParticipationScoreService.php b/app/Services/Scores/ParticipationScoreService.php index 53ae406e..ebcbf743 100644 --- a/app/Services/Scores/ParticipationScoreService.php +++ b/app/Services/Scores/ParticipationScoreService.php @@ -2,18 +2,20 @@ namespace App\Services\Scores; -use App\Models\ClassSection; use App\Models\GradingLock; +use App\Models\ClassSection; +use App\Services\Grading\Validation\ScoreValueValidator; use App\Models\MissingScoreOverride; use App\Models\Participation; use App\Models\Student; use App\Models\StudentClass; use App\Models\TeacherClass; -use App\Services\Grading\Validation\ScoreValueValidator; class ParticipationScoreService { - public function __construct(private ScoreTermService $term) {} + public function __construct(private ScoreTermService $term) + { + } public function resolveClassSectionIdForTeacher(int $teacherId, ?int $requestedClassSectionId, ?string $schoolYearInput): ?int { @@ -171,22 +173,22 @@ class ParticipationScoreService $count = 0; foreach ($scores as $studentId => $data) { - if (! is_numeric($studentId)) { + if (!is_numeric($studentId)) { continue; } - if (! is_array($data)) { + if (!is_array($data)) { $data = ['score' => $data]; } $student = Student::query()->find((int) $studentId); - if (! $student) { + if (!$student) { continue; } $rawScore = $data['score'] ?? null; - $validator = new ScoreValueValidator; + $validator = new ScoreValueValidator(); $normalizedScore = $validator->normalizeNullable($rawScore, ScoreValueValidator::DEFAULT_MAX_POINTS, 'Participation score'); - $missingAllowed = ! empty($missingOk[$studentId]); + $missingAllowed = !empty($missingOk[$studentId]); $existing = Participation::query() ->where('student_id', $studentId) diff --git a/app/Services/Scores/ProjectCalculator.php b/app/Services/Scores/ProjectCalculator.php index 15cce2f3..b4c4da27 100644 --- a/app/Services/Scores/ProjectCalculator.php +++ b/app/Services/Scores/ProjectCalculator.php @@ -7,12 +7,13 @@ use App\Models\Project; class ProjectCalculator implements ScoreCalculatorInterface { - public function __construct(private Project $projectModel) {} + public function __construct(private Project $projectModel) + { + } public function calculate(int $studentId, string $semester, string $schoolYear, ?int $classSectionId = null): array { $average = Project::averageScore($studentId, $semester, $schoolYear, $classSectionId); - return ['project_avg' => $average !== null ? (float) $average : null]; } } diff --git a/app/Services/Scores/ProjectScoreService.php b/app/Services/Scores/ProjectScoreService.php index 43c2eaa0..6372d968 100644 --- a/app/Services/Scores/ProjectScoreService.php +++ b/app/Services/Scores/ProjectScoreService.php @@ -2,18 +2,21 @@ namespace App\Services\Scores; -use App\Models\ClassSection; use App\Models\GradingLock; use App\Models\MissingScoreOverride; use App\Models\Project; use App\Models\Student; use App\Models\StudentClass; use App\Models\TeacherClass; +use App\Models\ClassSection; use App\Services\Grading\Validation\ScoreValueValidator; +use Illuminate\Support\Facades\DB; class ProjectScoreService { - public function __construct(private ScoreTermService $term) {} + public function __construct(private ScoreTermService $term) + { + } public function resolveClassSectionIdForTeacher(int $teacherId, ?int $requestedClassSectionId, ?string $schoolYearInput): ?int { @@ -165,7 +168,7 @@ class ProjectScoreService $studentIds = array_keys($scores); $indexSet = []; foreach ($scores as $projects) { - if (! is_array($projects)) { + if (!is_array($projects)) { continue; } foreach ($projects as $index => $_) { @@ -177,11 +180,11 @@ class ProjectScoreService $checkedItems = []; foreach ($missingOk as $studentId => $indexes) { - if (! is_array($indexes)) { + if (!is_array($indexes)) { continue; } foreach ($indexes as $index => $flag) { - if (! is_numeric($index)) { + if (!is_numeric($index)) { continue; } $checkedItems[] = [ @@ -204,21 +207,21 @@ class ProjectScoreService $count = 0; foreach ($scores as $studentId => $projects) { - if (! is_numeric($studentId) || ! is_array($projects)) { + if (!is_numeric($studentId) || !is_array($projects)) { continue; } $student = Student::query()->find((int) $studentId); - if (! $student) { + if (!$student) { continue; } foreach ($projects as $index => $score) { - if (! is_numeric($index)) { + if (!is_numeric($index)) { continue; } - $validator = new ScoreValueValidator; + $validator = new ScoreValueValidator(); $normalizedScore = $validator->normalizeNullable($score, ScoreValueValidator::DEFAULT_MAX_POINTS, 'Project score'); - $missingAllowed = ! empty($missingOk[$studentId][$index]); + $missingAllowed = !empty($missingOk[$studentId][$index]); $existing = Project::query() ->where('student_id', $studentId) @@ -272,7 +275,7 @@ class ProjectScoreService foreach ($students as $student) { $studentRow = Student::query()->find((int) $student->student_id); - if (! $studentRow) { + if (!$studentRow) { continue; } $exists = Project::query() diff --git a/app/Services/Scores/QuizCalculator.php b/app/Services/Scores/QuizCalculator.php index 03468d3e..63708f75 100644 --- a/app/Services/Scores/QuizCalculator.php +++ b/app/Services/Scores/QuizCalculator.php @@ -24,7 +24,7 @@ class QuizCalculator implements ScoreCalculatorInterface $count = 0; foreach ($rows as $row) { $score = $row->score ?? null; - if ($score === null || $score === '' || ! is_numeric($score)) { + if ($score === null || $score === '' || !is_numeric($score)) { continue; } $total += (float) $score; diff --git a/app/Services/Scores/QuizScoreService.php b/app/Services/Scores/QuizScoreService.php index 72c52c25..117ede14 100644 --- a/app/Services/Scores/QuizScoreService.php +++ b/app/Services/Scores/QuizScoreService.php @@ -2,18 +2,20 @@ namespace App\Services\Scores; -use App\Models\ClassSection; use App\Models\GradingLock; use App\Models\MissingScoreOverride; use App\Models\Quiz; use App\Models\Student; use App\Models\StudentClass; use App\Models\TeacherClass; +use App\Models\ClassSection; use App\Services\Grading\Validation\ScoreValueValidator; class QuizScoreService { - public function __construct(private ScoreTermService $term) {} + public function __construct(private ScoreTermService $term) + { + } public function resolveClassSectionIdForTeacher(int $teacherId, ?int $requestedClassSectionId, ?string $schoolYearInput): ?int { @@ -165,7 +167,7 @@ class QuizScoreService $studentIds = array_keys($scores); $indexSet = []; foreach ($scores as $quizData) { - if (! is_array($quizData)) { + if (!is_array($quizData)) { continue; } foreach ($quizData as $index => $_) { @@ -177,11 +179,11 @@ class QuizScoreService $checkedItems = []; foreach ($missingOk as $studentId => $indexes) { - if (! is_array($indexes)) { + if (!is_array($indexes)) { continue; } foreach ($indexes as $index => $flag) { - if (! is_numeric($index)) { + if (!is_numeric($index)) { continue; } $checkedItems[] = [ @@ -204,23 +206,23 @@ class QuizScoreService $count = 0; foreach ($scores as $studentId => $quizData) { - if (! is_numeric($studentId)) { + if (!is_numeric($studentId)) { continue; } - if (! is_array($quizData)) { + if (!is_array($quizData)) { $quizData = [1 => $quizData]; } $student = Student::query()->find((int) $studentId); - if (! $student) { + if (!$student) { continue; } foreach ($quizData as $quizNumber => $score) { - if (! is_numeric($quizNumber)) { + if (!is_numeric($quizNumber)) { continue; } - $validator = new ScoreValueValidator; + $validator = new ScoreValueValidator(); $normalizedScore = $validator->normalizeNullable($score, ScoreValueValidator::DEFAULT_MAX_POINTS, 'Quiz score'); - $missingAllowed = ! empty($missingOk[$studentId][$quizNumber]); + $missingAllowed = !empty($missingOk[$studentId][$quizNumber]); $existing = Quiz::query() ->where('student_id', $studentId) @@ -274,7 +276,7 @@ class QuizScoreService foreach ($students as $student) { $studentRow = Student::query()->find((int) $student->student_id); - if (! $studentRow) { + if (!$studentRow) { continue; } $exists = Quiz::query() diff --git a/app/Services/Scores/ScoreCommentService.php b/app/Services/Scores/ScoreCommentService.php index b316b943..6796f64c 100644 --- a/app/Services/Scores/ScoreCommentService.php +++ b/app/Services/Scores/ScoreCommentService.php @@ -2,18 +2,21 @@ namespace App\Services\Scores; -use App\Models\ClassSection; use App\Models\Configuration; +use App\Models\ClassSection; use App\Models\GradingLock; use App\Models\MissingScoreOverride; use App\Models\ScoreComment; use App\Models\SemesterScore; use App\Models\Student; +use App\Models\StudentClass; use Illuminate\Support\Facades\DB; class ScoreCommentService { - public function __construct(private ScoreTermService $term) {} + public function __construct(private ScoreTermService $term) + { + } public function list(?string $classSectionId, ?string $semester, ?string $schoolYear): array { @@ -30,24 +33,24 @@ class ScoreCommentService ->orderBy('s.firstname') ->distinct(); - if (! $isAllSections && $classSectionInt) { + if (!$isAllSections && $classSectionInt) { $builder->where('sc.class_section_id', $classSectionInt); } $students = $builder->get()->map(fn ($row) => (array) $row)->all(); $comments = []; - if (! empty($students)) { + if (!empty($students)) { $studentIds = array_map(static fn ($r) => (int) $r['student_id'], $students); $rawComments = ScoreComment::query() ->whereIn('student_id', $studentIds) ->whereIn('score_type', ['midterm', 'final', 'ptap', 'attendance']) ->where('semester', $activeSemester) ->where('school_year', $schoolYear) - ->when(! $isAllSections && $classSectionInt, function ($q) use ($classSectionInt) { + ->when(!$isAllSections && $classSectionInt, function ($q) use ($classSectionInt) { $q->where(function ($w) use ($classSectionInt) { $w->where('class_section_id', $classSectionInt) - ->orWhereNull('class_section_id'); + ->orWhereNull('class_section_id'); }); }) ->get(); @@ -84,7 +87,7 @@ class ScoreCommentService if ($hasComment) { continue; } - if (! function_exists('attendance_comment_from_score')) { + if (!function_exists('attendance_comment_from_score')) { continue; } $score = $attendanceScores[$sid] ?? null; @@ -109,10 +112,10 @@ class ScoreCommentService 'semester' => $activeSemester, 'schoolYear' => $schoolYear, 'classSectionId' => $isAllSections ? 'allsections' : $classSectionInt, - 'classSectionName' => (! $isAllSections && $classSectionInt) + 'classSectionName' => (!$isAllSections && $classSectionInt) ? (string) (ClassSection::query()->where('class_section_id', $classSectionInt)->value('class_section_name') ?? '') : '', - 'scoresLocked' => (! $isAllSections && $classSectionInt) + 'scoresLocked' => (!$isAllSections && $classSectionInt) ? GradingLock::isLocked($classSectionInt, $activeSemester, $schoolYear) : false, ]; @@ -140,7 +143,7 @@ class ScoreCommentService foreach ($commentTypes as $type) { $checkedItems = []; foreach ($missingOk as $studentId => $types) { - if (! is_array($types) || empty($types[$type])) { + if (!is_array($types) || empty($types[$type])) { continue; } $checkedItems[] = [ @@ -172,7 +175,6 @@ class ScoreCommentService $validationError = $this->validateComment($trimmedComment, (int) $studentId, $normalizedScoreType, $studentNames); if ($validationError !== null) { $errors[] = $validationError; - continue; } @@ -183,7 +185,7 @@ class ScoreCommentService ->where('school_year', $schoolYear) ->where(function ($q) use ($classSectionId) { $q->where('class_section_id', $classSectionId) - ->orWhereNull('class_section_id'); + ->orWhereNull('class_section_id'); }) ->first(); @@ -237,7 +239,6 @@ class ScoreCommentService $validationError = $this->validateComment($commentText, (int) $studentId, $normalizedScoreType, $studentNames); if ($validationError !== null) { $errors[] = $validationError; - continue; } } @@ -249,7 +250,7 @@ class ScoreCommentService ->where('school_year', $schoolYear) ->where(function ($q) use ($classSectionId) { $q->where('class_section_id', $classSectionId) - ->orWhereNull('class_section_id'); + ->orWhereNull('class_section_id'); }) ->first(); @@ -321,8 +322,8 @@ class ScoreCommentService return "{$firstName}'s {$scoreType} comment must be at most 400 characters."; } - $pattern = '/^'.preg_quote($firstName, '/').'\\b/i'; - if (! preg_match($pattern, $trimmed)) { + $pattern = '/^' . preg_quote($firstName, '/') . '\\b/i'; + if (!preg_match($pattern, $trimmed)) { return "{$firstName}'s {$scoreType} comment must start with the student's first name."; } @@ -332,7 +333,7 @@ class ScoreCommentService private function isReviewer(): bool { $user = auth()->user(); - if (! $user) { + if (!$user) { return false; } @@ -353,6 +354,6 @@ class ScoreCommentService explode(',', $reviewerRoles) ); - return ! empty(array_intersect($currentUserRoles, $allowedRoles)); + return !empty(array_intersect($currentUserRoles, $allowedRoles)); } } diff --git a/app/Services/Scores/ScoreDashboardService.php b/app/Services/Scores/ScoreDashboardService.php index cbe417e4..fe4546ce 100644 --- a/app/Services/Scores/ScoreDashboardService.php +++ b/app/Services/Scores/ScoreDashboardService.php @@ -3,6 +3,7 @@ namespace App\Services\Scores; use App\Models\ClassSection; +use App\Models\Configuration; use App\Models\GradingLock; use App\Models\MissingScoreOverride; use App\Models\ScoreComment; @@ -14,14 +15,16 @@ use Illuminate\Support\Facades\DB; class ScoreDashboardService { - public function __construct(private ScoreTermService $term) {} + public function __construct(private ScoreTermService $term) + { + } public function overview(int $teacherId, ?int $classSectionId, ?string $semester, ?string $schoolYear): array { $schoolYear = $this->term->schoolYear($schoolYear); $semesterLabel = $this->term->semesterLabel($semester, $semester); - if ($teacherId <= 0 && ! empty($classSectionId)) { + if ($teacherId <= 0 && !empty($classSectionId)) { $allowedIds = [(int) $classSectionId]; } else { $allowedIds = TeacherClass::query() @@ -42,7 +45,7 @@ class ScoreDashboardService $studentQuery = StudentClass::query(); if (DB::connection()->getDriverName() === 'sqlite') { - $studentQuery->whereRaw('class_section_id = '.(int) $classSectionId); + $studentQuery->whereRaw('class_section_id = ' . (int) $classSectionId); if ($schoolYear !== '') { $quoted = DB::connection()->getPdo()->quote($schoolYear); $studentQuery->whereRaw("school_year = {$quoted}"); @@ -59,9 +62,9 @@ class ScoreDashboardService ->map(fn ($v) => (int) $v) ->all(); if (empty($studentIds) && DB::connection()->getDriverName() === 'sqlite') { - $schoolYearClause = $schoolYear !== '' ? ' and school_year = '.DB::connection()->getPdo()->quote($schoolYear) : ''; + $schoolYearClause = $schoolYear !== '' ? " and school_year = " . DB::connection()->getPdo()->quote($schoolYear) : ''; $rows = DB::select( - 'select student_id from student_class where class_section_id = '.(int) $classSectionId.$schoolYearClause + "select student_id from student_class where class_section_id = " . (int) $classSectionId . $schoolYearClause ); $studentIds = array_values(array_filter(array_map( static fn ($row) => isset($row->student_id) ? (int) $row->student_id : 0, @@ -170,7 +173,7 @@ class ScoreDashboardService foreach ($commentTypes as $type) { $checkedItems = []; foreach ($missingOk as $studentId => $types) { - if (! is_array($types) || empty($types[$type])) { + if (!is_array($types) || empty($types[$type])) { continue; } $checkedItems[] = [ @@ -226,7 +229,7 @@ class ScoreDashboardService ->all(); $scores = []; - if (! empty($students)) { + if (!empty($students)) { $studentIdList = array_column($students, 'id'); $semesterScores = DB::table('semester_scores') ->whereIn('student_id', $studentIdList) diff --git a/app/Services/Scores/ScorePredictorDataService.php b/app/Services/Scores/ScorePredictorDataService.php index 04716959..66da63b8 100644 --- a/app/Services/Scores/ScorePredictorDataService.php +++ b/app/Services/Scores/ScorePredictorDataService.php @@ -56,7 +56,7 @@ class ScorePredictorDataService ->orWhere('cs.class_section_name', 'not like', 'KG%'); }); - if (! empty($classSectionId)) { + if (!empty($classSectionId)) { $builder->where('sc.class_section_id', $classSectionId); } @@ -81,7 +81,7 @@ class ScorePredictorDataService ->orWhere('cs.class_section_name', 'not like', 'KG%'); }); - if (! empty($classSectionId)) { + if (!empty($classSectionId)) { $builder->where('student_class.class_section_id', $classSectionId); } diff --git a/app/Services/Scores/ScorePredictorRiskService.php b/app/Services/Scores/ScorePredictorRiskService.php index 46382f0b..cbb9ad1b 100644 --- a/app/Services/Scores/ScorePredictorRiskService.php +++ b/app/Services/Scores/ScorePredictorRiskService.php @@ -14,7 +14,6 @@ class ScorePredictorRiskService $a5 = 1.061405429; $erf = 1 - ((((($a5 * $t + $a4) * $t + $a3) * $t + $a2) * $t + $a1) * $t) * exp(-$z * $z); $sign = $z < 0 ? -1 : 1; - return 0.5 * (1 + $sign * $erf); } @@ -27,7 +26,7 @@ class ScorePredictorRiskService $springScore = is_numeric($student['spring_score'] ?? null) ? (float) $student['spring_score'] : null; $scoreParts = array_values(array_filter([$fallScore, $springScore], static fn ($v) => $v !== null)); - $finalAvg = ! empty($scoreParts) ? round(array_sum($scoreParts) / count($scoreParts), 1) : null; + $finalAvg = !empty($scoreParts) ? round(array_sum($scoreParts) / count($scoreParts), 1) : null; $waitingForSpring = ($fallScore !== null && $springScore === null); @@ -41,8 +40,8 @@ class ScorePredictorRiskService $probLow = $zLow !== null ? $this->normalCdf($zLow) : null; $riskHigh = match (true) { - $finalAvg !== null && ! $waitingForSpring && $finalAvg >= $trophyThreshold => 'Achieved Trophy', - $finalAvg !== null && ! $waitingForSpring => 'Unreachable', + $finalAvg !== null && !$waitingForSpring && $finalAvg >= $trophyThreshold => 'Achieved Trophy', + $finalAvg !== null && !$waitingForSpring => 'Unreachable', $requiredHigh !== null && $requiredHigh > 100 => 'Unreachable', is_null($probHigh) => 'New student', $probHigh >= 0.66 => 'Low', @@ -51,8 +50,8 @@ class ScorePredictorRiskService }; $riskLow = match (true) { - $finalAvg !== null && ! $waitingForSpring && $finalAvg < $targetLow => 'High', - $finalAvg !== null && ! $waitingForSpring => 'Low', + $finalAvg !== null && !$waitingForSpring && $finalAvg < $targetLow => 'High', + $finalAvg !== null && !$waitingForSpring => 'Low', $requiredLow !== null && $requiredLow > 100 => 'Unlikely to pass', is_null($probLow) => 'New student', $probLow >= 0.66 => 'High', @@ -104,23 +103,19 @@ class ScorePredictorRiskService } foreach ($bySection as $cid => $indices) { - if (! empty($hasThresholdTrophy[$cid])) { + if (!empty($hasThresholdTrophy[$cid])) { continue; } $candidates = []; foreach ($indices as $idx) { $val = $results[$idx]['final_average'] ?? null; - if (! is_numeric($val)) { + if (!is_numeric($val)) { $fall = $results[$idx]['fall_score'] ?? null; $spring = $results[$idx]['spring_score'] ?? null; $parts = []; - if (is_numeric($fall)) { - $parts[] = (float) $fall; - } - if (is_numeric($spring)) { - $parts[] = (float) $spring; - } - if (! empty($parts)) { + if (is_numeric($fall)) $parts[] = (float) $fall; + if (is_numeric($spring)) $parts[] = (float) $spring; + if (!empty($parts)) { $val = array_sum($parts) / count($parts); } } diff --git a/app/Services/Scores/ScorePredictorService.php b/app/Services/Scores/ScorePredictorService.php index 7e860ab1..5f37be9f 100644 --- a/app/Services/Scores/ScorePredictorService.php +++ b/app/Services/Scores/ScorePredictorService.php @@ -9,7 +9,8 @@ class ScorePredictorService public function __construct( private ScorePredictorDataService $dataService, private ScorePredictorRiskService $riskService - ) {} + ) { + } public function report(?string $schoolYear, ?int $classSectionId = null): array { diff --git a/app/Services/Scores/ScoreTermService.php b/app/Services/Scores/ScoreTermService.php index f9cbf145..1ab6eb47 100644 --- a/app/Services/Scores/ScoreTermService.php +++ b/app/Services/Scores/ScoreTermService.php @@ -15,7 +15,6 @@ class ScoreTermService if ($value === 'spring' || str_contains($value, '2')) { return 'spring'; } - return ''; } diff --git a/app/Services/Scores/SemesterScoreService.php b/app/Services/Scores/SemesterScoreService.php index 978e45ae..5bd082f8 100644 --- a/app/Services/Scores/SemesterScoreService.php +++ b/app/Services/Scores/SemesterScoreService.php @@ -3,17 +3,21 @@ namespace App\Services\Scores; use App\Interfaces\ScoreCalculatorInterface; -use App\Models\AttendanceRecord; -use App\Models\CalendarEvent; use App\Models\Configuration; use App\Models\FinalExam; -use App\Models\Homework; use App\Models\MidtermExam; use App\Models\Participation; -use App\Models\Project; use App\Models\SemesterScore; -use App\Services\Grading\Policy\GradingPolicyResolver; +use App\Services\Scores\AttendanceCalculator; +use App\Services\Scores\HomeworkCalculator; +use App\Services\Scores\ProjectCalculator; +use App\Services\Scores\QuizCalculator; +use App\Models\AttendanceRecord; +use App\Models\CalendarEvent; +use App\Models\Homework; +use App\Models\Project; use RuntimeException; +use App\Services\Grading\Policy\GradingPolicyResolver; class SemesterScoreService { @@ -21,11 +25,8 @@ class SemesterScoreService private array $scoreCalculators = []; private string $semester; - private string $schoolYear; - private ?int $actorId; - private GradingPolicyResolver $policyResolver; public function __construct( @@ -37,16 +38,16 @@ class SemesterScoreService $this->semester = (string) (Configuration::getConfig('semester') ?? ''); $this->schoolYear = (string) (Configuration::getConfig('school_year') ?? ''); $this->actorId = (int) (auth()->id() ?? 0) ?: null; - $this->policyResolver = new GradingPolicyResolver; + $this->policyResolver = new GradingPolicyResolver(); $attendanceCalculator = $attendanceCalculator ?? new AttendanceCalculator( - new AttendanceRecord, - new Configuration, - new CalendarEvent + new AttendanceRecord(), + new Configuration(), + new CalendarEvent() ); - $homeworkCalculator = $homeworkCalculator ?? new HomeworkCalculator(new Homework); - $quizCalculator = $quizCalculator ?? new QuizCalculator; - $projectCalculator = $projectCalculator ?? new ProjectCalculator(new Project); + $homeworkCalculator = $homeworkCalculator ?? new HomeworkCalculator(new Homework()); + $quizCalculator = $quizCalculator ?? new QuizCalculator(); + $projectCalculator = $projectCalculator ?? new ProjectCalculator(new Project()); $this->scoreCalculators = [ AttendanceCalculator::class => $attendanceCalculator, @@ -69,7 +70,7 @@ class SemesterScoreService public function updateStudentScores(array $student, string $semester, string $schoolYear): void { foreach (['student_id', 'school_id', 'class_section_id'] as $field) { - if (! isset($student[$field])) { + if (!isset($student[$field])) { throw new RuntimeException("Missing required student field: {$field}"); } } @@ -113,7 +114,7 @@ class SemesterScoreService } } - if (! isset($scoreData['participation_score'])) { + if (!isset($scoreData['participation_score'])) { $participationScore = Participation::getParticipationScore($studentId, $semester, $schoolYear, $classSectionId); $scoreData['participation_score'] = $participationScore !== null ? (float) $participationScore : null; } @@ -139,27 +140,24 @@ class SemesterScoreService $hasTests = $avgQuiz !== null; $hasProjects = $avgProject !== null; - if (! $hasTests && ! $hasProjects) { + if (!$hasTests && !$hasProjects) { if ($avgHomework === null || $avgParticipation === null) { return null; } - return round(($avgHomework * 0.5) + ($avgParticipation * 0.5), 1); } - if (! $hasProjects) { + if (!$hasProjects) { if ($avgHomework === null || $avgParticipation === null || $avgQuiz === null) { return null; } - return round(($avgHomework * 0.4) + ($avgParticipation * 0.4) + ($avgQuiz * 0.2), 1); } - if (! $hasTests) { + if (!$hasTests) { if ($avgHomework === null || $avgParticipation === null || $avgProject === null) { return null; } - return round(($avgHomework * 0.34) + ($avgParticipation * 0.33) + ($avgProject * 0.33), 1); } @@ -190,7 +188,6 @@ class SemesterScoreService if ($ptap !== null && $attendance !== null && $out['midterm_score'] !== null) { $out['semester_score'] = round(($attendance * 0.2) + ($ptap * 0.2) + ($out['midterm_score'] * 0.6), 1); } - return $out; } @@ -200,7 +197,6 @@ class SemesterScoreService if ($ptap !== null && $attendance !== null && $out['final_score'] !== null) { $out['semester_score'] = round(($attendance * 0.2) + ($ptap * 0.2) + ($out['final_score'] * 0.6), 1); } - return $out; } diff --git a/app/Services/Security/IpBanCommandService.php b/app/Services/Security/IpBanCommandService.php index 5a237752..26b92a0f 100644 --- a/app/Services/Security/IpBanCommandService.php +++ b/app/Services/Security/IpBanCommandService.php @@ -10,7 +10,7 @@ class IpBanCommandService public function banNow(?int $id, ?string $ip, int $hours): ?IpAttempt { $row = $this->findRow($id, $ip); - if (! $row) { + if (!$row) { return null; } @@ -21,7 +21,6 @@ class IpBanCommandService $row->blocked_until = $blockedUntil; $row->attempts = $attempts; $row->save(); - return $row->refresh(); }); } @@ -29,7 +28,7 @@ class IpBanCommandService public function unbanOne(?int $id, ?string $ip): ?IpAttempt { $row = $this->findRow($id, $ip); - if (! $row) { + if (!$row) { return null; } @@ -37,7 +36,6 @@ class IpBanCommandService $row->blocked_until = null; $row->attempts = 0; $row->save(); - return $row->refresh(); }); } @@ -69,7 +67,6 @@ class IpBanCommandService if (function_exists('utc_now')) { return (string) utc_now(); } - return now('UTC')->toDateTimeString(); } @@ -79,7 +76,6 @@ class IpBanCommandService if (function_exists('utc_now')) { return (string) date('Y-m-d H:i:s', strtotime("+{$hours} hours", strtotime((string) utc_now()))); } - return now('UTC')->addHours($hours)->toDateTimeString(); } } diff --git a/app/Services/Security/IpBanQueryService.php b/app/Services/Security/IpBanQueryService.php index 4de54ef5..d3468abb 100644 --- a/app/Services/Security/IpBanQueryService.php +++ b/app/Services/Security/IpBanQueryService.php @@ -16,8 +16,8 @@ class IpBanQueryService $query->where('blocked_until', '>', $this->utcNow()); } - if (! empty($filters['search'])) { - $term = '%'.trim((string) $filters['search']).'%'; + if (!empty($filters['search'])) { + $term = '%' . trim((string) $filters['search']) . '%'; $query->where('ip_address', 'like', $term); } @@ -41,7 +41,6 @@ class IpBanQueryService if (function_exists('utc_now')) { return (string) utc_now(); } - return now('UTC')->toDateTimeString(); } } diff --git a/app/Services/SemesterRangeService.php b/app/Services/SemesterRangeService.php index e9dec0d5..f13d68a3 100644 --- a/app/Services/SemesterRangeService.php +++ b/app/Services/SemesterRangeService.php @@ -7,7 +7,9 @@ use DateTimeImmutable; class SemesterRangeService { - public function __construct(private SemesterConfigService $configService) {} + public function __construct(private SemesterConfigService $configService) + { + } public function getSchoolYearRange(string $schoolYear): array { @@ -29,7 +31,7 @@ class SemesterRangeService public function getSemesterRange(string $schoolYear, string $semester): ?array { - if (! preg_match('/^(\\d{4})-(\\d{4})$/', $schoolYear, $m)) { + if (!preg_match('/^(\\d{4})-(\\d{4})$/', $schoolYear, $m)) { return null; } $y1 = (int) $m[1]; @@ -55,7 +57,6 @@ class SemesterRangeService if ($value === 'spring') { return 'Spring'; } - return ''; } @@ -102,15 +103,13 @@ class SemesterRangeService if (preg_match('/^(\\d{4})-(\\d{4})$/', $schoolYear, $m)) { $y1 = (int) $m[1]; $y2 = (int) $m[2]; - - return [new DateTimeImmutable($y1.'-09-01'), new DateTimeImmutable($y2.'-06-30')]; + return [new DateTimeImmutable($y1 . '-09-01'), new DateTimeImmutable($y2 . '-06-30')]; } $currentYear = (int) date('Y'); - return [ - new DateTimeImmutable($currentYear.'-09-01'), - new DateTimeImmutable(($currentYear + 1).'-06-30'), + new DateTimeImmutable($currentYear . '-09-01'), + new DateTimeImmutable(($currentYear + 1) . '-06-30'), ]; } } diff --git a/app/Services/Settings/ConfigurationService.php b/app/Services/Settings/ConfigurationService.php index 966b0523..fe1030db 100644 --- a/app/Services/Settings/ConfigurationService.php +++ b/app/Services/Settings/ConfigurationService.php @@ -25,7 +25,7 @@ class ConfigurationService public function update(int $id, array $payload): ?Configuration { $config = Configuration::query()->find($id); - if (! $config) { + if (!$config) { return null; } @@ -40,7 +40,7 @@ class ConfigurationService public function delete(int $id): bool { $config = Configuration::query()->find($id); - if (! $config) { + if (!$config) { return false; } diff --git a/app/Services/Settings/SchoolCalendar/SchoolCalendarFormatterService.php b/app/Services/Settings/SchoolCalendar/SchoolCalendarFormatterService.php index b0b62602..c75ab131 100644 --- a/app/Services/Settings/SchoolCalendar/SchoolCalendarFormatterService.php +++ b/app/Services/Settings/SchoolCalendar/SchoolCalendarFormatterService.php @@ -101,28 +101,28 @@ class SchoolCalendarFormatterService $parentName = (string) ($row['parent_name'] ?? 'Parent'); $studentName = (string) ($row['student_name'] ?? 'Student'); $time = trim((string) ($row['time'] ?? '')); - $timeLabel = $time !== '' ? (' '.$time) : ''; + $timeLabel = $time !== '' ? (' ' . $time) : ''; $title = $audience === 'admin' ? 'Admin Meeting' : 'Parent Meeting'; $descParts = []; - $descParts[] = 'Student: '.$studentName; - $descParts[] = 'Parent: '.$parentName; - if (! empty($row['class_section_name'])) { - $descParts[] = 'Section: '.$row['class_section_name']; + $descParts[] = 'Student: ' . $studentName; + $descParts[] = 'Parent: ' . $parentName; + if (!empty($row['class_section_name'])) { + $descParts[] = 'Section: ' . $row['class_section_name']; } - $descParts[] = 'Date/Time: '.($row['date'] ?? '').$timeLabel; - if (! empty($row['notes'])) { - $descParts[] = 'Notes: '.$row['notes']; + $descParts[] = 'Date/Time: ' . ($row['date'] ?? '') . $timeLabel; + if (!empty($row['notes'])) { + $descParts[] = 'Notes: ' . $row['notes']; } $start = (string) ($row['date'] ?? ''); if ($start !== '' && $time !== '') { - $start = $start.'T'.$time; + $start = $start . 'T' . $time; } return [ - 'id' => 'pm-'.(int) ($row['id'] ?? 0), + 'id' => 'pm-' . (int) ($row['id'] ?? 0), 'title' => $title, 'start' => $start, 'description' => implode("\n", $descParts), diff --git a/app/Services/Settings/SchoolCalendar/SchoolCalendarMeetingService.php b/app/Services/Settings/SchoolCalendar/SchoolCalendarMeetingService.php index 79c7e016..f05032d6 100644 --- a/app/Services/Settings/SchoolCalendar/SchoolCalendarMeetingService.php +++ b/app/Services/Settings/SchoolCalendar/SchoolCalendarMeetingService.php @@ -23,7 +23,7 @@ class SchoolCalendarMeetingService if ($audience === 'parent' && $parentUserId) { $studentIds = $this->getStudentIdsForParent($parentUserId); - if (! empty($studentIds)) { + if (!empty($studentIds)) { $builder->where(function (Builder $q) use ($parentUserId, $studentIds) { $q->where('parent_user_id', $parentUserId) ->orWhereIn('student_id', $studentIds); @@ -34,7 +34,7 @@ class SchoolCalendarMeetingService } $rows = $builder->orderBy('date', 'ASC')->get()->toArray(); - if (! empty($rows)) { + if (!empty($rows)) { return $rows; } @@ -48,7 +48,7 @@ class SchoolCalendarMeetingService if ($audience === 'parent' && $parentUserId) { $studentIds = $this->getStudentIdsForParent($parentUserId); - if (! empty($studentIds)) { + if (!empty($studentIds)) { $fallback->where(function (Builder $q) use ($parentUserId, $studentIds) { $q->where('parent_user_id', $parentUserId) ->orWhereIn('student_id', $studentIds); diff --git a/app/Services/Settings/SchoolCalendar/SchoolCalendarMutationService.php b/app/Services/Settings/SchoolCalendar/SchoolCalendarMutationService.php index faed63ea..a14861e1 100644 --- a/app/Services/Settings/SchoolCalendar/SchoolCalendarMutationService.php +++ b/app/Services/Settings/SchoolCalendar/SchoolCalendarMutationService.php @@ -11,7 +11,8 @@ class SchoolCalendarMutationService public function __construct( private SchoolCalendarContextService $context, private SchoolCalendarNotificationService $notificationService - ) {} + ) { + } public function create(array $payload, array $targets = []): array { @@ -22,7 +23,7 @@ class SchoolCalendarMutationService return CalendarEvent::query()->create($data); }); } catch (\Throwable $e) { - Log::error('SchoolCalendar create failed: '.$e->getMessage()); + Log::error('SchoolCalendar create failed: ' . $e->getMessage()); throw $e; } @@ -44,7 +45,7 @@ class SchoolCalendarMutationService $event->save(); }); } catch (\Throwable $e) { - Log::error('SchoolCalendar update failed: '.$e->getMessage()); + Log::error('SchoolCalendar update failed: ' . $e->getMessage()); throw $e; } @@ -64,7 +65,7 @@ class SchoolCalendarMutationService $event->delete(); }); } catch (\Throwable $e) { - Log::error('SchoolCalendar delete failed: '.$e->getMessage()); + Log::error('SchoolCalendar delete failed: ' . $e->getMessage()); throw $e; } } @@ -76,15 +77,15 @@ class SchoolCalendarMutationService 'description' => $payload['description'] ?? null, 'event_type' => $payload['event_type'] ?? null, 'date' => $payload['date'] ?? $payload['start'] ?? null, - 'notify_parent' => ! empty($payload['notify_parent']) ? 1 : 0, - 'notify_teacher' => ! empty($payload['notify_teacher']) ? 1 : 0, - 'notify_admin' => ! empty($payload['notify_admin']) ? 1 : 0, - 'no_school' => ! empty($payload['no_school']) ? 1 : 0, + 'notify_parent' => !empty($payload['notify_parent']) ? 1 : 0, + 'notify_teacher' => !empty($payload['notify_teacher']) ? 1 : 0, + 'notify_admin' => !empty($payload['notify_admin']) ? 1 : 0, + 'no_school' => !empty($payload['no_school']) ? 1 : 0, 'school_year' => $payload['school_year'] ?? $this->context->defaultSchoolYear(), 'semester' => $payload['semester'] ?? $this->context->defaultSemester(), ]; - if (! CalendarEvent::supportsEventType()) { + if (!CalendarEvent::supportsEventType()) { unset($data['event_type']); } diff --git a/app/Services/Settings/SchoolCalendar/SchoolCalendarNotificationService.php b/app/Services/Settings/SchoolCalendar/SchoolCalendarNotificationService.php index a62ae428..d51eba72 100644 --- a/app/Services/Settings/SchoolCalendar/SchoolCalendarNotificationService.php +++ b/app/Services/Settings/SchoolCalendar/SchoolCalendarNotificationService.php @@ -13,7 +13,8 @@ class SchoolCalendarNotificationService public function __construct( private EmailService $emailService, private ApplicationUrlService $urls, - ) {} + ) { + } public function notify(array $event, array $targets): array { @@ -26,7 +27,7 @@ class SchoolCalendarNotificationService $results = []; foreach ($targets as $group => $enabled) { - if (! $enabled) { + if (!$enabled) { continue; } @@ -34,13 +35,12 @@ class SchoolCalendarNotificationService if (empty($emails)) { Log::warning("SchoolCalendar: no recipient emails for {$group}."); $results[$group] = null; - continue; } $to = $emails[0] ?? ''; $ok = $this->emailService->send($to, $subject, $body, 'general', [], $emails); - if (! $ok) { + if (!$ok) { Log::warning("SchoolCalendar: failed to send calendar notification to {$group}."); } $results[$group] = $ok; @@ -52,9 +52,8 @@ class SchoolCalendarNotificationService private function buildSubject(array $event): string { $title = trim((string) ($event['title'] ?? 'School Calendar Update')); - $typeSegment = ! empty($event['event_type']) ? ' ('.$event['event_type'].')' : ''; - - return 'School Calendar: '.$title.$typeSegment; + $typeSegment = !empty($event['event_type']) ? ' (' . $event['event_type'] . ')' : ''; + return 'School Calendar: ' . $title . $typeSegment; } private function buildBody(array $event): string @@ -64,12 +63,12 @@ class SchoolCalendarNotificationService $date = (string) ($event['date'] ?? ''); $description = trim((string) ($event['description'] ?? '')); - $body = '

'.e($title).'

'; - $body .= '

Date: '.e($date).'

'; + $body = '

' . e($title) . '

'; + $body .= '

Date: ' . e($date) . '

'; if ($description !== '') { - $body .= '

'.nl2br(e($description)).'

'; + $body .= '

' . nl2br(e($description)) . '

'; } - $body .= '

View calendar

'; + $body .= '

View calendar

'; return $body; } @@ -89,19 +88,18 @@ class SchoolCalendarNotificationService $emails = []; foreach ($rows as $row) { $email = trim((string) ($row['email'] ?? '')); - if ($email === '' || ! filter_var($email, FILTER_VALIDATE_EMAIL)) { + if ($email === '' || !filter_var($email, FILTER_VALIDATE_EMAIL)) { continue; } $emails[$email] = true; } - return array_values(array_keys($emails)); } private function fetchAdminRows(): array { $excluded = ['guest', 'teacher', 'teacher_assistant', 'parent']; - $escaped = array_map(static fn ($value) => "'".addslashes($value)."'", $excluded); + $escaped = array_map(static fn ($value) => "'" . addslashes($value) . "'", $excluded); $excludedList = implode(', ', $escaped); $builder = DB::table('users as u') diff --git a/app/Services/Settings/SchoolCalendar/SchoolCalendarQueryService.php b/app/Services/Settings/SchoolCalendar/SchoolCalendarQueryService.php index 0090ef9f..9688cc42 100644 --- a/app/Services/Settings/SchoolCalendar/SchoolCalendarQueryService.php +++ b/app/Services/Settings/SchoolCalendar/SchoolCalendarQueryService.php @@ -3,6 +3,7 @@ namespace App\Services\Settings\SchoolCalendar; use App\Models\CalendarEvent; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Collection; class SchoolCalendarQueryService @@ -35,7 +36,7 @@ class SchoolCalendarQueryService public function filterEventsForAudience(Collection $events, ?string $audience): Collection { $audience = $audience !== null ? strtolower(trim($audience)) : null; - if (! in_array($audience, ['parent', 'teacher', 'admin'], true)) { + if (!in_array($audience, ['parent', 'teacher', 'admin'], true)) { return $events; } @@ -50,7 +51,7 @@ class SchoolCalendarQueryService return true; } - if (! $hasAny) { + if (!$hasAny) { return true; } diff --git a/app/Services/Settings/SettingsService.php b/app/Services/Settings/SettingsService.php index 6ed06c95..045ae1a4 100644 --- a/app/Services/Settings/SettingsService.php +++ b/app/Services/Settings/SettingsService.php @@ -32,7 +32,7 @@ class SettingsService $updates['timezone'] = $payload['timezone']; } - if (! empty($updates)) { + if (!empty($updates)) { $updates['updated_by'] = $userId; $settings->fill($updates); $settings->save(); diff --git a/app/Services/Staff/StaffCommandService.php b/app/Services/Staff/StaffCommandService.php index edd3fd4d..9aab50c5 100644 --- a/app/Services/Staff/StaffCommandService.php +++ b/app/Services/Staff/StaffCommandService.php @@ -10,7 +10,9 @@ use RuntimeException; class StaffCommandService { - public function __construct(private GlobalConfigService $configService) {} + public function __construct(private GlobalConfigService $configService) + { + } public function create(array $payload): Staff { @@ -19,7 +21,7 @@ class StaffCommandService $roleName = (string) ($payload['role_name'] ?? ''); $activeRole = strtolower($roleName); - if (! empty($payload['status']) && strtolower((string) $payload['status']) === 'inactive') { + if (!empty($payload['status']) && strtolower((string) $payload['status']) === 'inactive') { $activeRole = 'inactive'; } @@ -38,7 +40,7 @@ class StaffCommandService 'updated_at' => now(), ]); - if (! $staff) { + if (!$staff) { throw new RuntimeException('Failed to create staff member.'); } @@ -59,13 +61,13 @@ class StaffCommandService if (array_key_exists('role_name', $updates)) { $updates['active_role'] = strtolower((string) $updates['role_name']); } - if (! empty($payload['status']) && strtolower((string) $payload['status']) === 'inactive') { + if (!empty($payload['status']) && strtolower((string) $payload['status']) === 'inactive') { $updates['active_role'] = 'inactive'; } $updates['updated_at'] = now(); - if (! empty($updates)) { + if (!empty($updates)) { $staff->fill($updates); $staff->save(); } @@ -81,7 +83,7 @@ class StaffCommandService private function resolveUserId(array $payload): int { - if (! empty($payload['user_id'])) { + if (!empty($payload['user_id'])) { return (int) $payload['user_id']; } diff --git a/app/Services/Staff/StaffQueryService.php b/app/Services/Staff/StaffQueryService.php index 29db117b..5e465d95 100644 --- a/app/Services/Staff/StaffQueryService.php +++ b/app/Services/Staff/StaffQueryService.php @@ -6,11 +6,14 @@ use App\Models\Staff; use App\Models\TeacherClass; use App\Models\User; use App\Services\System\GlobalConfigService; +use Illuminate\Contracts\Pagination\LengthAwarePaginator; use Illuminate\Support\Facades\DB; class StaffQueryService { - public function __construct(private GlobalConfigService $configService) {} + public function __construct(private GlobalConfigService $configService) + { + } public function paginate(array $filters, int $page, int $perPage): array { @@ -19,7 +22,7 @@ class StaffQueryService $query = Staff::query() ->whereNotIn(DB::raw('LOWER(active_role)'), $excluded); - if (! empty($filters['role'])) { + if (!empty($filters['role'])) { $role = strtolower(trim((string) $filters['role'])); $query->whereRaw('LOWER(active_role) = ?', [$role]); } @@ -44,7 +47,7 @@ class StaffQueryService if (in_array($role, ['teacher', 'teacher_assistant'], true)) { $tid = (int) ($staff->user_id ?? 0); $labels = $assignByTeacher[$tid] ?? []; - if (! empty($labels)) { + if (!empty($labels)) { $staff->class_section = implode(', ', array_unique($labels)); $staff->verification_issue = false; } else { @@ -98,10 +101,10 @@ class StaffQueryService continue; } $pos = strtolower((string) ($row->position ?? '')); - if (! in_array($pos, ['main', 'ta'], true)) { + if (!in_array($pos, ['main', 'ta'], true)) { continue; } - $assignByTeacher[$tid][] = $name.' ('.$pos.')'; + $assignByTeacher[$tid][] = $name . ' (' . $pos . ')'; } return $assignByTeacher; diff --git a/app/Services/Staff/StaffTimeOffLinkService.php b/app/Services/Staff/StaffTimeOffLinkService.php index d66f9ea8..c1bd8719 100644 --- a/app/Services/Staff/StaffTimeOffLinkService.php +++ b/app/Services/Staff/StaffTimeOffLinkService.php @@ -5,11 +5,9 @@ namespace App\Services\Staff; class StaffTimeOffLinkService { private const TOKEN_CONTEXT = 'timeoff_notify'; - private const DEFAULT_TTL = 1209600; private string $secret; - private int $ttl; public function __construct(?string $secret = null, ?int $ttlSeconds = null) @@ -32,12 +30,12 @@ class StaffTimeOffLinkService public function parseToken(?string $token): ?array { - if (! is_string($token) || $token === '') { + if (!is_string($token) || $token === '') { return null; } $payload = $this->jwtDecode($token, $this->secret); - if (! is_array($payload)) { + if (!is_array($payload)) { return null; } @@ -79,17 +77,17 @@ class StaffTimeOffLinkService $payload = json_decode($this->base64UrlDecode($payload64), true); $signature = $this->base64UrlDecode($signature64); - if (! is_array($header) || ! is_array($payload)) { + if (!is_array($header) || !is_array($payload)) { return null; } - $signingInput = $header64.'.'.$payload64; + $signingInput = $header64 . '.' . $payload64; if (($header['alg'] ?? '') !== 'HS256') { return null; } $expected = hash_hmac('sha256', $signingInput, $secret, true); - if (! hash_equals($expected, $signature)) { + if (!hash_equals($expected, $signature)) { return null; } @@ -107,7 +105,6 @@ class StaffTimeOffLinkService if ($padding > 0) { $data .= str_repeat('=', 4 - $padding); } - return base64_decode(strtr($data, '-_', '+/')) ?: ''; } } diff --git a/app/Services/Students/StudentAssignmentService.php b/app/Services/Students/StudentAssignmentService.php index a7461e44..c4ff20e2 100644 --- a/app/Services/Students/StudentAssignmentService.php +++ b/app/Services/Students/StudentAssignmentService.php @@ -11,7 +11,9 @@ use Illuminate\Support\Facades\Schema; class StudentAssignmentService { - public function __construct(private StudentConfigService $configService) {} + public function __construct(private StudentConfigService $configService) + { + } public function assignClasses(int $studentId, array $classSectionIds, bool $isEventOnly, ?int $userId = null): array { @@ -25,7 +27,7 @@ class StudentAssignmentService } $student = Student::query()->find($studentId); - if (! $student) { + if (!$student) { return ['ok' => false, 'message' => 'Student not found.']; } @@ -45,7 +47,7 @@ class StudentAssignmentService } $missing = array_diff($classSectionIds, array_keys($sectionMap)); - if (! empty($missing)) { + if (!empty($missing)) { return ['ok' => false, 'message' => 'One or more selected classes do not exist.']; } @@ -103,7 +105,7 @@ class StudentAssignmentService } } - if (! $isEventOnly) { + if (!$isEventOnly) { $enrollment = Enrollment::query() ->where('student_id', $studentId) ->where('school_year', $schoolYear) @@ -141,12 +143,12 @@ class StudentAssignmentService $displayNames = []; foreach ($classSectionIds as $classSectionId) { $section = $sectionMap[$classSectionId] ?? null; - if (! $section) { + if (!$section) { continue; } $name = (string) ($section['class_section_name'] ?? ''); if ($name === '') { - $name = 'Section #'.$classSectionId; + $name = 'Section #' . $classSectionId; } $displayNames[] = $name; } @@ -178,7 +180,7 @@ class StudentAssignmentService ->where('school_year', $schoolYear) ->first(); - if (! $row) { + if (!$row) { return ['ok' => false, 'message' => 'Assignment not found for this student/class.']; } @@ -210,7 +212,7 @@ class StudentAssignmentService ->first(); if ($enrollment) { - $newClassId = ! empty($remainingIds) ? $remainingIds[0] : null; + $newClassId = !empty($remainingIds) ? $remainingIds[0] : null; if ((int) $enrollment->class_section_id === $classSectionId || $newClassId !== null) { $enrollment->update([ 'class_section_id' => $newClassId, @@ -296,7 +298,7 @@ class StudentAssignmentService $payload['updated_by'] = $modifiedBy; } - $results[$table.'_updated'] = DB::table($table) + $results[$table . '_updated'] = DB::table($table) ->where('student_id', $studentId) ->where('semester', $semester) ->where('school_year', $schoolYear) @@ -310,7 +312,6 @@ class StudentAssignmentService { $values = array_map('intval', $ids); $values = array_values(array_unique(array_filter($values, static fn ($v) => $v > 0))); - return $values; } } diff --git a/app/Services/Students/StudentAutoDistributionService.php b/app/Services/Students/StudentAutoDistributionService.php index 92b4857f..0f857b60 100644 --- a/app/Services/Students/StudentAutoDistributionService.php +++ b/app/Services/Students/StudentAutoDistributionService.php @@ -9,7 +9,9 @@ use Illuminate\Support\Facades\DB; class StudentAutoDistributionService { - public function __construct(private StudentConfigService $configService) {} + public function __construct(private StudentConfigService $configService) + { + } public function promotionTotals(?string $schoolYear = null): array { @@ -28,7 +30,6 @@ class StudentAutoDistributionService $name = strtolower($nameRaw); if ($name === 'kg' || $name === 'youth') { $wanted[] = $row; - continue; } if (ctype_digit($name)) { @@ -166,7 +167,6 @@ class StudentAutoDistributionService $bestIdx = $idx; } } - return $bestIdx; }; diff --git a/app/Services/Students/StudentDirectoryService.php b/app/Services/Students/StudentDirectoryService.php index 27343327..1367ba70 100644 --- a/app/Services/Students/StudentDirectoryService.php +++ b/app/Services/Students/StudentDirectoryService.php @@ -10,7 +10,9 @@ use Illuminate\Support\Facades\DB; class StudentDirectoryService { - public function __construct(private StudentConfigService $configService) {} + public function __construct(private StudentConfigService $configService) + { + } public function assignmentData(?string $schoolYear = null): array { @@ -53,7 +55,7 @@ class StudentDirectoryService $studentId = (int) ($student['id'] ?? 0); $sectionNames = StudentClass::getClassSectionsByStudentIdWithFlags($studentId, $schoolYear, true); $sectionIds = StudentClass::getClassSectionIdsByStudentId($studentId, $schoolYear); - $sectionDisplay = ! empty($sectionNames) ? implode(', ', $sectionNames) : ''; + $sectionDisplay = !empty($sectionNames) ? implode(', ', $sectionNames) : ''; $parentId = (int) ($student['parent_id'] ?? 0); $emergencyInfo = $parentId > 0 @@ -62,7 +64,7 @@ class StudentDirectoryService $studentData[] = [ 'student_id' => $studentId, - 'name' => trim(($student['firstname'] ?? '').' '.($student['lastname'] ?? '')), + 'name' => trim(($student['firstname'] ?? '') . ' ' . ($student['lastname'] ?? '')), 'age' => $student['age'] ?? 'N/A', 'email' => $emergencyInfo['email'] ?? '', 'phone' => $emergencyInfo['cellphone'] ?? '', @@ -147,10 +149,9 @@ class StudentDirectoryService $names = $classMap[$studentId] ?? []; $names = array_values(array_unique(array_filter($names))); $student['class_sections'] = $names; - $student['class_section_name'] = ! empty($names) ? implode(', ', $names) : 'No class assigned'; + $student['class_section_name'] = !empty($names) ? implode(', ', $names) : 'No class assigned'; } unset($student); - return $students; }; @@ -187,7 +188,7 @@ class StudentDirectoryService 'school_year', ]) ->when($selectedYear !== '', fn ($q) => $q->where('school_year', $selectedYear)) - ->when(! empty($parentIds), fn ($q) => $q->whereIn('parent_id', $parentIds)) + ->when(!empty($parentIds), fn ($q) => $q->whereIn('parent_id', $parentIds)) ->orderBy('lastname') ->orderBy('firstname') ->get() diff --git a/app/Services/Students/StudentProfileService.php b/app/Services/Students/StudentProfileService.php index c5495f62..975e895a 100644 --- a/app/Services/Students/StudentProfileService.php +++ b/app/Services/Students/StudentProfileService.php @@ -12,7 +12,7 @@ class StudentProfileService public function updateStudent(int $studentId, array $payload): array { $student = Student::query()->find($studentId); - if (! $student) { + if (!$student) { return ['ok' => false, 'message' => 'Student not found.']; } @@ -43,7 +43,7 @@ class StudentProfileService $shouldSyncAllergies = array_key_exists('allergies', $payload); DB::transaction(function () use ($student, $studentData, $conditions, $allergies): void { - if (! empty($studentData)) { + if (!empty($studentData)) { $student->update($studentData); } @@ -84,7 +84,7 @@ class StudentProfileService private function syncHealthList(int $studentId, string $modelClass, string $field, array $newValues): void { - $model = new $modelClass; + $model = new $modelClass(); $rows = $model->newQuery()->where('student_id', $studentId)->get(['id', $field])->toArray(); $current = []; @@ -111,14 +111,14 @@ class StudentProfileService $toDeleteKeys = array_diff(array_keys($current), array_keys($incoming)); $toInsertKeys = array_diff(array_keys($incoming), array_keys($current)); - if (! empty($toDeleteKeys)) { + if (!empty($toDeleteKeys)) { $ids = array_map(fn ($key) => $byId[$key], $toDeleteKeys); - if (! empty($ids)) { + if (!empty($ids)) { $model->newQuery()->whereIn('id', $ids)->delete(); } } - if (! empty($toInsertKeys)) { + if (!empty($toInsertKeys)) { $batch = []; foreach ($toInsertKeys as $key) { $batch[] = [ @@ -126,7 +126,7 @@ class StudentProfileService $field => $incoming[$key], ]; } - if (! empty($batch)) { + if (!empty($batch)) { $model->newQuery()->insert($batch); } } diff --git a/app/Services/Students/StudentScoreCardService.php b/app/Services/Students/StudentScoreCardService.php index 1400758e..96379ae7 100644 --- a/app/Services/Students/StudentScoreCardService.php +++ b/app/Services/Students/StudentScoreCardService.php @@ -146,7 +146,7 @@ class StudentScoreCardService ->where('id', $studentId) ->first(); - if (! $student) { + if (!$student) { return ['ok' => false, 'message' => 'Student not found.']; } @@ -177,7 +177,6 @@ class StudentScoreCardService if ($ay !== $by) { return strcmp($by, $ay); } - return strcmp((string) ($a['semester'] ?? ''), (string) ($b['semester'] ?? '')); }); @@ -185,7 +184,7 @@ class StudentScoreCardService foreach ($rows as $row) { $year = (string) ($row['school_year'] ?? ''); $score = $row['semester_score'] ?? null; - if ($year === '' || ! is_numeric($score)) { + if ($year === '' || !is_numeric($score)) { continue; } $yearScoreMap[$year][] = (float) $score; @@ -225,14 +224,14 @@ class StudentScoreCardService if ($commentVal === '') { continue; } - $key = $year.'|'.$semester; + $key = $year . '|' . $semester; $commentMap[$key][$type] = $commentVal; } foreach ($rows as &$row) { $year = (string) ($row['school_year'] ?? ''); $semester = strtolower(trim((string) ($row['semester'] ?? ''))); - $row['comments'] = $commentMap[$year.'|'.$semester] ?? []; + $row['comments'] = $commentMap[$year . '|' . $semester] ?? []; $row['year_score'] = $yearAvg[$year] ?? null; } unset($row); diff --git a/app/Services/Students/StudentStatusService.php b/app/Services/Students/StudentStatusService.php index 51faa77e..86b272f6 100644 --- a/app/Services/Students/StudentStatusService.php +++ b/app/Services/Students/StudentStatusService.php @@ -8,7 +8,9 @@ use App\Models\StudentClass; class StudentStatusService { - public function __construct(private StudentConfigService $configService) {} + public function __construct(private StudentConfigService $configService) + { + } public function setActive(int $studentId, bool $isActive, ?int $userId = null): array { @@ -17,7 +19,7 @@ class StudentStatusService $schoolYear = (string) ($context['school_year'] ?? ''); $student = Student::query()->find($studentId); - if (! $student) { + if (!$student) { return ['ok' => false, 'message' => 'Student not found.']; } @@ -34,7 +36,7 @@ class StudentStatusService ->when($schoolYear !== '', fn ($q) => $q->where('school_year', $schoolYear)) ->first(); - if (! $hasCurrentClass) { + if (!$hasCurrentClass) { $lastClass = StudentClass::query() ->where('student_id', $studentId) ->orderByDesc('updated_at') @@ -57,7 +59,7 @@ class StudentStatusService $classLabel = ClassSection::getClassSectionNameBySectionId($restoreClassId); if ($classLabel) { - $message .= ' Class assignment restored to '.$classLabel.'.'; + $message .= ' Class assignment restored to ' . $classLabel . '.'; } } else { $message .= ' No class assignment found for the current term.'; diff --git a/app/Services/Subjects/SubjectCurriculumService.php b/app/Services/Subjects/SubjectCurriculumService.php index 4b4f9f88..232c2748 100644 --- a/app/Services/Subjects/SubjectCurriculumService.php +++ b/app/Services/Subjects/SubjectCurriculumService.php @@ -80,7 +80,7 @@ class SubjectCurriculumService public function update(int $id, array $payload): ?SubjectCurriculum { $entry = SubjectCurriculum::query()->find($id); - if (! $entry) { + if (!$entry) { return null; } @@ -104,7 +104,7 @@ class SubjectCurriculumService public function delete(int $id): bool { $entry = SubjectCurriculum::query()->find($id); - if (! $entry) { + if (!$entry) { return false; } diff --git a/app/Services/Support/ContactMessageService.php b/app/Services/Support/ContactMessageService.php index 6a205c05..4ca3882d 100644 --- a/app/Services/Support/ContactMessageService.php +++ b/app/Services/Support/ContactMessageService.php @@ -7,7 +7,9 @@ use Illuminate\Support\Facades\Log; class ContactMessageService { - public function __construct(private EmailDispatchService $emailService) {} + public function __construct(private EmailDispatchService $emailService) + { + } public function send(array $payload): array { @@ -18,17 +20,17 @@ class ContactMessageService $formatted = "

From: {$name} ({$email})

"; $formatted .= "

Subject: {$subject}

"; - $formatted .= '

'.nl2br($message).'

'; + $formatted .= '

' . nl2br($message) . '

'; $recipient = (string) config('support.contact_email', 'support@alrahmaisgl.org'); $emailSent = false; - if (! app()->runningUnitTests()) { + if (!app()->runningUnitTests()) { $emailSent = $this->emailService->send($recipient, $subject, $formatted, null, $email, $name); } - if (! $emailSent) { - Log::warning('Contact email failed for '.$email); + if (!$emailSent) { + Log::warning('Contact email failed for ' . $email); } return [ diff --git a/app/Services/Support/SupportRequestService.php b/app/Services/Support/SupportRequestService.php index e5bafee0..a70e90cf 100644 --- a/app/Services/Support/SupportRequestService.php +++ b/app/Services/Support/SupportRequestService.php @@ -14,19 +14,20 @@ class SupportRequestService public function __construct( private GlobalConfigService $configService, private EmailDispatchService $emailService - ) {} + ) { + } public function list(int $userId, array $filters, int $page, int $perPage, bool $asAdmin = false): LengthAwarePaginator { $query = SupportRequest::query()->with('user'); - if (! $asAdmin) { + if (!$asAdmin) { $query->where('user_id', $userId); - } elseif (! empty($filters['user_id'])) { + } elseif (!empty($filters['user_id'])) { $query->where('user_id', (int) $filters['user_id']); } - if (! empty($filters['status'])) { + if (!empty($filters['status'])) { $query->where('status', $filters['status']); } @@ -54,7 +55,7 @@ class SupportRequestService $recipient = (string) config('support.contact_email', 'support@alrahmaisgl.org'); $emailSent = false; - if (! app()->runningUnitTests()) { + if (!app()->runningUnitTests()) { $emailSent = $this->emailService->send( $recipient, (string) $payload['subject'], @@ -65,8 +66,8 @@ class SupportRequestService ); } - if (! $emailSent) { - Log::warning('Support email failed for user '.$userId); + if (!$emailSent) { + Log::warning('Support email failed for user ' . $userId); } return [ diff --git a/app/Services/System/CleanupSchedulerService.php b/app/Services/System/CleanupSchedulerService.php index 18d616bd..31e299e7 100644 --- a/app/Services/System/CleanupSchedulerService.php +++ b/app/Services/System/CleanupSchedulerService.php @@ -10,7 +10,7 @@ class CleanupSchedulerService public function shouldRun(string $cacheKey, int $minutes): bool { $lastRun = Cache::get($cacheKey); - if (! $lastRun) { + if (!$lastRun) { return true; } diff --git a/app/Services/System/ConfigUpdateService.php b/app/Services/System/ConfigUpdateService.php index 8aaa3647..163abd26 100644 --- a/app/Services/System/ConfigUpdateService.php +++ b/app/Services/System/ConfigUpdateService.php @@ -24,34 +24,31 @@ class ConfigUpdateService public function runTask(string $task, bool $dry, bool $force, DateTimeZone $tz): array { - if (! isset($this->tasks[$task])) { + if (!isset($this->tasks[$task])) { return ['ok' => false, 'message' => 'Unknown task.']; } - $lockFile = storage_path('app/locks/config_update_'.$task.'.lock'); - if (! is_dir(dirname($lockFile))) { + $lockFile = storage_path('app/locks/config_update_' . $task . '.lock'); + if (!is_dir(dirname($lockFile))) { @mkdir(dirname($lockFile), 0775, true); } $fp = @fopen($lockFile, 'c+'); - if (! $fp) { + if (!$fp) { return ['ok' => false, 'message' => 'Unable to open lock file.']; } - if (! $force && ! flock($fp, LOCK_EX | LOCK_NB)) { + if (!$force && !flock($fp, LOCK_EX | LOCK_NB)) { fclose($fp); - return ['ok' => false, 'message' => 'Task is already running.']; } try { $method = $this->tasks[$task]; $ok = (bool) $this->{$method}($dry, $force, $tz); - return ['ok' => $ok, 'task' => $task]; } catch (\Throwable $e) { - Log::error('ConfigUpdate task failed: '.$e->getMessage()); - + Log::error('ConfigUpdate task failed: ' . $e->getMessage()); return ['ok' => false, 'message' => $e->getMessage()]; } finally { try { @@ -77,12 +74,11 @@ class ConfigUpdateService { $now = new DateTimeImmutable('now', $tz); $isJune1 = $now->format('n-j') === '6-1'; - if (! $isJune1 && ! $force) { + if (!$isJune1 && !$force) { return true; } - $value = $now->format('Y').'-12-31'; - + $value = $now->format('Y') . '-12-31'; return $dry ? true : Configuration::setConfigValueByKey('date_age_reference', $value); } @@ -90,7 +86,7 @@ class ConfigUpdateService { $now = new DateTimeImmutable('now', $tz); $isFeb1 = $now->format('n-j') === '2-1'; - if (! $isFeb1 && ! $force) { + if (!$isFeb1 && !$force) { return true; } @@ -101,7 +97,7 @@ class ConfigUpdateService { $now = new DateTimeImmutable('now', $tz); $isJun1 = $now->format('n-j') === '6-1'; - if (! $isJun1 && ! $force) { + if (!$isJun1 && !$force) { return true; } diff --git a/app/Services/System/HealthCheckService.php b/app/Services/System/HealthCheckService.php index c5e32806..de744997 100644 --- a/app/Services/System/HealthCheckService.php +++ b/app/Services/System/HealthCheckService.php @@ -12,10 +12,10 @@ class HealthCheckService $uploadsBase = storage_path('app/uploads'); $paths = [ 'uploads' => $uploadsBase, - 'uploads/reimbursements' => $uploadsBase.DIRECTORY_SEPARATOR.'reimbursements', - 'uploads/receipts' => $uploadsBase.DIRECTORY_SEPARATOR.'receipts', - 'uploads/checks' => $uploadsBase.DIRECTORY_SEPARATOR.'checks', - 'uploads/early_dismissal_signatures' => $uploadsBase.DIRECTORY_SEPARATOR.'early_dismissal_signatures', + 'uploads/reimbursements' => $uploadsBase . DIRECTORY_SEPARATOR . 'reimbursements', + 'uploads/receipts' => $uploadsBase . DIRECTORY_SEPARATOR . 'receipts', + 'uploads/checks' => $uploadsBase . DIRECTORY_SEPARATOR . 'checks', + 'uploads/early_dismissal_signatures' => $uploadsBase . DIRECTORY_SEPARATOR . 'early_dismissal_signatures', ]; $pathsStatus = []; @@ -48,10 +48,10 @@ class HealthCheckService }, true); $okDb = true; - if ($dbChecks['user_preferences_exists'] && ! $dbChecks['user_preferences_has_timezone']) { + if ($dbChecks['user_preferences_exists'] && !$dbChecks['user_preferences_has_timezone']) { $okDb = false; } - if ($dbChecks['settings_exists'] && ! $dbChecks['settings_has_timezone']) { + if ($dbChecks['settings_exists'] && !$dbChecks['settings_has_timezone']) { $okDb = false; } diff --git a/app/Services/System/TimeService.php b/app/Services/System/TimeService.php index 62eae8f6..2a90a6ed 100644 --- a/app/Services/System/TimeService.php +++ b/app/Services/System/TimeService.php @@ -50,7 +50,7 @@ class TimeService public function toUtc($value, ?string $fromTz = null, string $format = 'Y-m-d H:i:s'): ?string { $dt = $this->parse($value, $fromTz); - if (! $dt) { + if (!$dt) { return null; } @@ -60,7 +60,7 @@ class TimeService public function toLocal($value, ?string $sourceTz = null, ?string $targetTz = null, string $format = 'Y-m-d H:i:s'): ?string { $dt = $this->parse($value, $sourceTz); - if (! $dt) { + if (!$dt) { return null; } @@ -70,7 +70,7 @@ class TimeService public function formatLocal($value, string $format = 'Y-m-d', ?string $sourceTz = null): string { $dt = $this->parse($value, $sourceTz); - if (! $dt) { + if (!$dt) { return ''; } diff --git a/app/Services/Teachers/TeacherAbsenceService.php b/app/Services/Teachers/TeacherAbsenceService.php index cc96011d..43817f7c 100644 --- a/app/Services/Teachers/TeacherAbsenceService.php +++ b/app/Services/Teachers/TeacherAbsenceService.php @@ -2,12 +2,12 @@ namespace App\Services\Teachers; +use App\Services\ApplicationUrlService; +use App\Services\Staff\StaffTimeOffLinkService; use App\Models\StaffAttendance; use App\Models\TeacherClass; use App\Models\User; -use App\Services\ApplicationUrlService; use App\Services\SemesterRangeService; -use App\Services\Staff\StaffTimeOffLinkService; use Carbon\Carbon; use Illuminate\Support\Facades\Mail; @@ -18,7 +18,8 @@ class TeacherAbsenceService private SemesterRangeService $semesterRangeService, private StaffTimeOffLinkService $staffTimeOffLinkService, private ApplicationUrlService $urls, - ) {} + ) { + } public function formData(int $userId): array { @@ -28,7 +29,7 @@ class TeacherAbsenceService $teacher = User::query()->find($userId); $displayName = $teacher - ? trim(($teacher->firstname ?? '').' '.($teacher->lastname ?? '')) + ? trim(($teacher->firstname ?? '') . ' ' . ($teacher->lastname ?? '')) : 'Teacher'; $existing = StaffAttendance::query() @@ -75,7 +76,7 @@ class TeacherAbsenceService } $reasonBase = $reasonType !== '' ? strtolower($reasonType) : ''; - $reason = $reasonBase !== '' ? ($reasonBase.': '.$reasonText) : $reasonText; + $reason = $reasonBase !== '' ? ($reasonBase . ': ' . $reasonText) : $reasonText; $allowedSet = array_fill_keys($this->allowedAbsenceDates($schoolYear), true); $roleName = User::getUserRoleName($userId) ?: null; @@ -96,12 +97,10 @@ class TeacherAbsenceService $parsed = Carbon::createFromFormat('Y-m-d', $date); if ($parsed->format('Y-m-d') !== $date || empty($allowedSet[$date])) { $invalid[] = $date; - continue; } } catch (\Throwable) { $invalid[] = $date; - continue; } @@ -127,10 +126,10 @@ class TeacherAbsenceService } } - if (! empty($invalid)) { + if (!empty($invalid)) { return [ 'ok' => false, - 'message' => 'Invalid dates: '.implode(', ', $invalid), + 'message' => 'Invalid dates: ' . implode(', ', $invalid), 'status' => 422, ]; } @@ -148,7 +147,7 @@ class TeacherAbsenceService return [ 'ok' => true, - 'message' => $saved.' day(s) saved as absent.', + 'message' => $saved . ' day(s) saved as absent.', 'saved' => $saved, 'dates' => $savedDates, 'status' => 200, @@ -212,10 +211,10 @@ class TeacherAbsenceService ): void { try { $user = User::query()->find($userId); - $fullName = trim(($user->firstname ?? '').' '.($user->lastname ?? '')) ?: 'Teacher'; + $fullName = trim(($user->firstname ?? '') . ' ' . ($user->lastname ?? '')) ?: 'Teacher'; $userEmail = $user->email ?? ''; $role = $roleName ?: 'teacher'; - $dateList = ! empty($savedDates) ? implode(', ', $savedDates) : implode(', ', $dates); + $dateList = !empty($savedDates) ? implode(', ', $savedDates) : implode(', ', $dates); $subject = sprintf( 'TimeOff Request: %s (%s) — %s', @@ -228,21 +227,21 @@ class TeacherAbsenceService $assignedText = $this->formatAssignedClasses($userId, $schoolYear, $semester); $body = '
' - .'

New TimeOff Request

' - .'

A staff time-off request was submitted from the teacher portal.

' - .'' - .'' - .'' - .'' - .'' - .'' - .'' - .'' - .'' - .'' - .'' - .'
Name'.e($fullName).'
Email'.e($userEmail).'
Role'.e((string) $role).'
Semester'.e($semester).'
School Year'.e($schoolYear).'
Reason Type'.e($reasonType ?: '-').'
Reason'.e($reasonText).'
Dates'.e($dateList ?: '-').'
Assigned Class(es)'.e($assignedText).'
Submitted At'.e($submittedAt).'
' - .'
'; + . '

New TimeOff Request

' + . '

A staff time-off request was submitted from the teacher portal.

' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '' + . '
Name' . e($fullName) . '
Email' . e($userEmail) . '
Role' . e((string) $role) . '
Semester' . e($semester) . '
School Year' . e($schoolYear) . '
Reason Type' . e($reasonType ?: '-') . '
Reason' . e($reasonText) . '
Dates' . e($dateList ?: '-') . '
Assigned Class(es)' . e($assignedText) . '
Submitted At' . e($submittedAt) . '
' + . ''; $token = $this->staffTimeOffLinkService->createToken([ 'uid' => $userId, @@ -257,9 +256,9 @@ class TeacherAbsenceService ]); $notifyUrl = $this->urls->timeoffNotifyUrl($token); - $body .= '

Click Send confirmation email to ' - .e($fullName) - .' so the staff member is notified automatically. This link expires in 14 days.

'; + $body .= '

Click Send confirmation email to ' + . e($fullName) + . ' so the staff member is notified automatically. This link expires in 14 days.

'; $principalEmail = env('PRINCIPAL_EMAIL', 'principal@alrahmaisgl.org'); @@ -284,10 +283,10 @@ class TeacherAbsenceService $name = (string) ($assignment['class_section_name'] ?? ''); $role = (string) ($assignment['teacher_role'] ?? ''); if ($name !== '') { - $parts[] = $role !== '' ? ($name.' ('.$role.')') : $name; + $parts[] = $role !== '' ? ($name . ' (' . $role . ')') : $name; } } - return ! empty($parts) ? implode(', ', $parts) : 'No assigned class'; + return !empty($parts) ? implode(', ', $parts) : 'No assigned class'; } } diff --git a/app/Services/Teachers/TeacherAssignmentService.php b/app/Services/Teachers/TeacherAssignmentService.php index d5890e14..91595428 100644 --- a/app/Services/Teachers/TeacherAssignmentService.php +++ b/app/Services/Teachers/TeacherAssignmentService.php @@ -8,7 +8,9 @@ use App\Models\User; class TeacherAssignmentService { - public function __construct(private TeacherConfigService $configService) {} + public function __construct(private TeacherConfigService $configService) + { + } public function listAssignments(?string $schoolYear = null): array { @@ -51,7 +53,7 @@ class TeacherAssignmentService } $role = strtolower((string) ($assign['position'] ?? '')); - if (! in_array($role, ['main', 'ta'], true)) { + if (!in_array($role, ['main', 'ta'], true)) { continue; } @@ -71,9 +73,9 @@ class TeacherAssignmentService continue; } $assigned = $assignmentMap[$tid] ?? ['main' => [], 'ta' => []]; - $name = trim(($teacher['firstname'] ?? '').' '.($teacher['lastname'] ?? '')); + $name = trim(($teacher['firstname'] ?? '') . ' ' . ($teacher['lastname'] ?? '')); if ($name === '') { - $name = 'User#'.$tid; + $name = 'User#' . $tid; } $teacherData[] = [ @@ -168,7 +170,7 @@ class TeacherAssignmentService $semester = trim((string) ($input['semester'] ?? $context['semester'] ?? '')); $schoolYear = trim((string) ($input['school_year'] ?? $context['school_year'] ?? '')); - if ($teacherId <= 0 || $classSectionId <= 0 || ! in_array($position, ['main', 'ta'], true) || $schoolYear === '' || $semester === '') { + if ($teacherId <= 0 || $classSectionId <= 0 || !in_array($position, ['main', 'ta'], true) || $schoolYear === '' || $semester === '') { return ['ok' => false, 'message' => 'Missing or invalid data for deletion.']; } @@ -180,12 +182,12 @@ class TeacherAssignmentService ->where('position', $position) ->first(); - if (! $assignment) { + if (!$assignment) { return ['ok' => false, 'message' => 'Assignment not found.']; } $assignment->delete(); - return ['ok' => true, 'message' => ucfirst($position).' assignment removed successfully.']; + return ['ok' => true, 'message' => ucfirst($position) . ' assignment removed successfully.']; } } diff --git a/app/Services/Teachers/TeacherDashboardService.php b/app/Services/Teachers/TeacherDashboardService.php index fd677060..ee297c0d 100644 --- a/app/Services/Teachers/TeacherDashboardService.php +++ b/app/Services/Teachers/TeacherDashboardService.php @@ -11,7 +11,9 @@ use Illuminate\Support\Facades\DB; class TeacherDashboardService { - public function __construct(private TeacherConfigService $configService) {} + public function __construct(private TeacherConfigService $configService) + { + } public function classView(int $userId, ?int $requestedClassSectionId = null): array { @@ -27,7 +29,7 @@ class TeacherDashboardService ->map(fn ($r) => strtolower((string) $r)) ->all(); - if (! array_intersect($roles, ['teacher', 'teacher_assistant'])) { + if (!array_intersect($roles, ['teacher', 'teacher_assistant'])) { throw new \RuntimeException('Access denied.'); } @@ -48,24 +50,24 @@ class TeacherDashboardService $activeClassSectionId = null; if ($requestedClassSectionId && in_array($requestedClassSectionId, $classSectionIds, true)) { $activeClassSectionId = $requestedClassSectionId; - } elseif (! empty($classSectionIds)) { + } elseif (!empty($classSectionIds)) { $activeClassSectionId = $classSectionIds[0]; } $studentsBySection = []; - if (! empty($classSectionIds)) { + if (!empty($classSectionIds)) { $students = StudentClass::getStudentsByClassSectionIds($classSectionIds); - if (! empty($students)) { + if (!empty($students)) { $studentIds = array_values(array_unique(array_map( static fn (array $row) => (int) ($row['student_id'] ?? 0), $students ))); $studentIds = array_values(array_filter($studentIds)); - $conditionsByStudent = ! empty($studentIds) + $conditionsByStudent = !empty($studentIds) ? StudentMedicalCondition::getMedicalByStudentIds($studentIds) : []; - $allergiesByStudent = ! empty($studentIds) + $allergiesByStudent = !empty($studentIds) ? StudentAllergy::getAllergiesByStudentIds($studentIds) : []; @@ -87,7 +89,7 @@ class TeacherDashboardService } $assignedNames = []; - if (! empty($classSectionIds)) { + if (!empty($classSectionIds)) { $rows = DB::table('teacher_class as tc') ->select('tc.class_section_id', 'tc.position', 'u.firstname', 'u.lastname') ->join('users as u', 'u.id', '=', 'tc.teacher_id') @@ -101,7 +103,7 @@ class TeacherDashboardService foreach ($rows as $row) { $sid = (int) ($row->class_section_id ?? 0); $pos = strtolower((string) ($row->position ?? '')); - $name = trim(($row->firstname ?? '').' '.($row->lastname ?? '')); + $name = trim(($row->firstname ?? '') . ' ' . ($row->lastname ?? '')); if ($sid === 0 || $name === '') { continue; } diff --git a/app/Services/Ui/UiStyleService.php b/app/Services/Ui/UiStyleService.php index ddbff6fc..76483dcf 100644 --- a/app/Services/Ui/UiStyleService.php +++ b/app/Services/Ui/UiStyleService.php @@ -13,12 +13,10 @@ class UiStyleService $prefs = Preferences::query()->where('user_id', $userId)->first(); if ($prefs) { $prefs->fill($updates)->save(); - return $prefs->fresh(); } $updates['user_id'] = $userId; - return Preferences::query()->create($updates); } @@ -61,7 +59,6 @@ class UiStyleService private function sanitizePaletteKey(?string $value): ?string { $value = trim((string) $value); - return $value === '' ? null : $value; } @@ -71,7 +68,6 @@ class UiStyleService if (in_array($mode, ['light', 'dark', 'auto'], true)) { return $mode; } - return 'auto'; } @@ -82,19 +78,17 @@ class UiStyleService return null; } if ($h[0] !== '#') { - $h = '#'.$h; + $h = '#' . $h; } if (preg_match('/^#([0-9a-fA-F]{3})$/', $h, $m)) { $r = $m[1][0]; $g = $m[1][1]; $b = $m[1][2]; - - return strtoupper('#'.$r.$r.$g.$g.$b.$b); + return strtoupper('#' . $r . $r . $g . $g . $b . $b); } if (preg_match('/^#([0-9a-fA-F]{6})$/', $h)) { return strtoupper($h); } - return null; } } diff --git a/app/Services/Users/DeleteUnverifiedUserService.php b/app/Services/Users/DeleteUnverifiedUserService.php index 08558522..6070640f 100644 --- a/app/Services/Users/DeleteUnverifiedUserService.php +++ b/app/Services/Users/DeleteUnverifiedUserService.php @@ -9,12 +9,14 @@ use Illuminate\Support\Facades\Log; class DeleteUnverifiedUserService { - public function __construct(private AccountEventService $accountEvents) {} + public function __construct(private AccountEventService $accountEvents) + { + } public function deleteAfterTimeout(int $userId, int $timeoutSeconds = 86400): array { $user = User::query()->find($userId); - if (! $user) { + if (!$user) { return ['ok' => false, 'reason' => 'not_found']; } @@ -23,7 +25,7 @@ class DeleteUnverifiedUserService } $lastActivity = $user->updated_at ?? $user->created_at; - if (! $lastActivity) { + if (!$lastActivity) { return ['ok' => false, 'reason' => 'no_timestamp']; } diff --git a/app/Services/Users/InactiveUserCleanupService.php b/app/Services/Users/InactiveUserCleanupService.php index 335b78c6..ccbbac8d 100644 --- a/app/Services/Users/InactiveUserCleanupService.php +++ b/app/Services/Users/InactiveUserCleanupService.php @@ -21,7 +21,6 @@ class InactiveUserCleanupService if (empty($users)) { $orphans = $this->purgeOrphanedUserRoles(); - return [ 'deleted_users' => 0, 'deleted_parents' => 0, diff --git a/app/Services/Users/UserEventService.php b/app/Services/Users/UserEventService.php index 8c8a1cb6..d3c706ef 100644 --- a/app/Services/Users/UserEventService.php +++ b/app/Services/Users/UserEventService.php @@ -7,23 +7,24 @@ use Illuminate\Support\Facades\Log; class UserEventService { - public function __construct(private EmailService $emailService) {} + public function __construct(private EmailService $emailService) + { + } public function handleNewAccountAdded(array $userData): bool { $email = (string) ($userData['email'] ?? ''); if ($email === '') { Log::warning('UserEventService: missing email for welcome message.'); - return false; } - $name = trim((string) (($userData['firstname'] ?? '').' '.($userData['lastname'] ?? ''))); + $name = trim((string) (($userData['firstname'] ?? '') . ' ' . ($userData['lastname'] ?? ''))); $safeName = e($name !== '' ? $name : 'there'); $subject = 'Welcome to Al Rahma Sunday School!'; - $message = '

Hello '.$safeName.',

' - .'

Your account has been created. Welcome to Al Rahma Sunday School.

'; + $message = '

Hello ' . $safeName . ',

' + . '

Your account has been created. Welcome to Al Rahma Sunday School.

'; return $this->emailService->send($email, $subject, $message, 'registration'); } diff --git a/app/Services/Users/UserListService.php b/app/Services/Users/UserListService.php index b3490b88..918773cc 100644 --- a/app/Services/Users/UserListService.php +++ b/app/Services/Users/UserListService.php @@ -40,11 +40,11 @@ class UserListService $parentType = $guardianMap[$userId] ?? ''; $isSecond = false; - if ($userId > 0 && ! empty($secondByUserId[$userId])) { + if ($userId > 0 && !empty($secondByUserId[$userId])) { $isSecond = true; } else { $email = trim(strtolower((string) $user->email)); - if ($email !== '' && ! empty($secondByEmail[$email])) { + if ($email !== '' && !empty($secondByEmail[$email])) { $isSecond = true; } } @@ -91,7 +91,7 @@ class UserListService private function guardianTypeMap(): array { - if (! Schema::hasTable('family_guardians')) { + if (!Schema::hasTable('family_guardians')) { return []; } @@ -119,7 +119,7 @@ class UserListService private function secondParentMaps(): array { - if (! Schema::hasTable('parents')) { + if (!Schema::hasTable('parents')) { return [[], []]; } diff --git a/app/Services/Users/UserManagementService.php b/app/Services/Users/UserManagementService.php index f7cdb33a..2c3911c2 100644 --- a/app/Services/Users/UserManagementService.php +++ b/app/Services/Users/UserManagementService.php @@ -12,7 +12,9 @@ use Illuminate\Support\Facades\Validator; class UserManagementService { - public function __construct(private UserEventService $eventService) {} + public function __construct(private UserEventService $eventService) + { + } public function create(array $payload, ?int $actorId = null): array { @@ -43,7 +45,7 @@ class UserManagementService public function update(int $userId, array $payload): array { $user = User::query()->find($userId); - if (! $user) { + if (!$user) { return ['ok' => false, 'message' => 'User not found.']; } @@ -65,7 +67,7 @@ class UserManagementService public function delete(int $userId): array { $user = User::query()->find($userId); - if (! $user) { + if (!$user) { return ['ok' => false, 'message' => 'User not found.']; } @@ -85,11 +87,11 @@ class UserManagementService { $actorId = $actorId ?? (auth()->id() ?? null); - if (! User::query()->whereKey($userId)->exists()) { + if (!User::query()->whereKey($userId)->exists()) { return false; } - if (! Role::query()->whereKey($roleId)->exists()) { + if (!Role::query()->whereKey($roleId)->exists()) { return false; } @@ -131,10 +133,10 @@ class UserManagementService 'accept_school_policy' => ['required'], ]; - $isParent = ! empty($data['is_parent']); - $noSecondParent = ! empty($data['no_second_parent_info']); + $isParent = !empty($data['is_parent']); + $noSecondParent = !empty($data['no_second_parent_info']); - if ($isParent && ! $noSecondParent) { + if ($isParent && !$noSecondParent) { $rules += [ 'second_firstname' => ['required', 'regex:/^[a-zA-Z\\s\\-]+$/', 'min:2', 'max:30'], 'second_lastname' => ['required', 'regex:/^[a-zA-Z\\s\\-]+$/', 'min:2', 'max:30'], @@ -213,7 +215,7 @@ class UserManagementService ]; foreach ($map as $field) { - if (! array_key_exists($field, $payload)) { + if (!array_key_exists($field, $payload)) { continue; } @@ -263,7 +265,6 @@ class UserManagementService if ($value === 'female') { return 'Female'; } - return null; } @@ -273,7 +274,6 @@ class UserManagementService if ($value === '') { return ''; } - return ucfirst($value); } diff --git a/app/Services/Whatsapp/WhatsappContactService.php b/app/Services/Whatsapp/WhatsappContactService.php index 9ee94188..5075d8ab 100644 --- a/app/Services/Whatsapp/WhatsappContactService.php +++ b/app/Services/Whatsapp/WhatsappContactService.php @@ -8,7 +8,9 @@ use Illuminate\Support\Facades\Log; class WhatsappContactService { - public function __construct(private WhatsappContextService $context) {} + public function __construct(private WhatsappContextService $context) + { + } public function listParentContacts(?string $schoolYear, ?string $semester): array { @@ -16,7 +18,7 @@ class WhatsappContactService $semester = trim((string) $semester); $builder = DB::table('parents as sp'); - $builder->select(' + $builder->select(" sp.id AS parents_row_id, sp.firstparent_id AS firstparent_id, sp.secondparent_firstname AS sp_firstname, @@ -32,7 +34,7 @@ class WhatsappContactService u.cellphone AS u_phone, u.school_year AS u_school_year, u.semester AS u_semester - '); + "); $builder->leftJoin('users as u', 'u.id', '=', 'sp.firstparent_id'); if ($schoolYear !== '') { @@ -47,7 +49,7 @@ class WhatsappContactService $contacts = []; foreach ($rows as $row) { $r = (array) $row; - if (! empty($r['u_id'])) { + if (!empty($r['u_id'])) { $contacts[] = [ 'firstname' => (string) ($r['u_firstname'] ?? ''), 'lastname' => (string) ($r['u_lastname'] ?? ''), @@ -57,7 +59,7 @@ class WhatsappContactService ]; } - $hasSecond = ! empty($r['sp_firstname']) || ! empty($r['sp_lastname']) || ! empty($r['sp_email']) || ! empty($r['sp_phone']); + $hasSecond = !empty($r['sp_firstname']) || !empty($r['sp_lastname']) || !empty($r['sp_email']) || !empty($r['sp_phone']); if ($hasSecond) { $contacts[] = [ 'firstname' => (string) ($r['sp_firstname'] ?? ''), @@ -70,9 +72,8 @@ class WhatsappContactService } $contacts = array_values(array_filter($contacts, function ($c) { - $name = trim(($c['firstname'] ?? '').($c['lastname'] ?? '')); - $contact = trim(($c['phone'] ?? '').($c['email'] ?? '')); - + $name = trim(($c['firstname'] ?? '') . ($c['lastname'] ?? '')); + $contact = trim(($c['phone'] ?? '') . ($c['email'] ?? '')); return $name !== '' || $contact !== ''; })); @@ -89,7 +90,6 @@ class WhatsappContactService } $ta = ($a['type'] ?? '') === 'Primary Parent' ? 0 : 1; $tb = ($b['type'] ?? '') === 'Primary Parent' ? 0 : 1; - return $ta <=> $tb; }); @@ -115,7 +115,7 @@ class WhatsappContactService )))); $namesById = []; - if (! empty($sectionIds)) { + if (!empty($sectionIds)) { $nameRows = DB::table('classSection') ->select('class_section_id', 'class_section_name') ->whereIn('class_section_id', $sectionIds) @@ -130,13 +130,13 @@ class WhatsappContactService $classSections = []; foreach ($sections as $s) { $sid = (int) ($s->class_section_id ?? 0); - if (! $sid) { + if (!$sid) { continue; } $classSections[$sid] = [ 'class_section_id' => $sid, - 'class_section_name' => $namesById[$sid] ?? ('Section '.$sid), + 'class_section_name' => $namesById[$sid] ?? ('Section ' . $sid), 'semester' => $semester, 'school_year' => (string) ($s->school_year ?? ''), 'description' => '', @@ -178,11 +178,11 @@ class WhatsappContactService $r = (array) $row; $sid = (int) ($r['class_section_id'] ?? 0); $pid = (int) ($r['primary_id'] ?? 0); - if (! $sid || ! $pid || ! isset($classSections[$sid])) { + if (!$sid || !$pid || !isset($classSections[$sid])) { continue; } - $key = $sid.':'.$pid; + $key = $sid . ':' . $pid; if (isset($seen[$key])) { continue; } @@ -191,11 +191,11 @@ class WhatsappContactService $classSections[$sid]['parents'][] = [ 'class_section_id' => $sid, 'primary_id' => $pid, - 'primary_name' => trim(($r['primary_lastname'] ?? '').', '.($r['primary_firstname'] ?? '')), + 'primary_name' => trim(($r['primary_lastname'] ?? '') . ', ' . ($r['primary_firstname'] ?? '')), 'primary_phone' => (string) ($r['primary_phone'] ?? ''), 'primary_email' => (string) ($r['primary_email'] ?? ''), 'second_id' => (int) ($r['second_id'] ?? 0), - 'second_name' => trim(($r['second_lastname'] ?? '').', '.($r['second_firstname'] ?? '')), + 'second_name' => trim(($r['second_lastname'] ?? '') . ', ' . ($r['second_firstname'] ?? '')), 'second_phone' => (string) ($r['second_phone'] ?? ''), 'second_email' => (string) ($r['second_email'] ?? ''), 'primary_member' => null, @@ -225,7 +225,7 @@ class WhatsappContactService } unset($section); } catch (\Throwable $e) { - Log::warning('WhatsApp membership lookup failed: '.$e->getMessage()); + Log::warning('WhatsApp membership lookup failed: ' . $e->getMessage()); } try { @@ -239,10 +239,10 @@ class WhatsappContactService foreach ($teachers as $row) { $r = (array) $row; $sid = (int) ($r['class_section_id'] ?? 0); - if (! isset($classSections[$sid])) { + if (!isset($classSections[$sid])) { continue; } - $name = trim(($r['lastname'] ?? '').', '.($r['firstname'] ?? '')); + $name = trim(($r['lastname'] ?? '') . ', ' . ($r['firstname'] ?? '')); $pos = strtolower((string) ($r['position'] ?? 'main')); if ($pos === 'ta' || $pos === 'assistant') { $classSections[$sid]['teacher_assistants'][] = $name; @@ -251,7 +251,7 @@ class WhatsappContactService } } } catch (\Throwable $e) { - Log::warning('WhatsApp teacher list lookup failed: '.$e->getMessage()); + Log::warning('WhatsApp teacher list lookup failed: ' . $e->getMessage()); } foreach ($classSections as &$section) { diff --git a/app/Services/Whatsapp/WhatsappContextService.php b/app/Services/Whatsapp/WhatsappContextService.php index 37a8db4d..de77a5cc 100644 --- a/app/Services/Whatsapp/WhatsappContextService.php +++ b/app/Services/Whatsapp/WhatsappContextService.php @@ -47,11 +47,11 @@ class WhatsappContextService } $end = $start + 1; - $canonical = $start.'-'.$end; + $canonical = $start . '-' . $end; $aliases = [ $canonical, - $start.'/'.$end, + $start . '/' . $end, sprintf('%d-%02d', $start, $end % 100), (string) $start, ]; diff --git a/app/Services/Whatsapp/WhatsappInviteBundleService.php b/app/Services/Whatsapp/WhatsappInviteBundleService.php index e7050066..9e9e8de9 100644 --- a/app/Services/Whatsapp/WhatsappInviteBundleService.php +++ b/app/Services/Whatsapp/WhatsappInviteBundleService.php @@ -13,12 +13,12 @@ class WhatsappInviteBundleService $isPrimary = User::query()->whereKey($pickedId)->exists(); $primaryId = $pickedId; - if (! $isPrimary) { + if (!$isPrimary) { $map = ParentModel::query() ->select('firstparent_id', 'id') ->where('id', $pickedId) ->first(); - if (! $map || ! $map->firstparent_id) { + if (!$map || !$map->firstparent_id) { return null; } $primaryId = (int) $map->firstparent_id; @@ -28,7 +28,7 @@ class WhatsappInviteBundleService ->select('id', 'firstname', 'lastname', 'email') ->whereKey($primaryId) ->first(); - if (! $primary) { + if (!$primary) { return null; } @@ -50,7 +50,7 @@ class WhatsappInviteBundleService $linksSet = []; foreach ($sectionRows as $row) { $cid = (int) ($row->class_section_id ?? 0); - if (! $cid) { + if (!$cid) { continue; } $link = (string) ($linkBySection[$cid]['invite_link'] ?? ''); @@ -60,7 +60,7 @@ class WhatsappInviteBundleService $sections[] = [ 'class_section_id' => $cid, - 'class_section_name' => (string) ($linkBySection[$cid]['class_section_name'] ?? ('Section '.$cid)), + 'class_section_name' => (string) ($linkBySection[$cid]['class_section_name'] ?? ('Section ' . $cid)), 'invite_link' => $link, ]; $linksSet[$link] = true; @@ -71,10 +71,10 @@ class WhatsappInviteBundleService } $emails = []; - if (! empty($primary->email)) { + if (!empty($primary->email)) { $emails[$primary->email] = true; } - if (! empty($secondary?->secondparent_email)) { + if (!empty($secondary?->secondparent_email)) { $emails[$secondary->secondparent_email] = true; } if (empty($emails)) { @@ -89,7 +89,7 @@ class WhatsappInviteBundleService 'email' => (string) ($primary->email ?? ''), 'source' => 'users', ], - 'secondary_parents' => $secondary && ! empty($secondary->id) ? [[ + 'secondary_parents' => $secondary && !empty($secondary->id) ? [[ 'id' => (int) $secondary->id, 'firstname' => (string) ($secondary->secondparent_firstname ?? ''), 'lastname' => (string) ($secondary->secondparent_lastname ?? ''), @@ -186,15 +186,15 @@ class WhatsappInviteBundleService $bundles = []; foreach ($parentIds as $pid) { $profile = $profiles[$pid] ?? null; - if (! $profile) { + if (!$profile) { continue; } $emails = []; - if (! empty($profile['email'])) { + if (!empty($profile['email'])) { $emails[$profile['email']] = true; } - if (! empty($secondaryByPrimary[$pid]['email'])) { + if (!empty($secondaryByPrimary[$pid]['email'])) { $emails[$secondaryByPrimary[$pid]['email']] = true; } if (empty($emails)) { @@ -203,10 +203,10 @@ class WhatsappInviteBundleService $bundles[] = [ 'parent' => $profile, - 'secondary_parents' => ! empty($secondaryByPrimary[$pid]['id']) ? [$secondaryByPrimary[$pid]] : [], + 'secondary_parents' => !empty($secondaryByPrimary[$pid]['id']) ? [$secondaryByPrimary[$pid]] : [], 'sections' => [[ 'class_section_id' => $classSectionId, - 'class_section_name' => (string) ($linkBySection[$classSectionId]['class_section_name'] ?? ('Section '.$classSectionId)), + 'class_section_name' => (string) ($linkBySection[$classSectionId]['class_section_name'] ?? ('Section ' . $classSectionId)), 'invite_link' => $link, ]], 'links' => [$link], @@ -232,7 +232,7 @@ class WhatsappInviteBundleService $bundles = []; foreach ($sectionRows as $row) { $cid = (int) ($row->class_section_id ?? 0); - if (! $cid) { + if (!$cid) { continue; } @@ -255,7 +255,6 @@ class WhatsappInviteBundleService $out[$e] = true; } } - return array_keys($out); }; @@ -265,7 +264,6 @@ class WhatsappInviteBundleService $k = is_array($v) ? md5(json_encode($v)) : (string) $v; $map[$k] = $v; } - return array_values($map); }; @@ -275,10 +273,10 @@ class WhatsappInviteBundleService if (is_array($row)) { $id = $row['class_section_id'] ?? null; $name = $row['class_section_name'] ?? null; - $key = $id !== null ? ('id:'.(int) $id) : ('name:'.(string) $name); + $key = $id !== null ? ('id:' . (int) $id) : ('name:' . (string) $name); $byKey[$key] = $row; } else { - $byKey['name:'.(string) $row] = $row; + $byKey['name:' . (string) $row] = $row; } }; @@ -297,11 +295,11 @@ class WhatsappInviteBundleService $add = static function (&$index, $item) { if (is_array($item)) { $key = isset($item['class_section_id']) - ? 'id:'.(int) $item['class_section_id'] - : ('url:'.(string) ($item['invite_link'] ?? json_encode($item))); + ? 'id:' . (int) $item['class_section_id'] + : ('url:' . (string) ($item['invite_link'] ?? json_encode($item))); $index[$key] = $item; } else { - $index['url:'.(string) $item] = $item; + $index['url:' . (string) $item] = $item; } }; @@ -320,9 +318,9 @@ class WhatsappInviteBundleService $parent = $bundle['parent'] ?? []; $pid = (int) ($parent['id'] ?? 0); $primaryEmails = $normalizeEmails((array) ($bundle['emails'] ?? [])); - $groupKey = $pid > 0 ? ('pid:'.$pid) : ('mail:'.implode(',', $primaryEmails)); + $groupKey = $pid > 0 ? ('pid:' . $pid) : ('mail:' . implode(',', $primaryEmails)); - if (! isset($grouped[$groupKey])) { + if (!isset($grouped[$groupKey])) { $grouped[$groupKey] = [ 'parent' => $parent, 'emails' => $primaryEmails, @@ -332,11 +330,10 @@ class WhatsappInviteBundleService 'items' => (array) ($bundle['items'] ?? []), 'secondary_parents' => (array) ($bundle['secondary_parents'] ?? []), ]; - continue; } - $grouped[$groupKey]['parent'] = ! empty($parent) ? $parent : $grouped[$groupKey]['parent']; + $grouped[$groupKey]['parent'] = !empty($parent) ? $parent : $grouped[$groupKey]['parent']; $grouped[$groupKey]['emails'] = $normalizeEmails(array_merge($grouped[$groupKey]['emails'], $primaryEmails)); $grouped[$groupKey]['sections'] = $mergeSections($grouped[$groupKey]['sections'], (array) ($bundle['sections'] ?? [])); $grouped[$groupKey]['links'] = $mergeLinks($grouped[$groupKey]['links'], (array) ($bundle['links'] ?? [])); diff --git a/app/Services/Whatsapp/WhatsappInviteEmailService.php b/app/Services/Whatsapp/WhatsappInviteEmailService.php index 18911451..b70f42d8 100644 --- a/app/Services/Whatsapp/WhatsappInviteEmailService.php +++ b/app/Services/Whatsapp/WhatsappInviteEmailService.php @@ -8,7 +8,9 @@ use Illuminate\Support\Facades\Log; class WhatsappInviteEmailService { - public function __construct(private EmailService $emailService) {} + public function __construct(private EmailService $emailService) + { + } public function send(array $payload): array { @@ -16,13 +18,12 @@ class WhatsappInviteEmailService $cc = array_values(array_unique(array_filter((array) ($payload['cc_emails'] ?? [])))); $cc = array_values(array_diff($cc, $to)); - if (empty($to) && ! empty($cc)) { + if (empty($to) && !empty($cc)) { $to = $cc; $cc = []; } if (empty($to)) { Log::warning('WhatsappInvite: no recipients after normalization.'); - return ['ok' => false, 'message' => 'No recipients provided.']; } @@ -33,7 +34,6 @@ class WhatsappInviteEmailService $items = $this->buildItems($sections, $links); if (empty($items)) { Log::warning('WhatsappInvite: no invite items built.'); - return ['ok' => false, 'message' => 'No invite links found.']; } @@ -77,12 +77,12 @@ class WhatsappInviteEmailService foreach ($sections as $section) { $sid = isset($section['class_section_id']) ? (int) $section['class_section_id'] : 0; - $name = trim((string) ($section['class_section_name'] ?? ($sid ? ('Section '.$sid) : 'Class'))); + $name = trim((string) ($section['class_section_name'] ?? ($sid ? ('Section ' . $sid) : 'Class'))); $url = trim((string) ($section['invite_link'] ?? '')); if ($url === '') { continue; } - $key = $sid.'|'.$url; + $key = $sid . '|' . $url; if (isset($seen[$key])) { continue; } @@ -96,20 +96,20 @@ class WhatsappInviteEmailService ]; } - if (empty($items) && ! empty($links)) { + if (empty($items) && !empty($links)) { foreach ($links as $i => $url) { $url = trim((string) $url); if ($url === '') { continue; } - $key = '0|'.$url; + $key = '0|' . $url; if (isset($seen[$key])) { continue; } $seen[$key] = true; $items[] = [ 'class_section_id' => 0, - 'class_section_name' => 'Class Link '.($i + 1), + 'class_section_name' => 'Class Link ' . ($i + 1), 'invite_link' => $url, 'qr_src' => $this->generateQrImage($url), ]; @@ -126,23 +126,21 @@ class WhatsappInviteEmailService } $dir = storage_path('app/qrcodes'); - if (! is_dir($dir)) { - if (! @mkdir($dir, 0775, true) && ! is_dir($dir)) { + if (!is_dir($dir)) { + if (!@mkdir($dir, 0775, true) && !is_dir($dir)) { Log::error('QR: cannot create dir', ['dir' => $dir]); - return $this->qrFallback($inviteLink); } } - if (! is_writable($dir)) { + if (!is_writable($dir)) { Log::error('QR: dir not writable', ['dir' => $dir]); - return $this->qrFallback($inviteLink); } - $filename = 'wa_'.md5($inviteLink).'.png'; - $path = $dir.DIRECTORY_SEPARATOR.$filename; + $filename = 'wa_' . md5($inviteLink) . '.png'; + $path = $dir . DIRECTORY_SEPARATOR . $filename; - if (! is_file($path)) { + if (!is_file($path)) { $v5BuilderClass = '\\Endroid\\QrCode\\Builder\\Builder'; $v5WriterClass = '\\Endroid\\QrCode\\Writer\\PngWriter'; $v5Encoding = '\\Endroid\\QrCode\\Encoding\\Encoding'; @@ -159,7 +157,7 @@ class WhatsappInviteEmailService if (class_exists($v5BuilderClass) && class_exists($v5WriterClass)) { try { $builder = $v5BuilderClass::create() - ->writer(new $v5WriterClass) + ->writer(new $v5WriterClass()) ->data($inviteLink); if (class_exists($v5Encoding)) { $builder->encoding(new $v5Encoding('UTF-8')); @@ -167,11 +165,11 @@ class WhatsappInviteEmailService $result = $builder->size(500)->margin(2)->build(); $result->saveToFile($path); } catch (\Throwable $e) { - $lastErr = 'Endroid v5: '.$e->getMessage(); + $lastErr = 'Endroid v5: ' . $e->getMessage(); } } - if (! is_file($path) && class_exists($writerClass)) { + if (!is_file($path) && class_exists($writerClass)) { try { if (class_exists($qrClass) && method_exists($qrClass, 'create')) { $qr = $qrClass::create($inviteLink); @@ -185,7 +183,7 @@ class WhatsappInviteEmailService if (method_exists($qr, 'setMargin')) { $qr->setMargin(2); } - $writer = new $writerClass; + $writer = new $writerClass(); $result = $writer->write($qr); if (method_exists($result, 'saveToFile')) { $result->saveToFile($path); @@ -195,21 +193,21 @@ class WhatsappInviteEmailService } } } catch (\Throwable $e) { - $lastErr = 'Endroid v3/v4: '.$e->getMessage(); + $lastErr = 'Endroid v3/v4: ' . $e->getMessage(); } } - if (! is_file($path) && class_exists($chlQRCode) && class_exists($chlQROptions)) { + if (!is_file($path) && class_exists($chlQRCode) && class_exists($chlQROptions)) { try { $optsArr = [ 'version' => 5, - 'outputType' => constant($chlQRCode.'::OUTPUT_IMAGE_PNG'), + 'outputType' => constant($chlQRCode . '::OUTPUT_IMAGE_PNG'), 'scale' => 6, 'imageBase64' => false, 'quietzoneSize' => 2, ]; - if (class_exists($chlQRMatrix) && defined($chlQRMatrix.'::ECC_H')) { - $optsArr['eccLevel'] = constant($chlQRMatrix.'::ECC_H'); + if (class_exists($chlQRMatrix) && defined($chlQRMatrix . '::ECC_H')) { + $optsArr['eccLevel'] = constant($chlQRMatrix . '::ECC_H'); } $opts = new $chlQROptions($optsArr); @@ -217,11 +215,11 @@ class WhatsappInviteEmailService $png = $qrcode->render($inviteLink); file_put_contents($path, $png); } catch (\Throwable $e) { - $lastErr = 'chillerlan: '.$e->getMessage(); + $lastErr = 'chillerlan: ' . $e->getMessage(); } } - if (! is_file($path) && $lastErr) { + if (!is_file($path) && $lastErr) { Log::error('QR generation failed', ['error' => $lastErr]); } } @@ -229,7 +227,7 @@ class WhatsappInviteEmailService if (is_file($path)) { $data = @file_get_contents($path); if ($data !== false) { - return 'data:image/png;base64,'.base64_encode($data); + return 'data:image/png;base64,' . base64_encode($data); } } @@ -239,7 +237,6 @@ class WhatsappInviteEmailService private function qrFallback(string $inviteLink): string { $link = rawurlencode($inviteLink); - - return 'https://api.qrserver.com/v1/create-qr-code/?size=260x260&qzone=2&data='.$link; + return 'https://api.qrserver.com/v1/create-qr-code/?size=260x260&qzone=2&data=' . $link; } } diff --git a/app/Services/Whatsapp/WhatsappInviteNotificationService.php b/app/Services/Whatsapp/WhatsappInviteNotificationService.php index 91183220..6396bdc2 100644 --- a/app/Services/Whatsapp/WhatsappInviteNotificationService.php +++ b/app/Services/Whatsapp/WhatsappInviteNotificationService.php @@ -14,7 +14,6 @@ class WhatsappInviteNotificationService $listenerCount = Event::hasListeners(WhatsappInvitesSend::class) ? 1 : 0; if ($listenerCount === 0) { Log::warning('WhatsappInvitesSend has no listeners registered.'); - return [ 'ok' => false, 'message' => 'No event listener registered for WhatsappInvitesSend.', @@ -30,7 +29,6 @@ class WhatsappInviteNotificationService $out[$e] = true; } } - return array_keys($out); }; @@ -49,9 +47,9 @@ class WhatsappInviteNotificationService $to = $primaryEmails; $cc = []; - if (! empty($primaryEmails) && ! empty($secondaryEmails)) { + if (!empty($primaryEmails) && !empty($secondaryEmails)) { $cc = array_values(array_diff($secondaryEmails, $primaryEmails)); - } elseif (empty($primaryEmails) && ! empty($secondaryEmails)) { + } elseif (empty($primaryEmails) && !empty($secondaryEmails)) { $to = $secondaryEmails; } @@ -59,7 +57,6 @@ class WhatsappInviteNotificationService Log::warning('Skipping WhatsApp invite: no recipient email(s).', [ 'parent_id' => $bundle['parent']['id'] ?? null, ]); - continue; } @@ -76,7 +73,7 @@ class WhatsappInviteNotificationService $triggered++; } catch (\Throwable $e) { - Log::error('WhatsappInvitesSend dispatch failed: '.$e->getMessage()); + Log::error('WhatsappInvitesSend dispatch failed: ' . $e->getMessage()); } } @@ -90,12 +87,12 @@ class WhatsappInviteNotificationService { $collected = []; - if (! empty($bundle['emails_secondary'])) { + if (!empty($bundle['emails_secondary'])) { $collected = array_merge($collected, (array) $bundle['emails_secondary']); } - if (! empty($bundle['secondary_parents'])) { + if (!empty($bundle['secondary_parents'])) { foreach ((array) $bundle['secondary_parents'] as $sp) { - if (! empty($sp['email'])) { + if (!empty($sp['email'])) { $collected[] = $sp['email']; } } @@ -103,11 +100,11 @@ class WhatsappInviteNotificationService $parent = $bundle['parent'] ?? []; foreach (['secondary_email', 'secondparent_email', 'sec_email', 'email_secondary'] as $key) { - if (! empty($parent[$key])) { + if (!empty($parent[$key])) { $collected[] = $parent[$key]; } } - if (! empty($parent['secondparent']['email'])) { + if (!empty($parent['secondparent']['email'])) { $collected[] = $parent['secondparent']['email']; } @@ -119,12 +116,12 @@ class WhatsappInviteNotificationService ->where('firstparent_id', $primaryId) ->orderByDesc('updated_at') ->first(); - if (! empty($row?->secondparent_email)) { + if (!empty($row?->secondparent_email)) { $collected[] = $row->secondparent_email; } } } catch (\Throwable $e) { - Log::warning('Secondary email DB lookup failed: '.$e->getMessage()); + Log::warning('Secondary email DB lookup failed: ' . $e->getMessage()); } return $normalize($collected); diff --git a/app/Services/Whatsapp/WhatsappInviteService.php b/app/Services/Whatsapp/WhatsappInviteService.php index d9324092..16357587 100644 --- a/app/Services/Whatsapp/WhatsappInviteService.php +++ b/app/Services/Whatsapp/WhatsappInviteService.php @@ -10,7 +10,8 @@ class WhatsappInviteService private WhatsappContextService $context, private WhatsappInviteBundleService $bundleService, private WhatsappInviteNotificationService $notificationService - ) {} + ) { + } public function send(array $payload): array { @@ -24,7 +25,7 @@ class WhatsappInviteService $linkBySection = []; foreach ($links as $link) { $cid = (int) ($link->class_section_id ?? 0); - if ($cid && ! isset($linkBySection[$cid])) { + if ($cid && !isset($linkBySection[$cid])) { $linkBySection[$cid] = $link->toArray(); } } @@ -37,7 +38,7 @@ class WhatsappInviteService continue; } $bundle = $this->bundleService->bundleForParent($pickedId, $linkBySection, $schoolYear, $semester); - if ($bundle && ! empty($bundle['emails']) && ! empty($bundle['links'])) { + if ($bundle && !empty($bundle['emails']) && !empty($bundle['links'])) { $bundles[] = $bundle; } } @@ -45,14 +46,14 @@ class WhatsappInviteService $classSectionId = (int) ($payload['class_section_id'] ?? 0); $classBundles = $this->bundleService->bundlesForClass($classSectionId, $linkBySection, $schoolYear, $semester); foreach ($classBundles as $bundle) { - if (! empty($bundle['emails']) && ! empty($bundle['links'])) { + if (!empty($bundle['emails']) && !empty($bundle['links'])) { $bundles[] = $bundle; } } } elseif ($mode === 'all') { $allBundles = $this->bundleService->bundlesForAll($linkBySection, $schoolYear, $semester); foreach ($allBundles as $bundle) { - if (! empty($bundle['emails']) && ! empty($bundle['links'])) { + if (!empty($bundle['emails']) && !empty($bundle['links'])) { $bundles[] = $bundle; } } @@ -69,7 +70,7 @@ class WhatsappInviteService $consolidated = $this->bundleService->consolidateBundles($bundles); $result = $this->notificationService->dispatch($consolidated, $schoolYear, $semester); - if (! ($result['ok'] ?? false)) { + if (!($result['ok'] ?? false)) { return $result + ['ok' => false]; } diff --git a/app/Services/Whatsapp/WhatsappLinkService.php b/app/Services/Whatsapp/WhatsappLinkService.php index e9983575..03e5f727 100644 --- a/app/Services/Whatsapp/WhatsappLinkService.php +++ b/app/Services/Whatsapp/WhatsappLinkService.php @@ -9,7 +9,9 @@ use Illuminate\Database\Eloquent\ModelNotFoundException; class WhatsappLinkService { - public function __construct(private WhatsappContextService $context) {} + public function __construct(private WhatsappContextService $context) + { + } public function paginate(array $filters): LengthAwarePaginator { @@ -27,7 +29,7 @@ class WhatsappLinkService } } - if (! empty($filters['class_section_id'])) { + if (!empty($filters['class_section_id'])) { $query->forSection((int) $filters['class_section_id']); } @@ -35,11 +37,11 @@ class WhatsappLinkService $query->where('active', (int) (bool) $filters['active']); } - if (! empty($filters['search'])) { + if (!empty($filters['search'])) { $search = trim((string) $filters['search']); $query->where(function ($q) use ($search) { - $q->where('class_section_name', 'like', '%'.$search.'%') - ->orWhere('invite_link', 'like', '%'.$search.'%') + $q->where('class_section_name', 'like', '%' . $search . '%') + ->orWhere('invite_link', 'like', '%' . $search . '%') ->orWhere('class_section_id', $search); }); } @@ -48,7 +50,7 @@ class WhatsappLinkService $sortDir = strtolower((string) ($filters['sort_dir'] ?? 'desc')) === 'asc' ? 'asc' : 'desc'; $allowedSorts = ['class_section_id', 'class_section_name', 'school_year', 'semester', 'active', 'updated_at']; - if (! in_array($sortBy, $allowedSorts, true)) { + if (!in_array($sortBy, $allowedSorts, true)) { $sortBy = 'updated_at'; } @@ -61,7 +63,7 @@ class WhatsappLinkService public function findOrFail(int $id): WhatsappGroupLink { $link = WhatsappGroupLink::query()->find($id); - if (! $link) { + if (!$link) { throw new ModelNotFoundException('WhatsApp group link not found.'); } @@ -81,7 +83,7 @@ class WhatsappLinkService $link = WhatsappGroupLink::upsertLinkForSection( $sectionId, - $sectionName !== '' ? $sectionName : ('Section '.$sectionId), + $sectionName !== '' ? $sectionName : ('Section ' . $sectionId), $schoolYear, $semester, (string) ($payload['invite_link'] ?? ''), @@ -97,7 +99,7 @@ class WhatsappLinkService if (array_key_exists('class_section_name', $payload)) { $name = trim((string) ($payload['class_section_name'] ?? '')); - if ($name === '' && ! empty($payload['class_section_id'])) { + if ($name === '' && !empty($payload['class_section_id'])) { $name = $this->resolveSectionName((int) $payload['class_section_id']); } $payload['class_section_name'] = $name; @@ -118,7 +120,6 @@ class WhatsappLinkService private function resolveSectionName(int $sectionId): string { $name = ClassSection::getClassSectionNameBySectionId($sectionId); - - return $name !== null && $name !== '' ? $name : ('Section '.$sectionId); + return $name !== null && $name !== '' ? $name : ('Section ' . $sectionId); } } diff --git a/app/Services/Whatsapp/WhatsappMembershipService.php b/app/Services/Whatsapp/WhatsappMembershipService.php index 301e2770..cf6189e4 100644 --- a/app/Services/Whatsapp/WhatsappMembershipService.php +++ b/app/Services/Whatsapp/WhatsappMembershipService.php @@ -11,9 +11,8 @@ class WhatsappMembershipService { public function updateMembership(array $payload, ?int $verifiedBy = null): array { - if (! Schema::hasTable('whatsapp_group_memberships')) { + if (!Schema::hasTable('whatsapp_group_memberships')) { Log::error('WhatsApp membership table missing.'); - return [ 'ok' => false, 'message' => 'Membership table missing. Please run migrations.', @@ -57,7 +56,7 @@ class WhatsappMembershipService (bool) $primaryMember, $verifiedBy ); - $saved[] = 'primary='.$primaryId.':'.($primaryMember ? '1' : '0'); + $saved[] = 'primary=' . $primaryId . ':' . ($primaryMember ? '1' : '0'); if ($id <= 0) { Log::warning('Failed to upsert primary membership.', [ 'class_section_id' => $classSectionId, @@ -76,7 +75,7 @@ class WhatsappMembershipService (bool) $secondMember, $verifiedBy ); - $saved[] = 'second='.$secondId.':'.($secondMember ? '1' : '0'); + $saved[] = 'second=' . $secondId . ':' . ($secondMember ? '1' : '0'); if ($id <= 0) { Log::warning('Failed to upsert second membership.', [ 'class_section_id' => $classSectionId, diff --git a/app/Support/MailHtml.php b/app/Support/MailHtml.php index ee7ea41d..df06e07f 100644 --- a/app/Support/MailHtml.php +++ b/app/Support/MailHtml.php @@ -12,12 +12,12 @@ final class MailHtml $t = htmlspecialchars($title, ENT_QUOTES, 'UTF-8'); return '' - .'' - .''.$t.'' - .'' - .'
' + . '' + . ''.$t.'' + . '' + . '
' .$bodyInnerHtml - .'
'; + . '
'; } /** Late slip PDF body (Dompdf). */ diff --git a/app/Support/SessionTimeoutConfig.php b/app/Support/SessionTimeoutConfig.php index b7b6ff16..f8cce002 100644 --- a/app/Support/SessionTimeoutConfig.php +++ b/app/Support/SessionTimeoutConfig.php @@ -5,8 +5,6 @@ namespace App\Support; class SessionTimeoutConfig { public const TIMEOUT_DURATION = 1200; - public const WARNING_THRESHOLD = 900; - public const CHECK_INTERVAL = 60; } diff --git a/app/ThirdParty/fpdf/font/courier.php b/app/ThirdParty/fpdf/font/courier.php index 09219989..bc8478eb 100644 --- a/app/ThirdParty/fpdf/font/courier.php +++ b/app/ThirdParty/fpdf/font/courier.php @@ -1,11 +1,10 @@ [0, 128], 128 => 8364, 130 => 8218, 131 => 402, 132 => 8222, 133 => 8230, 134 => [8224, 2], 136 => 710, 137 => 8240, 138 => 352, 139 => 8249, 140 => 338, 142 => 381, 145 => [8216, 2], 147 => [8220, 2], 149 => 8226, 150 => [8211, 2], 152 => 732, 153 => 8482, 154 => 353, 155 => 8250, 156 => 339, 158 => 382, 159 => 376, 160 => [160, 96]]; +$uv = array(0=>array(0,128),128=>8364,130=>8218,131=>402,132=>8222,133=>8230,134=>array(8224,2),136=>710,137=>8240,138=>352,139=>8249,140=>338,142=>381,145=>array(8216,2),147=>array(8220,2),149=>8226,150=>array(8211,2),152=>732,153=>8482,154=>353,155=>8250,156=>339,158=>382,159=>376,160=>array(160,96)); +?> diff --git a/app/ThirdParty/fpdf/font/courierb.php b/app/ThirdParty/fpdf/font/courierb.php index 33bbd222..97ecd701 100644 --- a/app/ThirdParty/fpdf/font/courierb.php +++ b/app/ThirdParty/fpdf/font/courierb.php @@ -1,11 +1,10 @@ [0, 128], 128 => 8364, 130 => 8218, 131 => 402, 132 => 8222, 133 => 8230, 134 => [8224, 2], 136 => 710, 137 => 8240, 138 => 352, 139 => 8249, 140 => 338, 142 => 381, 145 => [8216, 2], 147 => [8220, 2], 149 => 8226, 150 => [8211, 2], 152 => 732, 153 => 8482, 154 => 353, 155 => 8250, 156 => 339, 158 => 382, 159 => 376, 160 => [160, 96]]; +$uv = array(0=>array(0,128),128=>8364,130=>8218,131=>402,132=>8222,133=>8230,134=>array(8224,2),136=>710,137=>8240,138=>352,139=>8249,140=>338,142=>381,145=>array(8216,2),147=>array(8220,2),149=>8226,150=>array(8211,2),152=>732,153=>8482,154=>353,155=>8250,156=>339,158=>382,159=>376,160=>array(160,96)); +?> diff --git a/app/ThirdParty/fpdf/font/courierbi.php b/app/ThirdParty/fpdf/font/courierbi.php index c9763421..c4bfff88 100644 --- a/app/ThirdParty/fpdf/font/courierbi.php +++ b/app/ThirdParty/fpdf/font/courierbi.php @@ -1,11 +1,10 @@ [0, 128], 128 => 8364, 130 => 8218, 131 => 402, 132 => 8222, 133 => 8230, 134 => [8224, 2], 136 => 710, 137 => 8240, 138 => 352, 139 => 8249, 140 => 338, 142 => 381, 145 => [8216, 2], 147 => [8220, 2], 149 => 8226, 150 => [8211, 2], 152 => 732, 153 => 8482, 154 => 353, 155 => 8250, 156 => 339, 158 => 382, 159 => 376, 160 => [160, 96]]; +$uv = array(0=>array(0,128),128=>8364,130=>8218,131=>402,132=>8222,133=>8230,134=>array(8224,2),136=>710,137=>8240,138=>352,139=>8249,140=>338,142=>381,145=>array(8216,2),147=>array(8220,2),149=>8226,150=>array(8211,2),152=>732,153=>8482,154=>353,155=>8250,156=>339,158=>382,159=>376,160=>array(160,96)); +?> diff --git a/app/ThirdParty/fpdf/font/courieri.php b/app/ThirdParty/fpdf/font/courieri.php index ba94e54b..015a15ad 100644 --- a/app/ThirdParty/fpdf/font/courieri.php +++ b/app/ThirdParty/fpdf/font/courieri.php @@ -1,11 +1,10 @@ [0, 128], 128 => 8364, 130 => 8218, 131 => 402, 132 => 8222, 133 => 8230, 134 => [8224, 2], 136 => 710, 137 => 8240, 138 => 352, 139 => 8249, 140 => 338, 142 => 381, 145 => [8216, 2], 147 => [8220, 2], 149 => 8226, 150 => [8211, 2], 152 => 732, 153 => 8482, 154 => 353, 155 => 8250, 156 => 339, 158 => 382, 159 => 376, 160 => [160, 96]]; +$uv = array(0=>array(0,128),128=>8364,130=>8218,131=>402,132=>8222,133=>8230,134=>array(8224,2),136=>710,137=>8240,138=>352,139=>8249,140=>338,142=>381,145=>array(8216,2),147=>array(8220,2),149=>8226,150=>array(8211,2),152=>732,153=>8482,154=>353,155=>8250,156=>339,158=>382,159=>376,160=>array(160,96)); +?> diff --git a/app/ThirdParty/fpdf/font/helvetica.php b/app/ThirdParty/fpdf/font/helvetica.php index 3dcf5e5d..927759b9 100644 --- a/app/ThirdParty/fpdf/font/helvetica.php +++ b/app/ThirdParty/fpdf/font/helvetica.php @@ -1,21 +1,21 @@ 278, chr(1) => 278, chr(2) => 278, chr(3) => 278, chr(4) => 278, chr(5) => 278, chr(6) => 278, chr(7) => 278, chr(8) => 278, chr(9) => 278, chr(10) => 278, chr(11) => 278, chr(12) => 278, chr(13) => 278, chr(14) => 278, chr(15) => 278, chr(16) => 278, chr(17) => 278, chr(18) => 278, chr(19) => 278, chr(20) => 278, chr(21) => 278, - chr(22) => 278, chr(23) => 278, chr(24) => 278, chr(25) => 278, chr(26) => 278, chr(27) => 278, chr(28) => 278, chr(29) => 278, chr(30) => 278, chr(31) => 278, ' ' => 278, '!' => 278, '"' => 355, '#' => 556, '$' => 556, '%' => 889, '&' => 667, '\'' => 191, '(' => 333, ')' => 333, '*' => 389, '+' => 584, - ',' => 278, '-' => 333, '.' => 278, '/' => 278, '0' => 556, '1' => 556, '2' => 556, '3' => 556, '4' => 556, '5' => 556, '6' => 556, '7' => 556, '8' => 556, '9' => 556, ':' => 278, ';' => 278, '<' => 584, '=' => 584, '>' => 584, '?' => 556, '@' => 1015, 'A' => 667, - 'B' => 667, 'C' => 722, 'D' => 722, 'E' => 667, 'F' => 611, 'G' => 778, 'H' => 722, 'I' => 278, 'J' => 500, 'K' => 667, 'L' => 556, 'M' => 833, 'N' => 722, 'O' => 778, 'P' => 667, 'Q' => 778, 'R' => 722, 'S' => 667, 'T' => 611, 'U' => 722, 'V' => 667, 'W' => 944, - 'X' => 667, 'Y' => 667, 'Z' => 611, '[' => 278, '\\' => 278, ']' => 278, '^' => 469, '_' => 556, '`' => 333, 'a' => 556, 'b' => 556, 'c' => 500, 'd' => 556, 'e' => 556, 'f' => 278, 'g' => 556, 'h' => 556, 'i' => 222, 'j' => 222, 'k' => 500, 'l' => 222, 'm' => 833, - 'n' => 556, 'o' => 556, 'p' => 556, 'q' => 556, 'r' => 333, 's' => 500, 't' => 278, 'u' => 556, 'v' => 500, 'w' => 722, 'x' => 500, 'y' => 500, 'z' => 500, '{' => 334, '|' => 260, '}' => 334, '~' => 584, chr(127) => 350, chr(128) => 556, chr(129) => 350, chr(130) => 222, chr(131) => 556, - chr(132) => 333, chr(133) => 1000, chr(134) => 556, chr(135) => 556, chr(136) => 333, chr(137) => 1000, chr(138) => 667, chr(139) => 333, chr(140) => 1000, chr(141) => 350, chr(142) => 611, chr(143) => 350, chr(144) => 350, chr(145) => 222, chr(146) => 222, chr(147) => 333, chr(148) => 333, chr(149) => 350, chr(150) => 556, chr(151) => 1000, chr(152) => 333, chr(153) => 1000, - chr(154) => 500, chr(155) => 333, chr(156) => 944, chr(157) => 350, chr(158) => 500, chr(159) => 667, chr(160) => 278, chr(161) => 333, chr(162) => 556, chr(163) => 556, chr(164) => 556, chr(165) => 556, chr(166) => 260, chr(167) => 556, chr(168) => 333, chr(169) => 737, chr(170) => 370, chr(171) => 556, chr(172) => 584, chr(173) => 333, chr(174) => 737, chr(175) => 333, - chr(176) => 400, chr(177) => 584, chr(178) => 333, chr(179) => 333, chr(180) => 333, chr(181) => 556, chr(182) => 537, chr(183) => 278, chr(184) => 333, chr(185) => 333, chr(186) => 365, chr(187) => 556, chr(188) => 834, chr(189) => 834, chr(190) => 834, chr(191) => 611, chr(192) => 667, chr(193) => 667, chr(194) => 667, chr(195) => 667, chr(196) => 667, chr(197) => 667, - chr(198) => 1000, chr(199) => 722, chr(200) => 667, chr(201) => 667, chr(202) => 667, chr(203) => 667, chr(204) => 278, chr(205) => 278, chr(206) => 278, chr(207) => 278, chr(208) => 722, chr(209) => 722, chr(210) => 778, chr(211) => 778, chr(212) => 778, chr(213) => 778, chr(214) => 778, chr(215) => 584, chr(216) => 778, chr(217) => 722, chr(218) => 722, chr(219) => 722, - chr(220) => 722, chr(221) => 667, chr(222) => 667, chr(223) => 611, chr(224) => 556, chr(225) => 556, chr(226) => 556, chr(227) => 556, chr(228) => 556, chr(229) => 556, chr(230) => 889, chr(231) => 500, chr(232) => 556, chr(233) => 556, chr(234) => 556, chr(235) => 556, chr(236) => 278, chr(237) => 278, chr(238) => 278, chr(239) => 278, chr(240) => 556, chr(241) => 556, - chr(242) => 556, chr(243) => 556, chr(244) => 556, chr(245) => 556, chr(246) => 556, chr(247) => 584, chr(248) => 611, chr(249) => 556, chr(250) => 556, chr(251) => 556, chr(252) => 556, chr(253) => 500, chr(254) => 556, chr(255) => 500]; +$cw = array( + chr(0)=>278,chr(1)=>278,chr(2)=>278,chr(3)=>278,chr(4)=>278,chr(5)=>278,chr(6)=>278,chr(7)=>278,chr(8)=>278,chr(9)=>278,chr(10)=>278,chr(11)=>278,chr(12)=>278,chr(13)=>278,chr(14)=>278,chr(15)=>278,chr(16)=>278,chr(17)=>278,chr(18)=>278,chr(19)=>278,chr(20)=>278,chr(21)=>278, + chr(22)=>278,chr(23)=>278,chr(24)=>278,chr(25)=>278,chr(26)=>278,chr(27)=>278,chr(28)=>278,chr(29)=>278,chr(30)=>278,chr(31)=>278,' '=>278,'!'=>278,'"'=>355,'#'=>556,'$'=>556,'%'=>889,'&'=>667,'\''=>191,'('=>333,')'=>333,'*'=>389,'+'=>584, + ','=>278,'-'=>333,'.'=>278,'/'=>278,'0'=>556,'1'=>556,'2'=>556,'3'=>556,'4'=>556,'5'=>556,'6'=>556,'7'=>556,'8'=>556,'9'=>556,':'=>278,';'=>278,'<'=>584,'='=>584,'>'=>584,'?'=>556,'@'=>1015,'A'=>667, + 'B'=>667,'C'=>722,'D'=>722,'E'=>667,'F'=>611,'G'=>778,'H'=>722,'I'=>278,'J'=>500,'K'=>667,'L'=>556,'M'=>833,'N'=>722,'O'=>778,'P'=>667,'Q'=>778,'R'=>722,'S'=>667,'T'=>611,'U'=>722,'V'=>667,'W'=>944, + 'X'=>667,'Y'=>667,'Z'=>611,'['=>278,'\\'=>278,']'=>278,'^'=>469,'_'=>556,'`'=>333,'a'=>556,'b'=>556,'c'=>500,'d'=>556,'e'=>556,'f'=>278,'g'=>556,'h'=>556,'i'=>222,'j'=>222,'k'=>500,'l'=>222,'m'=>833, + 'n'=>556,'o'=>556,'p'=>556,'q'=>556,'r'=>333,'s'=>500,'t'=>278,'u'=>556,'v'=>500,'w'=>722,'x'=>500,'y'=>500,'z'=>500,'{'=>334,'|'=>260,'}'=>334,'~'=>584,chr(127)=>350,chr(128)=>556,chr(129)=>350,chr(130)=>222,chr(131)=>556, + chr(132)=>333,chr(133)=>1000,chr(134)=>556,chr(135)=>556,chr(136)=>333,chr(137)=>1000,chr(138)=>667,chr(139)=>333,chr(140)=>1000,chr(141)=>350,chr(142)=>611,chr(143)=>350,chr(144)=>350,chr(145)=>222,chr(146)=>222,chr(147)=>333,chr(148)=>333,chr(149)=>350,chr(150)=>556,chr(151)=>1000,chr(152)=>333,chr(153)=>1000, + chr(154)=>500,chr(155)=>333,chr(156)=>944,chr(157)=>350,chr(158)=>500,chr(159)=>667,chr(160)=>278,chr(161)=>333,chr(162)=>556,chr(163)=>556,chr(164)=>556,chr(165)=>556,chr(166)=>260,chr(167)=>556,chr(168)=>333,chr(169)=>737,chr(170)=>370,chr(171)=>556,chr(172)=>584,chr(173)=>333,chr(174)=>737,chr(175)=>333, + chr(176)=>400,chr(177)=>584,chr(178)=>333,chr(179)=>333,chr(180)=>333,chr(181)=>556,chr(182)=>537,chr(183)=>278,chr(184)=>333,chr(185)=>333,chr(186)=>365,chr(187)=>556,chr(188)=>834,chr(189)=>834,chr(190)=>834,chr(191)=>611,chr(192)=>667,chr(193)=>667,chr(194)=>667,chr(195)=>667,chr(196)=>667,chr(197)=>667, + chr(198)=>1000,chr(199)=>722,chr(200)=>667,chr(201)=>667,chr(202)=>667,chr(203)=>667,chr(204)=>278,chr(205)=>278,chr(206)=>278,chr(207)=>278,chr(208)=>722,chr(209)=>722,chr(210)=>778,chr(211)=>778,chr(212)=>778,chr(213)=>778,chr(214)=>778,chr(215)=>584,chr(216)=>778,chr(217)=>722,chr(218)=>722,chr(219)=>722, + chr(220)=>722,chr(221)=>667,chr(222)=>667,chr(223)=>611,chr(224)=>556,chr(225)=>556,chr(226)=>556,chr(227)=>556,chr(228)=>556,chr(229)=>556,chr(230)=>889,chr(231)=>500,chr(232)=>556,chr(233)=>556,chr(234)=>556,chr(235)=>556,chr(236)=>278,chr(237)=>278,chr(238)=>278,chr(239)=>278,chr(240)=>556,chr(241)=>556, + chr(242)=>556,chr(243)=>556,chr(244)=>556,chr(245)=>556,chr(246)=>556,chr(247)=>584,chr(248)=>611,chr(249)=>556,chr(250)=>556,chr(251)=>556,chr(252)=>556,chr(253)=>500,chr(254)=>556,chr(255)=>500); $enc = 'cp1252'; -$uv = [0 => [0, 128], 128 => 8364, 130 => 8218, 131 => 402, 132 => 8222, 133 => 8230, 134 => [8224, 2], 136 => 710, 137 => 8240, 138 => 352, 139 => 8249, 140 => 338, 142 => 381, 145 => [8216, 2], 147 => [8220, 2], 149 => 8226, 150 => [8211, 2], 152 => 732, 153 => 8482, 154 => 353, 155 => 8250, 156 => 339, 158 => 382, 159 => 376, 160 => [160, 96]]; +$uv = array(0=>array(0,128),128=>8364,130=>8218,131=>402,132=>8222,133=>8230,134=>array(8224,2),136=>710,137=>8240,138=>352,139=>8249,140=>338,142=>381,145=>array(8216,2),147=>array(8220,2),149=>8226,150=>array(8211,2),152=>732,153=>8482,154=>353,155=>8250,156=>339,158=>382,159=>376,160=>array(160,96)); +?> diff --git a/app/ThirdParty/fpdf/font/helveticab.php b/app/ThirdParty/fpdf/font/helveticab.php index 3a9e482f..bcd73672 100644 --- a/app/ThirdParty/fpdf/font/helveticab.php +++ b/app/ThirdParty/fpdf/font/helveticab.php @@ -1,21 +1,21 @@ 278, chr(1) => 278, chr(2) => 278, chr(3) => 278, chr(4) => 278, chr(5) => 278, chr(6) => 278, chr(7) => 278, chr(8) => 278, chr(9) => 278, chr(10) => 278, chr(11) => 278, chr(12) => 278, chr(13) => 278, chr(14) => 278, chr(15) => 278, chr(16) => 278, chr(17) => 278, chr(18) => 278, chr(19) => 278, chr(20) => 278, chr(21) => 278, - chr(22) => 278, chr(23) => 278, chr(24) => 278, chr(25) => 278, chr(26) => 278, chr(27) => 278, chr(28) => 278, chr(29) => 278, chr(30) => 278, chr(31) => 278, ' ' => 278, '!' => 333, '"' => 474, '#' => 556, '$' => 556, '%' => 889, '&' => 722, '\'' => 238, '(' => 333, ')' => 333, '*' => 389, '+' => 584, - ',' => 278, '-' => 333, '.' => 278, '/' => 278, '0' => 556, '1' => 556, '2' => 556, '3' => 556, '4' => 556, '5' => 556, '6' => 556, '7' => 556, '8' => 556, '9' => 556, ':' => 333, ';' => 333, '<' => 584, '=' => 584, '>' => 584, '?' => 611, '@' => 975, 'A' => 722, - 'B' => 722, 'C' => 722, 'D' => 722, 'E' => 667, 'F' => 611, 'G' => 778, 'H' => 722, 'I' => 278, 'J' => 556, 'K' => 722, 'L' => 611, 'M' => 833, 'N' => 722, 'O' => 778, 'P' => 667, 'Q' => 778, 'R' => 722, 'S' => 667, 'T' => 611, 'U' => 722, 'V' => 667, 'W' => 944, - 'X' => 667, 'Y' => 667, 'Z' => 611, '[' => 333, '\\' => 278, ']' => 333, '^' => 584, '_' => 556, '`' => 333, 'a' => 556, 'b' => 611, 'c' => 556, 'd' => 611, 'e' => 556, 'f' => 333, 'g' => 611, 'h' => 611, 'i' => 278, 'j' => 278, 'k' => 556, 'l' => 278, 'm' => 889, - 'n' => 611, 'o' => 611, 'p' => 611, 'q' => 611, 'r' => 389, 's' => 556, 't' => 333, 'u' => 611, 'v' => 556, 'w' => 778, 'x' => 556, 'y' => 556, 'z' => 500, '{' => 389, '|' => 280, '}' => 389, '~' => 584, chr(127) => 350, chr(128) => 556, chr(129) => 350, chr(130) => 278, chr(131) => 556, - chr(132) => 500, chr(133) => 1000, chr(134) => 556, chr(135) => 556, chr(136) => 333, chr(137) => 1000, chr(138) => 667, chr(139) => 333, chr(140) => 1000, chr(141) => 350, chr(142) => 611, chr(143) => 350, chr(144) => 350, chr(145) => 278, chr(146) => 278, chr(147) => 500, chr(148) => 500, chr(149) => 350, chr(150) => 556, chr(151) => 1000, chr(152) => 333, chr(153) => 1000, - chr(154) => 556, chr(155) => 333, chr(156) => 944, chr(157) => 350, chr(158) => 500, chr(159) => 667, chr(160) => 278, chr(161) => 333, chr(162) => 556, chr(163) => 556, chr(164) => 556, chr(165) => 556, chr(166) => 280, chr(167) => 556, chr(168) => 333, chr(169) => 737, chr(170) => 370, chr(171) => 556, chr(172) => 584, chr(173) => 333, chr(174) => 737, chr(175) => 333, - chr(176) => 400, chr(177) => 584, chr(178) => 333, chr(179) => 333, chr(180) => 333, chr(181) => 611, chr(182) => 556, chr(183) => 278, chr(184) => 333, chr(185) => 333, chr(186) => 365, chr(187) => 556, chr(188) => 834, chr(189) => 834, chr(190) => 834, chr(191) => 611, chr(192) => 722, chr(193) => 722, chr(194) => 722, chr(195) => 722, chr(196) => 722, chr(197) => 722, - chr(198) => 1000, chr(199) => 722, chr(200) => 667, chr(201) => 667, chr(202) => 667, chr(203) => 667, chr(204) => 278, chr(205) => 278, chr(206) => 278, chr(207) => 278, chr(208) => 722, chr(209) => 722, chr(210) => 778, chr(211) => 778, chr(212) => 778, chr(213) => 778, chr(214) => 778, chr(215) => 584, chr(216) => 778, chr(217) => 722, chr(218) => 722, chr(219) => 722, - chr(220) => 722, chr(221) => 667, chr(222) => 667, chr(223) => 611, chr(224) => 556, chr(225) => 556, chr(226) => 556, chr(227) => 556, chr(228) => 556, chr(229) => 556, chr(230) => 889, chr(231) => 556, chr(232) => 556, chr(233) => 556, chr(234) => 556, chr(235) => 556, chr(236) => 278, chr(237) => 278, chr(238) => 278, chr(239) => 278, chr(240) => 611, chr(241) => 611, - chr(242) => 611, chr(243) => 611, chr(244) => 611, chr(245) => 611, chr(246) => 611, chr(247) => 584, chr(248) => 611, chr(249) => 611, chr(250) => 611, chr(251) => 611, chr(252) => 611, chr(253) => 556, chr(254) => 611, chr(255) => 556]; +$cw = array( + chr(0)=>278,chr(1)=>278,chr(2)=>278,chr(3)=>278,chr(4)=>278,chr(5)=>278,chr(6)=>278,chr(7)=>278,chr(8)=>278,chr(9)=>278,chr(10)=>278,chr(11)=>278,chr(12)=>278,chr(13)=>278,chr(14)=>278,chr(15)=>278,chr(16)=>278,chr(17)=>278,chr(18)=>278,chr(19)=>278,chr(20)=>278,chr(21)=>278, + chr(22)=>278,chr(23)=>278,chr(24)=>278,chr(25)=>278,chr(26)=>278,chr(27)=>278,chr(28)=>278,chr(29)=>278,chr(30)=>278,chr(31)=>278,' '=>278,'!'=>333,'"'=>474,'#'=>556,'$'=>556,'%'=>889,'&'=>722,'\''=>238,'('=>333,')'=>333,'*'=>389,'+'=>584, + ','=>278,'-'=>333,'.'=>278,'/'=>278,'0'=>556,'1'=>556,'2'=>556,'3'=>556,'4'=>556,'5'=>556,'6'=>556,'7'=>556,'8'=>556,'9'=>556,':'=>333,';'=>333,'<'=>584,'='=>584,'>'=>584,'?'=>611,'@'=>975,'A'=>722, + 'B'=>722,'C'=>722,'D'=>722,'E'=>667,'F'=>611,'G'=>778,'H'=>722,'I'=>278,'J'=>556,'K'=>722,'L'=>611,'M'=>833,'N'=>722,'O'=>778,'P'=>667,'Q'=>778,'R'=>722,'S'=>667,'T'=>611,'U'=>722,'V'=>667,'W'=>944, + 'X'=>667,'Y'=>667,'Z'=>611,'['=>333,'\\'=>278,']'=>333,'^'=>584,'_'=>556,'`'=>333,'a'=>556,'b'=>611,'c'=>556,'d'=>611,'e'=>556,'f'=>333,'g'=>611,'h'=>611,'i'=>278,'j'=>278,'k'=>556,'l'=>278,'m'=>889, + 'n'=>611,'o'=>611,'p'=>611,'q'=>611,'r'=>389,'s'=>556,'t'=>333,'u'=>611,'v'=>556,'w'=>778,'x'=>556,'y'=>556,'z'=>500,'{'=>389,'|'=>280,'}'=>389,'~'=>584,chr(127)=>350,chr(128)=>556,chr(129)=>350,chr(130)=>278,chr(131)=>556, + chr(132)=>500,chr(133)=>1000,chr(134)=>556,chr(135)=>556,chr(136)=>333,chr(137)=>1000,chr(138)=>667,chr(139)=>333,chr(140)=>1000,chr(141)=>350,chr(142)=>611,chr(143)=>350,chr(144)=>350,chr(145)=>278,chr(146)=>278,chr(147)=>500,chr(148)=>500,chr(149)=>350,chr(150)=>556,chr(151)=>1000,chr(152)=>333,chr(153)=>1000, + chr(154)=>556,chr(155)=>333,chr(156)=>944,chr(157)=>350,chr(158)=>500,chr(159)=>667,chr(160)=>278,chr(161)=>333,chr(162)=>556,chr(163)=>556,chr(164)=>556,chr(165)=>556,chr(166)=>280,chr(167)=>556,chr(168)=>333,chr(169)=>737,chr(170)=>370,chr(171)=>556,chr(172)=>584,chr(173)=>333,chr(174)=>737,chr(175)=>333, + chr(176)=>400,chr(177)=>584,chr(178)=>333,chr(179)=>333,chr(180)=>333,chr(181)=>611,chr(182)=>556,chr(183)=>278,chr(184)=>333,chr(185)=>333,chr(186)=>365,chr(187)=>556,chr(188)=>834,chr(189)=>834,chr(190)=>834,chr(191)=>611,chr(192)=>722,chr(193)=>722,chr(194)=>722,chr(195)=>722,chr(196)=>722,chr(197)=>722, + chr(198)=>1000,chr(199)=>722,chr(200)=>667,chr(201)=>667,chr(202)=>667,chr(203)=>667,chr(204)=>278,chr(205)=>278,chr(206)=>278,chr(207)=>278,chr(208)=>722,chr(209)=>722,chr(210)=>778,chr(211)=>778,chr(212)=>778,chr(213)=>778,chr(214)=>778,chr(215)=>584,chr(216)=>778,chr(217)=>722,chr(218)=>722,chr(219)=>722, + chr(220)=>722,chr(221)=>667,chr(222)=>667,chr(223)=>611,chr(224)=>556,chr(225)=>556,chr(226)=>556,chr(227)=>556,chr(228)=>556,chr(229)=>556,chr(230)=>889,chr(231)=>556,chr(232)=>556,chr(233)=>556,chr(234)=>556,chr(235)=>556,chr(236)=>278,chr(237)=>278,chr(238)=>278,chr(239)=>278,chr(240)=>611,chr(241)=>611, + chr(242)=>611,chr(243)=>611,chr(244)=>611,chr(245)=>611,chr(246)=>611,chr(247)=>584,chr(248)=>611,chr(249)=>611,chr(250)=>611,chr(251)=>611,chr(252)=>611,chr(253)=>556,chr(254)=>611,chr(255)=>556); $enc = 'cp1252'; -$uv = [0 => [0, 128], 128 => 8364, 130 => 8218, 131 => 402, 132 => 8222, 133 => 8230, 134 => [8224, 2], 136 => 710, 137 => 8240, 138 => 352, 139 => 8249, 140 => 338, 142 => 381, 145 => [8216, 2], 147 => [8220, 2], 149 => 8226, 150 => [8211, 2], 152 => 732, 153 => 8482, 154 => 353, 155 => 8250, 156 => 339, 158 => 382, 159 => 376, 160 => [160, 96]]; +$uv = array(0=>array(0,128),128=>8364,130=>8218,131=>402,132=>8222,133=>8230,134=>array(8224,2),136=>710,137=>8240,138=>352,139=>8249,140=>338,142=>381,145=>array(8216,2),147=>array(8220,2),149=>8226,150=>array(8211,2),152=>732,153=>8482,154=>353,155=>8250,156=>339,158=>382,159=>376,160=>array(160,96)); +?> diff --git a/app/ThirdParty/fpdf/font/helveticabi.php b/app/ThirdParty/fpdf/font/helveticabi.php index 943017fb..0243cde4 100644 --- a/app/ThirdParty/fpdf/font/helveticabi.php +++ b/app/ThirdParty/fpdf/font/helveticabi.php @@ -1,21 +1,21 @@ 278, chr(1) => 278, chr(2) => 278, chr(3) => 278, chr(4) => 278, chr(5) => 278, chr(6) => 278, chr(7) => 278, chr(8) => 278, chr(9) => 278, chr(10) => 278, chr(11) => 278, chr(12) => 278, chr(13) => 278, chr(14) => 278, chr(15) => 278, chr(16) => 278, chr(17) => 278, chr(18) => 278, chr(19) => 278, chr(20) => 278, chr(21) => 278, - chr(22) => 278, chr(23) => 278, chr(24) => 278, chr(25) => 278, chr(26) => 278, chr(27) => 278, chr(28) => 278, chr(29) => 278, chr(30) => 278, chr(31) => 278, ' ' => 278, '!' => 333, '"' => 474, '#' => 556, '$' => 556, '%' => 889, '&' => 722, '\'' => 238, '(' => 333, ')' => 333, '*' => 389, '+' => 584, - ',' => 278, '-' => 333, '.' => 278, '/' => 278, '0' => 556, '1' => 556, '2' => 556, '3' => 556, '4' => 556, '5' => 556, '6' => 556, '7' => 556, '8' => 556, '9' => 556, ':' => 333, ';' => 333, '<' => 584, '=' => 584, '>' => 584, '?' => 611, '@' => 975, 'A' => 722, - 'B' => 722, 'C' => 722, 'D' => 722, 'E' => 667, 'F' => 611, 'G' => 778, 'H' => 722, 'I' => 278, 'J' => 556, 'K' => 722, 'L' => 611, 'M' => 833, 'N' => 722, 'O' => 778, 'P' => 667, 'Q' => 778, 'R' => 722, 'S' => 667, 'T' => 611, 'U' => 722, 'V' => 667, 'W' => 944, - 'X' => 667, 'Y' => 667, 'Z' => 611, '[' => 333, '\\' => 278, ']' => 333, '^' => 584, '_' => 556, '`' => 333, 'a' => 556, 'b' => 611, 'c' => 556, 'd' => 611, 'e' => 556, 'f' => 333, 'g' => 611, 'h' => 611, 'i' => 278, 'j' => 278, 'k' => 556, 'l' => 278, 'm' => 889, - 'n' => 611, 'o' => 611, 'p' => 611, 'q' => 611, 'r' => 389, 's' => 556, 't' => 333, 'u' => 611, 'v' => 556, 'w' => 778, 'x' => 556, 'y' => 556, 'z' => 500, '{' => 389, '|' => 280, '}' => 389, '~' => 584, chr(127) => 350, chr(128) => 556, chr(129) => 350, chr(130) => 278, chr(131) => 556, - chr(132) => 500, chr(133) => 1000, chr(134) => 556, chr(135) => 556, chr(136) => 333, chr(137) => 1000, chr(138) => 667, chr(139) => 333, chr(140) => 1000, chr(141) => 350, chr(142) => 611, chr(143) => 350, chr(144) => 350, chr(145) => 278, chr(146) => 278, chr(147) => 500, chr(148) => 500, chr(149) => 350, chr(150) => 556, chr(151) => 1000, chr(152) => 333, chr(153) => 1000, - chr(154) => 556, chr(155) => 333, chr(156) => 944, chr(157) => 350, chr(158) => 500, chr(159) => 667, chr(160) => 278, chr(161) => 333, chr(162) => 556, chr(163) => 556, chr(164) => 556, chr(165) => 556, chr(166) => 280, chr(167) => 556, chr(168) => 333, chr(169) => 737, chr(170) => 370, chr(171) => 556, chr(172) => 584, chr(173) => 333, chr(174) => 737, chr(175) => 333, - chr(176) => 400, chr(177) => 584, chr(178) => 333, chr(179) => 333, chr(180) => 333, chr(181) => 611, chr(182) => 556, chr(183) => 278, chr(184) => 333, chr(185) => 333, chr(186) => 365, chr(187) => 556, chr(188) => 834, chr(189) => 834, chr(190) => 834, chr(191) => 611, chr(192) => 722, chr(193) => 722, chr(194) => 722, chr(195) => 722, chr(196) => 722, chr(197) => 722, - chr(198) => 1000, chr(199) => 722, chr(200) => 667, chr(201) => 667, chr(202) => 667, chr(203) => 667, chr(204) => 278, chr(205) => 278, chr(206) => 278, chr(207) => 278, chr(208) => 722, chr(209) => 722, chr(210) => 778, chr(211) => 778, chr(212) => 778, chr(213) => 778, chr(214) => 778, chr(215) => 584, chr(216) => 778, chr(217) => 722, chr(218) => 722, chr(219) => 722, - chr(220) => 722, chr(221) => 667, chr(222) => 667, chr(223) => 611, chr(224) => 556, chr(225) => 556, chr(226) => 556, chr(227) => 556, chr(228) => 556, chr(229) => 556, chr(230) => 889, chr(231) => 556, chr(232) => 556, chr(233) => 556, chr(234) => 556, chr(235) => 556, chr(236) => 278, chr(237) => 278, chr(238) => 278, chr(239) => 278, chr(240) => 611, chr(241) => 611, - chr(242) => 611, chr(243) => 611, chr(244) => 611, chr(245) => 611, chr(246) => 611, chr(247) => 584, chr(248) => 611, chr(249) => 611, chr(250) => 611, chr(251) => 611, chr(252) => 611, chr(253) => 556, chr(254) => 611, chr(255) => 556]; +$cw = array( + chr(0)=>278,chr(1)=>278,chr(2)=>278,chr(3)=>278,chr(4)=>278,chr(5)=>278,chr(6)=>278,chr(7)=>278,chr(8)=>278,chr(9)=>278,chr(10)=>278,chr(11)=>278,chr(12)=>278,chr(13)=>278,chr(14)=>278,chr(15)=>278,chr(16)=>278,chr(17)=>278,chr(18)=>278,chr(19)=>278,chr(20)=>278,chr(21)=>278, + chr(22)=>278,chr(23)=>278,chr(24)=>278,chr(25)=>278,chr(26)=>278,chr(27)=>278,chr(28)=>278,chr(29)=>278,chr(30)=>278,chr(31)=>278,' '=>278,'!'=>333,'"'=>474,'#'=>556,'$'=>556,'%'=>889,'&'=>722,'\''=>238,'('=>333,')'=>333,'*'=>389,'+'=>584, + ','=>278,'-'=>333,'.'=>278,'/'=>278,'0'=>556,'1'=>556,'2'=>556,'3'=>556,'4'=>556,'5'=>556,'6'=>556,'7'=>556,'8'=>556,'9'=>556,':'=>333,';'=>333,'<'=>584,'='=>584,'>'=>584,'?'=>611,'@'=>975,'A'=>722, + 'B'=>722,'C'=>722,'D'=>722,'E'=>667,'F'=>611,'G'=>778,'H'=>722,'I'=>278,'J'=>556,'K'=>722,'L'=>611,'M'=>833,'N'=>722,'O'=>778,'P'=>667,'Q'=>778,'R'=>722,'S'=>667,'T'=>611,'U'=>722,'V'=>667,'W'=>944, + 'X'=>667,'Y'=>667,'Z'=>611,'['=>333,'\\'=>278,']'=>333,'^'=>584,'_'=>556,'`'=>333,'a'=>556,'b'=>611,'c'=>556,'d'=>611,'e'=>556,'f'=>333,'g'=>611,'h'=>611,'i'=>278,'j'=>278,'k'=>556,'l'=>278,'m'=>889, + 'n'=>611,'o'=>611,'p'=>611,'q'=>611,'r'=>389,'s'=>556,'t'=>333,'u'=>611,'v'=>556,'w'=>778,'x'=>556,'y'=>556,'z'=>500,'{'=>389,'|'=>280,'}'=>389,'~'=>584,chr(127)=>350,chr(128)=>556,chr(129)=>350,chr(130)=>278,chr(131)=>556, + chr(132)=>500,chr(133)=>1000,chr(134)=>556,chr(135)=>556,chr(136)=>333,chr(137)=>1000,chr(138)=>667,chr(139)=>333,chr(140)=>1000,chr(141)=>350,chr(142)=>611,chr(143)=>350,chr(144)=>350,chr(145)=>278,chr(146)=>278,chr(147)=>500,chr(148)=>500,chr(149)=>350,chr(150)=>556,chr(151)=>1000,chr(152)=>333,chr(153)=>1000, + chr(154)=>556,chr(155)=>333,chr(156)=>944,chr(157)=>350,chr(158)=>500,chr(159)=>667,chr(160)=>278,chr(161)=>333,chr(162)=>556,chr(163)=>556,chr(164)=>556,chr(165)=>556,chr(166)=>280,chr(167)=>556,chr(168)=>333,chr(169)=>737,chr(170)=>370,chr(171)=>556,chr(172)=>584,chr(173)=>333,chr(174)=>737,chr(175)=>333, + chr(176)=>400,chr(177)=>584,chr(178)=>333,chr(179)=>333,chr(180)=>333,chr(181)=>611,chr(182)=>556,chr(183)=>278,chr(184)=>333,chr(185)=>333,chr(186)=>365,chr(187)=>556,chr(188)=>834,chr(189)=>834,chr(190)=>834,chr(191)=>611,chr(192)=>722,chr(193)=>722,chr(194)=>722,chr(195)=>722,chr(196)=>722,chr(197)=>722, + chr(198)=>1000,chr(199)=>722,chr(200)=>667,chr(201)=>667,chr(202)=>667,chr(203)=>667,chr(204)=>278,chr(205)=>278,chr(206)=>278,chr(207)=>278,chr(208)=>722,chr(209)=>722,chr(210)=>778,chr(211)=>778,chr(212)=>778,chr(213)=>778,chr(214)=>778,chr(215)=>584,chr(216)=>778,chr(217)=>722,chr(218)=>722,chr(219)=>722, + chr(220)=>722,chr(221)=>667,chr(222)=>667,chr(223)=>611,chr(224)=>556,chr(225)=>556,chr(226)=>556,chr(227)=>556,chr(228)=>556,chr(229)=>556,chr(230)=>889,chr(231)=>556,chr(232)=>556,chr(233)=>556,chr(234)=>556,chr(235)=>556,chr(236)=>278,chr(237)=>278,chr(238)=>278,chr(239)=>278,chr(240)=>611,chr(241)=>611, + chr(242)=>611,chr(243)=>611,chr(244)=>611,chr(245)=>611,chr(246)=>611,chr(247)=>584,chr(248)=>611,chr(249)=>611,chr(250)=>611,chr(251)=>611,chr(252)=>611,chr(253)=>556,chr(254)=>611,chr(255)=>556); $enc = 'cp1252'; -$uv = [0 => [0, 128], 128 => 8364, 130 => 8218, 131 => 402, 132 => 8222, 133 => 8230, 134 => [8224, 2], 136 => 710, 137 => 8240, 138 => 352, 139 => 8249, 140 => 338, 142 => 381, 145 => [8216, 2], 147 => [8220, 2], 149 => 8226, 150 => [8211, 2], 152 => 732, 153 => 8482, 154 => 353, 155 => 8250, 156 => 339, 158 => 382, 159 => 376, 160 => [160, 96]]; +$uv = array(0=>array(0,128),128=>8364,130=>8218,131=>402,132=>8222,133=>8230,134=>array(8224,2),136=>710,137=>8240,138=>352,139=>8249,140=>338,142=>381,145=>array(8216,2),147=>array(8220,2),149=>8226,150=>array(8211,2),152=>732,153=>8482,154=>353,155=>8250,156=>339,158=>382,159=>376,160=>array(160,96)); +?> diff --git a/app/ThirdParty/fpdf/font/helveticai.php b/app/ThirdParty/fpdf/font/helveticai.php index 5b05ec1a..06ec735b 100644 --- a/app/ThirdParty/fpdf/font/helveticai.php +++ b/app/ThirdParty/fpdf/font/helveticai.php @@ -1,21 +1,21 @@ 278, chr(1) => 278, chr(2) => 278, chr(3) => 278, chr(4) => 278, chr(5) => 278, chr(6) => 278, chr(7) => 278, chr(8) => 278, chr(9) => 278, chr(10) => 278, chr(11) => 278, chr(12) => 278, chr(13) => 278, chr(14) => 278, chr(15) => 278, chr(16) => 278, chr(17) => 278, chr(18) => 278, chr(19) => 278, chr(20) => 278, chr(21) => 278, - chr(22) => 278, chr(23) => 278, chr(24) => 278, chr(25) => 278, chr(26) => 278, chr(27) => 278, chr(28) => 278, chr(29) => 278, chr(30) => 278, chr(31) => 278, ' ' => 278, '!' => 278, '"' => 355, '#' => 556, '$' => 556, '%' => 889, '&' => 667, '\'' => 191, '(' => 333, ')' => 333, '*' => 389, '+' => 584, - ',' => 278, '-' => 333, '.' => 278, '/' => 278, '0' => 556, '1' => 556, '2' => 556, '3' => 556, '4' => 556, '5' => 556, '6' => 556, '7' => 556, '8' => 556, '9' => 556, ':' => 278, ';' => 278, '<' => 584, '=' => 584, '>' => 584, '?' => 556, '@' => 1015, 'A' => 667, - 'B' => 667, 'C' => 722, 'D' => 722, 'E' => 667, 'F' => 611, 'G' => 778, 'H' => 722, 'I' => 278, 'J' => 500, 'K' => 667, 'L' => 556, 'M' => 833, 'N' => 722, 'O' => 778, 'P' => 667, 'Q' => 778, 'R' => 722, 'S' => 667, 'T' => 611, 'U' => 722, 'V' => 667, 'W' => 944, - 'X' => 667, 'Y' => 667, 'Z' => 611, '[' => 278, '\\' => 278, ']' => 278, '^' => 469, '_' => 556, '`' => 333, 'a' => 556, 'b' => 556, 'c' => 500, 'd' => 556, 'e' => 556, 'f' => 278, 'g' => 556, 'h' => 556, 'i' => 222, 'j' => 222, 'k' => 500, 'l' => 222, 'm' => 833, - 'n' => 556, 'o' => 556, 'p' => 556, 'q' => 556, 'r' => 333, 's' => 500, 't' => 278, 'u' => 556, 'v' => 500, 'w' => 722, 'x' => 500, 'y' => 500, 'z' => 500, '{' => 334, '|' => 260, '}' => 334, '~' => 584, chr(127) => 350, chr(128) => 556, chr(129) => 350, chr(130) => 222, chr(131) => 556, - chr(132) => 333, chr(133) => 1000, chr(134) => 556, chr(135) => 556, chr(136) => 333, chr(137) => 1000, chr(138) => 667, chr(139) => 333, chr(140) => 1000, chr(141) => 350, chr(142) => 611, chr(143) => 350, chr(144) => 350, chr(145) => 222, chr(146) => 222, chr(147) => 333, chr(148) => 333, chr(149) => 350, chr(150) => 556, chr(151) => 1000, chr(152) => 333, chr(153) => 1000, - chr(154) => 500, chr(155) => 333, chr(156) => 944, chr(157) => 350, chr(158) => 500, chr(159) => 667, chr(160) => 278, chr(161) => 333, chr(162) => 556, chr(163) => 556, chr(164) => 556, chr(165) => 556, chr(166) => 260, chr(167) => 556, chr(168) => 333, chr(169) => 737, chr(170) => 370, chr(171) => 556, chr(172) => 584, chr(173) => 333, chr(174) => 737, chr(175) => 333, - chr(176) => 400, chr(177) => 584, chr(178) => 333, chr(179) => 333, chr(180) => 333, chr(181) => 556, chr(182) => 537, chr(183) => 278, chr(184) => 333, chr(185) => 333, chr(186) => 365, chr(187) => 556, chr(188) => 834, chr(189) => 834, chr(190) => 834, chr(191) => 611, chr(192) => 667, chr(193) => 667, chr(194) => 667, chr(195) => 667, chr(196) => 667, chr(197) => 667, - chr(198) => 1000, chr(199) => 722, chr(200) => 667, chr(201) => 667, chr(202) => 667, chr(203) => 667, chr(204) => 278, chr(205) => 278, chr(206) => 278, chr(207) => 278, chr(208) => 722, chr(209) => 722, chr(210) => 778, chr(211) => 778, chr(212) => 778, chr(213) => 778, chr(214) => 778, chr(215) => 584, chr(216) => 778, chr(217) => 722, chr(218) => 722, chr(219) => 722, - chr(220) => 722, chr(221) => 667, chr(222) => 667, chr(223) => 611, chr(224) => 556, chr(225) => 556, chr(226) => 556, chr(227) => 556, chr(228) => 556, chr(229) => 556, chr(230) => 889, chr(231) => 500, chr(232) => 556, chr(233) => 556, chr(234) => 556, chr(235) => 556, chr(236) => 278, chr(237) => 278, chr(238) => 278, chr(239) => 278, chr(240) => 556, chr(241) => 556, - chr(242) => 556, chr(243) => 556, chr(244) => 556, chr(245) => 556, chr(246) => 556, chr(247) => 584, chr(248) => 611, chr(249) => 556, chr(250) => 556, chr(251) => 556, chr(252) => 556, chr(253) => 500, chr(254) => 556, chr(255) => 500]; +$cw = array( + chr(0)=>278,chr(1)=>278,chr(2)=>278,chr(3)=>278,chr(4)=>278,chr(5)=>278,chr(6)=>278,chr(7)=>278,chr(8)=>278,chr(9)=>278,chr(10)=>278,chr(11)=>278,chr(12)=>278,chr(13)=>278,chr(14)=>278,chr(15)=>278,chr(16)=>278,chr(17)=>278,chr(18)=>278,chr(19)=>278,chr(20)=>278,chr(21)=>278, + chr(22)=>278,chr(23)=>278,chr(24)=>278,chr(25)=>278,chr(26)=>278,chr(27)=>278,chr(28)=>278,chr(29)=>278,chr(30)=>278,chr(31)=>278,' '=>278,'!'=>278,'"'=>355,'#'=>556,'$'=>556,'%'=>889,'&'=>667,'\''=>191,'('=>333,')'=>333,'*'=>389,'+'=>584, + ','=>278,'-'=>333,'.'=>278,'/'=>278,'0'=>556,'1'=>556,'2'=>556,'3'=>556,'4'=>556,'5'=>556,'6'=>556,'7'=>556,'8'=>556,'9'=>556,':'=>278,';'=>278,'<'=>584,'='=>584,'>'=>584,'?'=>556,'@'=>1015,'A'=>667, + 'B'=>667,'C'=>722,'D'=>722,'E'=>667,'F'=>611,'G'=>778,'H'=>722,'I'=>278,'J'=>500,'K'=>667,'L'=>556,'M'=>833,'N'=>722,'O'=>778,'P'=>667,'Q'=>778,'R'=>722,'S'=>667,'T'=>611,'U'=>722,'V'=>667,'W'=>944, + 'X'=>667,'Y'=>667,'Z'=>611,'['=>278,'\\'=>278,']'=>278,'^'=>469,'_'=>556,'`'=>333,'a'=>556,'b'=>556,'c'=>500,'d'=>556,'e'=>556,'f'=>278,'g'=>556,'h'=>556,'i'=>222,'j'=>222,'k'=>500,'l'=>222,'m'=>833, + 'n'=>556,'o'=>556,'p'=>556,'q'=>556,'r'=>333,'s'=>500,'t'=>278,'u'=>556,'v'=>500,'w'=>722,'x'=>500,'y'=>500,'z'=>500,'{'=>334,'|'=>260,'}'=>334,'~'=>584,chr(127)=>350,chr(128)=>556,chr(129)=>350,chr(130)=>222,chr(131)=>556, + chr(132)=>333,chr(133)=>1000,chr(134)=>556,chr(135)=>556,chr(136)=>333,chr(137)=>1000,chr(138)=>667,chr(139)=>333,chr(140)=>1000,chr(141)=>350,chr(142)=>611,chr(143)=>350,chr(144)=>350,chr(145)=>222,chr(146)=>222,chr(147)=>333,chr(148)=>333,chr(149)=>350,chr(150)=>556,chr(151)=>1000,chr(152)=>333,chr(153)=>1000, + chr(154)=>500,chr(155)=>333,chr(156)=>944,chr(157)=>350,chr(158)=>500,chr(159)=>667,chr(160)=>278,chr(161)=>333,chr(162)=>556,chr(163)=>556,chr(164)=>556,chr(165)=>556,chr(166)=>260,chr(167)=>556,chr(168)=>333,chr(169)=>737,chr(170)=>370,chr(171)=>556,chr(172)=>584,chr(173)=>333,chr(174)=>737,chr(175)=>333, + chr(176)=>400,chr(177)=>584,chr(178)=>333,chr(179)=>333,chr(180)=>333,chr(181)=>556,chr(182)=>537,chr(183)=>278,chr(184)=>333,chr(185)=>333,chr(186)=>365,chr(187)=>556,chr(188)=>834,chr(189)=>834,chr(190)=>834,chr(191)=>611,chr(192)=>667,chr(193)=>667,chr(194)=>667,chr(195)=>667,chr(196)=>667,chr(197)=>667, + chr(198)=>1000,chr(199)=>722,chr(200)=>667,chr(201)=>667,chr(202)=>667,chr(203)=>667,chr(204)=>278,chr(205)=>278,chr(206)=>278,chr(207)=>278,chr(208)=>722,chr(209)=>722,chr(210)=>778,chr(211)=>778,chr(212)=>778,chr(213)=>778,chr(214)=>778,chr(215)=>584,chr(216)=>778,chr(217)=>722,chr(218)=>722,chr(219)=>722, + chr(220)=>722,chr(221)=>667,chr(222)=>667,chr(223)=>611,chr(224)=>556,chr(225)=>556,chr(226)=>556,chr(227)=>556,chr(228)=>556,chr(229)=>556,chr(230)=>889,chr(231)=>500,chr(232)=>556,chr(233)=>556,chr(234)=>556,chr(235)=>556,chr(236)=>278,chr(237)=>278,chr(238)=>278,chr(239)=>278,chr(240)=>556,chr(241)=>556, + chr(242)=>556,chr(243)=>556,chr(244)=>556,chr(245)=>556,chr(246)=>556,chr(247)=>584,chr(248)=>611,chr(249)=>556,chr(250)=>556,chr(251)=>556,chr(252)=>556,chr(253)=>500,chr(254)=>556,chr(255)=>500); $enc = 'cp1252'; -$uv = [0 => [0, 128], 128 => 8364, 130 => 8218, 131 => 402, 132 => 8222, 133 => 8230, 134 => [8224, 2], 136 => 710, 137 => 8240, 138 => 352, 139 => 8249, 140 => 338, 142 => 381, 145 => [8216, 2], 147 => [8220, 2], 149 => 8226, 150 => [8211, 2], 152 => 732, 153 => 8482, 154 => 353, 155 => 8250, 156 => 339, 158 => 382, 159 => 376, 160 => [160, 96]]; +$uv = array(0=>array(0,128),128=>8364,130=>8218,131=>402,132=>8222,133=>8230,134=>array(8224,2),136=>710,137=>8240,138=>352,139=>8249,140=>338,142=>381,145=>array(8216,2),147=>array(8220,2),149=>8226,150=>array(8211,2),152=>732,153=>8482,154=>353,155=>8250,156=>339,158=>382,159=>376,160=>array(160,96)); +?> diff --git a/app/ThirdParty/fpdf/font/symbol.php b/app/ThirdParty/fpdf/font/symbol.php index 8580057f..f8f0c330 100644 --- a/app/ThirdParty/fpdf/font/symbol.php +++ b/app/ThirdParty/fpdf/font/symbol.php @@ -1,20 +1,20 @@ 250, chr(1) => 250, chr(2) => 250, chr(3) => 250, chr(4) => 250, chr(5) => 250, chr(6) => 250, chr(7) => 250, chr(8) => 250, chr(9) => 250, chr(10) => 250, chr(11) => 250, chr(12) => 250, chr(13) => 250, chr(14) => 250, chr(15) => 250, chr(16) => 250, chr(17) => 250, chr(18) => 250, chr(19) => 250, chr(20) => 250, chr(21) => 250, - chr(22) => 250, chr(23) => 250, chr(24) => 250, chr(25) => 250, chr(26) => 250, chr(27) => 250, chr(28) => 250, chr(29) => 250, chr(30) => 250, chr(31) => 250, ' ' => 250, '!' => 333, '"' => 713, '#' => 500, '$' => 549, '%' => 833, '&' => 778, '\'' => 439, '(' => 333, ')' => 333, '*' => 500, '+' => 549, - ',' => 250, '-' => 549, '.' => 250, '/' => 278, '0' => 500, '1' => 500, '2' => 500, '3' => 500, '4' => 500, '5' => 500, '6' => 500, '7' => 500, '8' => 500, '9' => 500, ':' => 278, ';' => 278, '<' => 549, '=' => 549, '>' => 549, '?' => 444, '@' => 549, 'A' => 722, - 'B' => 667, 'C' => 722, 'D' => 612, 'E' => 611, 'F' => 763, 'G' => 603, 'H' => 722, 'I' => 333, 'J' => 631, 'K' => 722, 'L' => 686, 'M' => 889, 'N' => 722, 'O' => 722, 'P' => 768, 'Q' => 741, 'R' => 556, 'S' => 592, 'T' => 611, 'U' => 690, 'V' => 439, 'W' => 768, - 'X' => 645, 'Y' => 795, 'Z' => 611, '[' => 333, '\\' => 863, ']' => 333, '^' => 658, '_' => 500, '`' => 500, 'a' => 631, 'b' => 549, 'c' => 549, 'd' => 494, 'e' => 439, 'f' => 521, 'g' => 411, 'h' => 603, 'i' => 329, 'j' => 603, 'k' => 549, 'l' => 549, 'm' => 576, - 'n' => 521, 'o' => 549, 'p' => 549, 'q' => 521, 'r' => 549, 's' => 603, 't' => 439, 'u' => 576, 'v' => 713, 'w' => 686, 'x' => 493, 'y' => 686, 'z' => 494, '{' => 480, '|' => 200, '}' => 480, '~' => 549, chr(127) => 0, chr(128) => 0, chr(129) => 0, chr(130) => 0, chr(131) => 0, - chr(132) => 0, chr(133) => 0, chr(134) => 0, chr(135) => 0, chr(136) => 0, chr(137) => 0, chr(138) => 0, chr(139) => 0, chr(140) => 0, chr(141) => 0, chr(142) => 0, chr(143) => 0, chr(144) => 0, chr(145) => 0, chr(146) => 0, chr(147) => 0, chr(148) => 0, chr(149) => 0, chr(150) => 0, chr(151) => 0, chr(152) => 0, chr(153) => 0, - chr(154) => 0, chr(155) => 0, chr(156) => 0, chr(157) => 0, chr(158) => 0, chr(159) => 0, chr(160) => 750, chr(161) => 620, chr(162) => 247, chr(163) => 549, chr(164) => 167, chr(165) => 713, chr(166) => 500, chr(167) => 753, chr(168) => 753, chr(169) => 753, chr(170) => 753, chr(171) => 1042, chr(172) => 987, chr(173) => 603, chr(174) => 987, chr(175) => 603, - chr(176) => 400, chr(177) => 549, chr(178) => 411, chr(179) => 549, chr(180) => 549, chr(181) => 713, chr(182) => 494, chr(183) => 460, chr(184) => 549, chr(185) => 549, chr(186) => 549, chr(187) => 549, chr(188) => 1000, chr(189) => 603, chr(190) => 1000, chr(191) => 658, chr(192) => 823, chr(193) => 686, chr(194) => 795, chr(195) => 987, chr(196) => 768, chr(197) => 768, - chr(198) => 823, chr(199) => 768, chr(200) => 768, chr(201) => 713, chr(202) => 713, chr(203) => 713, chr(204) => 713, chr(205) => 713, chr(206) => 713, chr(207) => 713, chr(208) => 768, chr(209) => 713, chr(210) => 790, chr(211) => 790, chr(212) => 890, chr(213) => 823, chr(214) => 549, chr(215) => 250, chr(216) => 713, chr(217) => 603, chr(218) => 603, chr(219) => 1042, - chr(220) => 987, chr(221) => 603, chr(222) => 987, chr(223) => 603, chr(224) => 494, chr(225) => 329, chr(226) => 790, chr(227) => 790, chr(228) => 786, chr(229) => 713, chr(230) => 384, chr(231) => 384, chr(232) => 384, chr(233) => 384, chr(234) => 384, chr(235) => 384, chr(236) => 494, chr(237) => 494, chr(238) => 494, chr(239) => 494, chr(240) => 0, chr(241) => 329, - chr(242) => 274, chr(243) => 686, chr(244) => 686, chr(245) => 686, chr(246) => 384, chr(247) => 384, chr(248) => 384, chr(249) => 384, chr(250) => 384, chr(251) => 384, chr(252) => 494, chr(253) => 494, chr(254) => 494, chr(255) => 0]; -$uv = [32 => 160, 33 => 33, 34 => 8704, 35 => 35, 36 => 8707, 37 => [37, 2], 39 => 8715, 40 => [40, 2], 42 => 8727, 43 => [43, 2], 45 => 8722, 46 => [46, 18], 64 => 8773, 65 => [913, 2], 67 => 935, 68 => [916, 2], 70 => 934, 71 => 915, 72 => 919, 73 => 921, 74 => 977, 75 => [922, 4], 79 => [927, 2], 81 => 920, 82 => 929, 83 => [931, 3], 86 => 962, 87 => 937, 88 => 926, 89 => 936, 90 => 918, 91 => 91, 92 => 8756, 93 => 93, 94 => 8869, 95 => 95, 96 => 63717, 97 => [945, 2], 99 => 967, 100 => [948, 2], 102 => 966, 103 => 947, 104 => 951, 105 => 953, 106 => 981, 107 => [954, 4], 111 => [959, 2], 113 => 952, 114 => 961, 115 => [963, 3], 118 => 982, 119 => 969, 120 => 958, 121 => 968, 122 => 950, 123 => [123, 3], 126 => 8764, 160 => 8364, 161 => 978, 162 => 8242, 163 => 8804, 164 => 8725, 165 => 8734, 166 => 402, 167 => 9827, 168 => 9830, 169 => 9829, 170 => 9824, 171 => 8596, 172 => [8592, 4], 176 => [176, 2], 178 => 8243, 179 => 8805, 180 => 215, 181 => 8733, 182 => 8706, 183 => 8226, 184 => 247, 185 => [8800, 2], 187 => 8776, 188 => 8230, 189 => [63718, 2], 191 => 8629, 192 => 8501, 193 => 8465, 194 => 8476, 195 => 8472, 196 => 8855, 197 => 8853, 198 => 8709, 199 => [8745, 2], 201 => 8835, 202 => 8839, 203 => 8836, 204 => 8834, 205 => 8838, 206 => [8712, 2], 208 => 8736, 209 => 8711, 210 => 63194, 211 => 63193, 212 => 63195, 213 => 8719, 214 => 8730, 215 => 8901, 216 => 172, 217 => [8743, 2], 219 => 8660, 220 => [8656, 4], 224 => 9674, 225 => 9001, 226 => [63720, 3], 229 => 8721, 230 => [63723, 10], 241 => 9002, 242 => 8747, 243 => 8992, 244 => 63733, 245 => 8993, 246 => [63734, 9]]; +$cw = array( + chr(0)=>250,chr(1)=>250,chr(2)=>250,chr(3)=>250,chr(4)=>250,chr(5)=>250,chr(6)=>250,chr(7)=>250,chr(8)=>250,chr(9)=>250,chr(10)=>250,chr(11)=>250,chr(12)=>250,chr(13)=>250,chr(14)=>250,chr(15)=>250,chr(16)=>250,chr(17)=>250,chr(18)=>250,chr(19)=>250,chr(20)=>250,chr(21)=>250, + chr(22)=>250,chr(23)=>250,chr(24)=>250,chr(25)=>250,chr(26)=>250,chr(27)=>250,chr(28)=>250,chr(29)=>250,chr(30)=>250,chr(31)=>250,' '=>250,'!'=>333,'"'=>713,'#'=>500,'$'=>549,'%'=>833,'&'=>778,'\''=>439,'('=>333,')'=>333,'*'=>500,'+'=>549, + ','=>250,'-'=>549,'.'=>250,'/'=>278,'0'=>500,'1'=>500,'2'=>500,'3'=>500,'4'=>500,'5'=>500,'6'=>500,'7'=>500,'8'=>500,'9'=>500,':'=>278,';'=>278,'<'=>549,'='=>549,'>'=>549,'?'=>444,'@'=>549,'A'=>722, + 'B'=>667,'C'=>722,'D'=>612,'E'=>611,'F'=>763,'G'=>603,'H'=>722,'I'=>333,'J'=>631,'K'=>722,'L'=>686,'M'=>889,'N'=>722,'O'=>722,'P'=>768,'Q'=>741,'R'=>556,'S'=>592,'T'=>611,'U'=>690,'V'=>439,'W'=>768, + 'X'=>645,'Y'=>795,'Z'=>611,'['=>333,'\\'=>863,']'=>333,'^'=>658,'_'=>500,'`'=>500,'a'=>631,'b'=>549,'c'=>549,'d'=>494,'e'=>439,'f'=>521,'g'=>411,'h'=>603,'i'=>329,'j'=>603,'k'=>549,'l'=>549,'m'=>576, + 'n'=>521,'o'=>549,'p'=>549,'q'=>521,'r'=>549,'s'=>603,'t'=>439,'u'=>576,'v'=>713,'w'=>686,'x'=>493,'y'=>686,'z'=>494,'{'=>480,'|'=>200,'}'=>480,'~'=>549,chr(127)=>0,chr(128)=>0,chr(129)=>0,chr(130)=>0,chr(131)=>0, + chr(132)=>0,chr(133)=>0,chr(134)=>0,chr(135)=>0,chr(136)=>0,chr(137)=>0,chr(138)=>0,chr(139)=>0,chr(140)=>0,chr(141)=>0,chr(142)=>0,chr(143)=>0,chr(144)=>0,chr(145)=>0,chr(146)=>0,chr(147)=>0,chr(148)=>0,chr(149)=>0,chr(150)=>0,chr(151)=>0,chr(152)=>0,chr(153)=>0, + chr(154)=>0,chr(155)=>0,chr(156)=>0,chr(157)=>0,chr(158)=>0,chr(159)=>0,chr(160)=>750,chr(161)=>620,chr(162)=>247,chr(163)=>549,chr(164)=>167,chr(165)=>713,chr(166)=>500,chr(167)=>753,chr(168)=>753,chr(169)=>753,chr(170)=>753,chr(171)=>1042,chr(172)=>987,chr(173)=>603,chr(174)=>987,chr(175)=>603, + chr(176)=>400,chr(177)=>549,chr(178)=>411,chr(179)=>549,chr(180)=>549,chr(181)=>713,chr(182)=>494,chr(183)=>460,chr(184)=>549,chr(185)=>549,chr(186)=>549,chr(187)=>549,chr(188)=>1000,chr(189)=>603,chr(190)=>1000,chr(191)=>658,chr(192)=>823,chr(193)=>686,chr(194)=>795,chr(195)=>987,chr(196)=>768,chr(197)=>768, + chr(198)=>823,chr(199)=>768,chr(200)=>768,chr(201)=>713,chr(202)=>713,chr(203)=>713,chr(204)=>713,chr(205)=>713,chr(206)=>713,chr(207)=>713,chr(208)=>768,chr(209)=>713,chr(210)=>790,chr(211)=>790,chr(212)=>890,chr(213)=>823,chr(214)=>549,chr(215)=>250,chr(216)=>713,chr(217)=>603,chr(218)=>603,chr(219)=>1042, + chr(220)=>987,chr(221)=>603,chr(222)=>987,chr(223)=>603,chr(224)=>494,chr(225)=>329,chr(226)=>790,chr(227)=>790,chr(228)=>786,chr(229)=>713,chr(230)=>384,chr(231)=>384,chr(232)=>384,chr(233)=>384,chr(234)=>384,chr(235)=>384,chr(236)=>494,chr(237)=>494,chr(238)=>494,chr(239)=>494,chr(240)=>0,chr(241)=>329, + chr(242)=>274,chr(243)=>686,chr(244)=>686,chr(245)=>686,chr(246)=>384,chr(247)=>384,chr(248)=>384,chr(249)=>384,chr(250)=>384,chr(251)=>384,chr(252)=>494,chr(253)=>494,chr(254)=>494,chr(255)=>0); +$uv = array(32=>160,33=>33,34=>8704,35=>35,36=>8707,37=>array(37,2),39=>8715,40=>array(40,2),42=>8727,43=>array(43,2),45=>8722,46=>array(46,18),64=>8773,65=>array(913,2),67=>935,68=>array(916,2),70=>934,71=>915,72=>919,73=>921,74=>977,75=>array(922,4),79=>array(927,2),81=>920,82=>929,83=>array(931,3),86=>962,87=>937,88=>926,89=>936,90=>918,91=>91,92=>8756,93=>93,94=>8869,95=>95,96=>63717,97=>array(945,2),99=>967,100=>array(948,2),102=>966,103=>947,104=>951,105=>953,106=>981,107=>array(954,4),111=>array(959,2),113=>952,114=>961,115=>array(963,3),118=>982,119=>969,120=>958,121=>968,122=>950,123=>array(123,3),126=>8764,160=>8364,161=>978,162=>8242,163=>8804,164=>8725,165=>8734,166=>402,167=>9827,168=>9830,169=>9829,170=>9824,171=>8596,172=>array(8592,4),176=>array(176,2),178=>8243,179=>8805,180=>215,181=>8733,182=>8706,183=>8226,184=>247,185=>array(8800,2),187=>8776,188=>8230,189=>array(63718,2),191=>8629,192=>8501,193=>8465,194=>8476,195=>8472,196=>8855,197=>8853,198=>8709,199=>array(8745,2),201=>8835,202=>8839,203=>8836,204=>8834,205=>8838,206=>array(8712,2),208=>8736,209=>8711,210=>63194,211=>63193,212=>63195,213=>8719,214=>8730,215=>8901,216=>172,217=>array(8743,2),219=>8660,220=>array(8656,4),224=>9674,225=>9001,226=>array(63720,3),229=>8721,230=>array(63723,10),241=>9002,242=>8747,243=>8992,244=>63733,245=>8993,246=>array(63734,9)); +?> diff --git a/app/ThirdParty/fpdf/font/times.php b/app/ThirdParty/fpdf/font/times.php index 68170a03..81f2a8b4 100644 --- a/app/ThirdParty/fpdf/font/times.php +++ b/app/ThirdParty/fpdf/font/times.php @@ -1,21 +1,21 @@ 250, chr(1) => 250, chr(2) => 250, chr(3) => 250, chr(4) => 250, chr(5) => 250, chr(6) => 250, chr(7) => 250, chr(8) => 250, chr(9) => 250, chr(10) => 250, chr(11) => 250, chr(12) => 250, chr(13) => 250, chr(14) => 250, chr(15) => 250, chr(16) => 250, chr(17) => 250, chr(18) => 250, chr(19) => 250, chr(20) => 250, chr(21) => 250, - chr(22) => 250, chr(23) => 250, chr(24) => 250, chr(25) => 250, chr(26) => 250, chr(27) => 250, chr(28) => 250, chr(29) => 250, chr(30) => 250, chr(31) => 250, ' ' => 250, '!' => 333, '"' => 408, '#' => 500, '$' => 500, '%' => 833, '&' => 778, '\'' => 180, '(' => 333, ')' => 333, '*' => 500, '+' => 564, - ',' => 250, '-' => 333, '.' => 250, '/' => 278, '0' => 500, '1' => 500, '2' => 500, '3' => 500, '4' => 500, '5' => 500, '6' => 500, '7' => 500, '8' => 500, '9' => 500, ':' => 278, ';' => 278, '<' => 564, '=' => 564, '>' => 564, '?' => 444, '@' => 921, 'A' => 722, - 'B' => 667, 'C' => 667, 'D' => 722, 'E' => 611, 'F' => 556, 'G' => 722, 'H' => 722, 'I' => 333, 'J' => 389, 'K' => 722, 'L' => 611, 'M' => 889, 'N' => 722, 'O' => 722, 'P' => 556, 'Q' => 722, 'R' => 667, 'S' => 556, 'T' => 611, 'U' => 722, 'V' => 722, 'W' => 944, - 'X' => 722, 'Y' => 722, 'Z' => 611, '[' => 333, '\\' => 278, ']' => 333, '^' => 469, '_' => 500, '`' => 333, 'a' => 444, 'b' => 500, 'c' => 444, 'd' => 500, 'e' => 444, 'f' => 333, 'g' => 500, 'h' => 500, 'i' => 278, 'j' => 278, 'k' => 500, 'l' => 278, 'm' => 778, - 'n' => 500, 'o' => 500, 'p' => 500, 'q' => 500, 'r' => 333, 's' => 389, 't' => 278, 'u' => 500, 'v' => 500, 'w' => 722, 'x' => 500, 'y' => 500, 'z' => 444, '{' => 480, '|' => 200, '}' => 480, '~' => 541, chr(127) => 350, chr(128) => 500, chr(129) => 350, chr(130) => 333, chr(131) => 500, - chr(132) => 444, chr(133) => 1000, chr(134) => 500, chr(135) => 500, chr(136) => 333, chr(137) => 1000, chr(138) => 556, chr(139) => 333, chr(140) => 889, chr(141) => 350, chr(142) => 611, chr(143) => 350, chr(144) => 350, chr(145) => 333, chr(146) => 333, chr(147) => 444, chr(148) => 444, chr(149) => 350, chr(150) => 500, chr(151) => 1000, chr(152) => 333, chr(153) => 980, - chr(154) => 389, chr(155) => 333, chr(156) => 722, chr(157) => 350, chr(158) => 444, chr(159) => 722, chr(160) => 250, chr(161) => 333, chr(162) => 500, chr(163) => 500, chr(164) => 500, chr(165) => 500, chr(166) => 200, chr(167) => 500, chr(168) => 333, chr(169) => 760, chr(170) => 276, chr(171) => 500, chr(172) => 564, chr(173) => 333, chr(174) => 760, chr(175) => 333, - chr(176) => 400, chr(177) => 564, chr(178) => 300, chr(179) => 300, chr(180) => 333, chr(181) => 500, chr(182) => 453, chr(183) => 250, chr(184) => 333, chr(185) => 300, chr(186) => 310, chr(187) => 500, chr(188) => 750, chr(189) => 750, chr(190) => 750, chr(191) => 444, chr(192) => 722, chr(193) => 722, chr(194) => 722, chr(195) => 722, chr(196) => 722, chr(197) => 722, - chr(198) => 889, chr(199) => 667, chr(200) => 611, chr(201) => 611, chr(202) => 611, chr(203) => 611, chr(204) => 333, chr(205) => 333, chr(206) => 333, chr(207) => 333, chr(208) => 722, chr(209) => 722, chr(210) => 722, chr(211) => 722, chr(212) => 722, chr(213) => 722, chr(214) => 722, chr(215) => 564, chr(216) => 722, chr(217) => 722, chr(218) => 722, chr(219) => 722, - chr(220) => 722, chr(221) => 722, chr(222) => 556, chr(223) => 500, chr(224) => 444, chr(225) => 444, chr(226) => 444, chr(227) => 444, chr(228) => 444, chr(229) => 444, chr(230) => 667, chr(231) => 444, chr(232) => 444, chr(233) => 444, chr(234) => 444, chr(235) => 444, chr(236) => 278, chr(237) => 278, chr(238) => 278, chr(239) => 278, chr(240) => 500, chr(241) => 500, - chr(242) => 500, chr(243) => 500, chr(244) => 500, chr(245) => 500, chr(246) => 500, chr(247) => 564, chr(248) => 500, chr(249) => 500, chr(250) => 500, chr(251) => 500, chr(252) => 500, chr(253) => 500, chr(254) => 500, chr(255) => 500]; +$cw = array( + chr(0)=>250,chr(1)=>250,chr(2)=>250,chr(3)=>250,chr(4)=>250,chr(5)=>250,chr(6)=>250,chr(7)=>250,chr(8)=>250,chr(9)=>250,chr(10)=>250,chr(11)=>250,chr(12)=>250,chr(13)=>250,chr(14)=>250,chr(15)=>250,chr(16)=>250,chr(17)=>250,chr(18)=>250,chr(19)=>250,chr(20)=>250,chr(21)=>250, + chr(22)=>250,chr(23)=>250,chr(24)=>250,chr(25)=>250,chr(26)=>250,chr(27)=>250,chr(28)=>250,chr(29)=>250,chr(30)=>250,chr(31)=>250,' '=>250,'!'=>333,'"'=>408,'#'=>500,'$'=>500,'%'=>833,'&'=>778,'\''=>180,'('=>333,')'=>333,'*'=>500,'+'=>564, + ','=>250,'-'=>333,'.'=>250,'/'=>278,'0'=>500,'1'=>500,'2'=>500,'3'=>500,'4'=>500,'5'=>500,'6'=>500,'7'=>500,'8'=>500,'9'=>500,':'=>278,';'=>278,'<'=>564,'='=>564,'>'=>564,'?'=>444,'@'=>921,'A'=>722, + 'B'=>667,'C'=>667,'D'=>722,'E'=>611,'F'=>556,'G'=>722,'H'=>722,'I'=>333,'J'=>389,'K'=>722,'L'=>611,'M'=>889,'N'=>722,'O'=>722,'P'=>556,'Q'=>722,'R'=>667,'S'=>556,'T'=>611,'U'=>722,'V'=>722,'W'=>944, + 'X'=>722,'Y'=>722,'Z'=>611,'['=>333,'\\'=>278,']'=>333,'^'=>469,'_'=>500,'`'=>333,'a'=>444,'b'=>500,'c'=>444,'d'=>500,'e'=>444,'f'=>333,'g'=>500,'h'=>500,'i'=>278,'j'=>278,'k'=>500,'l'=>278,'m'=>778, + 'n'=>500,'o'=>500,'p'=>500,'q'=>500,'r'=>333,'s'=>389,'t'=>278,'u'=>500,'v'=>500,'w'=>722,'x'=>500,'y'=>500,'z'=>444,'{'=>480,'|'=>200,'}'=>480,'~'=>541,chr(127)=>350,chr(128)=>500,chr(129)=>350,chr(130)=>333,chr(131)=>500, + chr(132)=>444,chr(133)=>1000,chr(134)=>500,chr(135)=>500,chr(136)=>333,chr(137)=>1000,chr(138)=>556,chr(139)=>333,chr(140)=>889,chr(141)=>350,chr(142)=>611,chr(143)=>350,chr(144)=>350,chr(145)=>333,chr(146)=>333,chr(147)=>444,chr(148)=>444,chr(149)=>350,chr(150)=>500,chr(151)=>1000,chr(152)=>333,chr(153)=>980, + chr(154)=>389,chr(155)=>333,chr(156)=>722,chr(157)=>350,chr(158)=>444,chr(159)=>722,chr(160)=>250,chr(161)=>333,chr(162)=>500,chr(163)=>500,chr(164)=>500,chr(165)=>500,chr(166)=>200,chr(167)=>500,chr(168)=>333,chr(169)=>760,chr(170)=>276,chr(171)=>500,chr(172)=>564,chr(173)=>333,chr(174)=>760,chr(175)=>333, + chr(176)=>400,chr(177)=>564,chr(178)=>300,chr(179)=>300,chr(180)=>333,chr(181)=>500,chr(182)=>453,chr(183)=>250,chr(184)=>333,chr(185)=>300,chr(186)=>310,chr(187)=>500,chr(188)=>750,chr(189)=>750,chr(190)=>750,chr(191)=>444,chr(192)=>722,chr(193)=>722,chr(194)=>722,chr(195)=>722,chr(196)=>722,chr(197)=>722, + chr(198)=>889,chr(199)=>667,chr(200)=>611,chr(201)=>611,chr(202)=>611,chr(203)=>611,chr(204)=>333,chr(205)=>333,chr(206)=>333,chr(207)=>333,chr(208)=>722,chr(209)=>722,chr(210)=>722,chr(211)=>722,chr(212)=>722,chr(213)=>722,chr(214)=>722,chr(215)=>564,chr(216)=>722,chr(217)=>722,chr(218)=>722,chr(219)=>722, + chr(220)=>722,chr(221)=>722,chr(222)=>556,chr(223)=>500,chr(224)=>444,chr(225)=>444,chr(226)=>444,chr(227)=>444,chr(228)=>444,chr(229)=>444,chr(230)=>667,chr(231)=>444,chr(232)=>444,chr(233)=>444,chr(234)=>444,chr(235)=>444,chr(236)=>278,chr(237)=>278,chr(238)=>278,chr(239)=>278,chr(240)=>500,chr(241)=>500, + chr(242)=>500,chr(243)=>500,chr(244)=>500,chr(245)=>500,chr(246)=>500,chr(247)=>564,chr(248)=>500,chr(249)=>500,chr(250)=>500,chr(251)=>500,chr(252)=>500,chr(253)=>500,chr(254)=>500,chr(255)=>500); $enc = 'cp1252'; -$uv = [0 => [0, 128], 128 => 8364, 130 => 8218, 131 => 402, 132 => 8222, 133 => 8230, 134 => [8224, 2], 136 => 710, 137 => 8240, 138 => 352, 139 => 8249, 140 => 338, 142 => 381, 145 => [8216, 2], 147 => [8220, 2], 149 => 8226, 150 => [8211, 2], 152 => 732, 153 => 8482, 154 => 353, 155 => 8250, 156 => 339, 158 => 382, 159 => 376, 160 => [160, 96]]; +$uv = array(0=>array(0,128),128=>8364,130=>8218,131=>402,132=>8222,133=>8230,134=>array(8224,2),136=>710,137=>8240,138=>352,139=>8249,140=>338,142=>381,145=>array(8216,2),147=>array(8220,2),149=>8226,150=>array(8211,2),152=>732,153=>8482,154=>353,155=>8250,156=>339,158=>382,159=>376,160=>array(160,96)); +?> diff --git a/app/ThirdParty/fpdf/font/timesb.php b/app/ThirdParty/fpdf/font/timesb.php index 3f689ac9..7db704fd 100644 --- a/app/ThirdParty/fpdf/font/timesb.php +++ b/app/ThirdParty/fpdf/font/timesb.php @@ -1,21 +1,21 @@ 250, chr(1) => 250, chr(2) => 250, chr(3) => 250, chr(4) => 250, chr(5) => 250, chr(6) => 250, chr(7) => 250, chr(8) => 250, chr(9) => 250, chr(10) => 250, chr(11) => 250, chr(12) => 250, chr(13) => 250, chr(14) => 250, chr(15) => 250, chr(16) => 250, chr(17) => 250, chr(18) => 250, chr(19) => 250, chr(20) => 250, chr(21) => 250, - chr(22) => 250, chr(23) => 250, chr(24) => 250, chr(25) => 250, chr(26) => 250, chr(27) => 250, chr(28) => 250, chr(29) => 250, chr(30) => 250, chr(31) => 250, ' ' => 250, '!' => 333, '"' => 555, '#' => 500, '$' => 500, '%' => 1000, '&' => 833, '\'' => 278, '(' => 333, ')' => 333, '*' => 500, '+' => 570, - ',' => 250, '-' => 333, '.' => 250, '/' => 278, '0' => 500, '1' => 500, '2' => 500, '3' => 500, '4' => 500, '5' => 500, '6' => 500, '7' => 500, '8' => 500, '9' => 500, ':' => 333, ';' => 333, '<' => 570, '=' => 570, '>' => 570, '?' => 500, '@' => 930, 'A' => 722, - 'B' => 667, 'C' => 722, 'D' => 722, 'E' => 667, 'F' => 611, 'G' => 778, 'H' => 778, 'I' => 389, 'J' => 500, 'K' => 778, 'L' => 667, 'M' => 944, 'N' => 722, 'O' => 778, 'P' => 611, 'Q' => 778, 'R' => 722, 'S' => 556, 'T' => 667, 'U' => 722, 'V' => 722, 'W' => 1000, - 'X' => 722, 'Y' => 722, 'Z' => 667, '[' => 333, '\\' => 278, ']' => 333, '^' => 581, '_' => 500, '`' => 333, 'a' => 500, 'b' => 556, 'c' => 444, 'd' => 556, 'e' => 444, 'f' => 333, 'g' => 500, 'h' => 556, 'i' => 278, 'j' => 333, 'k' => 556, 'l' => 278, 'm' => 833, - 'n' => 556, 'o' => 500, 'p' => 556, 'q' => 556, 'r' => 444, 's' => 389, 't' => 333, 'u' => 556, 'v' => 500, 'w' => 722, 'x' => 500, 'y' => 500, 'z' => 444, '{' => 394, '|' => 220, '}' => 394, '~' => 520, chr(127) => 350, chr(128) => 500, chr(129) => 350, chr(130) => 333, chr(131) => 500, - chr(132) => 500, chr(133) => 1000, chr(134) => 500, chr(135) => 500, chr(136) => 333, chr(137) => 1000, chr(138) => 556, chr(139) => 333, chr(140) => 1000, chr(141) => 350, chr(142) => 667, chr(143) => 350, chr(144) => 350, chr(145) => 333, chr(146) => 333, chr(147) => 500, chr(148) => 500, chr(149) => 350, chr(150) => 500, chr(151) => 1000, chr(152) => 333, chr(153) => 1000, - chr(154) => 389, chr(155) => 333, chr(156) => 722, chr(157) => 350, chr(158) => 444, chr(159) => 722, chr(160) => 250, chr(161) => 333, chr(162) => 500, chr(163) => 500, chr(164) => 500, chr(165) => 500, chr(166) => 220, chr(167) => 500, chr(168) => 333, chr(169) => 747, chr(170) => 300, chr(171) => 500, chr(172) => 570, chr(173) => 333, chr(174) => 747, chr(175) => 333, - chr(176) => 400, chr(177) => 570, chr(178) => 300, chr(179) => 300, chr(180) => 333, chr(181) => 556, chr(182) => 540, chr(183) => 250, chr(184) => 333, chr(185) => 300, chr(186) => 330, chr(187) => 500, chr(188) => 750, chr(189) => 750, chr(190) => 750, chr(191) => 500, chr(192) => 722, chr(193) => 722, chr(194) => 722, chr(195) => 722, chr(196) => 722, chr(197) => 722, - chr(198) => 1000, chr(199) => 722, chr(200) => 667, chr(201) => 667, chr(202) => 667, chr(203) => 667, chr(204) => 389, chr(205) => 389, chr(206) => 389, chr(207) => 389, chr(208) => 722, chr(209) => 722, chr(210) => 778, chr(211) => 778, chr(212) => 778, chr(213) => 778, chr(214) => 778, chr(215) => 570, chr(216) => 778, chr(217) => 722, chr(218) => 722, chr(219) => 722, - chr(220) => 722, chr(221) => 722, chr(222) => 611, chr(223) => 556, chr(224) => 500, chr(225) => 500, chr(226) => 500, chr(227) => 500, chr(228) => 500, chr(229) => 500, chr(230) => 722, chr(231) => 444, chr(232) => 444, chr(233) => 444, chr(234) => 444, chr(235) => 444, chr(236) => 278, chr(237) => 278, chr(238) => 278, chr(239) => 278, chr(240) => 500, chr(241) => 556, - chr(242) => 500, chr(243) => 500, chr(244) => 500, chr(245) => 500, chr(246) => 500, chr(247) => 570, chr(248) => 500, chr(249) => 556, chr(250) => 556, chr(251) => 556, chr(252) => 556, chr(253) => 500, chr(254) => 556, chr(255) => 500]; +$cw = array( + chr(0)=>250,chr(1)=>250,chr(2)=>250,chr(3)=>250,chr(4)=>250,chr(5)=>250,chr(6)=>250,chr(7)=>250,chr(8)=>250,chr(9)=>250,chr(10)=>250,chr(11)=>250,chr(12)=>250,chr(13)=>250,chr(14)=>250,chr(15)=>250,chr(16)=>250,chr(17)=>250,chr(18)=>250,chr(19)=>250,chr(20)=>250,chr(21)=>250, + chr(22)=>250,chr(23)=>250,chr(24)=>250,chr(25)=>250,chr(26)=>250,chr(27)=>250,chr(28)=>250,chr(29)=>250,chr(30)=>250,chr(31)=>250,' '=>250,'!'=>333,'"'=>555,'#'=>500,'$'=>500,'%'=>1000,'&'=>833,'\''=>278,'('=>333,')'=>333,'*'=>500,'+'=>570, + ','=>250,'-'=>333,'.'=>250,'/'=>278,'0'=>500,'1'=>500,'2'=>500,'3'=>500,'4'=>500,'5'=>500,'6'=>500,'7'=>500,'8'=>500,'9'=>500,':'=>333,';'=>333,'<'=>570,'='=>570,'>'=>570,'?'=>500,'@'=>930,'A'=>722, + 'B'=>667,'C'=>722,'D'=>722,'E'=>667,'F'=>611,'G'=>778,'H'=>778,'I'=>389,'J'=>500,'K'=>778,'L'=>667,'M'=>944,'N'=>722,'O'=>778,'P'=>611,'Q'=>778,'R'=>722,'S'=>556,'T'=>667,'U'=>722,'V'=>722,'W'=>1000, + 'X'=>722,'Y'=>722,'Z'=>667,'['=>333,'\\'=>278,']'=>333,'^'=>581,'_'=>500,'`'=>333,'a'=>500,'b'=>556,'c'=>444,'d'=>556,'e'=>444,'f'=>333,'g'=>500,'h'=>556,'i'=>278,'j'=>333,'k'=>556,'l'=>278,'m'=>833, + 'n'=>556,'o'=>500,'p'=>556,'q'=>556,'r'=>444,'s'=>389,'t'=>333,'u'=>556,'v'=>500,'w'=>722,'x'=>500,'y'=>500,'z'=>444,'{'=>394,'|'=>220,'}'=>394,'~'=>520,chr(127)=>350,chr(128)=>500,chr(129)=>350,chr(130)=>333,chr(131)=>500, + chr(132)=>500,chr(133)=>1000,chr(134)=>500,chr(135)=>500,chr(136)=>333,chr(137)=>1000,chr(138)=>556,chr(139)=>333,chr(140)=>1000,chr(141)=>350,chr(142)=>667,chr(143)=>350,chr(144)=>350,chr(145)=>333,chr(146)=>333,chr(147)=>500,chr(148)=>500,chr(149)=>350,chr(150)=>500,chr(151)=>1000,chr(152)=>333,chr(153)=>1000, + chr(154)=>389,chr(155)=>333,chr(156)=>722,chr(157)=>350,chr(158)=>444,chr(159)=>722,chr(160)=>250,chr(161)=>333,chr(162)=>500,chr(163)=>500,chr(164)=>500,chr(165)=>500,chr(166)=>220,chr(167)=>500,chr(168)=>333,chr(169)=>747,chr(170)=>300,chr(171)=>500,chr(172)=>570,chr(173)=>333,chr(174)=>747,chr(175)=>333, + chr(176)=>400,chr(177)=>570,chr(178)=>300,chr(179)=>300,chr(180)=>333,chr(181)=>556,chr(182)=>540,chr(183)=>250,chr(184)=>333,chr(185)=>300,chr(186)=>330,chr(187)=>500,chr(188)=>750,chr(189)=>750,chr(190)=>750,chr(191)=>500,chr(192)=>722,chr(193)=>722,chr(194)=>722,chr(195)=>722,chr(196)=>722,chr(197)=>722, + chr(198)=>1000,chr(199)=>722,chr(200)=>667,chr(201)=>667,chr(202)=>667,chr(203)=>667,chr(204)=>389,chr(205)=>389,chr(206)=>389,chr(207)=>389,chr(208)=>722,chr(209)=>722,chr(210)=>778,chr(211)=>778,chr(212)=>778,chr(213)=>778,chr(214)=>778,chr(215)=>570,chr(216)=>778,chr(217)=>722,chr(218)=>722,chr(219)=>722, + chr(220)=>722,chr(221)=>722,chr(222)=>611,chr(223)=>556,chr(224)=>500,chr(225)=>500,chr(226)=>500,chr(227)=>500,chr(228)=>500,chr(229)=>500,chr(230)=>722,chr(231)=>444,chr(232)=>444,chr(233)=>444,chr(234)=>444,chr(235)=>444,chr(236)=>278,chr(237)=>278,chr(238)=>278,chr(239)=>278,chr(240)=>500,chr(241)=>556, + chr(242)=>500,chr(243)=>500,chr(244)=>500,chr(245)=>500,chr(246)=>500,chr(247)=>570,chr(248)=>500,chr(249)=>556,chr(250)=>556,chr(251)=>556,chr(252)=>556,chr(253)=>500,chr(254)=>556,chr(255)=>500); $enc = 'cp1252'; -$uv = [0 => [0, 128], 128 => 8364, 130 => 8218, 131 => 402, 132 => 8222, 133 => 8230, 134 => [8224, 2], 136 => 710, 137 => 8240, 138 => 352, 139 => 8249, 140 => 338, 142 => 381, 145 => [8216, 2], 147 => [8220, 2], 149 => 8226, 150 => [8211, 2], 152 => 732, 153 => 8482, 154 => 353, 155 => 8250, 156 => 339, 158 => 382, 159 => 376, 160 => [160, 96]]; +$uv = array(0=>array(0,128),128=>8364,130=>8218,131=>402,132=>8222,133=>8230,134=>array(8224,2),136=>710,137=>8240,138=>352,139=>8249,140=>338,142=>381,145=>array(8216,2),147=>array(8220,2),149=>8226,150=>array(8211,2),152=>732,153=>8482,154=>353,155=>8250,156=>339,158=>382,159=>376,160=>array(160,96)); +?> diff --git a/app/ThirdParty/fpdf/font/timesbi.php b/app/ThirdParty/fpdf/font/timesbi.php index 2edef0e7..089f21ab 100644 --- a/app/ThirdParty/fpdf/font/timesbi.php +++ b/app/ThirdParty/fpdf/font/timesbi.php @@ -1,21 +1,21 @@ 250, chr(1) => 250, chr(2) => 250, chr(3) => 250, chr(4) => 250, chr(5) => 250, chr(6) => 250, chr(7) => 250, chr(8) => 250, chr(9) => 250, chr(10) => 250, chr(11) => 250, chr(12) => 250, chr(13) => 250, chr(14) => 250, chr(15) => 250, chr(16) => 250, chr(17) => 250, chr(18) => 250, chr(19) => 250, chr(20) => 250, chr(21) => 250, - chr(22) => 250, chr(23) => 250, chr(24) => 250, chr(25) => 250, chr(26) => 250, chr(27) => 250, chr(28) => 250, chr(29) => 250, chr(30) => 250, chr(31) => 250, ' ' => 250, '!' => 389, '"' => 555, '#' => 500, '$' => 500, '%' => 833, '&' => 778, '\'' => 278, '(' => 333, ')' => 333, '*' => 500, '+' => 570, - ',' => 250, '-' => 333, '.' => 250, '/' => 278, '0' => 500, '1' => 500, '2' => 500, '3' => 500, '4' => 500, '5' => 500, '6' => 500, '7' => 500, '8' => 500, '9' => 500, ':' => 333, ';' => 333, '<' => 570, '=' => 570, '>' => 570, '?' => 500, '@' => 832, 'A' => 667, - 'B' => 667, 'C' => 667, 'D' => 722, 'E' => 667, 'F' => 667, 'G' => 722, 'H' => 778, 'I' => 389, 'J' => 500, 'K' => 667, 'L' => 611, 'M' => 889, 'N' => 722, 'O' => 722, 'P' => 611, 'Q' => 722, 'R' => 667, 'S' => 556, 'T' => 611, 'U' => 722, 'V' => 667, 'W' => 889, - 'X' => 667, 'Y' => 611, 'Z' => 611, '[' => 333, '\\' => 278, ']' => 333, '^' => 570, '_' => 500, '`' => 333, 'a' => 500, 'b' => 500, 'c' => 444, 'd' => 500, 'e' => 444, 'f' => 333, 'g' => 500, 'h' => 556, 'i' => 278, 'j' => 278, 'k' => 500, 'l' => 278, 'm' => 778, - 'n' => 556, 'o' => 500, 'p' => 500, 'q' => 500, 'r' => 389, 's' => 389, 't' => 278, 'u' => 556, 'v' => 444, 'w' => 667, 'x' => 500, 'y' => 444, 'z' => 389, '{' => 348, '|' => 220, '}' => 348, '~' => 570, chr(127) => 350, chr(128) => 500, chr(129) => 350, chr(130) => 333, chr(131) => 500, - chr(132) => 500, chr(133) => 1000, chr(134) => 500, chr(135) => 500, chr(136) => 333, chr(137) => 1000, chr(138) => 556, chr(139) => 333, chr(140) => 944, chr(141) => 350, chr(142) => 611, chr(143) => 350, chr(144) => 350, chr(145) => 333, chr(146) => 333, chr(147) => 500, chr(148) => 500, chr(149) => 350, chr(150) => 500, chr(151) => 1000, chr(152) => 333, chr(153) => 1000, - chr(154) => 389, chr(155) => 333, chr(156) => 722, chr(157) => 350, chr(158) => 389, chr(159) => 611, chr(160) => 250, chr(161) => 389, chr(162) => 500, chr(163) => 500, chr(164) => 500, chr(165) => 500, chr(166) => 220, chr(167) => 500, chr(168) => 333, chr(169) => 747, chr(170) => 266, chr(171) => 500, chr(172) => 606, chr(173) => 333, chr(174) => 747, chr(175) => 333, - chr(176) => 400, chr(177) => 570, chr(178) => 300, chr(179) => 300, chr(180) => 333, chr(181) => 576, chr(182) => 500, chr(183) => 250, chr(184) => 333, chr(185) => 300, chr(186) => 300, chr(187) => 500, chr(188) => 750, chr(189) => 750, chr(190) => 750, chr(191) => 500, chr(192) => 667, chr(193) => 667, chr(194) => 667, chr(195) => 667, chr(196) => 667, chr(197) => 667, - chr(198) => 944, chr(199) => 667, chr(200) => 667, chr(201) => 667, chr(202) => 667, chr(203) => 667, chr(204) => 389, chr(205) => 389, chr(206) => 389, chr(207) => 389, chr(208) => 722, chr(209) => 722, chr(210) => 722, chr(211) => 722, chr(212) => 722, chr(213) => 722, chr(214) => 722, chr(215) => 570, chr(216) => 722, chr(217) => 722, chr(218) => 722, chr(219) => 722, - chr(220) => 722, chr(221) => 611, chr(222) => 611, chr(223) => 500, chr(224) => 500, chr(225) => 500, chr(226) => 500, chr(227) => 500, chr(228) => 500, chr(229) => 500, chr(230) => 722, chr(231) => 444, chr(232) => 444, chr(233) => 444, chr(234) => 444, chr(235) => 444, chr(236) => 278, chr(237) => 278, chr(238) => 278, chr(239) => 278, chr(240) => 500, chr(241) => 556, - chr(242) => 500, chr(243) => 500, chr(244) => 500, chr(245) => 500, chr(246) => 500, chr(247) => 570, chr(248) => 500, chr(249) => 556, chr(250) => 556, chr(251) => 556, chr(252) => 556, chr(253) => 444, chr(254) => 500, chr(255) => 444]; +$cw = array( + chr(0)=>250,chr(1)=>250,chr(2)=>250,chr(3)=>250,chr(4)=>250,chr(5)=>250,chr(6)=>250,chr(7)=>250,chr(8)=>250,chr(9)=>250,chr(10)=>250,chr(11)=>250,chr(12)=>250,chr(13)=>250,chr(14)=>250,chr(15)=>250,chr(16)=>250,chr(17)=>250,chr(18)=>250,chr(19)=>250,chr(20)=>250,chr(21)=>250, + chr(22)=>250,chr(23)=>250,chr(24)=>250,chr(25)=>250,chr(26)=>250,chr(27)=>250,chr(28)=>250,chr(29)=>250,chr(30)=>250,chr(31)=>250,' '=>250,'!'=>389,'"'=>555,'#'=>500,'$'=>500,'%'=>833,'&'=>778,'\''=>278,'('=>333,')'=>333,'*'=>500,'+'=>570, + ','=>250,'-'=>333,'.'=>250,'/'=>278,'0'=>500,'1'=>500,'2'=>500,'3'=>500,'4'=>500,'5'=>500,'6'=>500,'7'=>500,'8'=>500,'9'=>500,':'=>333,';'=>333,'<'=>570,'='=>570,'>'=>570,'?'=>500,'@'=>832,'A'=>667, + 'B'=>667,'C'=>667,'D'=>722,'E'=>667,'F'=>667,'G'=>722,'H'=>778,'I'=>389,'J'=>500,'K'=>667,'L'=>611,'M'=>889,'N'=>722,'O'=>722,'P'=>611,'Q'=>722,'R'=>667,'S'=>556,'T'=>611,'U'=>722,'V'=>667,'W'=>889, + 'X'=>667,'Y'=>611,'Z'=>611,'['=>333,'\\'=>278,']'=>333,'^'=>570,'_'=>500,'`'=>333,'a'=>500,'b'=>500,'c'=>444,'d'=>500,'e'=>444,'f'=>333,'g'=>500,'h'=>556,'i'=>278,'j'=>278,'k'=>500,'l'=>278,'m'=>778, + 'n'=>556,'o'=>500,'p'=>500,'q'=>500,'r'=>389,'s'=>389,'t'=>278,'u'=>556,'v'=>444,'w'=>667,'x'=>500,'y'=>444,'z'=>389,'{'=>348,'|'=>220,'}'=>348,'~'=>570,chr(127)=>350,chr(128)=>500,chr(129)=>350,chr(130)=>333,chr(131)=>500, + chr(132)=>500,chr(133)=>1000,chr(134)=>500,chr(135)=>500,chr(136)=>333,chr(137)=>1000,chr(138)=>556,chr(139)=>333,chr(140)=>944,chr(141)=>350,chr(142)=>611,chr(143)=>350,chr(144)=>350,chr(145)=>333,chr(146)=>333,chr(147)=>500,chr(148)=>500,chr(149)=>350,chr(150)=>500,chr(151)=>1000,chr(152)=>333,chr(153)=>1000, + chr(154)=>389,chr(155)=>333,chr(156)=>722,chr(157)=>350,chr(158)=>389,chr(159)=>611,chr(160)=>250,chr(161)=>389,chr(162)=>500,chr(163)=>500,chr(164)=>500,chr(165)=>500,chr(166)=>220,chr(167)=>500,chr(168)=>333,chr(169)=>747,chr(170)=>266,chr(171)=>500,chr(172)=>606,chr(173)=>333,chr(174)=>747,chr(175)=>333, + chr(176)=>400,chr(177)=>570,chr(178)=>300,chr(179)=>300,chr(180)=>333,chr(181)=>576,chr(182)=>500,chr(183)=>250,chr(184)=>333,chr(185)=>300,chr(186)=>300,chr(187)=>500,chr(188)=>750,chr(189)=>750,chr(190)=>750,chr(191)=>500,chr(192)=>667,chr(193)=>667,chr(194)=>667,chr(195)=>667,chr(196)=>667,chr(197)=>667, + chr(198)=>944,chr(199)=>667,chr(200)=>667,chr(201)=>667,chr(202)=>667,chr(203)=>667,chr(204)=>389,chr(205)=>389,chr(206)=>389,chr(207)=>389,chr(208)=>722,chr(209)=>722,chr(210)=>722,chr(211)=>722,chr(212)=>722,chr(213)=>722,chr(214)=>722,chr(215)=>570,chr(216)=>722,chr(217)=>722,chr(218)=>722,chr(219)=>722, + chr(220)=>722,chr(221)=>611,chr(222)=>611,chr(223)=>500,chr(224)=>500,chr(225)=>500,chr(226)=>500,chr(227)=>500,chr(228)=>500,chr(229)=>500,chr(230)=>722,chr(231)=>444,chr(232)=>444,chr(233)=>444,chr(234)=>444,chr(235)=>444,chr(236)=>278,chr(237)=>278,chr(238)=>278,chr(239)=>278,chr(240)=>500,chr(241)=>556, + chr(242)=>500,chr(243)=>500,chr(244)=>500,chr(245)=>500,chr(246)=>500,chr(247)=>570,chr(248)=>500,chr(249)=>556,chr(250)=>556,chr(251)=>556,chr(252)=>556,chr(253)=>444,chr(254)=>500,chr(255)=>444); $enc = 'cp1252'; -$uv = [0 => [0, 128], 128 => 8364, 130 => 8218, 131 => 402, 132 => 8222, 133 => 8230, 134 => [8224, 2], 136 => 710, 137 => 8240, 138 => 352, 139 => 8249, 140 => 338, 142 => 381, 145 => [8216, 2], 147 => [8220, 2], 149 => 8226, 150 => [8211, 2], 152 => 732, 153 => 8482, 154 => 353, 155 => 8250, 156 => 339, 158 => 382, 159 => 376, 160 => [160, 96]]; +$uv = array(0=>array(0,128),128=>8364,130=>8218,131=>402,132=>8222,133=>8230,134=>array(8224,2),136=>710,137=>8240,138=>352,139=>8249,140=>338,142=>381,145=>array(8216,2),147=>array(8220,2),149=>8226,150=>array(8211,2),152=>732,153=>8482,154=>353,155=>8250,156=>339,158=>382,159=>376,160=>array(160,96)); +?> diff --git a/app/ThirdParty/fpdf/font/timesi.php b/app/ThirdParty/fpdf/font/timesi.php index e26de456..f958b5b6 100644 --- a/app/ThirdParty/fpdf/font/timesi.php +++ b/app/ThirdParty/fpdf/font/timesi.php @@ -1,21 +1,21 @@ 250, chr(1) => 250, chr(2) => 250, chr(3) => 250, chr(4) => 250, chr(5) => 250, chr(6) => 250, chr(7) => 250, chr(8) => 250, chr(9) => 250, chr(10) => 250, chr(11) => 250, chr(12) => 250, chr(13) => 250, chr(14) => 250, chr(15) => 250, chr(16) => 250, chr(17) => 250, chr(18) => 250, chr(19) => 250, chr(20) => 250, chr(21) => 250, - chr(22) => 250, chr(23) => 250, chr(24) => 250, chr(25) => 250, chr(26) => 250, chr(27) => 250, chr(28) => 250, chr(29) => 250, chr(30) => 250, chr(31) => 250, ' ' => 250, '!' => 333, '"' => 420, '#' => 500, '$' => 500, '%' => 833, '&' => 778, '\'' => 214, '(' => 333, ')' => 333, '*' => 500, '+' => 675, - ',' => 250, '-' => 333, '.' => 250, '/' => 278, '0' => 500, '1' => 500, '2' => 500, '3' => 500, '4' => 500, '5' => 500, '6' => 500, '7' => 500, '8' => 500, '9' => 500, ':' => 333, ';' => 333, '<' => 675, '=' => 675, '>' => 675, '?' => 500, '@' => 920, 'A' => 611, - 'B' => 611, 'C' => 667, 'D' => 722, 'E' => 611, 'F' => 611, 'G' => 722, 'H' => 722, 'I' => 333, 'J' => 444, 'K' => 667, 'L' => 556, 'M' => 833, 'N' => 667, 'O' => 722, 'P' => 611, 'Q' => 722, 'R' => 611, 'S' => 500, 'T' => 556, 'U' => 722, 'V' => 611, 'W' => 833, - 'X' => 611, 'Y' => 556, 'Z' => 556, '[' => 389, '\\' => 278, ']' => 389, '^' => 422, '_' => 500, '`' => 333, 'a' => 500, 'b' => 500, 'c' => 444, 'd' => 500, 'e' => 444, 'f' => 278, 'g' => 500, 'h' => 500, 'i' => 278, 'j' => 278, 'k' => 444, 'l' => 278, 'm' => 722, - 'n' => 500, 'o' => 500, 'p' => 500, 'q' => 500, 'r' => 389, 's' => 389, 't' => 278, 'u' => 500, 'v' => 444, 'w' => 667, 'x' => 444, 'y' => 444, 'z' => 389, '{' => 400, '|' => 275, '}' => 400, '~' => 541, chr(127) => 350, chr(128) => 500, chr(129) => 350, chr(130) => 333, chr(131) => 500, - chr(132) => 556, chr(133) => 889, chr(134) => 500, chr(135) => 500, chr(136) => 333, chr(137) => 1000, chr(138) => 500, chr(139) => 333, chr(140) => 944, chr(141) => 350, chr(142) => 556, chr(143) => 350, chr(144) => 350, chr(145) => 333, chr(146) => 333, chr(147) => 556, chr(148) => 556, chr(149) => 350, chr(150) => 500, chr(151) => 889, chr(152) => 333, chr(153) => 980, - chr(154) => 389, chr(155) => 333, chr(156) => 667, chr(157) => 350, chr(158) => 389, chr(159) => 556, chr(160) => 250, chr(161) => 389, chr(162) => 500, chr(163) => 500, chr(164) => 500, chr(165) => 500, chr(166) => 275, chr(167) => 500, chr(168) => 333, chr(169) => 760, chr(170) => 276, chr(171) => 500, chr(172) => 675, chr(173) => 333, chr(174) => 760, chr(175) => 333, - chr(176) => 400, chr(177) => 675, chr(178) => 300, chr(179) => 300, chr(180) => 333, chr(181) => 500, chr(182) => 523, chr(183) => 250, chr(184) => 333, chr(185) => 300, chr(186) => 310, chr(187) => 500, chr(188) => 750, chr(189) => 750, chr(190) => 750, chr(191) => 500, chr(192) => 611, chr(193) => 611, chr(194) => 611, chr(195) => 611, chr(196) => 611, chr(197) => 611, - chr(198) => 889, chr(199) => 667, chr(200) => 611, chr(201) => 611, chr(202) => 611, chr(203) => 611, chr(204) => 333, chr(205) => 333, chr(206) => 333, chr(207) => 333, chr(208) => 722, chr(209) => 667, chr(210) => 722, chr(211) => 722, chr(212) => 722, chr(213) => 722, chr(214) => 722, chr(215) => 675, chr(216) => 722, chr(217) => 722, chr(218) => 722, chr(219) => 722, - chr(220) => 722, chr(221) => 556, chr(222) => 611, chr(223) => 500, chr(224) => 500, chr(225) => 500, chr(226) => 500, chr(227) => 500, chr(228) => 500, chr(229) => 500, chr(230) => 667, chr(231) => 444, chr(232) => 444, chr(233) => 444, chr(234) => 444, chr(235) => 444, chr(236) => 278, chr(237) => 278, chr(238) => 278, chr(239) => 278, chr(240) => 500, chr(241) => 500, - chr(242) => 500, chr(243) => 500, chr(244) => 500, chr(245) => 500, chr(246) => 500, chr(247) => 675, chr(248) => 500, chr(249) => 500, chr(250) => 500, chr(251) => 500, chr(252) => 500, chr(253) => 444, chr(254) => 500, chr(255) => 444]; +$cw = array( + chr(0)=>250,chr(1)=>250,chr(2)=>250,chr(3)=>250,chr(4)=>250,chr(5)=>250,chr(6)=>250,chr(7)=>250,chr(8)=>250,chr(9)=>250,chr(10)=>250,chr(11)=>250,chr(12)=>250,chr(13)=>250,chr(14)=>250,chr(15)=>250,chr(16)=>250,chr(17)=>250,chr(18)=>250,chr(19)=>250,chr(20)=>250,chr(21)=>250, + chr(22)=>250,chr(23)=>250,chr(24)=>250,chr(25)=>250,chr(26)=>250,chr(27)=>250,chr(28)=>250,chr(29)=>250,chr(30)=>250,chr(31)=>250,' '=>250,'!'=>333,'"'=>420,'#'=>500,'$'=>500,'%'=>833,'&'=>778,'\''=>214,'('=>333,')'=>333,'*'=>500,'+'=>675, + ','=>250,'-'=>333,'.'=>250,'/'=>278,'0'=>500,'1'=>500,'2'=>500,'3'=>500,'4'=>500,'5'=>500,'6'=>500,'7'=>500,'8'=>500,'9'=>500,':'=>333,';'=>333,'<'=>675,'='=>675,'>'=>675,'?'=>500,'@'=>920,'A'=>611, + 'B'=>611,'C'=>667,'D'=>722,'E'=>611,'F'=>611,'G'=>722,'H'=>722,'I'=>333,'J'=>444,'K'=>667,'L'=>556,'M'=>833,'N'=>667,'O'=>722,'P'=>611,'Q'=>722,'R'=>611,'S'=>500,'T'=>556,'U'=>722,'V'=>611,'W'=>833, + 'X'=>611,'Y'=>556,'Z'=>556,'['=>389,'\\'=>278,']'=>389,'^'=>422,'_'=>500,'`'=>333,'a'=>500,'b'=>500,'c'=>444,'d'=>500,'e'=>444,'f'=>278,'g'=>500,'h'=>500,'i'=>278,'j'=>278,'k'=>444,'l'=>278,'m'=>722, + 'n'=>500,'o'=>500,'p'=>500,'q'=>500,'r'=>389,'s'=>389,'t'=>278,'u'=>500,'v'=>444,'w'=>667,'x'=>444,'y'=>444,'z'=>389,'{'=>400,'|'=>275,'}'=>400,'~'=>541,chr(127)=>350,chr(128)=>500,chr(129)=>350,chr(130)=>333,chr(131)=>500, + chr(132)=>556,chr(133)=>889,chr(134)=>500,chr(135)=>500,chr(136)=>333,chr(137)=>1000,chr(138)=>500,chr(139)=>333,chr(140)=>944,chr(141)=>350,chr(142)=>556,chr(143)=>350,chr(144)=>350,chr(145)=>333,chr(146)=>333,chr(147)=>556,chr(148)=>556,chr(149)=>350,chr(150)=>500,chr(151)=>889,chr(152)=>333,chr(153)=>980, + chr(154)=>389,chr(155)=>333,chr(156)=>667,chr(157)=>350,chr(158)=>389,chr(159)=>556,chr(160)=>250,chr(161)=>389,chr(162)=>500,chr(163)=>500,chr(164)=>500,chr(165)=>500,chr(166)=>275,chr(167)=>500,chr(168)=>333,chr(169)=>760,chr(170)=>276,chr(171)=>500,chr(172)=>675,chr(173)=>333,chr(174)=>760,chr(175)=>333, + chr(176)=>400,chr(177)=>675,chr(178)=>300,chr(179)=>300,chr(180)=>333,chr(181)=>500,chr(182)=>523,chr(183)=>250,chr(184)=>333,chr(185)=>300,chr(186)=>310,chr(187)=>500,chr(188)=>750,chr(189)=>750,chr(190)=>750,chr(191)=>500,chr(192)=>611,chr(193)=>611,chr(194)=>611,chr(195)=>611,chr(196)=>611,chr(197)=>611, + chr(198)=>889,chr(199)=>667,chr(200)=>611,chr(201)=>611,chr(202)=>611,chr(203)=>611,chr(204)=>333,chr(205)=>333,chr(206)=>333,chr(207)=>333,chr(208)=>722,chr(209)=>667,chr(210)=>722,chr(211)=>722,chr(212)=>722,chr(213)=>722,chr(214)=>722,chr(215)=>675,chr(216)=>722,chr(217)=>722,chr(218)=>722,chr(219)=>722, + chr(220)=>722,chr(221)=>556,chr(222)=>611,chr(223)=>500,chr(224)=>500,chr(225)=>500,chr(226)=>500,chr(227)=>500,chr(228)=>500,chr(229)=>500,chr(230)=>667,chr(231)=>444,chr(232)=>444,chr(233)=>444,chr(234)=>444,chr(235)=>444,chr(236)=>278,chr(237)=>278,chr(238)=>278,chr(239)=>278,chr(240)=>500,chr(241)=>500, + chr(242)=>500,chr(243)=>500,chr(244)=>500,chr(245)=>500,chr(246)=>500,chr(247)=>675,chr(248)=>500,chr(249)=>500,chr(250)=>500,chr(251)=>500,chr(252)=>500,chr(253)=>444,chr(254)=>500,chr(255)=>444); $enc = 'cp1252'; -$uv = [0 => [0, 128], 128 => 8364, 130 => 8218, 131 => 402, 132 => 8222, 133 => 8230, 134 => [8224, 2], 136 => 710, 137 => 8240, 138 => 352, 139 => 8249, 140 => 338, 142 => 381, 145 => [8216, 2], 147 => [8220, 2], 149 => 8226, 150 => [8211, 2], 152 => 732, 153 => 8482, 154 => 353, 155 => 8250, 156 => 339, 158 => 382, 159 => 376, 160 => [160, 96]]; +$uv = array(0=>array(0,128),128=>8364,130=>8218,131=>402,132=>8222,133=>8230,134=>array(8224,2),136=>710,137=>8240,138=>352,139=>8249,140=>338,142=>381,145=>array(8216,2),147=>array(8220,2),149=>8226,150=>array(8211,2),152=>732,153=>8482,154=>353,155=>8250,156=>339,158=>382,159=>376,160=>array(160,96)); +?> diff --git a/app/ThirdParty/fpdf/font/zapfdingbats.php b/app/ThirdParty/fpdf/font/zapfdingbats.php index d236518f..7c2cb5e4 100644 --- a/app/ThirdParty/fpdf/font/zapfdingbats.php +++ b/app/ThirdParty/fpdf/font/zapfdingbats.php @@ -1,20 +1,20 @@ 0, chr(1) => 0, chr(2) => 0, chr(3) => 0, chr(4) => 0, chr(5) => 0, chr(6) => 0, chr(7) => 0, chr(8) => 0, chr(9) => 0, chr(10) => 0, chr(11) => 0, chr(12) => 0, chr(13) => 0, chr(14) => 0, chr(15) => 0, chr(16) => 0, chr(17) => 0, chr(18) => 0, chr(19) => 0, chr(20) => 0, chr(21) => 0, - chr(22) => 0, chr(23) => 0, chr(24) => 0, chr(25) => 0, chr(26) => 0, chr(27) => 0, chr(28) => 0, chr(29) => 0, chr(30) => 0, chr(31) => 0, ' ' => 278, '!' => 974, '"' => 961, '#' => 974, '$' => 980, '%' => 719, '&' => 789, '\'' => 790, '(' => 791, ')' => 690, '*' => 960, '+' => 939, - ',' => 549, '-' => 855, '.' => 911, '/' => 933, '0' => 911, '1' => 945, '2' => 974, '3' => 755, '4' => 846, '5' => 762, '6' => 761, '7' => 571, '8' => 677, '9' => 763, ':' => 760, ';' => 759, '<' => 754, '=' => 494, '>' => 552, '?' => 537, '@' => 577, 'A' => 692, - 'B' => 786, 'C' => 788, 'D' => 788, 'E' => 790, 'F' => 793, 'G' => 794, 'H' => 816, 'I' => 823, 'J' => 789, 'K' => 841, 'L' => 823, 'M' => 833, 'N' => 816, 'O' => 831, 'P' => 923, 'Q' => 744, 'R' => 723, 'S' => 749, 'T' => 790, 'U' => 792, 'V' => 695, 'W' => 776, - 'X' => 768, 'Y' => 792, 'Z' => 759, '[' => 707, '\\' => 708, ']' => 682, '^' => 701, '_' => 826, '`' => 815, 'a' => 789, 'b' => 789, 'c' => 707, 'd' => 687, 'e' => 696, 'f' => 689, 'g' => 786, 'h' => 787, 'i' => 713, 'j' => 791, 'k' => 785, 'l' => 791, 'm' => 873, - 'n' => 761, 'o' => 762, 'p' => 762, 'q' => 759, 'r' => 759, 's' => 892, 't' => 892, 'u' => 788, 'v' => 784, 'w' => 438, 'x' => 138, 'y' => 277, 'z' => 415, '{' => 392, '|' => 392, '}' => 668, '~' => 668, chr(127) => 0, chr(128) => 390, chr(129) => 390, chr(130) => 317, chr(131) => 317, - chr(132) => 276, chr(133) => 276, chr(134) => 509, chr(135) => 509, chr(136) => 410, chr(137) => 410, chr(138) => 234, chr(139) => 234, chr(140) => 334, chr(141) => 334, chr(142) => 0, chr(143) => 0, chr(144) => 0, chr(145) => 0, chr(146) => 0, chr(147) => 0, chr(148) => 0, chr(149) => 0, chr(150) => 0, chr(151) => 0, chr(152) => 0, chr(153) => 0, - chr(154) => 0, chr(155) => 0, chr(156) => 0, chr(157) => 0, chr(158) => 0, chr(159) => 0, chr(160) => 0, chr(161) => 732, chr(162) => 544, chr(163) => 544, chr(164) => 910, chr(165) => 667, chr(166) => 760, chr(167) => 760, chr(168) => 776, chr(169) => 595, chr(170) => 694, chr(171) => 626, chr(172) => 788, chr(173) => 788, chr(174) => 788, chr(175) => 788, - chr(176) => 788, chr(177) => 788, chr(178) => 788, chr(179) => 788, chr(180) => 788, chr(181) => 788, chr(182) => 788, chr(183) => 788, chr(184) => 788, chr(185) => 788, chr(186) => 788, chr(187) => 788, chr(188) => 788, chr(189) => 788, chr(190) => 788, chr(191) => 788, chr(192) => 788, chr(193) => 788, chr(194) => 788, chr(195) => 788, chr(196) => 788, chr(197) => 788, - chr(198) => 788, chr(199) => 788, chr(200) => 788, chr(201) => 788, chr(202) => 788, chr(203) => 788, chr(204) => 788, chr(205) => 788, chr(206) => 788, chr(207) => 788, chr(208) => 788, chr(209) => 788, chr(210) => 788, chr(211) => 788, chr(212) => 894, chr(213) => 838, chr(214) => 1016, chr(215) => 458, chr(216) => 748, chr(217) => 924, chr(218) => 748, chr(219) => 918, - chr(220) => 927, chr(221) => 928, chr(222) => 928, chr(223) => 834, chr(224) => 873, chr(225) => 828, chr(226) => 924, chr(227) => 924, chr(228) => 917, chr(229) => 930, chr(230) => 931, chr(231) => 463, chr(232) => 883, chr(233) => 836, chr(234) => 836, chr(235) => 867, chr(236) => 867, chr(237) => 696, chr(238) => 696, chr(239) => 874, chr(240) => 0, chr(241) => 874, - chr(242) => 760, chr(243) => 946, chr(244) => 771, chr(245) => 865, chr(246) => 771, chr(247) => 888, chr(248) => 967, chr(249) => 888, chr(250) => 831, chr(251) => 873, chr(252) => 927, chr(253) => 970, chr(254) => 918, chr(255) => 0]; -$uv = [32 => 32, 33 => [9985, 4], 37 => 9742, 38 => [9990, 4], 42 => 9755, 43 => 9758, 44 => [9996, 28], 72 => 9733, 73 => [10025, 35], 108 => 9679, 109 => 10061, 110 => 9632, 111 => [10063, 4], 115 => 9650, 116 => 9660, 117 => 9670, 118 => 10070, 119 => 9687, 120 => [10072, 7], 128 => [10088, 14], 161 => [10081, 7], 168 => 9827, 169 => 9830, 170 => 9829, 171 => 9824, 172 => [9312, 10], 182 => [10102, 31], 213 => 8594, 214 => [8596, 2], 216 => [10136, 24], 241 => [10161, 14]]; +$cw = array( + chr(0)=>0,chr(1)=>0,chr(2)=>0,chr(3)=>0,chr(4)=>0,chr(5)=>0,chr(6)=>0,chr(7)=>0,chr(8)=>0,chr(9)=>0,chr(10)=>0,chr(11)=>0,chr(12)=>0,chr(13)=>0,chr(14)=>0,chr(15)=>0,chr(16)=>0,chr(17)=>0,chr(18)=>0,chr(19)=>0,chr(20)=>0,chr(21)=>0, + chr(22)=>0,chr(23)=>0,chr(24)=>0,chr(25)=>0,chr(26)=>0,chr(27)=>0,chr(28)=>0,chr(29)=>0,chr(30)=>0,chr(31)=>0,' '=>278,'!'=>974,'"'=>961,'#'=>974,'$'=>980,'%'=>719,'&'=>789,'\''=>790,'('=>791,')'=>690,'*'=>960,'+'=>939, + ','=>549,'-'=>855,'.'=>911,'/'=>933,'0'=>911,'1'=>945,'2'=>974,'3'=>755,'4'=>846,'5'=>762,'6'=>761,'7'=>571,'8'=>677,'9'=>763,':'=>760,';'=>759,'<'=>754,'='=>494,'>'=>552,'?'=>537,'@'=>577,'A'=>692, + 'B'=>786,'C'=>788,'D'=>788,'E'=>790,'F'=>793,'G'=>794,'H'=>816,'I'=>823,'J'=>789,'K'=>841,'L'=>823,'M'=>833,'N'=>816,'O'=>831,'P'=>923,'Q'=>744,'R'=>723,'S'=>749,'T'=>790,'U'=>792,'V'=>695,'W'=>776, + 'X'=>768,'Y'=>792,'Z'=>759,'['=>707,'\\'=>708,']'=>682,'^'=>701,'_'=>826,'`'=>815,'a'=>789,'b'=>789,'c'=>707,'d'=>687,'e'=>696,'f'=>689,'g'=>786,'h'=>787,'i'=>713,'j'=>791,'k'=>785,'l'=>791,'m'=>873, + 'n'=>761,'o'=>762,'p'=>762,'q'=>759,'r'=>759,'s'=>892,'t'=>892,'u'=>788,'v'=>784,'w'=>438,'x'=>138,'y'=>277,'z'=>415,'{'=>392,'|'=>392,'}'=>668,'~'=>668,chr(127)=>0,chr(128)=>390,chr(129)=>390,chr(130)=>317,chr(131)=>317, + chr(132)=>276,chr(133)=>276,chr(134)=>509,chr(135)=>509,chr(136)=>410,chr(137)=>410,chr(138)=>234,chr(139)=>234,chr(140)=>334,chr(141)=>334,chr(142)=>0,chr(143)=>0,chr(144)=>0,chr(145)=>0,chr(146)=>0,chr(147)=>0,chr(148)=>0,chr(149)=>0,chr(150)=>0,chr(151)=>0,chr(152)=>0,chr(153)=>0, + chr(154)=>0,chr(155)=>0,chr(156)=>0,chr(157)=>0,chr(158)=>0,chr(159)=>0,chr(160)=>0,chr(161)=>732,chr(162)=>544,chr(163)=>544,chr(164)=>910,chr(165)=>667,chr(166)=>760,chr(167)=>760,chr(168)=>776,chr(169)=>595,chr(170)=>694,chr(171)=>626,chr(172)=>788,chr(173)=>788,chr(174)=>788,chr(175)=>788, + chr(176)=>788,chr(177)=>788,chr(178)=>788,chr(179)=>788,chr(180)=>788,chr(181)=>788,chr(182)=>788,chr(183)=>788,chr(184)=>788,chr(185)=>788,chr(186)=>788,chr(187)=>788,chr(188)=>788,chr(189)=>788,chr(190)=>788,chr(191)=>788,chr(192)=>788,chr(193)=>788,chr(194)=>788,chr(195)=>788,chr(196)=>788,chr(197)=>788, + chr(198)=>788,chr(199)=>788,chr(200)=>788,chr(201)=>788,chr(202)=>788,chr(203)=>788,chr(204)=>788,chr(205)=>788,chr(206)=>788,chr(207)=>788,chr(208)=>788,chr(209)=>788,chr(210)=>788,chr(211)=>788,chr(212)=>894,chr(213)=>838,chr(214)=>1016,chr(215)=>458,chr(216)=>748,chr(217)=>924,chr(218)=>748,chr(219)=>918, + chr(220)=>927,chr(221)=>928,chr(222)=>928,chr(223)=>834,chr(224)=>873,chr(225)=>828,chr(226)=>924,chr(227)=>924,chr(228)=>917,chr(229)=>930,chr(230)=>931,chr(231)=>463,chr(232)=>883,chr(233)=>836,chr(234)=>836,chr(235)=>867,chr(236)=>867,chr(237)=>696,chr(238)=>696,chr(239)=>874,chr(240)=>0,chr(241)=>874, + chr(242)=>760,chr(243)=>946,chr(244)=>771,chr(245)=>865,chr(246)=>771,chr(247)=>888,chr(248)=>967,chr(249)=>888,chr(250)=>831,chr(251)=>873,chr(252)=>927,chr(253)=>970,chr(254)=>918,chr(255)=>0); +$uv = array(32=>32,33=>array(9985,4),37=>9742,38=>array(9990,4),42=>9755,43=>9758,44=>array(9996,28),72=>9733,73=>array(10025,35),108=>9679,109=>10061,110=>9632,111=>array(10063,4),115=>9650,116=>9660,117=>9670,118=>10070,119=>9687,120=>array(10072,7),128=>array(10088,14),161=>array(10081,7),168=>9827,169=>9830,170=>9829,171=>9824,172=>array(9312,10),182=>array(10102,31),213=>8594,214=>array(8596,2),216=>array(10136,24),241=>array(10161,14)); +?> diff --git a/app/ThirdParty/fpdf/fpdf.php b/app/ThirdParty/fpdf/fpdf.php index cefd575c..3c76bd5c 100644 --- a/app/ThirdParty/fpdf/fpdf.php +++ b/app/ThirdParty/fpdf/fpdf.php @@ -10,2065 +10,1810 @@ class FPDF { - const VERSION = '1.86'; - - protected $page; // current page number - - protected $n; // current object number - - protected $offsets; // array of object offsets - - protected $buffer; // buffer holding in-memory PDF - - protected $pages; // array containing pages - - protected $state; // current document state - - protected $compress; // compression flag - - protected $iconv; // whether iconv is available - - protected $k; // scale factor (number of points in user unit) - - protected $DefOrientation; // default orientation - - protected $CurOrientation; // current orientation - - protected $StdPageSizes; // standard page sizes - - protected $DefPageSize; // default page size - - protected $CurPageSize; // current page size - - protected $CurRotation; // current page rotation - - protected $PageInfo; // page-related data - - protected $wPt; - - protected $hPt; // dimensions of current page in points - - protected $w; - - protected $h; // dimensions of current page in user unit - - protected $lMargin; // left margin - - protected $tMargin; // top margin - - protected $rMargin; // right margin - - protected $bMargin; // page break margin - - protected $cMargin; // cell margin - - protected $x; - - protected $y; // current position in user unit - - protected $lasth; // height of last printed cell - - protected $LineWidth; // line width in user unit - - protected $fontpath; // directory containing fonts - - protected $CoreFonts; // array of core font names - - protected $fonts; // array of used fonts - - protected $FontFiles; // array of font files - - protected $encodings; // array of encodings - - protected $cmaps; // array of ToUnicode CMaps - - protected $FontFamily; // current font family - - protected $FontStyle; // current font style - - protected $underline; // underlining flag - - protected $CurrentFont; // current font info - - protected $FontSizePt; // current font size in points - - protected $FontSize; // current font size in user unit - - protected $DrawColor; // commands for drawing color - - protected $FillColor; // commands for filling color - - protected $TextColor; // commands for text color - - protected $ColorFlag; // indicates whether fill and text colors are different - - protected $WithAlpha; // indicates whether alpha channel is used - - protected $ws; // word spacing - - protected $images; // array of used images - - protected $PageLinks; // array of links in pages - - protected $links; // array of internal links - - protected $AutoPageBreak; // automatic page breaking - - protected $PageBreakTrigger; // threshold used to trigger page breaks - - protected $InHeader; // flag set when processing header - - protected $InFooter; // flag set when processing footer - - protected $AliasNbPages; // alias for total number of pages - - protected $ZoomMode; // zoom display mode - - protected $LayoutMode; // layout display mode - - protected $metadata; // document properties - - protected $CreationDate; // document creation date - - protected $PDFVersion; // PDF version number - - /******************************************************************************* - * Public methods * - *******************************************************************************/ - - public function __construct($orientation = 'P', $unit = 'mm', $size = 'A4') - { - // FIXED: Set font path directly - $this->fontpath = __DIR__.'/font/'; - - // Initialization of properties - $this->state = 0; - $this->page = 0; - $this->n = 2; - $this->buffer = ''; - $this->pages = []; - $this->PageInfo = []; - $this->fonts = []; - $this->FontFiles = []; - $this->encodings = []; - $this->cmaps = []; - $this->images = []; - $this->links = []; - $this->InHeader = false; - $this->InFooter = false; - $this->lasth = 0; - $this->FontFamily = ''; - $this->FontStyle = ''; - $this->FontSizePt = 12; - $this->underline = false; - $this->DrawColor = '0 G'; - $this->FillColor = '0 g'; - $this->TextColor = '0 g'; - $this->ColorFlag = false; - $this->WithAlpha = false; - $this->ws = 0; - $this->iconv = function_exists('iconv'); - - // Core fonts - $this->CoreFonts = ['courier', 'helvetica', 'times', 'symbol', 'zapfdingbats']; - - // Scale factor - if ($unit == 'pt') { - $this->k = 1; - } elseif ($unit == 'mm') { - $this->k = 72 / 25.4; - } elseif ($unit == 'cm') { - $this->k = 72 / 2.54; - } elseif ($unit == 'in') { - $this->k = 72; - } else { - $this->Error('Incorrect unit: '.$unit); - } - - // Page sizes - $this->StdPageSizes = [ - 'a3' => [841.89, 1190.55], - 'a4' => [595.28, 841.89], - 'a5' => [420.94, 595.28], - 'letter' => [612, 792], - 'legal' => [612, 1008], - ]; - $size = $this->_getpagesize($size); - $this->DefPageSize = $size; - $this->CurPageSize = $size; - - // Page orientation - $orientation = strtolower($orientation); - if ($orientation == 'p' || $orientation == 'portrait') { - $this->DefOrientation = 'P'; - $this->w = $size[0]; - $this->h = $size[1]; - } elseif ($orientation == 'l' || $orientation == 'landscape') { - $this->DefOrientation = 'L'; - $this->w = $size[1]; - $this->h = $size[0]; - } else { - $this->Error('Incorrect orientation: '.$orientation); - } - $this->CurOrientation = $this->DefOrientation; - $this->wPt = $this->w * $this->k; - $this->hPt = $this->h * $this->k; - - // Page rotation - $this->CurRotation = 0; - - // Margins - $margin = 28.35 / $this->k; - $this->SetMargins($margin, $margin); - $this->cMargin = $margin / 10; - $this->LineWidth = .567 / $this->k; - - // Page break - $this->SetAutoPageBreak(true, 2 * $margin); - - // Display and compression - $this->SetDisplayMode('default'); - $this->SetCompression(true); - - // Metadata - $this->metadata = ['Producer' => 'FPDF '.self::VERSION]; - $this->PDFVersion = '1.3'; - } - - public function SetMargins($left, $top, $right = null) - { - // Set left, top and right margins - $this->lMargin = $left; - $this->tMargin = $top; - if ($right === null) { - $right = $left; - } - $this->rMargin = $right; - } - - public function SetLeftMargin($margin) - { - // Set left margin - $this->lMargin = $margin; - if ($this->page > 0 && $this->x < $margin) { - $this->x = $margin; - } - } - - public function SetTopMargin($margin) - { - // Set top margin - $this->tMargin = $margin; - } - - public function SetRightMargin($margin) - { - // Set right margin - $this->rMargin = $margin; - } - - public function SetAutoPageBreak($auto, $margin = 0) - { - // Set auto page break mode and triggering margin - $this->AutoPageBreak = $auto; - $this->bMargin = $margin; - $this->PageBreakTrigger = $this->h - $margin; - } - - public function SetDisplayMode($zoom, $layout = 'default') - { - // Set display mode in viewer - if ($zoom == 'fullpage' || $zoom == 'fullwidth' || $zoom == 'real' || $zoom == 'default' || ! is_string($zoom)) { - $this->ZoomMode = $zoom; - } else { - $this->Error('Incorrect zoom display mode: '.$zoom); - } - if ($layout == 'single' || $layout == 'continuous' || $layout == 'two' || $layout == 'default') { - $this->LayoutMode = $layout; - } else { - $this->Error('Incorrect layout display mode: '.$layout); - } - } - - public function SetCompression($compress) - { - // Set page compression - if (function_exists('gzcompress')) { - $this->compress = $compress; - } else { - $this->compress = false; - } - } - - public function SetTitle($title, $isUTF8 = false) - { - // Title of document - $this->metadata['Title'] = $isUTF8 ? $title : $this->_UTF8encode($title); - } - - public function SetAuthor($author, $isUTF8 = false) - { - // Author of document - $this->metadata['Author'] = $isUTF8 ? $author : $this->_UTF8encode($author); - } - - public function SetSubject($subject, $isUTF8 = false) - { - // Subject of document - $this->metadata['Subject'] = $isUTF8 ? $subject : $this->_UTF8encode($subject); - } - - public function SetKeywords($keywords, $isUTF8 = false) - { - // Keywords of document - $this->metadata['Keywords'] = $isUTF8 ? $keywords : $this->_UTF8encode($keywords); - } - - public function SetCreator($creator, $isUTF8 = false) - { - // Creator of document - $this->metadata['Creator'] = $isUTF8 ? $creator : $this->_UTF8encode($creator); - } - - public function AliasNbPages($alias = '{nb}') - { - // Define an alias for total number of pages - $this->AliasNbPages = $alias; - } - - public function Error($msg) - { - // Fatal error - throw new Exception('FPDF error: '.$msg); - } - - public function Close() - { - // Terminate document - if ($this->state == 3) { - return; - } - if ($this->page == 0) { - $this->AddPage(); - } - // Page footer - $this->InFooter = true; - $this->Footer(); - $this->InFooter = false; - // Close page - $this->_endpage(); - // Close document - $this->_enddoc(); - } - - public function AddPage($orientation = '', $size = '', $rotation = 0) - { - // Start a new page - if ($this->state == 3) { - $this->Error('The document is closed'); - } - $family = $this->FontFamily; - $style = $this->FontStyle.($this->underline ? 'U' : ''); - $fontsize = $this->FontSizePt; - $lw = $this->LineWidth; - $dc = $this->DrawColor; - $fc = $this->FillColor; - $tc = $this->TextColor; - $cf = $this->ColorFlag; - if ($this->page > 0) { - // Page footer - $this->InFooter = true; - $this->Footer(); - $this->InFooter = false; - // Close page - $this->_endpage(); - } - // Start new page - $this->_beginpage($orientation, $size, $rotation); - // Set line cap style to square - $this->_out('2 J'); - // Set line width - $this->LineWidth = $lw; - $this->_out(sprintf('%.2F w', $lw * $this->k)); - // Set font - if ($family) { - $this->SetFont($family, $style, $fontsize); - } - // Set colors - $this->DrawColor = $dc; - if ($dc != '0 G') { - $this->_out($dc); - } - $this->FillColor = $fc; - if ($fc != '0 g') { - $this->_out($fc); - } - $this->TextColor = $tc; - $this->ColorFlag = $cf; - // Page header - $this->InHeader = true; - $this->Header(); - $this->InHeader = false; - // Restore line width - if ($this->LineWidth != $lw) { - $this->LineWidth = $lw; - $this->_out(sprintf('%.2F w', $lw * $this->k)); - } - // Restore font - if ($family) { - $this->SetFont($family, $style, $fontsize); - } - // Restore colors - if ($this->DrawColor != $dc) { - $this->DrawColor = $dc; - $this->_out($dc); - } - if ($this->FillColor != $fc) { - $this->FillColor = $fc; - $this->_out($fc); - } - $this->TextColor = $tc; - $this->ColorFlag = $cf; - } - - public function Header() - { - // To be implemented in your own inherited class - } - - public function Footer() - { - // To be implemented in your own inherited class - } - - public function PageNo() - { - // Get current page number - return $this->page; - } - - public function SetDrawColor($r, $g = null, $b = null) - { - // Set color for all stroking operations - if (($r == 0 && $g == 0 && $b == 0) || $g === null) { - $this->DrawColor = sprintf('%.3F G', $r / 255); - } else { - $this->DrawColor = sprintf('%.3F %.3F %.3F RG', $r / 255, $g / 255, $b / 255); - } - if ($this->page > 0) { - $this->_out($this->DrawColor); - } - } - - public function SetFillColor($r, $g = null, $b = null) - { - // Set color for all filling operations - if (($r == 0 && $g == 0 && $b == 0) || $g === null) { - $this->FillColor = sprintf('%.3F g', $r / 255); - } else { - $this->FillColor = sprintf('%.3F %.3F %.3F rg', $r / 255, $g / 255, $b / 255); - } - $this->ColorFlag = ($this->FillColor != $this->TextColor); - if ($this->page > 0) { - $this->_out($this->FillColor); - } - } - - public function SetTextColor($r, $g = null, $b = null) - { - // Set color for text - if (($r == 0 && $g == 0 && $b == 0) || $g === null) { - $this->TextColor = sprintf('%.3F g', $r / 255); - } else { - $this->TextColor = sprintf('%.3F %.3F %.3F rg', $r / 255, $g / 255, $b / 255); - } - $this->ColorFlag = ($this->FillColor != $this->TextColor); - } - - public function GetStringWidth($s) - { - // Get width of a string in the current font - $cw = $this->CurrentFont['cw']; - $w = 0; - $s = (string) $s; - $l = strlen($s); - for ($i = 0; $i < $l; $i++) { - $w += $cw[$s[$i]]; - } - - return $w * $this->FontSize / 1000; - } - - public function SetLineWidth($width) - { - // Set line width - $this->LineWidth = $width; - if ($this->page > 0) { - $this->_out(sprintf('%.2F w', $width * $this->k)); - } - } - - public function Line($x1, $y1, $x2, $y2) - { - // Draw a line - $this->_out(sprintf('%.2F %.2F m %.2F %.2F l S', $x1 * $this->k, ($this->h - $y1) * $this->k, $x2 * $this->k, ($this->h - $y2) * $this->k)); - } - - public function Rect($x, $y, $w, $h, $style = '') - { - // Draw a rectangle - if ($style == 'F') { - $op = 'f'; - } elseif ($style == 'FD' || $style == 'DF') { - $op = 'B'; - } else { - $op = 'S'; - } - $this->_out(sprintf('%.2F %.2F %.2F %.2F re %s', $x * $this->k, ($this->h - $y) * $this->k, $w * $this->k, -$h * $this->k, $op)); - } - - public function AddFont($family, $style = '', $file = '', $dir = '') - { - // Add a TrueType, OpenType or Type1 font - $family = strtolower($family); - if ($file == '') { - $file = str_replace(' ', '', $family).strtolower($style).'.php'; - } - $style = strtoupper($style); - if ($style == 'IB') { - $style = 'BI'; - } - $fontkey = $family.$style; - if (isset($this->fonts[$fontkey])) { - return; - } - if (strpos($file, '/') !== false || strpos($file, '\\') !== false) { - $this->Error('Incorrect font definition file name: '.$file); - } - if ($dir == '') { - $dir = $this->fontpath; - } - if (substr($dir, -1) != '/' && substr($dir, -1) != '\\') { - $dir .= '/'; - } - $info = $this->_loadfont($dir.$file); - $info['i'] = count($this->fonts) + 1; - if (! empty($info['file'])) { - // Embedded font - $info['file'] = $dir.$info['file']; - if ($info['type'] == 'TrueType') { - $this->FontFiles[$info['file']] = ['length1' => $info['originalsize']]; - } else { - $this->FontFiles[$info['file']] = ['length1' => $info['size1'], 'length2' => $info['size2']]; - } - } - $this->fonts[$fontkey] = $info; - } - - public function SetFont($family, $style = '', $size = 0) - { - // Select a font; size given in points - if ($family == '') { - $family = $this->FontFamily; - } else { - $family = strtolower($family); - } - $style = strtoupper($style); - if (strpos($style, 'U') !== false) { - $this->underline = true; - $style = str_replace('U', '', $style); - } else { - $this->underline = false; - } - if ($style == 'IB') { - $style = 'BI'; - } - if ($size == 0) { - $size = $this->FontSizePt; - } - // Test if font is already selected - if ($this->FontFamily == $family && $this->FontStyle == $style && $this->FontSizePt == $size) { - return; - } - // Test if font is already loaded - $fontkey = $family.$style; - if (! isset($this->fonts[$fontkey])) { - // Test if one of the core fonts - if ($family == 'arial') { - $family = 'helvetica'; - } - if (in_array($family, $this->CoreFonts)) { - if ($family == 'symbol' || $family == 'zapfdingbats') { - $style = ''; - } - $fontkey = $family.$style; - if (! isset($this->fonts[$fontkey])) { - $this->AddFont($family, $style); - } - } else { - $this->Error('Undefined font: '.$family.' '.$style); - } - } - // Select it - $this->FontFamily = $family; - $this->FontStyle = $style; - $this->FontSizePt = $size; - $this->FontSize = $size / $this->k; - $this->CurrentFont = $this->fonts[$fontkey]; - if ($this->page > 0) { - $this->_out(sprintf('BT /F%d %.2F Tf ET', $this->CurrentFont['i'], $this->FontSizePt)); - } - } - - public function SetFontSize($size) - { - // Set font size in points - if ($this->FontSizePt == $size) { - return; - } - $this->FontSizePt = $size; - $this->FontSize = $size / $this->k; - if ($this->page > 0 && isset($this->CurrentFont)) { - $this->_out(sprintf('BT /F%d %.2F Tf ET', $this->CurrentFont['i'], $this->FontSizePt)); - } - } - - public function AddLink() - { - // Create a new internal link - $n = count($this->links) + 1; - $this->links[$n] = [0, 0]; - - return $n; - } - - public function SetLink($link, $y = 0, $page = -1) - { - // Set destination of internal link - if ($y == -1) { - $y = $this->y; - } - if ($page == -1) { - $page = $this->page; - } - $this->links[$link] = [$page, $y]; - } - - public function Link($x, $y, $w, $h, $link) - { - // Put a link on the page - $this->PageLinks[$this->page][] = [$x * $this->k, $this->hPt - $y * $this->k, $w * $this->k, $h * $this->k, $link]; - } - - public function Text($x, $y, $txt) - { - // Output a string - if (! isset($this->CurrentFont)) { - $this->Error('No font has been set'); - } - $txt = (string) $txt; - $s = sprintf('BT %.2F %.2F Td (%s) Tj ET', $x * $this->k, ($this->h - $y) * $this->k, $this->_escape($txt)); - if ($this->underline && $txt !== '') { - $s .= ' '.$this->_dounderline($x, $y, $txt); - } - if ($this->ColorFlag) { - $s = 'q '.$this->TextColor.' '.$s.' Q'; - } - $this->_out($s); - } - - public function AcceptPageBreak() - { - // Accept automatic page break or not - return $this->AutoPageBreak; - } - - public function Cell($w, $h = 0, $txt = '', $border = 0, $ln = 0, $align = '', $fill = false, $link = '') - { - // Output a cell - $k = $this->k; - if ($this->y + $h > $this->PageBreakTrigger && ! $this->InHeader && ! $this->InFooter && $this->AcceptPageBreak()) { - // Automatic page break - $x = $this->x; - $ws = $this->ws; - if ($ws > 0) { - $this->ws = 0; - $this->_out('0 Tw'); - } - $this->AddPage($this->CurOrientation, $this->CurPageSize, $this->CurRotation); - $this->x = $x; - if ($ws > 0) { - $this->ws = $ws; - $this->_out(sprintf('%.3F Tw', $ws * $k)); - } - } - if ($w == 0) { - $w = $this->w - $this->rMargin - $this->x; - } - $s = ''; - if ($fill || $border == 1) { - if ($fill) { - $op = ($border == 1) ? 'B' : 'f'; - } else { - $op = 'S'; - } - $s = sprintf('%.2F %.2F %.2F %.2F re %s ', $this->x * $k, ($this->h - $this->y) * $k, $w * $k, -$h * $k, $op); - } - if (is_string($border)) { - $x = $this->x; - $y = $this->y; - if (strpos($border, 'L') !== false) { - $s .= sprintf('%.2F %.2F m %.2F %.2F l S ', $x * $k, ($this->h - $y) * $k, $x * $k, ($this->h - ($y + $h)) * $k); - } - if (strpos($border, 'T') !== false) { - $s .= sprintf('%.2F %.2F m %.2F %.2F l S ', $x * $k, ($this->h - $y) * $k, ($x + $w) * $k, ($this->h - $y) * $k); - } - if (strpos($border, 'R') !== false) { - $s .= sprintf('%.2F %.2F m %.2F %.2F l S ', ($x + $w) * $k, ($this->h - $y) * $k, ($x + $w) * $k, ($this->h - ($y + $h)) * $k); - } - if (strpos($border, 'B') !== false) { - $s .= sprintf('%.2F %.2F m %.2F %.2F l S ', $x * $k, ($this->h - ($y + $h)) * $k, ($x + $w) * $k, ($this->h - ($y + $h)) * $k); - } - } - $txt = (string) $txt; - if ($txt !== '') { - if (! isset($this->CurrentFont)) { - $this->Error('No font has been set'); - } - if ($align == 'R') { - $dx = $w - $this->cMargin - $this->GetStringWidth($txt); - } elseif ($align == 'C') { - $dx = ($w - $this->GetStringWidth($txt)) / 2; - } else { - $dx = $this->cMargin; - } - if ($this->ColorFlag) { - $s .= 'q '.$this->TextColor.' '; - } - $s .= sprintf('BT %.2F %.2F Td (%s) Tj ET', ($this->x + $dx) * $k, ($this->h - ($this->y + .5 * $h + .3 * $this->FontSize)) * $k, $this->_escape($txt)); - if ($this->underline) { - $s .= ' '.$this->_dounderline($this->x + $dx, $this->y + .5 * $h + .3 * $this->FontSize, $txt); - } - if ($this->ColorFlag) { - $s .= ' Q'; - } - if ($link) { - $this->Link($this->x + $dx, $this->y + .5 * $h - .5 * $this->FontSize, $this->GetStringWidth($txt), $this->FontSize, $link); - } - } - if ($s) { - $this->_out($s); - } - $this->lasth = $h; - if ($ln > 0) { - // Go to next line - $this->y += $h; - if ($ln == 1) { - $this->x = $this->lMargin; - } - } else { - $this->x += $w; - } - } - - public function MultiCell($w, $h, $txt, $border = 0, $align = 'J', $fill = false) - { - // Output text with automatic or explicit line breaks - if (! isset($this->CurrentFont)) { - $this->Error('No font has been set'); - } - $cw = $this->CurrentFont['cw']; - if ($w == 0) { - $w = $this->w - $this->rMargin - $this->x; - } - $wmax = ($w - 2 * $this->cMargin) * 1000 / $this->FontSize; - $s = str_replace("\r", '', (string) $txt); - $nb = strlen($s); - if ($nb > 0 && $s[$nb - 1] == "\n") { - $nb--; - } - $b = 0; - if ($border) { - if ($border == 1) { - $border = 'LTRB'; - $b = 'LRT'; - $b2 = 'LR'; - } else { - $b2 = ''; - if (strpos($border, 'L') !== false) { - $b2 .= 'L'; - } - if (strpos($border, 'R') !== false) { - $b2 .= 'R'; - } - $b = (strpos($border, 'T') !== false) ? $b2.'T' : $b2; - } - } - $sep = -1; - $i = 0; - $j = 0; - $l = 0; - $ns = 0; - $nl = 1; - while ($i < $nb) { - // Get next character - $c = $s[$i]; - if ($c == "\n") { - // Explicit line break - if ($this->ws > 0) { - $this->ws = 0; - $this->_out('0 Tw'); - } - $this->Cell($w, $h, substr($s, $j, $i - $j), $b, 2, $align, $fill); - $i++; - $sep = -1; - $j = $i; - $l = 0; - $ns = 0; - $nl++; - if ($border && $nl == 2) { - $b = $b2; - } - - continue; - } - if ($c == ' ') { - $sep = $i; - $ls = $l; - $ns++; - } - $l += $cw[$c]; - if ($l > $wmax) { - // Automatic line break - if ($sep == -1) { - if ($i == $j) { - $i++; - } - if ($this->ws > 0) { - $this->ws = 0; - $this->_out('0 Tw'); - } - $this->Cell($w, $h, substr($s, $j, $i - $j), $b, 2, $align, $fill); - } else { - if ($align == 'J') { - $this->ws = ($ns > 1) ? ($wmax - $ls) / 1000 * $this->FontSize / ($ns - 1) : 0; - $this->_out(sprintf('%.3F Tw', $this->ws * $this->k)); - } - $this->Cell($w, $h, substr($s, $j, $sep - $j), $b, 2, $align, $fill); - $i = $sep + 1; - } - $sep = -1; - $j = $i; - $l = 0; - $ns = 0; - $nl++; - if ($border && $nl == 2) { - $b = $b2; - } - } else { - $i++; - } - } - // Last chunk - if ($this->ws > 0) { - $this->ws = 0; - $this->_out('0 Tw'); - } - if ($border && strpos($border, 'B') !== false) { - $b .= 'B'; - } - $this->Cell($w, $h, substr($s, $j, $i - $j), $b, 2, $align, $fill); - $this->x = $this->lMargin; - } - - public function Write($h, $txt, $link = '') - { - // Output text in flowing mode - if (! isset($this->CurrentFont)) { - $this->Error('No font has been set'); - } - $cw = $this->CurrentFont['cw']; - $w = $this->w - $this->rMargin - $this->x; - $wmax = ($w - 2 * $this->cMargin) * 1000 / $this->FontSize; - $s = str_replace("\r", '', (string) $txt); - $nb = strlen($s); - $sep = -1; - $i = 0; - $j = 0; - $l = 0; - $nl = 1; - while ($i < $nb) { - // Get next character - $c = $s[$i]; - if ($c == "\n") { - // Explicit line break - $this->Cell($w, $h, substr($s, $j, $i - $j), 0, 2, '', false, $link); - $i++; - $sep = -1; - $j = $i; - $l = 0; - if ($nl == 1) { - $this->x = $this->lMargin; - $w = $this->w - $this->rMargin - $this->x; - $wmax = ($w - 2 * $this->cMargin) * 1000 / $this->FontSize; - } - $nl++; - - continue; - } - if ($c == ' ') { - $sep = $i; - } - $l += $cw[$c]; - if ($l > $wmax) { - // Automatic line break - if ($sep == -1) { - if ($this->x > $this->lMargin) { - // Move to next line - $this->x = $this->lMargin; - $this->y += $h; - $w = $this->w - $this->rMargin - $this->x; - $wmax = ($w - 2 * $this->cMargin) * 1000 / $this->FontSize; - $i++; - $nl++; - - continue; - } - if ($i == $j) { - $i++; - } - $this->Cell($w, $h, substr($s, $j, $i - $j), 0, 2, '', false, $link); - } else { - $this->Cell($w, $h, substr($s, $j, $sep - $j), 0, 2, '', false, $link); - $i = $sep + 1; - } - $sep = -1; - $j = $i; - $l = 0; - if ($nl == 1) { - $this->x = $this->lMargin; - $w = $this->w - $this->rMargin - $this->x; - $wmax = ($w - 2 * $this->cMargin) * 1000 / $this->FontSize; - } - $nl++; - } else { - $i++; - } - } - // Last chunk - if ($i != $j) { - $this->Cell($l / 1000 * $this->FontSize, $h, substr($s, $j), 0, 0, '', false, $link); - } - } - - public function Ln($h = null) - { - // Line feed; default value is the last cell height - $this->x = $this->lMargin; - if ($h === null) { - $this->y += $this->lasth; - } else { - $this->y += $h; - } - } - - public function Image($file, $x = null, $y = null, $w = 0, $h = 0, $type = '', $link = '') - { - // Put an image on the page - if ($file == '') { - $this->Error('Image file name is empty'); - } - if (! isset($this->images[$file])) { - // First use of this image, get info - if ($type == '') { - $pos = strrpos($file, '.'); - if (! $pos) { - $this->Error('Image file has no extension and no type was specified: '.$file); - } - $type = substr($file, $pos + 1); - } - $type = strtolower($type); - if ($type == 'jpeg') { - $type = 'jpg'; - } - $mtd = '_parse'.$type; - if (! method_exists($this, $mtd)) { - $this->Error('Unsupported image type: '.$type); - } - $info = $this->$mtd($file); - $info['i'] = count($this->images) + 1; - $this->images[$file] = $info; - } else { - $info = $this->images[$file]; - } - - // Automatic width and height calculation if needed - if ($w == 0 && $h == 0) { - // Put image at 96 dpi - $w = -96; - $h = -96; - } - if ($w < 0) { - $w = -$info['w'] * 72 / $w / $this->k; - } - if ($h < 0) { - $h = -$info['h'] * 72 / $h / $this->k; - } - if ($w == 0) { - $w = $h * $info['w'] / $info['h']; - } - if ($h == 0) { - $h = $w * $info['h'] / $info['w']; - } - - // Flowing mode - if ($y === null) { - if ($this->y + $h > $this->PageBreakTrigger && ! $this->InHeader && ! $this->InFooter && $this->AcceptPageBreak()) { - // Automatic page break - $x2 = $this->x; - $this->AddPage($this->CurOrientation, $this->CurPageSize, $this->CurRotation); - $this->x = $x2; - } - $y = $this->y; - $this->y += $h; - } - - if ($x === null) { - $x = $this->x; - } - $this->_out(sprintf('q %.2F 0 0 %.2F %.2F %.2F cm /I%d Do Q', $w * $this->k, $h * $this->k, $x * $this->k, ($this->h - ($y + $h)) * $this->k, $info['i'])); - if ($link) { - $this->Link($x, $y, $w, $h, $link); - } - } - - public function GetPageWidth() - { - // Get current page width - return $this->w; - } - - public function GetPageHeight() - { - // Get current page height - return $this->h; - } - - public function GetX() - { - // Get x position - return $this->x; - } - - public function SetX($x) - { - // Set x position - if ($x >= 0) { - $this->x = $x; - } else { - $this->x = $this->w + $x; - } - } - - public function GetY() - { - // Get y position - return $this->y; - } - - public function SetY($y, $resetX = true) - { - // Set y position and optionally reset x - if ($y >= 0) { - $this->y = $y; - } else { - $this->y = $this->h + $y; - } - if ($resetX) { - $this->x = $this->lMargin; - } - } - - public function SetXY($x, $y) - { - // Set x and y positions - $this->SetX($x); - $this->SetY($y, false); - } - - public function Output($dest = '', $name = '', $isUTF8 = false) - { - // Output PDF to some destination - $this->Close(); - if (strlen($name) == 1 && strlen($dest) != 1) { - // Fix parameter order - $tmp = $dest; - $dest = $name; - $name = $tmp; - } - if ($dest == '') { - $dest = 'I'; - } - if ($name == '') { - $name = 'doc.pdf'; - } - switch (strtoupper($dest)) { - case 'I': - // Send to standard output - $this->_checkoutput(); - if (PHP_SAPI != 'cli') { - // We send to a browser - header('Content-Type: application/pdf'); - header('Content-Disposition: inline; '.$this->_httpencode('filename', $name, $isUTF8)); - header('Cache-Control: private, max-age=0, must-revalidate'); - header('Pragma: public'); - } - echo $this->buffer; - break; - case 'D': - // Download file - $this->_checkoutput(); - header('Content-Type: application/pdf'); - header('Content-Disposition: attachment; '.$this->_httpencode('filename', $name, $isUTF8)); - header('Cache-Control: private, max-age=0, must-revalidate'); - header('Pragma: public'); - echo $this->buffer; - break; - case 'F': - // Save to local file - if (! file_put_contents($name, $this->buffer)) { - $this->Error('Unable to create output file: '.$name); - } - break; - case 'S': - // Return as a string - return $this->buffer; - default: - $this->Error('Incorrect output destination: '.$dest); - } - - return ''; - } - - /******************************************************************************* - * Protected methods * - *******************************************************************************/ - - protected function _checkoutput() - { - if (PHP_SAPI != 'cli') { - if (headers_sent($file, $line)) { - $this->Error("Some data has already been output, can't send PDF file (output started at $file:$line)"); - } - } - if (ob_get_length()) { - // The output buffer is not empty - if (preg_match('/^(\xEF\xBB\xBF)?\s*$/', ob_get_contents())) { - // It contains only a UTF-8 BOM and/or whitespace, let's clean it - ob_clean(); - } else { - $this->Error("Some data has already been output, can't send PDF file"); - } - } - } - - protected function _getpagesize($size) - { - if (is_string($size)) { - $size = strtolower($size); - if (! isset($this->StdPageSizes[$size])) { - $this->Error('Unknown page size: '.$size); - } - $a = $this->StdPageSizes[$size]; - - return [$a[0] / $this->k, $a[1] / $this->k]; - } else { - if ($size[0] > $size[1]) { - return [$size[1], $size[0]]; - } else { - return $size; - } - } - } - - protected function _beginpage($orientation, $size, $rotation) - { - $this->page++; - $this->pages[$this->page] = ''; - $this->PageLinks[$this->page] = []; - $this->state = 2; - $this->x = $this->lMargin; - $this->y = $this->tMargin; - $this->FontFamily = ''; - // Check page size and orientation - if ($orientation == '') { - $orientation = $this->DefOrientation; - } else { - $orientation = strtoupper($orientation[0]); - } - if ($size == '') { - $size = $this->DefPageSize; - } else { - $size = $this->_getpagesize($size); - } - if ($orientation != $this->CurOrientation || $size[0] != $this->CurPageSize[0] || $size[1] != $this->CurPageSize[1]) { - // New size or orientation - if ($orientation == 'P') { - $this->w = $size[0]; - $this->h = $size[1]; - } else { - $this->w = $size[1]; - $this->h = $size[0]; - } - $this->wPt = $this->w * $this->k; - $this->hPt = $this->h * $this->k; - $this->PageBreakTrigger = $this->h - $this->bMargin; - $this->CurOrientation = $orientation; - $this->CurPageSize = $size; - } - if ($orientation != $this->DefOrientation || $size[0] != $this->DefPageSize[0] || $size[1] != $this->DefPageSize[1]) { - $this->PageInfo[$this->page]['size'] = [$this->wPt, $this->hPt]; - } - if ($rotation != 0) { - if ($rotation % 90 != 0) { - $this->Error('Incorrect rotation value: '.$rotation); - } - $this->PageInfo[$this->page]['rotation'] = $rotation; - } - $this->CurRotation = $rotation; - } - - protected function _endpage() - { - $this->state = 1; - } - - protected function _loadfont($path) - { - // Load a font definition file - include $path; - if (! isset($name)) { - $this->Error('Could not include font definition file: '.$path); - } - if (isset($enc)) { - $enc = strtolower($enc); - } - if (! isset($subsetted)) { - $subsetted = false; - } - - return get_defined_vars(); - } - - protected function _isascii($s) - { - // Test if string is ASCII - $nb = strlen($s); - for ($i = 0; $i < $nb; $i++) { - if (ord($s[$i]) > 127) { - return false; - } - } - - return true; - } - - protected function _httpencode($param, $value, $isUTF8) - { - // Encode HTTP header field parameter - if ($this->_isascii($value)) { - return $param.'="'.$value.'"'; - } - if (! $isUTF8) { - $value = $this->_UTF8encode($value); - } - - return $param."*=UTF-8''".rawurlencode($value); - } - - protected function _UTF8encode($s) - { - // Convert ISO-8859-1 to UTF-8 - if ($this->iconv) { - return iconv('ISO-8859-1', 'UTF-8', $s); - } - $res = ''; - $nb = strlen($s); - for ($i = 0; $i < $nb; $i++) { - $c = $s[$i]; - $v = ord($c); - if ($v >= 128) { - $res .= chr(0xC0 | ($v >> 6)); - $res .= chr(0x80 | ($v & 0x3F)); - } else { - $res .= $c; - } - } - - return $res; - } - - protected function _UTF8toUTF16($s) - { - // Convert UTF-8 to UTF-16BE with BOM - $res = "\xFE\xFF"; - if ($this->iconv) { - return $res.iconv('UTF-8', 'UTF-16BE', $s); - } - $nb = strlen($s); - $i = 0; - while ($i < $nb) { - $c1 = ord($s[$i++]); - if ($c1 >= 224) { - // 3-byte character - $c2 = ord($s[$i++]); - $c3 = ord($s[$i++]); - $res .= chr((($c1 & 0x0F) << 4) + (($c2 & 0x3C) >> 2)); - $res .= chr((($c2 & 0x03) << 6) + ($c3 & 0x3F)); - } elseif ($c1 >= 192) { - // 2-byte character - $c2 = ord($s[$i++]); - $res .= chr(($c1 & 0x1C) >> 2); - $res .= chr((($c1 & 0x03) << 6) + ($c2 & 0x3F)); - } else { - // Single-byte character - $res .= "\0".chr($c1); - } - } - - return $res; - } - - protected function _escape($s) - { - // Escape special characters - if (strpos($s, '(') !== false || strpos($s, ')') !== false || strpos($s, '\\') !== false || strpos($s, "\r") !== false) { - return str_replace(['\\', '(', ')', "\r"], ['\\\\', '\\(', '\\)', '\\r'], $s); - } else { - return $s; - } - } - - protected function _textstring($s) - { - // Format a text string - if (! $this->_isascii($s)) { - $s = $this->_UTF8toUTF16($s); - } - - return '('.$this->_escape($s).')'; - } - - protected function _dounderline($x, $y, $txt) - { - // Underline text - $up = $this->CurrentFont['up']; - $ut = $this->CurrentFont['ut']; - $w = $this->GetStringWidth($txt) + $this->ws * substr_count($txt, ' '); - - return sprintf('%.2F %.2F %.2F %.2F re f', $x * $this->k, ($this->h - ($y - $up / 1000 * $this->FontSize)) * $this->k, $w * $this->k, -$ut / 1000 * $this->FontSizePt); - } - - protected function _parsejpg($file) - { - // Extract info from a JPEG file - $a = getimagesize($file); - if (! $a) { - $this->Error('Missing or incorrect image file: '.$file); - } - if ($a[2] != 2) { - $this->Error('Not a JPEG file: '.$file); - } - if (! isset($a['channels']) || $a['channels'] == 3) { - $colspace = 'DeviceRGB'; - } elseif ($a['channels'] == 4) { - $colspace = 'DeviceCMYK'; - } else { - $colspace = 'DeviceGray'; - } - $bpc = isset($a['bits']) ? $a['bits'] : 8; - $data = file_get_contents($file); - - return ['w' => $a[0], 'h' => $a[1], 'cs' => $colspace, 'bpc' => $bpc, 'f' => 'DCTDecode', 'data' => $data]; - } - - protected function _parsepng($file) - { - // Extract info from a PNG file - $f = fopen($file, 'rb'); - if (! $f) { - $this->Error('Can\'t open image file: '.$file); - } - $info = $this->_parsepngstream($f, $file); - fclose($f); - - return $info; - } - - protected function _parsepngstream($f, $file) - { - // Check signature - if ($this->_readstream($f, 8) != chr(137).'PNG'.chr(13).chr(10).chr(26).chr(10)) { - $this->Error('Not a PNG file: '.$file); - } - - // Read header chunk - $this->_readstream($f, 4); - if ($this->_readstream($f, 4) != 'IHDR') { - $this->Error('Incorrect PNG file: '.$file); - } - $w = $this->_readint($f); - $h = $this->_readint($f); - $bpc = ord($this->_readstream($f, 1)); - if ($bpc > 8) { - $this->Error('16-bit depth not supported: '.$file); - } - $ct = ord($this->_readstream($f, 1)); - if ($ct == 0 || $ct == 4) { - $colspace = 'DeviceGray'; - } elseif ($ct == 2 || $ct == 6) { - $colspace = 'DeviceRGB'; - } elseif ($ct == 3) { - $colspace = 'Indexed'; - } else { - $this->Error('Unknown color type: '.$file); - } - if (ord($this->_readstream($f, 1)) != 0) { - $this->Error('Unknown compression method: '.$file); - } - if (ord($this->_readstream($f, 1)) != 0) { - $this->Error('Unknown filter method: '.$file); - } - if (ord($this->_readstream($f, 1)) != 0) { - $this->Error('Interlacing not supported: '.$file); - } - $this->_readstream($f, 4); - $dp = '/Predictor 15 /Colors '.($colspace == 'DeviceRGB' ? 3 : 1).' /BitsPerComponent '.$bpc.' /Columns '.$w; - - // Scan chunks looking for palette, transparency and image data - $pal = ''; - $trns = ''; - $data = ''; - do { - $n = $this->_readint($f); - $type = $this->_readstream($f, 4); - if ($type == 'PLTE') { - // Read palette - $pal = $this->_readstream($f, $n); - $this->_readstream($f, 4); - } elseif ($type == 'tRNS') { - // Read transparency info - $t = $this->_readstream($f, $n); - if ($ct == 0) { - $trns = [ord(substr($t, 1, 1))]; - } elseif ($ct == 2) { - $trns = [ord(substr($t, 1, 1)), ord(substr($t, 3, 1)), ord(substr($t, 5, 1))]; - } else { - $pos = strpos($t, chr(0)); - if ($pos !== false) { - $trns = [$pos]; - } - } - $this->_readstream($f, 4); - } elseif ($type == 'IDAT') { - // Read image data block - $data .= $this->_readstream($f, $n); - $this->_readstream($f, 4); - } elseif ($type == 'IEND') { - break; - } else { - $this->_readstream($f, $n + 4); - } - } while ($n); - - if ($colspace == 'Indexed' && empty($pal)) { - $this->Error('Missing palette in '.$file); - } - $info = ['w' => $w, 'h' => $h, 'cs' => $colspace, 'bpc' => $bpc, 'f' => 'FlateDecode', 'dp' => $dp, 'pal' => $pal, 'trns' => $trns]; - if ($ct >= 4) { - // Extract alpha channel - if (! function_exists('gzuncompress')) { - $this->Error('Zlib not available, can\'t handle alpha channel: '.$file); - } - $data = gzuncompress($data); - $color = ''; - $alpha = ''; - if ($ct == 4) { - // Gray image - $len = 2 * $w; - for ($i = 0; $i < $h; $i++) { - $pos = (1 + $len) * $i; - $color .= $data[$pos]; - $alpha .= $data[$pos]; - $line = substr($data, $pos + 1, $len); - $color .= preg_replace('/(.)./s', '$1', $line); - $alpha .= preg_replace('/.(.)/s', '$1', $line); - } - } else { - // RGB image - $len = 4 * $w; - for ($i = 0; $i < $h; $i++) { - $pos = (1 + $len) * $i; - $color .= $data[$pos]; - $alpha .= $data[$pos]; - $line = substr($data, $pos + 1, $len); - $color .= preg_replace('/(.{3})./s', '$1', $line); - $alpha .= preg_replace('/.{3}(.)/s', '$1', $line); - } - } - unset($data); - $data = gzcompress($color); - $info['smask'] = gzcompress($alpha); - $this->WithAlpha = true; - if ($this->PDFVersion < '1.4') { - $this->PDFVersion = '1.4'; - } - } - $info['data'] = $data; - - return $info; - } - - protected function _readstream($f, $n) - { - // Read n bytes from stream - $res = ''; - while ($n > 0 && ! feof($f)) { - $s = fread($f, $n); - if ($s === false) { - $this->Error('Error while reading stream'); - } - $n -= strlen($s); - $res .= $s; - } - if ($n > 0) { - $this->Error('Unexpected end of stream'); - } - - return $res; - } - - protected function _readint($f) - { - // Read a 4-byte integer from stream - $a = unpack('Ni', $this->_readstream($f, 4)); - - return $a['i']; - } - - protected function _parsegif($file) - { - // Extract info from a GIF file (via PNG conversion) - if (! function_exists('imagepng')) { - $this->Error('GD extension is required for GIF support'); - } - if (! function_exists('imagecreatefromgif')) { - $this->Error('GD has no GIF read support'); - } - $im = imagecreatefromgif($file); - if (! $im) { - $this->Error('Missing or incorrect image file: '.$file); - } - imageinterlace($im, 0); - ob_start(); - imagepng($im); - $data = ob_get_clean(); - imagedestroy($im); - $f = fopen('php://temp', 'rb+'); - if (! $f) { - $this->Error('Unable to create memory stream'); - } - fwrite($f, $data); - rewind($f); - $info = $this->_parsepngstream($f, $file); - fclose($f); - - return $info; - } - - protected function _out($s) - { - // Add a line to the current page - if ($this->state == 2) { - $this->pages[$this->page] .= $s."\n"; - } elseif ($this->state == 0) { - $this->Error('No page has been added yet'); - } elseif ($this->state == 1) { - $this->Error('Invalid call'); - } elseif ($this->state == 3) { - $this->Error('The document is closed'); - } - } - - protected function _put($s) - { - // Add a line to the document - $this->buffer .= $s."\n"; - } - - protected function _getoffset() - { - return strlen($this->buffer); - } - - protected function _newobj($n = null) - { - // Begin a new object - if ($n === null) { - $n = ++$this->n; - } - $this->offsets[$n] = $this->_getoffset(); - $this->_put($n.' 0 obj'); - } - - protected function _putstream($data) - { - $this->_put('stream'); - $this->_put($data); - $this->_put('endstream'); - } - - protected function _putstreamobject($data) - { - if ($this->compress) { - $entries = '/Filter /FlateDecode '; - $data = gzcompress($data); - } else { - $entries = ''; - } - $entries .= '/Length '.strlen($data); - $this->_newobj(); - $this->_put('<<'.$entries.'>>'); - $this->_putstream($data); - $this->_put('endobj'); - } - - protected function _putlinks($n) - { - foreach ($this->PageLinks[$n] as $pl) { - $this->_newobj(); - $rect = sprintf('%.2F %.2F %.2F %.2F', $pl[0], $pl[1], $pl[0] + $pl[2], $pl[1] - $pl[3]); - $s = '<_textstring($pl[4]).'>>>>'; - } else { - $l = $this->links[$pl[4]]; - if (isset($this->PageInfo[$l[0]]['size'])) { - $h = $this->PageInfo[$l[0]]['size'][1]; - } else { - $h = ($this->DefOrientation == 'P') ? $this->DefPageSize[1] * $this->k : $this->DefPageSize[0] * $this->k; - } - $s .= sprintf('/Dest [%d 0 R /XYZ 0 %.2F null]>>', $this->PageInfo[$l[0]]['n'], $h - $l[1] * $this->k); - } - $this->_put($s); - $this->_put('endobj'); - } - } - - protected function _putpage($n) - { - $this->_newobj(); - $this->_put('<_put('/Parent 1 0 R'); - if (isset($this->PageInfo[$n]['size'])) { - $this->_put(sprintf('/MediaBox [0 0 %.2F %.2F]', $this->PageInfo[$n]['size'][0], $this->PageInfo[$n]['size'][1])); - } - if (isset($this->PageInfo[$n]['rotation'])) { - $this->_put('/Rotate '.$this->PageInfo[$n]['rotation']); - } - $this->_put('/Resources 2 0 R'); - if (! empty($this->PageLinks[$n])) { - $s = '/Annots ['; - foreach ($this->PageLinks[$n] as $pl) { - $s .= $pl[5].' 0 R '; - } - $s .= ']'; - $this->_put($s); - } - if ($this->WithAlpha) { - $this->_put('/Group <>'); - } - $this->_put('/Contents '.($this->n + 1).' 0 R>>'); - $this->_put('endobj'); - // Page content - if (! empty($this->AliasNbPages)) { - $this->pages[$n] = str_replace($this->AliasNbPages, $this->page, $this->pages[$n]); - } - $this->_putstreamobject($this->pages[$n]); - // Link annotations - $this->_putlinks($n); - } - - protected function _putpages() - { - $nb = $this->page; - $n = $this->n; - for ($i = 1; $i <= $nb; $i++) { - $this->PageInfo[$i]['n'] = ++$n; - $n++; - foreach ($this->PageLinks[$i] as &$pl) { - $pl[5] = ++$n; - } - unset($pl); - } - for ($i = 1; $i <= $nb; $i++) { - $this->_putpage($i); - } - // Pages root - $this->_newobj(1); - $this->_put('<PageInfo[$i]['n'].' 0 R '; - } - $kids .= ']'; - $this->_put($kids); - $this->_put('/Count '.$nb); - if ($this->DefOrientation == 'P') { - $w = $this->DefPageSize[0]; - $h = $this->DefPageSize[1]; - } else { - $w = $this->DefPageSize[1]; - $h = $this->DefPageSize[0]; - } - $this->_put(sprintf('/MediaBox [0 0 %.2F %.2F]', $w * $this->k, $h * $this->k)); - $this->_put('>>'); - $this->_put('endobj'); - } - - protected function _putfonts() - { - foreach ($this->FontFiles as $file => $info) { - // Font file embedding - $this->_newobj(); - $this->FontFiles[$file]['n'] = $this->n; - $font = file_get_contents($file); - if (! $font) { - $this->Error('Font file not found: '.$file); - } - $compressed = (substr($file, -2) == '.z'); - if (! $compressed && isset($info['length2'])) { - $font = substr($font, 6, $info['length1']).substr($font, 6 + $info['length1'] + 6, $info['length2']); - } - $this->_put('<_put('/Filter /FlateDecode'); - } - $this->_put('/Length1 '.$info['length1']); - if (isset($info['length2'])) { - $this->_put('/Length2 '.$info['length2'].' /Length3 0'); - } - $this->_put('>>'); - $this->_putstream($font); - $this->_put('endobj'); - } - foreach ($this->fonts as $k => $font) { - // Encoding - if (isset($font['diff'])) { - if (! isset($this->encodings[$font['enc']])) { - $this->_newobj(); - $this->_put('<>'); - $this->_put('endobj'); - $this->encodings[$font['enc']] = $this->n; - } - } - // ToUnicode CMap - if (isset($font['uv'])) { - if (isset($font['enc'])) { - $cmapkey = $font['enc']; - } else { - $cmapkey = $font['name']; - } - if (! isset($this->cmaps[$cmapkey])) { - $cmap = $this->_tounicodecmap($font['uv']); - $this->_putstreamobject($cmap); - $this->cmaps[$cmapkey] = $this->n; - } - } - // Font object - $this->fonts[$k]['n'] = $this->n + 1; - $type = $font['type']; - $name = $font['name']; - if ($font['subsetted']) { - $name = 'AAAAAA+'.$name; - } - if ($type == 'Core') { - // Core font - $this->_newobj(); - $this->_put('<_put('/BaseFont /'.$name); - $this->_put('/Subtype /Type1'); - if ($name != 'Symbol' && $name != 'ZapfDingbats') { - $this->_put('/Encoding /WinAnsiEncoding'); - } - if (isset($font['uv'])) { - $this->_put('/ToUnicode '.$this->cmaps[$cmapkey].' 0 R'); - } - $this->_put('>>'); - $this->_put('endobj'); - } elseif ($type == 'Type1' || $type == 'TrueType') { - // Additional Type1 or TrueType/OpenType font - $this->_newobj(); - $this->_put('<_put('/BaseFont /'.$name); - $this->_put('/Subtype /'.$type); - $this->_put('/FirstChar 32 /LastChar 255'); - $this->_put('/Widths '.($this->n + 1).' 0 R'); - $this->_put('/FontDescriptor '.($this->n + 2).' 0 R'); - if (isset($font['diff'])) { - $this->_put('/Encoding '.$this->encodings[$font['enc']].' 0 R'); - } else { - $this->_put('/Encoding /WinAnsiEncoding'); - } - if (isset($font['uv'])) { - $this->_put('/ToUnicode '.$this->cmaps[$cmapkey].' 0 R'); - } - $this->_put('>>'); - $this->_put('endobj'); - // Widths - $this->_newobj(); - $cw = $font['cw']; - $s = '['; - for ($i = 32; $i <= 255; $i++) { - $s .= $cw[chr($i)].' '; - } - $this->_put($s.']'); - $this->_put('endobj'); - // Descriptor - $this->_newobj(); - $s = '< $v) { - $s .= ' /'.$k.' '.$v; - } - if (! empty($font['file'])) { - $s .= ' /FontFile'.($type == 'Type1' ? '' : '2').' '.$this->FontFiles[$font['file']]['n'].' 0 R'; - } - $this->_put($s.'>>'); - $this->_put('endobj'); - } else { - // Allow for additional types - $mtd = '_put'.strtolower($type); - if (! method_exists($this, $mtd)) { - $this->Error('Unsupported font type: '.$type); - } - $this->$mtd($font); - } - } - } - - protected function _tounicodecmap($uv) - { - $ranges = ''; - $nbr = 0; - $chars = ''; - $nbc = 0; - foreach ($uv as $c => $v) { - if (is_array($v)) { - $ranges .= sprintf("<%02X> <%02X> <%04X>\n", $c, $c + $v[1] - 1, $v[0]); - $nbr++; - } else { - $chars .= sprintf("<%02X> <%04X>\n", $c, $v); - $nbc++; - } - } - $s = "/CIDInit /ProcSet findresource begin\n"; - $s .= "12 dict begin\n"; - $s .= "begincmap\n"; - $s .= "/CIDSystemInfo\n"; - $s .= "< 0) { - $s .= "$nbr beginbfrange\n"; - $s .= $ranges; - $s .= "endbfrange\n"; - } - if ($nbc > 0) { - $s .= "$nbc beginbfchar\n"; - $s .= $chars; - $s .= "endbfchar\n"; - } - $s .= "endcmap\n"; - $s .= "CMapName currentdict /CMap defineresource pop\n"; - $s .= "end\n"; - $s .= 'end'; - - return $s; - } - - protected function _putimages() - { - foreach (array_keys($this->images) as $file) { - $this->_putimage($this->images[$file]); - unset($this->images[$file]['data']); - unset($this->images[$file]['smask']); - } - } - - protected function _putimage(&$info) - { - $this->_newobj(); - $info['n'] = $this->n; - $this->_put('<_put('/Subtype /Image'); - $this->_put('/Width '.$info['w']); - $this->_put('/Height '.$info['h']); - if ($info['cs'] == 'Indexed') { - $this->_put('/ColorSpace [/Indexed /DeviceRGB '.(strlen($info['pal']) / 3 - 1).' '.($this->n + 1).' 0 R]'); - } else { - $this->_put('/ColorSpace /'.$info['cs']); - if ($info['cs'] == 'DeviceCMYK') { - $this->_put('/Decode [1 0 1 0 1 0 1 0]'); - } - } - $this->_put('/BitsPerComponent '.$info['bpc']); - if (isset($info['f'])) { - $this->_put('/Filter /'.$info['f']); - } - if (isset($info['dp'])) { - $this->_put('/DecodeParms <<'.$info['dp'].'>>'); - } - if (isset($info['trns']) && is_array($info['trns'])) { - $trns = ''; - for ($i = 0; $i < count($info['trns']); $i++) { - $trns .= $info['trns'][$i].' '.$info['trns'][$i].' '; - } - $this->_put('/Mask ['.$trns.']'); - } - if (isset($info['smask'])) { - $this->_put('/SMask '.($this->n + 1).' 0 R'); - } - $this->_put('/Length '.strlen($info['data']).'>>'); - $this->_putstream($info['data']); - $this->_put('endobj'); - // Soft mask - if (isset($info['smask'])) { - $dp = '/Predictor 15 /Colors 1 /BitsPerComponent 8 /Columns '.$info['w']; - $smask = ['w' => $info['w'], 'h' => $info['h'], 'cs' => 'DeviceGray', 'bpc' => 8, 'f' => $info['f'], 'dp' => $dp, 'data' => $info['smask']]; - $this->_putimage($smask); - } - // Palette - if ($info['cs'] == 'Indexed') { - $this->_putstreamobject($info['pal']); - } - } - - protected function _putxobjectdict() - { - foreach ($this->images as $image) { - $this->_put('/I'.$image['i'].' '.$image['n'].' 0 R'); - } - } - - protected function _putresourcedict() - { - $this->_put('/ProcSet [/PDF /Text /ImageB /ImageC /ImageI]'); - $this->_put('/Font <<'); - foreach ($this->fonts as $font) { - $this->_put('/F'.$font['i'].' '.$font['n'].' 0 R'); - } - $this->_put('>>'); - $this->_put('/XObject <<'); - $this->_putxobjectdict(); - $this->_put('>>'); - } - - protected function _putresources() - { - $this->_putfonts(); - $this->_putimages(); - // Resource dictionary - $this->_newobj(2); - $this->_put('<<'); - $this->_putresourcedict(); - $this->_put('>>'); - $this->_put('endobj'); - } - - protected function _putinfo() - { - $date = @date('YmdHisO', $this->CreationDate); - $this->metadata['CreationDate'] = 'D:'.substr($date, 0, -2)."'".substr($date, -2)."'"; - foreach ($this->metadata as $key => $value) { - $this->_put('/'.$key.' '.$this->_textstring($value)); - } - } - - protected function _putcatalog() - { - $n = $this->PageInfo[1]['n']; - $this->_put('/Type /Catalog'); - $this->_put('/Pages 1 0 R'); - if ($this->ZoomMode == 'fullpage') { - $this->_put('/OpenAction ['.$n.' 0 R /Fit]'); - } elseif ($this->ZoomMode == 'fullwidth') { - $this->_put('/OpenAction ['.$n.' 0 R /FitH null]'); - } elseif ($this->ZoomMode == 'real') { - $this->_put('/OpenAction ['.$n.' 0 R /XYZ null null 1]'); - } elseif (! is_string($this->ZoomMode)) { - $this->_put('/OpenAction ['.$n.' 0 R /XYZ null null '.sprintf('%.2F', $this->ZoomMode / 100).']'); - } - if ($this->LayoutMode == 'single') { - $this->_put('/PageLayout /SinglePage'); - } elseif ($this->LayoutMode == 'continuous') { - $this->_put('/PageLayout /OneColumn'); - } elseif ($this->LayoutMode == 'two') { - $this->_put('/PageLayout /TwoColumnLeft'); - } - } - - protected function _putheader() - { - $this->_put('%PDF-'.$this->PDFVersion); - } - - protected function _puttrailer() - { - $this->_put('/Size '.($this->n + 1)); - $this->_put('/Root '.$this->n.' 0 R'); - $this->_put('/Info '.($this->n - 1).' 0 R'); - } - - protected function _enddoc() - { - $this->CreationDate = time(); - $this->_putheader(); - $this->_putpages(); - $this->_putresources(); - // Info - $this->_newobj(); - $this->_put('<<'); - $this->_putinfo(); - $this->_put('>>'); - $this->_put('endobj'); - // Catalog - $this->_newobj(); - $this->_put('<<'); - $this->_putcatalog(); - $this->_put('>>'); - $this->_put('endobj'); - // Cross-ref - $offset = $this->_getoffset(); - $this->_put('xref'); - $this->_put('0 '.($this->n + 1)); - $this->_put('0000000000 65535 f '); - for ($i = 1; $i <= $this->n; $i++) { - $this->_put(sprintf('%010d 00000 n ', $this->offsets[$i])); - } - // Trailer - $this->_put('trailer'); - $this->_put('<<'); - $this->_puttrailer(); - $this->_put('>>'); - $this->_put('startxref'); - $this->_put($offset); - $this->_put('%%EOF'); - $this->state = 3; - } -} + const VERSION = '1.86'; + protected $page; // current page number + protected $n; // current object number + protected $offsets; // array of object offsets + protected $buffer; // buffer holding in-memory PDF + protected $pages; // array containing pages + protected $state; // current document state + protected $compress; // compression flag + protected $iconv; // whether iconv is available + protected $k; // scale factor (number of points in user unit) + protected $DefOrientation; // default orientation + protected $CurOrientation; // current orientation + protected $StdPageSizes; // standard page sizes + protected $DefPageSize; // default page size + protected $CurPageSize; // current page size + protected $CurRotation; // current page rotation + protected $PageInfo; // page-related data + protected $wPt, $hPt; // dimensions of current page in points + protected $w, $h; // dimensions of current page in user unit + protected $lMargin; // left margin + protected $tMargin; // top margin + protected $rMargin; // right margin + protected $bMargin; // page break margin + protected $cMargin; // cell margin + protected $x, $y; // current position in user unit + protected $lasth; // height of last printed cell + protected $LineWidth; // line width in user unit + protected $fontpath; // directory containing fonts + protected $CoreFonts; // array of core font names + protected $fonts; // array of used fonts + protected $FontFiles; // array of font files + protected $encodings; // array of encodings + protected $cmaps; // array of ToUnicode CMaps + protected $FontFamily; // current font family + protected $FontStyle; // current font style + protected $underline; // underlining flag + protected $CurrentFont; // current font info + protected $FontSizePt; // current font size in points + protected $FontSize; // current font size in user unit + protected $DrawColor; // commands for drawing color + protected $FillColor; // commands for filling color + protected $TextColor; // commands for text color + protected $ColorFlag; // indicates whether fill and text colors are different + protected $WithAlpha; // indicates whether alpha channel is used + protected $ws; // word spacing + protected $images; // array of used images + protected $PageLinks; // array of links in pages + protected $links; // array of internal links + protected $AutoPageBreak; // automatic page breaking + protected $PageBreakTrigger; // threshold used to trigger page breaks + protected $InHeader; // flag set when processing header + protected $InFooter; // flag set when processing footer + protected $AliasNbPages; // alias for total number of pages + protected $ZoomMode; // zoom display mode + protected $LayoutMode; // layout display mode + protected $metadata; // document properties + protected $CreationDate; // document creation date + protected $PDFVersion; // PDF version number + + /******************************************************************************* + * Public methods * + *******************************************************************************/ + + function __construct($orientation = 'P', $unit = 'mm', $size = 'A4') + { + // FIXED: Set font path directly + $this->fontpath = __DIR__ . '/font/'; + + // Initialization of properties + $this->state = 0; + $this->page = 0; + $this->n = 2; + $this->buffer = ''; + $this->pages = array(); + $this->PageInfo = array(); + $this->fonts = array(); + $this->FontFiles = array(); + $this->encodings = array(); + $this->cmaps = array(); + $this->images = array(); + $this->links = array(); + $this->InHeader = false; + $this->InFooter = false; + $this->lasth = 0; + $this->FontFamily = ''; + $this->FontStyle = ''; + $this->FontSizePt = 12; + $this->underline = false; + $this->DrawColor = '0 G'; + $this->FillColor = '0 g'; + $this->TextColor = '0 g'; + $this->ColorFlag = false; + $this->WithAlpha = false; + $this->ws = 0; + $this->iconv = function_exists('iconv'); + + // Core fonts + $this->CoreFonts = array('courier', 'helvetica', 'times', 'symbol', 'zapfdingbats'); + + // Scale factor + if ($unit == 'pt') + $this->k = 1; + elseif ($unit == 'mm') + $this->k = 72 / 25.4; + elseif ($unit == 'cm') + $this->k = 72 / 2.54; + elseif ($unit == 'in') + $this->k = 72; + else + $this->Error('Incorrect unit: ' . $unit); + + // Page sizes + $this->StdPageSizes = array( + 'a3' => array(841.89, 1190.55), + 'a4' => array(595.28, 841.89), + 'a5' => array(420.94, 595.28), + 'letter' => array(612, 792), + 'legal' => array(612, 1008) + ); + $size = $this->_getpagesize($size); + $this->DefPageSize = $size; + $this->CurPageSize = $size; + + // Page orientation + $orientation = strtolower($orientation); + if ($orientation == 'p' || $orientation == 'portrait') { + $this->DefOrientation = 'P'; + $this->w = $size[0]; + $this->h = $size[1]; + } elseif ($orientation == 'l' || $orientation == 'landscape') { + $this->DefOrientation = 'L'; + $this->w = $size[1]; + $this->h = $size[0]; + } else { + $this->Error('Incorrect orientation: ' . $orientation); + } + $this->CurOrientation = $this->DefOrientation; + $this->wPt = $this->w * $this->k; + $this->hPt = $this->h * $this->k; + + // Page rotation + $this->CurRotation = 0; + + // Margins + $margin = 28.35 / $this->k; + $this->SetMargins($margin, $margin); + $this->cMargin = $margin / 10; + $this->LineWidth = .567 / $this->k; + + // Page break + $this->SetAutoPageBreak(true, 2 * $margin); + + // Display and compression + $this->SetDisplayMode('default'); + $this->SetCompression(true); + + // Metadata + $this->metadata = array('Producer' => 'FPDF ' . self::VERSION); + $this->PDFVersion = '1.3'; + } + + + function SetMargins($left, $top, $right = null) + { + // Set left, top and right margins + $this->lMargin = $left; + $this->tMargin = $top; + if ($right === null) + $right = $left; + $this->rMargin = $right; + } + + function SetLeftMargin($margin) + { + // Set left margin + $this->lMargin = $margin; + if ($this->page > 0 && $this->x < $margin) + $this->x = $margin; + } + + function SetTopMargin($margin) + { + // Set top margin + $this->tMargin = $margin; + } + + function SetRightMargin($margin) + { + // Set right margin + $this->rMargin = $margin; + } + + function SetAutoPageBreak($auto, $margin = 0) + { + // Set auto page break mode and triggering margin + $this->AutoPageBreak = $auto; + $this->bMargin = $margin; + $this->PageBreakTrigger = $this->h - $margin; + } + + function SetDisplayMode($zoom, $layout = 'default') + { + // Set display mode in viewer + if ($zoom == 'fullpage' || $zoom == 'fullwidth' || $zoom == 'real' || $zoom == 'default' || !is_string($zoom)) + $this->ZoomMode = $zoom; + else + $this->Error('Incorrect zoom display mode: ' . $zoom); + if ($layout == 'single' || $layout == 'continuous' || $layout == 'two' || $layout == 'default') + $this->LayoutMode = $layout; + else + $this->Error('Incorrect layout display mode: ' . $layout); + } + + function SetCompression($compress) + { + // Set page compression + if (function_exists('gzcompress')) + $this->compress = $compress; + else + $this->compress = false; + } + + function SetTitle($title, $isUTF8 = false) + { + // Title of document + $this->metadata['Title'] = $isUTF8 ? $title : $this->_UTF8encode($title); + } + + function SetAuthor($author, $isUTF8 = false) + { + // Author of document + $this->metadata['Author'] = $isUTF8 ? $author : $this->_UTF8encode($author); + } + + function SetSubject($subject, $isUTF8 = false) + { + // Subject of document + $this->metadata['Subject'] = $isUTF8 ? $subject : $this->_UTF8encode($subject); + } + + function SetKeywords($keywords, $isUTF8 = false) + { + // Keywords of document + $this->metadata['Keywords'] = $isUTF8 ? $keywords : $this->_UTF8encode($keywords); + } + + function SetCreator($creator, $isUTF8 = false) + { + // Creator of document + $this->metadata['Creator'] = $isUTF8 ? $creator : $this->_UTF8encode($creator); + } + + function AliasNbPages($alias = '{nb}') + { + // Define an alias for total number of pages + $this->AliasNbPages = $alias; + } + + function Error($msg) + { + // Fatal error + throw new Exception('FPDF error: ' . $msg); + } + + function Close() + { + // Terminate document + if ($this->state == 3) + return; + if ($this->page == 0) + $this->AddPage(); + // Page footer + $this->InFooter = true; + $this->Footer(); + $this->InFooter = false; + // Close page + $this->_endpage(); + // Close document + $this->_enddoc(); + } + + function AddPage($orientation = '', $size = '', $rotation = 0) + { + // Start a new page + if ($this->state == 3) + $this->Error('The document is closed'); + $family = $this->FontFamily; + $style = $this->FontStyle . ($this->underline ? 'U' : ''); + $fontsize = $this->FontSizePt; + $lw = $this->LineWidth; + $dc = $this->DrawColor; + $fc = $this->FillColor; + $tc = $this->TextColor; + $cf = $this->ColorFlag; + if ($this->page > 0) { + // Page footer + $this->InFooter = true; + $this->Footer(); + $this->InFooter = false; + // Close page + $this->_endpage(); + } + // Start new page + $this->_beginpage($orientation, $size, $rotation); + // Set line cap style to square + $this->_out('2 J'); + // Set line width + $this->LineWidth = $lw; + $this->_out(sprintf('%.2F w', $lw * $this->k)); + // Set font + if ($family) + $this->SetFont($family, $style, $fontsize); + // Set colors + $this->DrawColor = $dc; + if ($dc != '0 G') + $this->_out($dc); + $this->FillColor = $fc; + if ($fc != '0 g') + $this->_out($fc); + $this->TextColor = $tc; + $this->ColorFlag = $cf; + // Page header + $this->InHeader = true; + $this->Header(); + $this->InHeader = false; + // Restore line width + if ($this->LineWidth != $lw) { + $this->LineWidth = $lw; + $this->_out(sprintf('%.2F w', $lw * $this->k)); + } + // Restore font + if ($family) + $this->SetFont($family, $style, $fontsize); + // Restore colors + if ($this->DrawColor != $dc) { + $this->DrawColor = $dc; + $this->_out($dc); + } + if ($this->FillColor != $fc) { + $this->FillColor = $fc; + $this->_out($fc); + } + $this->TextColor = $tc; + $this->ColorFlag = $cf; + } + + function Header() + { + // To be implemented in your own inherited class + } + + function Footer() + { + // To be implemented in your own inherited class + } + + function PageNo() + { + // Get current page number + return $this->page; + } + + function SetDrawColor($r, $g = null, $b = null) + { + // Set color for all stroking operations + if (($r == 0 && $g == 0 && $b == 0) || $g === null) + $this->DrawColor = sprintf('%.3F G', $r / 255); + else + $this->DrawColor = sprintf('%.3F %.3F %.3F RG', $r / 255, $g / 255, $b / 255); + if ($this->page > 0) + $this->_out($this->DrawColor); + } + + function SetFillColor($r, $g = null, $b = null) + { + // Set color for all filling operations + if (($r == 0 && $g == 0 && $b == 0) || $g === null) + $this->FillColor = sprintf('%.3F g', $r / 255); + else + $this->FillColor = sprintf('%.3F %.3F %.3F rg', $r / 255, $g / 255, $b / 255); + $this->ColorFlag = ($this->FillColor != $this->TextColor); + if ($this->page > 0) + $this->_out($this->FillColor); + } + + function SetTextColor($r, $g = null, $b = null) + { + // Set color for text + if (($r == 0 && $g == 0 && $b == 0) || $g === null) + $this->TextColor = sprintf('%.3F g', $r / 255); + else + $this->TextColor = sprintf('%.3F %.3F %.3F rg', $r / 255, $g / 255, $b / 255); + $this->ColorFlag = ($this->FillColor != $this->TextColor); + } + + function GetStringWidth($s) + { + // Get width of a string in the current font + $cw = $this->CurrentFont['cw']; + $w = 0; + $s = (string)$s; + $l = strlen($s); + for ($i = 0; $i < $l; $i++) + $w += $cw[$s[$i]]; + return $w * $this->FontSize / 1000; + } + + function SetLineWidth($width) + { + // Set line width + $this->LineWidth = $width; + if ($this->page > 0) + $this->_out(sprintf('%.2F w', $width * $this->k)); + } + + function Line($x1, $y1, $x2, $y2) + { + // Draw a line + $this->_out(sprintf('%.2F %.2F m %.2F %.2F l S', $x1 * $this->k, ($this->h - $y1) * $this->k, $x2 * $this->k, ($this->h - $y2) * $this->k)); + } + + function Rect($x, $y, $w, $h, $style = '') + { + // Draw a rectangle + if ($style == 'F') + $op = 'f'; + elseif ($style == 'FD' || $style == 'DF') + $op = 'B'; + else + $op = 'S'; + $this->_out(sprintf('%.2F %.2F %.2F %.2F re %s', $x * $this->k, ($this->h - $y) * $this->k, $w * $this->k, -$h * $this->k, $op)); + } + + function AddFont($family, $style = '', $file = '', $dir = '') + { + // Add a TrueType, OpenType or Type1 font + $family = strtolower($family); + if ($file == '') + $file = str_replace(' ', '', $family) . strtolower($style) . '.php'; + $style = strtoupper($style); + if ($style == 'IB') + $style = 'BI'; + $fontkey = $family . $style; + if (isset($this->fonts[$fontkey])) + return; + if (strpos($file, '/') !== false || strpos($file, "\\") !== false) + $this->Error('Incorrect font definition file name: ' . $file); + if ($dir == '') + $dir = $this->fontpath; + if (substr($dir, -1) != '/' && substr($dir, -1) != '\\') + $dir .= '/'; + $info = $this->_loadfont($dir . $file); + $info['i'] = count($this->fonts) + 1; + if (!empty($info['file'])) { + // Embedded font + $info['file'] = $dir . $info['file']; + if ($info['type'] == 'TrueType') + $this->FontFiles[$info['file']] = array('length1' => $info['originalsize']); + else + $this->FontFiles[$info['file']] = array('length1' => $info['size1'], 'length2' => $info['size2']); + } + $this->fonts[$fontkey] = $info; + } + + function SetFont($family, $style = '', $size = 0) + { + // Select a font; size given in points + if ($family == '') + $family = $this->FontFamily; + else + $family = strtolower($family); + $style = strtoupper($style); + if (strpos($style, 'U') !== false) { + $this->underline = true; + $style = str_replace('U', '', $style); + } else + $this->underline = false; + if ($style == 'IB') + $style = 'BI'; + if ($size == 0) + $size = $this->FontSizePt; + // Test if font is already selected + if ($this->FontFamily == $family && $this->FontStyle == $style && $this->FontSizePt == $size) + return; + // Test if font is already loaded + $fontkey = $family . $style; + if (!isset($this->fonts[$fontkey])) { + // Test if one of the core fonts + if ($family == 'arial') + $family = 'helvetica'; + if (in_array($family, $this->CoreFonts)) { + if ($family == 'symbol' || $family == 'zapfdingbats') + $style = ''; + $fontkey = $family . $style; + if (!isset($this->fonts[$fontkey])) + $this->AddFont($family, $style); + } else + $this->Error('Undefined font: ' . $family . ' ' . $style); + } + // Select it + $this->FontFamily = $family; + $this->FontStyle = $style; + $this->FontSizePt = $size; + $this->FontSize = $size / $this->k; + $this->CurrentFont = $this->fonts[$fontkey]; + if ($this->page > 0) + $this->_out(sprintf('BT /F%d %.2F Tf ET', $this->CurrentFont['i'], $this->FontSizePt)); + } + + function SetFontSize($size) + { + // Set font size in points + if ($this->FontSizePt == $size) + return; + $this->FontSizePt = $size; + $this->FontSize = $size / $this->k; + if ($this->page > 0 && isset($this->CurrentFont)) + $this->_out(sprintf('BT /F%d %.2F Tf ET', $this->CurrentFont['i'], $this->FontSizePt)); + } + + function AddLink() + { + // Create a new internal link + $n = count($this->links) + 1; + $this->links[$n] = array(0, 0); + return $n; + } + + function SetLink($link, $y = 0, $page = -1) + { + // Set destination of internal link + if ($y == -1) + $y = $this->y; + if ($page == -1) + $page = $this->page; + $this->links[$link] = array($page, $y); + } + + function Link($x, $y, $w, $h, $link) + { + // Put a link on the page + $this->PageLinks[$this->page][] = array($x * $this->k, $this->hPt - $y * $this->k, $w * $this->k, $h * $this->k, $link); + } + + function Text($x, $y, $txt) + { + // Output a string + if (!isset($this->CurrentFont)) + $this->Error('No font has been set'); + $txt = (string)$txt; + $s = sprintf('BT %.2F %.2F Td (%s) Tj ET', $x * $this->k, ($this->h - $y) * $this->k, $this->_escape($txt)); + if ($this->underline && $txt !== '') + $s .= ' ' . $this->_dounderline($x, $y, $txt); + if ($this->ColorFlag) + $s = 'q ' . $this->TextColor . ' ' . $s . ' Q'; + $this->_out($s); + } + + function AcceptPageBreak() + { + // Accept automatic page break or not + return $this->AutoPageBreak; + } + + function Cell($w, $h = 0, $txt = '', $border = 0, $ln = 0, $align = '', $fill = false, $link = '') + { + // Output a cell + $k = $this->k; + if ($this->y + $h > $this->PageBreakTrigger && !$this->InHeader && !$this->InFooter && $this->AcceptPageBreak()) { + // Automatic page break + $x = $this->x; + $ws = $this->ws; + if ($ws > 0) { + $this->ws = 0; + $this->_out('0 Tw'); + } + $this->AddPage($this->CurOrientation, $this->CurPageSize, $this->CurRotation); + $this->x = $x; + if ($ws > 0) { + $this->ws = $ws; + $this->_out(sprintf('%.3F Tw', $ws * $k)); + } + } + if ($w == 0) + $w = $this->w - $this->rMargin - $this->x; + $s = ''; + if ($fill || $border == 1) { + if ($fill) + $op = ($border == 1) ? 'B' : 'f'; + else + $op = 'S'; + $s = sprintf('%.2F %.2F %.2F %.2F re %s ', $this->x * $k, ($this->h - $this->y) * $k, $w * $k, -$h * $k, $op); + } + if (is_string($border)) { + $x = $this->x; + $y = $this->y; + if (strpos($border, 'L') !== false) + $s .= sprintf('%.2F %.2F m %.2F %.2F l S ', $x * $k, ($this->h - $y) * $k, $x * $k, ($this->h - ($y + $h)) * $k); + if (strpos($border, 'T') !== false) + $s .= sprintf('%.2F %.2F m %.2F %.2F l S ', $x * $k, ($this->h - $y) * $k, ($x + $w) * $k, ($this->h - $y) * $k); + if (strpos($border, 'R') !== false) + $s .= sprintf('%.2F %.2F m %.2F %.2F l S ', ($x + $w) * $k, ($this->h - $y) * $k, ($x + $w) * $k, ($this->h - ($y + $h)) * $k); + if (strpos($border, 'B') !== false) + $s .= sprintf('%.2F %.2F m %.2F %.2F l S ', $x * $k, ($this->h - ($y + $h)) * $k, ($x + $w) * $k, ($this->h - ($y + $h)) * $k); + } + $txt = (string)$txt; + if ($txt !== '') { + if (!isset($this->CurrentFont)) + $this->Error('No font has been set'); + if ($align == 'R') + $dx = $w - $this->cMargin - $this->GetStringWidth($txt); + elseif ($align == 'C') + $dx = ($w - $this->GetStringWidth($txt)) / 2; + else + $dx = $this->cMargin; + if ($this->ColorFlag) + $s .= 'q ' . $this->TextColor . ' '; + $s .= sprintf('BT %.2F %.2F Td (%s) Tj ET', ($this->x + $dx) * $k, ($this->h - ($this->y + .5 * $h + .3 * $this->FontSize)) * $k, $this->_escape($txt)); + if ($this->underline) + $s .= ' ' . $this->_dounderline($this->x + $dx, $this->y + .5 * $h + .3 * $this->FontSize, $txt); + if ($this->ColorFlag) + $s .= ' Q'; + if ($link) + $this->Link($this->x + $dx, $this->y + .5 * $h - .5 * $this->FontSize, $this->GetStringWidth($txt), $this->FontSize, $link); + } + if ($s) + $this->_out($s); + $this->lasth = $h; + if ($ln > 0) { + // Go to next line + $this->y += $h; + if ($ln == 1) + $this->x = $this->lMargin; + } else + $this->x += $w; + } + + function MultiCell($w, $h, $txt, $border = 0, $align = 'J', $fill = false) + { + // Output text with automatic or explicit line breaks + if (!isset($this->CurrentFont)) + $this->Error('No font has been set'); + $cw = $this->CurrentFont['cw']; + if ($w == 0) + $w = $this->w - $this->rMargin - $this->x; + $wmax = ($w - 2 * $this->cMargin) * 1000 / $this->FontSize; + $s = str_replace("\r", '', (string)$txt); + $nb = strlen($s); + if ($nb > 0 && $s[$nb - 1] == "\n") + $nb--; + $b = 0; + if ($border) { + if ($border == 1) { + $border = 'LTRB'; + $b = 'LRT'; + $b2 = 'LR'; + } else { + $b2 = ''; + if (strpos($border, 'L') !== false) + $b2 .= 'L'; + if (strpos($border, 'R') !== false) + $b2 .= 'R'; + $b = (strpos($border, 'T') !== false) ? $b2 . 'T' : $b2; + } + } + $sep = -1; + $i = 0; + $j = 0; + $l = 0; + $ns = 0; + $nl = 1; + while ($i < $nb) { + // Get next character + $c = $s[$i]; + if ($c == "\n") { + // Explicit line break + if ($this->ws > 0) { + $this->ws = 0; + $this->_out('0 Tw'); + } + $this->Cell($w, $h, substr($s, $j, $i - $j), $b, 2, $align, $fill); + $i++; + $sep = -1; + $j = $i; + $l = 0; + $ns = 0; + $nl++; + if ($border && $nl == 2) + $b = $b2; + continue; + } + if ($c == ' ') { + $sep = $i; + $ls = $l; + $ns++; + } + $l += $cw[$c]; + if ($l > $wmax) { + // Automatic line break + if ($sep == -1) { + if ($i == $j) + $i++; + if ($this->ws > 0) { + $this->ws = 0; + $this->_out('0 Tw'); + } + $this->Cell($w, $h, substr($s, $j, $i - $j), $b, 2, $align, $fill); + } else { + if ($align == 'J') { + $this->ws = ($ns > 1) ? ($wmax - $ls) / 1000 * $this->FontSize / ($ns - 1) : 0; + $this->_out(sprintf('%.3F Tw', $this->ws * $this->k)); + } + $this->Cell($w, $h, substr($s, $j, $sep - $j), $b, 2, $align, $fill); + $i = $sep + 1; + } + $sep = -1; + $j = $i; + $l = 0; + $ns = 0; + $nl++; + if ($border && $nl == 2) + $b = $b2; + } else + $i++; + } + // Last chunk + if ($this->ws > 0) { + $this->ws = 0; + $this->_out('0 Tw'); + } + if ($border && strpos($border, 'B') !== false) + $b .= 'B'; + $this->Cell($w, $h, substr($s, $j, $i - $j), $b, 2, $align, $fill); + $this->x = $this->lMargin; + } + + function Write($h, $txt, $link = '') + { + // Output text in flowing mode + if (!isset($this->CurrentFont)) + $this->Error('No font has been set'); + $cw = $this->CurrentFont['cw']; + $w = $this->w - $this->rMargin - $this->x; + $wmax = ($w - 2 * $this->cMargin) * 1000 / $this->FontSize; + $s = str_replace("\r", '', (string)$txt); + $nb = strlen($s); + $sep = -1; + $i = 0; + $j = 0; + $l = 0; + $nl = 1; + while ($i < $nb) { + // Get next character + $c = $s[$i]; + if ($c == "\n") { + // Explicit line break + $this->Cell($w, $h, substr($s, $j, $i - $j), 0, 2, '', false, $link); + $i++; + $sep = -1; + $j = $i; + $l = 0; + if ($nl == 1) { + $this->x = $this->lMargin; + $w = $this->w - $this->rMargin - $this->x; + $wmax = ($w - 2 * $this->cMargin) * 1000 / $this->FontSize; + } + $nl++; + continue; + } + if ($c == ' ') + $sep = $i; + $l += $cw[$c]; + if ($l > $wmax) { + // Automatic line break + if ($sep == -1) { + if ($this->x > $this->lMargin) { + // Move to next line + $this->x = $this->lMargin; + $this->y += $h; + $w = $this->w - $this->rMargin - $this->x; + $wmax = ($w - 2 * $this->cMargin) * 1000 / $this->FontSize; + $i++; + $nl++; + continue; + } + if ($i == $j) + $i++; + $this->Cell($w, $h, substr($s, $j, $i - $j), 0, 2, '', false, $link); + } else { + $this->Cell($w, $h, substr($s, $j, $sep - $j), 0, 2, '', false, $link); + $i = $sep + 1; + } + $sep = -1; + $j = $i; + $l = 0; + if ($nl == 1) { + $this->x = $this->lMargin; + $w = $this->w - $this->rMargin - $this->x; + $wmax = ($w - 2 * $this->cMargin) * 1000 / $this->FontSize; + } + $nl++; + } else + $i++; + } + // Last chunk + if ($i != $j) + $this->Cell($l / 1000 * $this->FontSize, $h, substr($s, $j), 0, 0, '', false, $link); + } + + function Ln($h = null) + { + // Line feed; default value is the last cell height + $this->x = $this->lMargin; + if ($h === null) + $this->y += $this->lasth; + else + $this->y += $h; + } + + function Image($file, $x = null, $y = null, $w = 0, $h = 0, $type = '', $link = '') + { + // Put an image on the page + if ($file == '') + $this->Error('Image file name is empty'); + if (!isset($this->images[$file])) { + // First use of this image, get info + if ($type == '') { + $pos = strrpos($file, '.'); + if (!$pos) + $this->Error('Image file has no extension and no type was specified: ' . $file); + $type = substr($file, $pos + 1); + } + $type = strtolower($type); + if ($type == 'jpeg') + $type = 'jpg'; + $mtd = '_parse' . $type; + if (!method_exists($this, $mtd)) + $this->Error('Unsupported image type: ' . $type); + $info = $this->$mtd($file); + $info['i'] = count($this->images) + 1; + $this->images[$file] = $info; + } else + $info = $this->images[$file]; + + // Automatic width and height calculation if needed + if ($w == 0 && $h == 0) { + // Put image at 96 dpi + $w = -96; + $h = -96; + } + if ($w < 0) + $w = -$info['w'] * 72 / $w / $this->k; + if ($h < 0) + $h = -$info['h'] * 72 / $h / $this->k; + if ($w == 0) + $w = $h * $info['w'] / $info['h']; + if ($h == 0) + $h = $w * $info['h'] / $info['w']; + + // Flowing mode + if ($y === null) { + if ($this->y + $h > $this->PageBreakTrigger && !$this->InHeader && !$this->InFooter && $this->AcceptPageBreak()) { + // Automatic page break + $x2 = $this->x; + $this->AddPage($this->CurOrientation, $this->CurPageSize, $this->CurRotation); + $this->x = $x2; + } + $y = $this->y; + $this->y += $h; + } + + if ($x === null) + $x = $this->x; + $this->_out(sprintf('q %.2F 0 0 %.2F %.2F %.2F cm /I%d Do Q', $w * $this->k, $h * $this->k, $x * $this->k, ($this->h - ($y + $h)) * $this->k, $info['i'])); + if ($link) + $this->Link($x, $y, $w, $h, $link); + } + + function GetPageWidth() + { + // Get current page width + return $this->w; + } + + function GetPageHeight() + { + // Get current page height + return $this->h; + } + + function GetX() + { + // Get x position + return $this->x; + } + + function SetX($x) + { + // Set x position + if ($x >= 0) + $this->x = $x; + else + $this->x = $this->w + $x; + } + + function GetY() + { + // Get y position + return $this->y; + } + + function SetY($y, $resetX = true) + { + // Set y position and optionally reset x + if ($y >= 0) + $this->y = $y; + else + $this->y = $this->h + $y; + if ($resetX) + $this->x = $this->lMargin; + } + + function SetXY($x, $y) + { + // Set x and y positions + $this->SetX($x); + $this->SetY($y, false); + } + + function Output($dest = '', $name = '', $isUTF8 = false) + { + // Output PDF to some destination + $this->Close(); + if (strlen($name) == 1 && strlen($dest) != 1) { + // Fix parameter order + $tmp = $dest; + $dest = $name; + $name = $tmp; + } + if ($dest == '') + $dest = 'I'; + if ($name == '') + $name = 'doc.pdf'; + switch (strtoupper($dest)) { + case 'I': + // Send to standard output + $this->_checkoutput(); + if (PHP_SAPI != 'cli') { + // We send to a browser + header('Content-Type: application/pdf'); + header('Content-Disposition: inline; ' . $this->_httpencode('filename', $name, $isUTF8)); + header('Cache-Control: private, max-age=0, must-revalidate'); + header('Pragma: public'); + } + echo $this->buffer; + break; + case 'D': + // Download file + $this->_checkoutput(); + header('Content-Type: application/pdf'); + header('Content-Disposition: attachment; ' . $this->_httpencode('filename', $name, $isUTF8)); + header('Cache-Control: private, max-age=0, must-revalidate'); + header('Pragma: public'); + echo $this->buffer; + break; + case 'F': + // Save to local file + if (!file_put_contents($name, $this->buffer)) + $this->Error('Unable to create output file: ' . $name); + break; + case 'S': + // Return as a string + return $this->buffer; + default: + $this->Error('Incorrect output destination: ' . $dest); + } + return ''; + } + + /******************************************************************************* + * Protected methods * + *******************************************************************************/ + + protected function _checkoutput() + { + if (PHP_SAPI != 'cli') { + if (headers_sent($file, $line)) + $this->Error("Some data has already been output, can't send PDF file (output started at $file:$line)"); + } + if (ob_get_length()) { + // The output buffer is not empty + if (preg_match('/^(\xEF\xBB\xBF)?\s*$/', ob_get_contents())) { + // It contains only a UTF-8 BOM and/or whitespace, let's clean it + ob_clean(); + } else + $this->Error("Some data has already been output, can't send PDF file"); + } + } + + protected function _getpagesize($size) + { + if (is_string($size)) { + $size = strtolower($size); + if (!isset($this->StdPageSizes[$size])) + $this->Error('Unknown page size: ' . $size); + $a = $this->StdPageSizes[$size]; + return array($a[0] / $this->k, $a[1] / $this->k); + } else { + if ($size[0] > $size[1]) + return array($size[1], $size[0]); + else + return $size; + } + } + + protected function _beginpage($orientation, $size, $rotation) + { + $this->page++; + $this->pages[$this->page] = ''; + $this->PageLinks[$this->page] = array(); + $this->state = 2; + $this->x = $this->lMargin; + $this->y = $this->tMargin; + $this->FontFamily = ''; + // Check page size and orientation + if ($orientation == '') + $orientation = $this->DefOrientation; + else + $orientation = strtoupper($orientation[0]); + if ($size == '') + $size = $this->DefPageSize; + else + $size = $this->_getpagesize($size); + if ($orientation != $this->CurOrientation || $size[0] != $this->CurPageSize[0] || $size[1] != $this->CurPageSize[1]) { + // New size or orientation + if ($orientation == 'P') { + $this->w = $size[0]; + $this->h = $size[1]; + } else { + $this->w = $size[1]; + $this->h = $size[0]; + } + $this->wPt = $this->w * $this->k; + $this->hPt = $this->h * $this->k; + $this->PageBreakTrigger = $this->h - $this->bMargin; + $this->CurOrientation = $orientation; + $this->CurPageSize = $size; + } + if ($orientation != $this->DefOrientation || $size[0] != $this->DefPageSize[0] || $size[1] != $this->DefPageSize[1]) + $this->PageInfo[$this->page]['size'] = array($this->wPt, $this->hPt); + if ($rotation != 0) { + if ($rotation % 90 != 0) + $this->Error('Incorrect rotation value: ' . $rotation); + $this->PageInfo[$this->page]['rotation'] = $rotation; + } + $this->CurRotation = $rotation; + } + + protected function _endpage() + { + $this->state = 1; + } + + protected function _loadfont($path) + { + // Load a font definition file + include($path); + if (!isset($name)) + $this->Error('Could not include font definition file: ' . $path); + if (isset($enc)) + $enc = strtolower($enc); + if (!isset($subsetted)) + $subsetted = false; + return get_defined_vars(); + } + + protected function _isascii($s) + { + // Test if string is ASCII + $nb = strlen($s); + for ($i = 0; $i < $nb; $i++) { + if (ord($s[$i]) > 127) + return false; + } + return true; + } + + protected function _httpencode($param, $value, $isUTF8) + { + // Encode HTTP header field parameter + if ($this->_isascii($value)) + return $param . '="' . $value . '"'; + if (!$isUTF8) + $value = $this->_UTF8encode($value); + return $param . "*=UTF-8''" . rawurlencode($value); + } + + protected function _UTF8encode($s) + { + // Convert ISO-8859-1 to UTF-8 + if ($this->iconv) + return iconv('ISO-8859-1', 'UTF-8', $s); + $res = ''; + $nb = strlen($s); + for ($i = 0; $i < $nb; $i++) { + $c = $s[$i]; + $v = ord($c); + if ($v >= 128) { + $res .= chr(0xC0 | ($v >> 6)); + $res .= chr(0x80 | ($v & 0x3F)); + } else + $res .= $c; + } + return $res; + } + + protected function _UTF8toUTF16($s) + { + // Convert UTF-8 to UTF-16BE with BOM + $res = "\xFE\xFF"; + if ($this->iconv) + return $res . iconv('UTF-8', 'UTF-16BE', $s); + $nb = strlen($s); + $i = 0; + while ($i < $nb) { + $c1 = ord($s[$i++]); + if ($c1 >= 224) { + // 3-byte character + $c2 = ord($s[$i++]); + $c3 = ord($s[$i++]); + $res .= chr((($c1 & 0x0F) << 4) + (($c2 & 0x3C) >> 2)); + $res .= chr((($c2 & 0x03) << 6) + ($c3 & 0x3F)); + } elseif ($c1 >= 192) { + // 2-byte character + $c2 = ord($s[$i++]); + $res .= chr(($c1 & 0x1C) >> 2); + $res .= chr((($c1 & 0x03) << 6) + ($c2 & 0x3F)); + } else { + // Single-byte character + $res .= "\0" . chr($c1); + } + } + return $res; + } + + protected function _escape($s) + { + // Escape special characters + if (strpos($s, '(') !== false || strpos($s, ')') !== false || strpos($s, '\\') !== false || strpos($s, "\r") !== false) + return str_replace(array('\\', '(', ')', "\r"), array('\\\\', '\\(', '\\)', '\\r'), $s); + else + return $s; + } + + protected function _textstring($s) + { + // Format a text string + if (!$this->_isascii($s)) + $s = $this->_UTF8toUTF16($s); + return '(' . $this->_escape($s) . ')'; + } + + protected function _dounderline($x, $y, $txt) + { + // Underline text + $up = $this->CurrentFont['up']; + $ut = $this->CurrentFont['ut']; + $w = $this->GetStringWidth($txt) + $this->ws * substr_count($txt, ' '); + return sprintf('%.2F %.2F %.2F %.2F re f', $x * $this->k, ($this->h - ($y - $up / 1000 * $this->FontSize)) * $this->k, $w * $this->k, -$ut / 1000 * $this->FontSizePt); + } + + protected function _parsejpg($file) + { + // Extract info from a JPEG file + $a = getimagesize($file); + if (!$a) + $this->Error('Missing or incorrect image file: ' . $file); + if ($a[2] != 2) + $this->Error('Not a JPEG file: ' . $file); + if (!isset($a['channels']) || $a['channels'] == 3) + $colspace = 'DeviceRGB'; + elseif ($a['channels'] == 4) + $colspace = 'DeviceCMYK'; + else + $colspace = 'DeviceGray'; + $bpc = isset($a['bits']) ? $a['bits'] : 8; + $data = file_get_contents($file); + return array('w' => $a[0], 'h' => $a[1], 'cs' => $colspace, 'bpc' => $bpc, 'f' => 'DCTDecode', 'data' => $data); + } + + protected function _parsepng($file) + { + // Extract info from a PNG file + $f = fopen($file, 'rb'); + if (!$f) + $this->Error('Can\'t open image file: ' . $file); + $info = $this->_parsepngstream($f, $file); + fclose($f); + return $info; + } + + protected function _parsepngstream($f, $file) + { + // Check signature + if ($this->_readstream($f, 8) != chr(137) . 'PNG' . chr(13) . chr(10) . chr(26) . chr(10)) + $this->Error('Not a PNG file: ' . $file); + + // Read header chunk + $this->_readstream($f, 4); + if ($this->_readstream($f, 4) != 'IHDR') + $this->Error('Incorrect PNG file: ' . $file); + $w = $this->_readint($f); + $h = $this->_readint($f); + $bpc = ord($this->_readstream($f, 1)); + if ($bpc > 8) + $this->Error('16-bit depth not supported: ' . $file); + $ct = ord($this->_readstream($f, 1)); + if ($ct == 0 || $ct == 4) + $colspace = 'DeviceGray'; + elseif ($ct == 2 || $ct == 6) + $colspace = 'DeviceRGB'; + elseif ($ct == 3) + $colspace = 'Indexed'; + else + $this->Error('Unknown color type: ' . $file); + if (ord($this->_readstream($f, 1)) != 0) + $this->Error('Unknown compression method: ' . $file); + if (ord($this->_readstream($f, 1)) != 0) + $this->Error('Unknown filter method: ' . $file); + if (ord($this->_readstream($f, 1)) != 0) + $this->Error('Interlacing not supported: ' . $file); + $this->_readstream($f, 4); + $dp = '/Predictor 15 /Colors ' . ($colspace == 'DeviceRGB' ? 3 : 1) . ' /BitsPerComponent ' . $bpc . ' /Columns ' . $w; + + // Scan chunks looking for palette, transparency and image data + $pal = ''; + $trns = ''; + $data = ''; + do { + $n = $this->_readint($f); + $type = $this->_readstream($f, 4); + if ($type == 'PLTE') { + // Read palette + $pal = $this->_readstream($f, $n); + $this->_readstream($f, 4); + } elseif ($type == 'tRNS') { + // Read transparency info + $t = $this->_readstream($f, $n); + if ($ct == 0) + $trns = array(ord(substr($t, 1, 1))); + elseif ($ct == 2) + $trns = array(ord(substr($t, 1, 1)), ord(substr($t, 3, 1)), ord(substr($t, 5, 1))); + else { + $pos = strpos($t, chr(0)); + if ($pos !== false) + $trns = array($pos); + } + $this->_readstream($f, 4); + } elseif ($type == 'IDAT') { + // Read image data block + $data .= $this->_readstream($f, $n); + $this->_readstream($f, 4); + } elseif ($type == 'IEND') + break; + else + $this->_readstream($f, $n + 4); + } while ($n); + + if ($colspace == 'Indexed' && empty($pal)) + $this->Error('Missing palette in ' . $file); + $info = array('w' => $w, 'h' => $h, 'cs' => $colspace, 'bpc' => $bpc, 'f' => 'FlateDecode', 'dp' => $dp, 'pal' => $pal, 'trns' => $trns); + if ($ct >= 4) { + // Extract alpha channel + if (!function_exists('gzuncompress')) + $this->Error('Zlib not available, can\'t handle alpha channel: ' . $file); + $data = gzuncompress($data); + $color = ''; + $alpha = ''; + if ($ct == 4) { + // Gray image + $len = 2 * $w; + for ($i = 0; $i < $h; $i++) { + $pos = (1 + $len) * $i; + $color .= $data[$pos]; + $alpha .= $data[$pos]; + $line = substr($data, $pos + 1, $len); + $color .= preg_replace('/(.)./s', '$1', $line); + $alpha .= preg_replace('/.(.)/s', '$1', $line); + } + } else { + // RGB image + $len = 4 * $w; + for ($i = 0; $i < $h; $i++) { + $pos = (1 + $len) * $i; + $color .= $data[$pos]; + $alpha .= $data[$pos]; + $line = substr($data, $pos + 1, $len); + $color .= preg_replace('/(.{3})./s', '$1', $line); + $alpha .= preg_replace('/.{3}(.)/s', '$1', $line); + } + } + unset($data); + $data = gzcompress($color); + $info['smask'] = gzcompress($alpha); + $this->WithAlpha = true; + if ($this->PDFVersion < '1.4') + $this->PDFVersion = '1.4'; + } + $info['data'] = $data; + return $info; + } + + protected function _readstream($f, $n) + { + // Read n bytes from stream + $res = ''; + while ($n > 0 && !feof($f)) { + $s = fread($f, $n); + if ($s === false) + $this->Error('Error while reading stream'); + $n -= strlen($s); + $res .= $s; + } + if ($n > 0) + $this->Error('Unexpected end of stream'); + return $res; + } + + protected function _readint($f) + { + // Read a 4-byte integer from stream + $a = unpack('Ni', $this->_readstream($f, 4)); + return $a['i']; + } + + protected function _parsegif($file) + { + // Extract info from a GIF file (via PNG conversion) + if (!function_exists('imagepng')) + $this->Error('GD extension is required for GIF support'); + if (!function_exists('imagecreatefromgif')) + $this->Error('GD has no GIF read support'); + $im = imagecreatefromgif($file); + if (!$im) + $this->Error('Missing or incorrect image file: ' . $file); + imageinterlace($im, 0); + ob_start(); + imagepng($im); + $data = ob_get_clean(); + imagedestroy($im); + $f = fopen('php://temp', 'rb+'); + if (!$f) + $this->Error('Unable to create memory stream'); + fwrite($f, $data); + rewind($f); + $info = $this->_parsepngstream($f, $file); + fclose($f); + return $info; + } + + protected function _out($s) + { + // Add a line to the current page + if ($this->state == 2) + $this->pages[$this->page] .= $s . "\n"; + elseif ($this->state == 0) + $this->Error('No page has been added yet'); + elseif ($this->state == 1) + $this->Error('Invalid call'); + elseif ($this->state == 3) + $this->Error('The document is closed'); + } + + protected function _put($s) + { + // Add a line to the document + $this->buffer .= $s . "\n"; + } + + protected function _getoffset() + { + return strlen($this->buffer); + } + + protected function _newobj($n = null) + { + // Begin a new object + if ($n === null) + $n = ++$this->n; + $this->offsets[$n] = $this->_getoffset(); + $this->_put($n . ' 0 obj'); + } + + protected function _putstream($data) + { + $this->_put('stream'); + $this->_put($data); + $this->_put('endstream'); + } + + protected function _putstreamobject($data) + { + if ($this->compress) { + $entries = '/Filter /FlateDecode '; + $data = gzcompress($data); + } else + $entries = ''; + $entries .= '/Length ' . strlen($data); + $this->_newobj(); + $this->_put('<<' . $entries . '>>'); + $this->_putstream($data); + $this->_put('endobj'); + } + + protected function _putlinks($n) + { + foreach ($this->PageLinks[$n] as $pl) { + $this->_newobj(); + $rect = sprintf('%.2F %.2F %.2F %.2F', $pl[0], $pl[1], $pl[0] + $pl[2], $pl[1] - $pl[3]); + $s = '<_textstring($pl[4]) . '>>>>'; + else { + $l = $this->links[$pl[4]]; + if (isset($this->PageInfo[$l[0]]['size'])) + $h = $this->PageInfo[$l[0]]['size'][1]; + else + $h = ($this->DefOrientation == 'P') ? $this->DefPageSize[1] * $this->k : $this->DefPageSize[0] * $this->k; + $s .= sprintf('/Dest [%d 0 R /XYZ 0 %.2F null]>>', $this->PageInfo[$l[0]]['n'], $h - $l[1] * $this->k); + } + $this->_put($s); + $this->_put('endobj'); + } + } + + protected function _putpage($n) + { + $this->_newobj(); + $this->_put('<_put('/Parent 1 0 R'); + if (isset($this->PageInfo[$n]['size'])) + $this->_put(sprintf('/MediaBox [0 0 %.2F %.2F]', $this->PageInfo[$n]['size'][0], $this->PageInfo[$n]['size'][1])); + if (isset($this->PageInfo[$n]['rotation'])) + $this->_put('/Rotate ' . $this->PageInfo[$n]['rotation']); + $this->_put('/Resources 2 0 R'); + if (!empty($this->PageLinks[$n])) { + $s = '/Annots ['; + foreach ($this->PageLinks[$n] as $pl) + $s .= $pl[5] . ' 0 R '; + $s .= ']'; + $this->_put($s); + } + if ($this->WithAlpha) + $this->_put('/Group <>'); + $this->_put('/Contents ' . ($this->n + 1) . ' 0 R>>'); + $this->_put('endobj'); + // Page content + if (!empty($this->AliasNbPages)) + $this->pages[$n] = str_replace($this->AliasNbPages, $this->page, $this->pages[$n]); + $this->_putstreamobject($this->pages[$n]); + // Link annotations + $this->_putlinks($n); + } + + protected function _putpages() + { + $nb = $this->page; + $n = $this->n; + for ($i = 1; $i <= $nb; $i++) { + $this->PageInfo[$i]['n'] = ++$n; + $n++; + foreach ($this->PageLinks[$i] as &$pl) + $pl[5] = ++$n; + unset($pl); + } + for ($i = 1; $i <= $nb; $i++) + $this->_putpage($i); + // Pages root + $this->_newobj(1); + $this->_put('<PageInfo[$i]['n'] . ' 0 R '; + $kids .= ']'; + $this->_put($kids); + $this->_put('/Count ' . $nb); + if ($this->DefOrientation == 'P') { + $w = $this->DefPageSize[0]; + $h = $this->DefPageSize[1]; + } else { + $w = $this->DefPageSize[1]; + $h = $this->DefPageSize[0]; + } + $this->_put(sprintf('/MediaBox [0 0 %.2F %.2F]', $w * $this->k, $h * $this->k)); + $this->_put('>>'); + $this->_put('endobj'); + } + + protected function _putfonts() + { + foreach ($this->FontFiles as $file => $info) { + // Font file embedding + $this->_newobj(); + $this->FontFiles[$file]['n'] = $this->n; + $font = file_get_contents($file); + if (!$font) + $this->Error('Font file not found: ' . $file); + $compressed = (substr($file, -2) == '.z'); + if (!$compressed && isset($info['length2'])) + $font = substr($font, 6, $info['length1']) . substr($font, 6 + $info['length1'] + 6, $info['length2']); + $this->_put('<_put('/Filter /FlateDecode'); + $this->_put('/Length1 ' . $info['length1']); + if (isset($info['length2'])) + $this->_put('/Length2 ' . $info['length2'] . ' /Length3 0'); + $this->_put('>>'); + $this->_putstream($font); + $this->_put('endobj'); + } + foreach ($this->fonts as $k => $font) { + // Encoding + if (isset($font['diff'])) { + if (!isset($this->encodings[$font['enc']])) { + $this->_newobj(); + $this->_put('<>'); + $this->_put('endobj'); + $this->encodings[$font['enc']] = $this->n; + } + } + // ToUnicode CMap + if (isset($font['uv'])) { + if (isset($font['enc'])) + $cmapkey = $font['enc']; + else + $cmapkey = $font['name']; + if (!isset($this->cmaps[$cmapkey])) { + $cmap = $this->_tounicodecmap($font['uv']); + $this->_putstreamobject($cmap); + $this->cmaps[$cmapkey] = $this->n; + } + } + // Font object + $this->fonts[$k]['n'] = $this->n + 1; + $type = $font['type']; + $name = $font['name']; + if ($font['subsetted']) + $name = 'AAAAAA+' . $name; + if ($type == 'Core') { + // Core font + $this->_newobj(); + $this->_put('<_put('/BaseFont /' . $name); + $this->_put('/Subtype /Type1'); + if ($name != 'Symbol' && $name != 'ZapfDingbats') + $this->_put('/Encoding /WinAnsiEncoding'); + if (isset($font['uv'])) + $this->_put('/ToUnicode ' . $this->cmaps[$cmapkey] . ' 0 R'); + $this->_put('>>'); + $this->_put('endobj'); + } elseif ($type == 'Type1' || $type == 'TrueType') { + // Additional Type1 or TrueType/OpenType font + $this->_newobj(); + $this->_put('<_put('/BaseFont /' . $name); + $this->_put('/Subtype /' . $type); + $this->_put('/FirstChar 32 /LastChar 255'); + $this->_put('/Widths ' . ($this->n + 1) . ' 0 R'); + $this->_put('/FontDescriptor ' . ($this->n + 2) . ' 0 R'); + if (isset($font['diff'])) + $this->_put('/Encoding ' . $this->encodings[$font['enc']] . ' 0 R'); + else + $this->_put('/Encoding /WinAnsiEncoding'); + if (isset($font['uv'])) + $this->_put('/ToUnicode ' . $this->cmaps[$cmapkey] . ' 0 R'); + $this->_put('>>'); + $this->_put('endobj'); + // Widths + $this->_newobj(); + $cw = $font['cw']; + $s = '['; + for ($i = 32; $i <= 255; $i++) + $s .= $cw[chr($i)] . ' '; + $this->_put($s . ']'); + $this->_put('endobj'); + // Descriptor + $this->_newobj(); + $s = '< $v) + $s .= ' /' . $k . ' ' . $v; + if (!empty($font['file'])) + $s .= ' /FontFile' . ($type == 'Type1' ? '' : '2') . ' ' . $this->FontFiles[$font['file']]['n'] . ' 0 R'; + $this->_put($s . '>>'); + $this->_put('endobj'); + } else { + // Allow for additional types + $mtd = '_put' . strtolower($type); + if (!method_exists($this, $mtd)) + $this->Error('Unsupported font type: ' . $type); + $this->$mtd($font); + } + } + } + + protected function _tounicodecmap($uv) + { + $ranges = ''; + $nbr = 0; + $chars = ''; + $nbc = 0; + foreach ($uv as $c => $v) { + if (is_array($v)) { + $ranges .= sprintf("<%02X> <%02X> <%04X>\n", $c, $c + $v[1] - 1, $v[0]); + $nbr++; + } else { + $chars .= sprintf("<%02X> <%04X>\n", $c, $v); + $nbc++; + } + } + $s = "/CIDInit /ProcSet findresource begin\n"; + $s .= "12 dict begin\n"; + $s .= "begincmap\n"; + $s .= "/CIDSystemInfo\n"; + $s .= "< 0) { + $s .= "$nbr beginbfrange\n"; + $s .= $ranges; + $s .= "endbfrange\n"; + } + if ($nbc > 0) { + $s .= "$nbc beginbfchar\n"; + $s .= $chars; + $s .= "endbfchar\n"; + } + $s .= "endcmap\n"; + $s .= "CMapName currentdict /CMap defineresource pop\n"; + $s .= "end\n"; + $s .= "end"; + return $s; + } + + protected function _putimages() + { + foreach (array_keys($this->images) as $file) { + $this->_putimage($this->images[$file]); + unset($this->images[$file]['data']); + unset($this->images[$file]['smask']); + } + } + + protected function _putimage(&$info) + { + $this->_newobj(); + $info['n'] = $this->n; + $this->_put('<_put('/Subtype /Image'); + $this->_put('/Width ' . $info['w']); + $this->_put('/Height ' . $info['h']); + if ($info['cs'] == 'Indexed') + $this->_put('/ColorSpace [/Indexed /DeviceRGB ' . (strlen($info['pal']) / 3 - 1) . ' ' . ($this->n + 1) . ' 0 R]'); + else { + $this->_put('/ColorSpace /' . $info['cs']); + if ($info['cs'] == 'DeviceCMYK') + $this->_put('/Decode [1 0 1 0 1 0 1 0]'); + } + $this->_put('/BitsPerComponent ' . $info['bpc']); + if (isset($info['f'])) + $this->_put('/Filter /' . $info['f']); + if (isset($info['dp'])) + $this->_put('/DecodeParms <<' . $info['dp'] . '>>'); + if (isset($info['trns']) && is_array($info['trns'])) { + $trns = ''; + for ($i = 0; $i < count($info['trns']); $i++) + $trns .= $info['trns'][$i] . ' ' . $info['trns'][$i] . ' '; + $this->_put('/Mask [' . $trns . ']'); + } + if (isset($info['smask'])) + $this->_put('/SMask ' . ($this->n + 1) . ' 0 R'); + $this->_put('/Length ' . strlen($info['data']) . '>>'); + $this->_putstream($info['data']); + $this->_put('endobj'); + // Soft mask + if (isset($info['smask'])) { + $dp = '/Predictor 15 /Colors 1 /BitsPerComponent 8 /Columns ' . $info['w']; + $smask = array('w' => $info['w'], 'h' => $info['h'], 'cs' => 'DeviceGray', 'bpc' => 8, 'f' => $info['f'], 'dp' => $dp, 'data' => $info['smask']); + $this->_putimage($smask); + } + // Palette + if ($info['cs'] == 'Indexed') + $this->_putstreamobject($info['pal']); + } + + protected function _putxobjectdict() + { + foreach ($this->images as $image) + $this->_put('/I' . $image['i'] . ' ' . $image['n'] . ' 0 R'); + } + + protected function _putresourcedict() + { + $this->_put('/ProcSet [/PDF /Text /ImageB /ImageC /ImageI]'); + $this->_put('/Font <<'); + foreach ($this->fonts as $font) + $this->_put('/F' . $font['i'] . ' ' . $font['n'] . ' 0 R'); + $this->_put('>>'); + $this->_put('/XObject <<'); + $this->_putxobjectdict(); + $this->_put('>>'); + } + + protected function _putresources() + { + $this->_putfonts(); + $this->_putimages(); + // Resource dictionary + $this->_newobj(2); + $this->_put('<<'); + $this->_putresourcedict(); + $this->_put('>>'); + $this->_put('endobj'); + } + + protected function _putinfo() + { + $date = @date('YmdHisO', $this->CreationDate); + $this->metadata['CreationDate'] = 'D:' . substr($date, 0, -2) . "'" . substr($date, -2) . "'"; + foreach ($this->metadata as $key => $value) + $this->_put('/' . $key . ' ' . $this->_textstring($value)); + } + + protected function _putcatalog() + { + $n = $this->PageInfo[1]['n']; + $this->_put('/Type /Catalog'); + $this->_put('/Pages 1 0 R'); + if ($this->ZoomMode == 'fullpage') + $this->_put('/OpenAction [' . $n . ' 0 R /Fit]'); + elseif ($this->ZoomMode == 'fullwidth') + $this->_put('/OpenAction [' . $n . ' 0 R /FitH null]'); + elseif ($this->ZoomMode == 'real') + $this->_put('/OpenAction [' . $n . ' 0 R /XYZ null null 1]'); + elseif (!is_string($this->ZoomMode)) + $this->_put('/OpenAction [' . $n . ' 0 R /XYZ null null ' . sprintf('%.2F', $this->ZoomMode / 100) . ']'); + if ($this->LayoutMode == 'single') + $this->_put('/PageLayout /SinglePage'); + elseif ($this->LayoutMode == 'continuous') + $this->_put('/PageLayout /OneColumn'); + elseif ($this->LayoutMode == 'two') + $this->_put('/PageLayout /TwoColumnLeft'); + } + + protected function _putheader() + { + $this->_put('%PDF-' . $this->PDFVersion); + } + + protected function _puttrailer() + { + $this->_put('/Size ' . ($this->n + 1)); + $this->_put('/Root ' . $this->n . ' 0 R'); + $this->_put('/Info ' . ($this->n - 1) . ' 0 R'); + } + + protected function _enddoc() + { + $this->CreationDate = time(); + $this->_putheader(); + $this->_putpages(); + $this->_putresources(); + // Info + $this->_newobj(); + $this->_put('<<'); + $this->_putinfo(); + $this->_put('>>'); + $this->_put('endobj'); + // Catalog + $this->_newobj(); + $this->_put('<<'); + $this->_putcatalog(); + $this->_put('>>'); + $this->_put('endobj'); + // Cross-ref + $offset = $this->_getoffset(); + $this->_put('xref'); + $this->_put('0 ' . ($this->n + 1)); + $this->_put('0000000000 65535 f '); + for ($i = 1; $i <= $this->n; $i++) + $this->_put(sprintf('%010d 00000 n ', $this->offsets[$i])); + // Trailer + $this->_put('trailer'); + $this->_put('<<'); + $this->_puttrailer(); + $this->_put('>>'); + $this->_put('startxref'); + $this->_put($offset); + $this->_put('%%EOF'); + $this->state = 3; + } +} \ No newline at end of file diff --git a/bootstrap/app.php b/bootstrap/app.php index 0b4f4ca5..4f387ec5 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -1,27 +1,11 @@ withRouting( @@ -31,39 +15,39 @@ return Application::configure(basePath: dirname(__DIR__)) health: '/up', ) ->withMiddleware(function (Middleware $middleware): void { - $middleware->append(SecurityHeaders::class); + $middleware->append(\App\Http\Middleware\SecurityHeaders::class); $middleware->alias([ // JWT auth - 'jwt.auth' => Authenticate::class, - 'jwt.refresh' => RefreshToken::class, - 'api.jwt' => ApiJwtAuth::class, + 'jwt.auth' => \PHPOpenSourceSaver\JWTAuth\Http\Middleware\Authenticate::class, + 'jwt.refresh' => \PHPOpenSourceSaver\JWTAuth\Http\Middleware\RefreshToken::class, + 'api.jwt' => \App\Http\Middleware\ApiJwtAuth::class, // Multi-guard auth (both the new and legacy alias point at the same class) - 'auth.multi' => MultiAuth::class, - 'multi.auth' => MultiAuth::class, + 'auth.multi' => \App\Http\Middleware\MultiAuth::class, + 'multi.auth' => \App\Http\Middleware\MultiAuth::class, // Permission / role gates - 'perm' => RequirePermission::class, - 'require.permission' => RequirePermission::class, - 'admin.access' => EnsurePrintRequestsAdminAccess::class, - 'parent.access' => EnsureParentProgressAccess::class, - 'parent.progress' => EnsureParentProgressAccess::class, - 'teacher.progress' => EnsureClassProgressTeacherPortalAccess::class, - 'class_progress.teacher' => EnsureClassProgressTeacherPortalAccess::class, - 'school_year.editable' => EnsureSchoolYearEditable::class, - 'print_requests.teacher' => EnsurePrintRequestsTeacherAccess::class, - 'print_requests.admin' => EnsurePrintRequestsAdminAccess::class, - 'badge_scan.logs' => EnsureBadgeScanLogsAccess::class, + 'perm' => \App\Http\Middleware\RequirePermission::class, + 'require.permission' => \App\Http\Middleware\RequirePermission::class, + 'admin.access' => \App\Http\Middleware\EnsurePrintRequestsAdminAccess::class, + 'parent.access' => \App\Http\Middleware\EnsureParentProgressAccess::class, + 'parent.progress' => \App\Http\Middleware\EnsureParentProgressAccess::class, + 'teacher.progress' => \App\Http\Middleware\EnsureClassProgressTeacherPortalAccess::class, + 'class_progress.teacher' => \App\Http\Middleware\EnsureClassProgressTeacherPortalAccess::class, + 'school_year.editable' => \App\Http\Middleware\EnsureSchoolYearEditable::class, + 'print_requests.teacher' => \App\Http\Middleware\EnsurePrintRequestsTeacherAccess::class, + 'print_requests.admin' => \App\Http\Middleware\EnsurePrintRequestsAdminAccess::class, + 'badge_scan.logs' => \App\Http\Middleware\EnsureBadgeScanLogsAccess::class, // Docs + teacher portal - 'auth.docs' => ApiDocsAuth::class, - 'teacher.portal.auth' => TeacherPortalAuthenticate::class, + 'auth.docs' => \App\Http\Middleware\ApiDocsAuth::class, + 'teacher.portal.auth' => \App\Http\Middleware\TeacherPortalAuthenticate::class, // Utility - 'timezone' => PrimeTimezone::class, - 'prime.timezone' => PrimeTimezone::class, - 'cleanup.scheduler' => CleanupScheduler::class, + 'timezone' => \App\Http\Middleware\PrimeTimezone::class, + 'prime.timezone' => \App\Http\Middleware\PrimeTimezone::class, + 'cleanup.scheduler' => \App\Http\Middleware\CleanupScheduler::class, ]); $middleware->redirectGuestsTo(function (Request $request) { diff --git a/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/bootstrap/providers.php b/bootstrap/providers.php index 78c06f72..86f5297a 100644 --- a/bootstrap/providers.php +++ b/bootstrap/providers.php @@ -1,9 +1,6 @@ [ 'users' => [ 'driver' => 'eloquent', - 'model' => env('AUTH_MODEL', User::class), + 'model' => env('AUTH_MODEL', App\Models\User::class), ], // 'users' => [ diff --git a/config/jwt.php b/config/jwt.php index 50871625..d131f6e4 100644 --- a/config/jwt.php +++ b/config/jwt.php @@ -1,8 +1,5 @@ Lcobucci::class, + 'jwt' => PHPOpenSourceSaver\JWTAuth\Providers\JWT\Lcobucci::class, /* |-------------------------------------------------------------------------- @@ -309,7 +306,7 @@ return [ | */ - 'auth' => Illuminate::class, + 'auth' => PHPOpenSourceSaver\JWTAuth\Providers\Auth\Illuminate::class, /* |-------------------------------------------------------------------------- diff --git a/config/sanctum.php b/config/sanctum.php index cde73cff..44527d68 100644 --- a/config/sanctum.php +++ b/config/sanctum.php @@ -1,8 +1,5 @@ [ - 'authenticate_session' => AuthenticateSession::class, - 'encrypt_cookies' => EncryptCookies::class, - 'validate_csrf_token' => ValidateCsrfToken::class, + 'authenticate_session' => Laravel\Sanctum\Http\Middleware\AuthenticateSession::class, + 'encrypt_cookies' => Illuminate\Cookie\Middleware\EncryptCookies::class, + 'validate_csrf_token' => Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class, ], ]; diff --git a/database/database.sqlite b/database/database.sqlite deleted file mode 100644 index c00c33ec55bd6e10dd6218c7e9ad5765764a506f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1265664 zcmeFaeUKdKecv|&EOvmw%kq76JRPrZkH^8>0*9G>0S@=V;~s901hK$@i-kytJoWbW zJUiQmenWT90&^rS;`NTCXv?XvWK|^VA1g}PQYp4xl2Xc1*(E!XT#>CPQpx2c<#G}; zF6G6n#7dPdTlqat_tWn)JBtHxT-5i9T)#g=HgP%7)`vTA<8T zK2WJvE1yx6N~QX*=>H3q_`m9B>D!6;N4lB%e#p1f$8+EP-Fibjw$JF9Y)H1#J_e>nA>so$Ua-KpQ1`t7OTn);2Y|9t9iPW_FkzdrTXrv9U; zccyMmeRZlYnhqHOm$L*WYm?%_-Tq{)ar}7o+fM}Ay2%a8E-x=1t1HTy zAM|wh^P#wCMaFDX``uT0O%Y5B4+`w>1 zlus|;iEcVROW`bZBLs%@qv^b3HPEi|nUr>~**Y^v4#q$?4xOUR8-n#qd zzFOmnC#v5*EbN}$Abt4Em;X*??Oa|S-@bKPOwaKKk*#D*TeFxlfUOk+~+8S~3bPMRhh*DIYpXcGgji=It@VTJL*&`nlj4&x)^+?EA(05-cmX7BE%6F92wPz>Ucp=?GX*tuG` zJ*W1B%&Ic8x{Od#PUiRxuwPi zDKk=@9Tj%bSSoNtj+hxt&M6imE%3ZWr0k<5xo4@tz~!-(C*88Ert2A&<_)3^vm|@W zFLE4@7drRG3`~5(QDZ#}VN!2h+%wsjnW$=)l20 zIrwu2?Sn7X|3m%n)xTT+O8wabe|F%v4t(Rl@`1c6c1O4X~jE8nmD8lP6-zn9KURK9*-wmo}NZO^K+bLwn+ezx6KL&u># zC!I>IYk|J0lA0}__BT2Szx?@$%1`E6cTUEwbuuJhrJSuNVko(^rf>43ENcFlv->JH z_vYI#bdu3s#|u!Kb`e{Q03z0kq133!b67d1Wg*@=puZ#sWc zj(vSo3w%mA>6E`kEA~~>=|zivy=2kz3x$R~&*{6W#jJJOTQ?0?j=NMg?);GEreR-= z8XkXbqM{ZiJSQ89c;Y!GO;zY-;mkzEDTvQz#BC>_9e|Eu+_%DJ=Uye9JXUBIBMlho zkRq4gU$+e3=ZEKXFX|>GfOMn_J8LgaR9L>Rx##5^WM843h*`h*nTg6*3oTB`7BuQx zOed3|M=*xoH~Ut`9A7EX%v`J)`8dU5U0SorGs&(sp4bZcs84&G6aTiOQ8iA2Di=ZS?JKB-`vvR4x@{W5n0`#@nGTX4@tD z>%_~Z*lUrGlatc6+>Z?8*%M_GiPueSgpUb->^xV3wG&4X%2g=Y@%X0i%fLE!yhMGS z*t|uYa%r`UPHB`VM3#Q6tWj*~uBqwaaXxK`b2BB4+ObxWMkb>)A{uG_*%F^^$7)C# ziQP`l@441QMawIs9V?_@jwg>!RIU}|W3zX)flCpHj>@(lK7x#9cdA*kJG5ilr*`6@ zMzP^bpDJ$TopxdfQNv8ErY$WRcIKH9mu@E}5w?zm+n+3nlaxI~ek13|JkOyFm*)wc zr%SA;9h*ePeWF2Tr%MbwX%Yd9#){fJ|I|dKUw|GPQDzu5quv*kiOOJjOL^2FOZQpc z-e;dI&a}OdSYdQb3EN79pC}@1FC<2&d4ZuD^mvoIu0VL|iK1b&7ZL*upLwL!8x8gB zj~2bXy^yj*{;(vHeev<)?%Q5SS)X{YV$n&Mb`XBlWZixo-(LE7(E{5GDGL;hQ@d3r z9cw>UMkKbtl7$`nk%`KiIi0l^Vl&JI`xhET4Y!k2Q*0(;O|;Lc^pKJA3wm-KvOn>$ ziHcqrDYh%i=+g6jOJz4SD{L_H(V{!G=VOzg2lqZbm5X|x{YZ)Z%*VE1X?CANMb^?g zA1*pndp@=Xa%|dr#6g^%IYd7Ap|U2i9hm$9wr?6PPjsW1J@>(4&}%1|F3HMMdN{=M zA1L}?dp@=R%A_5GvK09sRW&JBjDSs*0gJUCwT!0KK2l;T^ReRPFckA?ZxYUP@^CR4 zx98*dLoG;@XRQ`#K9z~g9x78!toWo|>P&R%V2Pv7#kx=0M8Exr)54j0341JU)FuB|M=H@0@z%wxU;sCOgjH7%}9xpWh2D2 zuU|UHp8`~;zE`3D;Xe=n0T2KI5C8!X009sH0T2KI z5CDPQO`uWT+u4)(O9b)zuF5a)^FRI3KmG#&5C8!X009sH0T2KI5C8!X009tqgbCpM z{|GA=O9KHA009sH0T2KI5C8!X009sHfrtRk|1bp*009sH0T2KI5C8!X009sH0T6if z3E=$y=<66O1OX5L0T2KI5C8!X009sH0T2KIod011AOHd&00JNY0w4eaAOHd&00JQJ z=o8@Q|H-OenOZ#Zf7kz3Rj>V{i9dYwSN%{|c+Uz;Hh<^1C=4OXM96t2zx^qXGRR&AF=8W7jTDS8@&vs-P8UmaA}HapmMQmseJnF087nXU{KPT3tMUF|!_C z+7jVPW^Oc9HydGHd7Z^7!^06@TItx_R;Ej5TQ%FCo1~E^SeT zZasU`V>%;~pn71zW|}J%g)V5n+F5F+pe|S_w-GT>`PszX-X3-8HHC4qx$qx zK6H}otf`E8zMssJ)7q8J7LT;OUdL=wYHYLZ5^_b$<<-s$fVsQ(sYH23hp||b*FC2y zTu~)&Axl*NHGhaZkAh5DW|YJ*mW=3qQ;v zS4iK_*H$OP6}7AVz@w^%BPHF;?GkIzsTSX;9-_@-=w#nE+;sEYQmFDgqqq=hAl&SE z#Ipy|bu%7Ec_L*3RQ;KU_cE#2V4Cf;0Vgxy9wEC-gwNWd>>vx<&?tW2=YBBuFmc^^ zDl_f|Rr#(tYrQ`gcwO5W;I-TmRr$wK| zckCCh?8)>zT@;vBj!qlh>DY`SyvkKJP_%yFPy*4%L)DPFYfu3`#&Gw`SAlH3HfGC;E)v{<-dnAOJ9?h zcMz?&O|tb($1#O5Q!3ycojaMhyfDwO7P=o+{LUon(pzKqkxrIQCS>>!eaZQ&21Sb- zaZrjwOWczv0llVdn2r_{??$!6L$4a@L%DT~?LrSIpwzjIX*X1t%rEe#<5W+YQgV_b z59PWys!WtLoGhm`3BLK7tc z^gdErZvOSwy_JJC9&~@|%TjAm8 zd9%7f(QMo%ecMRokaM#oTe$Afks^5@P=syx>-4ZdrXHnzFxkUMDgSCe>F4Bp$y{i8 zEIRz;;%p|NNyTlkn+%wiI(v3#OBrjgC`vu3Slqhh*K3W1h3ee{Ij2fWG$)>wTsu6> zy%Lf4t#%e_fF22(AAKS&CX1WMd}te{9pOKrYIp$A6& z_9ym>4e0Jy1$91nT(Uaz!@(@)%)Rje4^LIH{EE9p0r9Y%BBKX7`h1MbFnPvTr|$_3@>tysgVTE0!4F?0Ke^&6qMc zD2-9+ux0n?!GO$WG6HhQ^uq%wkVF-&`&Qo%!YxBSbCmmihexzRaN*l|>8a_x?&dD@ zHXP5|_Tgo~G<}mEilpN~ahftHjFuV^?F$P}8{qKAKWS?#m8y&LKkEP8|ML#C1OX5L z0T2KI5C8!X009sH0T2Lz-9P}J|KAN-!W=;W1V8`;KmY_l00ck)1V8`;K;XR*!1@2Z z@k476009sH0T2KI5C8!X009sH0T9>?1aSV}4O+q+K>!3m00ck)1V8`;KmY_l00cnb zy%E6q|Gn`;YY+ed5C8!X009sH0T2KI5C8!X*bM}5{@)E+!W=;W1V8`;KmY_l00ck) z1V8`;K;XR*!1@2Z@k476009sH0T2KI5C8!X009sH0T9>?1o-*CRy|&+9zV3V{`!7> za^uJo6BD~(=I_HC@2(yRtILMvf1~>Fy!fU=wT}G{7AkIv%j1{XbTyBPd1i`9$|{*r z;a4$`YWJ;mmMTbHs2&?Qfo7`1D$K_PDx2-++EF?3bU+zop=mJ5iki*7 z&DYXwhP6h^%TEuhm|PfiWG&7!mseJnF087nXU{KPT3tMUu}}uKv@mBzYGi%oV(sTn zu2O3Km@*xyR}DPP_BTYq(rH<$wRK1Rh%oDKeo&N`wV3Z~J?6`QeKj$w zI+|L+ZwpnrUC(elBN!wW%#|r&B^GQ`ZCcF?tyc>IszJ-E04J>kor0mt#Yyid@0-I4 zdoG$*k@`4ZQg7XU`$M%xt5tpHgs?Ev=^3`F>4C9Di_QM@v8=UaZ;o$seSeh2F-tSd z94<*&xL!CcFYV0Z7U?h0EVrnLa_^?bPCrl!tuHSx$103VpBt{Kb-Nz<+^G48<&aYr zl$@FM$Ve2AxNTKnLEr%@f9-=p`S-$I)6@c{`X;TLIQiM1KAKa0Ms$4Tmm1~VY)Jv> z;wsCem}3XILf1ouc-?Xt@nUx%|9;^EwZ_v=SMR+T7APm14ya_fONGgkZ$FY% zZtBkXn&pl1-ZH7Q}Y8kSNgYPaYC=_rlVgaMwG|!xDgX5a2n}> z<M(B&{-p=<~FdctiI_uW+pR_&0loJY|k-G z*3G85(mtA?FO00@8p2!H?xfB*=900@8p2!O!*Kmh0e_W?SF z1OX5L0T2KI5C8!X009sH0T2LzhfDzH|A!15!+`(@fB*=900@8p2!H?xfB*=9!23V| z=l}NsI)(%R5C8!X009sH0T2KI5C8!X0D*^0;Bf6;Fr2@*z*-eI9P|v2ek6jUTBsn$7CB!%FD36BrwYt_6l;`vqe^FZ@@EKqx%(|612o_CpPY%y=3>YJKvGm}b82kb_Wtz4Sa92fUw0nKK* z#<;8;o|SN^uDfCPqB7)E&D*inzL%=2z0fJExttU%&sD`N7pHoctlOv3s@eLya-Ksf zobKCYw5Zny@O-QGrOWKGWUvG1dxFDZ)g8qA|-{9r48-hQi5Ydra6^_{0h zaaUX0QVpuv?8o2yXwJr^!1(q}Wn%lJV=AZ`WJ~83lwoS?EM+*N)@!D+Z^o|5%cmMT z*`H3;d{dRw%ZtW_ev@*%vQn_V;{~eYb$N+gS=?1lDX#J=>qr~E%4}`DPyo2tAgTmB z(yn3~9cfCj)V9`BZ{3;vSgrBmi&EK^;rryds;@g9Qyq#Cp3!Cgkjsx}4Pl7X_-3(v z9cQ6M)n{Q$8Br0sP{f#5!Isd&D$ivlmkUzd^3`DArlRp&z9%7L*j;ubF^Ga8lObrP z>0HlOf|heCSdLWj`XK2w7K&B;fXt=dqK)>K8_zsby>}%n>1=eV2DGKJ8=7UMzJDyQ z@xS!>b|~-Y+{ujc?ks<_#^?I(mw))@s;mvS*@BUj={yI9{Blj%FdZ!@ zF$Txt1-Qe_JOo+vgM|cT+(Ywqvdz(z$P06QP?mR0nYOeWs_RfJ^`{l8?#(ZM|6eV(EC1Zk{;81$DS6c3|%%I{i@hHr($ErWOK?(vV;8ZW$S#Acgp8HGX0Ma(S(K6vv*LUVE=_?>C#D|u z6KFv{pX(-0kV#vmdEZC)l}d*XtOKKB6HA^++wk&&8~+ll4d*l zz##XTp;Gl0e*gbIKYGCMAOHd&00JNY0w4eaAOHd&00JQJ5D4(|e|73kh5m>CKmY_l z00ck)1V8`;KmY_l00ck)1b%=7-mV_1ROZyDE7b#YbCp-MZjULKboz^J`nx1$+3x|U(n51tk{-ihR|I8Lnd2FHDB;IA9U`>rm&ug|t;Pabd6|4u9I*%#(czc4>r zatXix{{bo%U4sA!fB*=900@8p2!H?xfB*=9zz+!lod17F6oF}g00@8p2!H?xfB*=9 z00@8p2!Oy3kO0pAKR~$X8U#Q91V8`;KmY_l00ck)1V8`;en<%5{QpCu2uuS6KmY_l z00ck)1V8`;KmY_l00e%31P&kg)k>{$uX6a;4qmI=+y7tG{?~nfvgdaX{j17<^#dHV z(Dj|qJR$1tz59wRbrBf4;flh0RC6Nv;psw2i{#$;Wh`VFlyvn8S!^M!AQBdh8djL- zhq>5Kdj12ZQSmZaUXaS96zWYps5%l~rv@e@#S8h8X^+eT= zn57x%`hc^~mq4b9QoPV*SZh#DuT&juix;WM)_W@z^!t{jd9+$yWk$G`r(Ne(MU}Zo ziKb)s;yeC;it^A!DknAAwqPI^rgD!|cFql`RMEEbBOSF+c!-Jz9Z+hGQ>Uu$bc8#2 z%&^w`o=FHnpNmhgT^ZEyQP;ulFf;pm?DLwMGS$ zME^r;`Us&^X{~ze=EWyP@yA=IqR{C3*D1nK{V1P2&|}y7w0a7WrI(+{+QSg3@h!q| zRju2lp;J{l=TpV%LMIF7G>i}}ss9NP?QcI-vZUNn{7;Tr0!`y+eJz~{TwFPOesSfB za&GBLYf{g)4<&I&g{sVMz7D{`y-~5;mFF z5V5lIe`4AcbzG|C&5IGr?Yt!Pm)2z~t0KE&J1Gz7BdfF(n$Xt%`NwOG#~-i0dsW7O zFvH~*;VWf!vV#~$HFq7m8(H9n;rT(f@?)_wbEXAVU*1`R9u+g>8*gS4kIUX>`p$*l z&O)I$WP0xIremi}xRc*v#M+{DrlWtx&NJhWkQys}D2+9RO2p|cg4ZjgEXQ&cP9*l8 zf=vkHPMguV4N?EE@F)ToKmY_l00ck)1V8`;KmY_l00cnb5hQ@W|9=FPiY0*n2!H?x zfB*=900@8p2!H?xfIyA_&i^?e_z?s^00ck)1V8`;KmY_l00ck)1Rg;GIR8I_O2v{u z00ck)1V8`;KmY_l00ck)1VA810O$W45c~)NAOHd&00JNY0w4eaAOHd&00NI70i6FI zL8W3zAOHd&00JNY0w4eaAOHd&00JP8BY^XN4hVh(0T2KI5C8!X009sH0T2KI5CDNk zkig;k|5n+z=l3i7pPoFj=l2hP_}~W)?tcWAi6uSK1SX%E6zjY9@+WJJC!VPO)cL41 z>VTI7rh3rfzccybsNv=D3+I;93yq80Mul;=97AVnx>jgbB5(rDER?;^R2_9SqdQb0 zt^(R@Ki6)DC3M#{Q={7RYYG)lXP%*FM+_^ftImdM*i=o_GzF7l&3Unaz|(AB(*wh? z#Uf-0U{UwAP`RC|$m>_t4Z}=TS}iZi8%oOJ?xTB+)TLT1*mSx{x!-ib*9605`>O3+ zk7~Zlro-wImz4W0t4Lp3sRudGUAjwEw8g^Bhm26+b*%>PeyCPTWa2V8&ZAo?zHTcghHq zIArGjbPUwYW-sN!`HV8Wl=%48FPN!R(0f}yF`Zs-SlN3qbR;m+dP>cgR~jzZ z+4lZWZ{2-my4Gm5s^2;-jKX7vwchu5OA4*opN?kDB71XuyI?n5MrFD)c2S(WW;uPE zs)3gVy?E%Z8aPR;%SJ}cbv2WblpD-6W@f72enW;em-b+p70|z~HrWg_h&z=})fx*6)pwqj zqGB?sZBb?uYWmvotl`U8nHCw}{8O!pdvL}mbKER8|3#n30bF(G;f=g+n(^Usd^VSsKqNI^rEIVW*VmUUCih*Gd z3L?S4CB+E`mR1>0s+meg!^9|XRAy<0nS|KR=}yVqbbUOzO{R6#4UKQRq&P;2WHd<; zDvA?|l4-xc{ua||Uy(Uod7xi+x`Q>vVqK$e@fC)E({a8yf=_WRTx*yRSj_jeUW7<= z5M}bJZ)>i<>7;fyY4)))M{WqQ6dzL6sGq_Hz(wjU{Qm!uJ{V(hAOHd&00JNY0w4ea zAOHd&00JN&2;lq=LjVB~009sH0T2KI5C8!X009sHfk&SJ&i{|Tj`e<64*Vj8{8=mn7U!uDQYy!%nUW-=qmqW78m z!N6py?wAg}hf_+HzOj>DQ`_HxVBZfMOI`0Jy>?C}GmT##8TJ|q^S4~?K-_Y=DGtw< zbe?{LW$Gp8^j^vs+4e6s9=Bn=b?dDeVU2fd;j4X3O&9O5R(;*^nD6F(p37Qe{`UCR z7{01FZI0O&sro^`%iq#DLV`~)^&Zx2dx>%MORURYJ0D|?^)34v`o>W4mQvZ*P$`1# zJR8FPU=Fs)wwRfM9sWE!40d>XeAwH=6g|d1oV+r6_#1Nho73gXT!oaR^utB`E3I1N z#EI&iUXHl1zqEZv7dpe3NB75vp7=(<{8WsX%Y!R@0rBp@dy^flHJ*ODdOI>`=2?dC z(`(*+dJ(exww*(o+!-IHt)=%5@$%t}@s+<}UYg`kmmJq|?|!XWYs}46zx`xrM*QXI zX69Yz^iuEqrRQ?%E=r8Q{`8B#c}SGIo~S^QAtB!`o(`OHuZ<(xhT-`^mT7q{6H^;c zj^Y;u>+sdqBj-lRPVRlyQD&KLB(X)0Z|q4mu`X;Oe{l5ptB)6&)H3cI1x*`}*fbs*od zr^T!F>9XnQS4Gj7>G(D8$<_72v=Zs3b!Y0hDCP3bmB_H@26c9=PusTddg-4Q@@AI4 zIli?8OrsJY-iYK@>sm%wYbu(hQlejBap^dpwMbrps;oRq*3{^@MOJHucAG@d@~BLu zMd&!?8jQ|+a$=e8I5xwJYPQ60*UyN!hJLm0#+6KD$S&^bGG-4cl|UAvlFJ!An{~r8 z>4P#!y#N0_Jmx@C5C8!X009sH0T2KI5C8!X009u#Ed=oQ|GPy~m?sE;00@8p2!H?x zfB*=900@8p2;4sbod55i9a@3_2!H?xfB*=900@8p2!H?xfWU4cfb;)u(G=zh0w4ea zAOHd&00JNY0w4eaAOHgQPXOot`)7xiAOHd&00JNY0w4eaAOHd&00JPeTL|F%zgskg zd4d24fB*=900@8p2!H?xfB*=9!2J`z`Tzdep(O}_00@8p2!H?xfB*=900@8p2<#RD zIREb!O<|rO00JNY0w4eaAOHd&00JNY0w8ez1o-)Xe`UFH_}^BR>(>5%UOO>4J7Mhk ztM}96ombnn#z#L|eYY2A>n6L_H{Ry|KYeCpX>oN)SzUZ}c}ZF0m)4Y{^);irrWkg> zddyQUuADu;xN=1~w{%6h@W!fg;qvnGvAUwH`9Z(S>_ANmX9e_4$1%lyeN*#}&Yf&! zd(chI_f?+NV@(MRdq7jq4&>{O$JUe$)6oLu zwWZe=FE6JSqC1vF3!%2`Mi4HISJUxVQn+Qc|Z|CB1u0%wD4b z_&Nf^Vnr#r#-UR6W6HFp-B4YJR_9MEcHd&2p)0l%(D$aPblHa1Hv^^FX}6ms><3!V zC&6e1bcNl}`#y1`3Hgpa9WGayHXZ#cUDbl=czz@tU0okcOG{|ozI?LQXtk>Mm@py% zRgK{J*`Hp?8dmn^do!`DX2&$4a00==B`c6BU1;Z3TGV<$q)h9xd8wj7MOO&@mcg#4 zEG@i{Gwz~!MD25OMN3ZE8n-jD0m~I08+Uo;^2*B6g;jO+?D?fjtBdC^)?2q$t%{O$3zD_RbX9?1-bM+RN9Rb8w(c6<@DB$y_7 z0uNPOj>IUcMd4wnROpA>%XOK_0y3uU`m;1!>uo2O(%O585UrbU&kF~=eIik$t!=4B zz^qV>@lP+8C^5eI9#ok1PT6BdgISSKeLVcUxA54y{oGuw(P&ify%;ZEtl595xKw}q z722AWiW##Dg?+&6_mY@$a=v5-=rUjT43}?mWH54%>1zJwx}$j%kDWd@B^j48W5j;j z)Vh{o8-Czvf#aRX{1kT;TD$8Rj%Ng^-7-d=bSHKR?i+@$YP11w<&0Eru&EZ2FFh!0 zyJR6U4pK2o z5m~|+mflauMNx>aV>@#q#q|tZGsU7ygk@xlv#`EPNljPm-zf)@+7ok(MbZgWJUiY~ zz4cDLQ)@i_c=bD1BV}y#?JhwV|GbcgE3S?Y(bc?sqL`&vnoJe8Rm0wL44q|zNdZVW zXH~PDKF{f7o|GZtQ3lKtyj<9kGBKONW*O?-bvfWS&Y8PAzLS<6=VE&lD&3%YsKXQP zm|Cpez=_05!3m00ck) z1V8`;KmY_l00cnbehA?Fe?QdF1_VF=1V8`;KmY_l00ck)1V8`;b{7Gh|96+ZFk27+ z0T2KI5C8!X009sH0T2KI5V#)#IRD=dHM9W%5C8!X009sH0T2KI5C8!X0D;{_0O$YR zr7z4D1V8`;KmY_l00ck)1V8`;KmY{phX6nS@84UkOnrQB^~nB1)%sHhy0!mw^1r#C zzR(5)b{&Ct&OBdh%*<5(^2>p?ZZe-)%nz8S`nuyWKlkISXI7RLSC^F4#aEY?l(pR5 zHRWi1&FHQvh8?gT^OTD#XU{LLTv5&~T~RK)v8r6Syu5s@u293E-(_~7riHTt`ljQU z;=aDAc}M3?wz56wrsn&q&-B1>>`XteExo>Yc{w#$-|cDv>#FO6WM&1SO~+!_9q+2D zZS~fa4b#yAr66>zZ@fKH!1bKBXi6hxETc<&EtTEStdR21@iq)wGsh9sf`Hjw&DPmA zJ-V6~7`owV0-MnB(9zvMb7gmS-(sGj7m);HEhCqkwUMENNwGC$OY?-vXWKcI`=Lz? znC2x?r(4B2=^hgb*Mc>Lt3EI+7E7JEyt1-%VO3o{dw%KC>f-r}!XTxMjMt=oOqtd- zQ}3H0N6YE5X*qGlb^^ujo2Jrb8(QBCl%~mgnm%aeTDgvC=mT|&c|O_j_U+ZJ4)4OZ zHP_#CxbsX)Pix)$%BO3M#l`BaGocqp{$;VQK@(MV$L<0xEz?fZF7{?W6NvaUCy3E%-!{xfC!P$*Kj4{$f z;wU}RDGE}uPcu#C^$gl%B2$Ymyk4%w`2Ks-WQ<;*2s#p-v3d)?|KD{dR!kTKKmY_l00ck)1V8`;KmY_l00eeS0O$W5BSQla009sH z0T2KI5C8!X009sH0T9@A1aSV}bt=PzK>!3m00ck)1V8`;KmY_l00cl_#{_Wx-!U>Y z009sH0T2KI5C8!X009sH0T2LzT}J@t|6QjtOc(?}00ck)1V8`;KmY_l00ck)1a?dS z=l>leLjw>10T2KI5C8!X009sH0T2KI5ZHADaQ@$QD#L_900ck)1V8`;KmY_l00ck) z1VCWN1aSV}F)}m&0T2KI5C8!X009sH0T2KI5CDN)M*!#lU8gck7z9871V8`;KmY_l z00ck)1V8`;c1!^0{~aSk0}ucK5C8!X009sH0T2KI5C8!X*mVRBANXRWR#~jn|J|X# zU3+ZbcPGy5S*t7_Q0a1|a(cvaCSRWvckle@QmwJLSbgVApskzi`lc56n(L}P&*{6W z#jJJa`J0C855IHq%*xW@>XNd$`0DbKvNl|BO*vX$GrDVvVF#?oJmuob+4GAlSCn&0 zSCk8HtST2SFE1afE6SQ~YQC@fOb-mlR?|%~V!poVIHo#anzyEGX`a5Rc}Hg#TG`Hg zW-&itX_0nbq`&?a4Wy4AxcG2MHiKb_^z#(t@6c;DqmC1a)g~|WWrnArbgO8C$xc5wG}Mp%;lApr3e=&4msS_gUlaqCDs%g` z_13L7&eR%DJypG92o?8z=Bb`zGC%q5LROv0jq#Nz+N7N&b}%t}K48Yrv$spZNaxrA zkI_xK%w!||Ap?4tpOVv5U#&Gdo$B4=2~pb#$h~!q2egoE=F0gJ!Wn__NvE4-kV<0+ zSHabs)<@N@^W=Q8QO0^`%IPIA{V;yfKI?0}Xa^N$<_B8P_md9iX3IMC znH@yiYFSw3jm9v7A!2rrH9TqPahHYoQEwg5s(*v0M|YZ^t2LIEs_%S3SdqtE#|ufWWSy^Ina-$-1|qj57nT!a+#yLlnG{!l+ny91#cnULvy&olJC*|${Mmo0zT^|M)>3=LHrJ4h4=$q~{TEC{)dZ}2TV(qiF$ace3bM@n6e^3y_RUFPea;U?n=Cwf2{5`D>} zad<9MsZ@VKc-c94*$sA%OG$Bcezw0|Y<-1V8`;KmY_l00ck)1V8`;Mi9XHe*_%d0s#;J0T2KI5C8!X z009sH0T2LzM}z>*|Br|wu?!FZ0T2KI5C8!X009sH0T2KI5Ewz=@WH=Ssa0OB)F<{o zzVA0CetPc{hyOGB@zu%;BN~ZYlNTq&zjwd%My=6mRljvL(AG_+1p%|WnyoXns|~V0 zy?JJ3X>oN)SzUZ}c}ZEz-dt0T*4K>gnqt@i>oHHcxN`RV;>s1}+|m{0!W*l~h0Dv! z$Lfl*rkk4Yt3J~M!?D$LlZ;qb3)q^%zh;~Jffn@rH6>s-g7}W|+S2QbmzP(SX4lg; zf~FYA@2^`1t*5K550ZIF$#_1Am==Ts28PAr9%DJTzit}-=62mBVxzm$`hlZH!s=*I zk2x;0w;MQ?8mBYWW18>SYs!}9>6@B&bar8(Ra~>rEanHyOUt!l>LF!)({W67z_gUq z>70;G!*NAqrY6-L4}0eF%F5D(Rdx03`K3#%i{~$j0s1aCf*rT5w{E%TYmGB!s&{&M z)7Bl!Vs@Yg%yLcYJY~xHTVEb(^m)ngJ-{+-)z=-5?Az{J%ro>XQcJrr^1hrl&Ho0O zY{nA=GQ(Fja<45KATaEKbjbqnW+(KNQuP!ZrBo4VDMCKIERf$m7b5TSrP@wlY#6#G z{GGh*EpmNd8-DA@bI6BFj*mPXE*)&L$fIc`cHU{X$Fbq`@`qb%TB_c(A!7mBf&| zNVsk_7`WVG%L6-I^|a7j10yg=FcZ3@ZHwu5nXh|>%c+L_iL09B^lkE&QlIPg`HmNF zIRzryY&#U6yN2D%x>I`FiGy6cpOiYCTt+OugP53}bG>zYB>vDFKbEz*p>pG!UNmYdNys`OyFnQJoYhGpLWS8$jJD1amroN4 zO~Ut$9^YB-Yh$u0ktu0j7=;2Li!v55>QKKav@H5ghkPe3MltvL*;-?9v3h496dk5l zuIKbTMtK%(ZJNHxmF^F}vzk@)aKZ6)?J?K!a$zQ;$_>M0s;dQ?qa4}sjGnPg#MMNov|KIL82kwIa2!H?xfB*=900@8p2!H?xfWYo2fb;+E*CAE_0w4ea zAOHd&00JNY0w4eaAOHf}5#ZK z4-WtA;ojjF4p$ESlS4m$=&eI14;?u8CkKD-pndSA`hTeZz4~|SU#UNP;Li^H)`4#v zSU&LB{_pMot^Hrye`f!|+CQuPV$H3+Seu;u!^vNo^d~<*`H6l1bl`ApftIp1;v+eoWc3Tbq3WxrvW60krs^TwaUjF<<Ca76t`|BOHi7U@RQ#~%sn1SS{Cv~-lXC3qn;QLDwCk#d zy+wbS>8qyGix&NQ$)e{M3JrUn(|1*iS?kR6Hw{;gyHqyr{E+6RVPB0J9)E43q827R zCmZ&C=Bb`z(o}_R7S2pmoPzjlMm+tu;8C-4uaZt4E3}J|(x2z+n>3R6oCf{DC4Fcl z-HW z*=(elS4uQ97i&iT?Ucp3^!M-d_uUTtl`#LSlsorM_-z#TrRYVt)Bi6Yhxo^(AMzm+!kHH)DqqewnmrvO zWC48&p%<-r_Vh%>Cde8N&SJI16Rt#L5$mY;7xTE=T7}^gtm{#=gXt&diZh9LTSt z6EiIfli?pc^FMBlX87#vMCD4Mj~KPbHu`oql5KV-DwhhfG2&}|%_~Z zKRmYR^B&}+*ZPe9rd}AxvnR?X60e)u@Xv&VvYqEjuy*1o;(GKayR=JC?|vZFcf3S> zo!GoZoN{Tkj6V6GOE`1K${NL%PM-_X!@rdm1|vtBDQVP>wURV4*%mXSM)S{>_-s2? zL()k6eY~9CbFGPrmRCqSR!G4dPad79Tr0@OX76ePmm&~-hQjvwXM^GxbgEghJG5il zr*`2-r3wvS`c!cv@3a#`h#F>MHEn4Tl$mErT)Lf@MA$kKZhx{QPEz&|`Hh?-^E}6k ztmx?yD{99kk#V1BklE=H!%mt+0Hd*@HqSpbQRx?;$3~PHCj8KaRPPJQL}f6%rAr?z zxbDy&lJh@;&D;Czlf{{~7ZNMXw3P@yQAF5YNQ_YP0z)_G@A0|o3a35wMA0zX3yA@S zpCd}EHyY~MA1!)&dm&|s`~ygl?2C^Vci;9x%KF5Il`O{i4nlhZef+W?$G4Y0UbMjW zLdpU~UkV{0JCrce5b4;>mol#6vfRMsT61OCBF)i({7C%Vzhp8H@i z=(Uqfmt?s%EmxY)^B*YsUwb~b0LrAr$JnB@h*E0G6(e9%Wx!(XM=hgiwU3mT%6zPN zISj>o{-IL-IqH*#i_y3}AIBeRL83fswMg^g(>al94wb1UR(#S|Y_`$NWZ4KYt?2V)krHS2l?W%BNNgiQe@7>ZnOZy940uWr zb=%y_6Q?%68U2sGPr7}}S&syd?%`eL1!I%Y`6(wveY9F8880g}I6wb?zVde}Q@=a) z?$l?eK78b#ANiX{ydy6k*?;(dIQ&b8w+_E}xO(W14*krbwL?b_{(lGm!NH$7xN&f{ z{^#|-TmNQ#r9OS&Umy4f2flmYO9wu+|IhdT*GN*wY-TA?73xLtUmD{MZ=j;rW{s%H*aUH%p#CtTWTO zuKLCeietH^&6^a`WR@H@rAHUZh#RIB9obLbq(~;yu~;%0L}#?$iVsCI+T!W6;4%+? z;UCsb@Ubso&P9FW^=E-VBHmp)(@tMR#6|*-};ciZ)lzo;mj3lU}wr>8x zsL(rRZc@CH$IYZs99N<zYO!E#_}hoRc;iw_1obB*X`5K2o2$Ns&%IJBpjl z$IaY6e~SZsbe0T>9+jjvhuNDH_T+gxZke*DOv?z`$(t1Zq#ebL5<7}!Sv*Y4+@xrj z=q7@avZIhjG|aJ^6cS}Z6%Uh`O^)5nO^S=z&QH#tP7YE&B{_+0oxWK~p45e%FSO@U z2T9V2_|Uzk)0vR#>MDKsI5e?^pG?eIJfl2K0V>I0`NM<0E#GL6WG^rLrNniUW^pnr zA3l{@_1xDJYf9S2hR0Fx&(mfPzNfyHSVht(PGbB`YC`9|>&z3!mR}+#+!#JVw zbjtyQ+5Xy0cWnn`vmBKol#i8;ixN|M;PStxzt zdM-!i(@L_~Yr2(d@Z44|xfBiJ45sje;DvsPI+N$M=>r0f;f8Bah7u7t86*yrEG1T_ zNxNLBVxIHM<``>qLwp!Ie1s*C9rtpNXvLahMTYXpF=6&v5)6`Q#j4!UEW;eoE-XG5 z94%$eEwRib^h9lXefrq5L2C+?Gw&o}E*>b!Nux&Gk!V|B8&RtnyTmM$gjBSOv)7|m z5~`CS;%$I0I_kDy=F=c132>b4oLH09n{5 zaoyxuE}d2A4JEctTa;tdn+2r3&$1H3nvIW?%+d^c_!PZvCtA$Q-Q2@-zV6xBAL)fc zWQ&BCpOoo5%0JFcuYRHjB9V#dB_>9{Rf--8QA3@-??c^k`uz6?QLEYYvWdnqI&RfZ zotIB&C6OkT6hvo}B>OwIRuagP=e!ZRzErAOqSV<~|0KO(^!i@v(aedTD4S4h2IN*Q zJupc<6srZiEGINYI0bYp65slyVVQ)rf32d>$UfvT)E>&X;x&`|yajU4Ft%=9?{I$vpQ_mj$ zJBL15|6{sQ`rqW`Npbzomse|znVIUlmUum5?yZvfAJ42TEv_yptBbELFDYxeyKBnP z`kK*QQ|R@Qtj9d%;>y|ciz`=@b4yp03va9{7cMU^AFC_M8oiU$WNXTn#@|-%9i3fR zXk`WHE$?)nf6ZNIzU~<=f1~M|60jSA=uW(Wc|e6qcxec_ZrB4k1v#wp+S2QbmzP(S zX1kegBVNe3vnJ8&yT@+CtJOG;Ii|dB?Lg>W zya1IZzCK87RoX|)t`zdzYzbxAU4~X>Kwbkb(Nt&l`I2737tP8V7q(`lTb%BcwQ%T7 z<9@OPIb^J6KV>8w6^TL>kK4$aLhhA(=)e{ndPCFN##A~dR`^Cw4jH((@km8jexGg5KC+ABv zDQXTRGUWzBH5Y?EshfwrM8y|ruJm%yWv)^> z4`16{xbPE0O(1MAz5!$dW!em+DuuGj>?A87wq+4{L>4kfWHSPR6VU7CwRPG!b<(@k zmXs+dE0pKu%*Ga`Hb68F-Jy{xg7$~*{z%i2rEFjLXIP1{FdPa6w7e__4KD<0)y zmC(va-X)Ry_Q5aK8mCWJ?{;!D(=WD9P^GVK4C9#=dM`lbNr2~98PnqxjMVJbFLuX_ zQ1W8_F`Ob4=0S^~jtttJa2qJq6c59hIqXy$xB5R;YdrHz_3rUR8&RzQp@ZmqRzg0y zwNu{dNST{E2% ziR!IE$UQ2#AWJWFbG)P5J768Q$lGj@&QRtVh^33-`Di+z)LZ!dzj&B|FCYK{AOHd& z00JNY0w4eaAOHd&@JJKD`TvntFBS&^AOHd&00JNY0w4eaAOHd&00M#l&i^n35C8!X z009sH0T2KI5C8!X009tq^aqpxDTcdw>L+TAX0v+hc%ZGDOn2-Jqu2Mez;Nu$5B8arrNz}HWp(k@k9tj9d%;>y|ciz`=@b4yp03va9{7cMU^AFC^}nR=BC)|4&H(>FEm zXs6w7l}c@CW}gj_tGDib^vzo1nP;lswxXF^E(;7kZ(nzk@2&j2lUw6YoBB1=Gg-*b z*k=~=1Lh^OogcW?7qc3LyxtX zwCFt7ZikHwUv>NIrr~cgvXsEE2hv)K<3vqjc~zr9_}T))Dp{ZIF}l}P*U746rv*$> zXmnS7mk-n3X232p+4fSjV$;#Dj=kJac$=Xkp2dCmurnqr+W8vK^+vQJeCetYIx=9aAkx7Kh(MqQydSt?F7tELgwtk zLP^Zrma$Otwtic0-Ff}TYmHZ4slNMoK@*{=mAFUd##_Vnl97AR>KX0`8B3Q@+}UED zXLQNvA4)8|)~B71Z&JD4qY!{Y_v$33F+^f^TZy@CCFZx4SlCwLWVuAWb?fkzTI2D@ ztGBO(hQzmO^5gKI&76^hS9f3%V&k_pE1ho?G9*eQSht_aNeVK$=4!eTr1r#&xfD|t z;eT0~vV7%q)=BFvN+dtpICZLeXCdLC-hFdTq-WpydU&z#c|6>2m|9OyeedRQrEZPS ztZXmIt|QuPvRI4j?Rgsa8=09F(mXzU%3auvAiGuZ>mtMA`AI5M$xK2B`2nRDQ5Vww zn{T+x4wEDq#?}X_Ll;N5Fu$#K$?QDC(KDA9-bJE+mM354Yk&I^2sF4vIFAPr`<35#>rW>yuJgoro5eU?BO|-$8>{H zv>zej_t)Q|LquZJMWZKFzSfgQnK9Wk)8Xm2G}_Ubb(yYdzt4;}j zGY*Ue0w4eaAOHd&00JNY0w4eaAOHgI4*{J2-yiN66$C&41V8`;KmY_l00ck)1V8`; z9wq^t{~so8j0FN900JNY0w4eaAOHd&00JNY0`Ct2od4e+?idvWKmY_l00ck)1V8`; zKmY_l00bT;0e=3kPW{gn`XBxS0T2KI5C8!X009sH0T2KI5C8!X*p&p7>O^JngRbXT z4u2!(bMo!&@P#XRW4j!UOrY= zlr=x-cd1)7Eu59mP0jaJ`MHXW+-pm(FJ4|w_2z2c=+2Tks$DH$YYP9G9WN!~2U^hg z*OY+W2x1{cj@s0^mf`#KsSl-ZcWIFx+cMbordZc?BiQVE+I7uL4Az zVGkrRNhukw9rhfLs%|qqm)Tvz?uD&b)<7wrw%5$873GckDpZ z$;1m+wsMwU5E$R!3!SUBW|1LEwRGC;R%~m6c8O6J>_}&(>25l9s-xL_Kg`k$GZ|=h zq09h0W@`L{Oo`k=zMnEvqOLL)Qff!1OAPKonNsWahr6}L%uMy}n~_#&v1-69m)65* z%9$Vgd3|Q?j;~CeY&Ib^H=CnCIyE$#oMcDcOebM!2|_^`s_Ra7pl${h zY0+X`qi<#U6~;mVg-qDkqG^qeCY^9p>7?s}Bv_B>%5rM4_?gQqD@zwv)z!1-moBX? zp1)Xc-T9DSYb-8S-#HO#Mf1#o+BJM0I4LyxM$e`Y>yd(n-U)KL8Y(!x+C1huUXTvV zaxH>ksquDBmRN7m6}Sx)ymP}aS=P&ox^+CGXV}D~M6l4IMFCe025!puI9!oH$Ya=00H*WDdI1ryO9*>Fb*m$_p3#tO*qE zkFWWx!xlnY+;hk_<6behrpr8?(u~l7y3E%-!wq95Zy-!4?r2Lh`vl+a)0ScA*&u0Y zH`Kn*D8Z1rk&13c-pMyspUb3DsaPh;2WSMNt8UOIP@`-{3dEawC_#jI_!kLJL1akS zkaBW9BTb;-{(^)IFr~Rdd6QEAPQ0?B|8;U=6oQUwVqmbJFNuk~1BYvGUA zhFDs7cYHgGOgWR*W@3zCZ#jm}G6Gra7-!p$%}len2NmrsM%HXU*NzOP++s_i(~g~) zwNz$0P!yEH&9yu=ELZnKGO9uXTw*FKy;`H$tiG#ox427kCxREuJ%)RlA zBy2YNL=(!4vnQ0RHE$RNZIzf=Y&40Vm7UVcx(W>4aH9lLM9+dyh?C-3Iz5q6&B#QH ze$7L*luD*mc3LURGRy3?*xiFRL_W`Y3%~z=_>V6zA_#y02!H?xfB*=900@8p2!H?x zJZu6u|37Tv7!L$M00ck)1V8`;KmY_l00ck)1Rg#Coc|v_bBqWAAOHd&00JNY0w4ea zAOHd&00Ix20M7ppn>fY;0T2KI5C8!X009sH0T2KI5CDOPPXOothtC`%f&d7B00@8p z2!H?xfB*=900@ApW96tD3Wq;*2D+k`#_nQ;n-TU|V{Pn{t2Y#ds?uZps;x73)?wLbItd<3ibhp!iz z#rMW%CsrccZL{FCn`Cf|Y+rBP`ovqc#>+2PZ@*Dk64ewAJY&7jm&d0fdrDvb+VF*@ z#df&nbkp~|ZeGEAcqdekTa^n-)!&u~>#cV_X4D!_KVAKf9|Kamp0*JPjYr>pwTL0Q zGycj0rtzZZ!>2l?{TEB-@`~AVLBs3X=Sw^b>&upwFP2H?Ak@GJ%v1sTVoCe*io|lk z8%CjZp}=oicAOf>q^zJH#mRW>v1>Pl0>gl>buA+sO$vjDBD!N)R0cX7d_A^hu~0|9cP(|wBxvZu1?<%%`#(TF_w73vmfRiR&Y!!Tcq2T#@tD!Bmov!^JD=M}jDOH+s6x2D~6w)iMwZ|PR{3c(@hGiY|}Bj`K)vH`I1ugTDR*l%3~>BvrgjOoo$6@w-eTlAQguAG}FEl zh636aQ%m9IQjl>S+B{7)yXX#O?FC8Q>Dxg%^p%bo9j8=xlhVj!4dr8#PchG<9o5wA zULssNJa@oQSa<9|(*xCF!KOp!2N{Y>$DqxPWa-HEu&oup|KG`T2grf|2!H?xfB*=9 z00@8p2!H?xfWYn~fb;+E)Ej0E0w4eaAOHd&00JNY0w4eaAOHe8A%OG$PG~_E1V8`; zKmY_l00ck)1V8`;KmY`GCjp%Qcc(z66_y5P$bCbWm?~f*4KfL$guh;+E1OMaRyF2LzvXwihuGJb(J+ph@>f)=*OUhbuXH7X;Uo*OEieU$=$2{fY%GvXa zD_4|rOIMT&Z>%a8E-x=1t1HTyAN0G-4%D=8RzTl$98=uaH#P6*+{sq92i??sU-g+D z7>=Fk=e4ER7cVcT2J5?BEnr=BeUQv7H<;tGHD$wev_L5|_nF1~fO%`mmgb4c%q}F8 ziRGJ@T+gt3&2W9;^ao5!H9Oskg<}x9$Hd&UU`^q(3=E4ErQ`~RO4W}k)0TEabsa+Q zPb+rcVxFNZwiD3zrm1wn_$MB zO-H{L~Q)!WjaW^N!ATWWnO0S+1sI-*Mu@-vugHjQgS!6)GjlPE#?Uok1|2tf~;Va zh+k&(v&47|m+Iui>aCk!@oSB_xjnbu6zVV?yQkUyb>>Np`X!et_p)jz5gA_vrl!iV zGKkCVE3Ko9gzk7`K7ks}KGX$S+nZjk@$}Ps?)_vc(E21B#a!`i=Y0z{mi+nLeIWpd626#ZRHXN{IQ%LO!_13K=x7L`M*>mTWP`{dG zm;*J^g};&gv2rV?=j`qAbshDaw#9rvC~mx23By-uBeOJbpl&)mS_XzakQ=(>8M}0o zb`!p#Q@CP&N+A!3HH87=My=85?78(-!BIM-A9#J*(oCNfuT;LBrCF31A84@ggNXqp zO!-=`*BVbgx##wi*|o+CsC;Y4%En)i>}z;LqZG#_7aT4ScQNkHOPNGy8nLC3f1KT_ zHJ*89&z(!*T6Ify>t9QKU-@Qk#i@HcShQ|E5H*G&T-9=`5griTTOV;xPdP{=PRX)J zw73sNm$=LE+>2kz?;Rrj>|}S;dW+62ci--d?d02G_T|zxLYum+Ge7lh<=y|ky}JRE zlzq=u3XBIQF z2dr#anFFa8JMrTrZC;$XPim)ilQ?aYw07P4N!>bm+B8Y)w&~NRX`}W@+Qv=#mb6aN zHg$h9yR$R1JA1o#1j)lbUp@kPH}ik_{XhSA9Iz^57GBM7IdS61)`T=*+3A#=Crs6` z9M>!!Vv^ONFzT~2!HF}21tiNDcEY)`4cDD$Ll)xliEdP~>#T1?gVJ5gTkl!xg_Oku z#|h;Pk|n3_ulrshS#m@i6}Yl|u^eo-7jEn%+P0v`CeZ7o%WG=1y^5^Fo0i=-JAOME z7FB*j2r2QV+#bC30sRbZ%eMWV+`lZjK5aTt-?Us6L^CoK*Yre64ws#7dZ!x^U0k_- zy>)HbT)up@_3HA4t1qX6qIiATQO3DWl!u%A{GZ>sKmY**5I_I{1Q0*~0R#|0VDSaG z|1W+Y(?SFgKmY**5I_I{1Q0*~0R*`JGX@Yq009ILKmY**5I_I{1Q1w!0q*~c-^a8N z0R#|0009ILKmY**5I_I{?*EJd1Q0*~0R#|0009ILKmY**7GHq-|Kj&CEkpnT1Q0*~ z0R#|0009ILK!E!{V*mjJ5I_I{1Q0*~0R#|00D;99;Qqh(eM}1xKmY**5I_I{1Q0*~ z0R#}>{?8ad009ILKmY**5I_I{1Q0-A@ddd5FMc1>LIe;%009ILKmY**5I_I{1i1e* z1`t310R#|0009ILKmY**5LkSHV@Fl(X#L4r{mCO=KlDBAzj)x;eZO)12OjJm`}L!L z`{;zOE#_H6x~7b0ubRbj8&b^B%%SYBw^k>h(|#iQEKUA*49 zu-r11FFb#xWi%?1G>nrCr`<3dFH-9&FuH-$v4XyFQ}qq27x|872dbmI$nbpm)pOlb zMk9`5P17HJw0`$o9*v`dK@7LAXMXGIDQNw7< zSCP|E5?`;Id^7P_bu7mnl_Tl88@`tpEDx-9J5ZqnIjl<_Nwbwah}4~^`HlF1rnerc z$B#K)TippIX(DgBRv4K{H*7jl-z>eW{p=9p^0iB?*QE=#$D!^s$*0bpYZ{kYU%jw$rBFy(ZPF7rMtw3$baCbS z_13jzbNTYs)~m}GuD+aB8`>Mb@0xvO1vfJKlTV$lDqF^47^y_EBebSXN?t7Li!@-uWHY5M0^1|w; zwclUruSRvMUCo%Z+GL?HqkcCZ;>Ed>Cn?VV!Ym)mx@6!_WE)M8;u~w#ah=G?W{b?7 zboh$Yf6eW@?_Q1bk%Q5Q61HGdY!bBCnj>BY^Uo;E5jS<`Zt=zqxHhJr@dL7{>q9X%ic)# zl&R&0krnmiZMM*yeX3$DP(k1armP2{wJw0wZ3vT2T9t-o8{A|gIx4bczp$nik&nuG z7PQ07ceHP4`m@j0zx`M`G9x9em1R|$t-QA7*~(06oAS8!d&^^VO0xL}sx6Z+G(#B> z$;6mG8ncR~r|9~jOmE*CI+eDDWWOEgjE(V$_4+Bf@cxdy3$PCo2p6p`jX{q>q z?Ptn;pB&eU@8|FI;qH`=`YhUQ7Kl=k@~kSo8lRiR4jnkvD~BZPT7B2Ize-NGxhkT6r}nDXR6V0XK?tePFDK^Lx|y3p*qz>VS}@2b>hS@ue{NW zKN~M;`l}M}i`&JU9t}-7PRZD-F1Ys34`W@qcKMZ+ma%=v#qwr_o2HdWSL2$df8&|@ z*26O+9i4Lrr|tAxM{X*~9c@bZ|J+>E`4KreMOTzQS7S!uHDvhZMarqW$;}1`t310R#|0009ILKmY**5LkQx z?*EJ5$FvXu1Q0*~0R#|0009ILKmY;m|BL|y5I_I{1Q0*~0R#|0009IRUx2^=zxaJj z3lTs70R#|0009ILKmY**5a9mL7(f641Q0*~0R#|0009ILKw$9&xc@JHAJakv5I_I{ z1Q0*~0R#|0009KJ|1$;9{kR+pFP?- z{EdVE$HC8NzxTjDIq;hY)C0fvz}@}7vwyAr^Gmlr`<3dFH-9&FuH-$v4XyFQ}qq27x|872dX3eXL!E+>bdSI zqfyo=WS@AKhHh+HfxTe`#Uje%D63xb7haSy%PT)>q`Sqo@tIkm`rZjk+Z30BtDarEmf*e63Pse@5IN7RNJ(ohS8R< zBB!I|aj%UTs_{Y*djFe?>s36S#Tl*8`{>q*wGq1orXD{s}uv+|4 zO>aF`j~`2Cg`LD@y1sogG}WE16IhX~Is^N{yV}1}p0kBl6WIpiy^tblR9<$Y`NjAn z2lUI2)oW=4;VsvRlsOoKvILrioaULrH??1s)$;o^{jtaDTgOw?!pp*!wST#6TX>a3 zA_KlrHBkNAo(d}86k^ZD?`U|PDs|DI^k#D-en``w(d+S%0&-qVfw%TARl$xYEs&05 zel9+KKz~l(9&^B0`{VIL@q?PKOJLu~0aKsaFI1py$F*s>y<`%}+$dla`*}{Q_;`Fw z(_eV99-keIhhDeq_GJ{vI^_qBk{wW%MYHEQw|XiZe_8waVcg}lc_Ll0kb&kzlBBhR z6*#^eAJz0HpR8}4Pnjl?PCTgni)Frv1WDmLp4g>zv8cS0rReU#_=u*TJ6C`AOxl=k zFIl5QWe!)_NOi)hXSF|8Zc=*E9423tz_YEo42YGjO`OL+#=KVLs{#+UPlFvJ@Ow*xud?g6d%^~&wsxD zt>-HGVyqj&+P|<}56H%}iy_tZO=-_+VWrEgon*_+`yzQ9Sy4}_8Q;}&fOH6{cCuFv zMpLpC4t7U*B&(RRldZAzH0-TLew6IiGhid@&g?XEm9*=9TpH1q^Vh`ozV%9cNYl@r zt-td^ae;^e%L|j^g`C@mqvW~ zBO|}!;cC90O3GbZH{-YCgXx*+?&CSCtp92A50kj$%#IAAPib_gt~HLDt+Lto@P7_6GO3!8=&Lqjx7!S^$VRp@RtuQjvqq(f#g9B3buJ*G-h~ecndu~?b z8Sd@FQj?)kO4Mlb^Z)njcQYs#0R#|0009ILKmY**5I_Kdy(7T=fA91KksyEo0tg_0 z00IagfB*srAn<+(98L;;=>004auGlP0R#|0009ILKmY**5I|rr2=Md&z0eE9fB*sr zAb5I_I{1Q0*~0R#|00D-+Az@Pu$3%x)L2q1s} z0tg_000IagfB*sryuSk6|KDFl0tg_0 z00IagfB*srAb#&|M%CI(h)!a0R#|0009ILKmY**5ZDU>-2eANFAxI)2q1s} z0tg_000IagfB*vTuK<7l|NZr)bOaDU009ILKmY**5I_I{1onbJa{oV8`_t-Q!|+Gd_6J{>=<>2Ez-k6+Cm-u+Vin5IAbZ2emg zMAoXS^0ETeNfHHNQr?uuwO_q>y>(%^Wh`HK{z}VeOi9)-PBxr&!*IMvt*gN322RHc z`o>MwH>_UdJDx4INw|jR%dej6o+_4A%vwm0H*r*QlFbSR?Jhpd+G0liu4)*YR$y;f z!KgN)t=6ob8yP1$me;e~6H<;O@I2*im)H^TZZ}CB_Ez6kb~H*Xk2)$0t#zp^Qg@;x zNfMPMN!nJVBB!IWXRG~Uqyi!D+v6#F-6UK2aWj5A{)DE#ai$)>n0BYDHkE5iAFKyb zxr~R<^rUig@H`s{lc{Qddf1DXuU%@rZZsxSRF%^(e6K2P(%GeI8_oEKVqMcs3G}sT zf!e(w?YEh+{?g1?tBcw$-0>U|@ouMBc^cOKt#Y*a{Fpvd2Z2_kOkfLRz8QZu{{y&SLUL^X?DEhNhoATYvYhu|;MSL!M#u zsP@aluqGClQIdPNuq3l_v}6`1WG_^KtfIw7mYw!;^;#kYtvSk-48v7M9{6tC+_cYpu#I<n6&uka&u!?kNoZ8}K>{di| zCjNneq#TFjP}jRvfXvED8kK#W7crciwSRpW+RC-dudK9;;ch;)pgBOZx@7%JS1dV9 z6nUFJ`9S<|8uahi(}g`tKM~v17qx$N81!(FpPI0&R#=edJ56Tg7Bd#klKFJZ4ynl! zIy{pUo1CrcqeZyNr7BTYSa!fEZYTpTS>~%=mNR-*r?iWVWvzalTn)0JIsNP_9A=<4 z9d%o^lS6IS3PXvhRKILZj@OrS{K+#-NwVg6mTOv@DoBsIUazA9$CejX6e+K5dA2hB zn`4=j>#RGgj_X8yQ|c`2cZC%_P`7$cpky1(D;$~U=k+s5poMJnwNg?Aa+)5?TNoN? z>(le71n9_qUOsu3>uf8l?K+-xOL9gpzZ=#xew;5vZ2O(AtH#gvNdTjxTdM4QeKH8g zTk}oZ_tu=`kT8Slbkf|mZ%ZF}QE2wMu5Y#H%Vo)hBE6rPmTjx91UpY%U8|oS6h5I_I{1Q0*~0R#|0;C>L`{(nF85-}ox00IagfB*srAb&HHKD|7q7Q2EVqp1 z3(sF^88<4D+%QfazTvcQ7>*aIbrl#dU%!0y!u2A&g}I% zt17r*Y+8Z6VFf48o^F;Chf%MsyvQt;T%E$RIw~*dsng>ntp`@Skl;*`;8N?W7gnx} z#b&kzry@Q(P!f0B+&)#Wo3w75REJL)jZGB{Mn)2U!l^H6|6XZGHKrzP7$+M}yD=J9-N5NsLEpHk`i9kue8-bE zCsi7rFTZ-OE3vNye#b2I)hO~}qR5|_BrPW$Ii{;Nl`H)?ibSfB5~jb*T4Y;Y%XXr^ z*$Y*kyBtVlx#n=FhocHcNsP8yvwCi1oH%_#Y8@5q_q=FkYRg^s11H+(Od90nhBEV& z^qD7v&~h!$mcg-kCaKK!y)|dO7g*^q4n38RUg_1kPn zuJ%W8P)MZb{pcVVVr|q}7oX^#o_QR8-)!18XfxV;)U{QVIE}&mTAA z)A8e){?@bg_*KVit2s!YL zk&mwz>B=hDNneZgoouX6EG)Hue=Ou#B@H5-T3%v2v1-V$8(Spuf!Mt3#!XFs^2z$Q z9~i8G!>y{VY$ud8Gt9bd^q}_Z!rIQBin>Mxm#t8&bQ?1!a~0n*iulkl#Ie36?Y<8;ZLN;23g%z zI7pX%zpgwbt5UjvY`1M@%g=k=5u0}(j!$a((@)pmeJSnlRpt7!<~es{Y0p|4Rz9r# zyXAhbOmvU?Ube(ADjuzpLrSUR3)^GfNfW8EmgPyR`*FN`l6>tCu5;eK9-q+kkA1BE zQ-jI4=348?uiBq4(h1E-tp ziG}G`D8zPsIoWJeOqyUyIo9BO99eR{oKk#p9C5d=AWbr*hH!E!jCW7vRCuQ9elR*& zNUYKoBHihxSDVq2b#dkT_13jzbNTYs)~m}GuD+ahV#bxMLi1)k+~nu~d-o0Y0tg_0 z00IagfB*srAbI~{y<) zt~$z#OtmTh4!3((`wuQ&Z(Ue!8Os-*ztS=q+of$7CmT+?VK`o-)>UA11E*sJedDI; z8&)s!9nTKZdJWHy46o<9r;J8d1)=YmQNODi#-Tqj66=XP zN1>+tp=WjSV&rk)yUJV-tajUbgLP?oYm~AVPl*zwGG03|@qeV07g`hukrtMmRv*y@V${`~u7(N}2s-!zsLzN6<+wXZ%KHRcFl|!FSP+ftXGpVuyw{Nzcu;YZG zw06D<2BS&cu~j$e$T@S_erMJ3(yo^r^pt8R8S?2P_)4Zlr0zsXOwuyf zY%ALdMWmR{>a`u|vtc?}52Sb6rX`OG1HRWyzL|I&+8e&_ntkb}Vv(V&DWQy$(X(d! zqwyCt{dTLqr8!<(-6_o9iN(*%B{DyAW~SPIJX{qoU%S+Ly|h5gUqQq0z3sCm3+iNe zjb?l`KBwuORy_`9hAN|?uq@5dw789XchIWwg6C^ao=e*C<@hO0x1}Ak zLo>52c6QR*f3$;El#89O4Ot#(!KwI+raztcL`My`>ui=d?QIu}+Fu%rwW@&mqM95i zvTlw~i&ODwO+PE~-W-e^scv{)N)|{t!E7Jxe>fTKSoZm1EuPUN%Hg+iev)rWBV;B1 zAx(edYJKZ?8gJLN?7=~E)r#y5u}%+hrWJ54~KNg?T^s86v-~D8IC72cFtoOCw7&c>gHJFutSJ#74 zV@9`%?3gEGuh6>fURU2OlKg%zkaJ{qPDyVB`7v|&R(W!XaEF6+blQ~MgF{Ye`+*$S zyJ(kk*ieof62dM46`KC03IZoNsm?xLW@$%_*yh03P2q1s}0tg_000IagfWTfB;QqgtdxzK%KmY**5I_I{1Q0*~ z0R#}ZHv-)M?~Nm+AbX9udIyvXo; z`PFmXQ%1vzBIUI$&sJu~@~rhVfvGknnOP`IQsx!9{`%OvFzU4@b7W6FtD_pmrWM#5 zRv;y%E%kbxRTUH;58JNVrWG}ewwOdtM-_w3Q>^w!?UVuoWre<1QI=`@Jx>bDNV0|- zZMA0g+{id_=7i)I7pbjWuRQK`lOQG@hxUf=yJlZmrSL)$qeT{8{CEDS|+yM(p> zb~v&wU%S+L-Du2RUc>OcsmYV^Scss}j8DfGH2p0J`_=8Dkx(~%M^+rCy?xaG&3sXh z7dJELJcGpiMEn&^e^X+9Y1^2)RxnuMwh#Gl%op-lX)_}prEfIf(c<%({_L~$Z$Fl< zDpUG$%H!I9U0!3RB-_t4%w}|p2A1xI5G9(i~R(CIw$*kJx zB0l)W?5JqNmu)cLZ@PisaYAKAzS*_iVu@_hq8p$=}JcCZ~;Iim>+A%Of^VH(ADDnC2)7G#GA*;zZ=w&TB=)sFF!X16BGAsbxM4%Z4pGgRsBksN^J zS!R)bT{sss-+eItVNHMTx%zjXNM~u5P?qcx|Fq|||FS$!r={D~L@l%{n~I|umNaV6 zkn*-Z(fZ1q<7c3XVoT?y;`{Z$YCGP#DN}OIag*I)G@m4>@weF!D$MiZKuZT-_HI;5 zzI=pH;Ct)ox=|RiW5aW1eseQDKRRBw7nyITXVC0mP$;$ZqTG=UIfrK}O&0#dk)!aU zcrs3g#F1uF*8@~KPk6lcQp)JyItPIJ>Snp0XCZa`TzUz_Y#O1 z0R#|0009ILKmY**5I_Kdoe6ON- z2q1s}0tg_000IagfB*tJ6G-m=hmU=ucKk0K|Ec3&JO0rJ|N4W!@ZgU=s2}^w@{oTJ zKmY**5I_I{1Q0*~0R--iz>D#t57gp^4?T7IsV|zR<$q6`XP!DO|Cm-=K7q=%+N$H& z=_j5fA4nn}Wm5fjyayjPaXJ2!l=iV=X-Qe9&!nXlK66Dr&SLPXRY{41k9d%eUsw;6 zeA0z{+Lj+g;q&n$QkFJR)*!NCSyDvUTYXzTAj7nKfqZ;Q&+W-)c0{TZUW$KE3OSM& za?U(`CW$BwNIuKKS##{cC#gjKO(mZ~CE=(b8a!4KOY&h%;o10;`)fB#r6h%%HbaTj z^sLQ6ZKmaRH>_3p{4n|G1h@Zm`~&-IVMVUN$0w9NMag#kuqU67GH`qWMQ!i-xGtVsj)NAQ3SbdhLetNz1QCByr z6WuUQ9=_qUZ;U>6>gDT~uU@$RhVf$S4ddEN%f_{pD_2e(HjH8Z{ByeUEah~Ovg{2N z$fpWTOL1GNo2Go;WDwm*KaaGWKYc47o?7*8cn-ReeS*}^@JJLhEU3;1f3Uijsgz!h-~Z&!8+Os|tM3o9V|@7OF@;RWAkXx|UZJY}o52$4}E!NS*)Hk) zQ)cwZjrmNN(8xbDE>Bb*75V4i75pd0&hWyqO_EJV-A+GtcM5wtazkg`n>SJNS+s@v zDu+Z`o-794JmD91JajIuT)*DBwrnn6zS??q`NGwg(@^uz5w2*^M3%$NTCM)u=~BAH zQfk~_DJ^eo3%Zmxw|?TQnttxw(z}nQqyF}W6@^x}D<7P4@cq&;*N(lzLUDerRVm);o?`G-jF_B{CKCFVk599-H!%#lBy|5dG z$v$We4=w55mIXOFf>bO!{Q3VyKcmw=1Q0*~0R#|0009ILKmY**{?8ad009ILKmY**5I_I{ z1Q0-A@ddd5FMc1>LIe;%009ILKmY**5I_I{1i1e*1`t310R#|0009ILKmY**5LkQx z?*EJ5$FvXu1Q0*~0R#|0009ILKmY;m|BL|y5I_I{1Q0*~0R#|0009IRUx5Grzs2uk zT8IDw2q1s}0tg_000IagfIxEpKVCaotN)$)!^f{2-FHMkeC^QI!9TA3747JOuRL&T z|Ih6E8F^j1+lgP%^z-NI-&&8XRaZIQrtjFwbi6RKT(_gV$n3h77fyXq`(G|zZ(Ue! z8Os-*ztS=qQxi6flMSccFdQ#Z>nbq1fzz>qzHw9a4XYRVj%NodsnGCz`PFmXQ-#tB z84ItwR**zkEVl4Av^RX;HT%j68pfs-*c(-x9NUe}c1RcLzZPE>f9lq8Lvo}u==;V`&-?NaM?qp{<18;0*q&6bRviijG` z_%rcEO<$4bT-+`?b3ng+3;yPOEhrW>Gw5NLH{&0VUp}bEM_#CJJy{BL)soRD{^hSA!w9KWdPwy_2Sll{{xlM!BRR8}SR8{+0~6r%Mef?!IQvb8hvN zIn0)v9>eVyg19{3}hTJh;EiD@3SEj7Y@l()Py4rtNz2uE0GslZ7uM=ablUbK%Xf*lr|NI^h z0tg_000IagfB*srAbfB*srAb|7Rp1fB*srAbfB*sr zAb=*h$=X9pFV~K}dh}-x{@sJmYTrEY>-+!WzJI*$i}jyg`g=<+ANj)J z8^=#Qc;y3@iqFNirkm&M@oSFPR(IAM&+=?#dVb`rIkpu!zGu4rdT4g7KzWhrw9V}k zF4ZnxZ(Ue!8JDkJYQ1hW<}R^e_}(_j8^+1TD27He{zUwiroVKq9zUJN<9M6CBY`_! z7+J2{kyt}hzKnX|AmFM5wg0;uYgL+3A%hsJk|p5{s*~7075}KFzbdhvpB`Jgr_8n$ zB@v5zRgtxSJV$ipVz!SjODhq6CVo@XUy}%5m|DFAHY6Q(`RMxsaqSd_B*RuPUSe?P2sQ*Dk-ZGMZymNe2{TW$rxwQ$`~zqtU#3JAPf$&z-A( z`*>unx|JbT#ZvpfhG8#Xc>YSuXjG+1CXmx^7>*aIbrl%h!0A{)-?*vzhSiIF$Fl>K zOlw2dCd2Ewu7p;Y?OE67C6v--L%H(xsD-223wcMg#z>}Xv39HD_hd75trahML0uGLQ# zOJS968Jveq`L91X(j~{a;rrp9Jo>&meeM4(`_D)|QT(72X5>mZMoHR@=GJQb&uaRM zFV^3AChdvQyXqKcysrH}VqX=v!a|b4J~DF)$(E0? z1;2V_KNr7RyaJ4ZEGQ@ElZh+9)P%MFtvpz!CLF(1%rRB-(Vwj8*XLW)M*&Pf$qrWk z+W$IlAoc5HpgOmYM8|IQN_{$0r-!%khn!tYsskIoGm(veUL@hsOck4%(GvY&7}#{|Ed# z8&rn?0tg_000IagfB*srAb`M!Ai(|qLqMlW1Q0*~0R#|0009ILKmY**K41av{~s_o z)ggcY0tg_000IagfB*srAn+jw982na=tHQODiJ^c0R#|0009ILKmY**5J2Dq6X56n z9~d5I_I{1Q0*~0R#|0-~$uj{{Mkd zQxO6PAbj^i+%h0tg_000IagfB*srAb`LJCcw}CKQL-4 zLI42-5I_I{1Q0*~0R#|0;6oGO{{NxTQ!xSvAbm-I3R;N1M|^+IlwbYWn%}^>_9~ z)~c)49MAG>Wd_PtPB${+OYtpDSLf@o?|5x>XZ(GrZuOL82+h#m@O{_pD=RR2o+FQ^ zrd>LI@p|jRa?4n`cKMZ+mT~#orPk|4V`}<}vKxl)ZIh#6oNN>VYBXoHbJUKhK`cEu z40`#(^H*9k>UP=vdLDkOz=elcznCq?IVFNVQEXY8Y+# zDsnnX9{0M*H|598_(x)2)BUUUt$n3oAVoWCj%`Jb@0m%ADj1B0t|ebYrqdo93)3!!4p$J{7x~{)WW<;`R}lp`iA{S#ckpH|}C_bHqGIF7ZAY-_-Os zCEk~2#4Ags=P7qqz=!4yxK!R85f9T#$XDaHWs#JSZ_fz1)Voo?t7Zj%aNgjjlsrc> zCUQ!HF2#PtPDNSsw@!OTWHFmEHq zN}MD7Q4R_GOYz!#YiHN$&pD3v&l~x8k#mHeXGvpM^TqCXo4zB*=h^D}=8b*4$T?!q zv!t=N=ZigzdhOwvZTq3Mv@~z*<3-L9d!8j7?Dl+veRLR_we-~H4SlS{IU*nBkOO5q zUQKUR-&vV{piCB=%9CXBqV_w(K*w$y6HC@TIy)w(uu=byt`73FFqX1&raUdoj>6M? zx)&afFJXmOW9xL`O{tm5203~2ktnjAcbi*Latuz6m~x+Wx{eI*Q9<%B?5)17?DC`Z z@H0wOaY&YnZBMn+V{0;7#%@n*Qs3C+Nh(r7;0I<$g`u@BO^M`|mV`gNfUWk6;Mvo$ zf-A3O{QQ5~?`!ZJ0R#|0009ILKmY**5I_Kd#S!5Czc_tKOAtT+0R#|0009ILKmY** z5SS*w{ePMa&k;ZX0R#|0009ILKmY**5Lg@m?*EI^m$U={1Q0*~0R#|0009ILKmdVh z0^I+n$?zNj1Q0*~0R#|0009ILKmdWo5#av6IDJV=5I_I{1Q0*~0R#|0009ILm?pse zf0_)>5kLR|1Q0*~0R#|0009ILSR4WF|BKU?v;+YJ5I_I{1Q0*~0R#|00D)-&-2bP^ z@EidI5I_I{1Q0*~0R#|00D;93IF>Z!p~YzrEkOVQ1Q0*~0R#|0009ILKmdVB0e=2J zX~Gi(5I_I{1Q0*~0R#|0009IRO@N>OFItb%9t03T009ILKmY**5I_I{1SSRe`TwK| zPY^%=0R#|0009ILKmY**5Lh$;e*V8`JxY5JKmY**5I_I{1Q0*~0R#}36gYP1+qH+5 z4%Ci)^573^?E~L<;DP<$-hXu8JN4gL`g2PMj{fA4e{lFS$3O9)eb_(r8KovsxqFETCL_Iq9wn#1p%wpsOh>5~_)w=OKVjLX+9wO%(GGjcZ! z-MwH>_UdJDwe=j`WV<`SPphx)RQ)fAV~Vx5Z$MO)IcBte}|N?<&vn*3DJR zwLDujq|}ZI9D9_;Xsb1==SIef(nzrkQl3}$!7>31nOP?r@!Q#8|e1k47rbtHLsBjs5`{O&B z{(=N?VloIv;ur!vfPFC$%9W|VFMZ~3#4Qnn>xndHtkhQx0B!SGRDDG+cHW|xDM`Mn&6n(kk% z$G7KeW>(AGE&Rw1TR2|9{Ef?V%CvbQ4mADh)%x9U4mYChJ0icv9$ECnvOIGC1OfqMe&t8D# z?9-ORPqGAuelL(N4tt&C#AUu6`d-6`)SXD)dFr<8wVS>p2?|Xstgoy6(c!D`bZmvL zKJ$Du{)VQ%B!}Ini{oW<`Yw*Jssu|vI5|X$r}v5c+3J#ZR8~#W*@cv{O76K!!KL`t zZs+9G`MY#{h*v(fOif=LnZx-$;GCLc0#+_*ze~<_=j-q6OQ-MDoKs&cJy@Q=QxjIs z;P}i|P1E0!_372(aGF>?vRhWZ>s2okOOIBLznSHghT%l&B=r1V(~R5kH#OaUuD*44 zuAmE7x~a?Hrz*RCTJnseQ1VV%AdAAp;wR6>*19Ht{(qOhhe0+35I_I{1Q0*~0R#|0 z009K*m5h?-*Ab3-B6#)bgKmY**5I_I{1Q0*~fn5r4|KBAX*$_Yg0R#|0009ILKmY**5V(&7 z`1}9&QD+e<0tg_000IagfB*srAbMTM<009ILKmY**5I_I{1Q0-AmjcI*xV4Yf;@aV#KlC>bJ$2C2e)+&3JMg&&e&vBL z?f*Cb(9yGYE%9lnxVbn`>xqnR$$Idy>$BG_11;umT~#orPk|4V`n8d4By*6 zPs2FbCzGcYIu&DNTFRv-%3)7SH#zDCflo_6IW8`29o6*pv-LO} zXsaM{)*Rc4lne^n589!*se-{H$M?+0zo|U4=Q+1}%AQQTbZQv*%C*a{tc=EYMat>L z4(%rkPokd;M*NR&9nthx9;wIArj2mCP2aIqXs%jrI$NW_^1`)bShojrGfT1b`Lc8N ze0t77a9O$}qKZly&HmP5O~3Hzdi=>WBCFSSBGdKPlc|=djv~q5EPbvVOa5j&&me~U zeGv9=>!7ATE7cz# zuimnwWC)EtUOG9o>SBhfY6tHd&2UT8^yj3?kB*gX_QFE1=VD7Irq)@?QB~>%A>49|}Yujjg_j7C;>mP;ND4UA1Ius5urn7Ys_*=xy=kCZ|wzS}l8Ew`r{ zMx^dUlFU=L$DVXlWVNlx8YPhcWu0i7R@5-$g^HYxYQ|sKdO*`nSqxuGJELm_$u2oQ zOF}ctz1km)h-nFz9xwORv~&{%4mxjI^29$6K^Bp(Z0(noP~u+SKJEl&IR8hpcv>Y( zkIfl-RiU%vFXxgroZQ+cyS%hvu***i%M2qc>V-4h8*|29ENphngVd>aeJ=0FpWNl9 zHEikAbH-d%=DC6UNG$9s4MQ@VDb{YP$Vgee3M@{KGuy*?e$xnHu+} zhH;nAFVm8nlO=E8g81WGAJg=g&eY?l(~#ZdpcT4KH#vd_vKNe>!6y?eoh#E$rl>r7 zPbN!N!0f;;n2&CKzoxH9WEZDKmLGZtOUGoHOJA5B+;}MivB|R1`0~UO;)7cs)$|vi zsK-yHA+B2Ob!DnMw(2Gai*dT~*Gp&1v5mj4WE)R3hNf?QL{6R((ct7co_cc5kEd9A zYF1FWzyN)~HD1mn)|a=wPbP<4UI*E>zhai|ecL5nI#Z5!yR?;{+hr)@^7H>`zcImc z1Q0*~0R#|0009ILKmY**7DwP%(v*i5r#-X;0R#|0009ILKmY**5I_I{1SSRe`TwK| zPY^%=0R#|0009ILKmY**5Lh$;?*EI{qqGMB1Q0*~0R#|0009ILKmdVB0q*~kCOkm^ z0R#|0009ILKmY**5I|tj1i1e%T948m1Q0*~0R#|0009ILKmY**CIz_vPnz%q0R#|0 z009ILKmY**5I_KdMHAruzi2&5dk{bX0R#|0009ILKmY**5SSF;{y%BL69f=I009IL zKmY**5I_I{1Qtzz`~RZ#DD6Q20R#|0009ILKmY**5I|s3fcyWX2~Q9}009ILKmY** z5I_I{1Q1v>0q*~c)}yos0R#|0009ILKmY**5I_KdNdfNvlO{Yt009ILKmY**5I_I{ z1Q0-A(FC~vFItb%9t03T009ILKmY**5I_I{1SSQz|4*9m1OWsPKmY**5I_I{1Q0*~ zfkhMG{=aBFN_!AM009ILKmY**5I_I{1Q3`M;Ql{p!V?4#KmY**5I_I{1Q0*~0R$FJ G;Qt3@;ff3Z diff --git a/database/database.sqlite-journal b/database/database.sqlite-journal deleted file mode 100644 index aa4a8e4006503f342f4b1fde048440f50c869999..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4616 zcmeI$u?@f=3 + * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\ClassSection> */ class ClassSectionFactory extends Factory { @@ -19,7 +19,7 @@ class ClassSectionFactory extends Factory return [ 'class_id' => fake()->numberBetween(1, 20), 'class_section_id' => $sectionId, - 'class_section_name' => (string) $sectionId.fake()->randomElement(['A', 'B', 'C']), + 'class_section_name' => (string) $sectionId . fake()->randomElement(['A', 'B', 'C']), 'semester' => 'Fall', 'school_year' => '2025-2026', ]; diff --git a/database/factories/StudentFactory.php b/database/factories/StudentFactory.php index 4bd700bd..e50def9f 100644 --- a/database/factories/StudentFactory.php +++ b/database/factories/StudentFactory.php @@ -6,7 +6,7 @@ use App\Models\Student; use Illuminate\Database\Eloquent\Factories\Factory; /** - * @extends Factory + * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Student> */ class StudentFactory extends Factory { @@ -15,13 +15,13 @@ class StudentFactory extends Factory public function definition(): array { return [ - 'school_id' => 'S-'.fake()->numberBetween(100, 999), + 'school_id' => 'S-' . fake()->numberBetween(100, 999), 'firstname' => fake()->firstName(), 'lastname' => fake()->lastName(), 'age' => fake()->numberBetween(6, 18), 'gender' => fake()->randomElement(['Male', 'Female']), 'is_active' => 1, - 'registration_grade' => 'Grade '.fake()->numberBetween(1, 8), + 'registration_grade' => 'Grade ' . fake()->numberBetween(1, 8), 'is_new' => 1, 'photo_consent' => 1, 'parent_id' => fake()->numberBetween(1, 50), diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php index 6da14826..5212a445 100644 --- a/database/factories/UserFactory.php +++ b/database/factories/UserFactory.php @@ -2,12 +2,12 @@ namespace Database\Factories; -use App\Models\User; use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Support\Facades\Hash; +use Illuminate\Support\Str; /** - * @extends Factory + * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\User> */ class UserFactory extends Factory { diff --git a/database/migrations/0001_01_01_000000_create_users_table.php b/database/migrations/0001_01_01_000000_create_users_table.php index e3adbe14..eaafe5d6 100644 --- a/database/migrations/0001_01_01_000000_create_users_table.php +++ b/database/migrations/0001_01_01_000000_create_users_table.php @@ -1,6 +1,7 @@ string('email')->nullable()->after('name'); } - if (! Schema::hasColumn('suppliers', 'phone')) { + if (!Schema::hasColumn('suppliers', 'phone')) { $table->string('phone')->nullable()->after('email'); } - if (! Schema::hasColumn('suppliers', 'address')) { + if (!Schema::hasColumn('suppliers', 'address')) { $table->string('address')->nullable()->after('phone'); } - if (! Schema::hasColumn('suppliers', 'notes')) { + if (!Schema::hasColumn('suppliers', 'notes')) { $table->text('notes')->nullable()->after('address'); } }); diff --git a/database/migrations/2026_06_04_120000_create_certificate_records_table.php b/database/migrations/2026_06_04_120000_create_certificate_records_table.php index 4da26ef1..a5593ded 100644 --- a/database/migrations/2026_06_04_120000_create_certificate_records_table.php +++ b/database/migrations/2026_06_04_120000_create_certificate_records_table.php @@ -1,6 +1,5 @@ string('lifecycle_mode', 20)->default('legacy'); } - if (! Schema::hasColumn($table, 'lifecycle_policy_version')) { + if (!Schema::hasColumn($table, 'lifecycle_policy_version')) { $t->string('lifecycle_policy_version', 30)->default('legacy_v1'); } }); } - if (! Schema::hasTable('section_placement_batches')) { + if (!Schema::hasTable('section_placement_batches')) { Schema::create('section_placement_batches', function (Blueprint $table) { $table->id(); $table->string('from_school_year', 25); @@ -45,7 +45,7 @@ return new class extends Migration }); } - if (! Schema::hasTable('section_placement_batch_students')) { + if (!Schema::hasTable('section_placement_batch_students')) { Schema::create('section_placement_batch_students', function (Blueprint $table) { $table->id(); $table->unsignedBigInteger('batch_id'); @@ -70,7 +70,7 @@ return new class extends Migration if (Schema::hasTable('configuration')) { $exists = DB::table('configuration')->where('config_key', 'promotion.section_capacity')->exists(); - if (! $exists) { + if (!$exists) { DB::table('configuration')->insert([ 'config_key' => 'promotion.section_capacity', 'config_value' => '20', diff --git a/database/migrations/2026_06_04_200000_add_badge_scanning_list_nav_item.php b/database/migrations/2026_06_04_200000_add_badge_scanning_list_nav_item.php index 3cdcbf0c..458d827b 100644 --- a/database/migrations/2026_06_04_200000_add_badge_scanning_list_nav_item.php +++ b/database/migrations/2026_06_04_200000_add_badge_scanning_list_nav_item.php @@ -8,7 +8,7 @@ return new class extends Migration { public function up(): void { - if (! Schema::hasTable('nav_items') || ! Schema::hasTable('role_nav_items') || ! Schema::hasTable('roles')) { + if (!Schema::hasTable('nav_items') || !Schema::hasTable('role_nav_items') || !Schema::hasTable('roles')) { return; } @@ -26,7 +26,7 @@ return new class extends Migration ->where('url', $url) ->value('id'); - if (! $navId) { + if (!$navId) { $navId = DB::table('nav_items')->insertGetId([ 'menu_parent_id' => $parentId ?: null, 'label' => $label, @@ -70,7 +70,7 @@ return new class extends Migration ->where('nav_item_id', $navId) ->exists(); - if (! $exists) { + if (!$exists) { DB::table('role_nav_items')->insert([ 'role_id' => $roleId, 'nav_item_id' => $navId, @@ -83,7 +83,7 @@ return new class extends Migration public function down(): void { - if (! Schema::hasTable('nav_items')) { + if (!Schema::hasTable('nav_items')) { return; } diff --git a/database/migrations/2026_06_04_210000_add_strong_grading_backward_compatibility.php b/database/migrations/2026_06_04_210000_add_strong_grading_backward_compatibility.php index 639e67f3..f54c8795 100644 --- a/database/migrations/2026_06_04_210000_add_strong_grading_backward_compatibility.php +++ b/database/migrations/2026_06_04_210000_add_strong_grading_backward_compatibility.php @@ -10,24 +10,24 @@ return new class extends Migration public function up(): void { foreach (['homework', 'quiz', 'project', 'participation', 'midterm_exam', 'final_exam'] as $table) { - if (! Schema::hasTable($table)) { + if (!Schema::hasTable($table)) { continue; } Schema::table($table, function (Blueprint $tableBlueprint) use ($table) { - if (! Schema::hasColumn($table, 'max_points')) { + if (!Schema::hasColumn($table, 'max_points')) { $tableBlueprint->decimal('max_points', 8, 2)->default(100)->after('score'); } - if (! Schema::hasColumn($table, 'status')) { + if (!Schema::hasColumn($table, 'status')) { $tableBlueprint->string('status', 30)->nullable()->after('max_points'); } - if (! Schema::hasColumn($table, 'excused_reason')) { + if (!Schema::hasColumn($table, 'excused_reason')) { $tableBlueprint->text('excused_reason')->nullable()->after('status'); } - if (! Schema::hasColumn($table, 'locked_at')) { + if (!Schema::hasColumn($table, 'locked_at')) { $tableBlueprint->timestamp('locked_at')->nullable()->after('excused_reason'); } - if (! Schema::hasColumn($table, 'locked_by')) { + if (!Schema::hasColumn($table, 'locked_by')) { $tableBlueprint->unsignedBigInteger('locked_by')->nullable()->after('locked_at'); } }); @@ -41,13 +41,13 @@ return new class extends Migration if (Schema::hasTable('semester_scores')) { Schema::table('semester_scores', function (Blueprint $table) { - if (! Schema::hasColumn('semester_scores', 'calculation_mode')) { + if (!Schema::hasColumn('semester_scores', 'calculation_mode')) { $table->string('calculation_mode', 30)->default('legacy')->after('semester_score'); } - if (! Schema::hasColumn('semester_scores', 'calculation_policy_version')) { + if (!Schema::hasColumn('semester_scores', 'calculation_policy_version')) { $table->string('calculation_policy_version', 50)->default('legacy_v1')->after('calculation_mode'); } - if (! Schema::hasColumn('semester_scores', 'snapshot_id')) { + if (!Schema::hasColumn('semester_scores', 'snapshot_id')) { $table->unsignedBigInteger('snapshot_id')->nullable()->after('calculation_policy_version'); } }); @@ -56,7 +56,7 @@ return new class extends Migration DB::table('semester_scores')->whereNull('calculation_policy_version')->update(['calculation_policy_version' => 'legacy_v1']); } - if (! Schema::hasTable('semester_score_snapshots')) { + if (!Schema::hasTable('semester_score_snapshots')) { Schema::create('semester_score_snapshots', function (Blueprint $table) { $table->id(); $table->unsignedBigInteger('student_id'); @@ -96,7 +96,7 @@ return new class extends Migration } foreach (['homework', 'quiz', 'project', 'participation', 'midterm_exam', 'final_exam'] as $tableName) { - if (! Schema::hasTable($tableName)) { + if (!Schema::hasTable($tableName)) { continue; } Schema::table($tableName, function (Blueprint $table) use ($tableName) { diff --git a/database/migrations/2026_06_04_230000_add_finance_lifecycle_tables.php b/database/migrations/2026_06_04_230000_add_finance_lifecycle_tables.php index 140c4e7f..9bb7c9eb 100644 --- a/database/migrations/2026_06_04_230000_add_finance_lifecycle_tables.php +++ b/database/migrations/2026_06_04_230000_add_finance_lifecycle_tables.php @@ -8,7 +8,7 @@ return new class extends Migration { public function up(): void { - if (! Schema::hasTable('finance_follow_up_notes')) { + if (!Schema::hasTable('finance_follow_up_notes')) { Schema::create('finance_follow_up_notes', function (Blueprint $table) { $table->id(); $table->unsignedBigInteger('parent_id')->index(); @@ -26,7 +26,7 @@ return new class extends Migration }); } - if (! Schema::hasTable('finance_balance_carryforwards')) { + if (!Schema::hasTable('finance_balance_carryforwards')) { Schema::create('finance_balance_carryforwards', function (Blueprint $table) { $table->id(); $table->unsignedBigInteger('parent_id')->index(); @@ -52,7 +52,7 @@ return new class extends Migration }); } - if (! Schema::hasTable('invoice_installment_plans')) { + if (!Schema::hasTable('invoice_installment_plans')) { Schema::create('invoice_installment_plans', function (Blueprint $table) { $table->id(); $table->unsignedBigInteger('invoice_id')->index(); @@ -68,7 +68,7 @@ return new class extends Migration }); } - if (! Schema::hasTable('invoice_installments')) { + if (!Schema::hasTable('invoice_installments')) { Schema::create('invoice_installments', function (Blueprint $table) { $table->id(); $table->unsignedBigInteger('installment_plan_id')->index(); @@ -85,7 +85,7 @@ return new class extends Migration }); } - if (! Schema::hasTable('payment_installment_allocations')) { + if (!Schema::hasTable('payment_installment_allocations')) { Schema::create('payment_installment_allocations', function (Blueprint $table) { $table->id(); $table->unsignedBigInteger('payment_id')->index(); @@ -96,7 +96,7 @@ return new class extends Migration }); } - if (! Schema::hasTable('finance_notification_logs')) { + if (!Schema::hasTable('finance_notification_logs')) { Schema::create('finance_notification_logs', function (Blueprint $table) { $table->id(); $table->unsignedBigInteger('parent_id')->nullable()->index(); @@ -119,7 +119,7 @@ return new class extends Migration }); } - if (! Schema::hasTable('finance_notification_preferences')) { + if (!Schema::hasTable('finance_notification_preferences')) { Schema::create('finance_notification_preferences', function (Blueprint $table) { $table->id(); $table->unsignedBigInteger('parent_id')->unique(); @@ -132,7 +132,7 @@ return new class extends Migration }); } - if (! Schema::hasTable('finance_receipt_sequences')) { + if (!Schema::hasTable('finance_receipt_sequences')) { Schema::create('finance_receipt_sequences', function (Blueprint $table) { $table->id(); $table->string('school_year')->unique(); @@ -142,7 +142,7 @@ return new class extends Migration }); } - if (! Schema::hasTable('payment_carryforward_allocations')) { + if (!Schema::hasTable('payment_carryforward_allocations')) { Schema::create('payment_carryforward_allocations', function (Blueprint $table) { $table->id(); $table->unsignedBigInteger('payment_id')->index(); diff --git a/database/migrations/2026_06_05_000000_create_school_year_closure_tables.php b/database/migrations/2026_06_05_000000_create_school_year_closure_tables.php index 6206ba38..5c0a638a 100644 --- a/database/migrations/2026_06_05_000000_create_school_year_closure_tables.php +++ b/database/migrations/2026_06_05_000000_create_school_year_closure_tables.php @@ -9,7 +9,7 @@ return new class extends Migration { public function up(): void { - if (! Schema::hasTable('school_years')) { + if (!Schema::hasTable('school_years')) { Schema::create('school_years', function (Blueprint $table) { $table->id(); $table->string('name', 50)->unique(); @@ -23,7 +23,7 @@ return new class extends Migration }); } - if (! Schema::hasTable('parent_accounts')) { + if (!Schema::hasTable('parent_accounts')) { Schema::create('parent_accounts', function (Blueprint $table) { $table->id(); $table->unsignedBigInteger('parent_id')->index(); @@ -35,7 +35,7 @@ return new class extends Migration }); } - if (! Schema::hasTable('parent_balance_transfers')) { + if (!Schema::hasTable('parent_balance_transfers')) { Schema::create('parent_balance_transfers', function (Blueprint $table) { $table->id(); $table->unsignedBigInteger('parent_id')->index(); @@ -51,7 +51,7 @@ return new class extends Migration }); } - if (! Schema::hasTable('audit_logs')) { + if (!Schema::hasTable('audit_logs')) { Schema::create('audit_logs', function (Blueprint $table) { $table->id(); $table->unsignedBigInteger('user_id')->nullable()->index(); @@ -65,7 +65,7 @@ return new class extends Migration }); } - if (Schema::hasTable('invoices') && ! Schema::hasColumn('invoices', 'balance_transfer_id')) { + if (Schema::hasTable('invoices') && !Schema::hasColumn('invoices', 'balance_transfer_id')) { Schema::table('invoices', function (Blueprint $table) { $table->unsignedBigInteger('balance_transfer_id')->nullable()->index()->after('semester'); }); @@ -90,7 +90,7 @@ return new class extends Migration private function seedCurrentSchoolYear(): void { - if (! Schema::hasTable('configuration') || ! Schema::hasTable('school_years')) { + if (!Schema::hasTable('configuration') || !Schema::hasTable('school_years')) { return; } @@ -111,7 +111,6 @@ return new class extends Migration 'is_current' => true, 'updated_at' => now(), ]); - return; } diff --git a/database/migrations/2026_06_05_000000_sort_nav_items_alphabetically.php b/database/migrations/2026_06_05_000000_sort_nav_items_alphabetically.php index 505dd65a..4672ca80 100644 --- a/database/migrations/2026_06_05_000000_sort_nav_items_alphabetically.php +++ b/database/migrations/2026_06_05_000000_sort_nav_items_alphabetically.php @@ -8,7 +8,7 @@ return new class extends Migration { public function up(): void { - if (! Schema::hasTable('nav_items')) { + if (!Schema::hasTable('nav_items')) { return; } diff --git a/docs/alrahma_end_to_end_use_case_test_plan.md b/docs/alrahma_end_to_end_use_case_test_plan.md index e57957d3..db4dd862 100644 --- a/docs/alrahma_end_to_end_use_case_test_plan.md +++ b/docs/alrahma_end_to_end_use_case_test_plan.md @@ -2073,3 +2073,383 @@ A use case is done only when: - The test is automated or has a documented manual test script. Anything less is not done. It is merely optimistic. + +--- + +# Part VI: Executable Coverage Gates Added + +## 26. API Route to Use-Case Matrix + +The test suite now includes `tests/Feature/Api/ApiUseCaseCoverageMatrixTest.php`. + +Purpose: + +- Every API route must be assigned to a business use-case domain. +- Every required domain must have at least one route. +- Every protected API route must reject anonymous traffic before business logic is reached. + +This is not a substitute for feature tests. It is a guardrail that prevents undocumented endpoints from quietly entering the system. Silent endpoint creep is how applications become archaeology projects. + +The current executable domains are: + +- Public content and support +- Legacy compatibility +- Preferences, navigation, and UI +- Administration, enrollment, and school years +- Students, parents, and families +- Teachers, staff, and class operations +- Attendance and safety +- Grading, scores, and reporting +- Finance, billing, and inventory +- Communications and messaging +- Competition and public results + +When a new route is added, update the matrix and add or extend the corresponding workflow test. A route without a use case is not an API, it is a liability with a URL. + +## 27. New Workflow Scenarios + +The suite now includes two cross-cutting workflow files: + +- `ScenarioIPublicAuthAndSystemUseCaseTest.php` +- `ScenarioJCrossModuleApiUseCaseSmokeTest.php` + +Scenario I covers the public front door: public content, policies, pages, captcha, contact form, login, current-user lookup, logout, invalid login, and suspended-account handling. + +Scenario J covers broad authenticated module reachability and validation safety across admin, teacher, and parent workflows. It deliberately checks that major modules reject bad input cleanly instead of turning malformed requests into 500 errors, because production users are excellent chaos generators. + +## 28. Expansion Rule for “All APIs and Features” + +For every feature area, keep three layers: + +1. Route coverage matrix: the endpoint belongs to an explicit use-case domain. +2. Cross-module smoke test: authenticated users can reach the module and malformed requests do not crash it. +3. Dedicated end-to-end workflow: the correct role performs the real business action, the wrong role is blocked, and the expected database state is left behind. + +The suite already has Scenarios A through J. Future expansion should add one scenario file per real business journey, not one random test per controller method. Controller-method testing is a map of plumbing. Use-case testing is a map of the school actually running. + +## 29. Expanded Executable Workflow Scenarios + +The suite now adds two deeper workflow files beyond broad smoke coverage: + +- `ScenarioKParentSelfServiceAndPrivacyTest.php` +- `ScenarioLTeacherAcademicOperationsTest.php` + +Scenario K covers the parent portal as a complete use case: the parent reviews profile, attendance, invoices, enrollments, updates contact information, adds emergency contacts, and is blocked from mutating another family’s student or emergency contact records. + +Scenario L covers a teacher’s classroom loop: the teacher loads the roster, retrieves class progress metadata, submits weekly progress, updates progress status, adds homework scoring columns, records homework scores, and checks report-card completeness. It also verifies an unassigned teacher cannot view or update another teacher’s progress report. + +These are intentionally not controller-method checklists. They assert business behavior across controllers, services, middleware, and database state. That is the point. If a test cannot explain what real-world workflow it protects, it is probably test-shaped furniture. + +### Added executable workflow coverage: Scenarios M through O + +The expanded workflow suite adds three more explicit use-case tests. These are intentionally written as end-to-end API flows rather than isolated controller pokes, because endpoint confetti is not test strategy. + +- **Scenario M: Administrator enrollment and classroom operations** verifies that an administrator can create a parent, create a student, create a class section, assign the student to that class, assign a teacher, confirm roster visibility, and reverse assignments without deleting the underlying student/family records. +- **Scenario N: Communications and support lifecycle** verifies parent-to-teacher messaging, inbox visibility, read-state persistence, archival state changes, public contact submission, and authenticated support queue behavior. +- **Scenario O: Inventory, finance, and audit controls** verifies inventory category/item creation, quantity adjustment, audit fields, movement logging, inventory summary visibility, and finance follow-up actions for unpaid balances. + +These tests should continue to grow by scenario. The rule is simple: when a feature exists because a real person performs a job, the test should describe that job from beginning to end. Testing individual URLs is useful, but only after the business workflow is nailed down. + +### Added broad-batch executable workflow coverage: Scenarios P through Z + +The latest expansion adds a broad pass instead of incremental drip-feed coverage. The new tests are organized by real operational workflows, not by whichever controller happened to annoy the developer that day. + +- **Scenario P: Settings, configuration, and calendar governance** covers settings review/update, school calendar CRUD, notifications, and configuration-admin CRUD. +- **Scenario Q: School-year promotion and closure lifecycle** covers admin promotion review/evaluation, parent promotion steps, year close validation, preview, close, and reopen guardrails. +- **Scenario R: Attendance operations and exception handling** covers teacher attendance grids/submission, admin manual attendance edits, staff attendance, early dismissal, late slip surfaces, violation tracking, and attendance comment template lifecycle including legacy aliases. +- **Scenario S: Scoring, grading, and report-card lifecycle** covers teacher entry for homework, quizzes, projects, midterm, final, participation, score comments, grading overview/decisions, locks, below-sixty intervention, and report-card completeness. +- **Scenario T: Documents, print, badges, stickers, slips, and certificates** covers teacher print requests, admin print processing, file access, badge generation/logging, sticker/slip surfaces, and certificate generation/audit. +- **Scenario U: Finance billing, payments, refunds, and installments** covers invoice visibility, payment creation, transaction logging, receipts, fee calculations, installment plans, refund approval/payment, and balance carryforward drafts. +- **Scenario V: Procurement, suppliers, purchase orders, and reimbursements** covers supply categories, suppliers, purchase orders, receiving, reimbursement submission, reimbursement processing, batches, locks, exports, and donation marking. +- **Scenario W: Access control, roles, navigation, and preferences** covers role-permission management, permission assignment, nav-builder lifecycle, role switching, preferences, and non-admin denial. +- **Scenario X: Public, kiosk, scanner, and verification surfaces** covers public content/documentation, health checks, badge scan processing, scanner processing, badge scan logs, and invalid public token handling. +- **Scenario Y: Family, guardians, authorized users, and WhatsApp** covers family bootstrap, second guardian attachment, guardian flags, family lookup/card surfaces, authorized-user invite/update, parent-contact exports, WhatsApp link lifecycle, and membership updates. +- **Scenario Z: Legacy aliases, validation contracts, and compatibility** covers legacy teacher GET/POST aliases, structured validation errors for representative public/authenticated forms, redirects, and non-versioned auth aliases. + +This creates a stronger testing spine for “all APIs and features.” It still does not mean the entire product is done. That claim would be a clown car with a CI badge. The next hardening layer should add wrong-owner and wrong-role negative tests inside each scenario where the feature has ownership semantics, then tighten flexible assertions once production controllers have stable response contracts. + +## Full-Surface Batch Expansion + +The suite now includes a full-surface E2E contract layer under `tests/Feature/Api/V1/FullSurface`. + +These files are intentionally broad rather than drip-fed by individual feature: + +- `ApiFullSurfaceEndToEndContractTest.php` walks the registered Laravel API route table and probes every route with seeded administrator, teacher, parent, and student context. It verifies controlled response status, no 5xx crashes, malformed-payload safety, and wrong-role portal boundaries. +- `ApiRouteUseCaseCatalogCompletenessTest.php` forces every registered API route to map to a named user journey, not merely a controller namespace. +- `ApiPublicAndAuthFullSurfaceContractTest.php` covers public, documentation, page, policy, health, kiosk, registration, login, me, logout, and frontend identity contracts. +- `ApiAdminOperationsFullSurfaceContractTest.php` covers administrator enrollment, promotion, staff, emergency contacts, school years, permissions, finance dashboards, certificates, reimbursements, flags, and printables. +- `ApiAcademicAttendanceFullSurfaceContractTest.php` covers teacher classroom operations, attendance, tracking, templates, grading, score entry, report cards, print requests, and legacy teacher routes. +- `ApiFinanceInventoryCommunicationsFullSurfaceContractTest.php` covers finance, billing, refunds, carryforwards, event charges, installments, notifications, inventory, suppliers, support, messages, email, communications, and WhatsApp. +- `ApiFamilyPortalFullSurfaceContractTest.php` covers parent profile, student privacy, attendance, invoices, enrollments, promotions, emergency contacts, family guardians, and authorized users. + +Expansion rule: new APIs must now satisfy two layers. First, the route must be owned by an explicit user journey in the catalog. Second, it must either be covered by the full-surface contract probe or by a named scenario with stronger business assertions. Endpoint existence alone is not coverage. It is merely a URL with opinions. + +### Full-surface deep contract expansion + +The next expansion adds a deeper executable contract layer under `tests/Feature/Api/V1/FullSurface`. + +New support base: + +- `Support/FullSurfaceE2EContractCase.php` centralizes deterministic users, class/student/parent fixtures, route materialization, actor selection, payload generation, and controlled-response assertions. + +New deep contract suites: + +- `ApiDeepResponseShapeAndEnvelopeContractTest` verifies that successful API responses are parseable JSON and that collection endpoints expose a stable list/envelope shape. +- `ApiDeepValidationFailureContractTest` sends empty, wrong-type, oversized, and hostile nested payloads to mutating routes and requires clean validation/authorization responses instead of 5xx crashes. +- `ApiDeepAuthorizationIsolationMatrixTest` expands role isolation checks across parent, teacher, student, and administrator boundaries, including wrong-family access attempts. +- `ApiDeepPaginationSearchAndFilterContractTest` probes list endpoints with pagination, search, sort, status, school-year, and semester filters, plus invalid date ranges. +- `ApiDeepIdempotencyAndDuplicateSubmissionContractTest` checks duplicate-sensitive POST routes and retryable destructive/archive routes for controlled behavior. +- `ApiDeepFileImportExportAndPrintableContractTest` probes download/export/printable endpoints and upload/import-style endpoints with fake files. +- `ApiDeepStateTransitionInvariantContractTest` targets school-year transitions, promotion previews, attendance transitions, and finance transitions with invalid and repeated states. +- `ApiDeepDocumentationAndRouteParityContractTest` keeps documentation/catalog endpoints reachable and checks the route table remains documentable and grouped by deeper contract domains. + +These tests are intentionally broader than named workflow stories. The workflow tests prove business journeys. The full-surface tests prove API discipline: response shape, validation, authorization, idempotency, list/filter safety, file boundary safety, and state transition control. That is the layer that catches boring production failures before users discover them with the enthusiasm of unpaid QA. + +### Full-surface expansion v6: security, privacy, hygiene, and boundary contracts + +This pass adds another broad layer under `tests/Feature/Api/V1/FullSurface/`. It is intentionally not a new sequence of tiny scenario drips; it expands the contract net across the whole API surface. + +New test classes: + +- `ApiSecurityAndTransportContractTest` verifies unauthenticated transport behavior, wrong-method handling, and CORS/preflight-style control responses across major domains. +- `ApiErrorEnvelopeConsistencyContractTest` verifies missing routes, validation envelopes, and authorization denials do not leak stack traces, framework internals, or SQL errors. +- `ApiOwnerBoundaryAndPrivacyExpansionTest` verifies family isolation, unassigned-teacher isolation, and sensitive user serialization boundaries. +- `ApiDestructiveActionSafeguardContractTest` verifies delete, archive, remove, cancel, void, close, reopen, and bulk destructive routes handle missing/empty/duplicate attempts without uncontrolled failures. +- `ApiWriteAuditAndTimestampContractTest` verifies successful writes expose usable identifiers or status envelopes, timestamp fields are not serialized as broken values, and audit/log mutation routes are not public. +- `ApiRouteNamingVersioningAndHygieneTest` verifies routes do not drift into debug/temp naming, duplicate URI/method pairs, vague placeholders, or undocumented unversioned API surfaces. +- `ApiBulkImportExportAndPartialFailureExpansionTest` verifies bulk routes handle mixed valid/invalid targets, import routes reject wrong file types cleanly, and export/download/print routes remain controlled. +- `ApiLegacyCurrentAliasParityExpansionTest` verifies auth aliases, legacy teacher aliases, and unversioned compatibility routes do not drift into incompatible or crashing behavior. +- `ApiPermissionMatrixExhaustiveContractTest` adds explicit role-matrix probes for admin, teacher, parent, and student access boundaries. +- `ApiEmptyDatasetAndNullStateContractTest` verifies empty result sets, missing current school-year state, and empty class rosters remain controlled. +- `ApiQueryParameterFuzzingContractTest` probes hostile search, pagination, sort, filter, date, and include parameters to catch SQL leakage and uncontrolled crashes. +- `ApiContractSnapshotSeedTest` adds lightweight contract-key snapshots for core successful payloads so API responses cannot silently lose identity/resource keys without tests noticing. + +The intent is to force the backend to answer five hard questions for every feature area: + +1. Does the endpoint fail safely? +2. Does it expose the right role and owner boundary? +3. Does it keep response shape stable enough for clients? +4. Does it handle empty, duplicate, malformed, and hostile inputs? +5. Does compatibility stay deliberate rather than accidental? + +Future additions should be made in one of two places: workflow scenarios for business journeys, or full-surface contracts for systemic guarantees. Do not add isolated one-off tests unless the bug is too specific to generalize. + +## Full-surface v7 expansion: hardening, privacy, and invariants + +This pass adds another broad contract layer rather than isolated scenario trickles. The new tests are deliberately written as cross-cutting quality gates over the route table and seeded E2E actors. + +New coverage areas: + +- Token/session hardening: repeated bad login attempts, garbage bearer tokens, repeat logout safety. +- Sensitive serialization redaction: passwords, remember tokens, provider secrets, payment secrets, and credentials must not leak in success or error responses. +- Mass-assignment and overposting: write endpoints must ignore or reject privilege, ownership, audit, and finance tampering fields. +- Referential integrity: impossible foreign keys must fail with controlled validation/authorization/not-found responses, not raw database errors. +- Read-after-write consistency: successful creates must return an identifier, envelope, or location that supports follow-up reads; successful updates must not null out identity. +- Date, money, and timezone boundaries: impossible dates, ambiguous dates, negative amounts, overflow amounts, NaN/Infinity-style values, and invalid timezones must fail cleanly. +- Attachment/file safety: upload-like endpoints must reject executable files and download-like endpoints must resist path traversal probes. +- Observability: correlation/request IDs must be accepted safely without becoming response-header injection or error-handling hazards. +- Cache correctness: authenticated private resources must not be publicly cacheable, and conditional headers must not bypass authorization. +- Concurrency/stale writes: duplicate submissions and stale version headers must stay controlled and must not leak deadlock/duplicate-key internals. +- Localization fallback: supported and unsupported locales must not cause missing-translation or timezone runtime failures. +- External provider callbacks: webhook/callback/payment-provider routes must reject unsigned malformed payloads without exposing provider secrets. +- Report/export privacy: downloadable and aggregate reporting routes must stay scoped to the actor's family, class, or role. + +These tests are not replacements for scenario-level assertions. They are guardrails around the failure classes that normally escape happy-path E2E suites: identity leaks, unsafe error envelopes, overposting, stale writes, bad money math, attachment abuse, and report/export privacy leaks. + +## Full-surface v8 expansion: lifecycle depth, edge cases, and operational resilience + +This pass adds another broad batch of contract tests under `tests/Feature/Api/V1/FullSurface/`. It is designed to widen the test net without slipping back into route-by-route drip testing. + +New coverage areas: + +- Authentication lifecycle depth: valid login contract parity, invalid login behavior, identity endpoints, logout idempotency, password reset, registration, and verification safety. +- Role workflow negative matrix: non-admin mutation denial, actor-specific read boundaries, and blocked/suspended/unverified user access probes. +- Schema key stability: successful resource payloads must retain identity/display keys, and error envelopes must remain actionable for clients. +- Business rule edge cases: mismatched school year/semester/class payloads, financial amount boundary rules, and attendance status/time abuse. +- Notification and messaging delivery: recipient validation, bad channels, oversized messages, invalid email/phone values, read/archive/star/delete idempotency. +- Data export least privilege: parent, teacher, and student export/download/report access must remain scoped and must not leak sensitive bulk data. +- Audit and administrative traceability: admin mutations must remain controlled and audit/log routes must not be public or low-privilege mutable. +- Calendar and schedule conflicts: invalid date ranges, reversed times, impossible recurrence, broad private-event queries, and actor-scoped calendar reads. +- Search and autocomplete resilience: empty, tiny, hostile, huge, wildcard, and script-like search inputs must not leak SQL or reflect raw scripts. +- Feature flag/configuration drift: missing core configuration rows must fail closed, and low-privilege users must not mutate privileged settings. +- Transaction rollback backstops: failed multi-step mutations must not spray obvious partial users/students, and failed destructive actions must not remove seeded fixtures. +- Client compatibility/versioning: API routes must be versioned or explicitly legacy, route names must stay unique enough for client generation, and legacy/current auth failures must remain compatible. +- School operations regression backstop: administrator probes across core domains must stay controlled, and each user-facing portal must retain at least one reachable read contract. +- Operational payload resilience: huge bodies, deep arrays, huge ID lists, abusive include/fields/expand query strings must fail safely without memory or framework leaks. + +The point of v8 is not to worship the number of test files. The point is to make failure categories explicit: lifecycle, permissions, schema, business rules, delivery, exports, auditability, scheduling, search, config drift, transaction safety, versioning, portal reachability, and payload resilience. That is how the suite becomes a quality system rather than a decorative pile of assertions. + +## v9 broad expansion: documentation, media, cache, lifecycle, and abuse contracts + +This pass adds another full-surface contract layer rather than another drip of isolated scenarios. The v9 tests focus on API behavior that often slips between business workflows and endpoint smoke tests: + +- OpenAPI/documentation route drift and machine-readable route inventory expectations. +- Content negotiation and malformed media-type behavior. +- Soft-delete, restore, archive, cancel, void, and repeated destructive-action safeguards. +- Model lifecycle invariants across create/update/show/index-style resources. +- Notification preference, opt-out, recipient, and broadcast authorization boundaries. +- Report aggregation filters, date windows, grouping, and scoped report access. +- Deeply nested payloads, sparse arrays, oversized ID lists, and array abuse. +- Public/private cache separation and conditional request authorization. +- File upload/download metadata safety, misleading MIME types, and path traversal probes. +- Sorting, field selection, include/expand controls, and sensitive-field redaction. +- Calendar, schedule, timezone, recurrence, and DST boundary inputs. +- Webhook replay, missing signature, bad signature, and provider-secret redaction. +- Impersonation/delegation/acting-as route controls. +- Policy/authorization discovery for protected mutations and wildcard permissions. +- Cross-module referential misuse, where valid IDs from one domain are submitted as IDs for another domain. + +The intent remains the same: every new API surface should either be owned by a named user journey or covered by a full-surface contract probe that catches drift, unsafe errors, privacy leaks, role leaks, and malformed-input failures. Endpoint quantity is not quality; it is just a larger blast radius unless these contracts stay strict. + +## v10 broad contract expansion + +The v10 batch adds another full-surface test layer without executing the suite locally. The goal is to widen contractual pressure across the API surface instead of adding a few isolated scenario crumbs. + +New coverage areas: + +- Route-family heatmap coverage and mutating-route ownership checks. +- HTTP method semantics, wrong-method behavior, bodyless deletes, and GET routes receiving mutation-looking query input. +- Null and string relationship failure modes for write endpoints. +- Frontend/backend identity and login alias parity. +- Route-parameter abuse: path traversal, script payloads, overflow IDs, and malformed placeholders. +- Privacy, retention, archive, erasure, and sensitive-field redaction boundaries. +- Async/queue/job-style trigger authorization and duplicate client request IDs. +- Financial/report math invariants, invalid money formats, and impossible aggregation windows. +- Migration-safe defaults when optional configuration rows or school-year context are absent. +- Polymorphic ownership overposting and cross-actor owner query isolation. +- Enum/state-machine boundaries for unknown statuses and terminal-state transitions. +- Unicode, bidi, zero-width, email, and phone normalization probes. +- Backward-compatible response envelope shape for collection and detail endpoints. +- Academic calendar boundary dates and hostile semester/term values. +- Mobile-client headers and offline retry/idempotency tolerance. +- Import/template/CSV malformed input and template download access boundaries. +- Cross-version auth route compatibility and containment of unversioned legacy routes. + +Rule added by v10: every future API expansion must be visible in the route-family heatmap, must have controlled failure behavior under malformed relationship IDs, and must not rely on frontend assumptions that are absent from API response contracts. Otherwise the frontend gets to discover the contract in production, which is one way to run QA if you hate everyone. + + +## Batch 11 full-surface hardening expansion + +Batch 11 adds another broad contract layer rather than another drip of isolated scenarios. It targets high-risk API failure modes that are easy to miss when teams worship happy-path workflows like tiny golden idols. + +New coverage includes: + +- Tenant, school-year, and semester isolation across academic, attendance, finance, and reporting routes. +- Open redirect and hostile return-url handling for login, auth, navigation, dashboard, and frontend bootstrap routes. +- SSRF-style URL payloads for callback, webhook, import/export, media, notification, and settings routes. +- HTML/script reflection safety for user-controlled text fields. +- CSV and Excel formula injection payloads in import/export-facing text fields. +- Conditional request and stale ETag behavior on mutating routes. +- Idempotency-key replay and malformed idempotency header behavior. +- Authorization-cache and role-escalation probes after permission-like mutations. +- Null/boolean/string type coercion across broad mutation routes. +- Data minimization checks for parent and student responses. +- Suspended/disabled/pending account lifecycle access probes. +- Header spoofing checks for forwarded, override, impersonation, and fake admin headers. +- Relationship cardinality abuse where single IDs receive arrays or objects. +- Cursor, offset, page, and per-page abuse across list endpoints. +- Operational endpoint exposure checks for debug, logs, metrics, cache, queue, and maintenance-style routes. +- A batch-level regression net proving the major API families remain represented by high-risk negative probes. + +Rule going forward: future additions should be added as broad risk families first, then only split into scenario files when a real business workflow needs deeper assertions. More files are not automatically more quality. Sometimes they are just paperwork wearing a lab coat. + +## Batch 12 full-surface risk and abuse expansion + +Batch 12 adds another broad contract layer over the API surface rather than another tiny scenario drip. It targets production-grade failure modes that sit between normal workflow tests and security regression tests, where most teams conveniently lose interest because the bugs are ugly and the screenshots are boring. + +New Batch 12 classes: + +- `ApiBatch12MaintenanceModeAndOperationalControlContractTest` checks that maintenance, cache, queue, metrics, logs, debug, and operational control routes are admin-only and return controlled responses. +- `ApiBatch12SensitiveConfigurationExposureContractTest` checks that configuration, bootstrap, frontend, health, debug, and settings responses do not expose secrets or environment internals. +- `ApiBatch12BulkMutationBoundaryContractTest` checks empty bulk targets, cross-domain IDs, and unbounded ID lists across assign/import/delete/archive-style endpoints. +- `ApiBatch12WebhookProviderMatrixContractTest` expands provider callback coverage for missing, bad, and replay-like signature headers across webhook/payment/provider routes. +- `ApiBatch12CrossActorDataLeakageContractTest` probes parent, teacher, and student reads for sensitive-field leakage and owner-scope override attempts. +- `ApiBatch12AttachmentLifecycleContractTest` expands upload/download/file lifecycle checks for MIME spoofing, executable uploads, path traversal, and storage-driver probing. +- `ApiBatch12FinancialLedgerIntegrityContractTest` stresses finance routes with ledger-breaking amounts, wrong module IDs, and parent attempts to force all-family scope. +- `ApiBatch12AttendanceCorrectionAuditContractTest` checks attendance correction boundaries around status, dates, reasons, wrong IDs, and low-privilege mutation attempts. +- `ApiBatch12AcademicGradeBoundaryContractTest` checks score boundaries, wrong student/class contexts, and low-privilege grade finalization overrides. +- `ApiBatch12GuardianDelegationExpirationContractTest` checks guardian and authorized-user expiration, relationship, pickup/messaging flags, and student mutation denial. +- `ApiBatch12SearchEnumerationResistanceContractTest` probes search, lookup, autocomplete, user, parent, and student endpoints for enumeration and injection-style inputs. +- `ApiBatch12LongRunningExportJobContractTest` checks export/report/print/generation routes for async parameters, sensitive field requests, and low-privilege full-school export attempts. +- `ApiBatch12ConsistencyUnderPartialPayloadContractTest` checks sparse POST/PATCH/PUT payloads so validation failures stay controlled and successful partial updates do not null identity or ownership. +- `ApiBatch12RouteMetadataRiskRegisterTest` creates an executable risk register for identity, authorization, privacy, attendance, academics, finance, files, integrations, and operations. +- `ApiBatch12CombinedRegressionSweepTest` applies a combined hostile payload across high-risk route families to catch SQL leaks, environment leaks, auth-internal leaks, and uncontrolled crashes. + +Batch 12 is not meant to replace the earlier scenario tests. It is a pressure layer: broad, risk-family based, and intentionally hostile. Future batches should either close a named risk family not covered here or deepen a concrete workflow with stronger business assertions. Otherwise the test suite becomes a decorative haystack, which is less useful than it sounds. + + +## Batch 13 full-surface integrity and abuse expansion + +Batch 13 adds another broad contract layer on top of Batch 12. It focuses less on simply finding more endpoints and more on catching cross-domain integrity failures, forged business actions, and dangerous client assumptions. In plain terms: the API should not trust a request just because it contains an integer and a hopeful field name. + +New Batch 13 classes: + +- `ApiBatch13PaymentProviderLedgerReconciliationContractTest` stresses payment-provider references, transaction replay, ledger reconciliation payloads, and duplicate provider IDs. +- `ApiBatch13StudentEnrollmentLifecycleIntegrityContractTest` checks enrollment, withdrawal, registration, class assignment, and cross-family/class payload abuse. +- `ApiBatch13TeacherRosterAndAssignmentBoundaryContractTest` checks teacher roster scoping, forced teacher/class overrides, and unassigned-teacher academic mutations. +- `ApiBatch13ParentPortalInvoicePrivacyContractTest` checks parent invoice/payment privacy, all-family override attempts, and parent attempts to mark invoices paid/refunded. +- `ApiBatch13StaffHrPayrollBoundaryContractTest` checks staff/HR/payroll redaction and low-privilege mutation denial for personnel data. +- `ApiBatch13DiscountVoucherScholarshipAbuseContractTest` checks voucher, discount, scholarship, waiver, and fee-abuse payloads. +- `ApiBatch13InventoryProcurementReceivingIntegrityContractTest` checks procurement, receiving, supplier, inventory movement, and negative-stock abuse. +- `ApiBatch13ReportCardTranscriptCertificateForgeryContractTest` checks report-card, transcript, certificate, badge, and print generation forgery payloads. +- `ApiBatch13ContactMessagingAbuseModerationContractTest` checks message/contact/support/broadcast abuse, header injection, spam payloads, and HTML reflection. +- `ApiBatch13ApiPaginationPerformanceGuardContractTest` checks unbounded list parameters, deep includes, search abuse, and memory/time-error disclosure. +- `ApiBatch13LocaleCalendarReligiousDateContractTest` checks locale-specific calendar payloads, Hijri-style dates, reversed ranges, recurrence abuse, and academic date spoofing. +- `ApiBatch13MobileOfflineSyncConflictContractTest` checks mobile/offline sync headers, stale client versions, duplicate offline UUIDs, and conflict-resolution override attempts. +- `ApiBatch13AccountRecoveryCredentialChangeContractTest` checks password reset, verification, credential-change, token leakage, and cross-user credential mutation attempts. +- `ApiBatch13RolePermissionMutationRaceContractTest` checks wildcard permissions, recursive role inheritance, replayed role payloads, and non-admin escalation attempts. +- `ApiBatch13ObservableErrorCorrelationContractTest` checks request/correlation ID safety, header injection, and stack/controller/model disclosure. +- `ApiBatch13FullSurfaceRegressionSuperSweepTest` adds a combined cross-family hostile-payload sweep plus a mutation-family representation check. + +Batch 13 keeps the same rule: write broad, risk-family tests first; only split into deeper workflow scenarios when a real business flow needs stronger state assertions. Otherwise the suite becomes a decorative maze where bugs go to retire. + +## Batch 14 full-surface session, integrity, and tamper-resistance expansion + +Batch 14 adds another broad contract layer on top of Batch 13. It focuses on API boundaries that often break after the functional workflow looks “done”: session/cookie confusion, identity enumeration, tampered export scopes, forged approvals, device/kiosk trust, signed URL abuse, and cross-module ownership overrides. In other words, the boring stuff that becomes very exciting when it leaks data. + +New Batch 14 classes: + +- `ApiBatch14SessionCookieCsrfBoundaryContractTest` checks that API authentication does not silently depend on browser session cookies or fake CSRF headers, and that browser-only headers do not grant API privilege. +- `ApiBatch14AccountEnumerationAndIdentityPrivacyContractTest` checks recovery/verification routes for user-existence leakage and identity payloads for auth internals. +- `ApiBatch14BulkAssignmentCollisionContractTest` checks duplicate and conflicting bulk assignment payloads across class, teacher, student, enroll, and batch-style routes. +- `ApiBatch14PrintExportTamperingContractTest` checks print/export/certificate/report generation routes for forged scope, signature, deleted/private data, and low-privilege full-school export abuse. +- `ApiBatch14AuditTrailTamperResistanceContractTest` checks audit/log/history routes for forged actor, timestamp, and purge/rewrite attempts. +- `ApiBatch14FinanceReconciliationAndRefundAbuseContractTest` checks finance ledger, payment, refund, transaction, and reconciliation routes for mismatched ownership, negative amounts, fake provider IDs, and parent self-refund abuse. +- `ApiBatch14AcademicPromotionRollbackIntegrityContractTest` checks promotion, school-year, semester, close/reopen, and rollback routes for unsafe rollback/delete payloads and low-privilege promotion attempts. +- `ApiBatch14AttendanceDeviceAndKioskAbuseContractTest` checks scanner, kiosk, badge, dismissal, late, and attendance routes for forged device headers and self-marking abuse. +- `ApiBatch14NotificationTemplateInjectionContractTest` checks notification, template, broadcast, email, WhatsApp, and message routes for template injection, header injection, and non-admin broadcast abuse. +- `ApiBatch14ApiConsumerContractCompatibilityTest` checks core auth/frontend/profile contracts across web, mobile, and API consumer headers, including the token/user login contract. +- `ApiBatch14RelationshipOwnershipMatrixExpansionTest` checks parent, family, guardian, invoice, attendance, score, and student routes for owner override fields and cross-relationship graph access. +- `ApiBatch14StoragePathAndSignedUrlContractTest` checks file/media/upload/download/import/export/signed routes for path traversal, forged signatures, disk override, and secret exposure. +- `ApiBatch14AdminDelegationApprovalWorkflowContractTest` checks approval, delegation, impersonation, role, permission, and authorization routes for forged approval metadata and self-delegation abuse. +- `ApiBatch14DataLifecycleArchivePurgeContractTest` checks archive, restore, purge, erase, retention, delete, and destroy routes for unsafe hard-delete/cascade payloads and non-admin purge attempts. +- `ApiBatch14CrossModuleInvariantRiskRegisterTest` adds an executable Batch 14 risk register so these route families remain visible as the API evolves. +- `ApiBatch14RegressionMegaSweepTest` adds a combined hostile-payload sweep across auth, users, school, academic, attendance, finance, inventory, messaging, file, audit, and settings surfaces. + +Batch 14 keeps the same expansion rule: add risk-family tests when the API surface is broad, and only add narrower scenario tests when a workflow has stateful business rules worth proving. Endpoint confetti is still not a strategy, even if it makes the directory look busy. + + +## Batch 15: Identity, Privacy, Operations, and Abuse-Resistance Contract Expansion + +Batch 15 adds another full-surface contract layer focused on long-lived operational and security risks that are easy to miss when tests only chase happy-path workflows. + +New coverage areas: + +- Token rotation, revocation, refresh replay, logout replay, and identity/session route parity. +- Privacy, consent, erasure, retention, data-subject export, and student/family scope boundaries. +- Repeated abuse patterns across login, password recovery, contact, support, badge, and scanner endpoints. +- Route parameter poisoning with encoded traversal, null bytes, script payloads, SQL-ish values, overflowing IDs, and non-ASCII numeric forms. +- Feature flag, experiment, rollout, settings, and configuration override attempts by low-privilege actors. +- Command, scheduler, queue, sync, cleanup, recalculation, and background trigger authorization. +- Backup, restore, snapshot, database dump, archive, tenant, and residency exposure controls. +- Cross-school duplicate identity, external ID collision, student-number collision, and tenant alias abuse. +- Form-request authorization bypass attempts using `_method`, wildcard permissions, and fake roles. +- Domain event, outbox, notification, webhook, broadcast, email, and WhatsApp forged metadata. +- Impersonation, delegation, acting-as, switch-user, and behalf-of boundary assertions. +- Report drilldown, analytics, dashboard, summary, and export aggregate privacy. +- RTL, Unicode, bidi override, combining characters, emoji, and malformed encoding text payloads. +- Student safety, incident escalation, attendance alerts, flags, and parent/student mutation denial. +- Sparse fieldsets, metadata expansion, debug includes, global search, directory, lookup, and autocomplete enumeration resistance. +- A Batch 15 ultra sweep combining tenant override, owner override, secret includes, hard-delete flags, forged signatures, hostile redirects, and path traversal probes. + +This batch continues the rule used by the full-surface suite: broad risk-family tests first, then deeper scenario-specific tests only when a business workflow needs dedicated assertions. Otherwise the test suite becomes a very expensive scrapbook. diff --git a/routes/api.php b/routes/api.php index 8204d4c2..bd22a180 100644 --- a/routes/api.php +++ b/routes/api.php @@ -1,130 +1,137 @@ middleware('throttle:10,1'); Route::post('register', [RegisterController::class, 'store'])->middleware('throttle:5,1'); @@ -605,8 +612,8 @@ Route::prefix('v1')->group(function () { Route::get('teacher/select-semester', function () { return response()->json([ 'ok' => true, - 'school_year' => Configuration::getConfig('school_year'), - 'semester' => Configuration::getConfig('semester'), + 'school_year' => \App\Models\Configuration::getConfig('school_year'), + 'semester' => \App\Models\Configuration::getConfig('semester'), 'semester_options' => ['Fall', 'Spring'], ]); }); @@ -980,7 +987,7 @@ Route::prefix('v1')->group(function () { Route::post('event-charges/{eventCharge}/void', [EventChargeController::class, 'void'])->whereNumber('eventCharge'); Route::post('event-charges/{eventCharge}/attach-to-invoice', [EventChargeController::class, 'attachToInvoice'])->whereNumber('eventCharge'); - Route::apiResource('installment-plans', InstallmentPlanController::class)->only(['index', 'show']); + Route::apiResource('installment-plans', InstallmentPlanController::class)->only(['index','show']); Route::post('invoices/{invoice}/installment-plans', [InstallmentPlanController::class, 'store'])->whereNumber('invoice'); Route::post('installment-plans/{plan}/activate', [InstallmentPlanController::class, 'activate'])->whereNumber('plan'); Route::post('installment-plans/{plan}/cancel', [InstallmentPlanController::class, 'cancel'])->whereNumber('plan'); diff --git a/tests/Concerns/AssertsE2EApiResponses.php b/tests/Concerns/AssertsE2EApiResponses.php new file mode 100644 index 00000000..c9b754cc --- /dev/null +++ b/tests/Concerns/AssertsE2EApiResponses.php @@ -0,0 +1,34 @@ +assertLessThan( + 500, + $response->getStatusCode(), + $context . ' should return a controlled response, not a 5xx crash. Body: ' . $response->getContent() + ); + } + + /** + * @param array $statuses + */ + protected function assertStatusIn(TestResponse $response, array $statuses, string $context): void + { + $this->assertContains( + $response->getStatusCode(), + $statuses, + $context . ' returned unexpected status ' . $response->getStatusCode() . '. Body: ' . $response->getContent() + ); + } + + protected function assertSuccessfulOrValidation(TestResponse $response, string $context): void + { + $this->assertStatusIn($response, [200, 201, 202, 204, 302, 409, 422], $context); + } +} diff --git a/tests/Concerns/CreatesApiTestUsers.php b/tests/Concerns/CreatesApiTestUsers.php index 0d7a1ea2..5f22084b 100644 --- a/tests/Concerns/CreatesApiTestUsers.php +++ b/tests/Concerns/CreatesApiTestUsers.php @@ -68,7 +68,7 @@ trait CreatesApiTestUsers 'status' => 'Active', 'is_verified' => 1, 'is_suspended' => 0, - 'email' => $roleName.'-api-test-'.str_replace('.', '', uniqid('', true)).'@example.test', + 'email' => $roleName . '-api-test-' . str_replace('.', '', uniqid('', true)) . '@example.test', ], $overrides)); $role = $roles[$roleName] ?? Role::query()->where('name', $roleName)->firstOrFail(); diff --git a/tests/Concerns/SeedsAdministratorApiConfig.php b/tests/Concerns/SeedsAdministratorApiConfig.php index d1f8e010..9eac17ad 100644 --- a/tests/Concerns/SeedsAdministratorApiConfig.php +++ b/tests/Concerns/SeedsAdministratorApiConfig.php @@ -22,4 +22,4 @@ trait SeedsAdministratorApiConfig ['config_value' => $semester] ); } -} +} \ No newline at end of file diff --git a/tests/Concerns/SeedsE2ETestFixtures.php b/tests/Concerns/SeedsE2ETestFixtures.php index 59bda107..d3376cb6 100644 --- a/tests/Concerns/SeedsE2ETestFixtures.php +++ b/tests/Concerns/SeedsE2ETestFixtures.php @@ -18,9 +18,7 @@ trait SeedsE2ETestFixtures use CreatesApiTestUsers; protected const E2E_SCHOOL_YEAR = '2025-2026'; - protected const E2E_PREV_SCHOOL_YEAR = '2024-2025'; - protected const E2E_SEMESTER = 'Fall'; protected function seedE2EConfiguration(): void @@ -110,18 +108,18 @@ trait SeedsE2ETestFixtures $teacher = $this->createApiUserWithRole('teacher', [ 'firstname' => 'Tara', 'lastname' => 'Teacher', - 'email' => 'e2e.teacher.'.uniqid('', true).'@example.test', + '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', + 'email' => 'e2e.parent.' . uniqid('', true) . '@example.test', ]); $studentId = DB::table('students')->insertGetId([ - 'school_id' => 'S-E2E-'.random_int(1000, 9999), + 'school_id' => 'S-E2E-' . random_int(1000, 9999), 'firstname' => 'Kai', 'lastname' => 'Student', 'dob' => '2014-09-01', @@ -223,7 +221,7 @@ trait SeedsE2ETestFixtures protected function postEnrollmentStatuses(array $studentStatusMap): TestResponse { return $this->post( - '/api/v1/administrator/enrollment-withdrawal/update-statuses?'. + '/api/v1/administrator/enrollment-withdrawal/update-statuses?' . http_build_query(['enrollment_status' => $studentStatusMap]), [] ); @@ -233,7 +231,7 @@ trait SeedsE2ETestFixtures { return DB::table('invoices')->insertGetId(array_merge([ 'parent_id' => $parentId, - 'invoice_number' => 'INV-E2E-'.random_int(1000, 9999), + 'invoice_number' => 'INV-E2E-' . random_int(1000, 9999), 'total_amount' => 500.00, 'balance' => 500.00, 'paid_amount' => 0, @@ -252,7 +250,7 @@ trait SeedsE2ETestFixtures { return DB::table('invoices')->insertGetId([ 'parent_id' => $parentId, - 'invoice_number' => 'INV-PRIOR-'.random_int(1000, 9999), + 'invoice_number' => 'INV-PRIOR-' . random_int(1000, 9999), 'total_amount' => 200.00, 'balance' => 150.00, 'paid_amount' => 50.00, @@ -270,7 +268,7 @@ trait SeedsE2ETestFixtures protected function seedDiscountVoucher(): int { return DB::table('discount_vouchers')->insertGetId([ - 'code' => 'E2E10-'.random_int(100, 999), + 'code' => 'E2E10-' . random_int(100, 999), 'discount_type' => 'fixed', 'discount_value' => 50, 'max_uses' => 100, diff --git a/tests/Feature/Api/ApiAuthenticationAndAuthorizationTest.php b/tests/Feature/Api/ApiAuthenticationAndAuthorizationTest.php index edff0530..ebe6a27a 100644 --- a/tests/Feature/Api/ApiAuthenticationAndAuthorizationTest.php +++ b/tests/Feature/Api/ApiAuthenticationAndAuthorizationTest.php @@ -13,6 +13,7 @@ class ApiAuthenticationAndAuthorizationTest extends TestCase use CreatesApiTestUsers; use RefreshDatabase; + public function test_api_login_returns_token_and_user_object(): void { $user = $this->createApiUserWithRole('teacher', [ @@ -102,7 +103,7 @@ class ApiAuthenticationAndAuthorizationTest extends TestCase foreach ($this->sampleProtectedRoutes() as $route) { $method = $this->primaryMethod($route); - $uri = '/'.$this->uriWithPlaceholders($route->uri()); + $uri = '/' . $this->uriWithPlaceholders($route->uri()); $response = $this->json($method, $uri); $status = $response->getStatusCode(); @@ -112,7 +113,7 @@ class ApiAuthenticationAndAuthorizationTest extends TestCase } } - $this->assertSame([], $failures, "Anonymous requests reached unexpected responses:\n".implode("\n", $failures)); + $this->assertSame([], $failures, "Anonymous requests reached unexpected responses:\n" . implode("\n", $failures)); } /** diff --git a/tests/Feature/Api/ApiPreferencesFeatureTest.php b/tests/Feature/Api/ApiPreferencesFeatureTest.php index 041058e2..b01d52e4 100644 --- a/tests/Feature/Api/ApiPreferencesFeatureTest.php +++ b/tests/Feature/Api/ApiPreferencesFeatureTest.php @@ -47,6 +47,7 @@ class ApiPreferencesFeatureTest extends TestCase ->assertJsonPath('status', true); } + public function test_non_admin_cannot_list_or_manage_another_users_preferences(): void { $owner = $this->createApiUserWithRole('teacher'); diff --git a/tests/Feature/Api/ApiPublicEndpointsTest.php b/tests/Feature/Api/ApiPublicEndpointsTest.php index 7b78015f..990670a7 100644 --- a/tests/Feature/Api/ApiPublicEndpointsTest.php +++ b/tests/Feature/Api/ApiPublicEndpointsTest.php @@ -36,7 +36,7 @@ class ApiPublicEndpointsTest extends TestCase public function test_public_frontend_content_endpoints_do_not_server_error(): void { foreach (['/', 'facility', 'team', 'call-to-action', 'testimonial', 'not-found'] as $path) { - $uri = '/api/v1/frontend'.($path === '/' ? '' : '/'.$path); + $uri = '/api/v1/frontend' . ($path === '/' ? '' : '/' . $path); $response = $this->getJson($uri); $this->assertLessThan(500, $response->getStatusCode(), "$uri returned a server error"); diff --git a/tests/Feature/Api/ApiRolePermissionFeatureTest.php b/tests/Feature/Api/ApiRolePermissionFeatureTest.php index de0c6dc1..99e6d6a8 100644 --- a/tests/Feature/Api/ApiRolePermissionFeatureTest.php +++ b/tests/Feature/Api/ApiRolePermissionFeatureTest.php @@ -2,6 +2,8 @@ namespace Tests\Feature\Api; +use App\Models\Permission; +use App\Models\Role; use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\Concerns\CreatesApiTestUsers; diff --git a/tests/Feature/Api/ApiRouteContractTest.php b/tests/Feature/Api/ApiRouteContractTest.php index 70a87260..35df3ec7 100644 --- a/tests/Feature/Api/ApiRouteContractTest.php +++ b/tests/Feature/Api/ApiRouteContractTest.php @@ -24,11 +24,11 @@ class ApiRouteContractTest extends TestCase [$class, $method] = explode('@', $action, 2); if (! class_exists($class) || ! method_exists($class, $method)) { - $missing[] = implode('|', $route->methods()).' '.$route->uri().' -> '.$action; + $missing[] = implode('|', $route->methods()) . ' ' . $route->uri() . ' -> ' . $action; } } - $this->assertSame([], $missing, "These API routes reference missing controllers or methods:\n".implode("\n", $missing)); + $this->assertSame([], $missing, "These API routes reference missing controllers or methods:\n" . implode("\n", $missing)); } public function test_api_routes_do_not_duplicate_method_and_uri_pairs(): void @@ -42,7 +42,7 @@ class ApiRouteContractTest extends TestCase continue; } - $key = $method.' '.$route->uri(); + $key = $method . ' ' . $route->uri(); if (isset($seen[$key])) { $duplicates[] = $key; } @@ -90,11 +90,11 @@ class ApiRouteContractTest extends TestCase }); if (! $hasProtection) { - $unprotected[] = implode('|', $methods).' '.$route->uri(); + $unprotected[] = implode('|', $methods) . ' ' . $route->uri(); } } - $this->assertSame([], $unprotected, "Mutating API routes without an obvious auth/throttle middleware:\n".implode("\n", $unprotected)); + $this->assertSame([], $unprotected, "Mutating API routes without an obvious auth/throttle middleware:\n" . implode("\n", $unprotected)); } /** diff --git a/tests/Feature/Api/ApiUseCaseCoverageMatrixTest.php b/tests/Feature/Api/ApiUseCaseCoverageMatrixTest.php new file mode 100644 index 00000000..84812b97 --- /dev/null +++ b/tests/Feature/Api/ApiUseCaseCoverageMatrixTest.php @@ -0,0 +1,169 @@ +apiRoutes() as $route) { + $uri = $route->uri(); + + if ($this->domainFor($uri) === null) { + $unmapped[] = implode('|', array_diff($route->methods(), ['HEAD'])) . ' ' . $uri; + } + } + + $this->assertSame( + [], + $unmapped, + "API routes without an explicit use-case domain:\n" . implode("\n", $unmapped) . + "\n\nAdd the route to the matrix and then add/extend an E2E workflow test for that domain." + ); + } + + public function test_required_use_case_domains_have_route_coverage(): void + { + $domains = collect($this->apiRoutes()) + ->map(fn (LaravelRoute $route): ?string => $this->domainFor($route->uri())) + ->filter() + ->unique() + ->sort() + ->values() + ->all(); + + $missing = array_values(array_diff(array_keys($this->useCaseDomains()), $domains)); + + $this->assertSame( + [], + $missing, + "Required use-case domains with no matching API route coverage:\n" . implode("\n", $missing) + ); + } + + public function test_every_protected_api_route_rejects_anonymous_requests(): void + { + $failures = []; + + foreach ($this->protectedRoutes() as $route) { + $method = $this->primaryMethod($route); + $uri = '/' . $this->uriWithSafePlaceholders($route->uri()); + + $response = $this->json($method, $uri); + $status = $response->getStatusCode(); + + if (! in_array($status, [401, 403, 404, 405, 419, 422], true)) { + $failures[] = "$method $uri returned $status"; + } + } + + $this->assertSame( + [], + $failures, + "Anonymous requests reached protected API routes unexpectedly:\n" . implode("\n", $failures) + ); + } + + /** + * @return array + */ + private function useCaseDomains(): array + { + return [ + 'public_content_and_support' => '#^api/(?:v1/)?(?:login|register|auth|documentation|docs|docs/public|policies|pages|contact|frontend|health|system/db-check|access_denied|certificates/verify|timeoff/notify|winners/competitions|confirm_authorized_user|set_authorized_user_password|badge_scan|scanner)(?:/|$)#', + 'legacy_compatibility' => '#^api/(?:proofread|emails|compare|attendance-templates|attendance-comment-templates|administrator/attendance-templates)(?:/|$)#', + 'preferences_navigation_and_ui' => '#^api/v1/(?:preferences|nav-builder|landing|info-icon|dashboard|utilities|ui|settings|configuration|system|stats|role-switcher)(?:/|$)#', + 'administration_enrollment_and_school_years' => '#^api/v1/(?:administrator|school-years|class-sections|users|role-permissions|ip-bans|notifications)(?:/|$)#', + 'students_parents_and_families' => '#^api/v1/(?:students|parents|families|family-admin|emergency-contacts)(?:/|$)#', + 'teachers_staff_and_class_operations' => '#^api/v1/(?:teacher|teachers|staff|assignments|class-prep|class-progress)(?:/|$)#', + 'attendance_and_safety' => '#^api/v1/(?:attendance|attendance-tracking|attendance-comment-templates|attendance-templates|incidents)(?:/|$)#', + 'grading_scores_and_reporting' => '#^api/v1/(?:scores|grading|reports|badges|certificates|print-requests)(?:/|$)#', + 'finance_billing_and_inventory' => '#^api/v1/(?:finance|discounts|expenses|extra-charges|inventory|files)(?:/|$)#', + 'communications_and_messaging' => '#^api/v1/(?:messages|broadcast-email|email|email-extractor|communications|support|whatsapp)(?:/|$)#', + 'competition_and_public_results' => '#^api/v1/(?:competition-scores)(?:/|$)#', + ]; + } + + private function domainFor(string $uri): ?string + { + foreach ($this->useCaseDomains() as $domain => $pattern) { + if (preg_match($pattern, $uri) === 1) { + return $domain; + } + } + + return null; + } + + /** + * @return list + */ + private function apiRoutes(): array + { + return array_values(array_filter(Route::getRoutes()->getRoutes(), function (LaravelRoute $route): bool { + return str_starts_with($route->uri(), 'api/'); + })); + } + + /** + * @return list + */ + private function protectedRoutes(): array + { + return array_values(array_filter($this->apiRoutes(), function (LaravelRoute $route): bool { + if ($this->isStreamOrGeneratedFileRoute($route->uri())) { + return false; + } + + return collect($route->gatherMiddleware())->contains(function (string $middleware): bool { + return str_contains($middleware, 'auth:api') + || str_contains($middleware, 'jwt.auth') + || str_contains($middleware, 'admin.access') + || str_contains($middleware, 'parent.access') + || str_contains($middleware, 'teacher.portal.auth'); + }); + })); + } + + private function isStreamOrGeneratedFileRoute(string $uri): bool + { + return str_contains($uri, 'files/') + || str_contains($uri, 'receipts/') + || str_contains($uri, 'reimbursements/') + || str_contains($uri, 'attachments/') + || str_contains($uri, '/pdf') + || str_contains($uri, '/csv'); + } + + private function primaryMethod(LaravelRoute $route): string + { + foreach ($route->methods() as $method) { + if (! in_array($method, ['HEAD', 'OPTIONS'], true)) { + return $method; + } + } + + return 'GET'; + } + + private function uriWithSafePlaceholders(string $uri): string + { + return preg_replace('/\{[^}]+\}/', '1', $uri) ?? $uri; + } +} diff --git a/tests/Feature/Api/ApiUserManagementFeatureTest.php b/tests/Feature/Api/ApiUserManagementFeatureTest.php index 5c352464..20058411 100644 --- a/tests/Feature/Api/ApiUserManagementFeatureTest.php +++ b/tests/Feature/Api/ApiUserManagementFeatureTest.php @@ -2,6 +2,7 @@ namespace Tests\Feature\Api; +use App\Models\Role; use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Hash; diff --git a/tests/Feature/Api/V1/Administrator/AdministratorAbsenceControllerTest.php b/tests/Feature/Api/V1/Administrator/AdministratorAbsenceControllerTest.php index 203c6b48..cc354eab 100644 --- a/tests/Feature/Api/V1/Administrator/AdministratorAbsenceControllerTest.php +++ b/tests/Feature/Api/V1/Administrator/AdministratorAbsenceControllerTest.php @@ -121,4 +121,4 @@ class AdministratorAbsenceControllerTest extends TestCase 'dates' => ['2026-03-08'], ]); } -} +} \ No newline at end of file diff --git a/tests/Feature/Api/V1/Administrator/AdministratorEnrollmentControllerTest.php b/tests/Feature/Api/V1/Administrator/AdministratorEnrollmentControllerTest.php index 06c5daee..85c81f08 100644 --- a/tests/Feature/Api/V1/Administrator/AdministratorEnrollmentControllerTest.php +++ b/tests/Feature/Api/V1/Administrator/AdministratorEnrollmentControllerTest.php @@ -128,4 +128,4 @@ class AdministratorEnrollmentControllerTest extends TestCase 'message' => 'Enrollment statuses updated and notifications sent.', ]); } -} +} \ No newline at end of file diff --git a/tests/Feature/Api/V1/Administrator/AdministratorEnrollmentUpdateStatusesApiTest.php b/tests/Feature/Api/V1/Administrator/AdministratorEnrollmentUpdateStatusesApiTest.php index 41e287ce..15744d66 100644 --- a/tests/Feature/Api/V1/Administrator/AdministratorEnrollmentUpdateStatusesApiTest.php +++ b/tests/Feature/Api/V1/Administrator/AdministratorEnrollmentUpdateStatusesApiTest.php @@ -263,7 +263,6 @@ class AdministratorEnrollmentUpdateStatusesApiTest extends TestCase private function postUpdateStatuses(array $enrollmentStatus) { $query = http_build_query(['enrollment_status' => $enrollmentStatus]); - - return $this->post('/api/v1/administrator/enrollment-withdrawal/update-statuses?'.$query, []); + return $this->post('/api/v1/administrator/enrollment-withdrawal/update-statuses?' . $query, []); } } diff --git a/tests/Feature/Api/V1/Administrator/AdministratorNotificationControllerTest.php b/tests/Feature/Api/V1/Administrator/AdministratorNotificationControllerTest.php index 3b7dae6b..136c57e6 100644 --- a/tests/Feature/Api/V1/Administrator/AdministratorNotificationControllerTest.php +++ b/tests/Feature/Api/V1/Administrator/AdministratorNotificationControllerTest.php @@ -157,4 +157,4 @@ class AdministratorNotificationControllerTest extends TestCase 'message' => 'Print notification recipients updated.', ]); } -} +} \ No newline at end of file diff --git a/tests/Feature/Api/V1/Administrator/AdministratorTeacherSubmissionControllerTest.php b/tests/Feature/Api/V1/Administrator/AdministratorTeacherSubmissionControllerTest.php index 9ca1e12b..26020d38 100644 --- a/tests/Feature/Api/V1/Administrator/AdministratorTeacherSubmissionControllerTest.php +++ b/tests/Feature/Api/V1/Administrator/AdministratorTeacherSubmissionControllerTest.php @@ -2,10 +2,10 @@ namespace Tests\Feature\Api\Administrator; -use App\Http\Middleware\EnsurePrintRequestsAdminAccess; use App\Models\User; use App\Services\Administrator\TeacherSubmissionNotificationService; use App\Services\Administrator\TeacherSubmissionReportService; +use App\Http\Middleware\EnsurePrintRequestsAdminAccess; use Illuminate\Foundation\Testing\RefreshDatabase; use Laravel\Sanctum\Sanctum; use Mockery; diff --git a/tests/Feature/Api/V1/Administrator/AdministratorTeacherSubmissionReportApiTest.php b/tests/Feature/Api/V1/Administrator/AdministratorTeacherSubmissionReportApiTest.php index 8aae8e46..7aae2821 100644 --- a/tests/Feature/Api/V1/Administrator/AdministratorTeacherSubmissionReportApiTest.php +++ b/tests/Feature/Api/V1/Administrator/AdministratorTeacherSubmissionReportApiTest.php @@ -281,7 +281,7 @@ class AdministratorTeacherSubmissionReportApiTest extends TestCase 'class_section_id' => 12, 'admin_id' => 901, 'notification_category' => 'teacher_submissions', - 'message' => 'Reminder '.$i, + 'message' => 'Reminder ' . $i, 'status' => 'sent', 'school_year' => '2025-2026', 'semester' => 'Fall', diff --git a/tests/Feature/Api/V1/Administrator/EmergencyContactControllerTest.php b/tests/Feature/Api/V1/Administrator/EmergencyContactControllerTest.php index 4ecd47f4..6da928a2 100644 --- a/tests/Feature/Api/V1/Administrator/EmergencyContactControllerTest.php +++ b/tests/Feature/Api/V1/Administrator/EmergencyContactControllerTest.php @@ -80,7 +80,7 @@ class EmergencyContactControllerTest extends TestCase ]); Sanctum::actingAs($admin, [], 'api'); - $response = $this->patch('/api/v1/administrator/emergency-contacts/'.$contact->id, [ + $response = $this->patch('/api/v1/administrator/emergency-contacts/' . $contact->id, [ 'parent_id' => $parent->id, 'name' => 'New Name', 'cellphone' => '5555555555', @@ -116,7 +116,7 @@ class EmergencyContactControllerTest extends TestCase ]); Sanctum::actingAs($admin, [], 'api'); - $response = $this->delete('/api/v1/administrator/emergency-contacts/'.$contact->id, [ + $response = $this->delete('/api/v1/administrator/emergency-contacts/' . $contact->id, [ 'parent_id' => $parent->id, ], [ 'Accept' => 'application/json', @@ -145,7 +145,7 @@ class EmergencyContactControllerTest extends TestCase ]); Sanctum::actingAs($admin, [], 'api'); - $response = $this->get('/api/v1/administrator/emergency-contacts/'.$contact->id.'?parent_id='.$parent->id, [ + $response = $this->get('/api/v1/administrator/emergency-contacts/' . $contact->id . '?parent_id=' . $parent->id, [ 'Accept' => 'application/json', ]); @@ -171,7 +171,7 @@ class EmergencyContactControllerTest extends TestCase ]); Sanctum::actingAs($admin, [], 'api'); - $response = $this->patch('/api/v1/administrator/emergency-contacts/'.$contact->id, [ + $response = $this->patch('/api/v1/administrator/emergency-contacts/' . $contact->id, [ 'parent_id' => $otherParent->id, 'name' => 'New Name', 'cellphone' => '9999999999', @@ -205,7 +205,7 @@ class EmergencyContactControllerTest extends TestCase ]); Sanctum::actingAs($admin, [], 'api'); - $response = $this->delete('/api/v1/administrator/emergency-contacts/'.$contact->id, [ + $response = $this->delete('/api/v1/administrator/emergency-contacts/' . $contact->id, [ 'parent_id' => $otherParent->id, ], [ 'Accept' => 'application/json', diff --git a/tests/Feature/Api/V1/Assignment/AssignmentApiControllerTest.php b/tests/Feature/Api/V1/Assignment/AssignmentApiControllerTest.php index 1ee1b586..3a37da73 100644 --- a/tests/Feature/Api/V1/Assignment/AssignmentApiControllerTest.php +++ b/tests/Feature/Api/V1/Assignment/AssignmentApiControllerTest.php @@ -16,17 +16,11 @@ class AssignmentApiControllerTest extends TestCase use RefreshDatabase; protected User $user; - protected User $teacherMain; - protected User $teacherAssistant; - protected ClassSection $sectionA; - protected ClassSection $sectionB; - protected Student $studentOne; - protected Student $studentTwo; protected function setUp(): void @@ -424,4 +418,4 @@ class AssignmentApiControllerTest extends TestCase $this->assertSame(['Main Teacher'], $mainTeachers); } -} +} \ No newline at end of file diff --git a/tests/Feature/Api/V1/AssignmentApiTest.php b/tests/Feature/Api/V1/AssignmentApiTest.php index 64c3f6a1..c66523d4 100644 --- a/tests/Feature/Api/V1/AssignmentApiTest.php +++ b/tests/Feature/Api/V1/AssignmentApiTest.php @@ -2,12 +2,12 @@ namespace Tests\Feature\Api; -use App\Models\ClassSection; -use App\Models\Configuration; +use App\Models\User; use App\Models\Student; use App\Models\StudentClass; use App\Models\TeacherClass; -use App\Models\User; +use App\Models\ClassSection; +use App\Models\Configuration; use Illuminate\Foundation\Testing\RefreshDatabase; use Laravel\Sanctum\Sanctum; use PHPUnit\Framework\Attributes\Test; @@ -48,36 +48,36 @@ class AssignmentApiTest extends TestCase $teacher = User::factory()->create([ 'firstname' => 'Ahmad', - 'lastname' => 'Ali', + 'lastname' => 'Ali', ]); TeacherClass::query()->create([ - 'teacher_id' => $teacher->id, - 'class_section_id' => $section->class_section_id, - 'position' => 'main', - 'semester' => 'Fall', - 'school_year' => '2025-2026', - 'description' => 'Main teacher assignment', + 'teacher_id' => $teacher->id, + 'class_section_id' => $section->class_section_id, + 'position' => 'main', + 'semester' => 'Fall', + 'school_year' => '2025-2026', + 'description' => 'Main teacher assignment', ]); $student = Student::factory()->create([ - 'firstname' => 'Sara', - 'lastname' => 'Yousef', - 'is_active' => 1, - 'gender' => 'F', + 'firstname' => 'Sara', + 'lastname' => 'Yousef', + 'is_active' => 1, + 'gender' => 'F', 'registration_grade' => '5', - 'photo_consent' => 1, - 'tuition_paid' => 1, - 'school_id' => 'SCH-1001', + 'photo_consent' => 1, + 'tuition_paid' => 1, + 'school_id' => 'SCH-1001', ]); StudentClass::query()->create([ - 'student_id' => $student->id, - 'class_section_id' => $section->class_section_id, - 'semester' => 'Fall', - 'school_year' => '2025-2026', - 'description' => 'Student assigned', - 'is_active' => 1, // remove if your table doesn’t have this column + 'student_id' => $student->id, + 'class_section_id' => $section->class_section_id, + 'semester' => 'Fall', + 'school_year' => '2025-2026', + 'description' => 'Student assigned', + 'is_active' => 1, // remove if your table doesn’t have this column ]); $response = $this->getJson('/api/assignments'); @@ -96,7 +96,7 @@ class AssignmentApiTest extends TestCase 'semester', 'school_year', 'description', - ], + ] ], 'schoolYears', 'selectedYear', @@ -122,19 +122,19 @@ class AssignmentApiTest extends TestCase $teacher = User::factory()->create(['firstname' => 'Test', 'lastname' => 'Teacher']); TeacherClass::query()->create([ - 'teacher_id' => $teacher->id, + 'teacher_id' => $teacher->id, 'class_section_id' => $section1->class_section_id, - 'position' => 'main', - 'semester' => 'Fall', - 'school_year' => '2024-2025', + 'position' => 'main', + 'semester' => 'Fall', + 'school_year' => '2024-2025', ]); TeacherClass::query()->create([ - 'teacher_id' => $teacher->id, + 'teacher_id' => $teacher->id, 'class_section_id' => $section2->class_section_id, - 'position' => 'main', - 'semester' => 'Fall', - 'school_year' => '2025-2026', + 'position' => 'main', + 'semester' => 'Fall', + 'school_year' => '2025-2026', ]); $response = $this->getJson('/api/assignments?school_year=2025-2026'); @@ -158,11 +158,11 @@ class AssignmentApiTest extends TestCase $section = ClassSection::factory()->create(); $payload = [ - 'student_id' => $student->id, + 'student_id' => $student->id, 'class_section_id' => $section->class_section_id, - 'semester' => 'Spring', - 'school_year' => '2025-2026', - 'description' => 'Moved after placement review', + 'semester' => 'Spring', + 'school_year' => '2025-2026', + 'description' => 'Moved after placement review', ]; $response = $this->postJson('/api/assignments', $payload); @@ -171,10 +171,10 @@ class AssignmentApiTest extends TestCase ->assertJsonPath('message', 'Assignment saved successfully.'); $this->assertDatabaseHas('student_class', [ - 'student_id' => $student->id, + 'student_id' => $student->id, 'class_section_id' => $section->class_section_id, - 'semester' => 'Spring', - 'school_year' => '2025-2026', + 'semester' => 'Spring', + 'school_year' => '2025-2026', ]); } @@ -203,19 +203,19 @@ class AssignmentApiTest extends TestCase // First create $this->postJson('/api/assignments', [ - 'student_id' => $student->id, + 'student_id' => $student->id, 'class_section_id' => $sectionA->class_section_id, - 'semester' => 'Fall', - 'school_year' => '2025-2026', + 'semester' => 'Fall', + 'school_year' => '2025-2026', ])->assertCreated(); // Second call same unique key should update (per service updateOrCreate) $this->postJson('/api/assignments', [ - 'student_id' => $student->id, + 'student_id' => $student->id, 'class_section_id' => $sectionA->class_section_id, - 'semester' => 'Fall', - 'school_year' => '2025-2026', - 'description' => 'Reassigned', + 'semester' => 'Fall', + 'school_year' => '2025-2026', + 'description' => 'Reassigned', ])->assertCreated(); $this->assertEquals( @@ -228,10 +228,10 @@ class AssignmentApiTest extends TestCase ); $this->assertDatabaseHas('student_class', [ - 'student_id' => $student->id, + 'student_id' => $student->id, 'class_section_id' => $sectionA->class_section_id, - 'semester' => 'Fall', - 'school_year' => '2025-2026', + 'semester' => 'Fall', + 'school_year' => '2025-2026', ]); } diff --git a/tests/Feature/Api/V1/Attendance/LateSlipLogsControllerTest.php b/tests/Feature/Api/V1/Attendance/LateSlipLogsControllerTest.php index d88fef78..42b16cd4 100644 --- a/tests/Feature/Api/V1/Attendance/LateSlipLogsControllerTest.php +++ b/tests/Feature/Api/V1/Attendance/LateSlipLogsControllerTest.php @@ -113,7 +113,7 @@ class LateSlipLogsControllerTest extends TestCase $user = User::query()->create([ 'firstname' => 'Test', 'lastname' => 'User', - 'email' => $roleName.'@example.com', + 'email' => $roleName . '@example.com', 'cellphone' => '5555555555', 'address_street' => '123 Main', 'city' => 'City', diff --git a/tests/Feature/Api/V1/AttendanceCommentTemplateApiTest.php b/tests/Feature/Api/V1/AttendanceCommentTemplateApiTest.php index 511a4de6..f99f0e41 100644 --- a/tests/Feature/Api/V1/AttendanceCommentTemplateApiTest.php +++ b/tests/Feature/Api/V1/AttendanceCommentTemplateApiTest.php @@ -85,10 +85,10 @@ class AttendanceCommentTemplateApiTest extends TestCase $mock->shouldReceive('create') ->once() ->with(Mockery::on(function ($data) use ($payload) { - return (int) $data['min_score'] === 80 - && (int) $data['max_score'] === 100 + return (int)$data['min_score'] === 80 + && (int)$data['max_score'] === 100 && $data['template_text'] === $payload['template_text'] - && (bool) $data['is_active'] === true; + && (bool)$data['is_active'] === true; })) ->andReturn([ 'id' => 10, @@ -159,7 +159,7 @@ class AttendanceCommentTemplateApiTest extends TestCase ->once() ->with(7, Mockery::on(function ($data) { return $data['template_text'] === 'Updated comment text' - && (bool) $data['is_active'] === false; + && (bool)$data['is_active'] === false; })) ->andReturn([ 'id' => 7, @@ -221,7 +221,7 @@ class AttendanceCommentTemplateApiTest extends TestCase 'max_score' => 49, 'template_text' => 'Low attendance', 'is_active' => true, - ], + ] ]); $this->app->instance(AttendanceCommentTemplateService::class, $mock); @@ -232,4 +232,4 @@ class AttendanceCommentTemplateApiTest extends TestCase ->assertJsonCount(1, 'templates') ->assertJsonPath('templates.0.id', 1); } -} +} \ No newline at end of file diff --git a/tests/Feature/Api/V1/AttendanceTracking/AttendanceTrackingControllerTest.php b/tests/Feature/Api/V1/AttendanceTracking/AttendanceTrackingControllerTest.php index 2c5ee2e2..bfbc9683 100644 --- a/tests/Feature/Api/V1/AttendanceTracking/AttendanceTrackingControllerTest.php +++ b/tests/Feature/Api/V1/AttendanceTracking/AttendanceTrackingControllerTest.php @@ -4,7 +4,6 @@ namespace Tests\Feature\Api\V1\AttendanceTracking; use App\Services\AttendanceTracking\AttendanceTrackingService; use Mockery; -use Mockery\MockInterface; use Tests\TestCase; class AttendanceTrackingControllerTest extends TestCase @@ -15,7 +14,7 @@ class AttendanceTrackingControllerTest extends TestCase parent::tearDown(); } - protected function mockService(): MockInterface + protected function mockService(): \Mockery\MockInterface { $service = Mockery::mock(AttendanceTrackingService::class); $this->app->instance(AttendanceTrackingService::class, $service); diff --git a/tests/Feature/Api/V1/Auth/IpBanControllerTest.php b/tests/Feature/Api/V1/Auth/IpBanControllerTest.php index 834829c8..d12badd0 100644 --- a/tests/Feature/Api/V1/Auth/IpBanControllerTest.php +++ b/tests/Feature/Api/V1/Auth/IpBanControllerTest.php @@ -159,7 +159,7 @@ class IpBanControllerTest extends TestCase $user = User::query()->create([ 'firstname' => 'Test', 'lastname' => 'User', - 'email' => $roleName.'@example.com', + 'email' => $roleName . '@example.com', 'cellphone' => '5555555555', 'address_street' => '123 Main', 'city' => 'City', diff --git a/tests/Feature/Api/V1/BadgeControllerTest.php b/tests/Feature/Api/V1/BadgeControllerTest.php index d4361a44..8b6853f4 100644 --- a/tests/Feature/Api/V1/BadgeControllerTest.php +++ b/tests/Feature/Api/V1/BadgeControllerTest.php @@ -234,4 +234,4 @@ class BadgeControllerTest extends TestCase $this->postJson('/api/badges/log-print', ['user_ids' => [1]])->assertStatus(401); $this->postJson('/api/badges/pdf', ['user_ids' => [1]])->assertStatus(401); } -} +} \ No newline at end of file diff --git a/tests/Feature/Api/V1/BroadcastEmail/BroadcastEmailControllerTest.php b/tests/Feature/Api/V1/BroadcastEmail/BroadcastEmailControllerTest.php index e5018890..33ddaf1a 100644 --- a/tests/Feature/Api/V1/BroadcastEmail/BroadcastEmailControllerTest.php +++ b/tests/Feature/Api/V1/BroadcastEmail/BroadcastEmailControllerTest.php @@ -31,8 +31,7 @@ class BroadcastEmailControllerTest extends TestCase { $this->seedParentData(); - $fake = new class extends EmailService - { + $fake = new class extends EmailService { public array $sent = []; public function send( @@ -42,9 +41,9 @@ class BroadcastEmailControllerTest extends TestCase string $fromKey = 'general', array $cc = [], array $bcc = [] - ): bool { + ): bool + { $this->sent[] = compact('to', 'subject', 'html', 'fromKey', 'cc', 'bcc'); - return true; } }; diff --git a/tests/Feature/Api/V1/ClassProgress/ClassProgressControllerTest.php b/tests/Feature/Api/V1/ClassProgress/ClassProgressControllerTest.php index bf26a65d..cea23d1d 100644 --- a/tests/Feature/Api/V1/ClassProgress/ClassProgressControllerTest.php +++ b/tests/Feature/Api/V1/ClassProgress/ClassProgressControllerTest.php @@ -92,7 +92,7 @@ class ClassProgressControllerTest extends TestCase Sanctum::actingAs($user); - $response = $this->getJson('/api/v1/class-progress/'.$report->id); + $response = $this->getJson('/api/v1/class-progress/' . $report->id); $response->assertOk(); $response->assertJsonPath('status', true); @@ -110,7 +110,7 @@ class ClassProgressControllerTest extends TestCase Sanctum::actingAs($user); - $response = $this->patchJson('/api/v1/class-progress/'.$report->id, [ + $response = $this->patchJson('/api/v1/class-progress/' . $report->id, [ 'status' => 'behind', ]); @@ -132,7 +132,7 @@ class ClassProgressControllerTest extends TestCase Sanctum::actingAs($user); - $response = $this->deleteJson('/api/v1/class-progress/'.$report->id); + $response = $this->deleteJson('/api/v1/class-progress/' . $report->id); $response->assertOk(); $this->assertDatabaseMissing('class_progress_reports', [ @@ -152,7 +152,7 @@ class ClassProgressControllerTest extends TestCase Sanctum::actingAs($other); - $response = $this->getJson('/api/v1/class-progress/'.$report->id); + $response = $this->getJson('/api/v1/class-progress/' . $report->id); $response->assertStatus(403); } @@ -187,7 +187,7 @@ class ClassProgressControllerTest extends TestCase Sanctum::actingAs($user); - $response = $this->get('/api/v1/class-progress/attachments/'.$attachment->id); + $response = $this->get('/api/v1/class-progress/attachments/' . $attachment->id); $response->assertOk(); } diff --git a/tests/Feature/Api/V1/Classes/ClassSectionControllerTest.php b/tests/Feature/Api/V1/Classes/ClassSectionControllerTest.php index 34700615..3deb5d96 100644 --- a/tests/Feature/Api/V1/Classes/ClassSectionControllerTest.php +++ b/tests/Feature/Api/V1/Classes/ClassSectionControllerTest.php @@ -257,7 +257,7 @@ class ClassSectionControllerTest extends TestCase 'firstname' => 'Test', 'lastname' => 'User', 'cellphone' => '5555555555', - 'email' => $roleName.'@example.com', + 'email' => $roleName . '@example.com', 'address_street' => '123 Main', 'city' => 'City', 'state' => 'ST', diff --git a/tests/Feature/Api/V1/Communication/CommunicationControllerTest.php b/tests/Feature/Api/V1/Communication/CommunicationControllerTest.php index 340bab06..dddf92bb 100644 --- a/tests/Feature/Api/V1/Communication/CommunicationControllerTest.php +++ b/tests/Feature/Api/V1/Communication/CommunicationControllerTest.php @@ -50,8 +50,7 @@ class CommunicationControllerTest extends TestCase { $this->seedCommunicationData(); - $fake = new class extends EmailService - { + $fake = new class extends EmailService { public array $sent = []; public function send( @@ -63,7 +62,6 @@ class CommunicationControllerTest extends TestCase array $bcc = [] ): bool { $this->sent[] = compact('to', 'subject', 'html', 'fromKey', 'cc', 'bcc'); - return true; } }; diff --git a/tests/Feature/Api/V1/Family/FamilyAdminControllerTest.php b/tests/Feature/Api/V1/Family/FamilyAdminControllerTest.php index 472794fc..06903981 100644 --- a/tests/Feature/Api/V1/Family/FamilyAdminControllerTest.php +++ b/tests/Feature/Api/V1/Family/FamilyAdminControllerTest.php @@ -23,7 +23,7 @@ class FamilyAdminControllerTest extends TestCase $this->seedFamilyGuardian($familyId, $user->id); Sanctum::actingAs($user); - $response = $this->getJson('/api/v1/family-admin?student_id='.$studentId); + $response = $this->getJson('/api/v1/family-admin?student_id=' . $studentId); $response->assertOk(); $response->assertJsonPath('status', true); @@ -58,7 +58,7 @@ class FamilyAdminControllerTest extends TestCase $this->seedPayment($user->id); Sanctum::actingAs($user); - $response = $this->getJson('/api/v1/family-admin/card?family_id='.$familyId); + $response = $this->getJson('/api/v1/family-admin/card?family_id=' . $familyId); $response->assertOk(); $response->assertJsonPath('status', true); @@ -123,7 +123,7 @@ class FamilyAdminControllerTest extends TestCase 'firstname' => 'Parent', 'lastname' => 'User', 'cellphone' => '9999999999', - 'email' => 'parent'.$id.'@example.com', + 'email' => 'parent' . $id . '@example.com', 'address_street' => '123 Street', 'city' => 'City', 'state' => 'ST', @@ -163,7 +163,7 @@ class FamilyAdminControllerTest extends TestCase { return DB::table('families')->insertGetId([ 'family_code' => $code, - 'household_name' => 'Family '.$code, + 'household_name' => 'Family ' . $code, 'is_active' => 1, 'created_at' => now(), 'updated_at' => now(), diff --git a/tests/Feature/Api/V1/Family/FamilyControllerTest.php b/tests/Feature/Api/V1/Family/FamilyControllerTest.php index 735433dd..f9b1a40c 100644 --- a/tests/Feature/Api/V1/Family/FamilyControllerTest.php +++ b/tests/Feature/Api/V1/Family/FamilyControllerTest.php @@ -20,7 +20,7 @@ class FamilyControllerTest extends TestCase $this->seedFamilyStudent($familyId, $studentId); Sanctum::actingAs($user); - $response = $this->getJson('/api/v1/families/by-student/'.$studentId); + $response = $this->getJson('/api/v1/families/by-student/' . $studentId); $response->assertOk(); $response->assertJsonPath('status', true); @@ -34,7 +34,7 @@ class FamilyControllerTest extends TestCase $this->seedFamilyGuardian($familyId, $user->id); Sanctum::actingAs($user); - $response = $this->getJson('/api/v1/families/'.$familyId.'/guardians'); + $response = $this->getJson('/api/v1/families/' . $familyId . '/guardians'); $response->assertOk(); $response->assertJsonPath('status', true); @@ -292,7 +292,7 @@ class FamilyControllerTest extends TestCase { return DB::table('families')->insertGetId([ 'family_code' => $code, - 'household_name' => 'Family '.$code, + 'household_name' => 'Family ' . $code, 'is_active' => 1, 'created_at' => now(), 'updated_at' => now(), diff --git a/tests/Feature/Api/V1/Finance/EventChargeControllerTest.php b/tests/Feature/Api/V1/Finance/EventChargeControllerTest.php index 7947f84a..7d8300a6 100644 --- a/tests/Feature/Api/V1/Finance/EventChargeControllerTest.php +++ b/tests/Feature/Api/V1/Finance/EventChargeControllerTest.php @@ -28,7 +28,7 @@ class EventChargeControllerTest extends TestCase { $charge = $this->makeCharge(); - $this->getJson('/api/v1/finance/event-charges/'.$charge->id) + $this->getJson('/api/v1/finance/event-charges/' . $charge->id) ->assertStatus(401); } @@ -37,7 +37,7 @@ class EventChargeControllerTest extends TestCase $this->actingAsApiAdministrator(); $charge = $this->makeCharge(); - $this->getJson('/api/v1/finance/event-charges/'.$charge->id) + $this->getJson('/api/v1/finance/event-charges/' . $charge->id) ->assertOk() ->assertJsonPath('ok', true) ->assertJsonPath('event_charge.id', $charge->id) @@ -57,7 +57,7 @@ class EventChargeControllerTest extends TestCase $this->actingAsApiAdministrator(); $charge = $this->makeCharge(); - $this->putJson('/api/v1/finance/event-charges/'.$charge->id, [ + $this->putJson('/api/v1/finance/event-charges/' . $charge->id, [ 'event_name' => 'Museum Visit', 'amount' => 40.50, ]) @@ -76,7 +76,7 @@ class EventChargeControllerTest extends TestCase $this->actingAsApiAdministrator(); $charge = $this->makeCharge(['charged' => 0]); - $this->postJson('/api/v1/finance/event-charges/'.$charge->id.'/approve') + $this->postJson('/api/v1/finance/event-charges/' . $charge->id . '/approve') ->assertOk() ->assertJsonPath('ok', true); @@ -91,7 +91,7 @@ class EventChargeControllerTest extends TestCase $this->actingAsApiAdministrator(); $charge = $this->makeCharge(['charged' => 1]); - $this->postJson('/api/v1/finance/event-charges/'.$charge->id.'/void') + $this->postJson('/api/v1/finance/event-charges/' . $charge->id . '/void') ->assertOk() ->assertJsonPath('ok', true); @@ -106,7 +106,7 @@ class EventChargeControllerTest extends TestCase $this->actingAsApiAdministrator(); $charge = $this->makeCharge(['charged' => 1]); - $this->deleteJson('/api/v1/finance/event-charges/'.$charge->id) + $this->deleteJson('/api/v1/finance/event-charges/' . $charge->id) ->assertOk() ->assertJsonPath('ok', true); diff --git a/tests/Feature/Api/V1/Frontend/FrontendControllerTest.php b/tests/Feature/Api/V1/Frontend/FrontendControllerTest.php index 22a00ea8..fb68a7b2 100644 --- a/tests/Feature/Api/V1/Frontend/FrontendControllerTest.php +++ b/tests/Feature/Api/V1/Frontend/FrontendControllerTest.php @@ -49,7 +49,7 @@ class FrontendControllerTest extends TestCase $user = User::query()->create([ 'firstname' => 'Test', 'lastname' => 'User', - 'email' => $roleName.'@example.com', + 'email' => $roleName . '@example.com', 'cellphone' => '5555555555', 'address_street' => '123 Main', 'city' => 'City', diff --git a/tests/Feature/Api/V1/Frontend/InfoIconControllerTest.php b/tests/Feature/Api/V1/Frontend/InfoIconControllerTest.php index e04e2247..ebd0a5e2 100644 --- a/tests/Feature/Api/V1/Frontend/InfoIconControllerTest.php +++ b/tests/Feature/Api/V1/Frontend/InfoIconControllerTest.php @@ -4,6 +4,7 @@ namespace Tests\Feature\Api\V1\Frontend; use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Support\Facades\DB; use Laravel\Sanctum\Sanctum; use Tests\TestCase; diff --git a/tests/Feature/Api/V1/Frontend/LandingPageControllerTest.php b/tests/Feature/Api/V1/Frontend/LandingPageControllerTest.php index cbe73476..d60af1b5 100644 --- a/tests/Feature/Api/V1/Frontend/LandingPageControllerTest.php +++ b/tests/Feature/Api/V1/Frontend/LandingPageControllerTest.php @@ -57,7 +57,7 @@ class LandingPageControllerTest extends TestCase $user = User::query()->create([ 'firstname' => 'Test', 'lastname' => 'User', - 'email' => $roleName.'@example.com', + 'email' => $roleName . '@example.com', 'cellphone' => '5555555555', 'address_street' => '123 Main', 'city' => 'City', diff --git a/tests/Feature/Api/V1/Frontend/PageControllerTest.php b/tests/Feature/Api/V1/Frontend/PageControllerTest.php index 2b7df224..538d3cea 100644 --- a/tests/Feature/Api/V1/Frontend/PageControllerTest.php +++ b/tests/Feature/Api/V1/Frontend/PageControllerTest.php @@ -15,7 +15,7 @@ class PageControllerTest extends TestCase { $dir = public_path('html'); File::ensureDirectoryExists($dir); - File::put($dir.'/privacy_policy.html', '

Privacy

'); + File::put($dir . '/privacy_policy.html', '

Privacy

'); $response = $this->getJson('/api/v1/pages/privacy'); @@ -28,7 +28,7 @@ class PageControllerTest extends TestCase { $dir = public_path('html'); File::ensureDirectoryExists($dir); - File::delete($dir.'/help_center.html'); + File::delete($dir . '/help_center.html'); $response = $this->getJson('/api/v1/pages/help'); diff --git a/tests/Feature/Api/V1/FullSurface/ApiAcademicAttendanceFullSurfaceContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiAcademicAttendanceFullSurfaceContractTest.php new file mode 100644 index 00000000..a7579826 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiAcademicAttendanceFullSurfaceContractTest.php @@ -0,0 +1,173 @@ +seedE2EConfiguration(); + $world = $this->seedTeacherClassWithStudent(9301); + $this->actingAs($world['teacher'], 'api'); + + foreach ($this->teacherGetEndpoints($world) as $uri) { + $response = $this->getJson($uri); + $this->assertNoServerError($response, 'Teacher GET ' . $uri); + $this->assertStatusIn($response, [200, 204, 302, 400, 401, 403, 404, 405, 409, 419, 422], 'Teacher GET ' . $uri); + } + + foreach ($this->teacherMutationEndpoints($world) as [$method, $uri, $payload]) { + $response = $this->json($method, $uri, $payload); + $this->assertNoServerError($response, 'Teacher ' . $method . ' ' . $uri); + $this->assertStatusIn($response, [200, 201, 202, 204, 302, 400, 401, 403, 404, 405, 409, 419, 422], 'Teacher ' . $method . ' ' . $uri); + } + + $this->actingAsApiAdministrator(); + foreach ($this->adminAcademicEndpoints($world) as [$method, $uri, $payload]) { + $response = $this->json($method, $uri, $payload); + $this->assertNoServerError($response, 'Admin academic ' . $method . ' ' . $uri); + $this->assertStatusIn($response, [200, 201, 202, 204, 302, 400, 401, 403, 404, 405, 409, 419, 422], 'Admin academic ' . $method . ' ' . $uri); + } + } + + /** @param array $world @return list */ + private function teacherGetEndpoints(array $world): array + { + $classSectionId = (int) $world['class_section_id']; + $studentId = (int) $world['student_id']; + + return [ + '/api/v1/teachers/classes?class_section_id=' . $classSectionId, + '/api/v1/teacher/class-view?class_section_id=' . $classSectionId, + '/api/v1/teacher/no-classes', + '/api/v1/teacher/select-semester', + '/api/v1/teacher/homework-list?class_section_id=' . $classSectionId, + '/api/v1/teacher/add-homework?class_section_id=' . $classSectionId, + '/api/v1/teacher/add-quiz?class_section_id=' . $classSectionId, + '/api/v1/teacher/add-quiz-column-form?class_section_id=' . $classSectionId, + '/api/v1/teacher/add-midterm-exam?class_section_id=' . $classSectionId, + '/api/v1/teacher/add-final-exam?class_section_id=' . $classSectionId, + '/api/v1/teacher/add-participation?class_section_id=' . $classSectionId, + '/api/v1/teacher/add-project?class_section_id=' . $classSectionId, + '/api/v1/teacher/teacher-assignment', + '/api/v1/teacher/exam-drafts', + '/api/v1/teacher/drafts', + '/api/v1/teacher/inventory/books/distribute', + '/api/v1/teacher/print-requests/context', + '/api/v1/teacher/absence-vacation', + '/api/v1/teacher/showupdate-attendance?class_section_id=' . $classSectionId, + '/api/v1/teacher/showupdate_attendance?class_section_id=' . $classSectionId, + '/api/v1/teacher/scores?class_section_id=' . $classSectionId, + '/api/v1/teacher/calendar', + '/api/v1/teacher/class-progress-submit?class_section_id=' . $classSectionId, + '/api/v1/teacher/class-progress-history?class_section_id=' . $classSectionId, + '/api/v1/teacher/class-progress-view?class_section_id=' . $classSectionId, + '/api/v1/teacher/competition-scores', + '/api/v1/attendance/teacher/grid?class_section_id=' . $classSectionId, + '/api/v1/attendance/teacher/form?class_section_id=' . $classSectionId, + '/api/v1/attendance/admin/daily?date=2025-10-05&class_section_id=' . $classSectionId, + '/api/v1/attendance/staff/month?month=2025-10', + '/api/v1/attendance/staff/admins', + '/api/v1/attendance/late-slip-logs', + '/api/v1/attendance/late-slip-logs/1', + '/api/v1/attendance/early-dismissals', + '/api/v1/attendance/early-dismissals/student-options', + '/api/v1/attendance/management/dashboard', + '/api/v1/attendance-tracking/pending-violations', + '/api/v1/attendance-tracking/notified-violations', + '/api/v1/attendance-tracking/student-case/' . $studentId, + '/api/v1/attendance-tracking/compose', + '/api/v1/attendance-tracking/parents-info', + '/api/v1/attendance-comment-templates', + '/api/v1/attendance-comment-templates/list-data', + '/api/v1/attendance-comment-templates/1', + '/api/v1/attendance-templates', + '/api/v1/assignments', + '/api/v1/assignments/class-assignment-data', + '/api/v1/scores/overview?class_section_id=' . $classSectionId, + '/api/v1/scores/homework?class_section_id=' . $classSectionId, + '/api/v1/scores/quiz?class_section_id=' . $classSectionId, + '/api/v1/scores/project?class_section_id=' . $classSectionId, + '/api/v1/scores/midterm?class_section_id=' . $classSectionId, + '/api/v1/scores/final?class_section_id=' . $classSectionId, + '/api/v1/scores/participation?class_section_id=' . $classSectionId, + '/api/v1/grading/homework-tracking?class_section_id=' . $classSectionId, + '/api/v1/reports/report-cards/meta?class_section_id=' . $classSectionId, + ]; + } + + /** @param array $world @return list}> */ + private function teacherMutationEndpoints(array $world): array + { + $studentId = (int) $world['student_id']; + $classSectionId = (int) $world['class_section_id']; + $scorePayload = ['class_section_id' => $classSectionId, 'student_id' => $studentId, 'score' => 95, 'points' => 10, 'column' => 'E2E Probe']; + + return [ + ['POST', '/api/v1/attendance/teacher/submit', ['class_section_id' => $classSectionId, 'attendance' => [$studentId => 'present'], 'date' => '2025-10-05']], + ['POST', '/api/v1/attendance/admin/update', ['student_id' => $studentId, 'status' => 'present', 'date' => '2025-10-05']], + ['POST', '/api/v1/attendance/admin/add-entry', ['student_id' => $studentId, 'status' => 'present', 'date' => '2025-10-05']], + ['POST', '/api/v1/attendance/staff/admins/save', ['attendance' => []]], + ['POST', '/api/v1/attendance/staff/cell', ['user_id' => $world['teacher']->id, 'date' => '2025-10-05', 'status' => 'present']], + ['DELETE', '/api/v1/attendance/late-slip-logs/1', []], + ['POST', '/api/v1/attendance/early-dismissals', ['student_id' => $studentId, 'reason' => 'Contract probe', 'dismissal_time' => '12:00']], + ['POST', '/api/v1/attendance/early-dismissals/signature', ['student_id' => $studentId, 'signature' => 'data:image/png;base64,ZmFrZQ==']], + ['POST', '/api/v1/attendance/management/scan', ['badge_code' => 'INVALID-BADGE']], + ['POST', '/api/v1/attendance/management/manual-entry', ['student_id' => $studentId, 'status' => 'present']], + ['POST', '/api/v1/attendance/management/exit-entry', ['student_id' => $studentId]], + ['POST', '/api/v1/attendance/management/absent', ['student_id' => $studentId]], + ['POST', '/api/v1/attendance/management/1/follow-up', ['notes' => 'Contract probe']], + ['POST', '/api/v1/attendance/management/1/late-slip-reprint', ['reason' => 'Contract probe']], + ['POST', '/api/v1/attendance-tracking/record', ['student_id' => $studentId, 'violation_type' => 'late', 'date' => '2025-10-05']], + ['POST', '/api/v1/attendance-tracking/send-auto-emails', ['student_ids' => [$studentId]]], + ['POST', '/api/v1/attendance-tracking/send-manual-email', ['student_id' => $studentId, 'message' => 'Contract probe']], + ['POST', '/api/v1/attendance-tracking/save-notification-note', ['student_id' => $studentId, 'note' => 'Contract probe']], + ['POST', '/api/v1/attendance-comment-templates', ['name' => 'Probe', 'comment' => 'Contract probe']], + ['PUT', '/api/v1/attendance-comment-templates/1', ['name' => 'Probe Updated', 'comment' => 'Contract probe updated']], + ['DELETE', '/api/v1/attendance-comment-templates/1', []], + ['POST', '/api/v1/attendance-templates/save', ['name' => 'Probe', 'comment' => 'Contract probe']], + ['POST', '/api/v1/attendance-templates/delete', ['id' => 1]], + ['POST', '/api/v1/assignments', ['class_section_id' => $classSectionId, 'title' => 'Contract Assignment']], + ['POST', '/api/v1/teacher/add-homework', $scorePayload], + ['POST', '/api/v1/teacher/add-quiz', $scorePayload], + ['POST', '/api/v1/teacher/add-quiz-column-form', ['class_section_id' => $classSectionId, 'name' => 'Probe Quiz']], + ['POST', '/api/v1/teacher/add-midterm-exam', $scorePayload], + ['POST', '/api/v1/teacher/add-final-exam', $scorePayload], + ['POST', '/api/v1/teacher/add-participation', $scorePayload], + ['POST', '/api/v1/teacher/add-project', $scorePayload], + ['POST', '/api/v1/teacher/exam-drafts', ['class_section_id' => $classSectionId, 'title' => 'Contract Draft', 'content' => 'Draft body']], + ['POST', '/api/v1/teacher/drafts', ['class_section_id' => $classSectionId, 'title' => 'Contract Draft', 'content' => 'Draft body']], + ['POST', '/api/v1/teacher/inventory/books/distribute', ['student_id' => $studentId, 'item_id' => 1, 'quantity' => 1]], + ['POST', '/api/v1/teacher/absence-vacation', ['date' => '2025-10-05', 'reason' => 'Contract probe']], + ['POST', '/api/v1/teacher/class-progress-submit', ['class_section_id' => $classSectionId, 'student_id' => $studentId, 'progress' => 'Good']], + ]; + } + + /** @param array $world @return list}> */ + private function adminAcademicEndpoints(array $world): array + { + return [ + ['GET', '/api/v1/reports/report-cards?student_id=' . $world['student_id'], []], + ['GET', '/api/v1/reports/stickers?student_id=' . $world['student_id'], []], + ['GET', '/api/v1/badges/form-options', []], + ['POST', '/api/v1/print-requests', ['title' => 'Contract Probe', 'copies' => 1, 'needed_by' => '2025-10-12']], + ['GET', '/api/v1/print-requests/admin', []], + ['PATCH', '/api/v1/print-requests/1/status', ['status' => 'completed']], + ['GET', '/api/v1/print-requests/teacher', []], + ['PATCH', '/api/v1/print-requests/1', ['title' => 'Contract Probe Updated']], + ['DELETE', '/api/v1/print-requests/1', []], + ]; + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiAccountStatusLifecycleContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiAccountStatusLifecycleContractTest.php new file mode 100644 index 00000000..fae87403 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiAccountStatusLifecycleContractTest.php @@ -0,0 +1,31 @@ +apiRoutesContainingAny(['auth/me', 'frontend', 'dashboard', 'profile', 'preferences', 'notifications']), 0, 80); + $statuses = ['suspended', 'disabled', 'inactive', 'pending', 'blocked']; + + foreach ($statuses as $status) { + $actor = $this->createApiUserWithRole('parent', ['status' => $status]); + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + if ($method !== 'GET') { + continue; + } + + $uri = $route->uri(); + $response = $this->requestAs($actor, 'GET', $uri); + + $this->assertControlled($response, 'GET', $uri); + $this->assertNoServerError($response, 'account status ' . $status . ' at GET ' . $uri); + } + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiAdminOperationsFullSurfaceContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiAdminOperationsFullSurfaceContractTest.php new file mode 100644 index 00000000..5e572473 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiAdminOperationsFullSurfaceContractTest.php @@ -0,0 +1,163 @@ +seedE2EConfiguration(); + $world = $this->seedTeacherClassWithStudent(9201); + $admin = $this->actingAsApiAdministrator(); + + foreach ($this->adminGetEndpoints($world) as $uri) { + $response = $this->getJson($uri); + $this->assertNoServerError($response, 'Admin GET ' . $uri); + $this->assertStatusIn($response, [200, 204, 302, 400, 401, 403, 404, 405, 409, 419, 422], 'Admin GET ' . $uri); + } + + foreach ($this->adminMutationEndpoints($world, $admin->id) as [$method, $uri, $payload]) { + $response = $this->json($method, $uri, $payload); + $this->assertNoServerError($response, 'Admin ' . $method . ' ' . $uri); + $this->assertStatusIn($response, [200, 201, 202, 204, 302, 400, 401, 403, 404, 405, 409, 419, 422], 'Admin ' . $method . ' ' . $uri); + } + } + + /** + * @param array $world + * @return list + */ + private function adminGetEndpoints(array $world): array + { + $studentId = (int) $world['student_id']; + $classSectionId = (int) $world['class_section_id']; + + return [ + '/api/v1/administrator/attendance-templates', + '/api/v1/administrator/absence', + '/api/v1/administrator/dashboard/metrics', + '/api/v1/administrator/dashboard/user-search?query=Fixture', + '/api/v1/administrator/teacher-submissions', + '/api/v1/administrator/trophy', + '/api/v1/administrator/trophy/winners', + '/api/v1/administrator/trophy/final', + '/api/v1/administrator/teacher-class/assignments', + '/api/v1/administrator/notifications/alerts', + '/api/v1/administrator/notifications/print-recipients', + '/api/v1/administrator/enrollment-withdrawal', + '/api/v1/administrator/enrollment-withdrawal/new-students', + '/api/v1/administrator/enroll-withdrawal', + '/api/v1/administrator/enroll-withdrawal/new-students', + '/api/v1/administrator/promotions', + '/api/v1/administrator/promotions/summary', + '/api/v1/administrator/promotions/levels', + '/api/v1/administrator/promotions/1', + '/api/v1/administrator/promotions/1/reminders', + '/api/v1/administrator/promotions/1/audit', + '/api/v1/administrator/emergency-contacts', + '/api/v1/administrator/emergency-contacts/data', + '/api/v1/administrator/emergency-contacts/1', + '/api/v1/administrator/staff', + '/api/v1/administrator/staff/1', + '/api/v1/administrator/flags/pending', + '/api/v1/administrator/flags/form-meta', + '/api/v1/administrator/flags/students/1', + '/api/v1/administrator/flags/processed', + '/api/v1/administrator/flags/analysis', + '/api/v1/administrator/grading/homework-tracking?class_section_id=' . $classSectionId, + '/api/v1/administrator/expenses', + '/api/v1/administrator/expenses/options', + '/api/v1/administrator/expenses/1', + '/api/v1/administrator/reports/financial-detailed', + '/api/v1/administrator/reports/stakeholder-analysis', + '/api/v1/administrator/reports/parent-payment-followups', + '/api/v1/administrator/invoices/management', + '/api/v1/administrator/paypal-transactions', + '/api/v1/administrator/reimbursements/under-processing', + '/api/v1/administrator/reimbursements/export', + '/api/v1/administrator/reimbursements/reimbursed-expenses', + '/api/v1/administrator/reimbursements', + '/api/v1/administrator/printables/badges/form-options', + '/api/v1/administrator/printables/report-card/meta', + '/api/v1/administrator/certificates/dashboard', + '/api/v1/administrator/certificates/form-options', + '/api/v1/administrator/certificates/audit-log', + '/api/v1/administrator/certificates/reprint/CERT-E2E-001', + '/api/v1/administrator/role-permission/roles', + '/api/v1/administrator/role-permission/roles/1', + '/api/v1/administrator/role-permission/users', + '/api/v1/administrator/role-permission/permissions', + '/api/v1/administrator/role-permission/permissions/1', + '/api/v1/administrator/role-permission/roles/1/permissions', + '/api/v1/school-years', + '/api/v1/school-years/current', + '/api/v1/school-years/options', + '/api/v1/school-years/1/summary', + '/api/v1/school-years/1/parent-balances', + '/api/v1/school-years/1/promotion-preview', + '/api/v1/class-sections', + '/api/v1/classes', + '/api/v1/users', + '/api/v1/users/' . $world['parent_id'], + '/api/v1/students/' . $studentId, + ]; + } + + /** + * @param array $world + * @return list}> + */ + private function adminMutationEndpoints(array $world, int $adminId): array + { + $studentId = (int) $world['student_id']; + $teacherId = (int) $world['teacher']->id; + $classSectionId = (int) $world['class_section_id']; + + return [ + ['POST', '/api/v1/administrator/absence', ['user_id' => $adminId, 'date' => '2025-10-05', 'reason' => 'Contract probe']], + ['POST', '/api/v1/administrator/teacher-submissions/notify', ['teacher_id' => $teacherId, 'message' => 'Contract probe']], + ['POST', '/api/v1/administrator/teacher-class/assign', ['teacher_id' => $teacherId, 'class_section_id' => $classSectionId, 'position' => 'main']], + ['POST', '/api/v1/administrator/teacher-class/delete', ['teacher_id' => $teacherId, 'class_section_id' => $classSectionId]], + ['POST', '/api/v1/administrator/notifications/alerts', ['alerts' => ['contract_probe' => true]]], + ['POST', '/api/v1/administrator/notifications/print-recipients', ['recipients' => [$adminId]]], + ['POST', '/api/v1/administrator/enrollment-withdrawal/update-statuses', ['enrollment_status' => [$studentId => 'enrolled']]], + ['POST', '/api/v1/administrator/promotions/evaluate', ['student_ids' => [$studentId]]], + ['POST', '/api/v1/administrator/promotions/reminders/dispatch', ['dry_run' => true]], + ['POST', '/api/v1/administrator/promotions/deadlines/expire', ['dry_run' => true]], + ['POST', '/api/v1/administrator/promotions/deadlines', ['deadline' => '2025-12-01']], + ['POST', '/api/v1/administrator/promotions/placement/preview', ['student_ids' => [$studentId]]], + ['POST', '/api/v1/administrator/promotions/placement/batches/1/finalize', ['confirm' => true]], + ['POST', '/api/v1/administrator/promotions/levels', ['from_level' => '1', 'to_level' => '2']], + ['POST', '/api/v1/administrator/promotions/levels/seed', ['confirm' => true]], + ['PATCH', '/api/v1/administrator/promotions/1/status', ['status' => 'approved']], + ['PATCH', '/api/v1/administrator/promotions/1/enrollment-steps', ['steps' => ['confirmed' => true]]], + ['PATCH', '/api/v1/administrator/emergency-contacts/1', ['name' => 'Contract Contact', 'phone' => '5551234567']], + ['DELETE', '/api/v1/administrator/emergency-contacts/1', []], + ['POST', '/api/v1/administrator/staff', ['firstname' => 'Staff', 'lastname' => 'Probe', 'email' => 'staff.probe@example.test']], + ['PATCH', '/api/v1/administrator/staff/1', ['firstname' => 'Staff Updated']], + ['DELETE', '/api/v1/administrator/staff/1', []], + ['POST', '/api/v1/administrator/flags', ['student_id' => $studentId, 'reason' => 'Contract probe']], + ['POST', '/api/v1/administrator/flags/1/state', ['state' => 'reviewing']], + ['POST', '/api/v1/administrator/flags/1/close', ['resolution' => 'closed']], + ['POST', '/api/v1/administrator/flags/1/cancel', ['reason' => 'cancelled']], + ['POST', '/api/v1/school-years', ['name' => '2026-2027', 'start_date' => '2026-09-01', 'end_date' => '2027-06-30']], + ['PATCH', '/api/v1/school-years/1', ['status' => 'active']], + ['POST', '/api/v1/school-years/1/validate-close', ['confirm' => true]], + ['POST', '/api/v1/school-years/1/preview-close', ['confirm' => true]], + ['POST', '/api/v1/school-years/1/close', ['confirm' => true]], + ['POST', '/api/v1/school-years/1/reopen', ['confirm' => true]], + ]; + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiAttachmentUploadContentSafetyContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiAttachmentUploadContentSafetyContractTest.php new file mode 100644 index 00000000..573fba77 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiAttachmentUploadContentSafetyContractTest.php @@ -0,0 +1,55 @@ +apiRoutesContainingAny(['upload', 'import', 'attachment', 'document', 'avatar', 'photo', 'file']); + + $this->assertNotEmpty($routes, 'Upload-like routes should be covered by content safety tests.'); + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + + if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) { + continue; + } + + foreach (['file', 'attachment', 'document', 'import_file', 'avatar'] as $field) { + $payload = $this->payloadFor($method, $route->uri()) + [ + $field => UploadedFile::fake()->create('malware.php', 4, 'application/x-php'), + ]; + + $response = $this->actingAs($this->actorFor($route->uri()), 'api') + ->json($method, $this->materializePath($route->uri()), $payload); + + $this->assertStatusIn($response, [400, 403, 404, 409, 413, 415, 422], "{$method} {$route->uri()} should reject executable {$field}."); + $this->assertStringNotContainsString('getContent()); + } + } + } + + public function test_download_and_file_response_routes_do_not_allow_path_traversal_parameters(): void + { + foreach ($this->apiRoutesContainingAny(['download', 'export', 'file', 'document', 'receipt', 'certificate']) as $route) { + $method = $this->primaryMethod($route); + + if ($method !== 'GET') { + continue; + } + + $path = $this->materializePath($route->uri()) . '?filename=../../.env&path=../../storage/logs/laravel.log'; + $response = $this->actingAs($this->actorFor($route->uri()), 'api')->getJson($path); + + $this->assertStatusIn($response, [200, 204, 400, 401, 403, 404, 409, 422], "GET {$route->uri()} path traversal probe"); + $body = $response->getContent(); + $this->assertStringNotContainsString('APP_KEY=', $body); + $this->assertStringNotContainsString('DB_PASSWORD=', $body); + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiAuditLogAndAdministrativeTraceabilityContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiAuditLogAndAdministrativeTraceabilityContractTest.php new file mode 100644 index 00000000..f4ad154c --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiAuditLogAndAdministrativeTraceabilityContractTest.php @@ -0,0 +1,46 @@ +apiRoutesContainingAny(['users', 'students', 'school-years', 'finance', 'inventory', 'permissions', 'roles', 'settings']); + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + if (! in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'], true)) { + continue; + } + + $uri = $route->uri(); + $response = $this->requestAs($this->admin, $method, $uri, $this->payloadFor($method, $uri)); + + $this->assertNoServerError($response, "$method $uri admin mutation traceability"); + $this->assertControlled($response, $method, $uri); + $this->assertStringNotContainsStringIgnoringCase('audit_log_id', $response->getContent(), $uri . ' should not leak raw internal audit identifiers by accident.'); + $this->assertStringNotContainsStringIgnoringCase('stack_trace', $response->getContent(), $uri . ' should not leak stack traces in mutation responses.'); + } + } + + public function test_audit_and_log_routes_are_not_mutable_by_public_or_low_privilege_users(): void + { + $routes = $this->apiRoutesContainingAny(['audit', 'logs', 'activity', 'history']); + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + $uri = $route->uri(); + + $public = $this->json($method, $this->materializePath($uri), $this->payloadFor($method, $uri)); + $parent = $this->requestAs($this->parent, $method, $uri, $this->payloadFor($method, $uri)); + + $this->assertNoServerError($public, "$method $uri public audit route"); + $this->assertNoServerError($parent, "$method $uri parent audit route"); + $this->assertStatusIn($public, [302, 400, 401, 403, 404, 405, 419, 422], "$method $uri public audit route"); + $this->assertStatusIn($parent, [302, 400, 401, 403, 404, 405, 419, 422], "$method $uri parent audit route"); + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiAuthenticationLifecycleDepthContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiAuthenticationLifecycleDepthContractTest.php new file mode 100644 index 00000000..a623a1d4 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiAuthenticationLifecycleDepthContractTest.php @@ -0,0 +1,90 @@ +postJson($path, [ + 'email' => $this->admin->email, + 'password' => 'password', + ]); + + $this->assertNoServerError($response, 'valid login at ' . $path); + + if ($response->getStatusCode() >= 200 && $response->getStatusCode() < 300) { + $this->assertTrue( + $response->json('token') !== null || $response->json('access_token') !== null || $response->json('user') !== null, + $path . ' must return a usable login contract, not a mystery envelope.' + ); + } + } + + foreach ($loginRoutes as $path) { + $response = $this->postJson($path, [ + 'email' => $this->admin->email, + 'password' => 'definitely-wrong-password', + ]); + + $this->assertStatusIn($response, [400, 401, 403, 419, 422], 'invalid login at ' . $path); + $this->assertNoServerError($response, 'invalid login at ' . $path); + } + + foreach (['/api/v1/auth/me', '/api/v1/me', '/api/user'] as $path) { + $response = $this->actingAs($this->admin, 'api')->getJson($path); + $this->assertNoServerError($response, 'authenticated identity lookup at ' . $path); + + if ($response->getStatusCode() === 200) { + $this->assertTrue( + $response->json('user') !== null || $response->json('id') !== null || $response->json('data') !== null, + $path . ' must expose a stable identity payload.' + ); + } + } + + foreach (['/api/v1/auth/logout', '/api/logout', '/user/logout'] as $path) { + $first = $this->actingAs($this->admin, 'api')->postJson($path); + $second = $this->actingAs($this->admin, 'api')->postJson($path); + + $this->assertNoServerError($first, 'first logout at ' . $path); + $this->assertNoServerError($second, 'repeat logout at ' . $path); + $this->assertStatusIn($first, [200, 202, 204, 302, 401, 404, 419, 422], 'first logout at ' . $path); + $this->assertStatusIn($second, [200, 202, 204, 302, 401, 404, 419, 422], 'repeat logout at ' . $path); + } + } + + public function test_password_reset_and_registration_like_routes_do_not_leak_account_existence(): void + { + $routes = $this->apiRoutesContainingAny(['forgot', 'password', 'reset', 'register', 'verify']); + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) { + continue; + } + + $uri = $route->uri(); + $response = $this->json($method, $this->materializePath($uri), [ + 'email' => 'nobody.' . uniqid() . '@example.test', + 'password' => 'password', + 'password_confirmation' => 'not-the-same', + 'token' => 'not-a-real-token', + ]); + + $this->assertControlled($response, $method, $uri); + $this->assertNoServerError($response, $method . ' ' . $uri); + $this->assertStringNotContainsStringIgnoringCase('select * from', $response->getContent(), $uri . ' must not leak account lookup SQL.'); + $this->assertStringNotContainsStringIgnoringCase('No query results for model', $response->getContent(), $uri . ' must not leak framework model lookups.'); + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiAuthorizationCacheInvalidationContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiAuthorizationCacheInvalidationContractTest.php new file mode 100644 index 00000000..5f567ec5 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiAuthorizationCacheInvalidationContractTest.php @@ -0,0 +1,29 @@ +apiRoutesContainingAny(['roles', 'permissions', 'users', 'settings', 'school-years', 'finance', 'inventory']), 0, 100); + + foreach ($privilegedRoutes as $route) { + $method = $this->primaryMethod($route); + $uri = $route->uri(); + $payload = $this->payloadFor($method, $uri) + [ + 'role' => 'administrator', + 'roles' => ['administrator'], + 'permissions' => ['*', 'admin.*', 'users.delete'], + 'is_admin' => true, + ]; + + $response = $this->requestAs($this->parent, $method, $uri, $payload); + + $this->assertStatusIn($response, [401, 403, 404, 405, 419, 422], 'parent privilege escalation should be blocked at ' . $method . ' ' . $uri); + $this->assertNoServerError($response, 'permission cache escalation probe at ' . $method . ' ' . $uri); + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiBatch11RouteRegressionNetTest.php b/tests/Feature/Api/V1/FullSurface/ApiBatch11RouteRegressionNetTest.php new file mode 100644 index 00000000..fab61286 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiBatch11RouteRegressionNetTest.php @@ -0,0 +1,59 @@ + ['auth', 'login', 'logout', 'frontend'], + 'school_year' => ['school-year', 'school_year', 'semester'], + 'academic' => ['scores', 'grading', 'homework', 'quiz', 'report-card'], + 'attendance' => ['attendance', 'absence', 'late', 'dismissal'], + 'finance' => ['finance', 'invoice', 'payment', 'refund'], + 'inventory' => ['inventory', 'supplier', 'supply'], + 'communications' => ['messages', 'support', 'email', 'whatsapp', 'notification'], + 'family' => ['parent', 'family', 'guardian', 'authorized'], + 'admin' => ['admin', 'users', 'roles', 'permissions', 'settings'], + 'files' => ['import', 'export', 'download', 'print', 'certificate', 'badge'], + ]; + + foreach ($families as $family => $needles) { + $this->assertNotEmpty( + $this->apiRoutesContainingAny($needles), + 'Batch 11 expects at least one route for the ' . $family . ' family. If this fails, update the route catalog instead of pretending the family vanished.' + ); + } + } + + public function test_batch_11_high_risk_routes_return_controlled_responses_under_combined_hostile_input(): void + { + $routes = array_slice($this->apiRoutesContainingAny(['auth', 'attendance', 'finance', 'students', 'users', 'settings', 'export', 'import', 'webhook', 'messages']), 0, 160); + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + $uri = $route->uri(); + $payload = $this->payloadFor($method, $uri) + [ + 'id' => '../../../etc/passwd', + 'student_id' => ['bad' => true], + 'amount' => 'Infinity', + 'notes' => '', + 'url' => 'http://169.254.169.254/latest/meta-data/', + 'return_url' => 'https://evil.example', + ]; + + $this->actingAs($this->actorFor($uri), 'api'); + $response = $this->withHeaders([ + 'Accept' => 'application/json', + 'X-Forwarded-For' => '127.0.0.1', + 'Idempotency-Key' => 'batch-11-regression-net', + ])->json($method, $this->materializePath($uri), $payload); + + $this->assertControlled($response, $method, $uri); + $this->assertNoServerError($response, 'batch 11 combined hostile probe at ' . $method . ' ' . $uri); + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiBatch12AcademicGradeBoundaryContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiBatch12AcademicGradeBoundaryContractTest.php new file mode 100644 index 00000000..3f070081 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiBatch12AcademicGradeBoundaryContractTest.php @@ -0,0 +1,49 @@ +apiRoutesContainingAny(['score', 'grade', 'homework', 'quiz', 'project', 'midterm', 'final', 'participation', 'report-card']), 0, 160); + $scores = [-1, 101, 'A+++', 'NaN', 'Infinity', 999999]; + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) { + continue; + } + + foreach ($scores as $score) { + $response = $this->requestAs($this->teacher, $method, $route->uri(), $this->payloadFor($method, $route->uri()) + [ + 'score' => $score, + 'points' => $score, + 'student_id' => $this->ids['invoiceId'], + 'class_section_id' => $this->ids['invoiceId'], + ]); + + $this->assertControlled($response, $method, $route->uri()); + $this->assertNoServerError($response, 'academic grade boundary '.$method.' '.$route->uri()); + } + } + } + + public function test_locked_or_finalized_grade_routes_do_not_accept_low_privilege_overrides(): void + { + $routes = array_slice($this->apiRoutesContainingAny(['lock', 'unlock', 'finalize', 'publish', 'report-card']), 0, 100); + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + if (! in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'], true)) { + continue; + } + + $response = $this->requestAs($this->parent, $method, $route->uri(), $this->payloadFor($method, $route->uri()) + ['force' => true, 'locked' => false]); + $this->assertStatusIn($response, [401, 403, 404, 405, 409, 419, 422], 'parent grade finalization override '.$method.' '.$route->uri()); + $this->assertNoServerError($response, 'parent grade finalization override '.$method.' '.$route->uri()); + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiBatch12AttachmentLifecycleContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiBatch12AttachmentLifecycleContractTest.php new file mode 100644 index 00000000..6fb6941e --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiBatch12AttachmentLifecycleContractTest.php @@ -0,0 +1,56 @@ +apiRoutesContainingAny(['upload', 'attachment', 'document', 'import', 'media', 'file']), 0, 120); + $files = [ + UploadedFile::fake()->create('normal.pdf', 12, 'application/pdf'), + UploadedFile::fake()->create('image-as-pdf.pdf', 12, 'image/png'), + UploadedFile::fake()->create('script.php', 1, 'application/x-php'), + ]; + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) { + continue; + } + + foreach ($files as $file) { + $response = $this->requestAs($this->actorFor($route->uri()), $method, $route->uri(), $this->payloadFor($method, $route->uri()) + [ + 'file' => $file, + 'attachment' => $file, + 'document' => $file, + ]); + + $this->assertControlled($response, $method, $route->uri()); + $this->assertNoServerError($response, 'attachment lifecycle '.$method.' '.$route->uri()); + } + } + } + + public function test_file_download_routes_reject_path_and_storage_driver_probing(): void + { + $routes = array_slice($this->apiRoutesContainingAny(['download', 'file', 'media', 'receipt', 'certificate', 'badge']), 0, 100); + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + $response = $this->requestAs($this->actorFor($route->uri()), $method, $route->uri(), [ + 'path' => '../../.env', + 'disk' => 's3', + 'filename' => '../storage/logs/laravel.log', + ]); + + $this->assertControlled($response, $method, $route->uri()); + $this->assertNoServerError($response, 'file path probe '.$method.' '.$route->uri()); + $this->assertStringNotContainsString('APP_KEY', $response->getContent(), 'download route exposed environment content'); + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiBatch12AttendanceCorrectionAuditContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiBatch12AttendanceCorrectionAuditContractTest.php new file mode 100644 index 00000000..2ff77558 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiBatch12AttendanceCorrectionAuditContractTest.php @@ -0,0 +1,51 @@ +apiRoutesContainingAny(['attendance', 'absence', 'late', 'dismissal', 'slip']), 0, 150); + $payloads = [ + ['status' => 'present', 'reason' => ''], + ['status' => 'teleported', 'reason' => 'nonsense'], + ['date' => '1900-01-01', 'reason' => 'too old'], + ['date' => '2099-12-31', 'reason' => 'future'], + ['student_id' => $this->ids['invoiceId'], 'reason' => 'wrong id type'], + ]; + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) { + continue; + } + + foreach ($payloads as $payload) { + $response = $this->requestAs($this->teacher, $method, $route->uri(), $this->payloadFor($method, $route->uri()) + $payload); + $this->assertControlled($response, $method, $route->uri()); + $this->assertNoServerError($response, 'attendance correction audit '.$method.' '.$route->uri()); + } + } + } + + public function test_parent_and_student_cannot_correct_attendance_records(): void + { + $routes = array_slice($this->apiRoutesContainingAny(['attendance', 'absence', 'late', 'dismissal']), 0, 100); + + foreach ([$this->parent, $this->studentUser] as $actor) { + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + if (! in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'], true)) { + continue; + } + + $response = $this->requestAs($actor, $method, $route->uri(), $this->payloadFor($method, $route->uri())); + $this->assertStatusIn($response, [401, 403, 404, 405, 409, 419, 422], 'low privilege attendance correction '.$method.' '.$route->uri()); + $this->assertNoServerError($response, 'low privilege attendance correction '.$method.' '.$route->uri()); + } + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiBatch12BulkMutationBoundaryContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiBatch12BulkMutationBoundaryContractTest.php new file mode 100644 index 00000000..4abd86d1 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiBatch12BulkMutationBoundaryContractTest.php @@ -0,0 +1,47 @@ +apiRoutesContainingAny(['bulk', 'batch', 'assign', 'remove', 'delete', 'archive', 'restore', 'import']), 0, 140); + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + if (! in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'], true)) { + continue; + } + + foreach ([ + ['ids' => []], + ['ids' => [$this->ids['studentId'], $this->ids['invoiceId'], $this->ids['classSectionId']]], + ['student_ids' => [$this->ids['invoiceId']], 'invoice_ids' => [$this->ids['studentId']]], + ] as $payload) { + $response = $this->requestAs($this->actorFor($route->uri()), $method, $route->uri(), $this->payloadFor($method, $route->uri()) + $payload); + $this->assertControlled($response, $method, $route->uri()); + $this->assertNoServerError($response, 'bulk boundary '.$method.' '.$route->uri()); + } + } + } + + public function test_bulk_routes_do_not_accept_unbounded_target_lists(): void + { + $routes = array_slice($this->apiRoutesContainingAny(['bulk', 'batch', 'assign', 'import']), 0, 100); + $ids = range(1, 750); + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) { + continue; + } + + $response = $this->requestAs($this->actorFor($route->uri()), $method, $route->uri(), $this->payloadFor($method, $route->uri()) + ['ids' => $ids, 'student_ids' => $ids]); + $this->assertControlled($response, $method, $route->uri()); + $this->assertNoServerError($response, 'unbounded bulk list '.$method.' '.$route->uri()); + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiBatch12CombinedRegressionSweepTest.php b/tests/Feature/Api/V1/FullSurface/ApiBatch12CombinedRegressionSweepTest.php new file mode 100644 index 00000000..ff53099c --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiBatch12CombinedRegressionSweepTest.php @@ -0,0 +1,51 @@ +apiRoutesContainingAny([ + 'auth', 'users', 'roles', 'permissions', 'students', 'parents', 'family', + 'attendance', 'scores', 'report-card', 'finance', 'invoice', 'payment', + 'inventory', 'import', 'export', 'webhook', 'settings', 'messages', + ]), 0, 220); + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + $uri = $route->uri(); + $payload = $this->payloadFor($method, $uri) + [ + 'scope' => 'all', + 'force' => true, + 'is_admin' => true, + 'role' => 'administrator', + 'student_id' => $this->ids['invoiceId'], + 'invoice_id' => $this->ids['studentId'], + 'amount' => '1e309', + 'status' => '', + 'return_url' => 'https://evil.example/after-login', + 'callback_url' => 'http://169.254.169.254/latest/meta-data/', + 'fields' => ['*', 'password', 'remember_token'], + ]; + + $this->actingAs($this->actorFor($uri), 'api'); + $response = $this->withHeaders([ + 'Accept' => 'application/json', + 'X-Admin' => '1', + 'X-Original-Method' => 'DELETE', + 'Idempotency-Key' => 'batch-12-combined-regression', + ])->json($method, $this->materializePath($uri), $payload); + + $this->assertControlled($response, $method, $uri); + $this->assertNoServerError($response, 'batch 12 combined regression '.$method.' '.$uri); + + $body = $response->getContent(); + $this->assertStringNotContainsString('SQLSTATE', $body, 'batch 12 response leaked SQL at '.$uri); + $this->assertStringNotContainsString('APP_KEY', $body, 'batch 12 response leaked env at '.$uri); + $this->assertStringNotContainsString('remember_token', strtolower($body), 'batch 12 response leaked auth internals at '.$uri); + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiBatch12ConsistencyUnderPartialPayloadContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiBatch12ConsistencyUnderPartialPayloadContractTest.php new file mode 100644 index 00000000..6492ea8c --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiBatch12ConsistencyUnderPartialPayloadContractTest.php @@ -0,0 +1,46 @@ +apiRoutes(), 0, 250); + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + if (! in_array($method, ['PUT', 'PATCH'], true)) { + continue; + } + + $response = $this->requestAs($this->actorFor($route->uri()), $method, $route->uri(), ['notes' => 'partial batch 12 update']); + $this->assertControlled($response, $method, $route->uri()); + $this->assertNoServerError($response, 'partial payload update '.$method.' '.$route->uri()); + + if ($response->getStatusCode() >= 200 && $response->getStatusCode() < 300) { + $body = strtolower($response->getContent()); + $this->assertStringNotContainsString('"id":null', $body, 'successful partial update nulled id'); + $this->assertStringNotContainsString('"user_id":null', $body, 'successful partial update nulled owner'); + } + } + } + + public function test_post_routes_handle_sparse_payloads_as_validation_not_crashes(): void + { + $routes = array_slice($this->apiRoutes(), 0, 250); + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + if ($method !== 'POST') { + continue; + } + + $response = $this->requestAs($this->actorFor($route->uri()), $method, $route->uri(), ['name' => null]); + $this->assertControlled($response, $method, $route->uri()); + $this->assertNoServerError($response, 'sparse post payload '.$method.' '.$route->uri()); + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiBatch12CrossActorDataLeakageContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiBatch12CrossActorDataLeakageContractTest.php new file mode 100644 index 00000000..0d1ed337 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiBatch12CrossActorDataLeakageContractTest.php @@ -0,0 +1,53 @@ +apiRoutesContainingAny(['students', 'parents', 'teacher', 'attendance', 'invoice', 'report-card', 'messages', 'profile']), 0, 150); + $sensitiveKeys = ['salary', 'permissions', 'roles_raw', 'password', 'remember_token', 'deleted_at', 'created_by', 'updated_by', 'internal_notes']; + + foreach ([$this->parent, $this->teacher, $this->studentUser] as $actor) { + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + if ($method !== 'GET') { + continue; + } + + $response = $this->requestAs($actor, $method, $route->uri()); + $this->assertControlled($response, $method, $route->uri()); + $this->assertNoServerError($response, 'cross actor leak '.$method.' '.$route->uri()); + + $body = strtolower($response->getContent()); + foreach ($sensitiveKeys as $key) { + $this->assertStringNotContainsString($key, $body, $route->uri().' leaked sensitive key '.$key); + } + } + } + } + + public function test_actor_cannot_select_a_different_owner_scope_with_query_parameters(): void + { + $routes = array_slice($this->apiRoutesContainingAny(['students', 'attendance', 'invoice', 'family', 'messages', 'report-card']), 0, 120); + + foreach ($routes as $route) { + if ($this->primaryMethod($route) !== 'GET') { + continue; + } + + $response = $this->actingAs($this->parent, 'api')->json('GET', $this->materializePath($route->uri()), [ + 'parent_id' => $this->ids['parentId'] + 999, + 'student_id' => $this->ids['studentId'] + 999, + 'family_id' => $this->ids['familyId'] + 999, + 'scope' => 'all', + ]); + + $this->assertControlled($response, 'GET', $route->uri()); + $this->assertNoServerError($response, 'owner query override '.$route->uri()); + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiBatch12FinancialLedgerIntegrityContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiBatch12FinancialLedgerIntegrityContractTest.php new file mode 100644 index 00000000..dfa602c3 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiBatch12FinancialLedgerIntegrityContractTest.php @@ -0,0 +1,51 @@ +apiRoutesContainingAny(['finance', 'invoice', 'payment', 'refund', 'charge', 'installment', 'fee']), 0, 150); + $amounts = ['-0.01', '-999999', '999999999999999999999', '1e309', 'NaN', 'Infinity', '0.00000001']; + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) { + continue; + } + + foreach ($amounts as $amount) { + $response = $this->requestAs($this->actorFor($route->uri()), $method, $route->uri(), $this->payloadFor($method, $route->uri()) + [ + 'amount' => $amount, + 'paid_amount' => $amount, + 'balance' => $amount, + 'invoice_id' => $this->ids['studentId'], + ]); + + $this->assertControlled($response, $method, $route->uri()); + $this->assertNoServerError($response, 'financial ledger boundary '.$method.' '.$route->uri()); + $this->assertStringNotContainsString('SQLSTATE', $response->getContent(), 'financial route leaked SQL'); + } + } + } + + public function test_parent_cannot_force_finance_scope_to_all_families(): void + { + $routes = array_slice($this->apiRoutesContainingAny(['invoice', 'payment', 'finance', 'receipt', 'balance']), 0, 100); + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + $response = $this->requestAs($this->parent, $method, $route->uri(), $this->payloadFor($method, $route->uri()) + [ + 'scope' => 'all', + 'parent_id' => $this->ids['parentId'] + 777, + 'include_voided' => true, + ]); + + $this->assertControlled($response, $method, $route->uri()); + $this->assertNoServerError($response, 'parent finance scope override '.$method.' '.$route->uri()); + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiBatch12GuardianDelegationExpirationContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiBatch12GuardianDelegationExpirationContractTest.php new file mode 100644 index 00000000..f4b94c2b --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiBatch12GuardianDelegationExpirationContractTest.php @@ -0,0 +1,48 @@ +apiRoutesContainingAny(['guardian', 'authorized', 'family', 'pickup', 'whatsapp']), 0, 130); + $payloads = [ + ['expires_at' => '1900-01-01', 'relationship' => ''], + ['expires_at' => 'not-a-date', 'relationship' => ''], + ['student_id' => $this->ids['invoiceId'], 'parent_id' => $this->ids['teacherId']], + ['can_pickup' => 'yes please', 'can_receive_messages' => ['bad' => true]], + ]; + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) { + continue; + } + + foreach ($payloads as $payload) { + $response = $this->requestAs($this->parent, $method, $route->uri(), $this->payloadFor($method, $route->uri()) + $payload); + $this->assertControlled($response, $method, $route->uri()); + $this->assertNoServerError($response, 'guardian delegation '.$method.' '.$route->uri()); + } + } + } + + public function test_student_cannot_manage_guardians_or_authorized_users(): void + { + $routes = array_slice($this->apiRoutesContainingAny(['guardian', 'authorized', 'family']), 0, 100); + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + if (! in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'], true)) { + continue; + } + + $response = $this->requestAs($this->studentUser, $method, $route->uri(), $this->payloadFor($method, $route->uri())); + $this->assertStatusIn($response, [401, 403, 404, 405, 419, 422], 'student guardian mutation '.$method.' '.$route->uri()); + $this->assertNoServerError($response, 'student guardian mutation '.$method.' '.$route->uri()); + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiBatch12LongRunningExportJobContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiBatch12LongRunningExportJobContractTest.php new file mode 100644 index 00000000..63e40d1b --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiBatch12LongRunningExportJobContractTest.php @@ -0,0 +1,41 @@ +apiRoutesContainingAny(['export', 'download', 'print', 'generate', 'report', 'certificate', 'badge', 'receipt']), 0, 160); + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + $payload = $this->payloadFor($method, $route->uri()) + [ + 'async' => true, + 'notify_when_ready' => true, + 'format' => 'xlsx', + 'fields' => ['*', 'password', 'remember_token'], + 'filters' => ['scope' => 'all', 'school_year' => self::E2E_SCHOOL_YEAR], + ]; + + $response = $this->requestAs($this->actorFor($route->uri()), $method, $route->uri(), $payload); + $this->assertControlled($response, $method, $route->uri()); + $this->assertNoServerError($response, 'long running export job '.$method.' '.$route->uri()); + $this->assertStringNotContainsString('remember_token', $response->getContent(), 'export route exposed sensitive fields'); + } + } + + public function test_low_privilege_users_cannot_request_full_school_exports(): void + { + $routes = array_slice($this->apiRoutesContainingAny(['export', 'download', 'report']), 0, 120); + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + $response = $this->requestAs($this->parent, $method, $route->uri(), ['scope' => 'all', 'include' => ['students', 'finance', 'users']]); + $this->assertControlled($response, $method, $route->uri()); + $this->assertNoServerError($response, 'low privilege full export '.$method.' '.$route->uri()); + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiBatch12MaintenanceModeAndOperationalControlContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiBatch12MaintenanceModeAndOperationalControlContractTest.php new file mode 100644 index 00000000..1f01f078 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiBatch12MaintenanceModeAndOperationalControlContractTest.php @@ -0,0 +1,47 @@ +apiRoutesContainingAny(['maintenance', 'cache', 'queue', 'job', 'horizon', 'telescope', 'metrics', 'logs', 'debug']), 0, 120); + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + $uri = $route->uri(); + + $parentResponse = $this->requestAs($this->parent, $method, $uri, $this->payloadFor($method, $uri)); + $this->assertStatusIn($parentResponse, [401, 403, 404, 405, 419, 422], 'parent must not control operational endpoint '.$method.' '.$uri); + $this->assertNoServerError($parentResponse, 'parent operational probe '.$method.' '.$uri); + + $adminResponse = $this->requestAs($this->admin, $method, $uri, $this->payloadFor($method, $uri)); + $this->assertControlled($adminResponse, $method, $uri); + $this->assertNoServerError($adminResponse, 'admin operational probe '.$method.' '.$uri); + } + } + + public function test_public_requests_cannot_toggle_system_state(): void + { + $routes = array_slice($this->apiRoutesContainingAny(['maintenance', 'cache', 'queue', 'config', 'settings']), 0, 80); + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + if (! in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'], true)) { + continue; + } + + $response = $this->json($method, $this->materializePath($route->uri()), [ + 'enabled' => true, + 'force' => true, + 'confirm' => true, + ]); + + $this->assertStatusIn($response, [401, 403, 404, 405, 419, 422], 'public system toggle must be blocked '.$method.' '.$route->uri()); + $this->assertNoServerError($response, 'public system toggle '.$method.' '.$route->uri()); + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiBatch12RouteMetadataRiskRegisterTest.php b/tests/Feature/Api/V1/FullSurface/ApiBatch12RouteMetadataRiskRegisterTest.php new file mode 100644 index 00000000..0c546bb7 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiBatch12RouteMetadataRiskRegisterTest.php @@ -0,0 +1,39 @@ + ['auth', 'login', 'logout', 'profile', 'frontend'], + 'authorization' => ['roles', 'permissions', 'switch-role', 'impersonate'], + 'student_privacy' => ['students', 'parents', 'family', 'guardian'], + 'attendance_safety' => ['attendance', 'late', 'dismissal', 'absence'], + 'academic_records' => ['scores', 'grades', 'report-card', 'certificate'], + 'financial_records' => ['finance', 'invoice', 'payment', 'refund', 'receipt'], + 'files' => ['upload', 'download', 'import', 'export', 'print'], + 'external_integrations' => ['webhook', 'callback', 'paypal', 'email', 'whatsapp'], + 'operations' => ['settings', 'configuration', 'health', 'debug', 'logs'], + ]; + + foreach ($riskRegister as $risk => $needles) { + $this->assertNotEmpty($this->apiRoutesContainingAny($needles), 'Batch 12 risk register missing route family: '.$risk); + } + } + + public function test_mutating_high_risk_routes_have_specific_route_names_when_available(): void + { + foreach (array_slice($this->apiRoutesContainingAny(['finance', 'attendance', 'users', 'roles', 'permissions', 'settings', 'import', 'webhook']), 0, 180) as $route) { + if (! in_array($this->primaryMethod($route), ['POST', 'PUT', 'PATCH', 'DELETE'], true)) { + continue; + } + + $name = $route->getName(); + $this->assertTrue($name === null || ! preg_match('/(^|\.)(store|update|destroy)$/', $name) || substr_count((string) $name, '.') >= 2, 'High-risk route name is too generic: '.($name ?? '[unnamed]').' '.$route->uri()); + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiBatch12SearchEnumerationResistanceContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiBatch12SearchEnumerationResistanceContractTest.php new file mode 100644 index 00000000..84ec755a --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiBatch12SearchEnumerationResistanceContractTest.php @@ -0,0 +1,27 @@ +apiRoutesContainingAny(['search', 'lookup', 'autocomplete', 'find', 'students', 'users', 'parents']), 0, 150); + $queries = ['a', '%', '_', '*', '000000', $this->admin->email, ' OR 1=1 --', '']; + + foreach ($routes as $route) { + if ($this->primaryMethod($route) !== 'GET') { + continue; + } + + foreach ($queries as $query) { + $response = $this->requestAs($this->parent, 'GET', $route->uri(), ['search' => $query, 'q' => $query, 'email' => $query]); + $this->assertControlled($response, 'GET', $route->uri()); + $this->assertNoServerError($response, 'enumeration resistant search '.$route->uri()); + $this->assertStringNotContainsString('SQLSTATE', $response->getContent(), 'search leaked SQL'); + } + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiBatch12SensitiveConfigurationExposureContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiBatch12SensitiveConfigurationExposureContractTest.php new file mode 100644 index 00000000..542d8657 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiBatch12SensitiveConfigurationExposureContractTest.php @@ -0,0 +1,43 @@ +apiRoutesContainingAny(['settings', 'configuration', 'config', 'frontend', 'bootstrap', 'env', 'health', 'debug']), 0, 120); + $forbidden = ['APP_KEY', 'DB_PASSWORD', 'MAIL_PASSWORD', 'STRIPE_SECRET', 'PAYPAL_SECRET', 'JWT_SECRET', 'SANCTUM', 'password_hash', 'remember_token']; + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + if ($method !== 'GET') { + continue; + } + + $response = $this->requestAs($this->actorFor($route->uri()), $method, $route->uri()); + $this->assertControlled($response, $method, $route->uri()); + $this->assertNoServerError($response, 'secret exposure probe '.$route->uri()); + + $body = $response->getContent(); + foreach ($forbidden as $needle) { + $this->assertStringNotContainsString($needle, $body, $route->uri().' must not expose '.$needle); + } + } + } + + public function test_low_privilege_users_cannot_read_sensitive_configuration_domains(): void + { + $routes = array_slice($this->apiRoutesContainingAny(['settings', 'configuration', 'config', 'permissions']), 0, 100); + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + $response = $this->requestAs($this->studentUser, $method, $route->uri(), $this->payloadFor($method, $route->uri())); + + $this->assertStatusIn($response, [401, 403, 404, 405, 419, 422], 'student must not read/mutate sensitive configuration '.$method.' '.$route->uri()); + $this->assertNoServerError($response, 'student configuration probe '.$method.' '.$route->uri()); + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiBatch12WebhookProviderMatrixContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiBatch12WebhookProviderMatrixContractTest.php new file mode 100644 index 00000000..c4cc5e6e --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiBatch12WebhookProviderMatrixContractTest.php @@ -0,0 +1,39 @@ +apiRoutesContainingAny(['webhook', 'callback', 'paypal', 'stripe', 'payment', 'provider']), 0, 120); + $headerSets = [ + [], + ['X-Signature' => 'bad-signature'], + ['PayPal-Transmission-Id' => 'replay-id', 'PayPal-Transmission-Sig' => 'bad'], + ['Stripe-Signature' => 't=1,v1=bad'], + ]; + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) { + continue; + } + + foreach ($headerSets as $headers) { + $response = $this->withHeaders($headers)->json($method, $this->materializePath($route->uri()), [ + 'id' => 'evt_batch_12', + 'type' => 'payment.succeeded', + 'amount' => 10, + 'metadata' => ['invoice_id' => $this->ids['invoiceId']], + ]); + + $this->assertStatusIn($response, [200, 202, 400, 401, 403, 404, 405, 409, 419, 422], 'provider callback '.$method.' '.$route->uri()); + $this->assertNoServerError($response, 'provider callback '.$method.' '.$route->uri()); + $this->assertStringNotContainsString('secret', strtolower($response->getContent()), 'provider callback must not expose secrets'); + } + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiBatch13AccountRecoveryCredentialChangeContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiBatch13AccountRecoveryCredentialChangeContractTest.php new file mode 100644 index 00000000..52e9697f --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiBatch13AccountRecoveryCredentialChangeContractTest.php @@ -0,0 +1,55 @@ +apiRoutesContainingAny(['password', 'reset', 'forgot', 'verify', 'verification', 'credential', 'profile']); + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + $payload = $this->payloadFor($method, $route->uri()) + [ + 'email' => 'does-not-exist-batch13@example.test', + 'token' => str_repeat('x', 2048), + 'password' => 'short', + 'password_confirmation' => 'different', + 'current_password' => 'wrong-password', + ]; + + $response = $this->requestAs($this->actorFor($route->uri()), $method, $route->uri(), $payload); + $this->assertControlled($response, $method, $route->uri()); + + $body = strtolower($response->getContent()); + $this->assertStringNotContainsString('password_resets', $body); + $this->assertStringNotContainsString('remember_token', $body); + $this->assertStringNotContainsString('reset_token', $body); + } + } + + public function test_low_privilege_users_cannot_change_other_users_credentials(): void + { + $routes = $this->apiRoutesContainingAny(['users', 'profile', 'password', 'credential']); + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) { + continue; + } + + $response = $this->requestAs($this->parent, $method, $route->uri(), [ + 'user_id' => $this->admin->id, + 'email' => $this->admin->email, + 'password' => 'NewPassword123!', + 'password_confirmation' => 'NewPassword123!', + 'role' => 'administrator', + ]); + + $this->assertControlled($response, $method, $route->uri()); + $this->assertNotContains($response->getStatusCode(), [200, 201]); + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiBatch13ApiPaginationPerformanceGuardContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiBatch13ApiPaginationPerformanceGuardContractTest.php new file mode 100644 index 00000000..cd5d77ca --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiBatch13ApiPaginationPerformanceGuardContractTest.php @@ -0,0 +1,48 @@ +apiRoutes(), fn ($route) => $this->primaryMethod($route) === 'GET'); + + foreach (array_slice(array_values($routes), 0, 80) as $route) { + $uri = $route->uri(); + $response = $this->requestAs($this->actorFor($uri), 'GET', $uri, [ + 'page' => 1, + 'per_page' => 1000000, + 'limit' => 1000000, + 'include' => implode(',', array_fill(0, 100, 'children.children.children')), + 'expand' => '*', + 'fields' => '*', + ]); + + $this->assertControlled($response, 'GET', $uri); + $this->assertStringNotContainsString('Allowed memory size', $response->getContent()); + $this->assertStringNotContainsString('Maximum execution time', $response->getContent()); + } + } + + public function test_search_routes_handle_unicode_and_extreme_terms_without_leaking_sql(): void + { + $routes = $this->apiRoutesContainingAny(['search', 'lookup', 'autocomplete', 'students', 'users', 'parents']); + + foreach ($routes as $route) { + if ($this->primaryMethod($route) !== 'GET') { + continue; + } + + $response = $this->requestAs($this->actorFor($route->uri()), 'GET', $route->uri(), [ + 'q' => str_repeat('أ', 1000)."%' OR 1=1 --", + 'search' => str_repeat('🧪', 300), + ]); + + $this->assertControlled($response, 'GET', $route->uri()); + $this->assertStringNotContainsString('SQLSTATE', $response->getContent()); + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiBatch13ContactMessagingAbuseModerationContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiBatch13ContactMessagingAbuseModerationContractTest.php new file mode 100644 index 00000000..994c9605 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiBatch13ContactMessagingAbuseModerationContractTest.php @@ -0,0 +1,56 @@ +apiRoutesContainingAny(['message', 'contact', 'support', 'communication', 'broadcast', 'email', 'whatsapp']); + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) { + continue; + } + + $payload = $this->payloadFor($method, $route->uri()) + [ + 'subject' => "Batch 13\r\nBcc: victim@example.test", + 'message' => ''.str_repeat('spam ', 2000), + 'body' => '', + 'from' => 'attacker@example.test\r\nCc: injected@example.test', + 'recipients' => array_fill(0, 500, 'victim@example.test'), + 'channel' => 'all', + ]; + + $response = $this->requestAs($this->parent, $method, $route->uri(), $payload); + $this->assertControlled($response, $method, $route->uri()); + $this->assertStringNotContainsString('Bcc: victim@example.test', $response->getContent()); + $this->assertStringNotContainsString('', $response->getContent()); + } + } + + public function test_broadcast_routes_remain_admin_only_under_channel_spoofing(): void + { + $routes = $this->apiRoutesContainingAny(['broadcast', 'email', 'notification', 'whatsapp']); + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + if (! in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'], true)) { + continue; + } + + $response = $this->requestAs($this->teacher, $method, $route->uri(), [ + 'recipients' => ['all'], + 'audience' => 'all_school', + 'channel' => 'admin', + 'force' => true, + ]); + + $this->assertControlled($response, $method, $route->uri()); + $this->assertNotContains($response->getStatusCode(), [200, 201]); + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiBatch13DiscountVoucherScholarshipAbuseContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiBatch13DiscountVoucherScholarshipAbuseContractTest.php new file mode 100644 index 00000000..b2caeb64 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiBatch13DiscountVoucherScholarshipAbuseContractTest.php @@ -0,0 +1,58 @@ +apiRoutesContainingAny(['discount', 'voucher', 'scholarship', 'fee', 'tuition', 'waiver']); + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) { + continue; + } + + $payload = $this->payloadFor($method, $route->uri()) + [ + 'voucher_id' => $this->ids['voucherId'], + 'discount_type' => 'percent', + 'percentage' => 1000, + 'amount' => -9999.99, + 'applies_to_all' => true, + 'family_id' => 99999999, + 'student_id' => 99999999, + 'approved_by' => $this->parent->id, + ]; + + $response = $this->requestAs($this->admin, $method, $route->uri(), $payload); + $this->assertControlled($response, $method, $route->uri()); + $this->assertStringNotContainsString('SQLSTATE', $response->getContent()); + } + } + + public function test_parent_cannot_self_grant_discounts_or_scholarships(): void + { + $routes = $this->apiRoutesContainingAny(['discount', 'voucher', 'scholarship', 'waiver']); + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + if (! in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'], true)) { + continue; + } + + $response = $this->requestAs($this->parent, $method, $route->uri(), [ + 'student_id' => $this->ids['studentId'], + 'amount' => 9999, + 'percentage' => 100, + 'approved' => true, + 'reason' => 'self-granted', + ]); + + $this->assertControlled($response, $method, $route->uri()); + $this->assertNotContains($response->getStatusCode(), [200, 201]); + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiBatch13FullSurfaceRegressionSuperSweepTest.php b/tests/Feature/Api/V1/FullSurface/ApiBatch13FullSurfaceRegressionSuperSweepTest.php new file mode 100644 index 00000000..6d1a9442 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiBatch13FullSurfaceRegressionSuperSweepTest.php @@ -0,0 +1,61 @@ + ['auth', 'login', 'profile', 'users', 'roles', 'permissions'], + 'school' => ['students', 'parents', 'families', 'classes', 'school-year', 'semester'], + 'academic' => ['teacher', 'scores', 'homework', 'quiz', 'report-card', 'certificate'], + 'safety' => ['attendance', 'dismissal', 'late', 'incident', 'emergency'], + 'finance' => ['finance', 'invoice', 'payment', 'refund', 'fee', 'discount'], + 'operations' => ['inventory', 'supplier', 'procurement', 'settings', 'configuration'], + 'communications' => ['message', 'support', 'email', 'whatsapp', 'notification'], + 'files' => ['import', 'export', 'download', 'print', 'upload', 'media'], + ]; + + foreach ($families as $needles) { + foreach (array_slice($this->apiRoutesContainingAny($needles), 0, 25) as $route) { + $method = $this->primaryMethod($route); + $payload = $this->payloadFor($method, $route->uri()) + [ + 'id' => '../../etc/passwd', + 'student_id' => 'not-an-int', + 'parent_id' => ['array-is-not-id'], + 'amount' => 'NaN', + 'status' => '', + 'include' => '*', + 'force' => true, + ]; + + $response = $this->requestAs($this->actorFor($route->uri()), $method, $route->uri(), $payload); + $this->assertControlled($response, $method, $route->uri()); + + $body = strtolower($response->getContent()); + $this->assertStringNotContainsString('sqlstate', $body); + $this->assertStringNotContainsString('app_key', $body); + $this->assertStringNotContainsString('stack trace', $body); + } + } + } + + public function test_batch13_major_mutation_families_are_represented(): void + { + $expectedFamilies = [ + 'students', 'attendance', 'scores', 'finance', 'payment', 'inventory', + 'message', 'support', 'settings', 'roles', 'permissions', 'family', + ]; + + foreach ($expectedFamilies as $family) { + $mutatingRoutes = array_filter($this->apiRoutesContainingAny([$family]), function ($route): bool { + return in_array($this->primaryMethod($route), ['POST', 'PUT', 'PATCH', 'DELETE'], true); + }); + + $this->assertNotEmpty($mutatingRoutes, "Batch 13 expected at least one mutating API route for {$family}."); + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiBatch13InventoryProcurementReceivingIntegrityContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiBatch13InventoryProcurementReceivingIntegrityContractTest.php new file mode 100644 index 00000000..6181b9e6 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiBatch13InventoryProcurementReceivingIntegrityContractTest.php @@ -0,0 +1,53 @@ +apiRoutesContainingAny(['inventory', 'procurement', 'purchase', 'receiving', 'supplier', 'supply']); + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) { + continue; + } + + $payload = $this->payloadFor($method, $route->uri()) + [ + 'item_id' => $this->ids['itemId'], + 'supplier_id' => $this->ids['supplierId'], + 'quantity' => -1000000, + 'received_quantity' => 999999999, + 'unit_cost' => -1, + 'total_cost' => -999999, + 'movement_type' => 'receive', + 'force_negative_stock' => true, + ]; + + $response = $this->requestAs($this->admin, $method, $route->uri(), $payload); + $this->assertControlled($response, $method, $route->uri()); + $this->assertStringNotContainsString('SQLSTATE', $response->getContent()); + } + } + + public function test_non_admins_cannot_adjust_inventory_or_procurement_state(): void + { + $routes = $this->apiRoutesContainingAny(['inventory', 'procurement', 'purchase', 'receiving', 'supplier']); + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + if (! in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'], true)) { + continue; + } + + foreach ([$this->teacher, $this->parent, $this->studentUser] as $actor) { + $response = $this->requestAs($actor, $method, $route->uri(), $this->payloadFor($method, $route->uri())); + $this->assertControlled($response, $method, $route->uri()); + $this->assertNotContains($response->getStatusCode(), [200, 201]); + } + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiBatch13LocaleCalendarReligiousDateContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiBatch13LocaleCalendarReligiousDateContractTest.php new file mode 100644 index 00000000..740bbcef --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiBatch13LocaleCalendarReligiousDateContractTest.php @@ -0,0 +1,50 @@ +apiRoutesContainingAny(['calendar', 'event', 'schedule', 'school-year', 'semester', 'holiday']); + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + $payload = $this->payloadFor($method, $route->uri()) + [ + 'date' => '1447-09-01', + 'start_date' => '2025-03-10', + 'end_date' => '2025-03-09', + 'locale' => 'ar_EG', + 'calendar' => 'hijri', + 'timezone' => 'America/New_York\0UTC', + 'recurrence' => 'FREQ=DAILY;COUNT=999999', + ]; + + $response = $this->requestAs($this->actorFor($route->uri()), $method, $route->uri(), $payload); + $this->assertControlled($response, $method, $route->uri()); + $this->assertStringNotContainsString('DateTimeZone::__construct', $response->getContent()); + } + } + + public function test_attendance_and_academic_routes_reject_out_of_term_religious_calendar_spoofing(): void + { + $routes = $this->apiRoutesContainingAny(['attendance', 'scores', 'homework', 'quiz', 'report-card']); + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) { + continue; + } + + $response = $this->requestAs($this->teacher, $method, $route->uri(), $this->payloadFor($method, $route->uri()) + [ + 'date' => '0000-00-00', + 'school_year' => '1447/1448', + 'semester' => 'ramadan-special', + ]); + + $response = $this->requestAs($this->actorFor($route->uri()), $method, $route->uri(), [ + 'batch13_malformed' => ['unexpected' => ['deep' => ['value' => str_repeat('x', 4096)]]], + ]); + + $this->assertControlled($response, $method, $route->uri()); + $content = $response->getContent(); + $this->assertStringNotContainsString('X-Injected: yes', $content); + $this->assertStringNotContainsString('', $content); + $this->assertStringNotContainsString('/var/www', $content); + } + } + + public function test_error_responses_do_not_disclose_controller_or_model_stack_details(): void + { + $routes = array_slice($this->apiRoutesContainingAny(['students', 'users', 'finance', 'attendance', 'settings']), 0, 60); + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + $response = $this->requestAs($this->actorFor($route->uri()), $method, $route->uri(), ['id' => '../invalid']); + $this->assertControlled($response, $method, $route->uri()); + + $body = strtolower($response->getContent()); + $this->assertStringNotContainsString('app\\http\\controllers', $body); + $this->assertStringNotContainsString('illuminate\\database', $body); + $this->assertStringNotContainsString('vendor/laravel', $body); + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiBatch13ParentPortalInvoicePrivacyContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiBatch13ParentPortalInvoicePrivacyContractTest.php new file mode 100644 index 00000000..240a5e29 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiBatch13ParentPortalInvoicePrivacyContractTest.php @@ -0,0 +1,57 @@ +apiRoutesContainingAny(['parent', 'invoice', 'payment', 'finance', 'receipt', 'billing']); + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + $payload = $this->payloadFor($method, $route->uri()) + [ + 'parent_id' => 99999999, + 'family_id' => 99999999, + 'student_id' => 99999999, + 'all' => true, + 'all_families' => true, + 'include_balances' => true, + 'include_deleted' => true, + 'school_year' => 'all', + ]; + + $response = $this->requestAs($this->parent, $method, $route->uri(), $payload); + $this->assertControlled($response, $method, $route->uri()); + $this->assertStringNotContainsString('credit_card', strtolower($response->getContent())); + $this->assertStringNotContainsString('bank_account', strtolower($response->getContent())); + $this->assertStringNotContainsString('ssn', strtolower($response->getContent())); + } + } + + public function test_parent_cannot_mark_invoice_paid_or_refunded_directly(): void + { + $routes = $this->apiRoutesContainingAny(['invoice', 'payment', 'refund', 'transaction']); + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + if (! in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'], true)) { + continue; + } + + $payload = $this->payloadFor($method, $route->uri()) + [ + 'invoice_id' => $this->ids['invoiceId'], + 'status' => 'paid', + 'paid' => true, + 'refunded' => true, + 'amount' => 0.01, + ]; + + $response = $this->requestAs($this->parent, $method, $route->uri(), $payload); + $this->assertControlled($response, $method, $route->uri()); + $this->assertNotContains($response->getStatusCode(), [200, 201], 'Parent should not directly mutate finance state through '.$route->uri()); + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiBatch13PaymentProviderLedgerReconciliationContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiBatch13PaymentProviderLedgerReconciliationContractTest.php new file mode 100644 index 00000000..2e4c93c3 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiBatch13PaymentProviderLedgerReconciliationContractTest.php @@ -0,0 +1,68 @@ +apiRoutesContainingAny(['paypal', 'stripe', 'payment', 'transaction', 'ledger', 'receipt', 'invoice']); + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) { + continue; + } + + $payload = $this->payloadFor($method, $route->uri()) + [ + 'provider' => 'paypal', + 'provider_transaction_id' => 'BATCH13-LEDGER-REPLAY', + 'transaction_id' => 'BATCH13-LEDGER-REPLAY', + 'external_id' => 'BATCH13-LEDGER-REPLAY', + 'amount' => '25.00', + 'currency' => 'USD', + 'status' => 'completed', + 'invoice_id' => $this->ids['invoiceId'], + 'parent_id' => $this->ids['parentId'], + 'student_id' => $this->ids['studentId'], + 'metadata' => [ + 'invoice_id' => $this->ids['invoiceId'], + 'amount' => '999999999999.99', + 'currency' => 'XXX', + ], + ]; + + $response = $this->requestAs($this->admin, $method, $route->uri(), $payload); + $this->assertControlled($response, $method, $route->uri()); + $this->assertStringNotContainsString('SQLSTATE', $response->getContent()); + $this->assertStringNotContainsString('ledger imbalance', strtolower($response->getContent())); + } + } + + public function test_duplicate_provider_reference_is_controlled_across_payment_routes(): void + { + $routes = array_slice($this->apiRoutesContainingAny(['payment', 'transaction', 'paypal', 'stripe']), 0, 25); + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) { + continue; + } + + $payload = $this->payloadFor($method, $route->uri()) + [ + 'provider_reference' => 'BATCH13-DUPLICATE-PROVIDER-REFERENCE', + 'transaction_id' => 'BATCH13-DUPLICATE-PROVIDER-REFERENCE', + 'idempotency_key' => 'BATCH13-DUPLICATE-PROVIDER-REFERENCE', + ]; + + $first = $this->requestAs($this->admin, $method, $route->uri(), $payload); + $second = $this->requestAs($this->admin, $method, $route->uri(), $payload); + + $this->assertControlled($first, $method, $route->uri()); + $this->assertControlled($second, $method, $route->uri()); + $this->assertNotContains($second->getStatusCode(), [500, 501, 502, 503]); + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiBatch13ReportCardTranscriptCertificateForgeryContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiBatch13ReportCardTranscriptCertificateForgeryContractTest.php new file mode 100644 index 00000000..9a4334b0 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiBatch13ReportCardTranscriptCertificateForgeryContractTest.php @@ -0,0 +1,51 @@ +apiRoutesContainingAny(['report-card', 'transcript', 'certificate', 'print', 'badge', 'sticker', 'slip']); + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) { + continue; + } + + $payload = $this->payloadFor($method, $route->uri()) + [ + 'student_id' => $this->ids['studentId'], + 'class_section_id' => $this->ids['classSectionId'], + 'finalized' => true, + 'locked' => true, + 'approved_by' => $this->parent->id, + 'principal_signature' => '', + 'certificate_number' => '../CERT-FORGED', + 'issue_date' => '2099-12-31', + ]; + + $response = $this->requestAs($this->teacher, $method, $route->uri(), $payload); + $this->assertControlled($response, $method, $route->uri()); + $this->assertStringNotContainsString('', $response->getContent()); + } + } + + public function test_students_and_parents_cannot_generate_admin_documents_for_other_students(): void + { + $routes = $this->apiRoutesContainingAny(['certificate', 'transcript', 'badge', 'report-card', 'print']); + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + $payload = ['student_id' => 99999999, 'all_students' => true, 'school_year' => 'all']; + + foreach ([$this->parent, $this->studentUser] as $actor) { + $response = $this->requestAs($actor, $method, $route->uri(), $payload); + $this->assertControlled($response, $method, $route->uri()); + $this->assertStringNotContainsString('private_key', strtolower($response->getContent())); + } + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiBatch13RolePermissionMutationRaceContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiBatch13RolePermissionMutationRaceContractTest.php new file mode 100644 index 00000000..99d3f27e --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiBatch13RolePermissionMutationRaceContractTest.php @@ -0,0 +1,55 @@ +apiRoutesContainingAny(['role', 'permission', 'acl', 'access']); + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) { + continue; + } + + $payload = $this->payloadFor($method, $route->uri()) + [ + 'role_id' => $this->ids['roleId'], + 'permissions' => ['*', 'admin.*', '../permissions/delete'], + 'permission_ids' => [$this->ids['permissionId'], 99999999, -1], + 'inherits_from' => $this->ids['roleId'], + 'is_super_admin' => true, + ]; + + $response = $this->requestAs($this->admin, $method, $route->uri(), $payload); + $this->assertControlled($response, $method, $route->uri()); + $this->assertStringNotContainsString('SQLSTATE', $response->getContent()); + } + } + + public function test_non_admin_cannot_gain_permission_by_replaying_role_payload(): void + { + $routes = $this->apiRoutesContainingAny(['role', 'permission', 'users']); + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) { + continue; + } + + foreach ([$this->teacher, $this->parent, $this->studentUser] as $actor) { + $response = $this->requestAs($actor, $method, $route->uri(), [ + 'role' => 'administrator', + 'role_id' => $this->ids['roleId'], + 'permissions' => ['*'], + 'approved_by' => $this->admin->id, + ]); + $this->assertControlled($response, $method, $route->uri()); + $this->assertNotContains($response->getStatusCode(), [200, 201]); + } + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiBatch13StaffHrPayrollBoundaryContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiBatch13StaffHrPayrollBoundaryContractTest.php new file mode 100644 index 00000000..fb41ebc8 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiBatch13StaffHrPayrollBoundaryContractTest.php @@ -0,0 +1,49 @@ +apiRoutesContainingAny(['staff', 'employee', 'payroll', 'salary', 'hr', 'reimbursement', 'expense']); + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + $response = $this->requestAs($this->admin, $method, $route->uri(), $this->payloadFor($method, $route->uri())); + $this->assertControlled($response, $method, $route->uri()); + + $body = strtolower($response->getContent()); + $this->assertStringNotContainsString('ssn', $body); + $this->assertStringNotContainsString('social_security', $body); + $this->assertStringNotContainsString('bank_account', $body); + $this->assertStringNotContainsString('routing_number', $body); + } + } + + public function test_low_privilege_users_cannot_mutate_staff_or_payroll_routes(): void + { + $routes = $this->apiRoutesContainingAny(['staff', 'employee', 'payroll', 'salary', 'hr']); + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + if (! in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'], true)) { + continue; + } + + $payload = $this->payloadFor($method, $route->uri()) + [ + 'salary' => 999999, + 'hourly_rate' => 999, + 'role' => 'administrator', + ]; + + foreach ([$this->teacher, $this->parent, $this->studentUser] as $actor) { + $response = $this->requestAs($actor, $method, $route->uri(), $payload); + $this->assertControlled($response, $method, $route->uri()); + $this->assertNotContains($response->getStatusCode(), [200, 201]); + } + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiBatch13StudentEnrollmentLifecycleIntegrityContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiBatch13StudentEnrollmentLifecycleIntegrityContractTest.php new file mode 100644 index 00000000..2eaacfa2 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiBatch13StudentEnrollmentLifecycleIntegrityContractTest.php @@ -0,0 +1,58 @@ +apiRoutesContainingAny(['enroll', 'withdraw', 'registration', 'student', 'class-section', 'assignment']); + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) { + continue; + } + + $payload = $this->payloadFor($method, $route->uri()) + [ + 'student_id' => $this->ids['studentId'], + 'students' => [$this->ids['studentId'], 99999999], + 'parent_id' => 99999999, + 'family_id' => 99999999, + 'class_section_id' => 99999999, + 'previous_class_section_id' => $this->ids['classSectionId'], + 'effective_date' => '1900-01-01', + 'reason' => 'Batch 13 enrollment integrity probe', + ]; + + $response = $this->requestAs($this->admin, $method, $route->uri(), $payload); + $this->assertControlled($response, $method, $route->uri()); + $this->assertStringNotContainsString('Integrity constraint violation', $response->getContent()); + } + } + + public function test_parent_cannot_self_enroll_or_move_student_into_arbitrary_class(): void + { + $routes = $this->apiRoutesContainingAny(['enroll', 'withdraw', 'class-section', 'student']); + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + if (! in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'], true)) { + continue; + } + + $payload = $this->payloadFor($method, $route->uri()) + [ + 'student_id' => $this->ids['studentId'], + 'class_section_id' => 99999999, + 'force' => true, + 'approved_by' => $this->admin->id, + ]; + + $response = $this->requestAs($this->parent, $method, $route->uri(), $payload); + $this->assertControlled($response, $method, $route->uri()); + $this->assertNotContains($response->getStatusCode(), [200, 201], 'Parent should not self-enroll or arbitrarily move students through '.$route->uri()); + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiBatch13TeacherRosterAndAssignmentBoundaryContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiBatch13TeacherRosterAndAssignmentBoundaryContractTest.php new file mode 100644 index 00000000..46db2c3f --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiBatch13TeacherRosterAndAssignmentBoundaryContractTest.php @@ -0,0 +1,54 @@ +apiRoutesContainingAny(['teacher', 'roster', 'class-progress', 'attendance', 'scores', 'homework', 'quiz']); + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + $payload = $this->payloadFor($method, $route->uri()) + [ + 'teacher_id' => $this->admin->id, + 'class_section_id' => 99999999, + 'student_id' => 99999999, + 'include_unassigned' => true, + 'all_classes' => true, + 'bypass_assignment' => true, + ]; + + $response = $this->requestAs($this->teacher, $method, $route->uri(), $payload); + $this->assertControlled($response, $method, $route->uri()); + $this->assertStringNotContainsString('password', strtolower($response->getContent())); + $this->assertStringNotContainsString('remember_token', strtolower($response->getContent())); + } + } + + public function test_unassigned_teacher_mutations_are_denied_or_validated(): void + { + $routes = $this->apiRoutesContainingAny(['attendance', 'scores', 'homework', 'quiz', 'class-progress', 'report-card']); + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + if (! in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'], true)) { + continue; + } + + $payload = $this->payloadFor($method, $route->uri()) + [ + 'class_section_id' => 99999999, + 'student_id' => $this->ids['studentId'], + 'teacher_id' => $this->teacher->id, + 'status' => 'finalized', + 'score' => 100, + ]; + + $response = $this->requestAs($this->teacher, $method, $route->uri(), $payload); + $this->assertControlled($response, $method, $route->uri()); + $this->assertNotContains($response->getStatusCode(), [500, 501, 502, 503]); + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiBatch14AcademicPromotionRollbackIntegrityContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiBatch14AcademicPromotionRollbackIntegrityContractTest.php new file mode 100644 index 00000000..d73cad49 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiBatch14AcademicPromotionRollbackIntegrityContractTest.php @@ -0,0 +1,32 @@ +apiRoutesContainingAny(['promotion', 'promote', 'school-year', 'semester', 'close', 'reopen', 'rollback']), 0, 45) as $route) { + $method = $this->primaryMethod($route); + $response = $this->requestAs($this->actorFor($route->uri()), $method, $route->uri(), $this->payloadFor($method, $route->uri()) + [ + 'from_school_year_id' => $this->ids['schoolYearId'], 'to_school_year_id' => $this->ids['schoolYearId'], 'target_school_year_id' => 99999999, 'rollback' => true, 'force' => true, 'delete_existing_records' => true, 'students' => [$this->ids['studentId'], 99999999], 'class_section_id' => 99999999, 'effective_date' => '1900-01-01', + ]); + $this->assertControlled($response, $method, $route->uri()); + $this->assertStringNotContainsString('sqlstate', strtolower($response->getContent())); + } + } + + public function test_low_privilege_actors_cannot_close_reopen_or_promote_school_years(): void + { + foreach (array_slice($this->apiRoutesContainingAny(['promotion', 'school-year', 'close', 'reopen']), 0, 30) as $route) { + $method = $this->primaryMethod($route); + if (! in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'], true)) { continue; } + foreach ([$this->teacher, $this->parent, $this->studentUser] as $actor) { + $response = $this->requestAs($actor, $method, $route->uri(), ['force' => true, 'confirmed' => true, 'school_year_id' => $this->ids['schoolYearId']]); + $this->assertStatusIn($response, [400, 401, 403, 404, 405, 409, 419, 422], $method . ' ' . $route->uri()); + } + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiBatch14AccountEnumerationAndIdentityPrivacyContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiBatch14AccountEnumerationAndIdentityPrivacyContractTest.php new file mode 100644 index 00000000..eeff406e --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiBatch14AccountEnumerationAndIdentityPrivacyContractTest.php @@ -0,0 +1,38 @@ +apiRoutesContainingAny(['forgot', 'password', 'reset', 'verify', 'verification', 'recovery']); + foreach (array_slice($routes, 0, 30) as $route) { + $method = $this->primaryMethod($route); + if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) { continue; } + foreach ([$this->admin->email, 'missing-user-' . uniqid() . '@example.test'] as $email) { + $response = $this->json($method, $this->materializePath($route->uri()), ['email' => $email, 'token' => 'invalid-token', 'password' => 'NewPassword123!', 'password_confirmation' => 'NewPassword123!']); + $this->assertControlled($response, $method, $route->uri()); + $body = strtolower($response->getContent()); + $this->assertStringNotContainsString('no user found', $body); + $this->assertStringNotContainsString('email does not exist', $body); + $this->assertStringNotContainsString('unknown email', $body); + } + } + } + + public function test_identity_payloads_do_not_leak_internal_auth_fields(): void + { + foreach (array_slice($this->apiRoutesContainingAny(['me', 'profile', 'frontend', 'identity', 'user']), 0, 35) as $route) { + $method = $this->primaryMethod($route); + $response = $this->requestAs($this->actorFor($route->uri()), $method, $route->uri(), $this->payloadFor($method, $route->uri())); + $this->assertControlled($response, $method, $route->uri()); + $body = strtolower($response->getContent()); + foreach (['password', 'remember_token', 'two_factor', 'api_token', 'provider_token', 'secret'] as $forbidden) { + $this->assertStringNotContainsString($forbidden, $body, $route->uri() . ' leaked identity internals.'); + } + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiBatch14AdminDelegationApprovalWorkflowContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiBatch14AdminDelegationApprovalWorkflowContractTest.php new file mode 100644 index 00000000..bb444c69 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiBatch14AdminDelegationApprovalWorkflowContractTest.php @@ -0,0 +1,30 @@ +apiRoutesContainingAny(['approve', 'approval', 'delegate', 'delegation', 'authorize', 'permission', 'role', 'impersonate']), 0, 50) as $route) { + $method = $this->primaryMethod($route); + $response = $this->requestAs($this->actorFor($route->uri()), $method, $route->uri(), $this->payloadFor($method, $route->uri()) + ['approved' => true, 'approved_by' => $this->admin->id, 'approved_at' => '2000-01-01 00:00:00', 'delegated_by' => $this->admin->id, 'delegated_to' => $this->parent->id, 'permissions' => ['*'], 'role' => 'administrator', 'expires_at' => '2099-01-01 00:00:00']); + $this->assertControlled($response, $method, $route->uri()); + $this->assertStringNotContainsString('sqlstate', strtolower($response->getContent())); + } + } + + public function test_low_privilege_users_cannot_self_approve_or_delegate_privileges(): void + { + foreach (array_slice($this->apiRoutesContainingAny(['approve', 'approval', 'delegate', 'permission', 'role']), 0, 35) as $route) { + $method = $this->primaryMethod($route); + if (! in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'], true)) { continue; } + foreach ([$this->teacher, $this->parent, $this->studentUser] as $actor) { + $response = $this->requestAs($actor, $method, $route->uri(), ['approved' => true, 'permissions' => ['*'], 'role' => 'administrator', 'force' => true]); + $this->assertStatusIn($response, [400, 401, 403, 404, 405, 409, 419, 422], $method . ' ' . $route->uri()); + } + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiBatch14ApiConsumerContractCompatibilityTest.php b/tests/Feature/Api/V1/FullSurface/ApiBatch14ApiConsumerContractCompatibilityTest.php new file mode 100644 index 00000000..b61722d2 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiBatch14ApiConsumerContractCompatibilityTest.php @@ -0,0 +1,36 @@ +apiRoutesContainingAny(['login', 'auth/me', 'me', 'frontend', 'profile']), 0, 25) as $route) { + $method = $this->primaryMethod($route); + foreach ([['Accept' => 'application/json', 'X-Client' => 'web'], ['Accept' => 'application/json', 'X-Client' => 'mobile', 'X-App-Version' => '1.0.0'], ['Accept' => 'application/vnd.alrahma.v1+json', 'X-Client' => 'api-consumer']] as $headers) { + $response = $this->withHeaders($headers)->actingAs($this->actorFor($route->uri()), 'api')->json($method, $this->materializePath($route->uri()), $this->payloadFor($method, $route->uri())); + $this->assertControlled($response, $method, $route->uri()); + $this->assertJsonResponseWhenSuccessful($response, $method . ' ' . $route->uri()); + if ($response->isSuccessful()) { $this->assertStringNotContainsString('undefined index', strtolower($response->getContent())); } + } + } + } + + public function test_successful_login_like_responses_keep_token_and_user_contract_when_present(): void + { + foreach (array_slice($this->apiRoutesContainingAny(['login', 'auth/login']), 0, 10) as $route) { + $method = $this->primaryMethod($route); + if ($method !== 'POST') { continue; } + $response = $this->json($method, $this->materializePath($route->uri()), ['email' => $this->admin->email, 'password' => 'password']); + $this->assertControlled($response, $method, $route->uri()); + if ($response->isSuccessful() && $this->isJsonString($response->getContent())) { + $data = $response->json(); + $this->assertTrue(isset($data['token']) || isset($data['access_token']), $route->uri() . ' should expose token/access_token when login succeeds.'); + $this->assertArrayHasKey('user', $data, $route->uri() . ' should expose user when login succeeds.'); + } + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiBatch14AttendanceDeviceAndKioskAbuseContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiBatch14AttendanceDeviceAndKioskAbuseContractTest.php new file mode 100644 index 00000000..1286d88a --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiBatch14AttendanceDeviceAndKioskAbuseContractTest.php @@ -0,0 +1,32 @@ +apiRoutesContainingAny(['attendance', 'scanner', 'scan', 'kiosk', 'badge', 'dismissal', 'late']), 0, 55) as $route) { + $method = $this->primaryMethod($route); + $response = $this->withHeaders(['X-Device-Id' => 'trusted-front-desk-tablet', 'X-Kiosk-Mode' => 'true', 'X-Scanner-Token' => 'forged-scanner-token', 'X-Forwarded-For' => '127.0.0.1']) + ->actingAs($this->actorFor($route->uri()), 'api') + ->json($method, $this->materializePath($route->uri()), $this->payloadFor($method, $route->uri()) + ['badge_token' => 'forged-badge-token', 'device_id' => 'trusted-front-desk-tablet', 'scan_time' => '2099-01-01 00:00:00', 'status' => 'present', 'override' => true]); + $this->assertControlled($response, $method, $route->uri()); + $this->assertStringNotContainsString('sqlstate', strtolower($response->getContent())); + } + } + + public function test_student_and_parent_cannot_self_mark_attendance_through_device_payloads(): void + { + foreach (array_slice($this->apiRoutesContainingAny(['attendance', 'scan', 'dismissal', 'late']), 0, 30) as $route) { + $method = $this->primaryMethod($route); + if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) { continue; } + foreach ([$this->studentUser, $this->parent] as $actor) { + $response = $this->requestAs($actor, $method, $route->uri(), ['student_id' => $this->ids['studentId'], 'status' => 'present', 'verified_by_device' => true, 'override_reason' => 'parent approved']); + $this->assertStatusIn($response, [400, 401, 403, 404, 405, 409, 419, 422], $method . ' ' . $route->uri()); + } + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiBatch14AuditTrailTamperResistanceContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiBatch14AuditTrailTamperResistanceContractTest.php new file mode 100644 index 00000000..b40a8c8e --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiBatch14AuditTrailTamperResistanceContractTest.php @@ -0,0 +1,32 @@ +apiRoutesContainingAny(['audit', 'log', 'activity', 'history', 'trace']), 0, 35) as $route) { + $method = $this->primaryMethod($route); + $response = $this->requestAs($this->actorFor($route->uri()), $method, $route->uri(), $this->payloadFor($method, $route->uri()) + [ + 'created_by' => $this->admin->id, 'updated_by' => $this->admin->id, 'deleted_by' => $this->admin->id, 'actor_id' => $this->admin->id, 'ip_address' => '127.0.0.1', 'user_agent' => 'forged-user-agent', 'created_at' => '1999-01-01 00:00:00', 'updated_at' => '1999-01-01 00:00:00', 'deleted_at' => '1999-01-01 00:00:00', + ]); + $this->assertControlled($response, $method, $route->uri()); + $this->assertStringNotContainsString('sqlstate', strtolower($response->getContent())); + } + } + + public function test_non_admin_users_cannot_clear_or_rewrite_audit_history(): void + { + foreach (array_slice($this->apiRoutesContainingAny(['audit', 'log', 'history', 'activity']), 0, 25) as $route) { + $method = $this->primaryMethod($route); + if (! in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'], true)) { continue; } + foreach ([$this->parent, $this->teacher, $this->studentUser] as $actor) { + $response = $this->requestAs($actor, $method, $route->uri(), ['action' => 'clear', 'purge' => true, 'rewrite' => true]); + $this->assertStatusIn($response, [400, 401, 403, 404, 405, 409, 419, 422], $method . ' ' . $route->uri()); + } + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiBatch14BulkAssignmentCollisionContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiBatch14BulkAssignmentCollisionContractTest.php new file mode 100644 index 00000000..4f1e3342 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiBatch14BulkAssignmentCollisionContractTest.php @@ -0,0 +1,37 @@ +apiRoutesContainingAny(['assign', 'assignment', 'enroll', 'bulk', 'batch', 'class', 'teacher', 'student']), 0, 50) as $route) { + $method = $this->primaryMethod($route); + if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) { continue; } + $response = $this->requestAs($this->actorFor($route->uri()), $method, $route->uri(), $this->payloadFor($method, $route->uri()) + [ + 'student_ids' => [$this->ids['studentId'], $this->ids['studentId'], 99999999], 'teacher_ids' => [$this->teacher->id, $this->teacher->id, $this->parent->id], + 'class_section_ids' => [$this->ids['classSectionId'], $this->ids['classSectionId'], 99999999], 'parent_ids' => [$this->ids['parentId'], $this->ids['parentId'], 99999999], 'force' => true, 'overwrite' => true, + ]); + $this->assertControlled($response, $method, $route->uri()); + $body = strtolower($response->getContent()); + $this->assertStringNotContainsString('duplicate entry', $body); + $this->assertStringNotContainsString('foreign key constraint', $body); + $this->assertStringNotContainsString('sqlstate', $body); + } + } + + public function test_parent_and_student_cannot_force_bulk_assignment_scope(): void + { + foreach (array_slice($this->apiRoutesContainingAny(['assign', 'bulk', 'batch', 'class-sections', 'teacher-class', 'enroll']), 0, 30) as $route) { + $method = $this->primaryMethod($route); + if (! in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'], true)) { continue; } + foreach ([$this->parent, $this->studentUser] as $actor) { + $response = $this->requestAs($actor, $method, $route->uri(), ['student_ids' => [$this->ids['studentId']], 'teacher_ids' => [$this->teacher->id], 'class_section_ids' => [$this->ids['classSectionId']], 'all' => true, 'force' => true]); + $this->assertStatusIn($response, [400, 401, 403, 404, 405, 409, 419, 422], $method . ' ' . $route->uri()); + } + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiBatch14CrossModuleInvariantRiskRegisterTest.php b/tests/Feature/Api/V1/FullSurface/ApiBatch14CrossModuleInvariantRiskRegisterTest.php new file mode 100644 index 00000000..05081135 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiBatch14CrossModuleInvariantRiskRegisterTest.php @@ -0,0 +1,38 @@ + ['auth', 'login', 'frontend'], + 'identity-privacy' => ['password', 'verify', 'profile', 'me'], + 'bulk-assignment' => ['assign', 'bulk', 'batch', 'class'], + 'print-export-tamper' => ['print', 'export', 'download', 'certificate'], + 'audit-tamper' => ['audit', 'log', 'activity'], + 'finance-reconcile' => ['finance', 'payment', 'refund', 'ledger'], + 'academic-rollback' => ['promotion', 'school-year', 'reopen'], + 'kiosk-device' => ['attendance', 'scanner', 'badge'], + 'notification-template' => ['notification', 'template', 'broadcast'], + 'storage-signed-url' => ['file', 'media', 'signed', 'upload'], + ]; + + foreach ($riskFamilies as $name => $needles) { + $this->assertNotEmpty($this->apiRoutesContainingAny($needles), 'Batch 14 expected route coverage for ' . $name); + } + } + + public function test_batch14_high_risk_mutation_families_exist(): void + { + foreach (['users', 'students', 'attendance', 'finance', 'payment', 'roles', 'permissions', 'settings', 'messages', 'inventory'] as $family) { + $mutations = array_filter($this->apiRoutesContainingAny([$family]), function ($route): bool { + return in_array($this->primaryMethod($route), ['POST', 'PUT', 'PATCH', 'DELETE'], true); + }); + $this->assertNotEmpty($mutations, 'Batch 14 expected mutating route family: ' . $family); + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiBatch14DataLifecycleArchivePurgeContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiBatch14DataLifecycleArchivePurgeContractTest.php new file mode 100644 index 00000000..825ff17a --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiBatch14DataLifecycleArchivePurgeContractTest.php @@ -0,0 +1,31 @@ +apiRoutesContainingAny(['archive', 'restore', 'purge', 'erase', 'retention', 'delete', 'destroy']), 0, 50) as $route) { + $method = $this->primaryMethod($route); + $response = $this->requestAs($this->actorFor($route->uri()), $method, $route->uri(), $this->payloadFor($method, $route->uri()) + ['ids' => [$this->ids['studentId'], 99999999, '../../.env'], 'hard_delete' => true, 'purge' => true, 'cascade' => true, 'include_audit_logs' => true, 'reason' => '', 'confirmed' => false]); + $this->assertControlled($response, $method, $route->uri()); + $body = strtolower($response->getContent()); + $this->assertStringNotContainsString('sqlstate', $body); $this->assertStringNotContainsString('foreign key constraint', $body); + } + } + + public function test_non_admin_users_cannot_purge_or_restore_global_records(): void + { + foreach (array_slice($this->apiRoutesContainingAny(['purge', 'erase', 'restore', 'archive', 'delete']), 0, 35) as $route) { + $method = $this->primaryMethod($route); + if (! in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'], true)) { continue; } + foreach ([$this->teacher, $this->parent, $this->studentUser] as $actor) { + $response = $this->requestAs($actor, $method, $route->uri(), ['ids' => [$this->ids['studentId']], 'hard_delete' => true, 'purge' => true, 'global' => true]); + $this->assertStatusIn($response, [400, 401, 403, 404, 405, 409, 419, 422], $method . ' ' . $route->uri()); + } + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiBatch14FinanceReconciliationAndRefundAbuseContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiBatch14FinanceReconciliationAndRefundAbuseContractTest.php new file mode 100644 index 00000000..c83be0f7 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiBatch14FinanceReconciliationAndRefundAbuseContractTest.php @@ -0,0 +1,31 @@ +apiRoutesContainingAny(['finance', 'invoice', 'payment', 'refund', 'transaction', 'ledger', 'reconcile']), 0, 55) as $route) { + $method = $this->primaryMethod($route); + $response = $this->requestAs($this->actorFor($route->uri()), $method, $route->uri(), $this->payloadFor($method, $route->uri()) + [ + 'invoice_id' => $this->ids['invoiceId'], 'parent_id' => 99999999, 'student_id' => 99999999, 'payment_id' => 99999999, 'refund_id' => 99999999, 'transaction_id' => 'provider-' . uniqid(), 'provider_transaction_id' => 'provider-' . uniqid(), 'amount' => -99999.99, 'net_amount' => -99999.99, 'fee_amount' => -99999.99, 'currency' => 'XXX', 'reconciled' => true, 'force_posted' => true, + ]); + $this->assertControlled($response, $method, $route->uri()); + $body = strtolower($response->getContent()); + $this->assertStringNotContainsString('sqlstate', $body); $this->assertStringNotContainsString('duplicate entry', $body); $this->assertStringNotContainsString('foreign key constraint', $body); + } + } + + public function test_parent_cannot_self_refund_or_self_reconcile(): void + { + foreach (array_slice($this->apiRoutesContainingAny(['refund', 'reconcile', 'ledger', 'payment']), 0, 35) as $route) { + $method = $this->primaryMethod($route); + if (! in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'], true)) { continue; } + $response = $this->requestAs($this->parent, $method, $route->uri(), ['invoice_id' => $this->ids['invoiceId'], 'amount' => 100.00, 'approved' => true, 'reconciled' => true, 'refund_now' => true]); + $this->assertStatusIn($response, [400, 401, 403, 404, 405, 409, 419, 422], $method . ' ' . $route->uri()); + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiBatch14NotificationTemplateInjectionContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiBatch14NotificationTemplateInjectionContractTest.php new file mode 100644 index 00000000..3c4dede5 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiBatch14NotificationTemplateInjectionContractTest.php @@ -0,0 +1,34 @@ +apiRoutesContainingAny(['notification', 'notify', 'message', 'email', 'whatsapp', 'template', 'broadcast']), 0, 55) as $route) { + $method = $this->primaryMethod($route); + if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) { continue; } + $response = $this->requestAs($this->actorFor($route->uri()), $method, $route->uri(), $this->payloadFor($method, $route->uri()) + [ + 'subject' => '{{ config("app.key") }}', 'message' => '{{ $user->password }}', 'body' => '${jndi:ldap://127.0.0.1/a}', 'template' => '@php echo env("APP_KEY"); @endphp', 'channel' => 'all', 'recipients' => ['*'], 'phone' => "+15551234567\r\nBcc: attacker@example.test", 'email' => "victim@example.test\r\nBcc: attacker@example.test", + ]); + $this->assertControlled($response, $method, $route->uri()); + $body = strtolower($response->getContent()); + $this->assertStringNotContainsString('app_key', $body); $this->assertStringNotContainsString('document.cookie', $body); $this->assertStringNotContainsString('jndi:ldap', $body); + } + } + + public function test_non_admin_users_cannot_broadcast_by_template_override(): void + { + foreach (array_slice($this->apiRoutesContainingAny(['broadcast', 'notification', 'email', 'whatsapp']), 0, 30) as $route) { + $method = $this->primaryMethod($route); + if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) { continue; } + foreach ([$this->parent, $this->teacher, $this->studentUser] as $actor) { + $response = $this->requestAs($actor, $method, $route->uri(), ['channel' => 'all', 'recipients' => ['all_school'], 'template_id' => 1, 'force_send' => true]); + $this->assertStatusIn($response, [400, 401, 403, 404, 405, 409, 419, 422], $method . ' ' . $route->uri()); + } + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiBatch14PrintExportTamperingContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiBatch14PrintExportTamperingContractTest.php new file mode 100644 index 00000000..6be2e9a7 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiBatch14PrintExportTamperingContractTest.php @@ -0,0 +1,33 @@ +apiRoutesContainingAny(['print', 'export', 'download', 'certificate', 'badge', 'receipt', 'transcript', 'report-card']), 0, 50) as $route) { + $method = $this->primaryMethod($route); + $response = $this->requestAs($this->actorFor($route->uri()), $method, $route->uri(), $this->payloadFor($method, $route->uri()) + [ + 'parent_id' => 99999999, 'class_section_id' => 99999999, 'include_private_notes' => true, 'include_deleted' => true, 'include_financials' => true, 'signed_by' => 'Forged Principal', 'signature' => 'data:image/png;base64,forged', 'format' => '../../storage/logs/laravel.log', + ]); + $this->assertControlled($response, $method, $route->uri()); + $body = strtolower($response->getContent()); + foreach (['app_key', 'laravel.log', 'password', 'remember_token', 'sqlstate'] as $forbidden) { $this->assertStringNotContainsString($forbidden, $body); } + } + } + + public function test_low_privilege_users_cannot_generate_full_school_exports(): void + { + foreach (array_slice($this->apiRoutesContainingAny(['export', 'download', 'print', 'report', 'certificate']), 0, 35) as $route) { + $method = $this->primaryMethod($route); + foreach ([$this->parent, $this->teacher, $this->studentUser] as $actor) { + $response = $this->requestAs($actor, $method, $route->uri(), ['scope' => 'all_school', 'all_students' => true, 'include_finance' => true, 'include_guardians' => true, 'format' => 'csv']); + $this->assertStatusIn($response, [200, 204, 302, 400, 401, 403, 404, 405, 409, 419, 422], $method . ' ' . $route->uri()); + $this->assertStringNotContainsString('remember_token', strtolower($response->getContent())); + } + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiBatch14RegressionMegaSweepTest.php b/tests/Feature/Api/V1/FullSurface/ApiBatch14RegressionMegaSweepTest.php new file mode 100644 index 00000000..33cb73da --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiBatch14RegressionMegaSweepTest.php @@ -0,0 +1,33 @@ +apiRoutesContainingAny($needles), 0, 18) as $route) { + $method = $this->primaryMethod($route); + $payload = $this->payloadFor($method, $route->uri()) + [ + 'id' => '../../.env', 'student_id' => ['not', 'an', 'id'], 'parent_id' => 99999999, 'amount' => '-999999999.999', 'status' => '', + 'path' => '../../storage/logs/laravel.log', 'signature' => 'forged', 'approved_by' => $this->admin->id, 'owner_type' => 'App\\Models\\User', 'owner_id' => $this->admin->id, + 'include' => '*,password,remember_token,audit_logs', 'scope' => 'global', 'force' => true, 'hard_delete' => true, + ]; + $response = $this->requestAs($this->actorFor($route->uri()), $method, $route->uri(), $payload); + $this->assertControlled($response, $method, $route->uri()); + $body = strtolower($response->getContent()); + foreach (['sqlstate', 'app_key', 'db_password', 'remember_token', 'stack trace', 'laravel.log'] as $forbidden) { $this->assertStringNotContainsString($forbidden, $body); } + } + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiBatch14RelationshipOwnershipMatrixExpansionTest.php b/tests/Feature/Api/V1/FullSurface/ApiBatch14RelationshipOwnershipMatrixExpansionTest.php new file mode 100644 index 00000000..2c12014a --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiBatch14RelationshipOwnershipMatrixExpansionTest.php @@ -0,0 +1,31 @@ +apiRoutesContainingAny(['student', 'parent', 'family', 'invoice', 'attendance', 'score', 'guardian', 'authorized']), 0, 60) as $route) { + $method = $this->primaryMethod($route); + if (! in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'], true)) { continue; } + foreach ([$this->parent, $this->teacher] as $actor) { + $response = $this->requestAs($actor, $method, $route->uri(), $this->payloadFor($method, $route->uri()) + ['student_ids' => [$this->ids['studentId'], 99999999], 'parent_id' => 99999999, 'family_id' => 99999999, 'guardian_id' => 99999999, 'owner_id' => $this->admin->id, 'owner_type' => 'App\\Models\\User', 'created_by' => $this->admin->id, 'scope' => 'global']); + $this->assertControlled($response, $method, $route->uri()); + $body = strtolower($response->getContent()); $this->assertStringNotContainsString('sqlstate', $body); $this->assertStringNotContainsString('remember_token', $body); + } + } + } + + public function test_student_user_cannot_read_or_write_other_relationship_graphs(): void + { + foreach (array_slice($this->apiRoutesContainingAny(['parent', 'family', 'guardian', 'invoice', 'attendance', 'report-card']), 0, 40) as $route) { + $method = $this->primaryMethod($route); + $response = $this->requestAs($this->studentUser, $method, $route->uri(), ['student_id' => 99999999, 'parent_id' => 99999999, 'family_id' => 99999999, 'include' => 'guardians,invoices,attendance,notes']); + $this->assertStatusIn($response, [200, 204, 302, 400, 401, 403, 404, 405, 409, 419, 422], $method . ' ' . $route->uri()); + $this->assertStringNotContainsString('remember_token', strtolower($response->getContent())); + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiBatch14SessionCookieCsrfBoundaryContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiBatch14SessionCookieCsrfBoundaryContractTest.php new file mode 100644 index 00000000..2073b4dc --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiBatch14SessionCookieCsrfBoundaryContractTest.php @@ -0,0 +1,33 @@ +apiRoutesContainingAny(['auth', 'login', 'logout', 'me', 'profile', 'frontend']), 0, 40) as $route) { + $method = $this->primaryMethod($route); + $response = $this->withCookie('laravel_session', 'attacker-controlled-session') + ->withHeader('X-XSRF-TOKEN', 'not-a-real-xsrf-token') + ->actingAs($this->actorFor($route->uri()), 'api') + ->json($method, $this->materializePath($route->uri()), $this->payloadFor($method, $route->uri())); + $this->assertControlled($response, $method, $route->uri()); + $this->assertStringNotContainsString('csrf token mismatch', strtolower($response->getContent())); + } + } + + public function test_browser_only_session_headers_do_not_grant_api_privileges(): void + { + foreach (array_slice($this->apiRoutesContainingAny(['admin', 'users', 'roles', 'permissions', 'finance', 'inventory', 'attendance']), 0, 30) as $route) { + $method = $this->primaryMethod($route); + $response = $this->withCookie('laravel_session', 'fake-admin-browser-session') + ->withHeaders(['X-Requested-With' => 'XMLHttpRequest', 'X-CSRF-TOKEN' => 'fake-csrf', 'X-User-Id' => (string) $this->admin->id]) + ->json($method, $this->materializePath($route->uri()), $this->payloadFor($method, $route->uri())); + $this->assertStatusIn($response, [401, 403, 404, 405, 419, 422], $method . ' ' . $route->uri()); + $this->assertStringNotContainsString('sqlstate', strtolower($response->getContent())); + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiBatch14StoragePathAndSignedUrlContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiBatch14StoragePathAndSignedUrlContractTest.php new file mode 100644 index 00000000..ce080f4f --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiBatch14StoragePathAndSignedUrlContractTest.php @@ -0,0 +1,29 @@ +apiRoutesContainingAny(['file', 'media', 'upload', 'download', 'export', 'import', 'attachment', 'document', 'signed']), 0, 55) as $route) { + $method = $this->primaryMethod($route); + $response = $this->requestAs($this->actorFor($route->uri()), $method, $route->uri(), $this->payloadFor($method, $route->uri()) + ['path' => '../../.env', 'filename' => '../../storage/logs/laravel.log', 'disk' => 'local', 'storage_disk' => 's3://private-bucket', 'url' => 'file:///etc/passwd', 'expires' => '2099-01-01', 'signature' => 'forged-signature', 'mime_type' => 'text/x-php']); + $this->assertControlled($response, $method, $route->uri()); + $body = strtolower($response->getContent()); + foreach (['app_key', 'db_password', 'laravel.log', '/etc/passwd', 'begin rsa private key'] as $forbidden) { $this->assertStringNotContainsString($forbidden, $body); } + } + } + + public function test_public_file_routes_do_not_honor_expired_or_forged_signature_parameters(): void + { + foreach (array_slice($this->apiRoutesContainingAny(['download', 'signed', 'public', 'media', 'file']), 0, 35) as $route) { + $method = $this->primaryMethod($route); + $response = $this->json($method, $this->materializePath($route->uri()) . '?signature=forged&expires=1&path=../../.env'); + $this->assertStatusIn($response, [200, 204, 302, 400, 401, 403, 404, 405, 410, 419, 422], $method . ' ' . $route->uri()); + $this->assertStringNotContainsString('app_key', strtolower($response->getContent())); + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiBatch15AdminImpersonationSessionBoundaryContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiBatch15AdminImpersonationSessionBoundaryContractTest.php new file mode 100644 index 00000000..adc9fc2b --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiBatch15AdminImpersonationSessionBoundaryContractTest.php @@ -0,0 +1,32 @@ +apiRoutesContainingAny(['impersonate', 'acting-as', 'act-as', 'switch-user', 'delegate', 'behalf']); + + foreach (array_slice($routes, 0, 30) as $route) { + $method = $this->primaryMethod($route); + $uri = $route->uri(); + $payload = $this->payloadFor($method, $uri) + [ + 'impersonator_id' => $this->admin->id, + 'target_user_id' => $this->admin->id, + 'acting_as' => 'administrator', + 'reason' => 'Batch 15 impersonation boundary probe', + 'approved_by' => $this->admin->id, + 'expires_at' => '2099-01-01', + ]; + + $parentResponse = $this->requestAs($this->parent, $method, $uri, $payload); + $this->assertControlled($parentResponse, $method, $uri); + $this->assertStringNotContainsString('access_token', strtolower($parentResponse->getContent())); + } + } + +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiBatch15AuthTokenRotationAndRevocationContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiBatch15AuthTokenRotationAndRevocationContractTest.php new file mode 100644 index 00000000..eed4e194 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiBatch15AuthTokenRotationAndRevocationContractTest.php @@ -0,0 +1,50 @@ +apiRoutesContainingAny(['auth', 'login', 'logout', 'refresh', 'token', 'me', 'profile', 'session']); + + foreach (array_slice($routes, 0, 35) as $route) { + $method = $this->primaryMethod($route); + $uri = $route->uri(); + $payload = $this->payloadFor($method, $uri) + [ + 'token' => 'forged.batch15.token', + 'refresh_token' => 'stale-refresh-token', + 'access_token' => 'Bearer forged', + 'expires_in' => -1, + 'user_id' => $this->admin->id, + 'remember' => true, + ]; + + $response = $this->requestAs($this->actorFor($uri), $method, $uri, $payload); + $this->assertControlled($response, $method, $uri); + + $body = strtolower($response->getContent()); + foreach (['plain_password', 'remember_token', 'oauth_secret', 'personal_access_token', 'refresh_token_secret'] as $forbidden) { + $this->assertStringNotContainsString($forbidden, $body); + } + } + } + + public function test_batch15_repeat_logout_and_refresh_replay_are_controlled(): void + { + foreach (array_slice($this->apiRoutesContainingAny(['logout', 'refresh', 'token']), 0, 20) as $route) { + $method = $this->primaryMethod($route); + $uri = $route->uri(); + + foreach (['first-replay-key', 'first-replay-key'] as $key) { + $response = $this->withHeader('Idempotency-Key', $key) + ->requestAs($this->admin, $method, $uri, $this->payloadFor($method, $uri)); + $this->assertControlled($response, $method, $uri); + } + } + } + +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiBatch15BackgroundCommandAndSchedulerContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiBatch15BackgroundCommandAndSchedulerContractTest.php new file mode 100644 index 00000000..f45d2915 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiBatch15BackgroundCommandAndSchedulerContractTest.php @@ -0,0 +1,34 @@ +apiRoutesContainingAny(['command', 'schedule', 'scheduler', 'cron', 'job', 'queue', 'sync', 'recalculate', 'cleanup', 'send']); + + foreach (array_slice($routes, 0, 40) as $route) { + $method = $this->primaryMethod($route); + $uri = $route->uri(); + $payload = $this->payloadFor($method, $uri) + [ + 'command' => 'migrate:fresh --seed', + 'force' => true, + 'run_now' => true, + 'target' => 'all', + 'client_request_id' => 'batch15-command-replay', + ]; + + $teacherResponse = $this->requestAs($this->teacher, $method, $uri, $payload); + $this->assertControlled($teacherResponse, $method, $uri); + $this->assertStringNotContainsString('migrate:fresh', strtolower($teacherResponse->getContent())); + + $adminResponse = $this->requestAs($this->admin, $method, $uri, $payload); + $this->assertControlled($adminResponse, $method, $uri); + } + } + +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiBatch15ConsentPrivacyAndDataSubjectRightsContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiBatch15ConsentPrivacyAndDataSubjectRightsContractTest.php new file mode 100644 index 00000000..9d18ee5d --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiBatch15ConsentPrivacyAndDataSubjectRightsContractTest.php @@ -0,0 +1,48 @@ +apiRoutesContainingAny(['privacy', 'consent', 'erase', 'erasure', 'delete-account', 'export-data', 'data-subject', 'retention']); + + foreach (array_slice($routes, 0, 35) as $route) { + $method = $this->primaryMethod($route); + $uri = $route->uri(); + $payload = $this->payloadFor($method, $uri) + [ + 'user_id' => $this->admin->id, + 'parent_id' => $this->ids['parentId'], + 'student_id' => 99999999, + 'scope' => 'all_users', + 'include_deleted' => true, + 'include_sensitive' => true, + 'reason' => 'Batch 15 privacy boundary probe', + ]; + + $parentResponse = $this->requestAs($this->parent, $method, $uri, $payload); + $this->assertControlled($parentResponse, $method, $uri); + $this->assertStringNotContainsString('password', strtolower($parentResponse->getContent())); + $this->assertStringNotContainsString('remember_token', strtolower($parentResponse->getContent())); + } + } + + public function test_batch15_student_actor_cannot_request_family_wide_or_school_wide_privacy_exports(): void + { + foreach (array_slice($this->apiRoutesContainingAny(['export', 'privacy', 'download', 'report']), 0, 30) as $route) { + $method = $this->primaryMethod($route); + $uri = $route->uri(); + $response = $this->requestAs($this->studentUser, $method, $uri, $this->payloadFor($method, $uri) + [ + 'scope' => 'school', 'family_id' => 1, 'student_ids' => '*', 'include_finance' => true, + ]); + + $this->assertControlled($response, $method, $uri); + $this->assertStringNotContainsString('db_password', strtolower($response->getContent())); + } + } + +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiBatch15CrossSchoolIdentityCollisionContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiBatch15CrossSchoolIdentityCollisionContractTest.php new file mode 100644 index 00000000..c9c1c5bd --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiBatch15CrossSchoolIdentityCollisionContractTest.php @@ -0,0 +1,35 @@ +apiRoutesContainingAny(['students', 'users', 'parents', 'family', 'guardian', 'enrollment', 'registration']); + + foreach (array_slice($routes, 0, 35) as $route) { + $method = $this->primaryMethod($route); + if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) { + continue; + } + + $payload = $this->payloadFor($method, $route->uri()) + [ + 'external_id' => 'SHARED-EXTERNAL-ID', + 'student_number' => 'SHARED-STUDENT-NUMBER', + 'email' => $this->admin->email, + 'phone' => '+15550000000', + 'school_id' => 99999999, + 'tenant_id' => 99999999, + ]; + + $response = $this->requestAs($this->admin, $method, $route->uri(), $payload); + $this->assertControlled($response, $method, $route->uri()); + $this->assertStringNotContainsString('duplicate entry', strtolower($response->getContent())); + } + } + +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiBatch15DataResidencyBackupAndRestoreContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiBatch15DataResidencyBackupAndRestoreContractTest.php new file mode 100644 index 00000000..c1680cbe --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiBatch15DataResidencyBackupAndRestoreContractTest.php @@ -0,0 +1,36 @@ +apiRoutesContainingAny(['backup', 'restore', 'snapshot', 'residency', 'tenant', 'database', 'dump', 'archive']); + + foreach (array_slice($routes, 0, 30) as $route) { + $method = $this->primaryMethod($route); + $uri = $route->uri(); + $payload = $this->payloadFor($method, $uri) + [ + 'region' => 'external-untrusted-region', + 'path' => '../../database/database.sqlite', + 'include_secrets' => true, + 'restore_from' => 's3://evil-bucket/snapshot.sql', + 'truncate_before_restore' => true, + ]; + + foreach ([$this->parent, $this->teacher, $this->studentUser] as $actor) { + $response = $this->requestAs($actor, $method, $uri, $payload); + $this->assertControlled($response, $method, $uri); + $body = strtolower($response->getContent()); + foreach (['app_key', 'db_password', 'database.sqlite', 'backup.sql', 'aws_secret'] as $forbidden) { + $this->assertStringNotContainsString($forbidden, $body); + } + } + } + } + +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiBatch15DomainEventOutboxAndNotificationConsistencyContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiBatch15DomainEventOutboxAndNotificationConsistencyContractTest.php new file mode 100644 index 00000000..f27096a8 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiBatch15DomainEventOutboxAndNotificationConsistencyContractTest.php @@ -0,0 +1,32 @@ +apiRoutesContainingAny(['event', 'outbox', 'notification', 'broadcast', 'message', 'email', 'whatsapp', 'webhook']); + + foreach (array_slice($routes, 0, 45) as $route) { + $method = $this->primaryMethod($route); + $uri = $route->uri(); + $payload = $this->payloadFor($method, $uri) + [ + 'event_id' => 'evt_batch15_replay', + 'event_type' => 'UserPromotedToAdministrator', + 'aggregate_type' => 'school', + 'aggregate_id' => '*', + 'actor_id' => $this->admin->id, + 'delivered_at' => '2099-01-01T00:00:00Z', + 'signature' => 'forged-signature', + ]; + + $response = $this->requestAs($this->teacher, $method, $uri, $payload); + $this->assertControlled($response, $method, $uri); + } + } + +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiBatch15FeatureFlagExperimentAndRolloutContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiBatch15FeatureFlagExperimentAndRolloutContractTest.php new file mode 100644 index 00000000..90b1e66c --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiBatch15FeatureFlagExperimentAndRolloutContractTest.php @@ -0,0 +1,32 @@ +apiRoutesContainingAny(['feature', 'flag', 'experiment', 'rollout', 'settings', 'configuration']); + + foreach (array_slice($routes, 0, 35) as $route) { + $method = $this->primaryMethod($route); + $uri = $route->uri(); + $payload = $this->payloadFor($method, $uri) + [ + 'feature' => 'all_admin_features', + 'enabled' => true, + 'rollout' => 100, + 'audience' => 'all', + 'actor_id' => $this->parent->id, + 'bypass_permissions' => true, + 'expires_at' => '2099-01-01', + ]; + + $response = $this->requestAs($this->parent, $method, $uri, $payload); + $this->assertControlled($response, $method, $uri); + } + } + +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiBatch15FormRequestAuthorizationCoverageContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiBatch15FormRequestAuthorizationCoverageContractTest.php new file mode 100644 index 00000000..4eac3aa3 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiBatch15FormRequestAuthorizationCoverageContractTest.php @@ -0,0 +1,33 @@ +apiRoutes(), 0, 120) as $route) { + $method = $this->primaryMethod($route); + if (! in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'], true)) { + continue; + } + + $uri = $route->uri(); + $payload = $this->payloadFor($method, $uri) + [ + '_method' => $method, + 'authorize' => true, + 'can' => '*', + 'role' => 'administrator', + 'permissions' => ['*'], + ]; + + $this->actingAs($this->studentUser, 'api'); + $response = $this->json($method, $this->materializePath($uri), $payload); + $this->assertControlled($response, $method, $uri); + } + } + +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiBatch15GlobalSearchAndDirectoryEnumerationContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiBatch15GlobalSearchAndDirectoryEnumerationContractTest.php new file mode 100644 index 00000000..dc5b1331 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiBatch15GlobalSearchAndDirectoryEnumerationContractTest.php @@ -0,0 +1,31 @@ +apiRoutesContainingAny(['search', 'lookup', 'directory', 'autocomplete', 'users', 'students', 'parents']); + $queries = ['a', '*', '%', '@', 'admin@example.com', $this->admin->email, ' OR 1=1 --', str_repeat('x', 2048)]; + + foreach (array_slice($routes, 0, 45) as $route) { + $method = $this->primaryMethod($route); + foreach (array_slice($queries, 0, 4) as $query) { + $response = $this->requestAs($this->studentUser, $method, $route->uri(), $this->payloadFor($method, $route->uri()) + [ + 'q' => $query, + 'search' => $query, + 'email' => $query, + 'scope' => 'all', + ]); + + $this->assertControlled($response, $method, $route->uri()); + $this->assertStringNotContainsString('sqlstate', strtolower($response->getContent())); + } + } + } + +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiBatch15LocalizationContentDirectionAndEncodingContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiBatch15LocalizationContentDirectionAndEncodingContractTest.php new file mode 100644 index 00000000..50580682 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiBatch15LocalizationContentDirectionAndEncodingContractTest.php @@ -0,0 +1,35 @@ +apiRoutesContainingAny(['message', 'contact', 'support', 'notification', 'student', 'parent', 'settings', 'certificate']); + $text = "اختبار עברית café e\u{0301} \u{202E}evil.pdf 😬"; + + foreach (array_slice($routes, 0, 45) as $route) { + $method = $this->primaryMethod($route); + if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) { + continue; + } + + $response = $this->withHeader('Accept-Language', 'ar,en;q=0.7') + ->requestAs($this->actorFor($route->uri()), $method, $route->uri(), $this->payloadFor($method, $route->uri()) + [ + 'name' => $text, + 'title' => $text, + 'message' => $text, + 'notes' => $text, + 'description' => $text, + ]); + + $this->assertControlled($response, $method, $route->uri()); + $this->assertStringNotContainsString('malformed utf-8', strtolower($response->getContent())); + } + } + +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiBatch15MetadataFilteringAndSparseFieldsetSecurityContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiBatch15MetadataFilteringAndSparseFieldsetSecurityContractTest.php new file mode 100644 index 00000000..5ff26a3b --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiBatch15MetadataFilteringAndSparseFieldsetSecurityContractTest.php @@ -0,0 +1,35 @@ +apiRoutesContainingAny(['users', 'students', 'parents', 'family', 'finance', 'attendance', 'reports', 'dashboard']); + + foreach (array_slice($routes, 0, 55) as $route) { + $method = $this->primaryMethod($route); + if ($method !== 'GET') { + continue; + } + + $response = $this->requestAs($this->parent, $method, $route->uri(), [ + 'fields' => '*,password,remember_token,permissions,roles,audit_logs', + 'include' => 'users,roles,permissions,finance.raw_provider_payload,deleted,archived', + 'meta' => 'debug,sql,bindings,policies', + 'debug' => true, + ]); + + $this->assertControlled($response, $method, $route->uri()); + $body = strtolower($response->getContent()); + foreach (['remember_token', 'raw_provider_payload', 'sql', 'bindings', 'password'] as $forbidden) { + $this->assertStringNotContainsString($forbidden, $body); + } + } + } + +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiBatch15RateLimitAndAbusePatternContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiBatch15RateLimitAndAbusePatternContractTest.php new file mode 100644 index 00000000..11d8f6a4 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiBatch15RateLimitAndAbusePatternContractTest.php @@ -0,0 +1,35 @@ +apiRoutesContainingAny(['login', 'password', 'verify', 'contact', 'support', 'message', 'scanner', 'badge']); + + foreach (array_slice($routes, 0, 25) as $route) { + $method = $this->primaryMethod($route); + $uri = $route->uri(); + + for ($attempt = 1; $attempt <= 3; $attempt++) { + $response = $this->withHeader('X-Forwarded-For', '203.0.113.' . $attempt) + ->requestAs($this->actorFor($uri), $method, $uri, $this->payloadFor($method, $uri) + [ + 'email' => 'rate-limit-'.$attempt.'@example.test', + 'password' => 'wrong-password', + 'message' => str_repeat('abuse ', 200), + ]); + + $this->assertControlled($response, $method, $uri); + $body = strtolower($response->getContent()); + foreach (['too many connections', 'sqlstate', 'stack trace', 'vendor/laravel'] as $forbidden) { + $this->assertStringNotContainsString($forbidden, $body); + } + } + } + } + +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiBatch15RegressionUltraSweepTest.php b/tests/Feature/Api/V1/FullSurface/ApiBatch15RegressionUltraSweepTest.php new file mode 100644 index 00000000..c81337be --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiBatch15RegressionUltraSweepTest.php @@ -0,0 +1,47 @@ +apiRoutesContainingAny($needles), 0, 20) as $route) { + $method = $this->primaryMethod($route); + $uri = $route->uri(); + $payload = $this->payloadFor($method, $uri) + [ + 'tenant_id' => 99999999, + 'school_id' => 99999999, + 'owner_id' => $this->admin->id, + 'owner_type' => 'App\\Models\\User', + 'scope' => 'global', + 'include' => '*,password,remember_token,secrets,deleted', + 'force' => true, + 'hard_delete' => true, + 'amount' => 'NaN', + 'path' => '../../.env', + 'signature' => 'forged', + 'redirect' => 'https://evil.example.test', + ]; + + $response = $this->requestAs($this->actorFor($uri), $method, $uri, $payload); + $this->assertControlled($response, $method, $uri); + $body = strtolower($response->getContent()); + foreach (['app_key', 'db_password', 'remember_token', 'stack trace', 'sqlstate', '.env'] as $forbidden) { + $this->assertStringNotContainsString($forbidden, $body); + } + } + } + } + +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiBatch15ReportDrilldownAndAggregatePrivacyContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiBatch15ReportDrilldownAndAggregatePrivacyContractTest.php new file mode 100644 index 00000000..afdb971a --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiBatch15ReportDrilldownAndAggregatePrivacyContractTest.php @@ -0,0 +1,38 @@ +apiRoutesContainingAny(['report', 'analytics', 'dashboard', 'summary', 'drilldown', 'export']); + + foreach (array_slice($routes, 0, 50) as $route) { + $method = $this->primaryMethod($route); + $uri = $route->uri(); + $payload = $this->payloadFor($method, $uri) + [ + 'drilldown' => true, + 'group_by' => 'password', + 'include' => 'users.password,parents.ssn,finance.raw_provider_payload', + 'student_id' => 99999999, + 'parent_id' => 99999999, + 'scope' => 'global', + 'format' => 'csv', + ]; + + foreach ([$this->parent, $this->teacher] as $actor) { + $response = $this->requestAs($actor, $method, $uri, $payload); + $this->assertControlled($response, $method, $uri); + $body = strtolower($response->getContent()); + foreach (['password', 'remember_token', 'provider_secret', 'raw_provider_payload'] as $forbidden) { + $this->assertStringNotContainsString($forbidden, $body); + } + } + } + } + +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiBatch15RouteParameterPoisoningExpansionTest.php b/tests/Feature/Api/V1/FullSurface/ApiBatch15RouteParameterPoisoningExpansionTest.php new file mode 100644 index 00000000..0b9ffed2 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiBatch15RouteParameterPoisoningExpansionTest.php @@ -0,0 +1,30 @@ +', '1 or 1=1', str_repeat('9', 80), '٠١٢٣', 'null', '[]']; + + foreach (array_slice($this->apiRoutes(), 0, 80) as $route) { + if (! str_contains($route->uri(), '{')) { + continue; + } + + $method = $this->primaryMethod($route); + foreach (array_slice($poisoned, 0, 4) as $value) { + $path = '/' . preg_replace('/\{[^}]+\}/', $value, $route->uri()); + $this->actingAs($this->actorFor($route->uri()), 'api'); + $response = $this->json($method, $path, $this->payloadFor($method, $route->uri())); + $this->assertControlled($response, $method, $route->uri()); + $this->assertStringNotContainsString('laravel.log', strtolower($response->getContent())); + } + } + } + +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiBatch15StudentSafetyIncidentEscalationContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiBatch15StudentSafetyIncidentEscalationContractTest.php new file mode 100644 index 00000000..390e3284 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiBatch15StudentSafetyIncidentEscalationContractTest.php @@ -0,0 +1,33 @@ +apiRoutesContainingAny(['incident', 'violation', 'safety', 'alert', 'flag', 'dismissal', 'late', 'absence', 'attendance']); + + foreach (array_slice($routes, 0, 50) as $route) { + $method = $this->primaryMethod($route); + $uri = $route->uri(); + $payload = $this->payloadFor($method, $uri) + [ + 'severity' => 'critical', + 'student_id' => $this->ids['studentId'], + 'resolved' => true, + 'resolved_by' => $this->admin->id, + 'notify_parent' => false, + 'suppress_alert' => true, + ]; + + foreach ([$this->parent, $this->studentUser] as $actor) { + $response = $this->requestAs($actor, $method, $uri, $payload); + $this->assertControlled($response, $method, $uri); + } + } + } + +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiBulkImportExportAndPartialFailureExpansionTest.php b/tests/Feature/Api/V1/FullSurface/ApiBulkImportExportAndPartialFailureExpansionTest.php new file mode 100644 index 00000000..cce4744e --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiBulkImportExportAndPartialFailureExpansionTest.php @@ -0,0 +1,81 @@ +bulkRoutes() as $route) { + $method = $this->primaryMethod($route); + $uri = $route->uri(); + $response = $this->requestAs($this->actorFor($uri), $method, $uri, [ + 'ids' => [$this->ids['studentId'], 999999999, null, 'not-an-id'], + 'student_ids' => [$this->ids['studentId'], 999999999], + 'class_section_id' => $this->ids['classSectionId'], + 'school_year' => self::E2E_SCHOOL_YEAR, + 'semester' => self::E2E_SEMESTER, + ]); + + $this->assertControlled($response, $method, $uri); + $this->assertNoServerError($response, "$method $uri mixed bulk payload"); + } + } + + public function test_import_routes_reject_wrong_file_types_with_validation_not_crashes(): void + { + Storage::fake('local'); + + foreach ($this->importRoutes() as $route) { + $method = $this->primaryMethod($route); + $uri = $route->uri(); + $response = $this->requestAs($this->actorFor($uri), $method, $uri, [ + 'file' => UploadedFile::fake()->create('not-a-spreadsheet.exe', 4, 'application/x-msdownload'), + 'school_year' => self::E2E_SCHOOL_YEAR, + 'semester' => self::E2E_SEMESTER, + ]); + + $this->assertControlled($response, $method, $uri); + $this->assertNoServerError($response, "$method $uri wrong import file type"); + } + } + + public function test_export_download_routes_do_not_return_mutating_statuses(): void + { + foreach ($this->exportRoutes() as $route) { + $method = $this->primaryMethod($route); + $uri = $route->uri(); + $response = $this->requestAs($this->actorFor($uri), $method, $uri, [ + 'school_year' => self::E2E_SCHOOL_YEAR, + 'semester' => self::E2E_SEMESTER, + 'format' => 'xlsx', + ]); + + $this->assertStatusIn($response, [200, 202, 204, 302, 400, 401, 403, 404, 405, 409, 419, 422], "$method $uri export/download"); + $this->assertNoServerError($response, "$method $uri export/download"); + } + } + + /** @return list */ + private function bulkRoutes(): array + { + return array_slice(array_filter($this->apiRoutes(), fn (LaravelRoute $route): bool => preg_match('/bulk|batch|selected|import|assign|sync/i', $route->uri()) === 1), 0, 80); + } + + /** @return list */ + private function importRoutes(): array + { + return array_slice(array_filter($this->apiRoutes(), fn (LaravelRoute $route): bool => preg_match('/import|upload|attachment|document/i', $route->uri()) === 1 && in_array($this->primaryMethod($route), ['POST', 'PUT', 'PATCH'], true)), 0, 50); + } + + /** @return list */ + private function exportRoutes(): array + { + return array_slice(array_filter($this->apiRoutes(), fn (LaravelRoute $route): bool => preg_match('/export|download|pdf|print|receipt|certificate|badge/i', $route->uri()) === 1), 0, 80); + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiBusinessRuleEdgeCaseContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiBusinessRuleEdgeCaseContractTest.php new file mode 100644 index 00000000..8ec6b046 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiBusinessRuleEdgeCaseContractTest.php @@ -0,0 +1,85 @@ +apiRoutesContainingAny(['students', 'class', 'attendance', 'scores', 'report-card', 'teacher']); + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) { + continue; + } + + $uri = $route->uri(); + $payload = $this->payloadFor($method, $uri) + [ + 'school_year' => '1900-1901', + 'semester' => 'Summer-Does-Not-Exist', + 'class_section_id' => 999999, + 'student_id' => 999999, + ]; + + $response = $this->requestAs($this->actorFor($uri), $method, $uri, $payload); + $this->assertNoServerError($response, "$method $uri mismatched academic context"); + $this->assertStatusIn($response, [200, 201, 202, 204, 302, 400, 401, 403, 404, 409, 419, 422], "$method $uri mismatched academic context"); + $this->assertStringNotContainsStringIgnoringCase('foreign key constraint', $response->getContent(), $uri . ' should validate academic context before DB failure leaks.'); + } + } + + public function test_financial_amount_rules_reject_zero_negative_and_precision_abuse_cleanly(): void + { + $routes = $this->apiRoutesContainingAny(['finance', 'payment', 'refund', 'invoice', 'fee', 'installment', 'charge']); + $amounts = [0, -0.01, -999999, '0.0000000000001', '1000000000000000000000000.99']; + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) { + continue; + } + + foreach ($amounts as $amount) { + $uri = $route->uri(); + $payload = $this->payloadFor($method, $uri); + $payload['amount'] = $amount; + $payload['total'] = $amount; + $payload['balance'] = $amount; + + $response = $this->requestAs($this->admin, $method, $uri, $payload); + $this->assertNoServerError($response, "$method $uri amount $amount"); + $this->assertStringNotContainsStringIgnoringCase('decimal', $response->getContent(), $uri . ' should not leak decimal cast internals.'); + $this->assertStringNotContainsStringIgnoringCase('SQLSTATE', $response->getContent(), $uri . ' should not leak SQLSTATE for financial edge cases.'); + } + } + } + + public function test_attendance_status_and_time_edge_cases_are_controlled(): void + { + $routes = $this->apiRoutesContainingAny(['attendance', 'late', 'dismissal', 'absence']); + $statuses = ['present', 'absent', 'late', 'excused', 'early_dismissal', 'not-a-real-status']; + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) { + continue; + } + + foreach ($statuses as $status) { + $uri = $route->uri(); + $payload = $this->payloadFor($method, $uri) + [ + 'status' => $status, + 'arrival_time' => '25:99', + 'dismissal_time' => '-01:00', + ]; + + $response = $this->requestAs($this->actorFor($uri), $method, $uri, $payload); + $this->assertNoServerError($response, "$method $uri status $status"); + $this->assertControlled($response, $method, $uri); + } + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiCacheFreshnessAndConditionalRequestContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiCacheFreshnessAndConditionalRequestContractTest.php new file mode 100644 index 00000000..14603ec4 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiCacheFreshnessAndConditionalRequestContractTest.php @@ -0,0 +1,42 @@ +apiRoutesContainingAny(['profile', 'auth/me', 'dashboard', 'finance', 'messages', 'students', 'parents']) as $route) { + if ($this->primaryMethod($route) !== 'GET') { + continue; + } + + $response = $this->requestAs($this->actorFor($route->uri()), 'GET', $route->uri()); + $this->assertControlled($response, 'GET', $route->uri()); + + $cacheControl = strtolower((string) $response->headers->get('Cache-Control')); + $this->assertFalse( + str_contains($cacheControl, 'public') && ! str_contains($cacheControl, 'private'), + "{$route->uri()} should not publicly cache private authenticated data." + ); + } + } + + public function test_conditional_headers_do_not_bypass_authorization_or_return_wrong_user_data(): void + { + foreach ($this->apiRoutesContainingAny(['profile', 'auth/me', 'dashboard', 'students', 'invoices']) as $route) { + if ($this->primaryMethod($route) !== 'GET') { + continue; + } + + $response = $this->withHeaders([ + 'If-None-Match' => '"fake-etag-from-other-user"', + 'If-Modified-Since' => 'Wed, 21 Oct 2015 07:28:00 GMT', + ])->getJson($this->materializePath($route->uri())); + + $this->assertStatusIn($response, [401, 403, 404, 419], "Conditional unauthenticated request should not bypass {$route->uri()}."); + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiCalendarScheduleConflictContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiCalendarScheduleConflictContractTest.php new file mode 100644 index 00000000..35bece9b --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiCalendarScheduleConflictContractTest.php @@ -0,0 +1,56 @@ +apiRoutesContainingAny(['calendar', 'schedule', 'event', 'school-day', 'semester']); + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) { + continue; + } + + $uri = $route->uri(); + $payload = $this->payloadFor($method, $uri) + [ + 'title' => 'E2E Conflict Probe', + 'start_date' => '2026-06-20', + 'end_date' => '2025-09-01', + 'start_time' => '18:00', + 'end_time' => '08:00', + 'school_year' => self::E2E_PREV_SCHOOL_YEAR, + 'recurrence' => 'FREQ=MINUTELY;INTERVAL=0', + ]; + + $response = $this->requestAs($this->admin, $method, $uri, $payload); + $this->assertNoServerError($response, "$method $uri invalid schedule conflict"); + $this->assertControlled($response, $method, $uri); + } + } + + public function test_calendar_list_routes_tolerate_window_queries_and_do_not_leak_unscoped_private_events(): void + { + $routes = $this->apiRoutesContainingAny(['calendar', 'schedule', 'event']); + + foreach ($routes as $route) { + if ($this->primaryMethod($route) !== 'GET') { + continue; + } + + $uri = $route->uri(); + $query = '?from=1900-01-01&to=2100-12-31&scope=all&include_private=true'; + + foreach (['parent' => $this->parent, 'teacher' => $this->teacher, 'student' => $this->studentUser] as $label => $actor) { + $response = $this->requestAs($actor, 'GET', $uri . $query); + $this->assertNoServerError($response, "$label GET $uri calendar scope"); + $this->assertStatusIn($response, [200, 204, 302, 400, 401, 403, 404, 409, 419, 422], "$label GET $uri calendar scope"); + $this->assertStringNotContainsStringIgnoringCase('private_note', $response->getContent(), "$uri should not expose private event notes to $label."); + } + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiClientCompatibilityVersioningContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiClientCompatibilityVersioningContractTest.php new file mode 100644 index 00000000..91e422d9 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiClientCompatibilityVersioningContractTest.php @@ -0,0 +1,71 @@ +apiRoutes() as $route) { + $uri = $route->uri(); + if (str_starts_with($uri, 'api/v1/')) { + $this->assertTrue(true); + continue; + } + + $isAllowed = false; + foreach ($allowedUnversioned as $allowed) { + if (str_starts_with($uri, $allowed)) { + $isAllowed = true; + break; + } + } + + $this->assertTrue( + $isAllowed, + $uri . ' is an unversioned API route. If this is deliberate legacy support, add it to the explicit allow-list.' + ); + } + } + + public function test_route_names_are_unique_enough_for_client_generation(): void + { + $seen = []; + foreach (Route::getRoutes()->getRoutes() as $route) { + $name = $route->getName(); + if ($name === null || ! str_starts_with($route->uri(), 'api/')) { + continue; + } + + $key = $name . '|' . $this->primaryMethod($route); + $this->assertArrayNotHasKey($key, $seen, 'Duplicate API route name/method pair: ' . $key . ' for ' . $route->uri() . ' and ' . ($seen[$key] ?? 'unknown')); + $seen[$key] = $route->uri(); + } + } + + public function test_legacy_and_current_auth_contracts_return_compatible_failure_shapes(): void + { + $pairs = [ + ['/api/login', '/api/v1/auth/login'], + ['/user/login', '/api/v1/auth/login'], + ]; + + foreach ($pairs as [$legacy, $current]) { + $legacyResponse = $this->postJson($legacy, ['email' => 'bad@example.test', 'password' => 'wrong']); + $currentResponse = $this->postJson($current, ['email' => 'bad@example.test', 'password' => 'wrong']); + + $this->assertNoServerError($legacyResponse, $legacy . ' legacy auth failure'); + $this->assertNoServerError($currentResponse, $current . ' current auth failure'); + $this->assertStatusIn($legacyResponse, [400, 401, 403, 419, 422], $legacy . ' legacy auth failure'); + $this->assertStatusIn($currentResponse, [400, 401, 403, 419, 422], $current . ' current auth failure'); + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiConcurrencyAndStaleWriteContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiConcurrencyAndStaleWriteContractTest.php new file mode 100644 index 00000000..82fe49da --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiConcurrencyAndStaleWriteContractTest.php @@ -0,0 +1,49 @@ +apiRoutesContainingAny(['profile', 'settings', 'attendance', 'inventory', 'payments', 'refunds', 'school-years', 'messages']) as $route) { + $method = $this->primaryMethod($route); + + if (! in_array($method, ['PUT', 'PATCH', 'POST'], true)) { + continue; + } + + $payload = $this->payloadFor($method, $route->uri()) + [ + 'client_request_id' => 'e2e-concurrency-' . md5($route->uri()), + 'updated_at' => '2000-01-01T00:00:00Z', + ]; + + $first = $this->requestAs($this->actorFor($route->uri()), $method, $route->uri(), $payload); + $second = $this->requestAs($this->actorFor($route->uri()), $method, $route->uri(), $payload); + + $this->assertControlled($first, $method, $route->uri()); + $this->assertControlled($second, $method, $route->uri()); + $this->assertStringNotContainsString('deadlock', strtolower($second->getContent())); + $this->assertStringNotContainsString('duplicate entry', strtolower($second->getContent())); + } + } + + public function test_stale_if_match_headers_do_not_force_successful_mutations(): void + { + foreach ($this->apiRoutesContainingAny(['students', 'parents', 'inventory', 'payments', 'attendance', 'settings']) as $route) { + $method = $this->primaryMethod($route); + + if (! in_array($method, ['PUT', 'PATCH', 'DELETE'], true)) { + continue; + } + + $response = $this->actingAs($this->actorFor($route->uri()), 'api') + ->withHeader('If-Match', '"stale-version-that-should-not-win"') + ->json($method, $this->materializePath($route->uri()), $this->payloadFor($method, $route->uri())); + + $this->assertStatusIn($response, [200, 202, 204, 400, 403, 404, 409, 412, 422], "{$method} {$route->uri()} stale write"); + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiContentNegotiationAndMediaTypeContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiContentNegotiationAndMediaTypeContractTest.php new file mode 100644 index 00000000..d7aca39f --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiContentNegotiationAndMediaTypeContractTest.php @@ -0,0 +1,61 @@ +apiRoutesContainingAny([ + 'auth', 'users', 'students', 'parents', 'teacher', 'attendance', 'finance', 'inventory', 'messages', 'settings', + ]), 0, 80); + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + $uri = $route->uri(); + + $this->actingAs($this->actorFor($uri), 'api'); + $response = $this->withHeaders(['Accept' => 'application/json'])->json($method, $this->materializePath($uri), $this->payloadFor($method, $uri)); + + $this->assertControlled($response, $method, $uri); + $this->assertJsonResponseWhenSuccessful($response, "$method $uri with JSON Accept"); + } + } + + /** @test */ + public function unsupported_accept_headers_do_not_trigger_server_errors(): void + { + foreach (array_slice($this->apiRoutes(), 0, 60) as $route) { + $method = $this->primaryMethod($route); + $uri = $route->uri(); + + $this->actingAs($this->actorFor($uri), 'api'); + $response = $this->withHeaders(['Accept' => 'application/xml'])->json($method, $this->materializePath($uri), $this->payloadFor($method, $uri)); + + $this->assertStatusIn($response, [200, 201, 202, 204, 302, 304, 400, 401, 403, 404, 405, 406, 409, 415, 419, 422], "$method $uri unsupported Accept"); + } + } + + /** @test */ + public function malformed_content_type_for_mutations_fails_cleanly(): void + { + $routes = array_slice(array_filter($this->apiRoutes(), fn ($route) => in_array($this->primaryMethod($route), ['POST', 'PUT', 'PATCH'], true)), 0, 70); + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + $uri = $route->uri(); + + $this->actingAs($this->actorFor($uri), 'api'); + $response = $this->withHeaders([ + 'Accept' => 'application/json', + 'Content-Type' => 'text/plain', + ])->call($method, $this->materializePath($uri), [], [], [], [], 'not-json=not-really'); + + $this->assertStatusIn($response, [200, 201, 202, 204, 400, 401, 403, 404, 405, 409, 415, 419, 422], "$method $uri malformed content type"); + $this->assertLessThan(500, $response->getStatusCode(), "$method $uri should not explode on plain text bodies."); + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiContractCoverageHeatmapExpansionTest.php b/tests/Feature/Api/V1/FullSurface/ApiContractCoverageHeatmapExpansionTest.php new file mode 100644 index 00000000..2161c9df --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiContractCoverageHeatmapExpansionTest.php @@ -0,0 +1,74 @@ + ['auth', 'login', 'logout', 'me', 'password', 'register'], + 'users_roles' => ['users', 'roles', 'permissions', 'preferences', 'navigation'], + 'students_families' => ['students', 'parents', 'family', 'families', 'guardian', 'authorized'], + 'classes_teachers' => ['classes', 'class-sections', 'teachers', 'class-progress', 'assignments'], + 'attendance_safety' => ['attendance', 'absence', 'late', 'dismissal', 'incident', 'violation'], + 'academics_grades' => ['scores', 'homework', 'quiz', 'project', 'midterm', 'final', 'report-card', 'grading'], + 'finance' => ['finance', 'invoice', 'payment', 'refund', 'fee', 'installment', 'charge', 'reimbursement'], + 'inventory_procurement' => ['inventory', 'supplier', 'supply', 'procurement', 'purchase'], + 'communications' => ['messages', 'support', 'contact', 'email', 'whatsapp', 'notification'], + 'documents_print' => ['print', 'certificate', 'badge', 'sticker', 'receipt', 'export', 'download'], + 'operations' => ['settings', 'configuration', 'school-years', 'calendar', 'dashboard', 'reports'], + ]; + + $routes = $this->apiRoutes(); + + foreach ($families as $family => $needles) { + $matches = array_filter($routes, fn (LaravelRoute $route): bool => $this->uriContainsAny($route->uri(), $needles)); + + $this->assertNotEmpty($matches, $family . ' must have route-level E2E contract coverage candidates.'); + } + } + + public function test_mutating_route_surface_is_not_accidentally_unowned_by_contract_suites(): void + { + $ownedNeedles = [ + 'auth', 'users', 'roles', 'permissions', 'students', 'parents', 'family', 'guardian', 'authorized', + 'classes', 'teachers', 'attendance', 'scores', 'homework', 'quiz', 'project', 'midterm', 'final', + 'finance', 'invoice', 'payment', 'refund', 'fee', 'inventory', 'supplier', 'messages', 'support', + 'contact', 'email', 'whatsapp', 'print', 'certificate', 'badge', 'settings', 'configuration', 'school-years', + 'calendar', 'reports', 'promotion', 'withdrawal', 'enrollment', 'assignment', 'expense', 'reimbursement', + ]; + + $unowned = []; + + foreach ($this->apiRoutes() as $route) { + $method = $this->primaryMethod($route); + $uri = $route->uri(); + + if (! in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'], true)) { + continue; + } + + if (! $this->uriContainsAny($uri, $ownedNeedles)) { + $unowned[] = $method . ' ' . $uri; + } + } + + $this->assertSame([], $unowned, 'Mutating API routes must be assigned to a full-surface use-case contract bucket: ' . implode(', ', $unowned)); + } + + /** @param list $needles */ + private function uriContainsAny(string $uri, array $needles): bool + { + foreach ($needles as $needle) { + if (str_contains($uri, $needle)) { + return true; + } + } + + return false; + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiContractSnapshotSeedTest.php b/tests/Feature/Api/V1/FullSurface/ApiContractSnapshotSeedTest.php new file mode 100644 index 00000000..ad7b8bc6 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiContractSnapshotSeedTest.php @@ -0,0 +1,58 @@ +contractSamples() as $label => $sample) { + $response = $this->requestAs($sample['actor'], $sample['method'], $sample['path'], $sample['payload'] ?? []); + + $this->assertStatusIn($response, [200, 201, 202, 204, 302, 400, 401, 403, 404, 405, 409, 419, 422], $label); + $this->assertNoServerError($response, $label); + + if (in_array($response->getStatusCode(), [200, 201, 202], true) && $response->getContent() !== '' && $this->isJsonString($response->getContent())) { + $flat = json_encode($response->json(), JSON_THROW_ON_ERROR); + + foreach ($sample['expected_any'] as $key) { + if (str_contains($flat, $key)) { + $this->assertTrue(true); + continue 2; + } + } + + $this->fail($label . ' did not expose any expected contract key: ' . implode(', ', $sample['expected_any'])); + } + } + } + + public function test_auth_identity_payload_never_drops_user_identity_when_successful(): void + { + $response = $this->requestAs($this->admin, 'GET', 'api/v1/auth/me'); + + $this->assertStatusIn($response, [200, 204, 302, 401, 403, 404, 405, 419, 422], 'GET api/v1/auth/me'); + $this->assertNoServerError($response, 'GET api/v1/auth/me'); + + if ($response->getStatusCode() === 200 && $this->isJsonString($response->getContent())) { + $flat = json_encode($response->json(), JSON_THROW_ON_ERROR); + $this->assertStringContainsString('email', $flat, 'auth/me successful payload should expose identity email.'); + $this->assertStringContainsString('id', $flat, 'auth/me successful payload should expose identity id.'); + } + } + + /** @return array,expected_any:list}> */ + private function contractSamples(): array + { + return [ + 'users index contract' => ['actor' => $this->admin, 'method' => 'GET', 'path' => 'api/v1/users', 'expected_any' => ['data', 'users', 'id', 'email']], + 'students index contract' => ['actor' => $this->admin, 'method' => 'GET', 'path' => 'api/v1/students', 'expected_any' => ['data', 'students', 'student_id', 'firstname']], + 'parent profile contract' => ['actor' => $this->parent, 'method' => 'GET', 'path' => 'api/v1/parents/profile', 'expected_any' => ['user', 'parent', 'email', 'firstname']], + 'teacher classes contract' => ['actor' => $this->teacher, 'method' => 'GET', 'path' => 'api/v1/teacher/classes', 'expected_any' => ['classes', 'class_section_id', 'data']], + 'finance invoices contract' => ['actor' => $this->admin, 'method' => 'GET', 'path' => 'api/v1/finance/invoices', 'expected_any' => ['invoice', 'amount', 'data', 'parent_id']], + 'inventory items contract' => ['actor' => $this->admin, 'method' => 'GET', 'path' => 'api/v1/inventory/items', 'expected_any' => ['item', 'quantity', 'data', 'name']], + ]; + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiCrossModuleReferentialWorkflowExpansionTest.php b/tests/Feature/Api/V1/FullSurface/ApiCrossModuleReferentialWorkflowExpansionTest.php new file mode 100644 index 00000000..a05e35d3 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiCrossModuleReferentialWorkflowExpansionTest.php @@ -0,0 +1,32 @@ +apiRoutes(), fn ($route) => in_array($this->primaryMethod($route), ['POST', 'PUT', 'PATCH'], true)), 0, 120); + + $crossLinked = [ + 'student_id' => $this->ids['invoiceId'], + 'invoice_id' => $this->ids['studentId'], + 'parent_id' => $this->teacher->id, + 'teacher_id' => $this->parent->id, + 'class_section_id' => $this->ids['parentId'], + 'user_id' => $this->ids['studentId'], + ]; + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + $uri = $route->uri(); + $response = $this->requestAs($this->actorFor($uri), $method, $uri, $this->payloadFor($method, $uri) + $crossLinked); + + $this->assertControlled($response, $method, $uri); + $this->assertLessThan(500, $response->getStatusCode(), "$method $uri should not explode on cross-module ID misuse."); + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiCrossVersionRouteCompatibilityMatrixTest.php b/tests/Feature/Api/V1/FullSurface/ApiCrossVersionRouteCompatibilityMatrixTest.php new file mode 100644 index 00000000..24823658 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiCrossVersionRouteCompatibilityMatrixTest.php @@ -0,0 +1,46 @@ +postJson($versioned, ['email' => 'bad@example.test', 'password' => 'wrong']) + : $this->actingAs($this->admin, 'api')->getJson($versioned); + + $legacyResponse = str_contains($legacy, 'login') + ? $this->postJson($legacy, ['email' => 'bad@example.test', 'password' => 'wrong']) + : $this->actingAs($this->admin, 'api')->getJson($legacy); + + $this->assertNoServerError($versionedResponse, 'versioned compatibility ' . $versioned); + $this->assertNoServerError($legacyResponse, 'legacy compatibility ' . $legacy); + $this->assertLessThan(500, $versionedResponse->getStatusCode()); + $this->assertLessThan(500, $legacyResponse->getStatusCode()); + } + } + + public function test_legacy_routes_do_not_expand_beyond_documented_aliases_silently(): void + { + $legacyLike = []; + + foreach ($this->apiRoutes() as $route) { + $uri = $route->uri(); + if (! str_starts_with($uri, 'api/v1/') && ! str_contains($uri, 'docs') && ! str_contains($uri, 'health')) { + $legacyLike[] = $this->primaryMethod($route) . ' ' . $uri; + } + } + + $this->assertLessThan(80, count($legacyLike), 'Unversioned API compatibility routes should remain bounded, not become a second API by accident: ' . implode(', ', $legacyLike)); + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiCsvExcelFormulaInjectionContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiCsvExcelFormulaInjectionContractTest.php new file mode 100644 index 00000000..daaeb484 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiCsvExcelFormulaInjectionContractTest.php @@ -0,0 +1,35 @@ +apiRoutesContainingAny(['export', 'import', 'students', 'parents', 'finance', 'inventory', 'reports', 'messages']), 0, 140); + $formulas = ['=HYPERLINK("https://evil.example","click")', '+cmd|\'/C calc\'!A0', '-10+20', '@SUM(1+1)']; + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + $uri = $route->uri(); + + foreach ($formulas as $formula) { + $payload = $this->payloadFor($method, $uri) + [ + 'name' => $formula, + 'firstname' => $formula, + 'lastname' => $formula, + 'notes' => $formula, + 'description' => $formula, + 'csv_value' => $formula, + ]; + + $response = $this->requestAs($this->actorFor($uri), $method, $uri, $payload); + + $this->assertControlled($response, $method, $uri); + $this->assertNoServerError($response, 'CSV formula payload at ' . $method . ' ' . $uri); + } + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiDataExportLeastPrivilegeContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiDataExportLeastPrivilegeContractTest.php new file mode 100644 index 00000000..9e703288 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiDataExportLeastPrivilegeContractTest.php @@ -0,0 +1,61 @@ +apiRoutesContainingAny(['export', 'download', 'pdf', 'csv', 'excel', 'print', 'receipt', 'certificate']); + $actors = [ + 'parent' => $this->parent, + 'teacher' => $this->teacher, + 'student' => $this->studentUser, + ]; + + foreach ($actors as $label => $actor) { + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + if ($method !== 'GET') { + continue; + } + + $uri = $route->uri(); + $response = $this->requestAs($actor, $method, $uri . '?all=true&include_sensitive=true&format=csv'); + + $this->assertNoServerError($response, "$label GET $uri export"); + $this->assertStatusIn($response, [200, 202, 204, 302, 400, 401, 403, 404, 409, 419, 422], "$label GET $uri export"); + + if ($label !== 'admin' && $response->getStatusCode() === 200) { + $body = $response->getContent(); + $this->assertStringNotContainsStringIgnoringCase('password', $body, "$uri export must not leak passwords."); + $this->assertStringNotContainsStringIgnoringCase('remember_token', $body, "$uri export must not leak remember tokens."); + $this->assertStringNotContainsStringIgnoringCase('all parents', $body, "$uri export must not leak bulk family scope text."); + } + } + } + } + + public function test_export_filters_respect_actor_scope_parameters(): void + { + $routes = $this->apiRoutesContainingAny(['report', 'export', 'download']); + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + if ($method !== 'GET') { + continue; + } + + $uri = $route->uri(); + $query = '?parent_id=999999&student_id=999999&class_section_id=999999&scope=all&include=financial,medical,contacts'; + + foreach (['parent' => $this->parent, 'teacher' => $this->teacher] as $label => $actor) { + $response = $this->requestAs($actor, 'GET', $uri . $query); + $this->assertNoServerError($response, "$label GET $uri scoped export"); + $this->assertStatusIn($response, [200, 202, 204, 302, 400, 401, 403, 404, 409, 419, 422], "$label GET $uri scoped export"); + } + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiDataMinimizationByRoleContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiDataMinimizationByRoleContractTest.php new file mode 100644 index 00000000..1095b6eb --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiDataMinimizationByRoleContractTest.php @@ -0,0 +1,33 @@ +apiRoutesContainingAny(['parent', 'parents', 'student', 'students', 'dashboard', 'profile', 'invoices']), 0, 120); + $forbidden = ['cost_basis', 'internal_notes', 'admin_notes', 'deleted_at', 'password', 'remember_token', 'permissions', 'roles_raw', 'salary', 'ssn']; + + foreach ($routes as $route) { + if ($this->primaryMethod($route) !== 'GET') { + continue; + } + + $uri = $route->uri(); + foreach ([$this->parent, $this->studentUser] as $actor) { + $response = $this->requestAs($actor, 'GET', $uri); + $this->assertControlled($response, 'GET', $uri); + $this->assertNoServerError($response, 'data minimization at GET ' . $uri); + + if ($response->getStatusCode() >= 200 && $response->getStatusCode() < 300) { + foreach ($forbidden as $field) { + $this->assertStringNotContainsString('"' . $field . '"', $response->getContent(), 'Low-privilege response leaked ' . $field . ' at GET ' . $uri); + } + } + } + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiDataShapeBackwardCompatibilityContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiDataShapeBackwardCompatibilityContractTest.php new file mode 100644 index 00000000..02602378 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiDataShapeBackwardCompatibilityContractTest.php @@ -0,0 +1,61 @@ +apiRoutes(), fn ($route) => $this->primaryMethod($route) === 'GET' && ! str_contains($route->uri(), '{')), 0, 120); + + foreach ($routes as $route) { + $uri = $route->uri(); + $response = $this->actingAs($this->actorFor($uri), 'api')->getJson($this->materializePath($uri)); + + $this->assertControlled($response, 'GET', $uri); + $this->assertNoServerError($response, 'collection envelope ' . $uri); + + if ($response->getStatusCode() !== 200 || ! $this->isJsonString($response->getContent())) { + continue; + } + + $payload = $response->json(); + $this->assertTrue( + is_array($payload) && ( + array_is_list($payload) + || array_key_exists('data', $payload) + || array_key_exists('items', $payload) + || array_key_exists('results', $payload) + || array_key_exists('message', $payload) + || array_key_exists('status', $payload) + ), + $uri . ' should keep a predictable collection/read envelope.' + ); + } + } + + public function test_successful_detail_endpoints_keep_identifier_or_data_keys(): void + { + $routes = array_slice(array_filter($this->apiRoutes(), fn ($route) => $this->primaryMethod($route) === 'GET' && str_contains($route->uri(), '{')), 0, 100); + + foreach ($routes as $route) { + $uri = $route->uri(); + $response = $this->actingAs($this->actorFor($uri), 'api')->getJson($this->materializePath($uri)); + + $this->assertControlled($response, 'GET', $uri); + $this->assertNoServerError($response, 'detail envelope ' . $uri); + + if ($response->getStatusCode() !== 200 || ! $this->isJsonString($response->getContent())) { + continue; + } + + $payload = $response->json(); + $this->assertTrue( + data_get($payload, 'id') !== null || data_get($payload, 'data.id') !== null || data_get($payload, 'message') !== null || data_get($payload, 'status') !== null, + $uri . ' should keep an identifier, data wrapper, or explicit status/message contract.' + ); + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiDatabaseTransactionRollbackContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiDatabaseTransactionRollbackContractTest.php new file mode 100644 index 00000000..529d79e4 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiDatabaseTransactionRollbackContractTest.php @@ -0,0 +1,62 @@ +apiRoutesContainingAny(['students', 'users', 'finance', 'payments', 'inventory', 'reimbursements', 'families', 'guardians']); + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) { + continue; + } + + $uri = $route->uri(); + $beforeUsers = DB::table('users')->count(); + $beforeStudents = DB::table('students')->count(); + + $payload = $this->payloadFor($method, $uri) + [ + 'email' => 'rollback.' . uniqid() . '@example.test', + 'firstname' => 'Rollback', + 'lastname' => 'Probe', + 'parent_id' => 999999, + 'student_id' => 999999, + 'class_section_id' => 999999, + 'force_validation_failure' => true, + ]; + + $response = $this->requestAs($this->actorFor($uri), $method, $uri, $payload); + $this->assertNoServerError($response, "$method $uri rollback probe"); + + if (in_array($response->getStatusCode(), [400, 401, 403, 404, 409, 419, 422], true)) { + $this->assertLessThanOrEqual($beforeUsers + 1, DB::table('users')->count(), "$uri should not spray partial user rows on failed mutation."); + $this->assertLessThanOrEqual($beforeStudents + 1, DB::table('students')->count(), "$uri should not spray partial student rows on failed mutation."); + } + } + } + + public function test_delete_failures_do_not_remove_parent_or_student_fixtures(): void + { + $routes = $this->apiRoutesContainingAny(['delete', 'remove', 'withdraw', 'archive']); + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + if (! in_array($method, ['DELETE', 'POST', 'PATCH'], true)) { + continue; + } + + $uri = $route->uri(); + $response = $this->requestAs($this->actorFor($uri), $method, $uri, ['id' => 999999, 'ids' => [999999]]); + + $this->assertNoServerError($response, "$method $uri destructive rollback"); + $this->assertNotNull(DB::table('users')->where('id', $this->parent->id)->first(), "$uri must not delete seeded parent fixture during failed destructive action."); + $this->assertNotNull(DB::table('students')->where('id', $this->ids['studentId'])->first(), "$uri must not delete seeded student fixture during failed destructive action."); + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiDateMoneyAndTimezoneBoundaryContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiDateMoneyAndTimezoneBoundaryContractTest.php new file mode 100644 index 00000000..6ee4c025 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiDateMoneyAndTimezoneBoundaryContractTest.php @@ -0,0 +1,68 @@ +apiRoutesContainingAny(['attendance', 'finance', 'reports', 'school-years', 'calendar', 'payments', 'invoices']) as $route) { + $method = $this->primaryMethod($route); + + if (! in_array($method, ['GET', 'POST', 'PUT', 'PATCH'], true)) { + continue; + } + + foreach ($badDates as $badDate) { + $payload = $this->payloadFor($method, $route->uri()) + [ + 'date' => $badDate, + 'from' => $badDate, + 'to' => $badDate, + 'start_date' => $badDate, + 'end_date' => $badDate, + ]; + + $response = $this->requestAs($this->actorFor($route->uri()), $method, $route->uri(), $payload); + $this->assertStatusIn($response, [200, 204, 400, 403, 404, 409, 422], "{$method} {$route->uri()} bad date {$badDate}"); + $this->assertStringNotContainsString('SQLSTATE', $response->getContent()); + } + } + } + + public function test_money_accepting_endpoints_reject_negative_nan_and_overflow_amounts_cleanly(): void + { + $badAmounts = [-1, -0.01, 0, 'NaN', 'Infinity', '999999999999999999999999', '1e309']; + + foreach ($this->apiRoutesContainingAny(['finance', 'payments', 'refunds', 'invoices', 'fees', 'reimbursements', 'expenses']) as $route) { + $method = $this->primaryMethod($route); + + if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) { + continue; + } + + foreach ($badAmounts as $amount) { + $payload = $this->payloadFor($method, $route->uri()) + [ + 'amount' => $amount, + 'paid_amount' => $amount, + 'total' => $amount, + 'unit_cost' => $amount, + 'fee' => $amount, + ]; + + $response = $this->requestAs($this->actorFor($route->uri()), $method, $route->uri(), $payload); + $this->assertStatusIn($response, [400, 403, 404, 409, 422], "{$method} {$route->uri()} should reject bad amount."); + $this->assertStringNotContainsString('SQLSTATE', $response->getContent()); + } + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiDeepAuthorizationIsolationMatrixTest.php b/tests/Feature/Api/V1/FullSurface/ApiDeepAuthorizationIsolationMatrixTest.php new file mode 100644 index 00000000..de886ce9 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiDeepAuthorizationIsolationMatrixTest.php @@ -0,0 +1,70 @@ +isolationMatrix() as $label => [$actor, $method, $uri]) { + $response = $this->requestAs($actor, $method, $uri, $this->payloadFor($method, ltrim($uri, '/'))); + $status = $response->getStatusCode(); + + if (! in_array($status, [401, 403, 404, 405, 419, 422], true)) { + $failures[] = "$label allowed cross-portal access with $status: " . $response->getContent(); + } + } + + $this->assertSame([], $failures, "Cross-role authorization isolation failed:\n" . implode("\n", $failures)); + } + + public function test_owner_scoped_parent_routes_do_not_expose_another_family_student(): void + { + $otherWorld = $this->seedTeacherClassWithStudent(9702); + $foreignStudentId = (int) $otherWorld['student_id']; + + $checks = [ + ['GET', "/api/v1/parents/students/$foreignStudentId"], + ['PATCH', "/api/v1/parents/students/$foreignStudentId"], + ['GET', "/api/v1/parents/students/$foreignStudentId/attendance"], + ['GET', "/api/v1/parents/students/$foreignStudentId/invoices"], + ['POST', "/api/v1/parents/students/$foreignStudentId/emergency-contacts"], + ]; + + $failures = []; + foreach ($checks as [$method, $uri]) { + $response = $this->requestAs($this->parent, $method, $uri, $this->payloadFor($method, ltrim($uri, '/'))); + $status = $response->getStatusCode(); + + if (! in_array($status, [401, 403, 404, 405, 419, 422], true)) { + $failures[] = "$method $uri leaked cross-family access with $status: " . $response->getContent(); + } + } + + $this->assertSame([], $failures, "Parent owner-scope boundaries failed:\n" . implode("\n", $failures)); + } + + /** @return array */ + private function isolationMatrix(): array + { + return [ + 'parent cannot see admin metrics' => [$this->parent, 'GET', '/api/v1/administrator/dashboard/metrics'], + 'parent cannot manage school years' => [$this->parent, 'POST', '/api/v1/school-years'], + 'parent cannot close school year' => [$this->parent, 'POST', '/api/v1/school-years/{schoolYear}/close'], + 'parent cannot view finance report' => [$this->parent, 'GET', '/api/v1/finance/financial-report'], + 'parent cannot manage inventory' => [$this->parent, 'POST', '/api/v1/inventory/items'], + 'teacher cannot manage users' => [$this->teacher, 'POST', '/api/v1/users'], + 'teacher cannot manage role permissions' => [$this->teacher, 'POST', '/api/v1/roles/{roleId}/permissions'], + 'teacher cannot close school year' => [$this->teacher, 'POST', '/api/v1/school-years/{schoolYear}/close'], + 'student cannot access teacher classes' => [$this->studentUser, 'GET', '/api/v1/teachers/classes'], + 'student cannot access parent profile' => [$this->studentUser, 'GET', '/api/v1/parents/profile'], + 'student cannot access admin dashboard' => [$this->studentUser, 'GET', '/api/v1/administrator/dashboard/metrics'], + 'admin cannot mutate parent-scoped child profile as portal parent' => [$this->admin, 'PATCH', '/api/v1/parents/students/{studentId}'], + ]; + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiDeepDocumentationAndRouteParityContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiDeepDocumentationAndRouteParityContractTest.php new file mode 100644 index 00000000..c028cf76 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiDeepDocumentationAndRouteParityContractTest.php @@ -0,0 +1,76 @@ +apiRoutes() as $route) { + $uri = $route->uri(); + + if ($route->getName() !== null) { + continue; + } + + if ($this->isAllowedUnnamedRoute($uri)) { + continue; + } + + $unnamed[] = $this->primaryMethod($route) . ' ' . $uri; + } + + $this->assertSame([], $unnamed, "API routes should be nameable for docs, client generation, and redirects. Unnamed routes:\n" . implode("\n", $unnamed)); + } + + public function test_openapi_or_docs_catalog_endpoint_is_reachable_and_route_table_remains_documentable(): void + { + $docsChecks = [ + ['GET', '/api/documentation'], + ['GET', '/api/documentation/swagger'], + ['GET', '/api/documentation/swagger.json'], + ['GET', '/api/docs/public'], + ]; + + $failures = []; + foreach ($docsChecks as [$method, $uri]) { + $response = $this->requestAs($this->admin, $method, $uri); + + if ($response->getStatusCode() >= 500) { + $failures[] = "$method $uri docs endpoint crashed: " . $response->getContent(); + } + } + + $this->assertSame([], $failures, "Documentation endpoints must not be decorative rubble:\n" . implode("\n", $failures)); + } + + public function test_route_use_case_catalog_covers_new_deep_contract_domains(): void + { + $coverageNeedles = [ + 'import' => ['upload', 'import', 'attachment', 'document'], + 'export' => ['download', 'export', 'pdf', 'print'], + 'pagination' => ['students', 'users', 'invoices', 'inventory', 'messages'], + 'state_transition' => ['close', 'reopen', 'approve', 'archive', 'resolve', 'lock', 'unlock'], + 'identity' => ['auth', 'login', 'logout', 'me', 'roles'], + ]; + + foreach ($coverageNeedles as $label => $needles) { + $this->assertNotSame([], $this->apiRoutesContainingAny($needles), "The $label contract bucket should map to at least one route."); + } + } + + private function isAllowedUnnamedRoute(string $uri): bool + { + return str_contains($uri, 'api/v1') + || str_contains($uri, 'login') + || str_contains($uri, 'register') + || str_contains($uri, 'proofread') + || str_contains($uri, 'emails') + || str_contains($uri, 'compare'); + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiDeepFileImportExportAndPrintableContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiDeepFileImportExportAndPrintableContractTest.php new file mode 100644 index 00000000..9ca2d8b6 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiDeepFileImportExportAndPrintableContractTest.php @@ -0,0 +1,82 @@ +downloadLikeRoutes() as $route) { + $method = $this->primaryMethod($route); + $uri = $route->uri(); + $response = $this->probeRoute($route); + $status = $response->getStatusCode(); + + if ($status >= 500) { + $failures[] = "$method $uri crashed when generating downloadable/printable output: " . $response->getContent(); + continue; + } + + if (! in_array($status, [200, 202, 204, 302, 400, 401, 403, 404, 405, 409, 419, 422], true)) { + $failures[] = "$method $uri returned unexpected $status for downloadable/printable output: " . $response->getContent(); + } + } + + $this->assertSame([], $failures, "Download/export/print routes must fail cleanly when fixtures are thin:\n" . implode("\n", $failures)); + } + + public function test_import_upload_routes_accept_test_files_or_return_validation_without_server_errors(): void + { + Storage::fake('local'); + $failures = []; + + foreach ($this->uploadLikeRoutes() as $route) { + $method = $this->primaryMethod($route); + $uri = $route->uri(); + $payload = $this->payloadFor($method, $uri) + [ + 'file' => UploadedFile::fake()->create('e2e-import.csv', 2, 'text/csv'), + 'document' => UploadedFile::fake()->create('e2e-document.pdf', 2, 'application/pdf'), + ]; + + $response = $this->requestAs($this->actorFor($uri), $method, $uri, $payload); + $status = $response->getStatusCode(); + + if ($status >= 500) { + $failures[] = "$method $uri crashed on upload/import probe: " . $response->getContent(); + } + } + + $this->assertSame([], $failures, "Upload/import routes must validate file input, not panic:\n" . implode("\n", $failures)); + } + + /** @return list */ + private function downloadLikeRoutes(): array + { + return $this->apiRoutesContainingAny(['download', 'export', 'pdf', 'print', 'certificate', 'badge', 'receipt']); + } + + /** @return list */ + private function uploadLikeRoutes(): array + { + return array_values(array_filter($this->apiRoutes(), function (LaravelRoute $route): bool { + if (! in_array($this->primaryMethod($route), ['POST', 'PUT', 'PATCH'], true)) { + return false; + } + + $uri = $route->uri(); + + return str_contains($uri, 'upload') + || str_contains($uri, 'import') + || str_contains($uri, 'scan') + || str_contains($uri, 'document') + || str_contains($uri, 'attachment'); + })); + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiDeepIdempotencyAndDuplicateSubmissionContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiDeepIdempotencyAndDuplicateSubmissionContractTest.php new file mode 100644 index 00000000..601786a2 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiDeepIdempotencyAndDuplicateSubmissionContractTest.php @@ -0,0 +1,95 @@ +duplicateSensitiveRoutes() as $route) { + $method = $this->primaryMethod($route); + $uri = $route->uri(); + $payload = $this->payloadFor($method, $uri) + [ + 'external_reference' => 'E2E-DUP-' . md5($uri), + 'idempotency_key' => 'E2E-DUP-' . md5($uri), + ]; + + $first = $this->requestAs($this->actorFor($uri), $method, $uri, $payload); + $second = $this->requestAs($this->actorFor($uri), $method, $uri, $payload); + + foreach (['first' => $first, 'second' => $second] as $label => $response) { + $status = $response->getStatusCode(); + + if ($status >= 500) { + $failures[] = "$method $uri $label duplicate request crashed with $status: " . $response->getContent(); + } + + if (! in_array($status, [200, 201, 202, 204, 302, 400, 401, 403, 404, 405, 409, 419, 422], true)) { + $failures[] = "$method $uri $label duplicate request returned unexpected $status: " . $response->getContent(); + } + } + } + + $this->assertSame([], $failures, "Duplicate submissions need controlled behavior, because users double-click like it's a sport:\n" . implode("\n", $failures)); + } + + public function test_repeat_delete_or_archive_requests_are_safe_to_retry_or_fail_cleanly(): void + { + $failures = []; + + foreach ($this->retryableDestructiveRoutes() as $route) { + $uri = $route->uri(); + $first = $this->requestAs($this->actorFor($uri), 'DELETE', $uri); + $second = $this->requestAs($this->actorFor($uri), 'DELETE', $uri); + + foreach (['first' => $first, 'second' => $second] as $label => $response) { + if ($response->getStatusCode() >= 500) { + $failures[] = "DELETE $uri $label retry crashed: " . $response->getContent(); + } + } + } + + $this->assertSame([], $failures, "Retrying destructive operations must not crash:\n" . implode("\n", $failures)); + } + + /** @return list */ + private function duplicateSensitiveRoutes(): array + { + return array_slice(array_values(array_filter($this->apiRoutes(), function (LaravelRoute $route): bool { + if ($this->primaryMethod($route) !== 'POST') { + return false; + } + + $uri = $route->uri(); + + return str_contains($uri, 'payments') + || str_contains($uri, 'refund') + || str_contains($uri, 'invoices') + || str_contains($uri, 'attendance') + || str_contains($uri, 'messages') + || str_contains($uri, 'support') + || str_contains($uri, 'students') + || str_contains($uri, 'users') + || str_contains($uri, 'inventory') + || str_contains($uri, 'reimbursements'); + })), 0, 80); + } + + /** @return list */ + private function retryableDestructiveRoutes(): array + { + return array_slice(array_values(array_filter($this->apiRoutes(), function (LaravelRoute $route): bool { + $uri = $route->uri(); + + return $this->primaryMethod($route) === 'DELETE' + || str_contains($uri, 'archive') + || str_contains($uri, 'remove') + || str_contains($uri, 'delete'); + })), 0, 50); + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiDeepPaginationSearchAndFilterContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiDeepPaginationSearchAndFilterContractTest.php new file mode 100644 index 00000000..8b2b6096 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiDeepPaginationSearchAndFilterContractTest.php @@ -0,0 +1,98 @@ +listRoutes() as $route) { + $method = $this->primaryMethod($route); + $uri = $route->uri(); + $path = $this->materializePath($uri) . '?' . http_build_query([ + 'page' => 1, + 'per_page' => 10, + 'search' => 'e2e', + 'sort' => 'created_at', + 'direction' => 'desc', + 'school_year' => self::E2E_SCHOOL_YEAR, + 'semester' => self::E2E_SEMESTER, + 'status' => 'active', + ]); + + $this->actingAs($this->actorFor($uri), 'api'); + $response = $this->json($method, $path); + $status = $response->getStatusCode(); + + if ($status >= 500) { + $failures[] = "$method $uri crashed with list filters: " . $response->getContent(); + continue; + } + + if (! in_array($status, [200, 204, 302, 400, 401, 403, 404, 405, 409, 422], true)) { + $failures[] = "$method $uri returned unexpected $status with list filters: " . $response->getContent(); + } + } + + $this->assertSame([], $failures, "List endpoints must not crash when clients send boring grid params:\n" . implode("\n", $failures)); + } + + public function test_date_range_filters_fail_cleanly_when_dates_are_invalid(): void + { + $failures = []; + + foreach ($this->dateFilterRoutes() as $route) { + $uri = $route->uri(); + $path = $this->materializePath($uri) . '?' . http_build_query([ + 'from' => 'not-a-date', + 'to' => 'still-not-a-date', + 'start_date' => '2025-99-99', + 'end_date' => 'yesterday-ish', + ]); + + $this->actingAs($this->actorFor($uri), 'api'); + $response = $this->json('GET', $path); + + if ($response->getStatusCode() >= 500) { + $failures[] = "GET $uri crashed on invalid date filters: " . $response->getContent(); + } + } + + $this->assertSame([], $failures, "Invalid date filters should be validation, not detonation:\n" . implode("\n", $failures)); + } + + /** @return list */ + private function listRoutes(): array + { + return array_values(array_filter($this->apiRoutes(), function (LaravelRoute $route): bool { + $uri = $route->uri(); + + return $this->primaryMethod($route) === 'GET' + && ! preg_match('#/\{[^}]+\}$#', $uri) + && ! str_contains($uri, 'download') + && ! str_contains($uri, 'export') + && ! str_contains($uri, 'pdf') + && ! str_contains($uri, 'swagger'); + })); + } + + /** @return list */ + private function dateFilterRoutes(): array + { + return array_values(array_filter($this->listRoutes(), function (LaravelRoute $route): bool { + $uri = $route->uri(); + + return str_contains($uri, 'attendance') + || str_contains($uri, 'finance') + || str_contains($uri, 'reports') + || str_contains($uri, 'payments') + || str_contains($uri, 'invoices') + || str_contains($uri, 'school-years'); + })); + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiDeepResponseShapeAndEnvelopeContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiDeepResponseShapeAndEnvelopeContractTest.php new file mode 100644 index 00000000..4256e069 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiDeepResponseShapeAndEnvelopeContractTest.php @@ -0,0 +1,99 @@ +representativeDomainRoutes() as $route) { + $method = $this->primaryMethod($route); + $uri = $route->uri(); + $response = $this->probeRoute($route); + $status = $response->getStatusCode(); + + if ($status >= 500) { + $failures[] = "$method $uri crashed with $status: " . $response->getContent(); + continue; + } + + if ($status >= 200 && $status < 300 && $response->getContent() !== '' && ! $this->isJsonString($response->getContent())) { + $failures[] = "$method $uri returned non-json successful API response: " . $response->getContent(); + } + } + + $this->assertSame([], $failures, "Successful API responses should be machine-readable JSON:\n" . implode("\n", $failures)); + } + + public function test_collection_endpoints_expose_list_like_payloads_or_explicit_empty_state(): void + { + $failures = []; + + foreach ($this->collectionLikeRoutes() as $route) { + $method = $this->primaryMethod($route); + $uri = $route->uri(); + + if ($method !== 'GET') { + continue; + } + + $response = $this->probeRoute($route); + $status = $response->getStatusCode(); + + if (! in_array($status, [200, 204, 302, 401, 403, 404, 422], true)) { + $failures[] = "$method $uri returned unexpected $status for collection contract: " . $response->getContent(); + continue; + } + + if ($status === 200 && $this->isJsonString($response->getContent())) { + $payload = $response->json(); + $hasCollectionShape = is_array($payload) + && (array_is_list($payload) + || array_key_exists('data', $payload) + || array_key_exists('items', $payload) + || array_key_exists('results', $payload) + || array_key_exists('rows', $payload) + || array_key_exists('ok', $payload) + || array_key_exists('status', $payload)); + + if (! $hasCollectionShape) { + $failures[] = "$method $uri returned JSON but no recognizable collection/envelope keys: " . $response->getContent(); + } + } + } + + $this->assertSame([], $failures, "Collection routes need a stable list/envelope shape:\n" . implode("\n", $failures)); + } + + /** @return list */ + private function representativeDomainRoutes(): array + { + return array_slice($this->apiRoutesContainingAny([ + 'auth', 'administrator', 'teachers', 'parents', 'students', 'attendance', 'scores', + 'finance', 'inventory', 'messages', 'support', 'settings', 'school-years', 'families', + ]), 0, 120); + } + + /** @return list */ + private function collectionLikeRoutes(): array + { + return array_values(array_filter($this->apiRoutes(), function (LaravelRoute $route): bool { + if ($this->primaryMethod($route) !== 'GET') { + return false; + } + + $uri = $route->uri(); + + return ! preg_match('#/\{[^}]+\}$#', $uri) + && ! str_contains($uri, 'download') + && ! str_contains($uri, 'export') + && ! str_contains($uri, 'print') + && ! str_contains($uri, 'pdf'); + })); + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiDeepStateTransitionInvariantContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiDeepStateTransitionInvariantContractTest.php new file mode 100644 index 00000000..efe19437 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiDeepStateTransitionInvariantContractTest.php @@ -0,0 +1,77 @@ +ids['schoolYear']; + $checks = [ + ['POST', "/api/v1/school-years/$schoolYearId/validate-close", []], + ['POST', "/api/v1/school-years/$schoolYearId/preview-close", []], + ['POST', "/api/v1/school-years/$schoolYearId/close", ['confirm' => true]], + ['POST', "/api/v1/school-years/$schoolYearId/reopen", ['reason' => 'E2E invariant probe']], + ['GET', '/api/v1/administrator/promotions'], + ['POST', '/api/v1/administrator/promotions/preview', ['school_year' => self::E2E_SCHOOL_YEAR]], + ]; + + $this->assertTransitionChecksAreControlled($checks, 'school year and promotion'); + } + + public function test_finance_transition_routes_do_not_allow_negative_balance_crashes_or_untracked_state(): void + { + $invoiceId = (int) $this->ids['invoiceId']; + $checks = [ + ['POST', '/api/v1/finance/payments', ['invoice_id' => $invoiceId, 'amount' => -100, 'payment_method' => 'cash']], + ['POST', '/api/v1/finance/payments', ['invoice_id' => $invoiceId, 'amount' => 25, 'payment_method' => 'cash']], + ['POST', '/api/v1/finance/refunds', ['invoice_id' => $invoiceId, 'amount' => 999999, 'reason' => 'Over-refund probe']], + ['POST', '/api/v1/finance/installments', ['invoice_id' => $invoiceId, 'amount' => 25, 'due_date' => '2025-11-01']], + ['GET', '/api/v1/finance/payment-followups'], + ]; + + $this->assertTransitionChecksAreControlled($checks, 'finance'); + + $invoice = DB::table('invoices')->where('id', $invoiceId)->first(); + $this->assertNotNull($invoice, 'Finance probes must not delete the invoice fixture just because math got spicy.'); + } + + public function test_attendance_transition_routes_keep_single_day_student_context_controlled(): void + { + $studentId = (int) $this->ids['studentId']; + $classSectionId = (int) $this->ids['classSectionId']; + $checks = [ + ['POST', '/api/v1/teacher/attendance', ['student_id' => $studentId, 'class_section_id' => $classSectionId, 'date' => '2025-10-05', 'status' => 'present']], + ['POST', '/api/v1/teacher/attendance', ['student_id' => $studentId, 'class_section_id' => $classSectionId, 'date' => '2025-10-05', 'status' => 'absent']], + ['POST', '/api/v1/attendance/late-slips', ['student_id' => $studentId, 'class_section_id' => $classSectionId, 'date' => '2025-10-05', 'arrival_time' => '10:45']], + ['POST', '/api/v1/attendance/early-dismissals', ['student_id' => $studentId, 'class_section_id' => $classSectionId, 'date' => '2025-10-05', 'dismissal_time' => '12:30']], + ]; + + $this->assertTransitionChecksAreControlled($checks, 'attendance'); + } + + /** @param list}> $checks */ + private function assertTransitionChecksAreControlled(array $checks, string $domain): void + { + $failures = []; + + foreach ($checks as [$method, $uri, $payload]) { + $payload ??= $this->payloadFor($method, ltrim($uri, '/')); + $response = $this->requestAs($this->actorFor($uri), $method, $uri, $payload); + + if ($response->getStatusCode() >= 500) { + $failures[] = "$method $uri crashed during $domain transition probe: " . $response->getContent(); + continue; + } + + if (! in_array($response->getStatusCode(), $this->controlledStatuses($method), true)) { + $failures[] = "$method $uri returned unexpected {$response->getStatusCode()} during $domain transition probe: " . $response->getContent(); + } + } + + $this->assertSame([], $failures, ucfirst($domain) . " state transitions must be controlled:\n" . implode("\n", $failures)); + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiDeepValidationFailureContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiDeepValidationFailureContractTest.php new file mode 100644 index 00000000..70529b1d --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiDeepValidationFailureContractTest.php @@ -0,0 +1,98 @@ +mutatingRoutes() as $route) { + $method = $this->primaryMethod($route); + $uri = $route->uri(); + + foreach ($this->hostilePayloads() as $label => $payload) { + $response = $this->requestAs($this->actorFor($uri), $method, $uri, $payload); + $status = $response->getStatusCode(); + + if ($status >= 500) { + $failures[] = "$method $uri crashed on $label hostile payload with $status: " . $response->getContent(); + continue; + } + + if (! in_array($status, [200, 201, 202, 204, 302, 400, 401, 403, 404, 405, 409, 419, 422], true)) { + $failures[] = "$method $uri returned unexpected $status on $label hostile payload: " . $response->getContent(); + } + } + } + + $this->assertSame([], $failures, "Hostile payloads must fail cleanly, not leak framework explosions:\n" . implode("\n", $failures)); + } + + public function test_required_field_validation_uses_machine_readable_error_shape_when_validation_fails(): void + { + $failures = []; + + foreach ($this->validationSensitiveRoutes() as $route) { + $method = $this->primaryMethod($route); + $uri = $route->uri(); + $response = $this->requestAs($this->actorFor($uri), $method, $uri, []); + + if ($response->getStatusCode() !== 422) { + continue; + } + + if (! $this->isJsonString($response->getContent())) { + $failures[] = "$method $uri returned 422 but not JSON: " . $response->getContent(); + continue; + } + + $json = $response->json(); + if (! is_array($json) || (! array_key_exists('errors', $json) && ! array_key_exists('message', $json))) { + $failures[] = "$method $uri returned 422 without message/errors: " . $response->getContent(); + } + } + + $this->assertSame([], $failures, "Validation errors must be usable by clients, not poetic prose:\n" . implode("\n", $failures)); + } + + /** @return list */ + private function mutatingRoutes(): array + { + return array_slice(array_values(array_filter($this->apiRoutes(), function (LaravelRoute $route): bool { + return in_array($this->primaryMethod($route), ['POST', 'PUT', 'PATCH', 'DELETE'], true) + && ! str_contains($route->uri(), 'auth/logout'); + })), 0, 150); + } + + /** @return list */ + private function validationSensitiveRoutes(): array + { + return array_values(array_filter($this->mutatingRoutes(), function (LaravelRoute $route): bool { + $uri = $route->uri(); + + return str_contains($uri, 'students') + || str_contains($uri, 'users') + || str_contains($uri, 'attendance') + || str_contains($uri, 'finance') + || str_contains($uri, 'scores') + || str_contains($uri, 'support') + || str_contains($uri, 'inventory'); + })); + } + + /** @return array> */ + private function hostilePayloads(): array + { + return [ + 'empty' => [], + 'wrong_scalar_types' => ['id' => 'not-an-int', 'amount' => 'NaN', 'date' => 'not-a-date', 'email' => 'not-email'], + 'unexpected_nested_arrays' => ['name' => ['nested' => ['too' => ['deep' => true]]], 'student_id' => ['x']], + 'oversized_strings' => ['name' => str_repeat('x', 1024), 'description' => str_repeat('y', 2048)], + ]; + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiDestructiveActionSafeguardContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiDestructiveActionSafeguardContractTest.php new file mode 100644 index 00000000..185c5995 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiDestructiveActionSafeguardContractTest.php @@ -0,0 +1,87 @@ +deleteRoutes() as $route) { + $uri = $this->replaceAllPlaceholdersWithMissingId($route->uri()); + $response = $this->requestAs($this->actorFor($route->uri()), 'DELETE', $uri); + + $this->assertStatusIn($response, [200, 202, 204, 302, 400, 401, 403, 404, 405, 409, 419, 422], "DELETE missing $uri"); + $this->assertNoServerError($response, "DELETE missing $uri"); + } + } + + public function test_bulk_delete_style_routes_require_explicit_targets(): void + { + foreach ($this->bulkDestructiveRoutes() as $route) { + $method = $this->primaryMethod($route); + $uri = $route->uri(); + $response = $this->requestAs($this->actorFor($uri), $method, $uri, [ + 'ids' => [], + 'confirm' => false, + ]); + + $this->assertStatusIn($response, [200, 202, 204, 302, 400, 401, 403, 404, 405, 409, 419, 422], "$method $uri empty bulk destructive payload"); + $this->assertNoServerError($response, "$method $uri empty bulk destructive payload"); + + if ($response->getStatusCode() >= 200 && $response->getStatusCode() < 300) { + $this->assertTrue( + in_array($response->getStatusCode(), [200, 202, 204], true), + "$method $uri may succeed only as an explicit no-op, never by deleting an unspecified set." + ); + } + } + } + + public function test_archive_remove_cancel_and_void_routes_are_idempotent_or_conflict_safe(): void + { + foreach ($this->softDestructiveRoutes() as $route) { + $method = $this->primaryMethod($route); + $uri = $route->uri(); + $first = $this->requestAs($this->actorFor($uri), $method, $uri, $this->payloadFor($method, $uri)); + $second = $this->requestAs($this->actorFor($uri), $method, $uri, $this->payloadFor($method, $uri)); + + $this->assertControlled($first, $method, $uri); + $this->assertControlled($second, $method, $uri); + $this->assertNoServerError($first, "$method $uri first soft destructive attempt"); + $this->assertNoServerError($second, "$method $uri second soft destructive attempt"); + } + } + + /** @return list */ + private function deleteRoutes(): array + { + return array_slice(array_filter($this->apiRoutes(), fn (LaravelRoute $route): bool => $this->primaryMethod($route) === 'DELETE'), 0, 80); + } + + /** @return list */ + private function bulkDestructiveRoutes(): array + { + return array_slice(array_filter($this->apiRoutes(), function (LaravelRoute $route): bool { + $uri = $route->uri(); + + return in_array($this->primaryMethod($route), ['POST', 'DELETE'], true) + && preg_match('/bulk|batch|mass|selected|purge|delete/i', $uri) === 1; + }), 0, 60); + } + + /** @return list */ + private function softDestructiveRoutes(): array + { + return array_slice(array_filter($this->apiRoutes(), function (LaravelRoute $route): bool { + return preg_match('/archive|remove|cancel|void|resolve|close|reopen|restore/i', $route->uri()) === 1; + }), 0, 80); + } + + private function replaceAllPlaceholdersWithMissingId(string $uri): string + { + return preg_replace('/\{[^}]+\}/', '999999999', $uri) ?? $uri; + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiEmptyDatasetAndNullStateContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiEmptyDatasetAndNullStateContractTest.php new file mode 100644 index 00000000..f1d57e99 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiEmptyDatasetAndNullStateContractTest.php @@ -0,0 +1,92 @@ +listPaths() as $path) { + $response = $this->requestAs($this->actorFor($path), 'GET', $path . '?search=__no_match_contract_probe__&page=1&per_page=5'); + + $this->assertStatusIn($response, [200, 204, 302, 400, 401, 403, 404, 405, 419, 422], "empty list $path"); + $this->assertNoServerError($response, "empty list $path"); + + if ($response->getStatusCode() === 200 && $this->isJsonString($response->getContent())) { + $json = $response->json(); + $this->assertTrue( + is_array($json) || $json === null, + "$path empty list response should remain array/envelope/null, not scalar junk." + ); + } + } + } + + public function test_current_school_year_null_state_is_handled_without_crash(): void + { + DB::table('school_years')->update(['is_current' => false]); + + foreach ($this->schoolContextPaths() as $path) { + $response = $this->requestAs($this->admin, 'GET', $path); + + $this->assertStatusIn($response, [200, 204, 302, 400, 401, 403, 404, 405, 409, 419, 422], "null school year $path"); + $this->assertNoServerError($response, "null school year $path"); + } + } + + public function test_empty_class_roster_and_attendance_context_are_controlled(): void + { + $emptyClassSectionId = $this->seedClassSection(9911, 'Empty Contract Class', 99); + $this->ids['classSectionId'] = $emptyClassSectionId; + $this->ids['class_section_id'] = $emptyClassSectionId; + + foreach ($this->emptyClassPaths($emptyClassSectionId) as $path) { + $response = $this->requestAs($this->admin, 'GET', $path); + + $this->assertStatusIn($response, [200, 204, 302, 400, 401, 403, 404, 405, 409, 419, 422], "empty class $path"); + $this->assertNoServerError($response, "empty class $path"); + } + } + + /** @return list */ + private function listPaths(): array + { + return [ + 'api/v1/users', + 'api/v1/students', + 'api/v1/classes', + 'api/v1/attendance', + 'api/v1/finance/invoices', + 'api/v1/inventory/items', + 'api/v1/messages', + 'api/v1/support', + ]; + } + + /** @return list */ + private function schoolContextPaths(): array + { + return [ + 'api/v1/system/semester', + 'api/v1/school-years/current', + 'api/v1/classes', + 'api/v1/attendance', + 'api/v1/report-cards', + 'api/v1/finance/reports', + ]; + } + + /** @return list */ + private function emptyClassPaths(int $classSectionId): array + { + return [ + "api/v1/classes/$classSectionId/students", + "api/v1/teacher/classes/$classSectionId/roster", + "api/v1/attendance/class/$classSectionId", + "api/v1/class-progress/$classSectionId", + ]; + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiEnumAndStateMachineContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiEnumAndStateMachineContractTest.php new file mode 100644 index 00000000..49ddfce5 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiEnumAndStateMachineContractTest.php @@ -0,0 +1,44 @@ +apiRoutesContainingAny(['attendance', 'invoice', 'payment', 'refund', 'support', 'message', 'inventory', 'promotion', 'school-year']); + $states = ['approved_by_accident', 'null', '', 'DROP TABLE', 'deleted-but-visible', 'paid_and_refunded']; + + foreach (array_slice($routes, 0, 120) as $route) { + $method = $this->primaryMethod($route); + if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) { + continue; + } + + foreach ($states as $state) { + $uri = $route->uri(); + $response = $this->requestAs($this->actorFor($uri), $method, $uri, $this->payloadFor($method, $uri) + ['status' => $state, 'state' => $state]); + + $this->assertControlled($response, $method, $uri); + $this->assertNoServerError($response, 'unknown status ' . $state . ' at ' . $method . ' ' . $uri); + } + } + } + + public function test_terminal_states_are_not_reopened_by_low_privilege_users(): void + { + $routes = $this->apiRoutesContainingAny(['reopen', 'restore', 'unarchive', 'cancel', 'void', 'resolve']); + + foreach (array_slice($routes, 0, 80) as $route) { + $method = $this->primaryMethod($route); + $uri = $route->uri(); + + $response = $this->requestAs($this->parent, $method, $uri, $this->payloadFor($method, $uri)); + + $this->assertStatusIn($response, [400, 401, 403, 404, 405, 409, 419, 422], 'low privilege terminal state transition ' . $method . ' ' . $uri); + $this->assertNoServerError($response, 'low privilege terminal state transition ' . $method . ' ' . $uri); + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiErrorEnvelopeConsistencyContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiErrorEnvelopeConsistencyContractTest.php new file mode 100644 index 00000000..71104655 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiErrorEnvelopeConsistencyContractTest.php @@ -0,0 +1,89 @@ +unknownApiPaths() as $path) { + $response = $this->requestAs($this->admin, 'GET', $path); + + $this->assertStatusIn($response, [302, 401, 403, 404, 405, 419, 422], "GET $path"); + $this->assertNoServerError($response, "GET $path"); + + if ($response->getStatusCode() === 404 && $response->getContent() !== '') { + $this->assertTrue($this->isJsonString($response->getContent()) || str_contains($response->getContent(), 'routesWorthValidationProbing() as $route) { + $method = $this->primaryMethod($route); + $uri = $route->uri(); + $response = $this->requestAs($this->actorFor($uri), $method, $uri, ['__invalid_contract_probe' => ['nested' => ['bad' => true]]]); + + $this->assertControlled($response, $method, $uri); + $this->assertNoServerError($response, "$method $uri invalid contract payload"); + + if ($response->getStatusCode() === 422 && $this->isJsonString($response->getContent())) { + $json = $response->json(); + $this->assertTrue( + array_key_exists('message', $json) || array_key_exists('errors', $json) || array_key_exists('error', $json), + "$method $uri validation response must expose message, error, or errors." + ); + } + } + } + + public function test_authorization_error_envelopes_do_not_leak_stack_traces(): void + { + foreach ($this->adminOnlyReadRoutes() as $uri) { + $response = $this->requestAs($this->parent, 'GET', $uri); + $body = $response->getContent(); + + $this->assertStatusIn($response, [302, 401, 403, 404, 405, 419], "parent denied $uri"); + $this->assertStringNotContainsString('Stack trace', $body, "$uri leaked a stack trace."); + $this->assertStringNotContainsString('vendor/laravel', $body, "$uri leaked framework internals."); + $this->assertStringNotContainsString('SQLSTATE', $body, "$uri leaked SQL details."); + } + } + + /** @return list */ + private function unknownApiPaths(): array + { + return [ + '/api/v1/__missing_contract_probe__', + '/api/v1/admin/__missing_contract_probe__', + '/api/v1/parents/__missing_contract_probe__', + '/api/v1/teacher/__missing_contract_probe__', + '/api/v1/finance/__missing_contract_probe__', + ]; + } + + /** @return list */ + private function routesWorthValidationProbing(): array + { + return array_slice(array_filter($this->apiRoutes(), function (LaravelRoute $route): bool { + return in_array($this->primaryMethod($route), ['POST', 'PUT', 'PATCH'], true); + }), 0, 80); + } + + /** @return list */ + private function adminOnlyReadRoutes(): array + { + return [ + 'api/v1/users', + 'api/v1/roles', + 'api/v1/finance/reports', + 'api/v1/inventory/items', + 'api/v1/school-years', + 'api/v1/admin/dashboard', + ]; + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiEtagConditionalMutationContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiEtagConditionalMutationContractTest.php new file mode 100644 index 00000000..4e38533b --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiEtagConditionalMutationContractTest.php @@ -0,0 +1,28 @@ +apiRoutes(), fn ($route) => in_array($this->primaryMethod($route), ['PUT', 'PATCH', 'DELETE'], true)), 0, 120); + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + $uri = $route->uri(); + + $this->actingAs($this->actorFor($uri), 'api'); + $response = $this->withHeaders([ + 'If-Match' => '"definitely-stale-e2e-etag"', + 'If-Unmodified-Since' => 'Mon, 01 Jan 1990 00:00:00 GMT', + 'Accept' => 'application/json', + ])->json($method, $this->materializePath($uri), $this->payloadFor($method, $uri)); + + $this->assertControlled($response, $method, $uri); + $this->assertNoServerError($response, 'stale conditional mutation at ' . $method . ' ' . $uri); + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiFailureModeResilienceExpansionTest.php b/tests/Feature/Api/V1/FullSurface/ApiFailureModeResilienceExpansionTest.php new file mode 100644 index 00000000..7b8679ab --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiFailureModeResilienceExpansionTest.php @@ -0,0 +1,51 @@ +apiRoutes(), fn ($route) => in_array($this->primaryMethod($route), ['POST', 'PUT', 'PATCH'], true)), 0, 120); + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + $uri = $route->uri(); + $payload = $this->payloadFor($method, $uri) + [ + 'student_id' => null, + 'parent_id' => null, + 'class_section_id' => null, + 'school_year_id' => null, + 'invoice_id' => null, + ]; + + $response = $this->requestAs($this->actorFor($uri), $method, $uri, $payload); + + $this->assertControlled($response, $method, $uri); + $this->assertNoServerError($response, 'null relationship probe for ' . $method . ' ' . $uri); + } + } + + public function test_mutation_routes_fail_cleanly_when_relationships_are_strings_instead_of_ids(): void + { + $routes = array_slice(array_filter($this->apiRoutes(), fn ($route) => in_array($this->primaryMethod($route), ['POST', 'PUT', 'PATCH'], true)), 0, 120); + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + $uri = $route->uri(); + $payload = $this->payloadFor($method, $uri) + [ + 'student_id' => 'student-' . $this->ids['studentId'], + 'parent_id' => 'parent-' . $this->ids['parentId'], + 'class_section_id' => 'class-' . $this->ids['classSectionId'], + 'invoice_id' => 'invoice-' . $this->ids['invoiceId'], + ]; + + $response = $this->requestAs($this->actorFor($uri), $method, $uri, $payload); + + $this->assertControlled($response, $method, $uri); + $this->assertNoServerError($response, 'string relationship probe for ' . $method . ' ' . $uri); + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiFamilyPortalFullSurfaceContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiFamilyPortalFullSurfaceContractTest.php new file mode 100644 index 00000000..b0c4c74c --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiFamilyPortalFullSurfaceContractTest.php @@ -0,0 +1,98 @@ +seedE2EConfiguration(); + $world = $this->seedTeacherClassWithStudent(9501); + $otherParent = $this->createApiUserWithRole('parent'); + $this->actingAs($world['parent'], 'api'); + + foreach ($this->parentEndpoints($world) as [$method, $uri, $payload]) { + $response = $this->json($method, $uri, $payload); + $this->assertNoServerError($response, 'Parent ' . $method . ' ' . $uri); + $this->assertStatusIn($response, [200, 201, 202, 204, 302, 400, 401, 403, 404, 405, 409, 419, 422], 'Parent ' . $method . ' ' . $uri); + } + + $this->actingAs($otherParent, 'api'); + foreach ($this->wrongFamilyBoundaryEndpoints($world) as [$method, $uri, $payload]) { + $response = $this->json($method, $uri, $payload); + $this->assertNoServerError($response, 'Wrong family ' . $method . ' ' . $uri); + $this->assertStatusIn($response, [401, 403, 404, 405, 409, 419, 422], 'Wrong family ' . $method . ' ' . $uri); + } + } + + /** @param array $world @return list}> */ + private function parentEndpoints(array $world): array + { + $studentId = (int) $world['student_id']; + $parentId = (int) $world['parent_id']; + + return [ + ['GET', '/api/v1/parents/profile', []], + ['PATCH', '/api/v1/parents/profile', ['firstname' => 'Parent', 'lastname' => 'Updated']], + ['GET', '/api/v1/parents/students', []], + ['GET', '/api/v1/parents/students/' . $studentId, []], + ['PATCH', '/api/v1/parents/students/' . $studentId, ['firstname' => 'Kai', 'lastname' => 'Updated', 'dob' => '2014-09-01', 'gender' => 'Male']], + ['GET', '/api/v1/parents/attendance', []], + ['GET', '/api/v1/parents/attendance/student/' . $studentId, []], + ['GET', '/api/v1/parents/invoices', []], + ['GET', '/api/v1/parents/enrollments', []], + ['GET', '/api/v1/parents/promotions', []], + ['GET', '/api/v1/parents/promotions/1', []], + ['PATCH', '/api/v1/parents/promotions/1/enrollment-steps', ['steps' => ['confirmed' => true]]], + ['POST', '/api/v1/parents/promotions/1/decline', ['reason' => 'Contract probe']], + ['GET', '/api/v1/emergency-contacts', []], + ['POST', '/api/v1/emergency-contacts', ['student_id' => $studentId, 'name' => 'Emergency Probe', 'phone' => '5551234567', 'relation' => 'Aunt']], + ['GET', '/api/v1/emergency-contacts/1', []], + ['PATCH', '/api/v1/emergency-contacts/1', ['name' => 'Emergency Updated', 'phone' => '5557654321']], + ['DELETE', '/api/v1/emergency-contacts/1', []], + ['GET', '/api/v1/family-admin/card?student_id=' . $studentId, []], + ['POST', '/api/v1/family-admin/compose-email', ['parent_id' => $parentId, 'subject' => 'Contract probe', 'message' => 'Body']], + ['GET', '/api/v1/families/by-student/' . $studentId, []], + ['GET', '/api/v1/families/1/guardians', []], + ['POST', '/api/v1/families/bootstrap', ['student_id' => $studentId, 'parent_id' => $parentId]], + ['POST', '/api/v1/families/attach-second/by-user', ['student_id' => $studentId, 'user_id' => $parentId]], + ['POST', '/api/v1/families/attach-second/by-email', ['student_id' => $studentId, 'email' => 'second.guardian@example.test']], + ['POST', '/api/v1/families/set-primary-home', ['student_id' => $studentId, 'parent_id' => $parentId]], + ['POST', '/api/v1/families/set-guardian-flags', ['student_id' => $studentId, 'parent_id' => $parentId, 'can_pickup' => true]], + ['POST', '/api/v1/families/unlink-guardian', ['student_id' => $studentId, 'parent_id' => $parentId]], + ['POST', '/api/v1/families/unlink-student', ['student_id' => $studentId, 'parent_id' => $parentId]], + ['POST', '/api/v1/families/import-legacy-second-parents', ['dry_run' => true]], + ['GET', '/api/v1/authorized-users', []], + ['POST', '/api/v1/authorized-users', ['student_id' => $studentId, 'firstname' => 'Authorized', 'lastname' => 'Probe', 'email' => 'authorized@example.test']], + ['GET', '/api/v1/authorized-users/1', []], + ['PATCH', '/api/v1/authorized-users/1', ['firstname' => 'Authorized Updated']], + ['DELETE', '/api/v1/authorized-users/1', []], + ]; + } + + /** @param array $world @return list}> */ + private function wrongFamilyBoundaryEndpoints(array $world): array + { + $studentId = (int) $world['student_id']; + + return [ + ['GET', '/api/v1/parents/students/' . $studentId, []], + ['PATCH', '/api/v1/parents/students/' . $studentId, ['firstname' => 'Hijacked', 'lastname' => 'Student']], + ['GET', '/api/v1/parents/attendance/student/' . $studentId, []], + ['POST', '/api/v1/emergency-contacts', ['student_id' => $studentId, 'name' => 'Wrong Family', 'phone' => '5550000000']], + ['POST', '/api/v1/authorized-users', ['student_id' => $studentId, 'firstname' => 'Wrong', 'lastname' => 'Family', 'email' => 'wrong@example.test']], + ]; + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiFeatureFlagAndConfigurationDriftContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiFeatureFlagAndConfigurationDriftContractTest.php new file mode 100644 index 00000000..118e6a07 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiFeatureFlagAndConfigurationDriftContractTest.php @@ -0,0 +1,55 @@ +whereIn('config_key', [ + 'current_school_year', 'current_semester', 'fall_semester_start', 'spring_semester_start', 'last_school_day', + ])->delete(); + + $routes = $this->apiRoutesContainingAny(['semester', 'school-year', 'attendance', 'report-card', 'promotion', 'finance']); + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + $uri = $route->uri(); + $response = $this->requestAs($this->actorFor($uri), $method, $uri, $this->payloadFor($method, $uri)); + + $this->assertNoServerError($response, "$method $uri missing config"); + $this->assertControlled($response, $method, $uri); + $this->assertStringNotContainsStringIgnoringCase('Trying to access array offset', $response->getContent(), $uri . ' should not assume configuration rows exist.'); + } + } + + public function test_configuration_mutations_reject_unrecognized_or_privileged_keys_from_low_privilege_users(): void + { + $routes = $this->apiRoutesContainingAny(['configuration', 'settings', 'preferences', 'feature']); + $payload = [ + 'key' => 'force_admin_mode', + 'value' => true, + 'config_key' => 'force_admin_mode', + 'config_value' => true, + 'is_admin' => true, + 'maintenance_mode' => false, + ]; + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + if (! in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'], true)) { + continue; + } + + $uri = $route->uri(); + foreach (['teacher' => $this->teacher, 'parent' => $this->parent, 'student' => $this->studentUser] as $label => $actor) { + $response = $this->requestAs($actor, $method, $uri, $payload); + $this->assertNoServerError($response, "$label $method $uri config escalation"); + $this->assertStatusIn($response, [302, 400, 401, 403, 404, 405, 409, 419, 422], "$label must not mutate privileged configuration $uri"); + } + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiFileMediaMetadataSafetyContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiFileMediaMetadataSafetyContractTest.php new file mode 100644 index 00000000..14fac1fa --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiFileMediaMetadataSafetyContractTest.php @@ -0,0 +1,57 @@ +apiRoutesContainingAny(['upload', 'import', 'document', 'attachment', 'file', 'photo', 'avatar']), 0, 80); + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + $uri = $route->uri(); + + if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) { + continue; + } + + $payload = $this->payloadFor($method, $uri) + [ + 'file' => UploadedFile::fake()->create('../evil.php', 12, 'application/x-php'), + 'filename' => '../storage/logs/laravel.log', + 'original_name' => "invoice\0.pdf", + 'mime_type' => 'image/png; charset=utf-8', + ]; + + $response = $this->requestAs($this->actorFor($uri), $method, $uri, $payload); + + $this->assertControlled($response, $method, $uri); + $this->assertLessThan(500, $response->getStatusCode(), "$method $uri should handle misleading upload metadata without server errors."); + } + } + + /** @test */ + public function download_routes_do_not_accept_path_traversal_identifiers(): void + { + $routes = array_slice($this->apiRoutesContainingAny(['download', 'export', 'file', 'attachment', 'receipt', 'certificate']), 0, 90); + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + $uri = $route->uri(); + $payload = $this->payloadFor($method, $uri) + [ + 'filename' => '../../.env', + 'path' => '../../storage/logs/laravel.log', + 'file' => '../../composer.json', + ]; + + $response = $this->requestAs($this->actorFor($uri), $method, $uri, $payload); + + $this->assertControlled($response, $method, $uri); + $this->assertStringNotContainsString('APP_KEY=', $response->getContent(), "$method $uri must not leak environment files."); + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiFinanceInventoryCommunicationsFullSurfaceContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiFinanceInventoryCommunicationsFullSurfaceContractTest.php new file mode 100644 index 00000000..ca0b3baf --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiFinanceInventoryCommunicationsFullSurfaceContractTest.php @@ -0,0 +1,134 @@ +seedE2EConfiguration(); + $world = $this->seedTeacherClassWithStudent(9401); + $invoiceId = $this->seedInvoiceForParent((int) $world['parent_id']); + $this->actingAsApiAdministrator(); + + foreach ($this->endpoints($world, $invoiceId) as [$method, $uri, $payload]) { + $response = $this->json($method, $uri, $payload); + $this->assertNoServerError($response, $method . ' ' . $uri); + $this->assertStatusIn($response, [200, 201, 202, 204, 302, 400, 401, 403, 404, 405, 409, 419, 422], $method . ' ' . $uri); + } + } + + /** + * @param array $world + * @return list}> + */ + private function endpoints(array $world, int $invoiceId): array + { + $parentId = (int) $world['parent_id']; + $studentId = (int) $world['student_id']; + $teacherId = (int) $world['teacher']->id; + + return [ + ['GET', '/api/v1/finance/financial-report', []], + ['GET', '/api/v1/finance/financial-summary', []], + ['GET', '/api/v1/finance/stakeholder-analysis', []], + ['GET', '/api/v1/finance/parent-payment-followups', []], + ['POST', '/api/v1/finance/parent-payment-followups/' . $parentId . '/note', ['note' => 'Contract probe']], + ['POST', '/api/v1/finance/parent-payment-followups/' . $parentId . '/mark-contacted', ['contacted_at' => '2025-10-05']], + ['POST', '/api/v1/finance/parent-payment-followups/' . $parentId . '/promise-to-pay', ['amount' => 25, 'promise_date' => '2025-10-15']], + ['POST', '/api/v1/finance/parent-payment-followups/' . $parentId . '/resolve', ['resolution' => 'resolved']], + ['GET', '/api/v1/finance/carryforwards/preview', []], + ['POST', '/api/v1/finance/carryforwards/draft', ['parent_id' => $parentId, 'amount' => 25]], + ['POST', '/api/v1/finance/carryforwards/1/approve', ['approved' => true]], + ['POST', '/api/v1/finance/carryforwards/1/post', ['school_year' => '2026-2027']], + ['POST', '/api/v1/finance/carryforwards/1/waive', ['reason' => 'Contract probe']], + ['POST', '/api/v1/finance/carryforwards/1/adjust', ['amount' => 10, 'reason' => 'Contract probe']], + ['GET', '/api/v1/finance/carryforwards/report', []], + ['GET', '/api/v1/finance/event-charges', []], + ['POST', '/api/v1/finance/event-charges', ['student_id' => $studentId, 'amount' => 25, 'description' => 'Contract event']], + ['GET', '/api/v1/finance/event-charges/1', []], + ['PUT', '/api/v1/finance/event-charges/1', ['amount' => 30, 'description' => 'Updated contract event']], + ['DELETE', '/api/v1/finance/event-charges/1', []], + ['POST', '/api/v1/finance/event-charges/1/approve', ['approved' => true]], + ['POST', '/api/v1/finance/event-charges/1/void', ['reason' => 'Contract probe']], + ['POST', '/api/v1/finance/event-charges/1/attach-to-invoice', ['invoice_id' => $invoiceId]], + ['GET', '/api/v1/finance/installment-plans', []], + ['GET', '/api/v1/finance/installment-plans/1', []], + ['POST', '/api/v1/finance/invoices/' . $invoiceId . '/installment-plans', ['amount' => 100, 'installments' => 2]], + ['POST', '/api/v1/finance/installment-plans/1/activate', ['activate' => true]], + ['POST', '/api/v1/finance/installment-plans/1/cancel', ['reason' => 'Contract probe']], + ['POST', '/api/v1/finance/installment-plans/1/restructure', ['installments' => 3]], + ['POST', '/api/v1/finance/payments/1/allocate-installments', ['installment_ids' => [1]]], + ['GET', '/api/v1/finance/installments/due', []], + ['GET', '/api/v1/finance/installments/overdue', []], + ['POST', '/api/v1/finance/payments/1/send-receipt', ['email' => 'receipt@example.test']], + ['POST', '/api/v1/finance/refunds/1/send-receipt', ['email' => 'refund@example.test']], + ['POST', '/api/v1/finance/invoices/' . $invoiceId . '/send-statement', ['email' => 'statement@example.test']], + ['POST', '/api/v1/finance/parents/' . $parentId . '/send-overdue-reminder', ['email' => 'overdue@example.test']], + ['POST', '/api/v1/finance/installments/1/send-reminder', ['email' => 'installment@example.test']], + ['GET', '/api/v1/finance/notification-logs', []], + ['GET', '/api/v1/finance/unpaid-parents', []], + ['POST', '/api/v1/finance/fees/refund', ['parent_id' => $parentId, 'amount' => 25]], + ['POST', '/api/v1/finance/fees/tuition-total', ['parent_id' => $parentId]], + ['POST', '/api/v1/finance/fees/tuition-breakdown', ['parent_id' => $parentId]], + ['POST', '/api/v1/finance/fees/family-balance', ['parent_id' => $parentId]], + ['GET', '/api/v1/finance/fees/charges', []], + ['POST', '/api/v1/finance/fees/charges/extra', ['parent_id' => $parentId, 'amount' => 25, 'description' => 'Contract probe']], + ['POST', '/api/v1/finance/fees/charges/event', ['student_id' => $studentId, 'amount' => 25, 'description' => 'Contract probe']], + ['POST', '/api/v1/finance/fees/charges/extra/1/cancel', ['reason' => 'Contract probe']], + ['POST', '/api/v1/finance/fees/charges/extra/1/apply', ['invoice_id' => $invoiceId]], + ['POST', '/api/v1/finance/fees/charges/event/1/cancel', ['reason' => 'Contract probe']], + ['POST', '/api/v1/finance/fees/charges/event/1/mark-paid', ['amount' => 25]], + ['GET', '/api/v1/finance/refunds', []], + ['POST', '/api/v1/finance/refunds/recalculate-overpayments', ['parent_id' => $parentId]], + ['GET', '/api/v1/finance/refunds/parent-balances/' . $parentId, []], + ['POST', '/api/v1/finance/refunds', ['parent_id' => $parentId, 'amount' => 25, 'reason' => 'Contract probe']], + ['GET', '/api/v1/finance/refunds/1', []], + ['PATCH', '/api/v1/finance/refunds/1', ['amount' => 20]], + ['DELETE', '/api/v1/finance/refunds/1', []], + ['POST', '/api/v1/finance/refunds/1/approve', ['approved' => true]], + ['POST', '/api/v1/finance/refunds/1/reject', ['reason' => 'Contract probe']], + ['POST', '/api/v1/finance/refunds/1/payments', ['amount' => 20, 'payment_method' => 'cash']], + ['GET', '/api/v1/inventory/categories', []], + ['POST', '/api/v1/inventory/categories', ['name' => 'Contract Category']], + ['GET', '/api/v1/inventory/items', []], + ['POST', '/api/v1/inventory/items', ['name' => 'Contract Item', 'sku' => 'E2E-' . uniqid(), 'quantity' => 2]], + ['GET', '/api/v1/inventory/items/1', []], + ['PATCH', '/api/v1/inventory/items/1', ['name' => 'Contract Item Updated']], + ['DELETE', '/api/v1/inventory/items/1', []], + ['GET', '/api/v1/inventory/movements', []], + ['POST', '/api/v1/inventory/movements', ['item_id' => 1, 'quantity' => 1, 'type' => 'adjustment']], + ['GET', '/api/v1/suppliers', []], + ['POST', '/api/v1/suppliers', ['name' => 'Contract Supplier', 'email' => 'supplier@example.test']], + ['GET', '/api/v1/supply-categories', []], + ['POST', '/api/v1/supply-categories', ['name' => 'Contract Supply']], + ['GET', '/api/v1/messages', []], + ['POST', '/api/v1/messages', ['recipient_id' => $teacherId, 'subject' => 'Contract probe', 'body' => 'Message body']], + ['GET', '/api/v1/messages/1', []], + ['PATCH', '/api/v1/messages/1/read', ['read' => true]], + ['PATCH', '/api/v1/messages/1/archive', ['archived' => true]], + ['GET', '/api/v1/support', []], + ['POST', '/api/v1/support', ['subject' => 'Contract support', 'message' => 'Support body']], + ['GET', '/api/v1/support/teacher', []], + ['POST', '/api/v1/broadcast-email', ['subject' => 'Contract email', 'body' => 'Email body', 'recipients' => [$parentId]]], + ['GET', '/api/v1/email-extractor/emails', []], + ['POST', '/api/v1/email-extractor/compare', ['emails' => ['one@example.test'], 'against' => ['two@example.test']]], + ['GET', '/api/v1/communications', []], + ['POST', '/api/v1/communications', ['subject' => 'Contract comm', 'message' => 'Body']], + ['GET', '/api/v1/whatsapp/contacts', []], + ['POST', '/api/v1/whatsapp/links', ['phone' => '5551234567', 'message' => 'Contract probe']], + ]; + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiFrontendBackendContractParityTest.php b/tests/Feature/Api/V1/FullSurface/ApiFrontendBackendContractParityTest.php new file mode 100644 index 00000000..4c305f89 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiFrontendBackendContractParityTest.php @@ -0,0 +1,66 @@ +actingAs($this->admin, 'api')->getJson($path); + + $this->assertNoServerError($response, 'identity contract at ' . $path); + + if ($response->getStatusCode() === 200) { + $body = $response->json(); + $this->assertIsArray($body, $path . ' should return an inspectable JSON identity payload.'); + $this->assertTrue( + data_get($body, 'user.email') !== null || data_get($body, 'email') !== null || data_get($body, 'data.email') !== null, + $path . ' should expose a stable user email for frontend identity hydration.' + ); + } + } + } + + public function test_login_aliases_return_compatible_token_and_user_contracts(): void + { + $paths = ['/api/v1/auth/login', '/api/login', '/user/login']; + + foreach ($paths as $path) { + $response = $this->postJson($path, [ + 'email' => $this->admin->email, + 'password' => 'password', + ]); + + $this->assertNoServerError($response, 'login alias at ' . $path); + + if ($response->getStatusCode() >= 200 && $response->getStatusCode() < 300) { + $this->assertTrue($response->json('token') !== null || $response->json('access_token') !== null, $path . ' must expose a token field.'); + $this->assertTrue($response->json('user') !== null || $response->json('data.user') !== null, $path . ' must expose a user object.'); + } + } + } + + public function test_frontend_bootstrap_routes_do_not_require_admin_privilege_when_they_are_identity_or_config_only(): void + { + $routes = $this->apiRoutesContainingAny(['frontend', 'bootstrap', 'navigation', 'menu', 'preferences']); + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + + if ($method !== 'GET') { + continue; + } + + $uri = $route->uri(); + $response = $this->actingAs($this->parent, 'api')->getJson($this->materializePath($uri)); + + $this->assertControlled($response, 'GET', $uri); + $this->assertNoServerError($response, 'frontend bootstrap route at ' . $uri); + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiFullSurfaceEndToEndContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiFullSurfaceEndToEndContractTest.php new file mode 100644 index 00000000..60d66d0b --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiFullSurfaceEndToEndContractTest.php @@ -0,0 +1,367 @@ + + */ + private array $ids = []; + + private User $admin; + private User $teacher; + private User $parent; + private User $studentUser; + + protected function setUp(): void + { + parent::setUp(); + + $this->seedE2EConfiguration(); + $world = $this->seedTeacherClassWithStudent(9101); + + $this->teacher = $world['teacher']; + $this->parent = $world['parent']; + $this->studentUser = $this->createApiUserWithRole('student'); + $this->admin = $this->createApiUserWithRole('administrator'); + + $invoiceId = $this->seedInvoiceForParent((int) $world['parent_id']); + $voucherId = $this->seedDiscountVoucher(); + + $this->ids = [ + 'id' => 1, + 'userId' => $this->admin->id, + 'roleId' => (int) DB::table('roles')->where('name', 'parent')->value('id'), + 'permissionId' => 1, + 'student' => (int) $world['student_id'], + 'studentId' => (int) $world['student_id'], + 'parent' => (int) $world['parent_id'], + 'parentId' => (int) $world['parent_id'], + 'teacher' => $this->teacher->id, + 'teacherId' => $this->teacher->id, + 'classSection' => (int) $world['class_section_id'], + 'classSectionId' => (int) $world['class_section_id'], + 'class_section_id' => (int) $world['class_section_id'], + 'invoice' => $invoiceId, + 'invoiceId' => $invoiceId, + 'payment' => 1, + 'paymentId' => 1, + 'refund' => 1, + 'refundId' => 1, + 'voucher' => $voucherId, + 'schoolYear' => (int) DB::table('school_years')->where('name', self::E2E_SCHOOL_YEAR)->value('id'), + 'promotionId' => 1, + 'batchId' => 1, + 'contactId' => 1, + 'familyId' => 1, + 'authorizedUserId' => 1, + 'eventCharge' => 1, + 'plan' => 1, + 'carryforward' => 1, + 'installment' => 1, + 'certificateNumber' => 'CERT-E2E-001', + 'token' => 'invalid-token-for-contract-probe', + 'name' => 'placeholder.txt', + 'mode' => 'view', + ]; + } + + public function test_every_registered_api_route_has_a_controlled_e2e_contract_response(): void + { + $failures = []; + + foreach ($this->probeableApiRoutes() as $route) { + $method = $this->primaryMethod($route); + $uri = '/' . $this->materializeUri($route->uri()); + $actor = $this->actorFor($route->uri()); + $payload = $this->payloadFor($method, $route->uri()); + + $this->actingAs($actor, 'api'); + $response = $this->json($method, $uri, $payload); + $status = $response->getStatusCode(); + + if ($status >= 500) { + $failures[] = "$method $uri crashed with $status: " . $response->getContent(); + continue; + } + + if (! in_array($status, $this->allowedContractStatuses($method), true)) { + $failures[] = "$method $uri returned unexpected $status: " . $response->getContent(); + } + } + + $this->assertSame([], $failures, "Full-surface API contract failures:\n" . implode("\n", $failures)); + } + + public function test_all_mutating_api_routes_reject_empty_or_malformed_payloads_without_server_errors(): void + { + $failures = []; + + foreach ($this->probeableApiRoutes() as $route) { + $method = $this->primaryMethod($route); + if (! in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'], true)) { + continue; + } + + $uri = '/' . $this->materializeUri($route->uri()); + $this->actingAs($this->actorFor($route->uri()), 'api'); + $response = $this->json($method, $uri, []); + $status = $response->getStatusCode(); + + if ($status >= 500) { + $failures[] = "$method $uri malformed payload crashed with $status: " . $response->getContent(); + } + } + + $this->assertSame([], $failures, "Malformed-payload probes must fail cleanly, not explode:\n" . implode("\n", $failures)); + } + + public function test_portal_routes_enforce_wrong_actor_boundaries_across_the_full_surface(): void + { + $failures = []; + + foreach ($this->portalBoundaryProbes() as [$actor, $method, $uri]) { + $this->actingAs($actor, 'api'); + $response = $this->json($method, $this->materializePath($uri), $this->payloadFor($method, ltrim($uri, '/'))); + $status = $response->getStatusCode(); + + if (! in_array($status, [401, 403, 404, 405, 419, 422], true)) { + $failures[] = "$method $uri allowed wrong actor with $status: " . $response->getContent(); + } + } + + $this->assertSame([], $failures, "Wrong-role portal boundary failures:\n" . implode("\n", $failures)); + } + + /** + * @return list + */ + private function probeableApiRoutes(): array + { + return array_values(array_filter(Route::getRoutes()->getRoutes(), function (LaravelRoute $route): bool { + $uri = $route->uri(); + + if (! str_starts_with($uri, 'api/')) { + return false; + } + + if (str_contains($uri, 'documentation/swagger.json')) { + return false; + } + + if (str_contains($uri, 'files/') || str_contains($uri, '/pdf') || str_contains($uri, '/csv')) { + return false; + } + + return ! in_array($this->primaryMethod($route), ['HEAD', 'OPTIONS'], true); + })); + } + + private function primaryMethod(LaravelRoute $route): string + { + foreach ($route->methods() as $method) { + if (! in_array($method, ['HEAD', 'OPTIONS'], true)) { + return $method; + } + } + + return 'GET'; + } + + private function actorFor(string $uri): User + { + if (str_contains($uri, '/parents') || str_contains($uri, '/parent') || str_contains($uri, 'confirm_authorized_user')) { + return $this->parent; + } + + if (str_contains($uri, '/teacher') || str_contains($uri, '/teachers') || str_contains($uri, 'class-progress')) { + return $this->teacher; + } + + if (str_contains($uri, '/students/dashboard')) { + return $this->studentUser; + } + + return $this->admin; + } + + /** + * @return list + */ + private function portalBoundaryProbes(): array + { + return [ + [$this->parent, 'GET', '/api/v1/administrator/dashboard/metrics'], + [$this->parent, 'GET', '/api/v1/administrator/enrollment-withdrawal'], + [$this->teacher, 'GET', '/api/v1/parents/profile'], + [$this->admin, 'PATCH', '/api/v1/parents/students/{studentId}'], + [$this->parent, 'GET', '/api/v1/finance/financial-report'], + [$this->parent, 'GET', '/api/v1/inventory/items'], + [$this->teacher, 'POST', '/api/v1/school-years/{schoolYear}/close'], + [$this->studentUser, 'GET', '/api/v1/teachers/classes'], + ]; + } + + private function materializeUri(string $uri): string + { + return preg_replace_callback('/\{([^}:]+)(?:\?)?[^}]*\}/', function (array $matches): string { + $key = $matches[1]; + return (string) ($this->ids[$key] ?? $this->ids['id']); + }, $uri) ?? $uri; + } + + private function materializePath(string $path): string + { + return '/' . ltrim($this->materializeUri(ltrim($path, '/')), '/'); + } + + /** + * @return array + */ + private function payloadFor(string $method, string $uri): array + { + if ($method === 'GET' || $method === 'DELETE') { + return []; + } + + $base = [ + 'school_year' => self::E2E_SCHOOL_YEAR, + 'semester' => self::E2E_SEMESTER, + 'date' => '2025-10-05', + 'notes' => 'E2E full-surface contract probe', + ]; + + if (str_contains($uri, 'auth/login') || preg_match('#api/(?:v1/)?login$#', $uri)) { + return ['email' => $this->admin->email, 'password' => 'password']; + } + + if (str_contains($uri, 'users')) { + return $this->validUserPayload((int) $this->ids['roleId']); + } + + if (str_contains($uri, 'students')) { + return $base + [ + 'firstname' => 'Contract', + 'lastname' => 'Student', + 'dob' => '2015-01-01', + 'gender' => 'Male', + 'parent_id' => $this->ids['parentId'], + 'student_id' => $this->ids['studentId'], + 'class_section_id' => [$this->ids['classSectionId']], + ]; + } + + if (str_contains($uri, 'attendance')) { + return $base + [ + 'student_id' => $this->ids['studentId'], + 'class_section_id' => $this->ids['classSectionId'], + 'status' => 'present', + 'attendance' => [$this->ids['studentId'] => 'present'], + 'arrival_time' => '10:05', + ]; + } + + if (str_contains($uri, 'scores') || str_contains($uri, 'grading')) { + return $base + [ + 'student_id' => $this->ids['studentId'], + 'class_section_id' => $this->ids['classSectionId'], + 'score' => 95, + 'points' => 10, + 'column' => 'Contract Probe', + 'comment' => 'Solid work.', + ]; + } + + if (str_contains($uri, 'finance') || str_contains($uri, 'invoice') || str_contains($uri, 'payment') || str_contains($uri, 'refund')) { + return $base + [ + 'parent_id' => $this->ids['parentId'], + 'student_id' => $this->ids['studentId'], + 'invoice_id' => $this->ids['invoiceId'], + 'amount' => 25.00, + 'payment_method' => 'cash', + 'reason' => 'E2E contract probe', + ]; + } + + if (str_contains($uri, 'inventory') || str_contains($uri, 'suppliers') || str_contains($uri, 'supply')) { + return $base + [ + 'name' => 'Contract Probe Item', + 'sku' => 'E2E-' . uniqid(), + 'quantity' => 3, + 'unit_cost' => 5.25, + ]; + } + + if (str_contains($uri, 'messages') || str_contains($uri, 'support') || str_contains($uri, 'contact') || str_contains($uri, 'email')) { + return $base + [ + 'subject' => 'Contract Probe', + 'message' => 'This is an automated E2E contract probe.', + 'body' => 'This is an automated E2E contract probe.', + 'email' => 'contract.probe@example.test', + 'recipient_id' => $this->teacher->id, + ]; + } + + if (str_contains($uri, 'preferences') || str_contains($uri, 'settings') || str_contains($uri, 'configuration')) { + return $base + [ + 'key' => 'contract_probe', + 'value' => 'enabled', + 'config_key' => 'contract_probe', + 'config_value' => 'enabled', + ]; + } + + if (str_contains($uri, 'families') || str_contains($uri, 'authorized')) { + return $base + [ + 'parent_id' => $this->ids['parentId'], + 'student_id' => $this->ids['studentId'], + 'email' => 'guardian.contract@example.test', + 'firstname' => 'Guardian', + 'lastname' => 'Probe', + ]; + } + + return $base + [ + 'name' => 'Contract Probe', + 'title' => 'Contract Probe', + 'description' => 'E2E full-surface contract probe', + 'status' => 'active', + ]; + } + + /** + * @return list + */ + private function allowedContractStatuses(string $method): array + { + return match ($method) { + 'POST' => [200, 201, 202, 204, 302, 400, 401, 403, 404, 405, 409, 419, 422], + 'PUT', 'PATCH' => [200, 202, 204, 302, 400, 401, 403, 404, 405, 409, 419, 422], + 'DELETE' => [200, 202, 204, 302, 400, 401, 403, 404, 405, 409, 419, 422], + default => [200, 204, 302, 304, 400, 401, 403, 404, 405, 409, 419, 422], + }; + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiHtmlJavaScriptEscapingContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiHtmlJavaScriptEscapingContractTest.php new file mode 100644 index 00000000..2792efdf --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiHtmlJavaScriptEscapingContractTest.php @@ -0,0 +1,34 @@ +apiRoutes(), fn ($route) => in_array($this->primaryMethod($route), ['POST', 'PUT', 'PATCH'], true)), 0, 160); + $script = ''; + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + $uri = $route->uri(); + $payload = $this->payloadFor($method, $uri) + [ + 'name' => $script, + 'title' => $script, + 'description' => $script, + 'notes' => $script, + 'comment' => $script, + 'message' => $script, + ]; + + $response = $this->requestAs($this->actorFor($uri), $method, $uri, $payload); + + $this->assertControlled($response, $method, $uri); + $this->assertNoServerError($response, 'HTML/script payload at ' . $method . ' ' . $uri); + $this->assertStringNotContainsString('', $response->getContent()); + $this->assertStringNotContainsString('onerror=alert(1)', $response->getContent()); + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiHttpSemanticsAndMethodSafetyContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiHttpSemanticsAndMethodSafetyContractTest.php new file mode 100644 index 00000000..d164d977 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiHttpSemanticsAndMethodSafetyContractTest.php @@ -0,0 +1,55 @@ +apiRoutes(), fn ($route) => $this->primaryMethod($route) === 'GET'), 0, 120); + + foreach ($routes as $route) { + $uri = $route->uri(); + $response = $this->actingAs($this->actorFor($uri), 'api')->json('GET', $this->materializePath($uri), [ + 'is_admin' => true, + 'status' => 'deleted', + 'amount' => -999999, + 'role' => 'administrator', + ]); + + $this->assertControlled($response, 'GET', $uri); + $this->assertNoServerError($response, 'GET with mutating-looking query at ' . $uri); + } + } + + public function test_wrong_methods_fail_with_controlled_statuses_not_framework_explosions(): void + { + $routes = array_slice($this->apiRoutes(), 0, 120); + + foreach ($routes as $route) { + $uri = $route->uri(); + $declared = $this->primaryMethod($route); + $wrong = $declared === 'GET' ? 'POST' : 'GET'; + + $response = $this->actingAs($this->actorFor($uri), 'api')->json($wrong, $this->materializePath($uri), $this->payloadFor($wrong, $uri)); + + $this->assertStatusIn($response, [200, 201, 202, 204, 302, 400, 401, 403, 404, 405, 409, 419, 422], $wrong . ' ' . $uri); + $this->assertNoServerError($response, 'wrong method probe ' . $wrong . ' ' . $uri); + } + } + + public function test_delete_routes_do_not_require_json_body_to_be_safe(): void + { + $routes = array_slice(array_filter($this->apiRoutes(), fn ($route) => $this->primaryMethod($route) === 'DELETE'), 0, 80); + + foreach ($routes as $route) { + $uri = $route->uri(); + $response = $this->actingAs($this->actorFor($uri), 'api')->deleteJson($this->materializePath($uri)); + + $this->assertControlled($response, 'DELETE', $uri); + $this->assertNoServerError($response, 'bodyless delete at ' . $uri); + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiIdempotencyScopeAndReplayContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiIdempotencyScopeAndReplayContractTest.php new file mode 100644 index 00000000..2824012b --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiIdempotencyScopeAndReplayContractTest.php @@ -0,0 +1,42 @@ +apiRoutes(), fn ($route) => in_array($this->primaryMethod($route), ['POST', 'PUT', 'PATCH'], true)), 0, 100); + $sharedKey = 'cross-route-replay-e2e-key'; + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + $uri = $route->uri(); + + $this->actingAs($this->actorFor($uri), 'api'); + $first = $this->withHeaders(['Idempotency-Key' => $sharedKey])->json($method, $this->materializePath($uri), $this->payloadFor($method, $uri)); + $this->actingAs($this->actorFor($uri), 'api'); + $second = $this->withHeaders(['Idempotency-Key' => $sharedKey])->json($method, $this->materializePath($uri), $this->payloadFor($method, $uri)); + + $this->assertControlled($first, $method, $uri); + $this->assertControlled($second, $method, $uri); + $this->assertNoServerError($second, 'idempotency replay at ' . $method . ' ' . $uri); + } + } + + public function test_idempotency_keys_with_control_characters_are_rejected_or_ignored_cleanly(): void + { + $routes = array_slice(array_filter($this->apiRoutes(), fn ($route) => $this->primaryMethod($route) === 'POST'), 0, 80); + + foreach ($routes as $route) { + $uri = $route->uri(); + $this->actingAs($this->actorFor($uri), 'api'); + $response = $this->withHeaders(['Idempotency-Key' => "bad\r\nX-Injected: yes"])->json('POST', $this->materializePath($uri), $this->payloadFor('POST', $uri)); + + $this->assertControlled($response, 'POST', $uri); + $this->assertNoServerError($response, 'malformed idempotency key at POST ' . $uri); + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiImpersonationDelegationAndActingAsContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiImpersonationDelegationAndActingAsContractTest.php new file mode 100644 index 00000000..cda456d4 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiImpersonationDelegationAndActingAsContractTest.php @@ -0,0 +1,32 @@ +apiRoutesContainingAny(['impersonate', 'delegate', 'acting-as', 'switch-user', 'assume', 'behalf']), 0, 80); + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + $uri = $route->uri(); + $payload = $this->payloadFor($method, $uri) + [ + 'user_id' => $this->admin->id, + 'target_user_id' => $this->parent->id, + 'reason' => 'E2E impersonation probe', + ]; + + $adminResponse = $this->requestAs($this->admin, $method, $uri, $payload); + $parentResponse = $this->requestAs($this->parent, $method, $uri, $payload); + $teacherResponse = $this->requestAs($this->teacher, $method, $uri, $payload); + + $this->assertControlled($adminResponse, $method, $uri); + $this->assertStatusIn($parentResponse, [401, 403, 404, 405, 409, 419, 422], "$method $uri should deny parent impersonation attempts."); + $this->assertStatusIn($teacherResponse, [401, 403, 404, 405, 409, 419, 422], "$method $uri should deny teacher impersonation attempts."); + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiImportTemplateAndCsvContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiImportTemplateAndCsvContractTest.php new file mode 100644 index 00000000..1ab392a1 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiImportTemplateAndCsvContractTest.php @@ -0,0 +1,47 @@ +apiRoutesContainingAny(['import', 'upload', 'csv', 'bulk']); + $file = UploadedFile::fake()->createWithContent('malformed.csv', "name,email,amount\n\"unterminated,row\n', + ]; + + foreach ($routes as $route) { + $uri = $route->uri(); + $response = $this->requestAs($this->actorFor($uri), 'GET', $uri, $query); + + $this->assertControlled($response, 'GET', $uri); + $this->assertNoServerError($response, 'pagination cursor abuse at GET ' . $uri); + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiPermissionMatrixExhaustiveContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiPermissionMatrixExhaustiveContractTest.php new file mode 100644 index 00000000..13f22053 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiPermissionMatrixExhaustiveContractTest.php @@ -0,0 +1,81 @@ +permissionMatrix() as $label => $case) { + /** @var User $actor */ + $actor = $case['actor']; + $response = $this->requestAs($actor, $case['method'], $case['path'], $case['payload'] ?? []); + + $this->assertStatusIn($response, $case['allowed_statuses'], $label); + $this->assertNoServerError($response, $label); + } + } + + public function test_admin_only_mutations_are_not_available_to_lower_roles(): void + { + foreach ($this->adminOnlyMutations() as [$method, $path, $payload]) { + foreach ([$this->parent, $this->teacher, $this->studentUser] as $actor) { + $response = $this->requestAs($actor, $method, $path, $payload); + + $this->assertStatusIn($response, [302, 401, 403, 404, 405, 419, 422], "$method $path denied to {$actor->id}"); + $this->assertNoServerError($response, "$method $path denied to {$actor->id}"); + } + } + } + + public function test_student_role_cannot_access_parent_teacher_or_admin_portals(): void + { + foreach ($this->studentForbiddenPaths() as $path) { + $response = $this->requestAs($this->studentUser, 'GET', $path); + + $this->assertStatusIn($response, [302, 401, 403, 404, 405, 419, 422], "student forbidden $path"); + $this->assertNoServerError($response, "student forbidden $path"); + } + } + + /** @return array,allowed_statuses:list}> */ + private function permissionMatrix(): array + { + return [ + 'admin can reach users index' => ['actor' => $this->admin, 'method' => 'GET', 'path' => 'api/v1/users', 'allowed_statuses' => [200, 204, 302, 401, 403, 404, 405, 419, 422]], + 'teacher can reach teacher classes' => ['actor' => $this->teacher, 'method' => 'GET', 'path' => 'api/v1/teacher/classes', 'allowed_statuses' => [200, 204, 302, 401, 403, 404, 405, 419, 422]], + 'parent can reach parent profile' => ['actor' => $this->parent, 'method' => 'GET', 'path' => 'api/v1/parents/profile', 'allowed_statuses' => [200, 204, 302, 401, 403, 404, 405, 419, 422]], + 'parent denied users index' => ['actor' => $this->parent, 'method' => 'GET', 'path' => 'api/v1/users', 'allowed_statuses' => [302, 401, 403, 404, 405, 419, 422]], + 'teacher denied role permissions' => ['actor' => $this->teacher, 'method' => 'GET', 'path' => 'api/v1/roles', 'allowed_statuses' => [302, 401, 403, 404, 405, 419, 422]], + 'student denied finance reports' => ['actor' => $this->studentUser, 'method' => 'GET', 'path' => 'api/v1/finance/reports', 'allowed_statuses' => [302, 401, 403, 404, 405, 419, 422]], + ]; + } + + /** @return list}> */ + private function adminOnlyMutations(): array + { + return [ + ['POST', 'api/v1/users', $this->validUserPayload((int) $this->ids['roleId'])], + ['POST', 'api/v1/students', $this->payloadFor('POST', 'api/v1/students')], + ['POST', 'api/v1/school-years', ['name' => '2099-2100', 'start_date' => '2099-09-01', 'end_date' => '2100-06-01']], + ['POST', 'api/v1/inventory/items', $this->payloadFor('POST', 'api/v1/inventory/items')], + ['POST', 'api/v1/finance/payments', $this->payloadFor('POST', 'api/v1/finance/payments')], + ]; + } + + /** @return list */ + private function studentForbiddenPaths(): array + { + return [ + 'api/v1/admin/dashboard', + 'api/v1/teacher/classes', + 'api/v1/parents/profile', + 'api/v1/users', + 'api/v1/inventory/items', + 'api/v1/finance/reports', + ]; + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiPolicyAuthorizationDiscoveryContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiPolicyAuthorizationDiscoveryContractTest.php new file mode 100644 index 00000000..20d54283 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiPolicyAuthorizationDiscoveryContractTest.php @@ -0,0 +1,49 @@ +apiRoutes(), fn ($route) => in_array($this->primaryMethod($route), ['POST', 'PUT', 'PATCH', 'DELETE'], true)), 0, 140); + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + $uri = $route->uri(); + + if (str_contains($uri, 'auth/login') || str_contains($uri, 'register') || str_contains($uri, 'contact')) { + continue; + } + + $response = $this->json($method, $this->materializePath($uri), $this->payloadFor($method, $uri)); + $this->assertStatusIn($response, [302, 400, 401, 403, 404, 405, 419, 422], "$method $uri should not be publicly mutable."); + } + } + + /** @test */ + public function permission_like_routes_reject_wildcards_from_non_admins(): void + { + $routes = $this->apiRoutesContainingAny(['permission', 'role', 'access', 'acl', 'authorization']); + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + $uri = $route->uri(); + + if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) { + continue; + } + + $payload = $this->payloadFor($method, $uri) + [ + 'permissions' => ['*', 'admin.*', 'finance.*'], + 'roles' => ['administrator'], + ]; + + $response = $this->requestAs($this->teacher, $method, $uri, $payload); + $this->assertStatusIn($response, [401, 403, 404, 405, 409, 419, 422], "$method $uri should deny wildcard permission changes from teacher."); + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiPolymorphicOwnershipBoundaryContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiPolymorphicOwnershipBoundaryContractTest.php new file mode 100644 index 00000000..094ddff7 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiPolymorphicOwnershipBoundaryContractTest.php @@ -0,0 +1,54 @@ +apiRoutesContainingAny(['message', 'support', 'document', 'attachment', 'note', 'comment', 'contact', 'notification']); + + foreach (array_slice($routes, 0, 100) as $route) { + $method = $this->primaryMethod($route); + if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) { + continue; + } + + $uri = $route->uri(); + $payload = $this->payloadFor($method, $uri) + [ + 'owner_type' => 'App\\Models\\User', + 'owner_id' => $this->admin->id, + 'created_by_type' => 'App\\Models\\User', + 'created_by_id' => $this->admin->id, + 'recipient_type' => 'administrator', + 'recipient_id' => $this->admin->id, + ]; + + $response = $this->requestAs($this->parent, $method, $uri, $payload); + + $this->assertStatusIn($response, [200, 201, 202, 204, 400, 401, 403, 404, 405, 409, 419, 422], 'polymorphic overpost ' . $method . ' ' . $uri); + $this->assertNoServerError($response, 'polymorphic overpost ' . $method . ' ' . $uri); + $this->assertStringNotContainsString('App\\Models\\User', $response->getContent(), $uri . ' must not leak model class names.'); + } + } + + public function test_cross_actor_owner_queries_do_not_return_private_data(): void + { + $routes = $this->apiRoutesContainingAny(['messages', 'documents', 'attachments', 'notifications', 'notes', 'comments']); + + foreach (array_slice($routes, 0, 80) as $route) { + if ($this->primaryMethod($route) !== 'GET') { + continue; + } + + $uri = $route->uri(); + $response = $this->actingAs($this->parent, 'api')->getJson($this->materializePath($uri) . '?owner_type=user&owner_id=' . $this->admin->id); + + $this->assertControlled($response, 'GET', $uri); + $this->assertNoServerError($response, 'cross actor owner query ' . $uri); + $this->assertStringNotContainsStringIgnoringCase($this->admin->email, $response->getContent(), $uri . ' must not leak admin-owned data to parent.'); + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiPrivacyRetentionAndErasureContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiPrivacyRetentionAndErasureContractTest.php new file mode 100644 index 00000000..41e12b3d --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiPrivacyRetentionAndErasureContractTest.php @@ -0,0 +1,49 @@ +apiRoutesContainingAny(['privacy', 'erase', 'erasure', 'retention', 'archive', 'anonymize', 'gdpr', 'delete-account']); + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + $uri = $route->uri(); + + if (! in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'], true)) { + continue; + } + + $response = $this->json($method, $this->materializePath($uri), $this->payloadFor($method, $uri)); + + $this->assertStatusIn($response, [400, 401, 403, 404, 405, 419, 422], 'public privacy mutation ' . $method . ' ' . $uri); + $this->assertNoServerError($response, 'public privacy mutation ' . $method . ' ' . $uri); + } + } + + public function test_privacy_related_exports_do_not_leak_sensitive_user_fields(): void + { + $routes = $this->apiRoutesContainingAny(['export', 'download', 'report', 'privacy', 'profile']); + $forbidden = ['password', 'remember_token', 'two_factor', 'api_token', 'secret', 'private_key']; + + foreach (array_slice($routes, 0, 100) as $route) { + if ($this->primaryMethod($route) !== 'GET') { + continue; + } + + $uri = $route->uri(); + $response = $this->actingAs($this->actorFor($uri), 'api')->getJson($this->materializePath($uri)); + + $this->assertControlled($response, 'GET', $uri); + $this->assertNoServerError($response, 'privacy export/read at ' . $uri); + + foreach ($forbidden as $needle) { + $this->assertStringNotContainsStringIgnoringCase($needle, $response->getContent(), $uri . ' must not leak ' . $needle); + } + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiPublicAndAuthFullSurfaceContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiPublicAndAuthFullSurfaceContractTest.php new file mode 100644 index 00000000..59a31175 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiPublicAndAuthFullSurfaceContractTest.php @@ -0,0 +1,100 @@ +seedE2EConfiguration(); + $admin = $this->createApiUserWithRole('administrator'); + + foreach ($this->publicGetEndpoints() as $uri) { + $response = $this->getJson($uri); + $this->assertNoServerError($response, 'Public GET ' . $uri); + $this->assertStatusIn($response, [200, 302, 400, 401, 403, 404, 419, 422], 'Public GET ' . $uri); + } + + foreach ($this->publicPostEndpoints() as [$uri, $payload]) { + $response = $this->postJson($uri, $payload); + $this->assertNoServerError($response, 'Public POST ' . $uri); + $this->assertStatusIn($response, [200, 201, 202, 302, 400, 401, 403, 404, 419, 422], 'Public POST ' . $uri); + } + + $login = $this->postJson('/api/v1/auth/login', ['email' => $admin->email, 'password' => 'password']); + $this->assertNoServerError($login, 'Versioned auth login'); + $this->assertStatusIn($login, [200, 401, 403, 422], 'Versioned auth login'); + + $this->actingAs($admin, 'api'); + foreach (['/api/v1/auth/me', '/api/v1/frontend/me', '/api/v1/auth/logout'] as $uri) { + $response = str_ends_with($uri, 'logout') ? $this->postJson($uri) : $this->getJson($uri); + $this->assertNoServerError($response, 'Authenticated auth/frontend endpoint ' . $uri); + $this->assertStatusIn($response, [200, 204, 401, 403, 419, 422], 'Authenticated auth/frontend endpoint ' . $uri); + } + } + + /** + * @return list + */ + private function publicGetEndpoints(): array + { + return [ + '/api/documentation', + '/api/documentation/swagger', + '/api/docs/public', + '/api/access_denied', + '/api/certificates/verify/invalid-token', + '/api/timeoff/notify/invalid-token', + '/api/winners/competitions', + '/api/winners/competitions/1', + '/api/confirm_authorized_user?token=invalid-token', + '/api/set_authorized_user_password/1', + '/api/v1/auth/register/captcha', + '/api/v1/policies/school', + '/api/v1/policies/picture', + '/api/v1/pages/privacy', + '/api/v1/pages/terms', + '/api/v1/pages/help', + '/api/v1/frontend', + '/api/v1/frontend/facility', + '/api/v1/frontend/team', + '/api/v1/frontend/call-to-action', + '/api/v1/frontend/testimonial', + '/api/v1/frontend/not-found', + '/api/v1/health', + '/api/v1/system/db-check', + ]; + } + + /** + * @return list}> + */ + private function publicPostEndpoints(): array + { + return [ + ['/api/login', ['email' => 'missing@example.test', 'password' => 'wrong-password']], + ['/api/register', ['firstname' => 'Public', 'lastname' => 'Probe', 'email' => 'public.probe@example.test']], + ['/api/v1/login', ['email' => 'missing@example.test', 'password' => 'wrong-password']], + ['/api/v1/register', ['firstname' => 'Public', 'lastname' => 'Probe', 'email' => 'public.probe.v1@example.test']], + ['/api/v1/auth/login', ['email' => 'missing@example.test', 'password' => 'wrong-password']], + ['/api/v1/auth/register', ['firstname' => 'Public', 'lastname' => 'Probe', 'email' => 'public.auth@example.test']], + ['/api/v1/pages/contact', ['name' => 'Public Probe', 'email' => 'public@example.test', 'message' => 'Contract probe']], + ['/api/v1/contact', ['name' => 'Public Probe', 'email' => 'public@example.test', 'message' => 'Contract probe']], + ['/api/v1/badge_scan/scan', ['badge_code' => 'INVALID-BADGE']], + ['/api/v1/scanner/process', ['scan' => 'INVALID-BADGE']], + ['/api/set_authorized_user_password/1', ['password' => 'secret123', 'password_confirmation' => 'secret123', 'token' => 'invalid-token']], + ]; + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiPublicPrivateCacheSeparationContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiPublicPrivateCacheSeparationContractTest.php new file mode 100644 index 00000000..da4420bc --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiPublicPrivateCacheSeparationContractTest.php @@ -0,0 +1,54 @@ +apiRoutesContainingAny([ + 'users', 'students', 'parents', 'teacher', 'attendance', 'finance', 'inventory', 'messages', 'dashboard', 'reports', + ]), 0, 100); + + foreach ($privateRoutes as $route) { + $method = $this->primaryMethod($route); + + if ($method !== 'GET') { + continue; + } + + $uri = $route->uri(); + $response = $this->requestAs($this->actorFor($uri), $method, $uri); + $cacheControl = strtolower((string) $response->headers->get('Cache-Control')); + + $this->assertControlled($response, $method, $uri); + $this->assertFalse(str_contains($cacheControl, 'public') && str_contains($cacheControl, 'max-age'), "$method $uri should not mark private API data as publicly cacheable."); + } + } + + /** @test */ + public function conditional_cache_headers_do_not_bypass_authorization(): void + { + $routes = array_slice($this->apiRoutesContainingAny(['users', 'students', 'finance', 'attendance', 'dashboard', 'reports']), 0, 70); + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + + if ($method !== 'GET') { + continue; + } + + $uri = $route->uri(); + $response = $this->withHeaders([ + 'If-None-Match' => '"attacker-cache-tag"', + 'If-Modified-Since' => 'Wed, 21 Oct 2099 07:28:00 GMT', + ])->json('GET', $this->materializePath($uri)); + + $this->assertStatusIn($response, [200, 204, 302, 304, 401, 403, 404, 405, 409, 419, 422], "conditional unauthenticated GET $uri"); + $this->assertNotSame(500, $response->getStatusCode(), "conditional unauthenticated GET $uri should not crash."); + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiQueryParameterFuzzingContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiQueryParameterFuzzingContractTest.php new file mode 100644 index 00000000..a3bc11f5 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiQueryParameterFuzzingContractTest.php @@ -0,0 +1,98 @@ +collectionPaths() as $path) { + foreach ($this->hostileQueries() as $query) { + $response = $this->requestAs($this->actorFor($path), 'GET', $path . '?' . http_build_query($query)); + + $this->assertStatusIn($response, [200, 204, 302, 400, 401, 403, 404, 405, 409, 419, 422], "hostile query $path"); + $this->assertNoServerError($response, "hostile query $path"); + } + } + } + + public function test_date_range_endpoints_reject_inverted_or_impossible_ranges_cleanly(): void + { + foreach ($this->dateRangePaths() as $path) { + foreach ($this->badDateRanges() as $query) { + $response = $this->requestAs($this->actorFor($path), 'GET', $path . '?' . http_build_query($query)); + + $this->assertStatusIn($response, [200, 204, 302, 400, 401, 403, 404, 405, 409, 419, 422], "bad date range $path"); + $this->assertNoServerError($response, "bad date range $path"); + } + } + } + + public function test_sorting_contract_does_not_allow_raw_sql_fragments(): void + { + foreach ($this->collectionPaths() as $path) { + $response = $this->requestAs($this->actorFor($path), 'GET', $path . '?' . http_build_query([ + 'sort' => 'id desc; drop table users; --', + 'direction' => 'sideways', + ])); + + $this->assertStatusIn($response, [200, 204, 302, 400, 401, 403, 404, 405, 409, 419, 422], "SQL-ish sort $path"); + $this->assertNoServerError($response, "SQL-ish sort $path"); + $this->assertStringNotContainsString('SQLSTATE', $response->getContent(), "$path leaked SQLSTATE for malicious sort."); + } + } + + /** @return list */ + private function collectionPaths(): array + { + return [ + 'api/v1/users', + 'api/v1/students', + 'api/v1/classes', + 'api/v1/attendance', + 'api/v1/finance/invoices', + 'api/v1/finance/payments', + 'api/v1/inventory/items', + 'api/v1/messages', + 'api/v1/support', + 'api/v1/report-cards', + ]; + } + + /** @return list> */ + private function hostileQueries(): array + { + return [ + ['page' => -1, 'per_page' => 0], + ['page' => 'abc', 'per_page' => '999999999999'], + ['search' => str_repeat('x', 5000)], + ['status' => ['nested' => ['bad' => true]]], + ['include' => '../../../.env'], + ]; + } + + /** @return list */ + private function dateRangePaths(): array + { + return [ + 'api/v1/attendance', + 'api/v1/finance/reports', + 'api/v1/finance/payments', + 'api/v1/report-cards', + 'api/v1/messages', + ]; + } + + /** @return list> */ + private function badDateRanges(): array + { + return [ + ['start_date' => '2026-12-31', 'end_date' => '2025-01-01'], + ['from' => 'not-a-date', 'to' => 'also-not-a-date'], + ['date' => '0000-00-00'], + ['school_year' => '../../2025', 'semester' => ''], + ]; + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiQueueJobAndAsyncTriggerContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiQueueJobAndAsyncTriggerContractTest.php new file mode 100644 index 00000000..9912e8a7 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiQueueJobAndAsyncTriggerContractTest.php @@ -0,0 +1,52 @@ +apiRoutesContainingAny(['send', 'dispatch', 'queue', 'job', 'broadcast', 'notify', 'reminder', 'sync', 'import', 'export']); + + foreach (array_slice($routes, 0, 120) as $route) { + $method = $this->primaryMethod($route); + $uri = $route->uri(); + + if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) { + continue; + } + + $studentAttempt = $this->requestAs($this->studentUser, $method, $uri, $this->payloadFor($method, $uri)); + $this->assertStatusIn($studentAttempt, [400, 401, 403, 404, 405, 409, 419, 422], 'student async trigger ' . $method . ' ' . $uri); + $this->assertNoServerError($studentAttempt, 'student async trigger ' . $method . ' ' . $uri); + + $adminAttempt = $this->requestAs($this->admin, $method, $uri, $this->payloadFor($method, $uri)); + $this->assertControlled($adminAttempt, $method, $uri); + $this->assertNoServerError($adminAttempt, 'admin async trigger ' . $method . ' ' . $uri); + } + } + + public function test_async_triggers_tolerate_duplicate_client_request_ids(): void + { + $routes = $this->apiRoutesContainingAny(['send', 'dispatch', 'notify', 'broadcast', 'reminder']); + + foreach (array_slice($routes, 0, 60) as $route) { + $method = $this->primaryMethod($route); + if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) { + continue; + } + + $uri = $route->uri(); + $payload = $this->payloadFor($method, $uri) + ['client_request_id' => 'e2e-duplicate-async-trigger']; + + $first = $this->requestAs($this->actorFor($uri), $method, $uri, $payload); + $second = $this->requestAs($this->actorFor($uri), $method, $uri, $payload); + + $this->assertControlled($first, $method, $uri); + $this->assertControlled($second, $method, $uri); + $this->assertNoServerError($second, 'duplicate async trigger ' . $method . ' ' . $uri); + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiRateLimitTokenAndSessionHardeningContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiRateLimitTokenAndSessionHardeningContractTest.php new file mode 100644 index 00000000..04280b5b --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiRateLimitTokenAndSessionHardeningContractTest.php @@ -0,0 +1,86 @@ + $this->routeExistsFor('POST', $path)))); + + $this->assertNotEmpty($paths, 'At least one API login route should exist for brute-force hardening coverage.'); + + foreach ($paths as $path) { + for ($attempt = 1; $attempt <= 8; $attempt++) { + $response = $this->postJson($path, [ + 'email' => $this->admin->email, + 'password' => 'definitely-not-the-password-' . $attempt, + ]); + + $this->assertStatusIn($response, [400, 401, 403, 419, 422, 429], "Bad login attempt {$attempt} for {$path}"); + $response->assertJsonMissingPath('token'); + $response->assertJsonMissingPath('access_token'); + $response->assertJsonMissingPath('user.remember_token'); + } + } + } + + public function test_expired_or_garbage_bearer_tokens_are_rejected_without_fallback_identity(): void + { + foreach ($this->apiRoutesContainingAny(['auth/me', 'dashboard', 'profile', 'settings', 'finance', 'inventory']) as $route) { + $method = $this->primaryMethod($route); + $uri = $route->uri(); + + if ($method !== 'GET') { + continue; + } + + $response = $this->withHeader('Authorization', 'Bearer garbage-token-that-must-not-authenticate') + ->json('GET', $this->materializePath($uri)); + + $this->assertStatusIn($response, [401, 403, 404, 419], "Garbage bearer token should not authenticate {$uri}"); + } + } + + public function test_logout_is_safe_to_repeat_and_does_not_leak_token_state(): void + { + $logoutRoutes = $this->apiRoutesContainingAny(['logout']); + + $this->assertNotEmpty($logoutRoutes, 'Logout routes should be covered by repeat/logout contract tests.'); + + foreach ($logoutRoutes as $route) { + if ($this->primaryMethod($route) !== 'POST') { + continue; + } + + $path = $this->materializePath($route->uri()); + + $first = $this->requestAs($this->admin, 'POST', $route->uri()); + $this->assertControlled($first, 'POST', $route->uri()); + $first->assertJsonMissingPath('token'); + $first->assertJsonMissingPath('access_token'); + + $second = $this->postJson($path); + $this->assertStatusIn($second, [200, 204, 401, 403, 419], 'Repeated logout should stay controlled.'); + $second->assertJsonMissingPath('token'); + $second->assertJsonMissingPath('access_token'); + } + } + + private function routeExistsFor(string $method, string $path): bool + { + foreach ($this->apiRoutes() as $route) { + if ($this->primaryMethod($route) === $method && '/' . ltrim($route->uri(), '/') === $path) { + return true; + } + } + + return false; + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiReferentialIntegrityAndForeignKeyContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiReferentialIntegrityAndForeignKeyContractTest.php new file mode 100644 index 00000000..fdbc865f --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiReferentialIntegrityAndForeignKeyContractTest.php @@ -0,0 +1,62 @@ + 99999999, + 'parent_id' => 99999999, + 'teacher_id' => 99999999, + 'class_section_id' => 99999999, + 'invoice_id' => 99999999, + 'payment_id' => 99999999, + 'school_year_id' => 99999999, + 'category_id' => 99999999, + 'item_id' => 99999999, + 'supplier_id' => 99999999, + ]; + + foreach ($this->apiRoutesContainingAny([ + 'students', 'attendance', 'payments', 'refunds', 'invoices', 'inventory', + 'suppliers', 'messages', 'classes', 'teachers', 'parents', 'guardians', + ]) as $route) { + $method = $this->primaryMethod($route); + + if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) { + continue; + } + + $uri = $route->uri(); + $response = $this->requestAs($this->actorFor($uri), $method, $uri, $this->payloadFor($method, $uri) + $foreignKeyPayload); + + $this->assertStatusIn($response, [400, 403, 404, 409, 422], "{$method} {$uri} should reject impossible foreign keys cleanly."); + $body = strtolower($response->getContent()); + $this->assertStringNotContainsString('integrity constraint violation', $body); + $this->assertStringNotContainsString('foreign key constraint', $body); + $this->assertStringNotContainsString('sqlstate', $body); + } + } + + public function test_delete_routes_for_parent_records_protect_children_or_fail_cleanly(): void + { + foreach ($this->apiRoutesContainingAny(['parents', 'students', 'school-years', 'classes', 'invoices', 'inventory']) as $route) { + $method = $this->primaryMethod($route); + + if ($method !== 'DELETE') { + continue; + } + + $response = $this->requestAs($this->admin, 'DELETE', $route->uri()); + $this->assertStatusIn($response, [200, 202, 204, 400, 403, 404, 409, 422], "DELETE {$route->uri()} should be controlled."); + + $body = strtolower($response->getContent()); + $this->assertStringNotContainsString('constraint violation', $body); + $this->assertStringNotContainsString('sqlstate', $body); + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiRelationshipCardinalityContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiRelationshipCardinalityContractTest.php new file mode 100644 index 00000000..0062c542 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiRelationshipCardinalityContractTest.php @@ -0,0 +1,30 @@ +apiRoutes(), fn ($route) => in_array($this->primaryMethod($route), ['POST', 'PUT', 'PATCH'], true)), 0, 140); + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + $uri = $route->uri(); + $payload = $this->payloadFor($method, $uri) + [ + 'student_id' => [$this->ids['studentId'], 999999], + 'parent_id' => ['id' => $this->ids['parentId']], + 'teacher_id' => [$this->teacher->id], + 'class_section_id' => ['unexpected' => $this->ids['classSectionId']], + 'invoice_id' => [$this->ids['invoiceId']], + ]; + + $response = $this->requestAs($this->actorFor($uri), $method, $uri, $payload); + + $this->assertControlled($response, $method, $uri); + $this->assertNoServerError($response, 'relationship cardinality probe at ' . $method . ' ' . $uri); + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiReportConsistencyAndAggregationExpansionTest.php b/tests/Feature/Api/V1/FullSurface/ApiReportConsistencyAndAggregationExpansionTest.php new file mode 100644 index 00000000..ad91f925 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiReportConsistencyAndAggregationExpansionTest.php @@ -0,0 +1,51 @@ +apiRoutesContainingAny(['report', 'dashboard', 'summary', 'analytics', 'statistics']), 0, 100); + + $filterSets = [ + ['from' => '2025-09-01', 'to' => '2025-09-30', 'group_by' => 'day'], + ['from' => '2025-09-30', 'to' => '2025-09-01', 'group_by' => 'student'], + ['school_year' => self::E2E_SCHOOL_YEAR, 'semester' => self::E2E_SEMESTER, 'group_by' => 'class'], + ['include_inactive' => true, 'format' => 'json'], + ]; + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + $uri = $route->uri(); + + foreach ($filterSets as $filters) { + $payload = $method === 'GET' ? $filters : $this->payloadFor($method, $uri) + $filters; + $response = $this->requestAs($this->actorFor($uri), $method, $uri, $payload); + + $this->assertControlled($response, $method, $uri); + $this->assertLessThan(500, $response->getStatusCode(), "$method $uri report filters should not crash."); + } + } + } + + /** @test */ + public function parent_and_teacher_report_access_is_scoped_not_global(): void + { + $routes = $this->apiRoutesContainingAny(['report', 'dashboard', 'summary', 'analytics']); + + foreach (array_slice($routes, 0, 80) as $route) { + $method = $this->primaryMethod($route); + $uri = $route->uri(); + + $parentResponse = $this->requestAs($this->parent, $method, $uri, $this->payloadFor($method, $uri)); + $teacherResponse = $this->requestAs($this->teacher, $method, $uri, $this->payloadFor($method, $uri)); + + $this->assertStatusIn($parentResponse, [200, 204, 302, 401, 403, 404, 405, 409, 419, 422], "parent $method $uri report access"); + $this->assertStatusIn($teacherResponse, [200, 204, 302, 401, 403, 404, 405, 409, 419, 422], "teacher $method $uri report access"); + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiReportExportPrivacyAndAggregationContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiReportExportPrivacyAndAggregationContractTest.php new file mode 100644 index 00000000..e4b204e6 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiReportExportPrivacyAndAggregationContractTest.php @@ -0,0 +1,51 @@ +apiRoutesContainingAny(['reports', 'export', 'download', 'print', 'pdf', 'report-cards']) as $route) { + if ($this->primaryMethod($route) !== 'GET') { + continue; + } + + $uri = $route->uri(); + + $parentResponse = $this->requestAs($this->parent, 'GET', $uri, [ + 'student_id' => 999999, + 'parent_id' => 999999, + 'class_section_id' => 999999, + ]); + $this->assertStatusIn($parentResponse, [200, 204, 400, 401, 403, 404, 409, 422], "Parent report/export scope {$uri}"); + $this->assertStringNotContainsString('999999', $parentResponse->getContent()); + + $teacherResponse = $this->requestAs($this->teacher, 'GET', $uri, [ + 'student_id' => 999999, + 'class_section_id' => 999999, + ]); + $this->assertStatusIn($teacherResponse, [200, 204, 400, 401, 403, 404, 409, 422], "Teacher report/export scope {$uri}"); + $this->assertStringNotContainsString('999999', $teacherResponse->getContent()); + } + } + + public function test_aggregate_dashboard_numbers_do_not_include_other_family_details_for_parent(): void + { + foreach ($this->apiRoutesContainingAny(['dashboard', 'summary', 'stats', 'reports']) as $route) { + if ($this->primaryMethod($route) !== 'GET') { + continue; + } + + $response = $this->requestAs($this->parent, 'GET', $route->uri()); + $this->assertStatusIn($response, [200, 204, 400, 401, 403, 404, 409, 422], "Parent aggregate {$route->uri()}"); + + if ($response->getStatusCode() < 300) { + $this->assertStringNotContainsString('other-family', strtolower($response->getContent())); + $this->assertStringNotContainsString('all parents', strtolower($response->getContent())); + } + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiReportMathAndFinancialInvariantContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiReportMathAndFinancialInvariantContractTest.php new file mode 100644 index 00000000..0aa8a96b --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiReportMathAndFinancialInvariantContractTest.php @@ -0,0 +1,53 @@ +apiRoutesContainingAny(['finance', 'invoice', 'payment', 'refund', 'fee', 'charge', 'installment']); + $amounts = ['25,00', '$25.00', '1e309', '0x10', '--25', '25 USD']; + + foreach (array_slice($routes, 0, 100) as $route) { + $method = $this->primaryMethod($route); + if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) { + continue; + } + + foreach ($amounts as $amount) { + $uri = $route->uri(); + $response = $this->requestAs($this->admin, $method, $uri, $this->payloadFor($method, $uri) + ['amount' => $amount, 'total' => $amount, 'balance' => $amount]); + + $this->assertControlled($response, $method, $uri); + $this->assertNoServerError($response, 'bad financial amount ' . $amount . ' for ' . $method . ' ' . $uri); + } + } + } + + public function test_report_filters_do_not_create_negative_or_impossible_aggregation_windows(): void + { + $routes = $this->apiRoutesContainingAny(['report', 'summary', 'dashboard', 'analytics', 'finance']); + + foreach (array_slice($routes, 0, 100) as $route) { + if ($this->primaryMethod($route) !== 'GET') { + continue; + } + + $uri = $route->uri(); + $response = $this->actingAs($this->actorFor($uri), 'api')->getJson($this->materializePath($uri) . '?' . http_build_query([ + 'from' => '2026-12-31', + 'to' => '2025-01-01', + 'group_by' => 'parent.password', + 'aggregate' => 'sum(password)', + 'school_year' => '../../../../.env', + ])); + + $this->assertControlled($response, 'GET', $uri); + $this->assertNoServerError($response, 'bad report aggregation query at ' . $uri); + $this->assertStringNotContainsStringIgnoringCase('password', $response->getContent(), $uri . ' must not echo sensitive aggregate fields.'); + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiRequestHeaderSpoofingContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiRequestHeaderSpoofingContractTest.php new file mode 100644 index 00000000..c1e1f579 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiRequestHeaderSpoofingContractTest.php @@ -0,0 +1,31 @@ +apiRoutesContainingAny(['admin', 'users', 'finance', 'inventory', 'settings', 'school-years', 'permissions']), 0, 120); + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + $uri = $route->uri(); + + $this->actingAs($this->parent, 'api'); + $response = $this->withHeaders([ + 'X-Forwarded-For' => '127.0.0.1', + 'X-Original-Method' => 'GET', + 'X-HTTP-Method-Override' => 'GET', + 'X-User-Role' => 'administrator', + 'X-Admin' => 'true', + 'X-Impersonate-User' => (string) $this->admin->id, + ])->json($method, $this->materializePath($uri), $this->payloadFor($method, $uri)); + + $this->assertStatusIn($response, [401, 403, 404, 405, 419, 422], 'spoofed headers should not authorize parent at ' . $method . ' ' . $uri); + $this->assertNoServerError($response, 'header spoofing at ' . $method . ' ' . $uri); + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiRoleWorkflowNegativeMatrixContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiRoleWorkflowNegativeMatrixContractTest.php new file mode 100644 index 00000000..17521fc9 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiRoleWorkflowNegativeMatrixContractTest.php @@ -0,0 +1,83 @@ +apiRoutesContainingAny([ + 'users', 'roles', 'permissions', 'school-years', 'configuration', 'settings', 'inventory', 'finance', 'reimbursement', 'suppliers', + ]), fn ($route) => in_array($this->primaryMethod($route), ['POST', 'PUT', 'PATCH', 'DELETE'], true)); + + $actors = [ + 'teacher' => $this->teacher, + 'parent' => $this->parent, + 'student' => $this->studentUser, + ]; + + foreach ($actors as $label => $actor) { + foreach (array_slice($adminMutationRoutes, 0, 60) as $route) { + $method = $this->primaryMethod($route); + $uri = $route->uri(); + $response = $this->requestAs($actor, $method, $uri, $this->payloadFor($method, $uri)); + + $this->assertNoServerError($response, "$label $method $uri"); + $this->assertStatusIn($response, [302, 400, 401, 403, 404, 405, 409, 419, 422], "$label must not perform admin mutation $method $uri"); + } + } + } + + public function test_actor_specific_read_routes_do_not_collapse_into_admin_visible_data(): void + { + $expectations = [ + 'parent' => [$this->parent, ['admin', 'roles', 'permissions', 'finance/reports', 'inventory']], + 'teacher' => [$this->teacher, ['families', 'parent/invoices', 'school-years/close', 'permissions']], + 'student' => [$this->studentUser, ['teacher', 'parents', 'admin', 'finance', 'users']], + ]; + + foreach ($expectations as $label => [$actor, $needles]) { + foreach ($this->apiRoutesContainingAny($needles) as $route) { + $method = $this->primaryMethod($route); + if ($method !== 'GET') { + continue; + } + + $uri = $route->uri(); + $response = $this->requestAs($actor, $method, $uri); + + $this->assertNoServerError($response, "$label $method $uri"); + $this->assertStatusIn($response, [302, 400, 401, 403, 404, 405, 409, 419, 422], "$label should not read unrelated domain $uri"); + } + } + } + + public function test_suspended_disabled_or_unverified_users_do_not_gain_api_access(): void + { + $blockedUsers = [ + 'inactive' => $this->blockedUser(['status' => 'Inactive']), + 'suspended' => $this->blockedUser(['status' => 'Suspended']), + 'unverified' => $this->blockedUser(['is_verified' => false]), + ]; + + foreach ($blockedUsers as $label => $user) { + foreach (['api/v1/auth/me', 'api/v1/users', 'api/v1/parent/profile', 'api/v1/teacher/classes'] as $uri) { + $response = $this->requestAs($user, 'GET', $uri); + $this->assertNoServerError($response, "$label GET $uri"); + $this->assertStatusIn($response, [200, 302, 400, 401, 403, 404, 419, 422], "$label GET $uri"); + } + } + } + + private function blockedUser(array $overrides): User + { + return $this->createApiUserWithRole('parent', array_merge([ + 'firstname' => 'Blocked', + 'lastname' => 'User', + 'email' => 'blocked.' . uniqid() . '@example.test', + ], $overrides)); + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiRouteNamingVersioningAndHygieneTest.php b/tests/Feature/Api/V1/FullSurface/ApiRouteNamingVersioningAndHygieneTest.php new file mode 100644 index 00000000..1659c926 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiRouteNamingVersioningAndHygieneTest.php @@ -0,0 +1,60 @@ +apiRoutes() as $route) { + $uri = $route->uri(); + $name = (string) $route->getName(); + + $this->assertDoesNotMatchRegularExpression('/\b(test|tmp|temp|debug|foo|bar|todo|fixme|wip)\b/i', $uri, "Temporary/debug URI leaked: $uri"); + $this->assertDoesNotMatchRegularExpression('/\b(test|tmp|temp|debug|foo|bar|todo|fixme|wip)\b/i', $name, "Temporary/debug route name leaked: $name"); + } + } + + public function test_versioned_api_routes_stay_inside_v1_or_documented_legacy_aliases(): void + { + foreach ($this->apiRoutes() as $route) { + $uri = $route->uri(); + + $this->assertTrue( + str_starts_with($uri, 'api/v1/') || $this->isKnownUnversionedAlias($uri), + "API route must be versioned or explicitly classified as legacy alias: $uri" + ); + } + } + + public function test_duplicate_uri_method_pairs_do_not_exist(): void + { + $seen = []; + + foreach ($this->apiRoutes() as $route) { + $key = $this->primaryMethod($route) . ' ' . $route->uri(); + $this->assertArrayNotHasKey($key, $seen, "Duplicate API method/URI pair: $key"); + $seen[$key] = true; + } + } + + public function test_route_placeholders_use_consistent_identifier_names(): void + { + foreach ($this->apiRoutes() as $route) { + preg_match_all('/\{([^}:?]+).*?\}/', $route->uri(), $matches); + + foreach ($matches[1] as $placeholder) { + $this->assertDoesNotMatchRegularExpression('/^[a-z]$/', $placeholder, "Placeholder is too vague in {$route->uri()}"); + $this->assertDoesNotMatchRegularExpression('/thing|stuff|data|param|value/i', $placeholder, "Placeholder is semantically useless in {$route->uri()}"); + } + } + } + + private function isKnownUnversionedAlias(string $uri): bool + { + return preg_match('#^api/(login|register|logout|user|auth|docs|health|captcha|frontend|public|db-check|scan|scanner)#', $uri) === 1; + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiRouteParameterAbuseContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiRouteParameterAbuseContractTest.php new file mode 100644 index 00000000..21297d3c --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiRouteParameterAbuseContractTest.php @@ -0,0 +1,43 @@ +apiRoutes(), fn ($route) => str_contains($route->uri(), '{')), 0, 100); + $badValues = ['../.env', '..%2F..%2F.env', '', 'null', 'undefined', '0 OR 1=1']; + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + $template = $route->uri(); + + foreach ($badValues as $badValue) { + $uri = preg_replace('/\{[^}]+\}/', rawurlencode($badValue), $template) ?? $template; + $response = $this->requestAs($this->actorFor($template), $method, $uri, $this->payloadFor($method, $template)); + + $this->assertControlled($response, $method, $uri); + $this->assertNoServerError($response, 'bad route parameter ' . $method . ' ' . $uri); + $this->assertStringNotContainsString('.env', $response->getContent(), $uri . ' must not leak filesystem targets.'); + } + } + } + + public function test_parameterized_routes_fail_cleanly_for_overflowing_integer_ids(): void + { + $routes = array_slice(array_filter($this->apiRoutes(), fn ($route) => str_contains($route->uri(), '{')), 0, 100); + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + $template = $route->uri(); + $uri = preg_replace('/\{[^}]+\}/', '92233720368547758079223372036854775807', $template) ?? $template; + $response = $this->requestAs($this->actorFor($template), $method, $uri, $this->payloadFor($method, $template)); + + $this->assertControlled($response, $method, $uri); + $this->assertNoServerError($response, 'overflowing route id ' . $method . ' ' . $uri); + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiRouteUseCaseCatalogCompletenessTest.php b/tests/Feature/Api/V1/FullSurface/ApiRouteUseCaseCatalogCompletenessTest.php new file mode 100644 index 00000000..f64c7dec --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiRouteUseCaseCatalogCompletenessTest.php @@ -0,0 +1,160 @@ +apiRoutes() as $route) { + $signature = $this->routeSignature($route); + + if ($this->journeyFor($signature) === null) { + $unowned[] = $signature; + } + } + + $this->assertSame( + [], + $unowned, + "API routes without an explicit E2E user journey owner:\n" . implode("\n", $unowned) . + "\n\nAdd the route to the journey catalog and add or extend the scenario that proves it." + ); + } + + public function test_each_declared_journey_has_at_least_one_registered_route(): void + { + $signatures = array_map(fn (LaravelRoute $route): string => $this->routeSignature($route), $this->apiRoutes()); + $missing = []; + + foreach ($this->journeyCatalog() as $journey => $patterns) { + $matched = false; + foreach ($signatures as $signature) { + if ($this->matchesAny($signature, $patterns)) { + $matched = true; + break; + } + } + + if (! $matched) { + $missing[] = $journey; + } + } + + $this->assertSame([], $missing, "Declared E2E journeys with no registered API routes:\n" . implode("\n", $missing)); + } + + /** + * @return array> + */ + private function journeyCatalog(): array + { + return [ + 'public visitor reads policies, pages, health, docs, winners, certificates, kiosk forms' => [ + '#^(GET|POST) api/(?:documentation|docs|docs/public|access_denied|certificates/verify|timeoff/notify|winners|confirm_authorized_user|set_authorized_user_password)#', + '#^(GET|POST) api/v1/(?:policies|pages|contact|frontend|health|system/db-check|badge_scan|scanner)#', + ], + 'guest registers and authenticates through legacy and versioned auth aliases' => [ + '#^POST api/(?:login|register)$#', + '#^(GET|POST) api/v1/(?:login|register|auth)(?:/|$)#', + ], + 'user resolves dashboard, landing, preferences, nav, ui, utility, settings context' => [ + '#^(GET|POST|PUT|PATCH|DELETE) api/v1/(?:preferences|nav-builder|landing|info-icon|dashboard|utilities|ui|settings|settings/events|settings/school-calendar|configuration|system|stats|role-switcher)(?:/|$)#', + ], + 'administrator manages users, roles, permissions, school years, enrollment, promotions, alerts' => [ + '#^(GET|POST|PUT|PATCH|DELETE) api/v1/(?:administrator|users|role-permissions|school-years|ip-bans|notifications)(?:/|$)#', + ], + 'administrator manages classes, class sections, assignments, staff, teacher-class relationships' => [ + '#^(GET|POST|PUT|PATCH|DELETE) api/v1/(?:class-sections|classes|assignments|staff|teachers)(?:/|$)#', + '#^(GET|POST|PUT|PATCH|DELETE) api/v1/administrator/(?:teacher-class|staff)(?:/|$)#', + ], + 'teacher runs classroom roster, preparation, progress, submissions, legacy teacher screens' => [ + '#^(GET|POST|PUT|PATCH|DELETE) api/v1/(?:teacher|teachers|class-prep|class-progress|subject-curriculum|exam-drafts)(?:/|$)#', + ], + 'student, parent, family, guardian, authorized-user self-service' => [ + '#^(GET|POST|PUT|PATCH|DELETE) api/v1/(?:students|parents|families|family-admin|emergency-contacts|authorized-users)(?:/|$)#', + ], + 'attendance day operations, safety exceptions, slips, tracking, templates, incidents' => [ + '#^(GET|POST|PUT|PATCH|DELETE) api/(?:v1/)?(?:attendance|attendance-tracking|attendance-comment-templates|attendance-templates|administrator/attendance-templates|incidents)(?:/|$)#', + ], + 'grading, score entry, score comments, report cards, printables, badges, certificates' => [ + '#^(GET|POST|PUT|PATCH|DELETE) api/v1/(?:scores|grading|reports|reports/report-cards|reports/slips|reports/stickers|badges|certificates|print-requests|competition-scores|comments|final|homework|midterm|participation|projects|quizzes|exams/drafts|subjects/curriculum)(?:/|$)#', + '#^(GET|POST|PUT|PATCH|DELETE) api/v1/administrator/(?:grading|printables|certificates|trophy)(?:/|$)#', + ], + 'finance billing, invoices, payments, discounts, refunds, reimbursements, reports, charges' => [ + '#^(GET|POST|PUT|PATCH|DELETE) api/v1/(?:finance|invoices|payments|manual-pay|payment-notifications|payment-transactions|paypal|paypal-transactions|purchase-orders|refunds|reimbursements|discounts|expenses|extra-charges|event-charges|fees/charges)(?:/|$)#', + '#^(GET|POST|PUT|PATCH|DELETE) api/v1/administrator/(?:expenses|reports|invoices|paypal-transactions|reimbursements)(?:/|$)#', + ], + 'inventory, suppliers, supply categories, movements, procurement support' => [ + '#^(GET|POST|PUT|PATCH|DELETE) api/v1/(?:inventory|suppliers|supply-categories|files)(?:/|$)#', + ], + 'communications, broadcast email, extracted email, messages, WhatsApp, support' => [ + '#^(GET|POST|PUT|PATCH|DELETE) api/(?:emails|compare|proofread)(?:/|$)#', + '#^(GET|POST|PUT|PATCH|DELETE) api/v1/(?:messages|broadcast-email|email|email-extractor|communications|support|whatsapp|payment-notifications)(?:/|$)#', + ], + 'legacy compatibility aliases continue to resolve to canonical behavior' => [ + '#^(GET|POST|PUT|PATCH|DELETE) api/(?:attendance-templates|attendance-comment-templates|administrator/attendance-templates|proofread|emails|compare)(?:/|$)#', + '#^(GET|POST) api/v1/teacher/(?:homework-list|add-homework|add-quiz|add-quiz-column-form|add-midterm-exam|add-final-exam|add-participation|add-project|teacher-assignment|exam-drafts|drafts|inventory/books/distribute|print-requests/context|class-view|no-classes|select-semester|absence-vacation|showupdate-attendance|showupdate_attendance|scores|calendar|class-progress-submit|class-progress-history|class-progress-view|competition-scores)(?:/|$)#', + ], + ]; + } + + private function journeyFor(string $signature): ?string + { + foreach ($this->journeyCatalog() as $journey => $patterns) { + if ($this->matchesAny($signature, $patterns)) { + return $journey; + } + } + + return null; + } + + /** + * @param list $patterns + */ + private function matchesAny(string $signature, array $patterns): bool + { + foreach ($patterns as $pattern) { + if (preg_match($pattern, $signature) === 1) { + return true; + } + } + + return false; + } + + /** + * @return list + */ + private function apiRoutes(): array + { + return array_values(array_filter(Route::getRoutes()->getRoutes(), function (LaravelRoute $route): bool { + return str_starts_with($route->uri(), 'api/'); + })); + } + + private function routeSignature(LaravelRoute $route): string + { + $methods = array_values(array_diff($route->methods(), ['HEAD', 'OPTIONS'])); + sort($methods); + + return implode(',', $methods) . ' ' . $route->uri(); + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiSchemaKeyStabilityExpansionContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiSchemaKeyStabilityExpansionContractTest.php new file mode 100644 index 00000000..16a65c25 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiSchemaKeyStabilityExpansionContractTest.php @@ -0,0 +1,83 @@ + ['id', 'email', 'firstname', 'lastname'], + 'api/v1/students' => ['id', 'firstname', 'lastname', 'parent_id'], + 'api/v1/classes' => ['id', 'name'], + 'api/v1/class-sections' => ['id', 'class_section_id', 'class_section_name'], + 'api/v1/parent/profile' => ['id', 'email'], + 'api/v1/teacher/classes' => ['class_section_id', 'class_section_name'], + 'api/v1/finance/invoices' => ['id', 'invoice_id', 'amount', 'balance'], + 'api/v1/inventory/items' => ['id', 'name', 'quantity'], + 'api/v1/messages' => ['id', 'subject', 'message', 'body'], + 'api/v1/support/tickets' => ['id', 'subject', 'status'], + ]; + + foreach ($contracts as $uri => $expectedAnyKeys) { + $response = $this->requestAs($this->actorFor($uri), 'GET', $uri); + $this->assertNoServerError($response, 'GET ' . $uri); + + if ($response->getStatusCode() !== 200 || ! $this->isJsonString($response->getContent())) { + continue; + } + + $payload = $this->flattenKeys($response->json()); + $this->assertTrue( + count(array_intersect($expectedAnyKeys, $payload)) > 0, + $uri . ' should expose at least one stable identity/display key. Keys: ' . implode(', ', $payload) + ); + } + } + + public function test_error_payloads_keep_client_actionable_fields(): void + { + foreach ($this->apiRoutes() as $route) { + $method = $this->primaryMethod($route); + if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) { + continue; + } + + $uri = $route->uri(); + $response = $this->requestAs($this->actorFor($uri), $method, $uri, ['force_validation_failure' => true]); + $this->assertNoServerError($response, "$method $uri"); + + if (in_array($response->getStatusCode(), [400, 401, 403, 404, 409, 419, 422], true) && $this->isJsonString($response->getContent())) { + $keys = $this->flattenKeys($response->json()); + $this->assertTrue( + count(array_intersect(['message', 'errors', 'error', 'ok', 'status'], $keys)) > 0, + "$method $uri must return an actionable error envelope. Keys: " . implode(', ', $keys) + ); + } + } + } + + /** @return list */ + private function flattenKeys(mixed $payload, string $prefix = ''): array + { + if (! is_array($payload)) { + return []; + } + + $keys = []; + foreach ($payload as $key => $value) { + if (is_string($key)) { + $keys[] = $key; + if (is_array($value)) { + $keys = array_merge($keys, $this->flattenKeys($value, $prefix . $key . '.')); + } + } elseif (is_array($value)) { + $keys = array_merge($keys, $this->flattenKeys($value, $prefix)); + } + } + + return array_values(array_unique($keys)); + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiSchoolCalendarAcademicBoundaryExpansionTest.php b/tests/Feature/Api/V1/FullSurface/ApiSchoolCalendarAcademicBoundaryExpansionTest.php new file mode 100644 index 00000000..f3e86844 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiSchoolCalendarAcademicBoundaryExpansionTest.php @@ -0,0 +1,49 @@ +apiRoutesContainingAny(['attendance', 'homework', 'quiz', 'project', 'midterm', 'final', 'class-progress', 'report-card', 'assignment']); + $dates = ['1900-01-01', '2099-12-31', '2025-02-30', '0000-00-00']; + + foreach (array_slice($routes, 0, 120) as $route) { + $method = $this->primaryMethod($route); + if (! in_array($method, ['POST', 'PUT', 'PATCH', 'GET'], true)) { + continue; + } + + foreach ($dates as $date) { + $uri = $route->uri(); + $payload = $this->payloadFor($method, $uri) + ['date' => $date, 'due_date' => $date, 'exam_date' => $date]; + $response = $method === 'GET' + ? $this->actingAs($this->actorFor($uri), 'api')->getJson($this->materializePath($uri) . '?date=' . urlencode($date)) + : $this->requestAs($this->actorFor($uri), $method, $uri, $payload); + + $this->assertControlled($response, $method, $uri); + $this->assertNoServerError($response, 'academic boundary date ' . $date . ' at ' . $method . ' ' . $uri); + } + } + } + + public function test_semester_and_term_values_are_not_free_form_privilege_channels(): void + { + $routes = $this->apiRoutesContainingAny(['attendance', 'scores', 'report-card', 'class-progress', 'finance', 'school-year']); + + foreach (array_slice($routes, 0, 100) as $route) { + $method = $this->primaryMethod($route); + $uri = $route->uri(); + $payload = $this->payloadFor($method, $uri) + ['semester' => 'admin', 'term' => '../../secret', 'quarter' => '']; + $response = $method === 'GET' + ? $this->actingAs($this->actorFor($uri), 'api')->getJson($this->materializePath($uri) . '?semester=admin&term=../../secret') + : $this->requestAs($this->actorFor($uri), $method, $uri, $payload); + + $this->assertControlled($response, $method, $uri); + $this->assertNoServerError($response, 'bad semester/term values at ' . $method . ' ' . $uri); + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiSchoolOperationsRegressionBackstopTest.php b/tests/Feature/Api/V1/FullSurface/ApiSchoolOperationsRegressionBackstopTest.php new file mode 100644 index 00000000..7ad80ddd --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiSchoolOperationsRegressionBackstopTest.php @@ -0,0 +1,52 @@ +apiRoutesContainingAny([ + 'dashboard', 'students', 'parents', 'classes', 'class-sections', 'teachers', 'attendance', 'scores', 'report-card', + 'finance', 'inventory', 'messages', 'support', 'school-years', 'promotions', 'settings', 'configuration', + ]); + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + $uri = $route->uri(); + $response = $this->requestAs($this->admin, $method, $uri, $this->payloadFor($method, $uri)); + + $this->assertNoServerError($response, "admin $method $uri regression backstop"); + $this->assertControlled($response, $method, $uri); + } + } + + public function test_user_facing_portals_have_at_least_one_reachable_read_contract_per_actor(): void + { + $portalGroups = [ + 'teacher' => [$this->teacher, ['teacher', 'class-progress', 'attendance']], + 'parent' => [$this->parent, ['parent', 'families', 'guardians']], + 'student' => [$this->studentUser, ['student']], + ]; + + foreach ($portalGroups as $label => [$actor, $needles]) { + $sawControlledRoute = false; + + foreach ($this->apiRoutesContainingAny($needles) as $route) { + if ($this->primaryMethod($route) !== 'GET') { + continue; + } + + $uri = $route->uri(); + $response = $this->requestAs($actor, 'GET', $uri); + $this->assertNoServerError($response, "$label GET $uri portal reachability"); + $this->assertControlled($response, 'GET', $uri); + $sawControlledRoute = true; + } + + $this->assertTrue($sawControlledRoute, "$label portal should have at least one registered GET route under its domain."); + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiSearchIndexAndAutocompleteContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiSearchIndexAndAutocompleteContractTest.php new file mode 100644 index 00000000..225dc5d8 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiSearchIndexAndAutocompleteContractTest.php @@ -0,0 +1,51 @@ +apiRoutesContainingAny(['search', 'lookup', 'autocomplete', 'suggest', 'filter']); + $terms = ['', 'a', ' ', '%', "' OR '1'='1", '', str_repeat('x', 2048)]; + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + if ($method !== 'GET') { + continue; + } + + $uri = $route->uri(); + foreach ($terms as $term) { + $response = $this->requestAs($this->actorFor($uri), 'GET', $uri . '?q=' . urlencode($term) . '&term=' . urlencode($term) . '&search=' . urlencode($term)); + $this->assertNoServerError($response, "GET $uri search term $term"); + $this->assertControlled($response, 'GET', $uri); + $this->assertStringNotContainsStringIgnoringCase('SQLSTATE', $response->getContent(), $uri . ' should not leak SQLSTATE from search.'); + $this->assertStringNotContainsString('', $response->getContent(), $uri . ' should not reflect raw script search terms.'); + } + } + } + + public function test_cross_domain_search_does_not_bypass_role_scope(): void + { + $routes = $this->apiRoutesContainingAny(['search', 'lookup', 'autocomplete']); + + foreach ($routes as $route) { + if ($this->primaryMethod($route) !== 'GET') { + continue; + } + + $uri = $route->uri(); + $query = '?q=*&scope=all&include=users,parents,students,finance,medical'; + + foreach (['parent' => $this->parent, 'teacher' => $this->teacher, 'student' => $this->studentUser] as $label => $actor) { + $response = $this->requestAs($actor, 'GET', $uri . $query); + $this->assertNoServerError($response, "$label GET $uri broad search"); + $this->assertStatusIn($response, [200, 204, 302, 400, 401, 403, 404, 409, 419, 422], "$label GET $uri broad search"); + $this->assertStringNotContainsStringIgnoringCase('remember_token', $response->getContent(), "$uri broad search must not leak auth internals."); + } + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiSecurityAndTransportContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiSecurityAndTransportContractTest.php new file mode 100644 index 00000000..c966cc44 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiSecurityAndTransportContractTest.php @@ -0,0 +1,88 @@ +representativeProtectedRoutes() as $route) { + $method = $this->primaryMethod($route); + $uri = $route->uri(); + + $response = $this->json($method, $this->materializePath($uri), $this->payloadFor($method, $uri)); + + $this->assertStatusIn($response, [200, 302, 401, 403, 404, 405, 419, 422], "$method $uri unauthenticated transport contract"); + $this->assertNoServerError($response, "$method $uri unauthenticated transport contract"); + } + } + + public function test_state_changing_api_routes_reject_wrong_http_method_cleanly(): void + { + foreach ($this->stateChangingRoutes() as $route) { + $uri = $route->uri(); + + $response = $this->requestAs($this->actorFor($uri), 'GET', $uri); + + $this->assertStatusIn($response, [200, 302, 401, 403, 404, 405, 409, 419, 422], "GET against mutating route $uri"); + $this->assertNoServerError($response, "GET against mutating route $uri"); + } + } + + public function test_options_preflight_for_major_domains_is_controlled(): void + { + foreach ($this->majorDomainPaths() as $path) { + $response = $this->json('OPTIONS', $path, [], [ + 'Origin' => 'https://frontend.example.test', + 'Access-Control-Request-Method' => 'POST', + 'Access-Control-Request-Headers' => 'Authorization, Content-Type', + ]); + + $this->assertStatusIn($response, [200, 204, 301, 302, 401, 403, 404, 405, 419], "OPTIONS $path"); + $this->assertNoServerError($response, "OPTIONS $path"); + } + } + + /** @return list */ + private function representativeProtectedRoutes(): array + { + return array_slice(array_filter($this->apiRoutes(), function (LaravelRoute $route): bool { + $uri = $route->uri(); + + return ! str_contains($uri, 'login') + && ! str_contains($uri, 'register') + && ! str_contains($uri, 'captcha') + && ! str_contains($uri, 'docs') + && ! str_contains($uri, 'health') + && ! str_contains($uri, 'public'); + }), 0, 60); + } + + /** @return list */ + private function stateChangingRoutes(): array + { + return array_slice(array_filter($this->apiRoutes(), function (LaravelRoute $route): bool { + return in_array($this->primaryMethod($route), ['POST', 'PUT', 'PATCH', 'DELETE'], true); + }), 0, 60); + } + + /** @return list */ + private function majorDomainPaths(): array + { + return [ + '/api/v1/auth/login', + '/api/v1/users', + '/api/v1/students', + '/api/v1/classes', + '/api/v1/attendance', + '/api/v1/finance/reports', + '/api/v1/inventory/items', + '/api/v1/messages', + '/api/v1/support', + '/api/v1/parents/profile', + ]; + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiSerializationSensitiveDataRedactionContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiSerializationSensitiveDataRedactionContractTest.php new file mode 100644 index 00000000..2325c9a0 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiSerializationSensitiveDataRedactionContractTest.php @@ -0,0 +1,70 @@ +apiRoutesContainingAny([ + 'users', 'parents', 'students', 'teachers', 'guardians', 'authorized-users', + 'families', 'finance', 'payments', 'invoices', 'profile', 'auth/me', + ]); + + $this->assertNotEmpty($routes, 'Sensitive-resource routes should exist for serialization redaction coverage.'); + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + + if ($method !== 'GET') { + continue; + } + + $response = $this->probeRoute($route); + $this->assertControlled($response, $method, $route->uri()); + + if (! $this->isJsonString($response->getContent())) { + continue; + } + + $body = strtolower($response->getContent()); + + foreach ([ + 'password', 'remember_token', 'two_factor_secret', 'two_factor_recovery_codes', + 'api_token', 'private_key', 'secret_key', 'card_number', 'cvv', 'cvc', + 'stripe_secret', 'paypal_secret', 'authorization_code', + ] as $forbidden) { + $this->assertStringNotContainsString($forbidden, $body, "{$route->uri()} leaked {$forbidden}"); + } + } + } + + public function test_error_responses_do_not_echo_credentials_or_uploaded_file_contents(): void + { + $payload = [ + 'email' => 'sensitive@example.test', + 'password' => 'SUPER_SECRET_PASSWORD_SHOULD_NOT_ECHO', + 'token' => 'SECRET_TOKEN_SHOULD_NOT_ECHO', + 'card_number' => '4111111111111111', + 'notes' => str_repeat('private-note-', 20), + ]; + + foreach ($this->apiRoutesContainingAny(['login', 'users', 'payments', 'refunds', 'support', 'messages']) as $route) { + $method = $this->primaryMethod($route); + + if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) { + continue; + } + + $response = $this->requestAs($this->actorFor($route->uri()), $method, $route->uri(), $payload); + $this->assertControlled($response, $method, $route->uri()); + + $body = $response->getContent(); + $this->assertStringNotContainsString('SUPER_SECRET_PASSWORD_SHOULD_NOT_ECHO', $body); + $this->assertStringNotContainsString('SECRET_TOKEN_SHOULD_NOT_ECHO', $body); + $this->assertStringNotContainsString('4111111111111111', $body); + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiSoftDeleteRestoreArchiveLifecycleContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiSoftDeleteRestoreArchiveLifecycleContractTest.php new file mode 100644 index 00000000..b7be62b9 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiSoftDeleteRestoreArchiveLifecycleContractTest.php @@ -0,0 +1,47 @@ +apiRoutesContainingAny(['archive', 'restore', 'unarchive', 'recover']); + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + $uri = $route->uri(); + + $adminResponse = $this->requestAs($this->admin, $method, $uri, $this->payloadFor($method, $uri)); + $this->assertControlled($adminResponse, $method, $uri); + + $parentResponse = $this->requestAs($this->parent, $method, $uri, $this->payloadFor($method, $uri)); + $this->assertStatusIn($parentResponse, [401, 403, 404, 405, 409, 419, 422], "$method $uri should not allow parent archive/restore privileges by accident."); + } + } + + /** @test */ + public function destructive_soft_delete_style_routes_tolerate_repeated_requests(): void + { + $routes = array_slice($this->apiRoutesContainingAny(['delete', 'destroy', 'archive', 'cancel', 'void', 'remove']), 0, 80); + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + $uri = $route->uri(); + + if (! in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'], true)) { + continue; + } + + $first = $this->requestAs($this->actorFor($uri), $method, $uri, $this->payloadFor($method, $uri)); + $second = $this->requestAs($this->actorFor($uri), $method, $uri, $this->payloadFor($method, $uri)); + + $this->assertControlled($first, $method, $uri); + $this->assertControlled($second, $method, $uri); + $this->assertLessThan(500, $second->getStatusCode(), "$method $uri repeated destructive request should be boring, not catastrophic."); + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiSortingFieldSelectionAndIncludeContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiSortingFieldSelectionAndIncludeContractTest.php new file mode 100644 index 00000000..ee6a5b39 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiSortingFieldSelectionAndIncludeContractTest.php @@ -0,0 +1,34 @@ +apiRoutes(), fn ($route) => $this->primaryMethod($route) === 'GET'), 0, 120); + + $querySets = [ + ['sort' => 'name', 'direction' => 'asc', 'fields' => 'id,name,created_at'], + ['sort' => '-created_at', 'include' => 'parent,student,classSection'], + ['sort' => 'nonexistent_column', 'direction' => 'sideways'], + ['fields' => 'id,password,remember_token,api_token', 'include' => 'user.roles.permissions'], + ['expand' => str_repeat('children.', 20) . 'children'], + ]; + + foreach ($routes as $route) { + $uri = $route->uri(); + + foreach ($querySets as $query) { + $response = $this->requestAs($this->actorFor($uri), 'GET', $uri, $query); + + $this->assertControlled($response, 'GET', $uri); + $this->assertStringNotContainsString('remember_token', $response->getContent(), "GET $uri should not expose remember_token through fields/include tricks."); + $this->assertStringNotContainsString('api_token', $response->getContent(), "GET $uri should not expose raw api_token through fields/include tricks."); + } + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiTenantSchoolYearAndSemesterIsolationContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiTenantSchoolYearAndSemesterIsolationContractTest.php new file mode 100644 index 00000000..73ac4ec6 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiTenantSchoolYearAndSemesterIsolationContractTest.php @@ -0,0 +1,56 @@ +insertGetId([ + 'name' => 'E2E Foreign Year', + 'status' => 'closed', + 'created_at' => now(), + 'updated_at' => now(), + ]); + + $routes = array_slice($this->apiRoutesContainingAny(['school-year', 'school_year', 'attendance', 'scores', 'grading', 'invoice', 'payment', 'report-card']), 0, 120); + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + $uri = $route->uri(); + $payload = $this->payloadFor($method, $uri) + [ + 'school_year_id' => $foreignYearId, + 'school_year' => 'E2E Foreign Year', + 'semester' => 'Archived Semester', + ]; + + $response = $this->requestAs($this->actorFor($uri), $method, $uri, $payload); + + $this->assertControlled($response, $method, $uri); + $this->assertNoServerError($response, 'foreign school year scope at ' . $method . ' ' . $uri); + } + } + + public function test_cross_school_year_filters_do_not_return_unscoped_private_data(): void + { + $routes = array_slice($this->apiRoutesContainingAny(['students', 'attendance', 'invoices', 'payments', 'reports', 'classes']), 0, 90); + + foreach ($routes as $route) { + if ($this->primaryMethod($route) !== 'GET') { + continue; + } + + $uri = $route->uri(); + $response = $this->requestAs($this->actorFor($uri), 'GET', $uri, [ + 'school_year' => '1900-1901', + 'semester' => 'Nonexistent', + ]); + + $this->assertControlled($response, 'GET', $uri); + $this->assertNoServerError($response, 'cross-year read filter at GET ' . $uri); + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiTimezoneCalendarRecurrenceDepthContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiTimezoneCalendarRecurrenceDepthContractTest.php new file mode 100644 index 00000000..b4b2a39c --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiTimezoneCalendarRecurrenceDepthContractTest.php @@ -0,0 +1,34 @@ +apiRoutesContainingAny(['calendar', 'schedule', 'event', 'school-year', 'semester']), 0, 80); + + $payloads = [ + ['date' => '2025-03-09', 'time' => '02:30', 'timezone' => 'America/New_York'], + ['starts_at' => '2025-11-02T01:30:00', 'ends_at' => '2025-11-02T01:15:00', 'timezone' => 'America/New_York'], + ['rrule' => 'FREQ=SECONDLY;INTERVAL=1;COUNT=999999'], + ['rrule' => 'FREQ=DAILY;UNTIL=not-a-date'], + ['timezone' => '../../UTC', 'date' => 'not-a-date'], + ]; + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + $uri = $route->uri(); + + foreach ($payloads as $payload) { + $response = $this->requestAs($this->actorFor($uri), $method, $uri, $this->payloadFor($method, $uri) + $payload); + + $this->assertControlled($response, $method, $uri); + $this->assertLessThan(500, $response->getStatusCode(), "$method $uri should not crash on calendar edge cases."); + } + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiUnicodeNormalizationAndInputContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiUnicodeNormalizationAndInputContractTest.php new file mode 100644 index 00000000..17b6e35f --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiUnicodeNormalizationAndInputContractTest.php @@ -0,0 +1,54 @@ +apiRoutes(), fn ($route) => in_array($this->primaryMethod($route), ['POST', 'PUT', 'PATCH'], true)), 0, 100); + $text = "Arabic العربية Hebrew עברית emoji 😀 combined e\u{0301} zero-width\u{200B} text"; + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + $uri = $route->uri(); + $response = $this->requestAs($this->actorFor($uri), $method, $uri, $this->payloadFor($method, $uri) + [ + 'name' => $text, + 'title' => $text, + 'description' => $text, + 'notes' => $text, + 'message' => $text, + ]); + + $this->assertControlled($response, $method, $uri); + $this->assertNoServerError($response, 'unicode input at ' . $method . ' ' . $uri); + + if ($response->getContent() !== '') { + $this->assertTrue($this->isJsonString($response->getContent()) || ! str_contains((string) $response->headers->get('Content-Type'), 'json'), $uri . ' must not emit broken JSON for unicode input.'); + } + } + } + + public function test_emails_and_phone_fields_reject_visually_confusable_values_cleanly(): void + { + $routes = $this->apiRoutesContainingAny(['users', 'parents', 'guardian', 'authorized', 'contact', 'support', 'messages', 'whatsapp']); + + foreach (array_slice($routes, 0, 100) as $route) { + $method = $this->primaryMethod($route); + if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) { + continue; + } + + $uri = $route->uri(); + $response = $this->requestAs($this->actorFor($uri), $method, $uri, $this->payloadFor($method, $uri) + [ + 'email' => "admin\u{202E}@example.test", + 'phone' => "+1\u{202E}5551234567", + ]); + + $this->assertControlled($response, $method, $uri); + $this->assertNoServerError($response, 'confusable email/phone at ' . $method . ' ' . $uri); + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiUrlFetchSsrfAndCallbackSafetyContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiUrlFetchSsrfAndCallbackSafetyContractTest.php new file mode 100644 index 00000000..e9554790 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiUrlFetchSsrfAndCallbackSafetyContractTest.php @@ -0,0 +1,43 @@ +apiRoutesContainingAny(['webhook', 'callback', 'import', 'export', 'avatar', 'logo', 'settings', 'notification', 'whatsapp']), 0, 100); + $dangerousUrls = [ + 'http://127.0.0.1:80/admin', + 'http://localhost:3306/', + 'http://169.254.169.254/latest/meta-data/', + 'file:///etc/passwd', + 'gopher://127.0.0.1:6379/_INFO', + ]; + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) { + continue; + } + + foreach ($dangerousUrls as $url) { + $uri = $route->uri(); + $payload = $this->payloadFor($method, $uri) + [ + 'url' => $url, + 'callback_url' => $url, + 'webhook_url' => $url, + 'image_url' => $url, + 'remote_file_url' => $url, + ]; + + $response = $this->requestAs($this->actorFor($uri), $method, $uri, $payload); + + $this->assertControlled($response, $method, $uri); + $this->assertNoServerError($response, 'SSRF URL probe at ' . $method . ' ' . $uri); + } + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiWebhookExternalProviderAndCallbackContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiWebhookExternalProviderAndCallbackContractTest.php new file mode 100644 index 00000000..bc4c8387 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiWebhookExternalProviderAndCallbackContractTest.php @@ -0,0 +1,49 @@ +apiRoutesContainingAny(['webhook', 'callback', 'paypal', 'payment-notification', 'verify-payment']); + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + + if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) { + continue; + } + + $response = $this->json($method, $this->materializePath($route->uri()), [ + 'provider' => 'e2e-fake-provider', + 'event_id' => 'evt_fake', + 'signature' => 'invalid-signature', + 'amount' => 25, + 'invoice_id' => $this->ids['invoiceId'], + ]); + + $this->assertStatusIn($response, [200, 202, 400, 401, 403, 404, 409, 422], "Unsigned external callback {$route->uri()}"); + $this->assertStringNotContainsString('secret', strtolower($response->getContent())); + } + } + + public function test_external_provider_status_routes_never_expose_provider_secrets(): void + { + foreach ($this->apiRoutesContainingAny(['paypal', 'payment', 'transaction', 'provider']) as $route) { + if ($this->primaryMethod($route) !== 'GET') { + continue; + } + + $response = $this->requestAs($this->admin, 'GET', $route->uri()); + $this->assertControlled($response, 'GET', $route->uri()); + + $body = strtolower($response->getContent()); + $this->assertStringNotContainsString('client_secret', $body); + $this->assertStringNotContainsString('webhook_secret', $body); + $this->assertStringNotContainsString('private_key', $body); + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiWebhookReplayAndSignatureDepthContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiWebhookReplayAndSignatureDepthContractTest.php new file mode 100644 index 00000000..0b9cc9f9 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiWebhookReplayAndSignatureDepthContractTest.php @@ -0,0 +1,40 @@ +apiRoutesContainingAny(['webhook', 'callback', 'paypal', 'stripe', 'provider', 'payment-notification']), 0, 80); + + $payload = [ + 'id' => 'evt_e2e_probe', + 'event' => 'payment.completed', + 'amount' => 25.00, + 'invoice_id' => $this->ids['invoiceId'], + ]; + + foreach ($routes as $route) { + $method = $this->primaryMethod($route); + $uri = $route->uri(); + + if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) { + continue; + } + + $missing = $this->json($method, $this->materializePath($uri), $payload); + $bad = $this->withHeaders(['X-Signature' => 'definitely-wrong', 'X-Timestamp' => '0'])->json($method, $this->materializePath($uri), $payload); + $replayed = $this->withHeaders(['X-Signature' => 'replayed', 'X-Webhook-Id' => 'evt_e2e_probe'])->json($method, $this->materializePath($uri), $payload); + + foreach ([$missing, $bad, $replayed] as $response) { + $this->assertStatusIn($response, [200, 202, 204, 400, 401, 403, 404, 405, 409, 419, 422], "$method $uri webhook security response"); + $this->assertLessThan(500, $response->getStatusCode(), "$method $uri webhook should not crash on bad signatures."); + $this->assertStringNotContainsString('secret', strtolower($response->getContent()), "$method $uri must not leak provider secrets."); + } + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/ApiWriteAuditAndTimestampContractTest.php b/tests/Feature/Api/V1/FullSurface/ApiWriteAuditAndTimestampContractTest.php new file mode 100644 index 00000000..3e8d8239 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/ApiWriteAuditAndTimestampContractTest.php @@ -0,0 +1,116 @@ +createRoutes() as $route) { + $method = $this->primaryMethod($route); + $uri = $route->uri(); + $response = $this->requestAs($this->actorFor($uri), $method, $uri, $this->payloadFor($method, $uri)); + + $this->assertControlled($response, $method, $uri); + $this->assertNoServerError($response, "$method $uri create identifier contract"); + + if (in_array($response->getStatusCode(), [200, 201, 202], true) && $this->isJsonString($response->getContent())) { + $json = $response->json(); + $flat = json_encode($json, JSON_THROW_ON_ERROR); + $this->assertTrue( + str_contains($flat, 'id') || str_contains($flat, 'uuid') || str_contains($flat, 'ok') || str_contains($flat, 'status'), + "$method $uri successful create response should expose an identifier, ok/status, or resource envelope." + ); + } + } + } + + public function test_successful_write_responses_do_not_emit_broken_timestamp_fields(): void + { + foreach ($this->timestampAwareWriteRoutes() as $route) { + $method = $this->primaryMethod($route); + $uri = $route->uri(); + $response = $this->requestAs($this->actorFor($uri), $method, $uri, $this->payloadFor($method, $uri)); + + $this->assertControlled($response, $method, $uri); + $this->assertNoServerError($response, "$method $uri timestamp serialization contract"); + + if (in_array($response->getStatusCode(), [200, 201, 202], true) && $this->isJsonString($response->getContent())) { + $flat = json_encode($response->json(), JSON_THROW_ON_ERROR); + + foreach (['created_at', 'updated_at', 'deleted_at'] as $field) { + if (! str_contains($flat, $field)) { + continue; + } + + $this->assertDoesNotMatchRegularExpression( + '/"' . $field . '"\s*:\s*"(?:0000-00-00|Invalid date|NaN)/i', + $flat, + "$method $uri emitted a broken $field timestamp." + ); + } + } + } + } + + public function test_audit_like_routes_are_not_publicly_mutable(): void + { + foreach ($this->auditOrLogRoutes() as $route) { + $method = $this->primaryMethod($route); + $uri = $route->uri(); + $response = $this->json($method, $this->materializePath($uri), $this->payloadFor($method, $uri)); + + $this->assertStatusIn($response, [302, 401, 403, 404, 405, 419, 422], "$method $uri public audit mutation"); + $this->assertNoServerError($response, "$method $uri public audit mutation"); + } + } + + /** @return list */ + private function createRoutes(): array + { + return array_slice(array_filter($this->apiRoutes(), fn (LaravelRoute $route): bool => $this->primaryMethod($route) === 'POST' && ! str_contains($route->uri(), 'login')), 0, 80); + } + + /** @return list */ + private function timestampAwareWriteRoutes(): array + { + return array_slice(array_filter($this->apiRoutes(), fn (LaravelRoute $route): bool => in_array($this->primaryMethod($route), ['POST', 'PUT', 'PATCH'], true)), 0, 50); + } + + /** @return list */ + private function auditOrLogRoutes(): array + { + return array_slice(array_filter($this->apiRoutes(), function (LaravelRoute $route): bool { + return preg_match('/audit|log|history|activity|movement|transaction/i', $route->uri()) === 1 + && in_array($this->primaryMethod($route), ['POST', 'PUT', 'PATCH', 'DELETE'], true); + }), 0, 40); + } + + /** @return list */ + private function candidateTablesFor(string $uri): array + { + return array_values(array_filter([ + str_contains($uri, 'users') ? 'users' : null, + str_contains($uri, 'students') ? 'students' : null, + str_contains($uri, 'attendance') ? 'attendance' : null, + str_contains($uri, 'invoice') ? 'invoices' : null, + str_contains($uri, 'payment') ? 'payments' : null, + str_contains($uri, 'inventory') ? 'inventory_items' : null, + str_contains($uri, 'message') ? 'messages' : null, + str_contains($uri, 'support') ? 'support_tickets' : null, + ])); + } + + private function tableHasColumn(string $table, string $column): bool + { + try { + return DB::getSchemaBuilder()->hasColumn($table, $column); + } catch (\Throwable) { + return false; + } + } +} diff --git a/tests/Feature/Api/V1/FullSurface/Support/FullSurfaceE2EContractCase.php b/tests/Feature/Api/V1/FullSurface/Support/FullSurfaceE2EContractCase.php new file mode 100644 index 00000000..4bfef597 --- /dev/null +++ b/tests/Feature/Api/V1/FullSurface/Support/FullSurfaceE2EContractCase.php @@ -0,0 +1,344 @@ + */ + protected array $ids = []; + + protected function setUp(): void + { + parent::setUp(); + + $this->seedE2EConfiguration(); + $world = $this->seedTeacherClassWithStudent(9701); + + $this->teacher = $world['teacher']; + $this->parent = $world['parent']; + $this->studentUser = $this->createApiUserWithRole('student'); + $this->admin = $this->createApiUserWithRole('administrator'); + + $invoiceId = $this->seedInvoiceForParent((int) $world['parent_id']); + $voucherId = $this->seedDiscountVoucher(); + + $this->ids = [ + 'id' => 1, + 'user' => $this->admin->id, + 'userId' => $this->admin->id, + 'role' => (int) DB::table('roles')->where('name', 'parent')->value('id'), + 'roleId' => (int) DB::table('roles')->where('name', 'parent')->value('id'), + 'permission' => 1, + 'permissionId' => 1, + 'student' => (int) $world['student_id'], + 'studentId' => (int) $world['student_id'], + 'parent' => (int) $world['parent_id'], + 'parentId' => (int) $world['parent_id'], + 'teacher' => $this->teacher->id, + 'teacherId' => $this->teacher->id, + 'classSection' => (int) $world['class_section_id'], + 'classSectionId' => (int) $world['class_section_id'], + 'class_section_id' => (int) $world['class_section_id'], + 'invoice' => $invoiceId, + 'invoiceId' => $invoiceId, + 'payment' => 1, + 'paymentId' => 1, + 'refund' => 1, + 'refundId' => 1, + 'voucher' => $voucherId, + 'voucherId' => $voucherId, + 'schoolYear' => (int) DB::table('school_years')->where('name', self::E2E_SCHOOL_YEAR)->value('id'), + 'schoolYearId' => (int) DB::table('school_years')->where('name', self::E2E_SCHOOL_YEAR)->value('id'), + 'promotion' => 1, + 'promotionId' => 1, + 'batch' => 1, + 'batchId' => 1, + 'contact' => 1, + 'contactId' => 1, + 'family' => 1, + 'familyId' => 1, + 'authorizedUser' => 1, + 'authorizedUserId' => 1, + 'guardian' => 1, + 'guardianId' => 1, + 'eventCharge' => 1, + 'eventChargeId' => 1, + 'plan' => 1, + 'planId' => 1, + 'carryforward' => 1, + 'carryforwardId' => 1, + 'installment' => 1, + 'installmentId' => 1, + 'category' => 1, + 'categoryId' => 1, + 'item' => 1, + 'itemId' => 1, + 'supplier' => 1, + 'supplierId' => 1, + 'request' => 1, + 'requestId' => 1, + 'message' => 1, + 'messageId' => 1, + 'ticket' => 1, + 'ticketId' => 1, + 'certificateNumber' => 'CERT-E2E-001', + 'token' => 'invalid-token-for-contract-probe', + 'filename' => 'placeholder.txt', + 'name' => 'placeholder.txt', + 'mode' => 'view', + ]; + } + + /** @return list */ + protected function apiRoutes(?string $contains = null): array + { + return array_values(array_filter(Route::getRoutes()->getRoutes(), function (LaravelRoute $route) use ($contains): bool { + $uri = $route->uri(); + + if (! str_starts_with($uri, 'api/')) { + return false; + } + + if ($contains !== null && ! str_contains($uri, $contains)) { + return false; + } + + return ! in_array($this->primaryMethod($route), ['HEAD', 'OPTIONS'], true); + })); + } + + /** @param list $needles @return list */ + protected function apiRoutesContainingAny(array $needles): array + { + return array_values(array_filter($this->apiRoutes(), function (LaravelRoute $route) use ($needles): bool { + foreach ($needles as $needle) { + if (str_contains($route->uri(), $needle)) { + return true; + } + } + + return false; + })); + } + + protected function primaryMethod(LaravelRoute $route): string + { + foreach ($route->methods() as $method) { + if (! in_array($method, ['HEAD', 'OPTIONS'], true)) { + return $method; + } + } + + return 'GET'; + } + + protected function materializeUri(string $uri): string + { + return preg_replace_callback('/\{([^}:]+)(?:\?)?[^}]*\}/', function (array $matches): string { + return (string) ($this->ids[$matches[1]] ?? $this->ids['id']); + }, $uri) ?? $uri; + } + + protected function materializePath(string $uri): string + { + return '/' . ltrim($this->materializeUri($uri), '/'); + } + + protected function actorFor(string $uri): User + { + if (str_contains($uri, '/parents') || str_contains($uri, '/parent') || str_contains($uri, 'family') || str_contains($uri, 'guardian')) { + return $this->parent; + } + + if (str_contains($uri, '/teacher') || str_contains($uri, '/teachers') || str_contains($uri, 'class-progress')) { + return $this->teacher; + } + + if (str_contains($uri, '/student/dashboard') || str_contains($uri, '/students/dashboard')) { + return $this->studentUser; + } + + return $this->admin; + } + + protected function requestAs(User $actor, string $method, string $uri, array $payload = []): TestResponse + { + $this->actingAs($actor, 'api'); + + return $this->json($method, $this->materializePath($uri), $payload); + } + + protected function probeRoute(LaravelRoute $route, ?array $payload = null): TestResponse + { + $method = $this->primaryMethod($route); + $uri = $route->uri(); + + return $this->requestAs($this->actorFor($uri), $method, $uri, $payload ?? $this->payloadFor($method, $uri)); + } + + /** @return array */ + protected function payloadFor(string $method, string $uri): array + { + if (in_array($method, ['GET', 'DELETE'], true)) { + return []; + } + + $base = [ + 'school_year' => self::E2E_SCHOOL_YEAR, + 'semester' => self::E2E_SEMESTER, + 'date' => '2025-10-05', + 'notes' => 'E2E deep contract probe', + ]; + + if (str_contains($uri, 'auth/login') || preg_match('#api/(?:v1/)?login$#', $uri)) { + return ['email' => $this->admin->email, 'password' => 'password']; + } + + if (str_contains($uri, 'users')) { + return $this->validUserPayload((int) $this->ids['roleId']); + } + + if (str_contains($uri, 'students')) { + return $base + [ + 'firstname' => 'Deep', + 'lastname' => 'Student', + 'dob' => '2015-01-01', + 'gender' => 'Male', + 'parent_id' => $this->ids['parentId'], + 'student_id' => $this->ids['studentId'], + 'class_section_id' => [$this->ids['classSectionId']], + ]; + } + + if (str_contains($uri, 'attendance')) { + return $base + [ + 'student_id' => $this->ids['studentId'], + 'class_section_id' => $this->ids['classSectionId'], + 'status' => 'present', + 'attendance' => [$this->ids['studentId'] => 'present'], + 'arrival_time' => '10:05', + ]; + } + + if (str_contains($uri, 'scores') || str_contains($uri, 'grading') || str_contains($uri, 'homework') || str_contains($uri, 'quiz')) { + return $base + [ + 'student_id' => $this->ids['studentId'], + 'class_section_id' => $this->ids['classSectionId'], + 'score' => 95, + 'points' => 10, + 'column' => 'Deep Contract Probe', + 'comment' => 'Solid work.', + ]; + } + + if (str_contains($uri, 'finance') || str_contains($uri, 'invoice') || str_contains($uri, 'payment') || str_contains($uri, 'refund')) { + return $base + [ + 'parent_id' => $this->ids['parentId'], + 'student_id' => $this->ids['studentId'], + 'invoice_id' => $this->ids['invoiceId'], + 'amount' => 25.00, + 'payment_method' => 'cash', + 'reason' => 'E2E deep contract probe', + ]; + } + + if (str_contains($uri, 'inventory') || str_contains($uri, 'suppliers') || str_contains($uri, 'supply')) { + return $base + [ + 'name' => 'Deep Contract Probe Item', + 'sku' => 'E2E-DEEP-' . uniqid(), + 'quantity' => 3, + 'unit_cost' => 5.25, + ]; + } + + if (str_contains($uri, 'messages') || str_contains($uri, 'support') || str_contains($uri, 'contact') || str_contains($uri, 'email') || str_contains($uri, 'whatsapp')) { + return $base + [ + 'subject' => 'Deep Contract Probe', + 'message' => 'This is an automated E2E deep contract probe.', + 'body' => 'This is an automated E2E deep contract probe.', + 'email' => 'deep.contract@example.test', + 'recipient_id' => $this->teacher->id, + 'phone' => '+15551234567', + ]; + } + + if (str_contains($uri, 'preferences') || str_contains($uri, 'settings') || str_contains($uri, 'configuration')) { + return $base + [ + 'key' => 'deep_contract_probe', + 'value' => 'enabled', + 'config_key' => 'deep_contract_probe', + 'config_value' => 'enabled', + ]; + } + + if (str_contains($uri, 'families') || str_contains($uri, 'authorized') || str_contains($uri, 'guardian')) { + return $base + [ + 'parent_id' => $this->ids['parentId'], + 'student_id' => $this->ids['studentId'], + 'email' => 'guardian.deep@example.test', + 'firstname' => 'Guardian', + 'lastname' => 'Probe', + 'relationship' => 'Uncle', + ]; + } + + return $base + [ + 'name' => 'Deep Contract Probe', + 'title' => 'Deep Contract Probe', + 'description' => 'E2E deep full-surface contract probe', + 'status' => 'active', + ]; + } + + /** @return list */ + protected function controlledStatuses(string $method): array + { + return match ($method) { + 'POST' => [200, 201, 202, 204, 302, 400, 401, 403, 404, 405, 409, 419, 422], + 'PUT', 'PATCH' => [200, 202, 204, 302, 400, 401, 403, 404, 405, 409, 419, 422], + 'DELETE' => [200, 202, 204, 302, 400, 401, 403, 404, 405, 409, 419, 422], + default => [200, 204, 302, 304, 400, 401, 403, 404, 405, 409, 419, 422], + }; + } + + protected function assertControlled(TestResponse $response, string $method, string $uri): void + { + $this->assertStatusIn($response, $this->controlledStatuses($method), "$method $uri"); + } + + protected function assertJsonResponseWhenSuccessful(TestResponse $response, string $context): void + { + if ($response->getStatusCode() >= 200 && $response->getStatusCode() < 300 && $response->getContent() !== '') { + $this->assertTrue( + str_contains((string) $response->headers->get('Content-Type'), 'json') || $this->isJsonString($response->getContent()), + $context . ' should return JSON for successful API responses. Body: ' . $response->getContent() + ); + } + } + + protected function isJsonString(string $content): bool + { + json_decode($content, true); + + return json_last_error() === JSON_ERROR_NONE; + } +} diff --git a/tests/Feature/Api/V1/Incidents/IncidentControllerTest.php b/tests/Feature/Api/V1/Incidents/IncidentControllerTest.php index 9289acbb..9f001c79 100644 --- a/tests/Feature/Api/V1/Incidents/IncidentControllerTest.php +++ b/tests/Feature/Api/V1/Incidents/IncidentControllerTest.php @@ -112,7 +112,7 @@ class IncidentControllerTest extends TestCase $storeResponse->assertJson(['ok' => true, 'created' => true]); $currentId = $storeResponse->json('incident_id'); - $closeResponse = $this->postJson('/api/v1/incidents/'.$currentId.'/close', [ + $closeResponse = $this->postJson('/api/v1/incidents/' . $currentId . '/close', [ 'state_description' => 'Resolved', 'action_taken' => 'Spoke with student', ]); diff --git a/tests/Feature/Api/V1/Inventory/InventoryCategoriesControllerTest.php b/tests/Feature/Api/V1/Inventory/InventoryCategoriesControllerTest.php index 3c1b893e..f36cdb71 100644 --- a/tests/Feature/Api/V1/Inventory/InventoryCategoriesControllerTest.php +++ b/tests/Feature/Api/V1/Inventory/InventoryCategoriesControllerTest.php @@ -45,7 +45,7 @@ class InventoryCategoriesControllerTest extends TestCase $categoryId = $this->seedCategory('office'); Sanctum::actingAs($user); - $response = $this->patchJson('/api/v1/inventory/categories/'.$categoryId, [ + $response = $this->patchJson('/api/v1/inventory/categories/' . $categoryId, [ 'name' => 'Updated', ]); @@ -59,7 +59,7 @@ class InventoryCategoriesControllerTest extends TestCase $categoryId = $this->seedCategory('office'); Sanctum::actingAs($user); - $response = $this->deleteJson('/api/v1/inventory/categories/'.$categoryId); + $response = $this->deleteJson('/api/v1/inventory/categories/' . $categoryId); $response->assertOk(); $this->assertDatabaseMissing('inventory_categories', ['id' => $categoryId]); @@ -101,7 +101,7 @@ class InventoryCategoriesControllerTest extends TestCase { return DB::table('inventory_categories')->insertGetId([ 'type' => $type, - 'name' => ucfirst($type).' Category', + 'name' => ucfirst($type) . ' Category', 'created_at' => now(), 'updated_at' => now(), ]); diff --git a/tests/Feature/Api/V1/Inventory/InventoryItemsControllerTest.php b/tests/Feature/Api/V1/Inventory/InventoryItemsControllerTest.php index c393ba49..4250285a 100644 --- a/tests/Feature/Api/V1/Inventory/InventoryItemsControllerTest.php +++ b/tests/Feature/Api/V1/Inventory/InventoryItemsControllerTest.php @@ -35,7 +35,7 @@ class InventoryItemsControllerTest extends TestCase $itemId = $this->seedInventoryItem($categoryId); Sanctum::actingAs($user); - $response = $this->getJson('/api/v1/inventory/items/'.$itemId); + $response = $this->getJson('/api/v1/inventory/items/' . $itemId); $response->assertOk(); $response->assertJsonPath('status', true); @@ -70,7 +70,7 @@ class InventoryItemsControllerTest extends TestCase $itemId = $this->seedInventoryItem($categoryId); Sanctum::actingAs($user); - $response = $this->patchJson('/api/v1/inventory/items/'.$itemId, [ + $response = $this->patchJson('/api/v1/inventory/items/' . $itemId, [ 'name' => 'Updated Item', ]); @@ -86,7 +86,7 @@ class InventoryItemsControllerTest extends TestCase $itemId = $this->seedInventoryItem($categoryId); Sanctum::actingAs($user); - $response = $this->deleteJson('/api/v1/inventory/items/'.$itemId); + $response = $this->deleteJson('/api/v1/inventory/items/' . $itemId); $response->assertOk(); $this->assertDatabaseMissing('inventory_items', ['id' => $itemId]); @@ -100,7 +100,7 @@ class InventoryItemsControllerTest extends TestCase $itemId = $this->seedInventoryItem($categoryId); Sanctum::actingAs($user); - $response = $this->postJson('/api/v1/inventory/items/'.$itemId.'/audit', [ + $response = $this->postJson('/api/v1/inventory/items/' . $itemId . '/audit', [ 'needs_repair_qty' => 1, 'need_replace_qty' => 1, 'cannot_find_qty' => 0, @@ -118,7 +118,7 @@ class InventoryItemsControllerTest extends TestCase $itemId = $this->seedInventoryItem($categoryId, 0); Sanctum::actingAs($user); - $response = $this->postJson('/api/v1/inventory/items/'.$itemId.'/adjust', [ + $response = $this->postJson('/api/v1/inventory/items/' . $itemId . '/adjust', [ 'mode' => 'in', 'quantity' => 2, 'reason' => 'Restock', @@ -170,7 +170,7 @@ class InventoryItemsControllerTest extends TestCase $bookId = $this->seedInventoryItem($categoryId, 5, 'book'); Sanctum::actingAs($user); - $formResponse = $this->getJson('/api/v1/inventory/teacher-distribution?class_section_id='.$classSectionId.'&item_id='.$bookId); + $formResponse = $this->getJson('/api/v1/inventory/teacher-distribution?class_section_id=' . $classSectionId . '&item_id=' . $bookId); $formResponse->assertOk(); $storeResponse = $this->postJson('/api/v1/inventory/teacher-distribution', [ @@ -233,7 +233,7 @@ class InventoryItemsControllerTest extends TestCase { return DB::table('inventory_categories')->insertGetId([ 'type' => $type, - 'name' => ucfirst($type).' Category', + 'name' => ucfirst($type) . ' Category', 'created_at' => now(), 'updated_at' => now(), ]); @@ -244,7 +244,7 @@ class InventoryItemsControllerTest extends TestCase return DB::table('inventory_items')->insertGetId([ 'type' => $type, 'category_id' => $categoryId, - 'name' => 'Item '.$type, + 'name' => 'Item ' . $type, 'quantity' => $quantity, 'semester' => 'Fall', 'school_year' => '2025-2026', diff --git a/tests/Feature/Api/V1/Inventory/InventoryMovementsControllerTest.php b/tests/Feature/Api/V1/Inventory/InventoryMovementsControllerTest.php index eefe70e2..6989113b 100644 --- a/tests/Feature/Api/V1/Inventory/InventoryMovementsControllerTest.php +++ b/tests/Feature/Api/V1/Inventory/InventoryMovementsControllerTest.php @@ -49,7 +49,7 @@ class InventoryMovementsControllerTest extends TestCase $movementId = $this->seedMovement($itemId); Sanctum::actingAs($user); - $response = $this->patchJson('/api/v1/inventory/movements/'.$movementId, [ + $response = $this->patchJson('/api/v1/inventory/movements/' . $movementId, [ 'qty_change' => 3, 'movement_type' => 'in', ]); @@ -65,7 +65,7 @@ class InventoryMovementsControllerTest extends TestCase $movementId = $this->seedMovement($itemId); Sanctum::actingAs($user); - $response = $this->deleteJson('/api/v1/inventory/movements/'.$movementId); + $response = $this->deleteJson('/api/v1/inventory/movements/' . $movementId); $response->assertOk(); $this->assertDatabaseMissing('inventory_movements', ['id' => $movementId]); diff --git a/tests/Feature/Api/V1/Inventory/SuppliersControllerTest.php b/tests/Feature/Api/V1/Inventory/SuppliersControllerTest.php index 843c3c22..4e926aad 100644 --- a/tests/Feature/Api/V1/Inventory/SuppliersControllerTest.php +++ b/tests/Feature/Api/V1/Inventory/SuppliersControllerTest.php @@ -45,7 +45,7 @@ class SuppliersControllerTest extends TestCase $supplierId = $this->seedSupplier('Supplier C'); Sanctum::actingAs($user); - $response = $this->patchJson('/api/v1/inventory/suppliers/'.$supplierId, [ + $response = $this->patchJson('/api/v1/inventory/suppliers/' . $supplierId, [ 'name' => 'Supplier Updated', ]); @@ -59,7 +59,7 @@ class SuppliersControllerTest extends TestCase $supplierId = $this->seedSupplier('Supplier D'); Sanctum::actingAs($user); - $response = $this->deleteJson('/api/v1/inventory/suppliers/'.$supplierId); + $response = $this->deleteJson('/api/v1/inventory/suppliers/' . $supplierId); $response->assertOk(); $this->assertDatabaseMissing('suppliers', ['id' => $supplierId]); diff --git a/tests/Feature/Api/V1/Inventory/SupplyCategoriesControllerTest.php b/tests/Feature/Api/V1/Inventory/SupplyCategoriesControllerTest.php index 3af44bbc..aec624fa 100644 --- a/tests/Feature/Api/V1/Inventory/SupplyCategoriesControllerTest.php +++ b/tests/Feature/Api/V1/Inventory/SupplyCategoriesControllerTest.php @@ -44,7 +44,7 @@ class SupplyCategoriesControllerTest extends TestCase $categoryId = $this->seedCategory('Pens'); Sanctum::actingAs($user); - $response = $this->patchJson('/api/v1/inventory/supply-categories/'.$categoryId, [ + $response = $this->patchJson('/api/v1/inventory/supply-categories/' . $categoryId, [ 'name' => 'Pens Updated', ]); @@ -58,7 +58,7 @@ class SupplyCategoriesControllerTest extends TestCase $categoryId = $this->seedCategory('Markers'); Sanctum::actingAs($user); - $response = $this->deleteJson('/api/v1/inventory/supply-categories/'.$categoryId); + $response = $this->deleteJson('/api/v1/inventory/supply-categories/' . $categoryId); $response->assertOk(); $this->assertDatabaseMissing('supply_categories', ['id' => $categoryId]); diff --git a/tests/Feature/Api/V1/Messaging/MessagesControllerTest.php b/tests/Feature/Api/V1/Messaging/MessagesControllerTest.php index 167cca7e..53c7065d 100644 --- a/tests/Feature/Api/V1/Messaging/MessagesControllerTest.php +++ b/tests/Feature/Api/V1/Messaging/MessagesControllerTest.php @@ -68,7 +68,7 @@ class MessagesControllerTest extends TestCase Sanctum::actingAs($recipient); - $response = $this->getJson('/api/v1/messages/'.$message->id); + $response = $this->getJson('/api/v1/messages/' . $message->id); $response->assertOk(); $this->assertDatabaseHas('messages', [ @@ -98,7 +98,7 @@ class MessagesControllerTest extends TestCase Sanctum::actingAs($sender); - $response = $this->patchJson('/api/v1/messages/'.$message->id, [ + $response = $this->patchJson('/api/v1/messages/' . $message->id, [ 'status' => 'trashed', ]); @@ -130,7 +130,7 @@ class MessagesControllerTest extends TestCase Sanctum::actingAs($sender); - $response = $this->deleteJson('/api/v1/messages/'.$message->id); + $response = $this->deleteJson('/api/v1/messages/' . $message->id); $response->assertOk(); $this->assertDatabaseHas('messages', [ @@ -197,7 +197,7 @@ class MessagesControllerTest extends TestCase Sanctum::actingAs($other); - $response = $this->getJson('/api/v1/messages/'.$message->id); + $response = $this->getJson('/api/v1/messages/' . $message->id); $response->assertForbidden(); } diff --git a/tests/Feature/Api/V1/Parents/AuthorizedUsersControllerTest.php b/tests/Feature/Api/V1/Parents/AuthorizedUsersControllerTest.php index a7531113..fcdb2cb7 100644 --- a/tests/Feature/Api/V1/Parents/AuthorizedUsersControllerTest.php +++ b/tests/Feature/Api/V1/Parents/AuthorizedUsersControllerTest.php @@ -18,7 +18,7 @@ class AuthorizedUsersControllerTest extends TestCase 'user_id' => $parentId, 'firstname' => 'Aunt', 'lastname' => 'Carer', - 'email' => 'carer-'.uniqid().'@example.test', + 'email' => 'carer-' . uniqid() . '@example.test', 'phone_number' => '5551112222', 'relation_to_user' => 'Aunt', 'status' => 'pending', @@ -74,7 +74,7 @@ class AuthorizedUsersControllerTest extends TestCase $foreign = $this->makeAuthorizedUser($otherParent->id); $this->actingAs($parent, 'api') - ->getJson('/api/v1/parents/authorized-users/'.$foreign->id) + ->getJson('/api/v1/parents/authorized-users/' . $foreign->id) ->assertNotFound(); } @@ -84,7 +84,7 @@ class AuthorizedUsersControllerTest extends TestCase $record = $this->makeAuthorizedUser($parent->id); $this->actingAs($parent, 'api') - ->deleteJson('/api/v1/parents/authorized-users/'.$record->id) + ->deleteJson('/api/v1/parents/authorized-users/' . $record->id) ->assertOk() ->assertJsonPath('status', true); @@ -98,7 +98,7 @@ class AuthorizedUsersControllerTest extends TestCase $foreign = $this->makeAuthorizedUser($otherParent->id); $this->actingAs($parent, 'api') - ->deleteJson('/api/v1/parents/authorized-users/'.$foreign->id) + ->deleteJson('/api/v1/parents/authorized-users/' . $foreign->id) ->assertNotFound(); $this->assertDatabaseHas('authorized_users', ['id' => $foreign->id]); diff --git a/tests/Feature/Api/V1/Parents/ParentAttendanceReportControllerTest.php b/tests/Feature/Api/V1/Parents/ParentAttendanceReportControllerTest.php index 10b9ffe0..f5a77139 100644 --- a/tests/Feature/Api/V1/Parents/ParentAttendanceReportControllerTest.php +++ b/tests/Feature/Api/V1/Parents/ParentAttendanceReportControllerTest.php @@ -106,7 +106,7 @@ class ParentAttendanceReportControllerTest extends TestCase ]); Sanctum::actingAs($parent); - $response = $this->patchJson('/api/v1/parents/attendance-reports/'.$reportId, [ + $response = $this->patchJson('/api/v1/parents/attendance-reports/' . $reportId, [ 'reason' => 'Family emergency', ]); @@ -193,7 +193,6 @@ class ParentAttendanceReportControllerTest extends TestCase if ((int) $dt->format('w') !== 0) { $dt->modify('next sunday'); } - return $dt->format('Y-m-d'); } } diff --git a/tests/Feature/Api/V1/PrintRequests/PrintRequestsWorkflowTest.php b/tests/Feature/Api/V1/PrintRequests/PrintRequestsWorkflowTest.php index 60a29d77..370ff8e7 100644 --- a/tests/Feature/Api/V1/PrintRequests/PrintRequestsWorkflowTest.php +++ b/tests/Feature/Api/V1/PrintRequests/PrintRequestsWorkflowTest.php @@ -208,7 +208,7 @@ class PrintRequestsWorkflowTest extends TestCase 'status' => 'Active', 'is_verified' => 1, 'is_suspended' => 0, - 'email' => $roleName.'-print-request-'.str_replace('.', '', uniqid('', true)).'@example.test', + 'email' => $roleName . '-print-request-' . str_replace('.', '', uniqid('', true)) . '@example.test', ]); $user->roles()->syncWithoutDetaching([$role->id]); @@ -230,7 +230,7 @@ class PrintRequestsWorkflowTest extends TestCase } /** - * @param array $overrides + * @param array $overrides */ private function seedPrintRequest(User $teacher, array $overrides = []): PrintRequest { diff --git a/tests/Feature/Api/V1/Promotions/ParentPromotionControllerTest.php b/tests/Feature/Api/V1/Promotions/ParentPromotionControllerTest.php index 2f1dc1fd..6fbf199b 100644 --- a/tests/Feature/Api/V1/Promotions/ParentPromotionControllerTest.php +++ b/tests/Feature/Api/V1/Promotions/ParentPromotionControllerTest.php @@ -37,12 +37,12 @@ class ParentPromotionControllerTest extends TestCase Sanctum::actingAs($parent); // Start enrollment - $start = $this->postJson('/api/v1/parents/promotions/'.$promotionId.'/start'); + $start = $this->postJson('/api/v1/parents/promotions/' . $promotionId . '/start'); $start->assertOk(); $start->assertJsonPath('data.promotion_status', StudentPromotionRecord::STATUS_ENROLLMENT_STARTED); // Complete the checklist incrementally - $update = $this->patchJson('/api/v1/parents/promotions/'.$promotionId.'/steps', [ + $update = $this->patchJson('/api/v1/parents/promotions/' . $promotionId . '/steps', [ 'info_confirmed' => true, 'documents_uploaded' => true, 'agreement_accepted' => true, @@ -51,7 +51,7 @@ class ParentPromotionControllerTest extends TestCase $update->assertJsonPath('data.promotion_status', StudentPromotionRecord::STATUS_ENROLLMENT_STARTED); // Final step finalises promotion - $finish = $this->patchJson('/api/v1/parents/promotions/'.$promotionId.'/steps', [ + $finish = $this->patchJson('/api/v1/parents/promotions/' . $promotionId . '/steps', [ 'payment_completed' => true, ]); $finish->assertOk(); @@ -72,7 +72,7 @@ class ParentPromotionControllerTest extends TestCase $promotionId = $this->seedPromotionRecord($studentId, $parentB->id); Sanctum::actingAs($parentA); - $response = $this->getJson('/api/v1/parents/promotions/'.$promotionId); + $response = $this->getJson('/api/v1/parents/promotions/' . $promotionId); $response->assertStatus(403); } @@ -140,7 +140,6 @@ class ParentPromotionControllerTest extends TestCase 'promoted_level_name' => 'Grade 4', 'enrollment_deadline' => now()->addDays(30)->toDateString(), ]); - return (int) $record->getKey(); } } diff --git a/tests/Feature/Api/V1/Public/PublicWinnersControllerTest.php b/tests/Feature/Api/V1/Public/PublicWinnersControllerTest.php index f923bf9a..8503ec7e 100644 --- a/tests/Feature/Api/V1/Public/PublicWinnersControllerTest.php +++ b/tests/Feature/Api/V1/Public/PublicWinnersControllerTest.php @@ -53,7 +53,7 @@ class PublicWinnersControllerTest extends TestCase 'published_at' => now(), ]); - $this->getJson('/api/winners/competitions/'.$competition->id) + $this->getJson('/api/winners/competitions/' . $competition->id) ->assertOk() ->assertJsonPath('status', true) ->assertJsonPath('data.competition.id', $competition->id) @@ -75,7 +75,7 @@ class PublicWinnersControllerTest extends TestCase 'is_published' => 0, ]); - $this->getJson('/api/winners/competitions/'.$competition->id) + $this->getJson('/api/winners/competitions/' . $competition->id) ->assertNotFound() ->assertJsonPath('status', false); } diff --git a/tests/Feature/Api/V1/Reports/FilesControllerTest.php b/tests/Feature/Api/V1/Reports/FilesControllerTest.php index 6c9edda9..978613d2 100644 --- a/tests/Feature/Api/V1/Reports/FilesControllerTest.php +++ b/tests/Feature/Api/V1/Reports/FilesControllerTest.php @@ -18,10 +18,10 @@ class FilesControllerTest extends TestCase Sanctum::actingAs($user); $dir = storage_path('uploads/receipts'); - if (! is_dir($dir)) { + if (!is_dir($dir)) { mkdir($dir, 0777, true); } - $path = $dir.DIRECTORY_SEPARATOR.'sample.pdf'; + $path = $dir . DIRECTORY_SEPARATOR . 'sample.pdf'; file_put_contents($path, 'PDFDATA'); $response = $this->getJson('/api/v1/files/receipts/sample.pdf?meta=1'); @@ -60,10 +60,10 @@ class FilesControllerTest extends TestCase ]); $dir = storage_path('uploads/exams/drafts'); - if (! is_dir($dir)) { + if (!is_dir($dir)) { mkdir($dir, 0777, true); } - $path = $dir.DIRECTORY_SEPARATOR.'draft1.pdf'; + $path = $dir . DIRECTORY_SEPARATOR . 'draft1.pdf'; file_put_contents($path, 'PDFDATA'); $response = $this->get('/api/v1/files/exams/teacher/draft1.pdf'); diff --git a/tests/Feature/Api/V1/Reports/ReportCardsControllerTest.php b/tests/Feature/Api/V1/Reports/ReportCardsControllerTest.php index c6408f00..27ba6b49 100644 --- a/tests/Feature/Api/V1/Reports/ReportCardsControllerTest.php +++ b/tests/Feature/Api/V1/Reports/ReportCardsControllerTest.php @@ -37,7 +37,7 @@ class ReportCardsControllerTest extends TestCase $data = $this->seedReportCardData($user->id); Sanctum::actingAs($user); - $response = $this->getJson('/api/v1/reports/report-cards/completeness?class_section_id='.$data['class_section_id'].'&school_year=2025-2026&semester=Fall'); + $response = $this->getJson('/api/v1/reports/report-cards/completeness?class_section_id=' . $data['class_section_id'] . '&school_year=2025-2026&semester=Fall'); $response->assertOk(); $response->assertJsonPath('status', true); @@ -53,7 +53,7 @@ class ReportCardsControllerTest extends TestCase $this->seedAcknowledgement($data['student_id']); Sanctum::actingAs($user); - $response = $this->getJson('/api/v1/reports/report-cards/acknowledgement?student_id='.$data['student_id'].'&school_year=2025-2026&semester=Fall'); + $response = $this->getJson('/api/v1/reports/report-cards/acknowledgement?student_id=' . $data['student_id'] . '&school_year=2025-2026&semester=Fall'); $response->assertOk(); $response->assertJsonPath('status', true); @@ -67,7 +67,7 @@ class ReportCardsControllerTest extends TestCase $data = $this->seedReportCardData($user->id); Sanctum::actingAs($user); - $response = $this->getJson('/api/v1/reports/report-cards/students/'.$data['student_id'].'?school_year=2025-2026&semester=Fall'); + $response = $this->getJson('/api/v1/reports/report-cards/students/' . $data['student_id'] . '?school_year=2025-2026&semester=Fall'); $response->assertOk(); $this->assertSame('application/pdf', $response->headers->get('content-type')); @@ -80,7 +80,7 @@ class ReportCardsControllerTest extends TestCase $data = $this->seedReportCardData($user->id); Sanctum::actingAs($user); - $response = $this->getJson('/api/v1/reports/report-cards/classes/'.$data['class_section_id'].'?school_year=2025-2026&semester=Fall'); + $response = $this->getJson('/api/v1/reports/report-cards/classes/' . $data['class_section_id'] . '?school_year=2025-2026&semester=Fall'); $response->assertOk(); $this->assertSame('application/pdf', $response->headers->get('content-type')); @@ -93,7 +93,7 @@ class ReportCardsControllerTest extends TestCase $data = $this->seedStudentOnly(); Sanctum::actingAs($user); - $response = $this->getJson('/api/v1/reports/report-cards/students/'.$data['student_id'].'?school_year=2025-2026&semester=Fall'); + $response = $this->getJson('/api/v1/reports/report-cards/students/' . $data['student_id'] . '?school_year=2025-2026&semester=Fall'); $response->assertStatus(422); $response->assertJsonPath('status', false); diff --git a/tests/Feature/Api/V1/Roles/RolePermissionControllerTest.php b/tests/Feature/Api/V1/Roles/RolePermissionControllerTest.php index 8d15e0c7..e38c0f0e 100644 --- a/tests/Feature/Api/V1/Roles/RolePermissionControllerTest.php +++ b/tests/Feature/Api/V1/Roles/RolePermissionControllerTest.php @@ -3,7 +3,6 @@ namespace Tests\Feature\Api\V1\Roles; use App\Models\User; -use App\Services\Roles\RoleCrudService; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\DB; use Laravel\Sanctum\Sanctum; @@ -50,7 +49,7 @@ class RolePermissionControllerTest extends TestCase $roleId = $this->seedRole(); Sanctum::actingAs($user); - $response = $this->patchJson('/api/v1/role-permissions/roles/'.$roleId, [ + $response = $this->patchJson('/api/v1/role-permissions/roles/' . $roleId, [ 'description' => 'updated', ]); @@ -64,7 +63,7 @@ class RolePermissionControllerTest extends TestCase $roleId = $this->seedRole(); Sanctum::actingAs($user); - $response = $this->deleteJson('/api/v1/role-permissions/roles/'.$roleId); + $response = $this->deleteJson('/api/v1/role-permissions/roles/' . $roleId); $response->assertOk(); $this->assertDatabaseMissing('roles', ['id' => $roleId]); @@ -93,7 +92,7 @@ class RolePermissionControllerTest extends TestCase $roleId = $this->seedRole(); Sanctum::actingAs($user); - $response = $this->postJson('/api/v1/role-permissions/users/'.$user->id.'/roles', [ + $response = $this->postJson('/api/v1/role-permissions/users/' . $user->id . '/roles', [ 'role_ids' => [$roleId], ]); @@ -119,15 +118,15 @@ class RolePermissionControllerTest extends TestCase $create->assertStatus(201); $permissionId = (int) $create->json('data.permission.id'); - $show = $this->getJson('/api/v1/role-permissions/permissions/'.$permissionId); + $show = $this->getJson('/api/v1/role-permissions/permissions/' . $permissionId); $show->assertOk(); - $update = $this->patchJson('/api/v1/role-permissions/permissions/'.$permissionId, [ + $update = $this->patchJson('/api/v1/role-permissions/permissions/' . $permissionId, [ 'description' => 'Updated', ]); $update->assertOk(); - $delete = $this->deleteJson('/api/v1/role-permissions/permissions/'.$permissionId); + $delete = $this->deleteJson('/api/v1/role-permissions/permissions/' . $permissionId); $delete->assertOk(); $this->assertDatabaseMissing('permissions', ['id' => $permissionId]); } @@ -139,7 +138,7 @@ class RolePermissionControllerTest extends TestCase $permissionId = $this->seedPermission(); Sanctum::actingAs($user); - $response = $this->putJson('/api/v1/role-permissions/roles/'.$roleId.'/permissions', [ + $response = $this->putJson('/api/v1/role-permissions/roles/' . $roleId . '/permissions', [ 'permissions' => [ $permissionId => ['create' => true, 'read' => true], ], @@ -171,9 +170,8 @@ class RolePermissionControllerTest extends TestCase $user = $this->seedUser(); Sanctum::actingAs($user); - $this->app->bind(RoleCrudService::class, function () { - return new class - { + $this->app->bind(\App\Services\Roles\RoleCrudService::class, function () { + return new class { public function create(array $payload) { throw new \RuntimeException('fail'); diff --git a/tests/Feature/Api/V1/Roles/RoleSwitcherControllerTest.php b/tests/Feature/Api/V1/Roles/RoleSwitcherControllerTest.php index 677dff29..b8afee8c 100644 --- a/tests/Feature/Api/V1/Roles/RoleSwitcherControllerTest.php +++ b/tests/Feature/Api/V1/Roles/RoleSwitcherControllerTest.php @@ -58,6 +58,7 @@ class RoleSwitcherControllerTest extends TestCase $response->assertStatus(401); } + public function test_switch_rejects_role_that_user_does_not_have(): void { $user = $this->seedUser(); diff --git a/tests/Feature/Api/V1/Settings/PreferencesControllerTest.php b/tests/Feature/Api/V1/Settings/PreferencesControllerTest.php index feb345d9..7c51b8d2 100644 --- a/tests/Feature/Api/V1/Settings/PreferencesControllerTest.php +++ b/tests/Feature/Api/V1/Settings/PreferencesControllerTest.php @@ -97,7 +97,7 @@ class PreferencesControllerTest extends TestCase $user = $this->createUser('teacher'); Sanctum::actingAs($user); - $response = $this->deleteJson('/api/v1/preferences/'.$user->id); + $response = $this->deleteJson('/api/v1/preferences/' . $user->id); $response->assertForbidden(); } @@ -113,7 +113,7 @@ class PreferencesControllerTest extends TestCase 'notification_sms' => 1, ]); - $response = $this->deleteJson('/api/v1/preferences/'.$admin->id); + $response = $this->deleteJson('/api/v1/preferences/' . $admin->id); $response->assertOk(); $this->assertDatabaseMissing('user_preferences', [ @@ -146,7 +146,7 @@ class PreferencesControllerTest extends TestCase 'id' => $id, 'firstname' => 'Test', 'lastname' => 'User', - 'email' => $roleName.$id.'@example.com', + 'email' => $roleName . $id . '@example.com', 'cellphone' => '5555555555', 'address_street' => '123 Main', 'city' => 'City', diff --git a/tests/Feature/Api/V1/Settings/SchoolCalendarControllerTest.php b/tests/Feature/Api/V1/Settings/SchoolCalendarControllerTest.php index 20076505..be959119 100644 --- a/tests/Feature/Api/V1/Settings/SchoolCalendarControllerTest.php +++ b/tests/Feature/Api/V1/Settings/SchoolCalendarControllerTest.php @@ -3,7 +3,6 @@ namespace Tests\Feature\Api\V1\Settings; use App\Models\User; -use App\Services\Settings\SchoolCalendar\SchoolCalendarMutationService; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\DB; use Laravel\Sanctum\Sanctum; @@ -51,7 +50,7 @@ class SchoolCalendarControllerTest extends TestCase $eventId = $this->seedEvent(); Sanctum::actingAs($user); - $response = $this->getJson('/api/v1/settings/school-calendar/events/'.$eventId); + $response = $this->getJson('/api/v1/settings/school-calendar/events/' . $eventId); $response->assertOk(); $response->assertJsonPath('status', true); @@ -89,7 +88,7 @@ class SchoolCalendarControllerTest extends TestCase $eventId = $this->seedEvent(); Sanctum::actingAs($user); - $response = $this->patchJson('/api/v1/settings/school-calendar/events/'.$eventId, [ + $response = $this->patchJson('/api/v1/settings/school-calendar/events/' . $eventId, [ 'title' => 'Updated Event', ]); @@ -108,7 +107,7 @@ class SchoolCalendarControllerTest extends TestCase $eventId = $this->seedEvent(); Sanctum::actingAs($user); - $response = $this->deleteJson('/api/v1/settings/school-calendar/events/'.$eventId); + $response = $this->deleteJson('/api/v1/settings/school-calendar/events/' . $eventId); $response->assertOk(); $response->assertJsonPath('status', true); @@ -135,9 +134,8 @@ class SchoolCalendarControllerTest extends TestCase $user = $this->seedUser(); Sanctum::actingAs($user); - $this->app->bind(SchoolCalendarMutationService::class, function () { - return new class - { + $this->app->bind(\App\Services\Settings\SchoolCalendar\SchoolCalendarMutationService::class, function () { + return new class { public function create(array $payload, array $targets = []): array { throw new \RuntimeException('fail'); diff --git a/tests/Feature/Api/V1/Settings/SettingsControllerTest.php b/tests/Feature/Api/V1/Settings/SettingsControllerTest.php index 8ba89095..fc49c78a 100644 --- a/tests/Feature/Api/V1/Settings/SettingsControllerTest.php +++ b/tests/Feature/Api/V1/Settings/SettingsControllerTest.php @@ -2,6 +2,7 @@ namespace Tests\Feature\Api\V1\Settings; +use App\Models\Setting; use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\DB; @@ -79,7 +80,7 @@ class SettingsControllerTest extends TestCase $user = User::query()->create([ 'firstname' => 'Test', 'lastname' => 'User', - 'email' => $roleName.'@example.com', + 'email' => $roleName . '@example.com', 'cellphone' => '5555555555', 'address_street' => '123 Main', 'city' => 'City', diff --git a/tests/Feature/Api/V1/Staff/StaffControllerTest.php b/tests/Feature/Api/V1/Staff/StaffControllerTest.php index d21063e9..e55f40c8 100644 --- a/tests/Feature/Api/V1/Staff/StaffControllerTest.php +++ b/tests/Feature/Api/V1/Staff/StaffControllerTest.php @@ -99,7 +99,7 @@ class StaffControllerTest extends TestCase 'school_year' => '2025-2026', ]); - $response = $this->patchJson('/api/v1/staff/'.$staff->id, [ + $response = $this->patchJson('/api/v1/staff/' . $staff->id, [ 'firstname' => 'Updated', ]); @@ -127,7 +127,7 @@ class StaffControllerTest extends TestCase 'school_year' => '2025-2026', ]); - $response = $this->deleteJson('/api/v1/staff/'.$staff->id); + $response = $this->deleteJson('/api/v1/staff/' . $staff->id); $response->assertOk(); $this->assertDatabaseMissing('staff', [ @@ -146,7 +146,7 @@ class StaffControllerTest extends TestCase $user = User::query()->create([ 'firstname' => 'Test', 'lastname' => 'User', - 'email' => $email ?? ($roleName.'@example.com'), + 'email' => $email ?? ($roleName . '@example.com'), 'cellphone' => '5555555555', 'address_street' => '123 Main', 'city' => 'City', diff --git a/tests/Feature/Api/V1/Students/StudentControllerTest.php b/tests/Feature/Api/V1/Students/StudentControllerTest.php index 4ab094e1..d3e2a5d9 100644 --- a/tests/Feature/Api/V1/Students/StudentControllerTest.php +++ b/tests/Feature/Api/V1/Students/StudentControllerTest.php @@ -59,7 +59,7 @@ class StudentControllerTest extends TestCase ]); Sanctum::actingAs($user); - $response = $this->getJson('/api/v1/students/'.$studentId.'/score-card'); + $response = $this->getJson('/api/v1/students/' . $studentId . '/score-card'); $response->assertOk(); $response->assertJson(['ok' => true]); diff --git a/tests/Feature/Api/V1/Support/SupportControllerTest.php b/tests/Feature/Api/V1/Support/SupportControllerTest.php index fa1fc7cb..53a84e87 100644 --- a/tests/Feature/Api/V1/Support/SupportControllerTest.php +++ b/tests/Feature/Api/V1/Support/SupportControllerTest.php @@ -85,7 +85,7 @@ class SupportControllerTest extends TestCase $user = User::query()->create([ 'firstname' => 'Test', 'lastname' => 'User', - 'email' => $roleName.'@example.com', + 'email' => $roleName . '@example.com', 'cellphone' => '5555555555', 'address_street' => '123 Main', 'city' => 'City', diff --git a/tests/Feature/Api/V1/Teachers/TeacherControllerTest.php b/tests/Feature/Api/V1/Teachers/TeacherControllerTest.php index f71bf372..1c48c6fd 100644 --- a/tests/Feature/Api/V1/Teachers/TeacherControllerTest.php +++ b/tests/Feature/Api/V1/Teachers/TeacherControllerTest.php @@ -22,7 +22,7 @@ class TeacherControllerTest extends TestCase $this->seedStudent($teacher->id, $classSectionId); Sanctum::actingAs($teacher); - $response = $this->getJson('/api/v1/teachers/classes?class_section_id='.$classSectionId); + $response = $this->getJson('/api/v1/teachers/classes?class_section_id=' . $classSectionId); $response->assertOk(); $response->assertJson(['ok' => true]); diff --git a/tests/Feature/Api/V1/Ui/UiControllerTest.php b/tests/Feature/Api/V1/Ui/UiControllerTest.php index 785458b8..9631367b 100644 --- a/tests/Feature/Api/V1/Ui/UiControllerTest.php +++ b/tests/Feature/Api/V1/Ui/UiControllerTest.php @@ -53,7 +53,7 @@ class UiControllerTest extends TestCase $user = User::query()->create([ 'firstname' => 'Test', 'lastname' => 'User', - 'email' => $roleName.'@example.com', + 'email' => $roleName . '@example.com', 'cellphone' => '5555555555', 'address_street' => '123 Main', 'city' => 'City', diff --git a/tests/Feature/Api/V1/Whatsapp/WhatsappInviteControllerTest.php b/tests/Feature/Api/V1/Whatsapp/WhatsappInviteControllerTest.php index 1e552075..578b2f87 100644 --- a/tests/Feature/Api/V1/Whatsapp/WhatsappInviteControllerTest.php +++ b/tests/Feature/Api/V1/Whatsapp/WhatsappInviteControllerTest.php @@ -58,7 +58,8 @@ class WhatsappInviteControllerTest extends TestCase Sanctum::actingAs($user); $this->seedInviteData(); - Event::listen(WhatsappInvitesSend::class, static function () {}); + Event::listen(WhatsappInvitesSend::class, static function () { + }); $response = $this->postJson('/api/v1/whatsapp/invites/send', [ 'mode' => 'parents', diff --git a/tests/Feature/Api/V1/Whatsapp/WhatsappLinkControllerTest.php b/tests/Feature/Api/V1/Whatsapp/WhatsappLinkControllerTest.php index b5daae92..0392ad5d 100644 --- a/tests/Feature/Api/V1/Whatsapp/WhatsappLinkControllerTest.php +++ b/tests/Feature/Api/V1/Whatsapp/WhatsappLinkControllerTest.php @@ -52,7 +52,7 @@ class WhatsappLinkControllerTest extends TestCase 'updated_at' => now(), ]); - $response = $this->getJson('/api/v1/whatsapp/links/'.$linkId); + $response = $this->getJson('/api/v1/whatsapp/links/' . $linkId); $response->assertOk(); $response->assertJsonPath('status', true); @@ -99,7 +99,7 @@ class WhatsappLinkControllerTest extends TestCase 'updated_at' => now(), ]); - $response = $this->patchJson('/api/v1/whatsapp/links/'.$linkId, [ + $response = $this->patchJson('/api/v1/whatsapp/links/' . $linkId, [ 'invite_link' => 'https://chat.whatsapp.com/updated', 'active' => false, ]); @@ -129,7 +129,7 @@ class WhatsappLinkControllerTest extends TestCase 'updated_at' => now(), ]); - $response = $this->deleteJson('/api/v1/whatsapp/links/'.$linkId); + $response = $this->deleteJson('/api/v1/whatsapp/links/' . $linkId); $response->assertOk(); $response->assertJsonPath('status', true); diff --git a/tests/Feature/Api/V1/Workflows/ScenarioAFamilyEnrollmentTest.php b/tests/Feature/Api/V1/Workflows/ScenarioAFamilyEnrollmentTest.php index a099571b..f4a8214b 100644 --- a/tests/Feature/Api/V1/Workflows/ScenarioAFamilyEnrollmentTest.php +++ b/tests/Feature/Api/V1/Workflows/ScenarioAFamilyEnrollmentTest.php @@ -54,11 +54,11 @@ class ScenarioAFamilyEnrollmentTest extends TestCase // Teacher sees student in roster. $this->actingAs($teacher, 'api'); - $this->getJson('/api/v1/teachers/classes?class_section_id='.$classSectionId) + $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'); + $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. @@ -89,7 +89,7 @@ class ScenarioAFamilyEnrollmentTest extends TestCase ]); $this->actingAs($parentA, 'api'); - $this->patchJson('/api/v1/parents/students/'.$studentB, [ + $this->patchJson('/api/v1/parents/students/' . $studentB, [ 'firstname' => 'Hacked', 'lastname' => 'Child', 'dob' => '2015-01-01', diff --git a/tests/Feature/Api/V1/Workflows/ScenarioBSundaySchoolDayTest.php b/tests/Feature/Api/V1/Workflows/ScenarioBSundaySchoolDayTest.php index eb0c5aea..2601f88b 100644 --- a/tests/Feature/Api/V1/Workflows/ScenarioBSundaySchoolDayTest.php +++ b/tests/Feature/Api/V1/Workflows/ScenarioBSundaySchoolDayTest.php @@ -27,7 +27,7 @@ class ScenarioBSundaySchoolDayTest extends TestCase $this->actingAs($teacher, 'api'); - $form = $this->getJson('/api/v1/attendance/teacher/form?class_section_id='.$classSectionId) + $form = $this->getJson('/api/v1/attendance/teacher/form?class_section_id=' . $classSectionId) ->assertOk(); $date = (string) ($form->json('data.current_sunday') ?? '2025-10-05'); @@ -52,7 +52,7 @@ class ScenarioBSundaySchoolDayTest extends TestCase // Parent views attendance for their child. $this->actingAs($parent, 'api'); - $this->getJson('/api/v1/parents/attendance?school_year='.self::E2E_SCHOOL_YEAR) + $this->getJson('/api/v1/parents/attendance?school_year=' . self::E2E_SCHOOL_YEAR) ->assertOk() ->assertJsonPath('ok', true); } diff --git a/tests/Feature/Api/V1/Workflows/ScenarioCAcademicTermTest.php b/tests/Feature/Api/V1/Workflows/ScenarioCAcademicTermTest.php index 89987c70..b7fa6038 100644 --- a/tests/Feature/Api/V1/Workflows/ScenarioCAcademicTermTest.php +++ b/tests/Feature/Api/V1/Workflows/ScenarioCAcademicTermTest.php @@ -51,7 +51,7 @@ class ScenarioCAcademicTermTest extends TestCase $this->actingAs($teacher, 'api'); - $this->getJson('/api/v1/scores/overview?class_section_id='.$classSectionId.'&semester='.self::E2E_SEMESTER.'&school_year='.self::E2E_SCHOOL_YEAR) + $this->getJson('/api/v1/scores/overview?class_section_id=' . $classSectionId . '&semester=' . self::E2E_SEMESTER . '&school_year=' . self::E2E_SCHOOL_YEAR) ->assertOk() ->assertJsonPath('ok', true); @@ -69,7 +69,7 @@ class ScenarioCAcademicTermTest extends TestCase 'is_locked' => 1, ]); - $this->getJson('/api/v1/reports/report-cards/students/'.$studentId.'?school_year='.self::E2E_SCHOOL_YEAR.'&semester='.self::E2E_SEMESTER) + $this->getJson('/api/v1/reports/report-cards/students/' . $studentId . '?school_year=' . self::E2E_SCHOOL_YEAR . '&semester=' . self::E2E_SEMESTER) ->assertOk(); } diff --git a/tests/Feature/Api/V1/Workflows/ScenarioDFamilyBillingTest.php b/tests/Feature/Api/V1/Workflows/ScenarioDFamilyBillingTest.php index 93e4a4f3..4d5d7fa0 100644 --- a/tests/Feature/Api/V1/Workflows/ScenarioDFamilyBillingTest.php +++ b/tests/Feature/Api/V1/Workflows/ScenarioDFamilyBillingTest.php @@ -61,7 +61,7 @@ class ScenarioDFamilyBillingTest extends TestCase ->value('id'); $this->assertGreaterThan(0, $chargeId); - $this->postJson('/api/v1/finance/event-charges/'.$chargeId.'/approve') + $this->postJson('/api/v1/finance/event-charges/' . $chargeId . '/approve') ->assertOk() ->assertJsonPath('ok', true); @@ -81,7 +81,7 @@ class ScenarioDFamilyBillingTest extends TestCase 'invoice_id' => $invoiceId, ]); - $this->getJson('/api/v1/finance/payments/parent/'.$parentId.'?school_year='.self::E2E_SCHOOL_YEAR) + $this->getJson('/api/v1/finance/payments/parent/' . $parentId . '?school_year=' . self::E2E_SCHOOL_YEAR) ->assertOk() ->assertJsonPath('ok', true); diff --git a/tests/Feature/Api/V1/Workflows/ScenarioEFinanceEdgeTest.php b/tests/Feature/Api/V1/Workflows/ScenarioEFinanceEdgeTest.php index 29a312c8..115544aa 100644 --- a/tests/Feature/Api/V1/Workflows/ScenarioEFinanceEdgeTest.php +++ b/tests/Feature/Api/V1/Workflows/ScenarioEFinanceEdgeTest.php @@ -26,7 +26,7 @@ class ScenarioEFinanceEdgeTest extends TestCase $parent = $this->createApiUserWithRole('parent'); $priorInvoiceId = $this->seedPriorYearUnpaidInvoice($parent->id); - $preview = $this->getJson('/api/v1/finance/carryforwards/preview?'.http_build_query([ + $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, @@ -44,24 +44,24 @@ class ScenarioEFinanceEdgeTest extends TestCase $this->assertNotEmpty($draft->json('result.created')); $carryforwardId = (int) $draft->json('result.created.0.id'); - $this->postJson('/api/v1/finance/carryforwards/'.$carryforwardId.'/approve') + $this->postJson('/api/v1/finance/carryforwards/' . $carryforwardId . '/approve') ->assertOk() ->assertJsonPath('ok', true); - $posted = $this->postJson('/api/v1/finance/carryforwards/'.$carryforwardId.'/post') + $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', [ + $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') + $this->postJson('/api/v1/finance/installment-plans/' . $planId . '/activate') ->assertOk() ->assertJsonPath('ok', true); @@ -81,7 +81,7 @@ class ScenarioEFinanceEdgeTest extends TestCase 'updated_at' => now(), ]); - $this->postJson('/api/v1/finance/payments/'.$paymentId.'/allocate-installments', []) + $this->postJson('/api/v1/finance/payments/' . $paymentId . '/allocate-installments', []) ->assertOk() ->assertJsonPath('ok', true); @@ -89,20 +89,20 @@ class ScenarioEFinanceEdgeTest extends TestCase ->assertOk() ->assertJsonPath('ok', true); - $this->postJson('/api/v1/finance/parent-payment-followups/'.$parent->id.'/note', [ + $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', [ + $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', [ + $this->postJson('/api/v1/finance/parent-payment-followups/' . $parent->id . '/resolve', [ 'invoice_id' => $newInvoiceId, 'note' => 'Balance plan agreed.', ])->assertCreated()->assertJsonPath('ok', true); diff --git a/tests/Feature/Api/V1/Workflows/ScenarioFTeacherSubmissionTest.php b/tests/Feature/Api/V1/Workflows/ScenarioFTeacherSubmissionTest.php index 4580c56b..045b90a9 100644 --- a/tests/Feature/Api/V1/Workflows/ScenarioFTeacherSubmissionTest.php +++ b/tests/Feature/Api/V1/Workflows/ScenarioFTeacherSubmissionTest.php @@ -3,6 +3,7 @@ namespace Tests\Feature\Api\V1\Workflows; use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Mail; use Tests\Concerns\SeedsE2ETestFixtures; use Tests\TestCase; diff --git a/tests/Feature/Api/V1/Workflows/ScenarioGPrintBadgeTest.php b/tests/Feature/Api/V1/Workflows/ScenarioGPrintBadgeTest.php index a114d620..29f93bc8 100644 --- a/tests/Feature/Api/V1/Workflows/ScenarioGPrintBadgeTest.php +++ b/tests/Feature/Api/V1/Workflows/ScenarioGPrintBadgeTest.php @@ -36,7 +36,7 @@ class ScenarioGPrintBadgeTest extends TestCase $this->actingAs($admin, 'api'); foreach (['assigned', 'done', 'delivered'] as $status) { - $this->patchJson('/api/v1/print-requests/'.$requestId.'/status', [ + $this->patchJson('/api/v1/print-requests/' . $requestId . '/status', [ 'status' => $status, ])->assertOk(); } @@ -46,7 +46,7 @@ class ScenarioGPrintBadgeTest extends TestCase 'status' => 'delivered', ]); - $badgeTag = 'E2E-BADGE-'.random_int(10000, 99999); + $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', [ diff --git a/tests/Feature/Api/V1/Workflows/ScenarioHSecurityUnauthorizedAccessTest.php b/tests/Feature/Api/V1/Workflows/ScenarioHSecurityUnauthorizedAccessTest.php index 3295f97d..d34d18df 100644 --- a/tests/Feature/Api/V1/Workflows/ScenarioHSecurityUnauthorizedAccessTest.php +++ b/tests/Feature/Api/V1/Workflows/ScenarioHSecurityUnauthorizedAccessTest.php @@ -29,7 +29,7 @@ class ScenarioHSecurityUnauthorizedAccessTest extends TestCase // Parent cannot update another family's student. $this->actingAs($parentA, 'api'); - $this->patchJson('/api/v1/parents/students/'.$world['student_id'], [ + $this->patchJson('/api/v1/parents/students/' . $world['student_id'], [ 'firstname' => 'Blocked', 'lastname' => 'Update', 'dob' => '2014-01-01', diff --git a/tests/Feature/Api/V1/Workflows/ScenarioIPublicAuthAndSystemUseCaseTest.php b/tests/Feature/Api/V1/Workflows/ScenarioIPublicAuthAndSystemUseCaseTest.php new file mode 100644 index 00000000..fb162379 --- /dev/null +++ b/tests/Feature/Api/V1/Workflows/ScenarioIPublicAuthAndSystemUseCaseTest.php @@ -0,0 +1,130 @@ +seedE2EConfiguration(); + + $publicGetEndpoints = [ + '/api/v1/health', + '/api/v1/policies/school', + '/api/v1/policies/picture', + '/api/v1/pages/privacy', + '/api/v1/pages/terms', + '/api/v1/pages/help', + '/api/v1/frontend', + '/api/v1/frontend/facility', + '/api/v1/frontend/team', + '/api/v1/frontend/call-to-action', + '/api/v1/frontend/testimonial', + '/api/v1/auth/register/captcha', + ]; + + foreach ($publicGetEndpoints as $endpoint) { + $response = $this->getJson($endpoint); + + $this->assertLessThan( + 500, + $response->getStatusCode(), + "$endpoint returned a server error. Public routes do not get to explode just because nobody logged in." + ); + } + + $this->postJson('/api/v1/contact', []) + ->assertStatus(422) + ->assertJsonValidationErrors(['name', 'email', 'subject', 'message']); + + $this->postJson('/api/v1/contact', [ + 'name' => 'Public Visitor', + 'email' => 'visitor@example.test', + 'subject' => 'Program question', + 'message' => 'Please send details about Sunday school registration.', + ]) + ->assertCreated() + ->assertJsonPath('status', true) + ->assertJsonStructure(['status', 'message', 'data' => ['contact']]); + } + + public function test_user_can_login_read_current_profile_and_logout_through_api(): void + { + $user = $this->createApiUserWithRole('teacher', [ + 'firstname' => 'E2E', + 'lastname' => 'Teacher', + 'email' => 'e2e.auth.teacher@example.test', + 'password' => Hash::make('secret123'), + ]); + + $login = $this->postJson('/api/v1/auth/login', [ + 'email' => strtoupper($user->email), + 'password' => 'secret123', + ]) + ->assertOk() + ->assertJsonPath('status', true) + ->assertJsonStructure([ + 'token', + 'access_token', + 'token_type', + 'expires_in', + 'user' => ['id', 'name', 'firstname', 'lastname', 'email', 'roles'], + ]); + + $token = (string) $login->json('token'); + $this->assertNotSame('', $token); + + $this->withHeader('Authorization', 'Bearer ' . $token) + ->getJson('/api/v1/auth/me') + ->assertOk() + ->assertJsonPath('status', true) + ->assertJsonPath('data.email', 'e2e.auth.teacher@example.test'); + + $this->withHeader('Authorization', 'Bearer ' . $token) + ->postJson('/api/v1/auth/logout') + ->assertOk() + ->assertJsonPath('status', true); + } + + public function test_invalid_inactive_and_suspended_login_paths_fail_safely(): void + { + $active = $this->createApiUserWithRole('parent', [ + 'email' => 'active.parent@example.test', + 'password' => Hash::make('secret123'), + 'is_suspended' => 0, + ]); + + $this->postJson('/api/v1/auth/login', [ + 'email' => $active->email, + 'password' => 'wrong-password', + ]) + ->assertUnauthorized() + ->assertJsonPath('status', false); + + $suspended = $this->createApiUserWithRole('parent', [ + 'email' => 'suspended.parent@example.test', + 'password' => Hash::make('secret123'), + 'is_suspended' => 1, + ]); + + $this->postJson('/api/v1/auth/login', [ + 'email' => $suspended->email, + 'password' => 'secret123', + ]) + ->assertForbidden() + ->assertJsonPath('status', false); + } +} diff --git a/tests/Feature/Api/V1/Workflows/ScenarioJCrossModuleApiUseCaseSmokeTest.php b/tests/Feature/Api/V1/Workflows/ScenarioJCrossModuleApiUseCaseSmokeTest.php new file mode 100644 index 00000000..cfcd6d1c --- /dev/null +++ b/tests/Feature/Api/V1/Workflows/ScenarioJCrossModuleApiUseCaseSmokeTest.php @@ -0,0 +1,127 @@ +seedTeacherClassWithStudent(); + $admin = $this->actingAsApiAdministrator(); + $this->actingAs($admin, 'api'); + + $invoiceId = $this->seedInvoiceForParent($world['parent_id']); + + $endpoints = [ + '/api/v1/administrator/dashboard/metrics', + '/api/v1/administrator/dashboard/user-search?query=Kai', + '/api/v1/users', + '/api/v1/students', + '/api/v1/students/' . $world['student_id'], + '/api/v1/students/' . $world['student_id'] . '/classes', + '/api/v1/class-sections', + '/api/v1/teachers/classes?class_section_id=' . $world['class_section_id'], + '/api/v1/attendance/admin/daily?date=2025-10-05', + '/api/v1/attendance-comment-templates', + '/api/v1/finance/invoices/parent/' . $world['parent_id'], + '/api/v1/finance/invoices/' . $invoiceId . '/preview', + '/api/v1/finance/paypal-transactions', + '/api/v1/inventory/items', + '/api/v1/inventory/categories', + '/api/v1/messages', + '/api/v1/support', + '/api/v1/settings', + '/api/v1/system/semester-range/resolve?date=2025-10-05', + ]; + + foreach ($endpoints as $endpoint) { + $response = $this->getJson($endpoint); + + $this->assertLessThan( + 500, + $response->getStatusCode(), + "$endpoint returned a server error for an authenticated admin." + ); + } + } + + public function test_major_mutating_use_cases_validate_bad_payloads_instead_of_crashing(): void + { + $world = $this->seedTeacherClassWithStudent(); + $admin = $this->actingAsApiAdministrator(); + $this->actingAs($admin, 'api'); + + $adminMutations = [ + ['POST', '/api/v1/users', []], + ['POST', '/api/v1/students', []], + ['POST', '/api/v1/class-sections', []], + ['POST', '/api/v1/attendance-comment-templates', []], + ['POST', '/api/v1/assignments', []], + ['POST', '/api/v1/messages', []], + ['POST', '/api/v1/support', []], + ['POST', '/api/v1/inventory/items', []], + ['POST', '/api/v1/settings/events', []], + ['POST', '/api/v1/finance/payments', []], + ['POST', '/api/v1/finance/refunds', []], + ['POST', '/api/v1/finance/fees/tuition-breakdown', []], + ['POST', '/api/v1/extra-charges', []], + ['POST', '/api/v1/reports/slips/preview', []], + ]; + + foreach ($adminMutations as [$method, $endpoint, $payload]) { + $response = $this->json($method, $endpoint, $payload); + + $this->assertLessThan( + 500, + $response->getStatusCode(), + "$method $endpoint crashed instead of validating/rejecting the request." + ); + } + + $this->actingAs($world['teacher'], 'api'); + foreach ([ + ['POST', '/api/v1/attendance/teacher/submit', []], + ['POST', '/api/v1/teacher/absence-vacation', []], + ['POST', '/api/v1/teacher/class-progress-submit', []], + ['POST', '/api/v1/scores/homework', []], + ] as [$method, $endpoint, $payload]) { + $response = $this->json($method, $endpoint, $payload); + + $this->assertLessThan( + 500, + $response->getStatusCode(), + "$method $endpoint crashed for an authenticated teacher." + ); + } + + $this->actingAs($world['parent'], 'api'); + foreach ([ + ['PATCH', '/api/v1/parents/profile', []], + ['POST', '/api/v1/parents/emergency-contacts', []], + ['POST', '/api/v1/parents/attendance-reports', []], + ['POST', '/api/v1/parents/events/participation', []], + ] as [$method, $endpoint, $payload]) { + $response = $this->json($method, $endpoint, $payload); + + $this->assertLessThan( + 500, + $response->getStatusCode(), + "$method $endpoint crashed for an authenticated parent." + ); + } + } +} diff --git a/tests/Feature/Api/V1/Workflows/ScenarioKParentSelfServiceAndPrivacyTest.php b/tests/Feature/Api/V1/Workflows/ScenarioKParentSelfServiceAndPrivacyTest.php new file mode 100644 index 00000000..877621e5 --- /dev/null +++ b/tests/Feature/Api/V1/Workflows/ScenarioKParentSelfServiceAndPrivacyTest.php @@ -0,0 +1,149 @@ +seedTeacherClassWithStudent(); + $parent = $world['parent']; + $studentId = $world['student_id']; + + $this->seedInvoiceForParent($parent->id); + DB::table('enrollments')->insert([ + 'parent_id' => $parent->id, + 'student_id' => $studentId, + 'school_year' => self::E2E_SCHOOL_YEAR, + 'semester' => self::E2E_SEMESTER, + 'enrollment_status' => 'enrolled', + 'enrollment_date' => '2025-09-01', + ]); + DB::table('attendance_data')->insert([ + 'class_id' => 3, + 'class_section_id' => $world['class_section_id'], + 'student_id' => $studentId, + 'school_id' => 'S-E2E', + 'date' => '2025-10-05', + 'status' => 'present', + 'semester' => self::E2E_SEMESTER, + 'school_year' => self::E2E_SCHOOL_YEAR, + 'created_at' => now(), + 'updated_at' => now(), + ]); + + $this->actingAs($parent, 'api'); + + $this->getJson('/api/v1/parents/profile') + ->assertOk() + ->assertJsonPath('ok', true); + + $this->patchJson('/api/v1/parents/profile', [ + 'firstname' => 'Updated', + 'lastname' => 'Parent', + 'cellphone' => '5557778888', + 'address_street' => '99 Parent Way', + 'city' => 'Brooklyn', + 'state' => 'NY', + 'zip' => '11215', + ])->assertOk()->assertJsonPath('ok', true); + + $this->assertDatabaseHas('users', [ + 'id' => $parent->id, + 'firstname' => 'Updated', + 'cellphone' => '5557778888', + ]); + + $this->postJson('/api/v1/parents/emergency-contacts', [ + 'name' => 'Amina Emergency', + 'cellphone' => '5550001111', + 'email' => 'amina.emergency@example.test', + 'relation' => 'Aunt', + ])->assertCreated()->assertJsonPath('ok', true); + + $this->assertDatabaseHas('emergency_contacts', [ + 'parent_id' => $parent->id, + 'emergency_contact_name' => 'Amina Emergency', + ]); + + $this->getJson('/api/v1/parents/emergency-contacts') + ->assertOk() + ->assertJsonPath('ok', true); + + $this->getJson('/api/v1/parents/attendance?school_year=' . self::E2E_SCHOOL_YEAR) + ->assertOk() + ->assertJsonPath('ok', true); + + $this->getJson('/api/v1/parents/invoices?school_year=' . self::E2E_SCHOOL_YEAR) + ->assertOk() + ->assertJsonPath('ok', true); + + $this->getJson('/api/v1/parents/enrollments?school_year=' . self::E2E_SCHOOL_YEAR) + ->assertOk() + ->assertJsonPath('ok', true); + } + + public function test_parent_cannot_mutate_another_family_student_or_emergency_contact(): void + { + $this->seedE2EConfiguration(); + $admin = $this->actingAsApiAdministrator(); + $parentA = $this->createApiUserWithRole('parent'); + $parentB = $this->createApiUserWithRole('parent'); + $this->actingAs($admin, 'api'); + $studentB = $this->createStudentViaApi($parentB->id, [ + 'firstname' => 'Private', + 'lastname' => 'Student', + ]); + $foreignContactId = DB::table('emergency_contacts')->insertGetId([ + 'parent_id' => $parentB->id, + 'emergency_contact_name' => 'Foreign Contact', + 'cellphone' => '5553334444', + 'email' => 'foreign.contact@example.test', + 'relation' => 'Uncle', + 'semester' => self::E2E_SEMESTER, + 'school_year' => self::E2E_SCHOOL_YEAR, + 'created_at' => now(), + 'updated_at' => now(), + ]); + + $this->actingAs($parentA, 'api'); + + $this->patchJson('/api/v1/parents/students/' . $studentB, [ + 'firstname' => 'Tampered', + 'lastname' => 'Student', + 'dob' => '2015-01-01', + 'gender' => 'Male', + ])->assertNotFound(); + + $this->patchJson('/api/v1/parents/emergency-contacts/' . $foreignContactId, [ + 'name' => 'Tampered Contact', + 'cellphone' => '5553334444', + 'email' => 'foreign.contact@example.test', + 'relation' => 'Uncle', + ])->assertNotFound(); + + $this->assertDatabaseHas('students', [ + 'id' => $studentB, + 'firstname' => 'Private', + ]); + $this->assertDatabaseHas('emergency_contacts', [ + 'id' => $foreignContactId, + 'emergency_contact_name' => 'Foreign Contact', + ]); + } +} diff --git a/tests/Feature/Api/V1/Workflows/ScenarioLTeacherAcademicOperationsTest.php b/tests/Feature/Api/V1/Workflows/ScenarioLTeacherAcademicOperationsTest.php new file mode 100644 index 00000000..66735456 --- /dev/null +++ b/tests/Feature/Api/V1/Workflows/ScenarioLTeacherAcademicOperationsTest.php @@ -0,0 +1,130 @@ +seedTeacherClassWithStudent(); + $teacher = $world['teacher']; + $classSectionId = $world['class_section_id']; + $studentId = $world['student_id']; + + $this->actingAs($teacher, 'api'); + + $this->getJson('/api/v1/teachers/classes?class_section_id=' . $classSectionId) + ->assertOk() + ->assertJsonPath('ok', true); + + $this->getJson('/api/v1/class-progress/meta?class_id=3&school_year=' . self::E2E_SCHOOL_YEAR . '&semester=' . self::E2E_SEMESTER) + ->assertOk() + ->assertJsonPath('status', true); + + $this->postJson('/api/v1/class-progress', [ + 'class_section_id' => $classSectionId, + 'week_start' => '2025-10-05', + 'week_end' => '2025-10-11', + 'covered_islamic' => 'Seerah review', + 'covered_quran' => 'Surah recitation practice', + 'homework_islamic' => 'Read lesson notes', + 'homework_quran' => 'Memorize assigned ayat', + 'flags' => ['on_track'], + ])->assertCreated()->assertJsonPath('status', true); + + $reportId = (int) DB::table('class_progress_reports') + ->where('teacher_id', $teacher->id) + ->where('class_section_id', $classSectionId) + ->orderBy('id') + ->value('id'); + $this->assertGreaterThan(0, $reportId); + + $this->patchJson('/api/v1/class-progress/' . $reportId, [ + 'status' => 'ahead', + ])->assertOk()->assertJsonPath('status', true); + + $this->postJson('/api/v1/scores/homework/columns', [ + 'class_section_id' => $classSectionId, + 'semester' => self::E2E_SEMESTER, + 'school_year' => self::E2E_SCHOOL_YEAR, + ])->assertOk()->assertJsonPath('ok', true); + + $this->postJson('/api/v1/scores/homework', [ + 'class_section_id' => $classSectionId, + 'semester' => self::E2E_SEMESTER, + 'school_year' => self::E2E_SCHOOL_YEAR, + 'scores' => [ + (string) $studentId => [ + '1' => 96, + ], + ], + 'missing_ok' => [], + ])->assertOk()->assertJsonPath('ok', true); + + $this->assertDatabaseHas('homework', [ + 'student_id' => $studentId, + 'class_section_id' => $classSectionId, + 'homework_index' => 1, + 'score' => 96, + 'school_year' => self::E2E_SCHOOL_YEAR, + ]); + + $this->getJson('/api/v1/scores/homework?class_section_id=' . $classSectionId . '&semester=' . self::E2E_SEMESTER . '&school_year=' . self::E2E_SCHOOL_YEAR) + ->assertOk() + ->assertJsonPath('ok', true); + + $this->getJson('/api/v1/reports/report-cards/completeness?class_section_id=' . $classSectionId . '&semester=' . self::E2E_SEMESTER . '&school_year=' . self::E2E_SCHOOL_YEAR) + ->assertOk() + ->assertJsonPath('status', true); + } + + public function test_unassigned_teacher_cannot_view_or_update_another_teachers_progress_report(): void + { + $world = $this->seedTeacherClassWithStudent(); + $otherTeacher = $this->createApiUserWithRole('teacher'); + + DB::table('class_progress_reports')->insert([ + 'teacher_id' => $world['teacher']->id, + 'class_section_id' => $world['class_section_id'], + 'subject' => 'Islamic Studies', + 'week_start' => '2025-10-05', + 'week_end' => '2025-10-11', + 'covered' => 'Private teacher notes', + 'homework' => 'Private homework', + 'status' => 'on_track', + 'next_week_plan' => 'Continue private unit', + 'created_at' => now(), + 'updated_at' => now(), + ]); + $reportId = (int) DB::table('class_progress_reports')->value('id'); + + $this->actingAs($otherTeacher, 'api'); + + $this->getJson('/api/v1/class-progress/' . $reportId) + ->assertForbidden(); + + $this->patchJson('/api/v1/class-progress/' . $reportId, [ + 'status' => 'behind', + ])->assertForbidden(); + + $this->assertDatabaseHas('class_progress_reports', [ + 'id' => $reportId, + 'status' => 'on_track', + ]); + } +} diff --git a/tests/Feature/Api/V1/Workflows/ScenarioMAdministratorEnrollmentAndClassOperationsTest.php b/tests/Feature/Api/V1/Workflows/ScenarioMAdministratorEnrollmentAndClassOperationsTest.php new file mode 100644 index 00000000..9ead2e69 --- /dev/null +++ b/tests/Feature/Api/V1/Workflows/ScenarioMAdministratorEnrollmentAndClassOperationsTest.php @@ -0,0 +1,134 @@ +seedE2EConfiguration(); + $admin = $this->actingAsApiAdministrator(); + $teacher = $this->createApiUserWithRole('teacher', [ + 'firstname' => 'Maryam', + 'lastname' => 'Teacher', + ]); + $this->actingAs($admin, 'api'); + + $parentId = $this->createParentAccountViaApi([ + 'firstname' => 'Roster', + 'lastname' => 'Parent', + ]); + $studentId = $this->createStudentViaApi($parentId, [ + 'firstname' => 'Roster', + 'lastname' => 'Student', + ]); + + DB::table('classes')->insert([ + 'id' => 9, + 'class_name' => 'Grade 9', + 'schedule' => 'Sunday', + 'capacity' => 25, + 'semester' => self::E2E_SEMESTER, + 'school_year' => self::E2E_SCHOOL_YEAR, + 'created_at' => now(), + 'updated_at' => now(), + ]); + + $this->postJson('/api/v1/class-sections', [ + 'class_id' => 9, + 'class_section_id' => 909, + 'class_section_name' => '9A', + 'semester' => self::E2E_SEMESTER, + 'school_year' => self::E2E_SCHOOL_YEAR, + ])->assertCreated(); + + $this->assignStudentToClassViaApi($studentId, 909) + ->assertOk() + ->assertJsonPath('ok', true); + + $this->postJson('/api/v1/administrator/teacher-class/assign', [ + 'teacher_id' => $teacher->id, + 'class_section_id' => 909, + 'teacher_role' => 'teacher', + 'school_year' => self::E2E_SCHOOL_YEAR, + ])->assertOk()->assertJsonPath('ok', true); + + $this->assertDatabaseHas('student_class', [ + 'student_id' => $studentId, + 'class_section_id' => 909, + 'school_year' => self::E2E_SCHOOL_YEAR, + ]); + $this->assertDatabaseHas('teacher_class', [ + 'teacher_id' => $teacher->id, + 'class_section_id' => 909, + 'position' => 'main', + 'school_year' => self::E2E_SCHOOL_YEAR, + ]); + + $this->getJson('/api/v1/students/' . $studentId . '/classes') + ->assertOk() + ->assertJsonPath('ok', true); + + $this->actingAs($teacher, 'api'); + $this->getJson('/api/v1/teachers/classes?class_section_id=909') + ->assertOk() + ->assertJsonPath('ok', true); + } + + public function test_admin_can_reverse_roster_and_teacher_assignments_without_orphaning_records(): void + { + $this->seedE2EConfiguration(); + $admin = $this->actingAsApiAdministrator(); + $teacher = $this->createApiUserWithRole('teacher'); + $classSectionId = $this->seedClassSection(910, '9B', 9); + $parentId = $this->createParentAccountViaApi(); + $studentId = $this->createStudentViaApi($parentId); + + $this->actingAs($admin, 'api'); + $this->assignStudentToClassViaApi($studentId, $classSectionId)->assertOk(); + $this->seedTeacherClassAssignment($teacher->id, $classSectionId); + + $this->postJson('/api/v1/students/remove-class', [ + 'student_id' => $studentId, + 'class_section_id' => $classSectionId, + ])->assertOk()->assertJsonPath('ok', true); + + $this->postJson('/api/v1/administrator/teacher-class/delete', [ + 'teacher_id' => $teacher->id, + 'class_section_id' => $classSectionId, + 'position' => 'main', + 'school_year' => self::E2E_SCHOOL_YEAR, + ])->assertOk()->assertJsonPath('ok', true); + + $this->assertDatabaseMissing('student_class', [ + 'student_id' => $studentId, + 'class_section_id' => $classSectionId, + 'school_year' => self::E2E_SCHOOL_YEAR, + ]); + $this->assertDatabaseMissing('teacher_class', [ + 'teacher_id' => $teacher->id, + 'class_section_id' => $classSectionId, + 'position' => 'main', + 'school_year' => self::E2E_SCHOOL_YEAR, + ]); + $this->assertDatabaseHas('students', [ + 'id' => $studentId, + 'parent_id' => $parentId, + ]); + } +} diff --git a/tests/Feature/Api/V1/Workflows/ScenarioNCommunicationsAndSupportLifecycleTest.php b/tests/Feature/Api/V1/Workflows/ScenarioNCommunicationsAndSupportLifecycleTest.php new file mode 100644 index 00000000..6da1af15 --- /dev/null +++ b/tests/Feature/Api/V1/Workflows/ScenarioNCommunicationsAndSupportLifecycleTest.php @@ -0,0 +1,99 @@ +seedTeacherClassWithStudent(); + $parent = $world['parent']; + $teacher = $world['teacher']; + + $this->actingAs($parent, 'api'); + $this->postJson('/api/v1/messages', [ + 'recipient_id' => $teacher->id, + 'subject' => 'Homework clarification', + 'message' => 'Can you confirm the Quran homework for this week?', + 'priority' => 'normal', + ])->assertCreated(); + + $messageId = (int) DB::table('messages') + ->where('sender_id', $parent->id) + ->where('recipient_id', $teacher->id) + ->where('subject', 'Homework clarification') + ->value('id'); + $this->assertGreaterThan(0, $messageId); + + $this->actingAs($teacher, 'api'); + $this->getJson('/api/v1/messages/inbox') + ->assertOk() + ->assertJsonPath('ok', true); + + $this->getJson('/api/v1/messages/' . $messageId) + ->assertOk() + ->assertJsonPath('ok', true); + + $this->assertDatabaseHas('messages', [ + 'id' => $messageId, + 'read_status' => 1, + ]); + + $this->patchJson('/api/v1/messages/' . $messageId, [ + 'status' => 'archived', + ])->assertOk()->assertJsonPath('ok', true); + + $this->assertDatabaseHas('messages', [ + 'id' => $messageId, + 'status' => 'archived', + ]); + } + + public function test_public_contact_support_ticket_and_admin_support_queue_are_connected(): void + { + $this->seedE2EConfiguration(); + $admin = $this->actingAsApiAdministrator(); + + $this->postJson('/api/v1/contact', [ + 'name' => 'Public Visitor', + 'email' => 'visitor.support@example.test', + 'subject' => 'Registration help', + 'message' => 'I need help completing registration.', + ])->assertSuccessful(); + + $this->actingAs($admin, 'api'); + $this->getJson('/api/v1/support') + ->assertOk() + ->assertJsonPath('ok', true); + + $response = $this->postJson('/api/v1/support', [ + 'name' => 'Internal Support Case', + 'email' => 'internal.support@example.test', + 'subject' => 'Follow up required', + 'message' => 'Please call the parent about registration.', + 'status' => 'open', + 'priority' => 'normal', + ]); + + $this->assertLessThan( + 500, + $response->getStatusCode(), + 'Authenticated support ticket creation should validate or persist, not crash.' + ); + } +} diff --git a/tests/Feature/Api/V1/Workflows/ScenarioOInventoryFinanceAndAuditControlsTest.php b/tests/Feature/Api/V1/Workflows/ScenarioOInventoryFinanceAndAuditControlsTest.php new file mode 100644 index 00000000..c9e75a34 --- /dev/null +++ b/tests/Feature/Api/V1/Workflows/ScenarioOInventoryFinanceAndAuditControlsTest.php @@ -0,0 +1,115 @@ +seedE2EConfiguration(); + $admin = $this->actingAsApiAdministrator(); + $this->actingAs($admin, 'api'); + + $categoryId = DB::table('inventory_categories')->insertGetId([ + 'type' => 'classroom', + 'name' => 'E2E Classroom Supplies', + 'created_at' => now(), + 'updated_at' => now(), + ]); + + $this->postJson('/api/v1/inventory/items', [ + 'type' => 'classroom', + 'category_id' => $categoryId, + 'name' => 'Dry erase markers', + 'quantity' => 12, + 'condition' => 'good', + 'semester' => self::E2E_SEMESTER, + 'school_year' => self::E2E_SCHOOL_YEAR, + ])->assertCreated(); + + $itemId = (int) DB::table('inventory_items') + ->where('name', 'Dry erase markers') + ->value('id'); + $this->assertGreaterThan(0, $itemId); + + $this->postJson('/api/v1/inventory/items/' . $itemId . '/adjust', [ + 'mode' => 'out', + 'quantity' => 2, + 'reason' => 'Issued to teacher cart', + ])->assertOk()->assertJsonPath('status', true); + + $this->postJson('/api/v1/inventory/items/' . $itemId . '/audit', [ + 'needs_repair_qty' => 1, + 'need_replace_qty' => 0, + 'cannot_find_qty' => 0, + ])->assertOk()->assertJsonPath('status', true); + + $this->assertDatabaseHas('inventory_movements', [ + 'item_id' => $itemId, + 'movement_type' => 'out', + ]); + $this->assertDatabaseHas('inventory_items', [ + 'id' => $itemId, + 'needs_repair_qty' => 1, + ]); + + $this->getJson('/api/v1/inventory/summary/classroom') + ->assertOk() + ->assertJsonPath('status', true); + } + + public function test_finance_followup_records_notes_promises_and_resolution_for_unpaid_parent(): void + { + $world = $this->seedTeacherClassWithStudent(); + $admin = $this->actingAsApiAdministrator(); + $parentId = $world['parent_id']; + $invoiceId = $this->seedInvoiceForParent($parentId, [ + 'balance' => 300.00, + 'paid_amount' => 200.00, + 'status' => 'partial', + ]); + + $this->actingAs($admin, 'api'); + + $this->getJson('/api/v1/finance/parent-payment-followups') + ->assertOk() + ->assertJsonPath('ok', true); + + $this->postJson('/api/v1/finance/parent-payment-followups/' . $parentId . '/note', [ + 'note' => 'Left voicemail about remaining balance.', + 'invoice_id' => $invoiceId, + ])->assertSuccessful(); + + $this->postJson('/api/v1/finance/parent-payment-followups/' . $parentId . '/promise-to-pay', [ + 'invoice_id' => $invoiceId, + 'amount' => 300.00, + 'promise_date' => '2025-10-20', + 'note' => 'Parent promised to pay next Sunday.', + ])->assertSuccessful(); + + $this->postJson('/api/v1/finance/parent-payment-followups/' . $parentId . '/mark-contacted', [ + 'invoice_id' => $invoiceId, + 'contacted_at' => '2025-10-06 12:00:00', + ])->assertSuccessful(); + + $this->postJson('/api/v1/finance/parent-payment-followups/' . $parentId . '/resolve', [ + 'invoice_id' => $invoiceId, + 'resolution_note' => 'Resolved after payment arrangement.', + ])->assertSuccessful(); + } +} diff --git a/tests/Feature/Api/V1/Workflows/ScenarioPSettingsConfigurationAndCalendarGovernanceTest.php b/tests/Feature/Api/V1/Workflows/ScenarioPSettingsConfigurationAndCalendarGovernanceTest.php new file mode 100644 index 00000000..5c60ff1b --- /dev/null +++ b/tests/Feature/Api/V1/Workflows/ScenarioPSettingsConfigurationAndCalendarGovernanceTest.php @@ -0,0 +1,139 @@ +seedE2EConfiguration(); + $admin = $this->actingAsApiAdministrator(); + $this->actingAs($admin, 'api'); + + $this->getJson('/api/v1/settings') + ->assertOk(); + + $settingsUpdate = $this->patchJson('/api/v1/settings', [ + 'timezone' => 'America/New_York', + 'school_name' => 'Al-Rahma Test School', + 'default_semester' => self::E2E_SEMESTER, + 'default_school_year' => self::E2E_SCHOOL_YEAR, + ]); + $this->assertNoServerError($settingsUpdate, 'Settings update'); + + $this->getJson('/api/v1/settings/school-calendar/options') + ->assertOk() + ->assertJsonPath('status', true); + + $event = $this->postJson('/api/v1/settings/school-calendar/events', [ + 'title' => 'E2E Parent Orientation', + 'date' => '2025-10-12', + 'description' => 'Orientation for new families.', + 'audience' => 'parents', + 'semester' => self::E2E_SEMESTER, + 'school_year' => self::E2E_SCHOOL_YEAR, + ]); + $event->assertCreated()->assertJsonPath('status', true); + + $eventId = (int) DB::table('calendar_events') + ->where('title', 'E2E Parent Orientation') + ->value('id'); + $this->assertGreaterThan(0, $eventId); + + $this->getJson('/api/v1/settings/school-calendar/events?school_year=' . self::E2E_SCHOOL_YEAR . '&include_meetings=1') + ->assertOk() + ->assertJsonPath('status', true); + + $this->patchJson('/api/v1/settings/school-calendar/events/' . $eventId, [ + 'title' => 'E2E Parent Orientation Updated', + 'date' => '2025-10-19', + 'description' => 'Updated orientation for new families.', + 'audience' => 'parents', + 'semester' => self::E2E_SEMESTER, + 'school_year' => self::E2E_SCHOOL_YEAR, + ])->assertOk()->assertJsonPath('status', true); + + $notification = $this->postJson('/api/v1/notifications', [ + 'title' => 'Orientation Reminder', + 'message' => 'Parent orientation moved to October 19.', + 'audience' => 'parents', + 'type' => 'announcement', + 'starts_at' => '2025-10-10 09:00:00', + 'ends_at' => '2025-10-20 09:00:00', + 'is_active' => true, + ]); + $this->assertSuccessfulOrValidation($notification, 'Notification creation'); + + $notificationId = (int) DB::table('notifications') + ->where('title', 'Orientation Reminder') + ->value('id'); + + if ($notificationId > 0) { + $this->getJson('/api/v1/notifications/active') + ->assertOk(); + + $this->postJson('/api/v1/notifications/' . $notificationId . '/read') + ->assertSuccessful(); + + $this->deleteJson('/api/v1/notifications/' . $notificationId) + ->assertSuccessful(); + + $this->postJson('/api/v1/notifications/' . $notificationId . '/restore') + ->assertSuccessful(); + } + + $this->deleteJson('/api/v1/settings/school-calendar/events/' . $eventId) + ->assertOk() + ->assertJsonPath('status', true); + } + + public function test_configuration_admin_crud_keeps_school_year_context_explicit(): void + { + $this->seedE2EConfiguration(); + $admin = $this->actingAsApiAdministrator(); + $this->actingAs($admin, 'api'); + + $this->getJson('/api/v1/configuration-admin') + ->assertOk(); + + $create = $this->postJson('/api/v1/configuration-admin', [ + 'config_key' => 'e2e_guardrail_message', + 'config_value' => 'E2E guardrail enabled', + 'description' => 'Created by explicit workflow coverage.', + ]); + $this->assertSuccessfulOrValidation($create, 'Configuration creation'); + + $configId = (int) DB::table('configuration') + ->where('config_key', 'e2e_guardrail_message') + ->value('id'); + + $this->getJson('/api/v1/configuration-admin/key/e2e_guardrail_message') + ->assertSuccessful(); + + if ($configId > 0) { + $this->patchJson('/api/v1/configuration-admin/' . $configId, [ + 'config_value' => 'E2E guardrail updated', + 'description' => 'Updated by explicit workflow coverage.', + ])->assertSuccessful(); + + $this->deleteJson('/api/v1/configuration-admin/' . $configId) + ->assertSuccessful(); + } + } +} diff --git a/tests/Feature/Api/V1/Workflows/ScenarioQSchoolYearPromotionClosureLifecycleTest.php b/tests/Feature/Api/V1/Workflows/ScenarioQSchoolYearPromotionClosureLifecycleTest.php new file mode 100644 index 00000000..868e57ca --- /dev/null +++ b/tests/Feature/Api/V1/Workflows/ScenarioQSchoolYearPromotionClosureLifecycleTest.php @@ -0,0 +1,151 @@ +seedTeacherClassWithStudent(); + $admin = $this->actingAsApiAdministrator(); + $parent = $world['parent']; + $studentId = $world['student_id']; + + DB::table('enrollments')->insert([ + 'parent_id' => $parent->id, + 'student_id' => $studentId, + 'school_year' => self::E2E_SCHOOL_YEAR, + 'semester' => self::E2E_SEMESTER, + 'enrollment_status' => 'enrolled', + 'enrollment_date' => '2025-09-01', + 'created_at' => now(), + 'updated_at' => now(), + ]); + + $this->actingAs($admin, 'api'); + + $this->getJson('/api/v1/administrator/promotions') + ->assertOk(); + $this->getJson('/api/v1/administrator/promotions/summary') + ->assertOk(); + + $evaluate = $this->postJson('/api/v1/administrator/promotions/evaluate', [ + 'school_year' => self::E2E_SCHOOL_YEAR, + 'student_ids' => [$studentId], + ]); + $this->assertNoServerError($evaluate, 'Promotion evaluation'); + + $promotionId = (int) DB::table('student_promotion_records') + ->where('student_id', $studentId) + ->value('id'); + + if ($promotionId > 0) { + $this->patchJson('/api/v1/administrator/promotions/' . $promotionId . '/status', [ + 'status' => 'parent_action_required', + 'decision' => 'promote', + 'notes' => 'Ready for next level.', + ])->assertSuccessful(); + + $this->actingAs($parent, 'api'); + $this->getJson('/api/v1/parents/promotions') + ->assertOk(); + $this->postJson('/api/v1/parents/promotions/' . $promotionId . '/start') + ->assertSuccessful(); + $this->patchJson('/api/v1/parents/promotions/' . $promotionId . '/steps', [ + 'confirm_returning' => true, + 'confirm_contact_info' => true, + 'confirm_policy' => true, + ])->assertSuccessful(); + $this->postJson('/api/v1/parents/promotions/' . $promotionId . '/submit') + ->assertSuccessful(); + } + } + + public function test_admin_previews_closes_and_reopens_school_year_with_balance_transfer_guardrails(): void + { + $world = $this->seedTeacherClassWithStudent(); + $admin = $this->actingAsApiAdministrator(); + $parentId = $world['parent_id']; + $studentId = $world['student_id']; + $this->seedInvoiceForParent($parentId, [ + 'invoice_number' => 'INV-CLOSE-' . random_int(1000, 9999), + 'balance' => 125.00, + 'paid_amount' => 375.00, + 'total_amount' => 500.00, + 'status' => 'partial', + ]); + + $activeYearId = (int) DB::table('school_years') + ->where('name', self::E2E_SCHOOL_YEAR) + ->value('id'); + $this->assertGreaterThan(0, $activeYearId); + + DB::table('enrollments')->updateOrInsert( + ['student_id' => $studentId, 'school_year' => self::E2E_SCHOOL_YEAR], + [ + 'parent_id' => $parentId, + 'semester' => self::E2E_SEMESTER, + 'enrollment_status' => 'enrolled', + 'enrollment_date' => '2025-09-01', + 'created_at' => now(), + 'updated_at' => now(), + ] + ); + + $this->actingAs($admin, 'api'); + + $this->postJson('/api/v1/school-years/' . $activeYearId . '/validate-close', [ + 'new_school_year' => [ + 'name' => '2026-2027', + 'start_date' => '2026-09-01', + 'end_date' => '2027-06-30', + ], + 'transfer_unpaid_balances' => true, + ])->assertSuccessful(); + + $this->postJson('/api/v1/school-years/' . $activeYearId . '/preview-close', [ + 'new_school_year' => [ + 'name' => '2026-2027', + 'start_date' => '2026-09-01', + 'end_date' => '2027-06-30', + ], + 'transfer_unpaid_balances' => true, + ])->assertSuccessful(); + + $close = $this->postJson('/api/v1/school-years/' . $activeYearId . '/close', [ + 'new_school_year' => [ + 'name' => '2026-2027', + 'start_date' => '2026-09-01', + 'end_date' => '2027-06-30', + ], + 'transfer_unpaid_balances' => true, + 'confirmation' => 'CLOSE ' . self::E2E_SCHOOL_YEAR, + ]); + $this->assertNoServerError($close, 'School year close'); + + $newYearId = (int) DB::table('school_years') + ->where('name', '2026-2027') + ->value('id'); + + if ($newYearId > 0) { + $this->postJson('/api/v1/school-years/' . $newYearId . '/reopen') + ->assertSuccessful(); + } + } +} diff --git a/tests/Feature/Api/V1/Workflows/ScenarioRAttendanceOperationsAndExceptionHandlingTest.php b/tests/Feature/Api/V1/Workflows/ScenarioRAttendanceOperationsAndExceptionHandlingTest.php new file mode 100644 index 00000000..dd1ebc22 --- /dev/null +++ b/tests/Feature/Api/V1/Workflows/ScenarioRAttendanceOperationsAndExceptionHandlingTest.php @@ -0,0 +1,175 @@ +seedTeacherClassWithStudent(); + $teacher = $world['teacher']; + $admin = $this->actingAsApiAdministrator(); + $studentId = $world['student_id']; + $classSectionId = $world['class_section_id']; + + $this->actingAs($teacher, 'api'); + $this->getJson('/api/v1/attendance/teacher/grid?class_section_id=' . $classSectionId . '&date=2025-10-05') + ->assertSuccessful(); + $this->getJson('/api/v1/attendance/teacher/form?class_section_id=' . $classSectionId . '&date=2025-10-05') + ->assertSuccessful(); + + $submit = $this->postJson('/api/v1/attendance/teacher/submit', [ + 'class_section_id' => $classSectionId, + 'date' => '2025-10-05', + 'attendance' => [ + [ + 'student_id' => $studentId, + 'status' => 'present', + 'comment' => 'On time', + ], + ], + 'semester' => self::E2E_SEMESTER, + 'school_year' => self::E2E_SCHOOL_YEAR, + ]); + $this->assertNoServerError($submit, 'Teacher attendance submit'); + + $this->actingAs($admin, 'api'); + $this->getJson('/api/v1/attendance/admin/daily?date=2025-10-05&class_section_id=' . $classSectionId) + ->assertSuccessful(); + + $manualAdd = $this->postJson('/api/v1/attendance/admin/add-entry', [ + 'student_id' => $studentId, + 'class_section_id' => $classSectionId, + 'date' => '2025-10-05', + 'status' => 'late', + 'comment' => 'Arrived after assembly.', + 'semester' => self::E2E_SEMESTER, + 'school_year' => self::E2E_SCHOOL_YEAR, + ]); + $this->assertNoServerError($manualAdd, 'Admin attendance manual entry'); + + $this->getJson('/api/v1/attendance/staff/month?month=2025-10') + ->assertSuccessful(); + $this->getJson('/api/v1/attendance/staff/admins') + ->assertSuccessful(); + + $staffCell = $this->postJson('/api/v1/attendance/staff/cell', [ + 'user_id' => $admin->id, + 'date' => '2025-10-05', + 'status' => 'present', + 'school_year' => self::E2E_SCHOOL_YEAR, + ]); + $this->assertNoServerError($staffCell, 'Staff attendance cell save'); + } + + public function test_early_dismissal_late_slip_and_violation_tracking_are_connected(): void + { + $world = $this->seedTeacherClassWithStudent(); + $admin = $this->actingAsApiAdministrator(); + $studentId = $world['student_id']; + $classSectionId = $world['class_section_id']; + $this->actingAs($admin, 'api'); + + $this->getJson('/api/v1/attendance/early-dismissals/student-options') + ->assertSuccessful(); + + $dismissal = $this->postJson('/api/v1/attendance/early-dismissals', [ + 'student_id' => $studentId, + 'class_section_id' => $classSectionId, + 'dismissal_date' => '2025-10-05', + 'dismissal_time' => '12:10', + 'reason' => 'Medical appointment', + 'released_to' => 'Parent Fixture', + 'relationship' => 'Parent', + ]); + $this->assertNoServerError($dismissal, 'Early dismissal creation'); + + $signature = $this->post('/api/v1/attendance/early-dismissals/signature', [ + 'student_id' => $studentId, + 'signature' => UploadedFile::fake()->image('signature.png', 200, 100), + ]); + $this->assertSuccessfulOrValidation($signature, 'Early dismissal signature upload'); + + $this->getJson('/api/v1/attendance/late-slip-logs') + ->assertSuccessful(); + + $record = $this->postJson('/api/v1/attendance-tracking/record', [ + 'student_id' => $studentId, + 'class_section_id' => $classSectionId, + 'date' => '2025-10-05', + 'type' => 'late', + 'status' => 'pending', + 'note' => 'Second late arrival this month.', + 'school_year' => self::E2E_SCHOOL_YEAR, + 'semester' => self::E2E_SEMESTER, + ]); + $this->assertNoServerError($record, 'Attendance tracking record'); + + $this->getJson('/api/v1/attendance-tracking/pending-violations') + ->assertSuccessful(); + $this->getJson('/api/v1/attendance-tracking/student-case/' . $studentId) + ->assertSuccessful(); + $this->getJson('/api/v1/attendance-tracking/compose?student_id=' . $studentId) + ->assertSuccessful(); + + $note = $this->postJson('/api/v1/attendance-tracking/save-notification-note', [ + 'student_id' => $studentId, + 'note' => 'Parent notified about attendance pattern.', + 'date' => '2025-10-05', + ]); + $this->assertNoServerError($note, 'Attendance tracking note'); + } + + public function test_attendance_comment_templates_support_create_update_delete_and_legacy_aliases(): void + { + $this->seedE2EConfiguration(); + $admin = $this->actingAsApiAdministrator(); + $this->actingAs($admin, 'api'); + + $create = $this->postJson('/api/v1/attendance-comment-templates', [ + 'comment' => 'E2E: arrived after first lesson.', + 'type' => 'late', + 'is_active' => true, + ]); + $create->assertCreated(); + + $templateId = (int) DB::table('attendance_comment_templates') + ->where('comment', 'E2E: arrived after first lesson.') + ->value('id'); + $this->assertGreaterThan(0, $templateId); + + $this->getJson('/api/v1/attendance-comment-templates/list-data') + ->assertOk(); + $this->putJson('/api/v1/attendance-comment-templates/' . $templateId, [ + 'comment' => 'E2E: arrived late, parent notified.', + 'type' => 'late', + 'is_active' => true, + ])->assertSuccessful(); + + $this->postJson('/api/v1/attendance-templates/save', [ + 'id' => $templateId, + 'comment' => 'Legacy alias update', + 'type' => 'late', + ])->assertSuccessful(); + + $this->deleteJson('/api/v1/attendance-comment-templates/' . $templateId) + ->assertSuccessful(); + } +} diff --git a/tests/Feature/Api/V1/Workflows/ScenarioSScoringGradingAndReportCardLifecycleTest.php b/tests/Feature/Api/V1/Workflows/ScenarioSScoringGradingAndReportCardLifecycleTest.php new file mode 100644 index 00000000..effaef43 --- /dev/null +++ b/tests/Feature/Api/V1/Workflows/ScenarioSScoringGradingAndReportCardLifecycleTest.php @@ -0,0 +1,131 @@ +seedTeacherClassWithStudent(); + $teacher = $world['teacher']; + $studentId = $world['student_id']; + $classSectionId = $world['class_section_id']; + $admin = $this->actingAsApiAdministrator(); + + $this->actingAs($teacher, 'api'); + + foreach (['homework', 'quizzes', 'projects'] as $type) { + $column = $this->postJson('/api/v1/scores/' . $type . '/columns', [ + 'class_section_id' => $classSectionId, + 'semester' => self::E2E_SEMESTER, + 'school_year' => self::E2E_SCHOOL_YEAR, + ]); + $this->assertNoServerError($column, ucfirst($type) . ' score column creation'); + + $save = $this->postJson('/api/v1/scores/' . $type, [ + 'class_section_id' => $classSectionId, + 'semester' => self::E2E_SEMESTER, + 'school_year' => self::E2E_SCHOOL_YEAR, + 'scores' => [ + (string) $studentId => ['1' => 91], + ], + 'missing_ok' => [], + ]); + $this->assertNoServerError($save, ucfirst($type) . ' score save'); + } + + foreach (['midterm', 'final', 'participation'] as $type) { + $save = $this->postJson('/api/v1/scores/' . $type, [ + 'class_section_id' => $classSectionId, + 'semester' => self::E2E_SEMESTER, + 'school_year' => self::E2E_SCHOOL_YEAR, + 'scores' => [ + (string) $studentId => 88, + ], + ]); + $this->assertNoServerError($save, ucfirst($type) . ' score save'); + } + + $this->postJson('/api/v1/scores/comments', [ + 'student_id' => $studentId, + 'class_section_id' => $classSectionId, + 'school_year' => self::E2E_SCHOOL_YEAR, + 'semester' => self::E2E_SEMESTER, + 'comment' => 'Consistent progress across the semester.', + ])->assertSuccessful(); + + $this->getJson('/api/v1/scores/overview?class_section_id=' . $classSectionId . '&school_year=' . self::E2E_SCHOOL_YEAR) + ->assertSuccessful(); + $this->getJson('/api/v1/scores/student-scores?student_id=' . $studentId . '&class_section_id=' . $classSectionId) + ->assertSuccessful(); + + $this->actingAs($admin, 'api'); + $this->getJson('/api/v1/grading/overview?class_section_id=' . $classSectionId) + ->assertSuccessful(); + $this->postJson('/api/v1/grading/decisions/generate', [ + 'class_section_id' => $classSectionId, + 'school_year' => self::E2E_SCHOOL_YEAR, + 'semester' => self::E2E_SEMESTER, + ])->assertSuccessful(); + $this->getJson('/api/v1/grading/decisions?class_section_id=' . $classSectionId) + ->assertSuccessful(); + $this->getJson('/api/v1/reports/report-cards/completeness?class_section_id=' . $classSectionId . '&school_year=' . self::E2E_SCHOOL_YEAR . '&semester=' . self::E2E_SEMESTER) + ->assertSuccessful(); + } + + public function test_grading_locks_and_below_sixty_intervention_are_visible_and_mutable(): void + { + $world = $this->seedTeacherClassWithStudent(); + $admin = $this->actingAsApiAdministrator(); + $studentId = $world['student_id']; + $classSectionId = $world['class_section_id']; + $this->actingAs($admin, 'api'); + + $this->postJson('/api/v1/grading/locks/toggle', [ + 'type' => 'homework', + 'class_section_id' => $classSectionId, + 'school_year' => self::E2E_SCHOOL_YEAR, + 'semester' => self::E2E_SEMESTER, + 'locked' => true, + ])->assertSuccessful(); + + $this->postJson('/api/v1/grading/locks/all', [ + 'class_section_id' => $classSectionId, + 'school_year' => self::E2E_SCHOOL_YEAR, + 'semester' => self::E2E_SEMESTER, + 'locked' => false, + ])->assertSuccessful(); + + $this->getJson('/api/v1/grading/below-sixty?school_year=' . self::E2E_SCHOOL_YEAR) + ->assertSuccessful(); + $this->postJson('/api/v1/grading/below-sixty/decisions', [ + 'student_id' => $studentId, + 'class_section_id' => $classSectionId, + 'decision' => 'monitor', + 'notes' => 'E2E intervention note.', + 'school_year' => self::E2E_SCHOOL_YEAR, + 'semester' => self::E2E_SEMESTER, + ])->assertSuccessful(); + $this->postJson('/api/v1/grading/below-sixty/meeting', [ + 'student_id' => $studentId, + 'meeting_date' => '2025-10-12', + 'notes' => 'Parent meeting scheduled.', + ])->assertSuccessful(); + } +} diff --git a/tests/Feature/Api/V1/Workflows/ScenarioTDocumentsPrintBadgesAndCertificatesLifecycleTest.php b/tests/Feature/Api/V1/Workflows/ScenarioTDocumentsPrintBadgesAndCertificatesLifecycleTest.php new file mode 100644 index 00000000..fe0583e3 --- /dev/null +++ b/tests/Feature/Api/V1/Workflows/ScenarioTDocumentsPrintBadgesAndCertificatesLifecycleTest.php @@ -0,0 +1,132 @@ +seedTeacherClassWithStudent(); + $teacher = $world['teacher']; + $admin = $this->actingAsApiAdministrator(); + $classSectionId = $world['class_section_id']; + + $this->actingAs($teacher, 'api'); + $this->getJson('/api/v1/print-requests/teacher') + ->assertSuccessful(); + + $create = $this->post('/api/v1/print-requests', [ + 'file' => UploadedFile::fake()->create('e2e-lesson.pdf', 32, 'application/pdf'), + 'class_id' => $classSectionId, + 'num_copies' => 2, + 'required_by' => '2025-10-12 11:00:00', + 'pickup_method' => 'Self Pickup', + 'page_selection' => '1-2', + 'notes' => 'E2E print request.', + ]); + $this->assertStatusIn($create, [201, 422], 'Print request upload'); + + $requestId = (int) DB::table('print_requests') + ->where('teacher_id', $teacher->id) + ->value('id'); + + if ($requestId > 0) { + $this->get('/api/v1/print-requests/' . $requestId . '/file') + ->assertSuccessful(); + + $this->actingAs($admin, 'api'); + $this->getJson('/api/v1/print-requests/admin') + ->assertSuccessful(); + $this->patchJson('/api/v1/print-requests/' . $requestId . '/status', [ + 'status' => 'processing', + 'admin_notes' => 'Queued for printing.', + ])->assertSuccessful(); + $this->get('/api/v1/print-requests/' . $requestId . '/file') + ->assertSuccessful(); + } + } + + public function test_badges_stickers_slips_and_certificates_are_generated_or_validated_without_crashing(): void + { + $world = $this->seedTeacherClassWithStudent(); + $admin = $this->actingAsApiAdministrator(); + $studentId = $world['student_id']; + $classSectionId = $world['class_section_id']; + $this->actingAs($admin, 'api'); + + $this->getJson('/api/v1/badges/form-data') + ->assertSuccessful(); + $this->getJson('/api/v1/badges/print-status?student_ids[]=' . $studentId) + ->assertSuccessful(); + + $badge = $this->postJson('/api/v1/badges/pdf', [ + 'student_ids' => [$studentId], + 'class_section_id' => $classSectionId, + 'school_year' => self::E2E_SCHOOL_YEAR, + 'semester' => self::E2E_SEMESTER, + ]); + $this->assertNoServerError($badge, 'Badge PDF generation'); + + $this->postJson('/api/v1/badges/log-print', [ + 'student_ids' => [$studentId], + 'printed_at' => '2025-10-05 10:00:00', + ])->assertSuccessful(); + + $this->getJson('/api/v1/reports/stickers/form-data') + ->assertSuccessful(); + $this->getJson('/api/v1/reports/stickers/students?class_section_id=' . $classSectionId) + ->assertSuccessful(); + $sticker = $this->postJson('/api/v1/reports/stickers/print', [ + 'student_ids' => [$studentId], + 'class_section_id' => $classSectionId, + ]); + $this->assertNoServerError($sticker, 'Sticker print'); + + $slipPreview = $this->postJson('/api/v1/reports/slips/preview', [ + 'student_id' => $studentId, + 'type' => 'late', + 'reason' => 'Late arrival', + 'date' => '2025-10-05', + ]); + $this->assertNoServerError($slipPreview, 'Slip preview'); + + $this->getJson('/api/v1/reports/slips/logs') + ->assertSuccessful(); + + $this->getJson('/api/v1/administrator/certificates/dashboard') + ->assertSuccessful(); + $this->getJson('/api/v1/administrator/certificates/form-options') + ->assertSuccessful(); + + $certificate = $this->postJson('/api/v1/administrator/certificates/generate', [ + 'student_id' => $studentId, + 'certificate_type' => 'completion', + 'title' => 'E2E Completion Certificate', + 'school_year' => self::E2E_SCHOOL_YEAR, + 'semester' => self::E2E_SEMESTER, + ]); + $this->assertNoServerError($certificate, 'Certificate generation'); + + $this->getJson('/api/v1/administrator/certificates/audit-log') + ->assertSuccessful(); + } +} diff --git a/tests/Feature/Api/V1/Workflows/ScenarioUFinanceBillingPaymentRefundAndInstallmentLifecycleTest.php b/tests/Feature/Api/V1/Workflows/ScenarioUFinanceBillingPaymentRefundAndInstallmentLifecycleTest.php new file mode 100644 index 00000000..7b808313 --- /dev/null +++ b/tests/Feature/Api/V1/Workflows/ScenarioUFinanceBillingPaymentRefundAndInstallmentLifecycleTest.php @@ -0,0 +1,157 @@ +seedTeacherClassWithStudent(); + $admin = $this->actingAsApiAdministrator(); + $parentId = $world['parent_id']; + $studentId = $world['student_id']; + $invoiceId = $this->seedInvoiceForParent($parentId, [ + 'invoice_number' => 'INV-E2E-PAY-' . random_int(1000, 9999), + 'total_amount' => 600.00, + 'balance' => 600.00, + 'status' => 'unpaid', + ]); + + $this->actingAs($admin, 'api'); + $this->getJson('/api/v1/finance/invoices/management') + ->assertSuccessful(); + $this->getJson('/api/v1/finance/invoices/parent/' . $parentId) + ->assertSuccessful(); + $this->getJson('/api/v1/finance/invoices/' . $invoiceId . '/preview') + ->assertSuccessful(); + + $payment = $this->postJson('/api/v1/finance/payments', [ + 'invoice_id' => $invoiceId, + 'parent_id' => $parentId, + 'amount' => 250.00, + 'payment_method' => 'cash', + 'payment_date' => '2025-10-05', + 'notes' => 'E2E partial payment.', + ]); + $this->assertNoServerError($payment, 'Payment creation'); + + $paymentId = (int) DB::table('payments') + ->where('invoice_id', $invoiceId) + ->value('id'); + + if ($paymentId > 0) { + $this->postJson('/api/v1/finance/payment-transactions', [ + 'payment_id' => $paymentId, + 'transaction_id' => 'E2E-TXN-' . random_int(1000, 9999), + 'amount' => 250.00, + 'status' => 'completed', + 'provider' => 'manual', + ])->assertSuccessful(); + + $this->getJson('/api/v1/finance/payment-transactions/payment/' . $paymentId) + ->assertSuccessful(); + $this->postJson('/api/v1/finance/payments/' . $paymentId . '/send-receipt') + ->assertSuccessful(); + } + + $this->postJson('/api/v1/finance/fees/tuition-total', [ + 'parent_id' => $parentId, + 'student_ids' => [$studentId], + 'school_year' => self::E2E_SCHOOL_YEAR, + ])->assertSuccessful(); + $this->postJson('/api/v1/finance/fees/family-balance', [ + 'parent_id' => $parentId, + 'school_year' => self::E2E_SCHOOL_YEAR, + ])->assertSuccessful(); + } + + public function test_admin_creates_installment_plan_refund_and_balance_carryforward_decision(): void + { + $world = $this->seedTeacherClassWithStudent(); + $admin = $this->actingAsApiAdministrator(); + $parentId = $world['parent_id']; + $invoiceId = $this->seedInvoiceForParent($parentId, [ + 'total_amount' => 900.00, + 'balance' => 900.00, + 'status' => 'unpaid', + ]); + + $this->actingAs($admin, 'api'); + + $plan = $this->postJson('/api/v1/finance/invoices/' . $invoiceId . '/installment-plans', [ + 'installments' => [ + ['due_date' => '2025-10-15', 'amount' => 300.00], + ['due_date' => '2025-11-15', 'amount' => 300.00], + ['due_date' => '2025-12-15', 'amount' => 300.00], + ], + 'notes' => 'E2E three-payment plan.', + ]); + $this->assertNoServerError($plan, 'Installment plan creation'); + + $planId = (int) DB::table('installment_plans') + ->where('invoice_id', $invoiceId) + ->value('id'); + if ($planId > 0) { + $this->postJson('/api/v1/finance/installment-plans/' . $planId . '/activate') + ->assertSuccessful(); + $this->getJson('/api/v1/finance/installment-plans/' . $planId) + ->assertSuccessful(); + $this->postJson('/api/v1/finance/installment-plans/' . $planId . '/restructure', [ + 'reason' => 'Parent requested revised dates.', + 'installments' => [ + ['due_date' => '2025-10-22', 'amount' => 450.00], + ['due_date' => '2025-11-22', 'amount' => 450.00], + ], + ])->assertSuccessful(); + } + + $refund = $this->postJson('/api/v1/finance/refunds', [ + 'parent_id' => $parentId, + 'invoice_id' => $invoiceId, + 'amount' => 50.00, + 'reason' => 'E2E overpayment adjustment.', + 'status' => 'pending', + ]); + $this->assertNoServerError($refund, 'Refund creation'); + + $refundId = (int) DB::table('refunds') + ->where('parent_id', $parentId) + ->value('id'); + if ($refundId > 0) { + $this->postJson('/api/v1/finance/refunds/' . $refundId . '/approve') + ->assertSuccessful(); + $this->postJson('/api/v1/finance/refunds/' . $refundId . '/payments', [ + 'amount' => 50.00, + 'payment_method' => 'check', + 'payment_date' => '2025-10-10', + ])->assertSuccessful(); + $this->postJson('/api/v1/finance/refunds/' . $refundId . '/send-receipt') + ->assertSuccessful(); + } + + $this->getJson('/api/v1/finance/carryforwards/preview?from_school_year=' . self::E2E_SCHOOL_YEAR . '&to_school_year=2026-2027') + ->assertSuccessful(); + $draft = $this->postJson('/api/v1/finance/carryforwards/draft', [ + 'from_school_year' => self::E2E_SCHOOL_YEAR, + 'to_school_year' => '2026-2027', + 'parent_ids' => [$parentId], + ]); + $this->assertNoServerError($draft, 'Balance carryforward draft'); + } +} diff --git a/tests/Feature/Api/V1/Workflows/ScenarioVProcurementReimbursementAndSupplyLifecycleTest.php b/tests/Feature/Api/V1/Workflows/ScenarioVProcurementReimbursementAndSupplyLifecycleTest.php new file mode 100644 index 00000000..dddf93cb --- /dev/null +++ b/tests/Feature/Api/V1/Workflows/ScenarioVProcurementReimbursementAndSupplyLifecycleTest.php @@ -0,0 +1,153 @@ +seedE2EConfiguration(); + $admin = $this->actingAsApiAdministrator(); + $this->actingAs($admin, 'api'); + + $category = $this->postJson('/api/v1/inventory/supply-categories', [ + 'name' => 'E2E Office Supplies', + 'description' => 'Supplies used by E2E procurement tests.', + ]); + $this->assertSuccessfulOrValidation($category, 'Supply category creation'); + + $supplier = $this->postJson('/api/v1/inventory/suppliers', [ + 'name' => 'E2E Supplier LLC', + 'contact_name' => 'Supply Contact', + 'email' => 'supplier@example.test', + 'phone' => '5552223333', + 'address' => '1 Supply Road', + ]); + $this->assertSuccessfulOrValidation($supplier, 'Supplier creation'); + + $supplierId = (int) DB::table('suppliers')->where('name', 'E2E Supplier LLC')->value('id'); + $categoryId = (int) DB::table('supply_categories')->where('name', 'E2E Office Supplies')->value('id'); + + if ($supplierId > 0 && $categoryId > 0) { + $order = $this->postJson('/api/v1/finance/purchase-orders', [ + 'supplier_id' => $supplierId, + 'order_date' => '2025-10-05', + 'expected_date' => '2025-10-12', + 'status' => 'draft', + 'items' => [ + [ + 'name' => 'Printer paper', + 'category_id' => $categoryId, + 'quantity' => 10, + 'unit_price' => 6.50, + ], + ], + 'notes' => 'E2E purchase order.', + ]); + $this->assertNoServerError($order, 'Purchase order creation'); + + $purchaseOrderId = (int) DB::table('purchase_orders') + ->where('supplier_id', $supplierId) + ->value('id'); + + if ($purchaseOrderId > 0) { + $this->getJson('/api/v1/finance/purchase-orders/' . $purchaseOrderId) + ->assertSuccessful(); + $this->postJson('/api/v1/finance/purchase-orders/' . $purchaseOrderId . '/receive', [ + 'received_date' => '2025-10-12', + 'items' => [ + ['name' => 'Printer paper', 'quantity_received' => 10], + ], + ])->assertSuccessful(); + } + } + + $this->getJson('/api/v1/finance/purchase-orders/options') + ->assertSuccessful(); + } + + public function test_reimbursement_request_batch_lock_export_and_mark_donation_flow(): void + { + Storage::fake('local'); + $this->seedE2EConfiguration(); + $admin = $this->actingAsApiAdministrator(); + $this->actingAs($admin, 'api'); + + $request = $this->post('/api/v1/finance/reimbursements', [ + 'requester_id' => $admin->id, + 'amount' => 45.75, + 'description' => 'E2E classroom material reimbursement.', + 'expense_date' => '2025-10-05', + 'category' => 'classroom', + 'receipt' => UploadedFile::fake()->create('receipt.pdf', 20, 'application/pdf'), + ]); + $this->assertNoServerError($request, 'Reimbursement creation'); + + $reimbursementId = (int) DB::table('reimbursements') + ->where('requester_id', $admin->id) + ->value('id'); + + if ($reimbursementId > 0) { + $this->putJson('/api/v1/finance/reimbursements/' . $reimbursementId, [ + 'amount' => 45.75, + 'description' => 'Updated E2E reimbursement description.', + 'status' => 'under_processing', + ])->assertSuccessful(); + + $this->postJson('/api/v1/finance/reimbursements/process', [ + 'ids' => [$reimbursementId], + 'status' => 'approved', + ])->assertSuccessful(); + } + + $batch = $this->postJson('/api/v1/finance/reimbursements/batches', [ + 'name' => 'E2E Reimbursement Batch', + 'reimbursement_ids' => $reimbursementId > 0 ? [$reimbursementId] : [], + ]); + $this->assertNoServerError($batch, 'Reimbursement batch creation'); + + $batchId = (int) DB::table('reimbursement_batches') + ->where('name', 'E2E Reimbursement Batch') + ->value('id'); + + if ($batchId > 0) { + $this->postJson('/api/v1/finance/reimbursements/batches/assign', [ + 'batch_id' => $batchId, + 'reimbursement_ids' => $reimbursementId > 0 ? [$reimbursementId] : [], + ])->assertSuccessful(); + $this->postJson('/api/v1/finance/reimbursements/batches/lock', [ + 'batch_id' => $batchId, + ])->assertSuccessful(); + $this->getJson('/api/v1/finance/reimbursements/batches/export?batch_id=' . $batchId) + ->assertSuccessful(); + } + + if ($reimbursementId > 0) { + $this->postJson('/api/v1/finance/reimbursements/mark-donation', [ + 'id' => $reimbursementId, + 'donation_note' => 'Donated back to school.', + ])->assertSuccessful(); + } + + $this->getJson('/api/v1/finance/reimbursements/under-processing') + ->assertSuccessful(); + } +} diff --git a/tests/Feature/Api/V1/Workflows/ScenarioWAccessControlRolesNavigationAndPreferencesTest.php b/tests/Feature/Api/V1/Workflows/ScenarioWAccessControlRolesNavigationAndPreferencesTest.php new file mode 100644 index 00000000..9d27c995 --- /dev/null +++ b/tests/Feature/Api/V1/Workflows/ScenarioWAccessControlRolesNavigationAndPreferencesTest.php @@ -0,0 +1,134 @@ +seedE2EConfiguration(); + $admin = $this->actingAsApiAdministrator(); + $teacher = $this->createApiUserWithRole('teacher'); + $this->actingAs($admin, 'api'); + + $this->getJson('/api/v1/role-permissions/roles') + ->assertOk(); + $permission = $this->postJson('/api/v1/role-permissions/permissions', [ + 'name' => 'e2e.view.dashboard', + 'display_name' => 'E2E View Dashboard', + 'description' => 'Permission created by E2E workflow tests.', + ]); + $this->assertSuccessfulOrValidation($permission, 'Permission creation'); + + $permissionId = (int) DB::table('permissions') + ->where('name', 'e2e.view.dashboard') + ->value('id'); + $teacherRoleId = (int) DB::table('roles')->where('name', 'teacher')->value('id'); + + if ($permissionId > 0 && $teacherRoleId > 0) { + $this->putJson('/api/v1/role-permissions/roles/' . $teacherRoleId . '/permissions', [ + 'permission_ids' => [$permissionId], + ])->assertSuccessful(); + $this->postJson('/api/v1/role-permissions/users/' . $teacher->id . '/roles', [ + 'role_ids' => [$teacherRoleId], + ])->assertSuccessful(); + } + + $this->getJson('/api/v1/nav-builder/menu') + ->assertOk(); + $nav = $this->postJson('/api/v1/nav-builder', [ + 'title' => 'E2E Dashboard', + 'label' => 'E2E Dashboard', + 'url' => '/e2e-dashboard', + 'route' => 'e2e.dashboard', + 'icon' => 'layout-dashboard', + 'role' => 'teacher', + 'sort_order' => 99, + 'is_active' => true, + ]); + $this->assertSuccessfulOrValidation($nav, 'Navigation item creation'); + + $navId = (int) DB::table('nav_items') + ->where('title', 'E2E Dashboard') + ->orWhere('label', 'E2E Dashboard') + ->value('id'); + + if ($navId > 0) { + $this->postJson('/api/v1/nav-builder/reorder', [ + 'items' => [ + ['id' => $navId, 'sort_order' => 1], + ], + ])->assertSuccessful(); + $this->deleteJson('/api/v1/nav-builder/' . $navId) + ->assertSuccessful(); + } + } + + public function test_user_preferences_and_role_switcher_preserve_selected_context(): void + { + $this->seedE2EConfiguration(); + $roles = $this->seedApiRoles(); + $user = $this->createApiUserWithRole('teacher'); + $user->roles()->syncWithoutDetaching([$roles['parent']->id]); + $this->actingAs($user->fresh(), 'api'); + + $this->getJson('/api/v1/role-switcher') + ->assertOk(); + $switch = $this->postJson('/api/v1/role-switcher/switch', [ + 'role' => 'parent', + ]); + $this->assertNoServerError($switch, 'Role switch'); + + $this->getJson('/api/v1/preferences') + ->assertOk(); + $this->postJson('/api/v1/preferences', [ + 'key' => 'theme', + 'value' => 'dark', + ])->assertSuccessful(); + $this->postJson('/api/v1/preferences', [ + 'key' => 'active_role', + 'value' => 'parent', + ])->assertSuccessful(); + $this->getJson('/api/v1/preferences/list') + ->assertOk(); + + $this->putJson('/api/v1/preferences/' . $user->id, [ + 'preferences' => [ + 'theme' => 'light', + 'active_role' => 'teacher', + ], + ])->assertSuccessful(); + $this->deleteJson('/api/v1/preferences/' . $user->id) + ->assertSuccessful(); + } + + public function test_non_admin_cannot_manage_role_permissions_or_admin_nav_builder(): void + { + $this->seedE2EConfiguration(); + $teacher = $this->createApiUserWithRole('teacher'); + $this->actingAs($teacher, 'api'); + + $this->getJson('/api/v1/role-permissions/roles') + ->assertForbidden(); + $this->postJson('/api/v1/role-permissions/permissions', [ + 'name' => 'e2e.illegal.permission', + ])->assertForbidden(); + } +} diff --git a/tests/Feature/Api/V1/Workflows/ScenarioXPublicKioskScannerAndVerificationLifecycleTest.php b/tests/Feature/Api/V1/Workflows/ScenarioXPublicKioskScannerAndVerificationLifecycleTest.php new file mode 100644 index 00000000..c56c2ca7 --- /dev/null +++ b/tests/Feature/Api/V1/Workflows/ScenarioXPublicKioskScannerAndVerificationLifecycleTest.php @@ -0,0 +1,107 @@ +seedE2EConfiguration(); + + foreach ([ + '/api/v1/frontend', + '/api/v1/frontend/facility', + '/api/v1/frontend/team', + '/api/v1/frontend/call-to-action', + '/api/v1/frontend/testimonial', + '/api/v1/pages/privacy', + '/api/v1/pages/terms', + '/api/v1/pages/help', + '/api/v1/policies/school', + '/api/v1/policies/picture', + '/api/v1/health', + '/api/v1/system/db-check', + '/api/documentation/swagger.json', + '/api/docs/public', + ] as $endpoint) { + $response = $this->getJson($endpoint); + $this->assertNoServerError($response, 'Public endpoint ' . $endpoint); + } + } + + public function test_badge_scan_and_scanner_process_known_and_unknown_codes_safely(): void + { + $world = $this->seedTeacherClassWithStudent(); + $admin = $this->actingAsApiAdministrator(); + $studentId = $world['student_id']; + $badgeCode = 'E2E-BADGE-' . random_int(1000, 9999); + + DB::table('students')->where('id', $studentId)->update([ + 'badge_scan' => $badgeCode, + ]); + + $knownScan = $this->postJson('/api/v1/badge_scan/scan', [ + 'code' => $badgeCode, + 'scanner' => 'front-desk', + 'scanned_at' => '2025-10-05 09:00:00', + ]); + $this->assertNoServerError($knownScan, 'Known badge scan'); + + $unknownScan = $this->postJson('/api/v1/badge_scan/scan', [ + 'code' => 'UNKNOWN-' . random_int(1000, 9999), + 'scanner' => 'front-desk', + 'scanned_at' => '2025-10-05 09:05:00', + ]); + $this->assertNoServerError($unknownScan, 'Unknown badge scan'); + + $scannerProcess = $this->postJson('/api/v1/scanner/process', [ + 'payload' => $badgeCode, + 'source' => 'kiosk', + ]); + $this->assertNoServerError($scannerProcess, 'Scanner process'); + + $this->actingAs($admin, 'api'); + $this->getJson('/api/v1/badge_scan/logs') + ->assertSuccessful(); + } + + public function test_public_verification_and_invite_tokens_fail_closed_for_invalid_values(): void + { + $this->getJson('/api/certificates/verify/not-a-real-token') + ->assertStatus(404); + + $this->assertStatusIn( + $this->getJson('/api/timeoff/notify/not-a-real-token'), + [200, 302, 404, 410, 422], + 'Invalid time-off notification token' + ); + + $this->assertStatusIn( + $this->getJson('/api/confirm_authorized_user?token=not-a-real-token'), + [200, 302, 404, 410, 422], + 'Invalid authorized-user confirmation token' + ); + + $this->assertStatusIn( + $this->getJson('/api/set_authorized_user_password/999999?token=not-a-real-token'), + [200, 302, 404, 410, 422], + 'Invalid authorized-user set-password token' + ); + } +} diff --git a/tests/Feature/Api/V1/Workflows/ScenarioYFamilyGuardianAuthorizedUserAndWhatsappLifecycleTest.php b/tests/Feature/Api/V1/Workflows/ScenarioYFamilyGuardianAuthorizedUserAndWhatsappLifecycleTest.php new file mode 100644 index 00000000..96575f39 --- /dev/null +++ b/tests/Feature/Api/V1/Workflows/ScenarioYFamilyGuardianAuthorizedUserAndWhatsappLifecycleTest.php @@ -0,0 +1,143 @@ +seedTeacherClassWithStudent(); + $admin = $this->actingAsApiAdministrator(); + $primaryParent = $world['parent']; + $studentId = $world['student_id']; + $secondParent = $this->createApiUserWithRole('parent', [ + 'firstname' => 'Second', + 'lastname' => 'Guardian', + 'email' => 'second.guardian.' . uniqid('', true) . '@example.test', + ]); + + $this->actingAs($admin, 'api'); + + $bootstrap = $this->postJson('/api/v1/families/bootstrap', [ + 'primary_parent_id' => $primaryParent->id, + 'student_ids' => [$studentId], + 'home_name' => 'E2E Household', + ]); + $this->assertNoServerError($bootstrap, 'Family bootstrap'); + + $familyId = (int) DB::table('families') + ->where('primary_parent_id', $primaryParent->id) + ->orWhere('name', 'E2E Household') + ->value('id'); + + if ($familyId > 0) { + $this->postJson('/api/v1/families/attach-second/by-user', [ + 'family_id' => $familyId, + 'user_id' => $secondParent->id, + 'relation' => 'Father', + ])->assertSuccessful(); + $this->postJson('/api/v1/families/set-primary-home', [ + 'family_id' => $familyId, + 'student_id' => $studentId, + ])->assertSuccessful(); + $this->postJson('/api/v1/families/set-guardian-flags', [ + 'family_id' => $familyId, + 'user_id' => $secondParent->id, + 'can_pickup' => true, + 'can_receive_billing' => false, + 'can_receive_academic' => true, + ])->assertSuccessful(); + + $this->getJson('/api/v1/families/' . $familyId . '/guardians') + ->assertSuccessful(); + $this->getJson('/api/v1/family-admin/card?family_id=' . $familyId) + ->assertSuccessful(); + } + + $this->getJson('/api/v1/families/by-student/' . $studentId) + ->assertSuccessful(); + $this->getJson('/api/v1/family-admin/search?q=E2E') + ->assertSuccessful(); + } + + public function test_parent_authorized_user_invite_and_household_privacy_flow(): void + { + $world = $this->seedTeacherClassWithStudent(); + $parent = $world['parent']; + $admin = $this->actingAsApiAdministrator(); + $this->actingAs($parent, 'api'); + + $invite = $this->postJson('/api/v1/parents/authorized-users', [ + 'name' => 'E2E Pickup Helper', + 'email' => 'pickup.helper.' . uniqid('', true) . '@example.test', + 'cellphone' => '5554446666', + 'relation' => 'Uncle', + 'permissions' => ['pickup'], + ]); + $this->assertNoServerError($invite, 'Authorized user invite creation'); + + $authorizedUserId = (int) DB::table('authorized_users') + ->where('parent_id', $parent->id) + ->value('id'); + + if ($authorizedUserId > 0) { + $this->getJson('/api/v1/parents/authorized-users') + ->assertSuccessful(); + $this->patchJson('/api/v1/parents/authorized-users/' . $authorizedUserId, [ + 'name' => 'E2E Pickup Helper Updated', + 'cellphone' => '5554447777', + 'relation' => 'Uncle', + 'permissions' => ['pickup', 'emergency'], + ])->assertSuccessful(); + } + + $this->actingAs($admin, 'api'); + $this->getJson('/api/v1/whatsapp/parent-contacts') + ->assertSuccessful(); + $this->getJson('/api/v1/whatsapp/parent-contacts-by-class') + ->assertSuccessful(); + + $link = $this->postJson('/api/v1/whatsapp/links', [ + 'title' => 'E2E Parent Group', + 'url' => 'https://chat.whatsapp.com/e2e-test-link', + 'audience' => 'parents', + 'class_section_id' => $world['class_section_id'], + 'is_active' => true, + ]); + $this->assertSuccessfulOrValidation($link, 'WhatsApp link creation'); + + $linkId = (int) DB::table('whatsapp_links') + ->where('title', 'E2E Parent Group') + ->value('id'); + if ($linkId > 0) { + $this->patchJson('/api/v1/whatsapp/links/' . $linkId, [ + 'title' => 'E2E Parent Group Updated', + 'url' => 'https://chat.whatsapp.com/e2e-test-link-updated', + 'audience' => 'parents', + 'is_active' => true, + ])->assertSuccessful(); + $this->postJson('/api/v1/whatsapp/membership', [ + 'link_id' => $linkId, + 'parent_id' => $parent->id, + 'status' => 'joined', + ])->assertSuccessful(); + } + } +} diff --git a/tests/Feature/Api/V1/Workflows/ScenarioZLegacyAliasesValidationAndCompatibilityTest.php b/tests/Feature/Api/V1/Workflows/ScenarioZLegacyAliasesValidationAndCompatibilityTest.php new file mode 100644 index 00000000..a5a3d976 --- /dev/null +++ b/tests/Feature/Api/V1/Workflows/ScenarioZLegacyAliasesValidationAndCompatibilityTest.php @@ -0,0 +1,155 @@ +seedTeacherClassWithStudent(); + $teacher = $world['teacher']; + $classSectionId = $world['class_section_id']; + $this->actingAs($teacher, 'api'); + + foreach ([ + '/api/v1/teacher/homework-list?class_section_id=' . $classSectionId, + '/api/v1/teacher/add-homework?class_section_id=' . $classSectionId, + '/api/v1/teacher/add-quiz?class_section_id=' . $classSectionId, + '/api/v1/teacher/add-midterm-exam?class_section_id=' . $classSectionId, + '/api/v1/teacher/add-final-exam?class_section_id=' . $classSectionId, + '/api/v1/teacher/add-participation?class_section_id=' . $classSectionId, + '/api/v1/teacher/add-project?class_section_id=' . $classSectionId, + '/api/v1/teacher/teacher-assignment', + '/api/v1/teacher/class-view?class_section_id=' . $classSectionId, + '/api/v1/teacher/showupdate-attendance?class_section_id=' . $classSectionId, + '/api/v1/teacher/showupdate_attendance?class_section_id=' . $classSectionId, + '/api/v1/teacher/scores?class_section_id=' . $classSectionId, + '/api/v1/teacher/calendar', + '/api/v1/teacher/class-progress-submit?class_section_id=' . $classSectionId, + '/api/v1/teacher/class-progress-history?class_section_id=' . $classSectionId, + '/api/v1/teacher/class-progress-view?class_section_id=' . $classSectionId, + '/api/v1/teacher/competition-scores?class_section_id=' . $classSectionId, + ] as $endpoint) { + $response = $this->getJson($endpoint); + $this->assertNoServerError($response, 'Legacy teacher alias ' . $endpoint); + } + } + + public function test_legacy_post_aliases_validate_payloads_without_500s(): void + { + $world = $this->seedTeacherClassWithStudent(); + $teacher = $world['teacher']; + $studentId = $world['student_id']; + $classSectionId = $world['class_section_id']; + $this->actingAs($teacher, 'api'); + + $scorePayload = [ + 'class_section_id' => $classSectionId, + 'school_year' => self::E2E_SCHOOL_YEAR, + 'semester' => self::E2E_SEMESTER, + 'scores' => [ + (string) $studentId => ['1' => 90], + ], + ]; + + foreach ([ + '/api/v1/teacher/add-homework', + '/api/v1/teacher/add-quiz', + '/api/v1/teacher/add-midterm-exam', + '/api/v1/teacher/add-final-exam', + '/api/v1/teacher/add-participation', + '/api/v1/teacher/add-project', + ] as $endpoint) { + $response = $this->postJson($endpoint, $scorePayload); + $this->assertNoServerError($response, 'Legacy score post alias ' . $endpoint); + } + + $progress = $this->postJson('/api/v1/teacher/class-progress-submit', [ + 'class_section_id' => $classSectionId, + 'week_start' => '2025-10-05', + 'week_end' => '2025-10-11', + 'covered_islamic' => 'Legacy form Islamic coverage.', + 'covered_quran' => 'Legacy form Quran coverage.', + 'homework_islamic' => 'Legacy Islamic homework.', + 'homework_quran' => 'Legacy Quran homework.', + ]); + $this->assertNoServerError($progress, 'Legacy class progress submit'); + } + + public function test_validation_errors_are_structured_for_representative_public_and_authenticated_forms(): void + { + $this->seedE2EConfiguration(); + + $this->postJson('/api/v1/auth/login', [ + 'email' => 'not-an-email', + 'password' => '', + ])->assertStatus(422) + ->assertJsonStructure(['message', 'errors']); + + $this->postJson('/api/v1/contact', [ + 'name' => '', + 'email' => 'bad', + 'message' => '', + ])->assertStatus(422) + ->assertJsonStructure(['message', 'errors']); + + $admin = $this->actingAsApiAdministrator(); + $this->actingAs($admin, 'api'); + + $this->postJson('/api/v1/users', [ + 'firstname' => '', + 'email' => 'not-an-email', + ])->assertStatus(422) + ->assertJsonStructure(['message', 'errors']); + + $this->postJson('/api/v1/students', [ + 'firstname' => '', + 'parent_id' => 999999, + ])->assertStatus(422) + ->assertJsonStructure(['message', 'errors']); + + $this->postJson('/api/v1/finance/payments', [ + 'invoice_id' => 999999, + 'amount' => -10, + ])->assertStatus(422) + ->assertJsonStructure(['message', 'errors']); + } + + public function test_legacy_redirects_and_non_versioned_auth_aliases_remain_available(): void + { + $this->get('/api/attendance-templates') + ->assertRedirect('/api/v1/attendance-templates'); + $this->get('/api/attendance-comment-templates') + ->assertRedirect('/api/v1/attendance-comment-templates'); + $this->get('/api/administrator/attendance-templates') + ->assertRedirect('/api/v1/administrator/attendance-templates'); + + $this->assertStatusIn( + $this->postJson('/api/login', [ + 'email' => 'missing@example.test', + 'password' => 'wrong-password', + ]), + [401, 422], + 'Non-versioned login alias failure contract' + ); + + $this->postJson('/api/register', []) + ->assertStatus(422) + ->assertJsonStructure(['message', 'errors']); + } +} diff --git a/tests/Feature/Api/V1/Workflows/StudentEnrollmentWorkflowTest.php b/tests/Feature/Api/V1/Workflows/StudentEnrollmentWorkflowTest.php index 4a5c91b0..aafc30fc 100644 --- a/tests/Feature/Api/V1/Workflows/StudentEnrollmentWorkflowTest.php +++ b/tests/Feature/Api/V1/Workflows/StudentEnrollmentWorkflowTest.php @@ -26,7 +26,6 @@ class StudentEnrollmentWorkflowTest extends TestCase use RefreshDatabase; private const SCHOOL_YEAR = '2025-2026'; - private const SEMESTER = 'Fall'; public function test_admin_creates_account_adds_student_and_enrolls_them(): void @@ -128,14 +127,14 @@ class StudentEnrollmentWorkflowTest extends TestCase ]); // The class roster endpoint reflects the assignment. - $classes = $this->getJson('/api/v1/students/'.$studentId.'/classes'); + $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?'. + '/api/v1/administrator/enrollment-withdrawal/update-statuses?' . http_build_query(['enrollment_status' => [$studentId => 'enrolled']]), [] ); @@ -195,14 +194,14 @@ class StudentEnrollmentWorkflowTest extends TestCase // First enrol... $this->post( - '/api/v1/administrator/enrollment-withdrawal/update-statuses?'. + '/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?'. + '/api/v1/administrator/enrollment-withdrawal/update-statuses?' . http_build_query(['enrollment_status' => [$studentId => 'withdrawn']]), [] )->assertOk(); diff --git a/tests/Feature/BulkUserRoleCreationTest.php b/tests/Feature/BulkUserRoleCreationTest.php index 4d772c88..2295f6af 100644 --- a/tests/Feature/BulkUserRoleCreationTest.php +++ b/tests/Feature/BulkUserRoleCreationTest.php @@ -104,7 +104,7 @@ class BulkUserRoleCreationTest extends TestCase 'firstname' => "Bulk{$name}", 'lastname' => "User{$number}", 'gender' => $index % 2 === 0 ? 'Female' : 'Male', - 'cellphone' => '555'.str_pad((string) $index, 7, '0', STR_PAD_LEFT), + 'cellphone' => '555' . str_pad((string) $index, 7, '0', STR_PAD_LEFT), 'email' => "bulk_api_{$roleName}_{$number}@example.test", 'address_street' => "{$index} Test Street", 'city' => 'Brooklyn', diff --git a/tests/Feature/Middleware/RequirePermissionMiddlewareTest.php b/tests/Feature/Middleware/RequirePermissionMiddlewareTest.php index 16ea4d60..a6467f88 100644 --- a/tests/Feature/Middleware/RequirePermissionMiddlewareTest.php +++ b/tests/Feature/Middleware/RequirePermissionMiddlewareTest.php @@ -2,9 +2,9 @@ namespace Tests\Feature\Middleware; -use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\DB; +use App\Models\User; use Illuminate\Support\Facades\Route; use Tests\TestCase; diff --git a/tests/Feature/Web/SchoolYearAdminPageTest.php b/tests/Feature/Web/SchoolYearAdminPageTest.php index 92b62f4e..5a5565e9 100644 --- a/tests/Feature/Web/SchoolYearAdminPageTest.php +++ b/tests/Feature/Web/SchoolYearAdminPageTest.php @@ -16,12 +16,12 @@ class SchoolYearAdminPageTest extends TestCase parent::setUp(); $compiledPath = storage_path('framework/views'); - if (! is_dir($compiledPath)) { + if (!is_dir($compiledPath)) { mkdir($compiledPath, 0777, true); } config([ - 'app.key' => 'base64:'.base64_encode(random_bytes(32)), + 'app.key' => 'base64:' . base64_encode(random_bytes(32)), 'app.cipher' => 'AES-256-CBC', 'jwt.secret' => bin2hex(random_bytes(32)), 'view.compiled' => $compiledPath, diff --git a/tests/Unit/ApiLoginSecurityServiceTest.php b/tests/Unit/ApiLoginSecurityServiceTest.php index 06bb63dd..ec4680e8 100644 --- a/tests/Unit/ApiLoginSecurityServiceTest.php +++ b/tests/Unit/ApiLoginSecurityServiceTest.php @@ -10,7 +10,7 @@ class ApiLoginSecurityServiceTest extends TestCase { public function test_roles_object_map_preserves_role_names(): void { - $service = new ApiLoginSecurityService; + $service = new ApiLoginSecurityService(); $map = $service->rolesToObjectMap(['Teacher', 'Admin']); $this->assertTrue($map->Teacher); @@ -24,7 +24,7 @@ class ApiLoginSecurityServiceTest extends TestCase public function test_build_login_response_includes_teacher_class_context(): void { - $service = new ApiLoginSecurityService; + $service = new ApiLoginSecurityService(); $user = $this->getMockBuilder(User::class) ->onlyMethods(['roleNames', 'teacherSessionContext']) @@ -57,7 +57,7 @@ class ApiLoginSecurityServiceTest extends TestCase public function test_build_login_response_includes_teacher_assistant_class_context(): void { - $service = new ApiLoginSecurityService; + $service = new ApiLoginSecurityService(); $user = $this->getMockBuilder(User::class) ->onlyMethods(['roleNames', 'teacherSessionContext']) diff --git a/tests/Unit/AuthSessionServiceTest.php b/tests/Unit/AuthSessionServiceTest.php index caee73e7..87e20fc5 100644 --- a/tests/Unit/AuthSessionServiceTest.php +++ b/tests/Unit/AuthSessionServiceTest.php @@ -15,7 +15,7 @@ class AuthSessionServiceTest extends TestCase parent::setUp(); // ApplicationUrlService is final (cannot be mocked); these tests only exercise // relative-path sanitization and never call docsHomeUrl(). - $this->svc = new AuthSessionService(new ApplicationUrlService); + $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 e38d7080..26cf43d9 100644 --- a/tests/Unit/Controllers/AttendanceCommentTemplateControllerTest.php +++ b/tests/Unit/Controllers/AttendanceCommentTemplateControllerTest.php @@ -13,7 +13,7 @@ class AttendanceCommentTemplateControllerTest extends TestCase { private function makeController(AttendanceCommentTemplateService $service): AttendanceCommentTemplateController { - return new AttendanceCommentTemplateController($service, new ApplicationUrlService); + return new AttendanceCommentTemplateController($service, new ApplicationUrlService()); } protected function tearDown(): void @@ -190,4 +190,4 @@ class AttendanceCommentTemplateControllerTest extends TestCase $this->assertArrayHasKey('templates', $data); $this->assertCount(1, $data['templates']); } -} +} \ No newline at end of file diff --git a/tests/Unit/Grading/ScoreValueValidatorTest.php b/tests/Unit/Grading/ScoreValueValidatorTest.php index 6b306877..2caebf76 100644 --- a/tests/Unit/Grading/ScoreValueValidatorTest.php +++ b/tests/Unit/Grading/ScoreValueValidatorTest.php @@ -10,7 +10,7 @@ class ScoreValueValidatorTest extends TestCase { public function test_blank_score_remains_null(): void { - $validator = new ScoreValueValidator; + $validator = new ScoreValueValidator(); $this->assertNull($validator->normalizeNullable('', 100)); $this->assertNull($validator->normalizeNullable(null, 100)); @@ -18,7 +18,7 @@ class ScoreValueValidatorTest extends TestCase public function test_score_must_be_inside_range(): void { - $validator = new ScoreValueValidator; + $validator = new ScoreValueValidator(); $this->assertSame(95.0, $validator->normalizeNullable('95', 100)); @@ -28,7 +28,7 @@ class ScoreValueValidatorTest extends TestCase public function test_status_inference_preserves_legacy_pending_behavior(): void { - $validator = new ScoreValueValidator; + $validator = new ScoreValueValidator(); $this->assertSame('scored', $validator->inferStatus(80.0)); $this->assertSame('pending', $validator->inferStatus(null, false)); diff --git a/tests/Unit/Models/AdditionalChargeTest.php b/tests/Unit/Models/AdditionalChargeTest.php index d1f1ceca..8ff72454 100644 --- a/tests/Unit/Models/AdditionalChargeTest.php +++ b/tests/Unit/Models/AdditionalChargeTest.php @@ -133,7 +133,7 @@ class AdditionalChargeTest extends TestCase 'created_by' => null, ]); - $model = new AdditionalCharge; + $model = new AdditionalCharge(); $result = $model->markApplied([$first->id, (string) $second->id], $invoiceId); $this->assertTrue($result); @@ -173,7 +173,7 @@ class AdditionalChargeTest extends TestCase 'created_by' => null, ]); - $paginator = (new AdditionalCharge)->listAllForTerm('2024-2025', 'Spring', null, null, 50, true); + $paginator = (new AdditionalCharge())->listAllForTerm('2024-2025', 'Spring', null, null, 50, true); $this->assertSame(1, $paginator->total()); $first = $paginator->items()[0]; @@ -225,19 +225,19 @@ class AdditionalChargeTest extends TestCase 'created_by' => null, ]); - $paginator = (new AdditionalCharge)->listAllForTerm('2024-2025', 'Fall', null, 'Math', 50, true); + $paginator = (new AdditionalCharge())->listAllForTerm('2024-2025', 'Fall', null, 'Math', 50, true); $this->assertSame(1, $paginator->total()); $this->assertSame('Math Club Fee', $paginator->items()[0]->title); - $byDescription = (new AdditionalCharge)->listAllForTerm('2024-2025', 'Fall', null, 'enrichment', 50, true); + $byDescription = (new AdditionalCharge())->listAllForTerm('2024-2025', 'Fall', null, 'enrichment', 50, true); $this->assertSame(1, $byDescription->total()); $this->assertSame('Math Club Fee', $byDescription->items()[0]->title); - $byParentName = (new AdditionalCharge)->listAllForTerm('2024-2025', 'Fall', null, 'Hassan', 50, true); + $byParentName = (new AdditionalCharge())->listAllForTerm('2024-2025', 'Fall', null, 'Hassan', 50, true); $this->assertSame(2, $byParentName->total()); - $byInvoice = (new AdditionalCharge)->listAllForTerm('2024-2025', 'Fall', null, 'INV-2025-001', 50, true); + $byInvoice = (new AdditionalCharge())->listAllForTerm('2024-2025', 'Fall', null, 'INV-2025-001', 50, true); $this->assertSame(2, $byInvoice->total()); } } diff --git a/tests/Unit/Models/AttendanceCommentTemplateModelTest.php b/tests/Unit/Models/AttendanceCommentTemplateModelTest.php index 399b21b8..d60a6d2c 100644 --- a/tests/Unit/Models/AttendanceCommentTemplateModelTest.php +++ b/tests/Unit/Models/AttendanceCommentTemplateModelTest.php @@ -48,7 +48,7 @@ class AttendanceCommentTemplateModelTest extends TestCase 'is_active' => 1, ]); - $model = new AttendanceCommentTemplate; + $model = new AttendanceCommentTemplate(); $results = $model->getActiveTemplates(); $this->assertCount(2, $results); diff --git a/tests/Unit/Models/AttendanceDataTest.php b/tests/Unit/Models/AttendanceDataTest.php index 57211980..5e8716a0 100644 --- a/tests/Unit/Models/AttendanceDataTest.php +++ b/tests/Unit/Models/AttendanceDataTest.php @@ -66,7 +66,7 @@ class AttendanceDataTest extends TestCase 'modified_by' => null, ]); - $model = new AttendanceData; + $model = new AttendanceData(); $rows = $model->getAttendanceByClass(1, 2, '2024-09-01'); $this->assertCount(1, $rows); @@ -125,7 +125,7 @@ class AttendanceDataTest extends TestCase ], ]); - $model = new AttendanceData; + $model = new AttendanceData(); $rows = $model->unreportedTermRows([1, 2, 3], '2024-2025', 'Fall'); $this->assertCount(1, $rows); @@ -182,7 +182,7 @@ class AttendanceDataTest extends TestCase ], ]); - $model = new AttendanceData; + $model = new AttendanceData(); $rows = $model->getUnreportedAbsencesAndLates('2024-2025', 'Fall'); $this->assertArrayHasKey(10, $rows); @@ -209,7 +209,7 @@ class AttendanceDataTest extends TestCase 'updated_at' => now(), ]); - $model = new AttendanceData; + $model = new AttendanceData(); $result = $model->markReportedAndNotified(20, ['2024-09-10'], 'Fall', '2024-2025'); $this->assertTrue($result); diff --git a/tests/Unit/Models/AttendanceEmailTemplateTest.php b/tests/Unit/Models/AttendanceEmailTemplateTest.php index 93ee46c1..48177958 100644 --- a/tests/Unit/Models/AttendanceEmailTemplateTest.php +++ b/tests/Unit/Models/AttendanceEmailTemplateTest.php @@ -28,7 +28,7 @@ class AttendanceEmailTemplateTest extends TestCase 'is_active' => true, ]); - $result = (new AttendanceEmailTemplate)->getTemplate('attendance_absent', 'answered'); + $result = (new AttendanceEmailTemplate())->getTemplate('attendance_absent', 'answered'); $this->assertIsArray($result); $this->assertSame('answered', $result['variant']); @@ -45,7 +45,7 @@ class AttendanceEmailTemplateTest extends TestCase 'is_active' => true, ]); - $result = (new AttendanceEmailTemplate)->getTemplate('attendance_late', 'answered'); + $result = (new AttendanceEmailTemplate())->getTemplate('attendance_late', 'answered'); $this->assertIsArray($result); $this->assertSame('default', $result['variant']); @@ -70,7 +70,7 @@ class AttendanceEmailTemplateTest extends TestCase 'is_active' => true, ]); - $result = (new AttendanceEmailTemplate)->getTemplate('attendance_present', 'no_answer'); + $result = (new AttendanceEmailTemplate())->getTemplate('attendance_present', 'no_answer'); $this->assertIsArray($result); $this->assertSame('default', $result['variant']); @@ -87,7 +87,7 @@ class AttendanceEmailTemplateTest extends TestCase 'is_active' => false, ]); - $result = (new AttendanceEmailTemplate)->getTemplate('attendance_unknown', 'anything'); + $result = (new AttendanceEmailTemplate())->getTemplate('attendance_unknown', 'anything'); $this->assertNull($result); } diff --git a/tests/Unit/Models/AttendanceRecordTest.php b/tests/Unit/Models/AttendanceRecordTest.php index 0a537376..2bbd0a02 100644 --- a/tests/Unit/Models/AttendanceRecordTest.php +++ b/tests/Unit/Models/AttendanceRecordTest.php @@ -9,16 +9,16 @@ class AttendanceRecordTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new AttendanceRecord; + $model = new AttendanceRecord(); $this->assertSame('attendance_record', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(true, $model->timestamps); - $this->assertSame(['class_section_id', 'student_id', 'school_id', 'total_absence', 'total_late', 'total_presence', 'total_attendance', 'semester', 'school_year', 'modified_by', 'created_at', 'updated_at'], $model->getFillable()); + $this->assertSame(["class_section_id", "student_id", "school_id", "total_absence", "total_late", "total_presence", "total_attendance", "semester", "school_year", "modified_by", "created_at", "updated_at"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(AttendanceRecord::class, new AttendanceRecord); + $this->assertInstanceOf(AttendanceRecord::class, new AttendanceRecord()); } } diff --git a/tests/Unit/Models/AttendanceTrackingTest.php b/tests/Unit/Models/AttendanceTrackingTest.php index e064914f..c0b3d251 100644 --- a/tests/Unit/Models/AttendanceTrackingTest.php +++ b/tests/Unit/Models/AttendanceTrackingTest.php @@ -9,16 +9,16 @@ class AttendanceTrackingTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new AttendanceTracking; + $model = new AttendanceTracking(); $this->assertSame('attendance_tracking', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(true, $model->timestamps); - $this->assertSame(['student_id', 'date', 'is_reported', 'reason', 'note', 'is_notified', 'notif_counter', 'semester', 'school_year', 'created_at', 'updated_at'], $model->getFillable()); + $this->assertSame(["student_id", "date", "is_reported", "reason", "note", "is_notified", "notif_counter", "semester", "school_year", "created_at", "updated_at"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(AttendanceTracking::class, new AttendanceTracking); + $this->assertInstanceOf(AttendanceTracking::class, new AttendanceTracking()); } } diff --git a/tests/Unit/Models/AuthorizedUserTest.php b/tests/Unit/Models/AuthorizedUserTest.php index fbd84cbf..9a07e756 100644 --- a/tests/Unit/Models/AuthorizedUserTest.php +++ b/tests/Unit/Models/AuthorizedUserTest.php @@ -9,16 +9,16 @@ class AuthorizedUserTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new AuthorizedUser; + $model = new AuthorizedUser(); $this->assertSame('authorized_users', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(true, $model->timestamps); - $this->assertSame(['user_id', 'authorized_user_id', 'firstname', 'lastname', 'phone_number', 'gender', 'email', 'relation_to_user', 'token', 'status', 'created_at', 'updated_at'], $model->getFillable()); + $this->assertSame(["user_id", "authorized_user_id", "firstname", "lastname", "phone_number", "gender", "email", "relation_to_user", "token", "status", "created_at", "updated_at"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(AuthorizedUser::class, new AuthorizedUser); + $this->assertInstanceOf(AuthorizedUser::class, new AuthorizedUser()); } } diff --git a/tests/Unit/Models/BadgePrintLogTest.php b/tests/Unit/Models/BadgePrintLogTest.php index 23167fd4..1707cb56 100644 --- a/tests/Unit/Models/BadgePrintLogTest.php +++ b/tests/Unit/Models/BadgePrintLogTest.php @@ -9,16 +9,16 @@ class BadgePrintLogTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new BadgePrintLog; + $model = new BadgePrintLog(); $this->assertSame('badge_print_logs', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(false, $model->timestamps); - $this->assertSame(['user_id', 'printed_by', 'school_year', 'printed_at', 'role', 'class_section_name', 'copies'], $model->getFillable()); + $this->assertSame(["user_id", "printed_by", "school_year", "printed_at", "role", "class_section_name", "copies"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(BadgePrintLog::class, new BadgePrintLog); + $this->assertInstanceOf(BadgePrintLog::class, new BadgePrintLog()); } } diff --git a/tests/Unit/Models/CalendarTest.php b/tests/Unit/Models/CalendarTest.php index c64ee20a..6082a0c0 100644 --- a/tests/Unit/Models/CalendarTest.php +++ b/tests/Unit/Models/CalendarTest.php @@ -9,16 +9,16 @@ class CalendarTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new Calendar; + $model = new Calendar(); $this->assertSame('calendar_events', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(true, $model->timestamps); - $this->assertSame(['title', 'date', 'description', 'notify_parent', 'notify_admin', 'notify_teacher', 'no_school', 'semester', 'school_year', 'notification_sent', 'created_at', 'updated_at'], $model->getFillable()); + $this->assertSame(["title", "date", "description", "notify_parent", "notify_admin", "notify_teacher", "no_school", "semester", "school_year", "notification_sent", "created_at", "updated_at"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(Calendar::class, new Calendar); + $this->assertInstanceOf(Calendar::class, new Calendar()); } } diff --git a/tests/Unit/Models/ClassPrepAdjustmentTest.php b/tests/Unit/Models/ClassPrepAdjustmentTest.php index 4205b45b..5402f76c 100644 --- a/tests/Unit/Models/ClassPrepAdjustmentTest.php +++ b/tests/Unit/Models/ClassPrepAdjustmentTest.php @@ -9,16 +9,16 @@ class ClassPrepAdjustmentTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new ClassPrepAdjustment; + $model = new ClassPrepAdjustment(); $this->assertSame('class_prep_adjustments', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(false, $model->timestamps); - $this->assertSame(['class_section_id', 'item_name', 'adjustment', 'adjustable', 'school_year', 'created_at'], $model->getFillable()); + $this->assertSame(["class_section_id", "item_name", "adjustment", "adjustable", "school_year", "created_at"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(ClassPrepAdjustment::class, new ClassPrepAdjustment); + $this->assertInstanceOf(ClassPrepAdjustment::class, new ClassPrepAdjustment()); } } diff --git a/tests/Unit/Models/ClassPreparationLogTest.php b/tests/Unit/Models/ClassPreparationLogTest.php index 2dae9b8d..2756d54c 100644 --- a/tests/Unit/Models/ClassPreparationLogTest.php +++ b/tests/Unit/Models/ClassPreparationLogTest.php @@ -9,16 +9,16 @@ class ClassPreparationLogTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new ClassPreparationLog; + $model = new ClassPreparationLog(); $this->assertSame('class_preparation_log', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(false, $model->timestamps); - $this->assertSame(['class_section_id', 'class_section', 'school_year', 'prep_data', 'created_at'], $model->getFillable()); + $this->assertSame(["class_section_id", "class_section", "school_year", "prep_data", "created_at"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(ClassPreparationLog::class, new ClassPreparationLog); + $this->assertInstanceOf(ClassPreparationLog::class, new ClassPreparationLog()); } } diff --git a/tests/Unit/Models/ClassProgressAttachmentTest.php b/tests/Unit/Models/ClassProgressAttachmentTest.php index 8ac34ef5..57fc0f56 100644 --- a/tests/Unit/Models/ClassProgressAttachmentTest.php +++ b/tests/Unit/Models/ClassProgressAttachmentTest.php @@ -9,16 +9,16 @@ class ClassProgressAttachmentTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new ClassProgressAttachment; + $model = new ClassProgressAttachment(); $this->assertSame('class_progress_attachments', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(false, $model->timestamps); - $this->assertSame(['report_id', 'file_path', 'original_name', 'mime_type', 'file_size', 'created_at'], $model->getFillable()); + $this->assertSame(["report_id", "file_path", "original_name", "mime_type", "file_size", "created_at"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(ClassProgressAttachment::class, new ClassProgressAttachment); + $this->assertInstanceOf(ClassProgressAttachment::class, new ClassProgressAttachment()); } } diff --git a/tests/Unit/Models/ClassProgressReportTest.php b/tests/Unit/Models/ClassProgressReportTest.php index f7b9e1ac..bea0c51b 100644 --- a/tests/Unit/Models/ClassProgressReportTest.php +++ b/tests/Unit/Models/ClassProgressReportTest.php @@ -9,16 +9,16 @@ class ClassProgressReportTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new ClassProgressReport; + $model = new ClassProgressReport(); $this->assertSame('class_progress_reports', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(true, $model->timestamps); - $this->assertSame(['class_section_id', 'teacher_id', 'week_start', 'week_end', 'subject', 'unit_title', 'materials', 'covered', 'homework', 'assessment', 'status', 'status_notes', 'class_notes', 'next_week_plan', 'support_needed', 'flags_json', 'attachment_path', 'created_at', 'updated_at'], $model->getFillable()); + $this->assertSame(["class_section_id", "teacher_id", "week_start", "week_end", "subject", "unit_title", "materials", "covered", "homework", "assessment", "status", "status_notes", "class_notes", "next_week_plan", "support_needed", "flags_json", "attachment_path", "created_at", "updated_at"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(ClassProgressReport::class, new ClassProgressReport); + $this->assertInstanceOf(ClassProgressReport::class, new ClassProgressReport()); } } diff --git a/tests/Unit/Models/ClassSectionTest.php b/tests/Unit/Models/ClassSectionTest.php index 629b0303..639f1970 100644 --- a/tests/Unit/Models/ClassSectionTest.php +++ b/tests/Unit/Models/ClassSectionTest.php @@ -9,16 +9,16 @@ class ClassSectionTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new ClassSection; + $model = new ClassSection(); $this->assertSame('classSection', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(true, $model->timestamps); - $this->assertSame(['class_id', 'class_section_id', 'class_section_name', 'created_at', 'updated_at', 'semester', 'school_year'], $model->getFillable()); + $this->assertSame(["class_id", "class_section_id", "class_section_name", "created_at", "updated_at", "semester", "school_year"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(ClassSection::class, new ClassSection); + $this->assertInstanceOf(ClassSection::class, new ClassSection()); } } diff --git a/tests/Unit/Models/ClassTest.php b/tests/Unit/Models/ClassTest.php index 8dae2386..a19ff4db 100644 --- a/tests/Unit/Models/ClassTest.php +++ b/tests/Unit/Models/ClassTest.php @@ -9,16 +9,16 @@ class ClassTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new SchoolClass; + $model = new SchoolClass(); $this->assertSame('classes', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(false, $model->timestamps); - $this->assertSame(['class_name', 'schedule', 'capacity', 'semester', 'school_year'], $model->getFillable()); + $this->assertSame(["class_name", "schedule", "capacity", "semester", "school_year"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(SchoolClass::class, new SchoolClass); + $this->assertInstanceOf(SchoolClass::class, new SchoolClass()); } } diff --git a/tests/Unit/Models/CommunicationLogTest.php b/tests/Unit/Models/CommunicationLogTest.php index 0b284957..2f8b6b54 100644 --- a/tests/Unit/Models/CommunicationLogTest.php +++ b/tests/Unit/Models/CommunicationLogTest.php @@ -9,16 +9,16 @@ class CommunicationLogTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new CommunicationLog; + $model = new CommunicationLog(); $this->assertSame('communication_logs', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(false, $model->timestamps); - $this->assertSame(['student_id', 'family_id', 'student_name', 'template_key', 'subject', 'body', 'recipients', 'cc', 'bcc', 'attachments', 'status', 'error_message', 'sent_by', 'metadata'], $model->getFillable()); + $this->assertSame(["student_id", "family_id", "student_name", "template_key", "subject", "body", "recipients", "cc", "bcc", "attachments", "status", "error_message", "sent_by", "metadata"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(CommunicationLog::class, new CommunicationLog); + $this->assertInstanceOf(CommunicationLog::class, new CommunicationLog()); } } diff --git a/tests/Unit/Models/CompetitionClassWinnerTest.php b/tests/Unit/Models/CompetitionClassWinnerTest.php index 7c4151bc..a860bd7b 100644 --- a/tests/Unit/Models/CompetitionClassWinnerTest.php +++ b/tests/Unit/Models/CompetitionClassWinnerTest.php @@ -9,16 +9,16 @@ class CompetitionClassWinnerTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new CompetitionClassWinner; + $model = new CompetitionClassWinner(); $this->assertSame('competition_class_winners', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(true, $model->timestamps); - $this->assertSame(['competition_id', 'class_section_id', 'winners_override', 'created_at', 'updated_at', 'question_count', 'prize_1', 'prize_2', 'prize_3', 'prize_4', 'prize_5', 'prize_6'], $model->getFillable()); + $this->assertSame(["competition_id", "class_section_id", "winners_override", "created_at", "updated_at", "question_count", "prize_1", "prize_2", "prize_3", "prize_4", "prize_5", "prize_6"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(CompetitionClassWinner::class, new CompetitionClassWinner); + $this->assertInstanceOf(CompetitionClassWinner::class, new CompetitionClassWinner()); } } diff --git a/tests/Unit/Models/CompetitionScoreTest.php b/tests/Unit/Models/CompetitionScoreTest.php index 71566a7e..2dc1eb9c 100644 --- a/tests/Unit/Models/CompetitionScoreTest.php +++ b/tests/Unit/Models/CompetitionScoreTest.php @@ -9,16 +9,16 @@ class CompetitionScoreTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new CompetitionScore; + $model = new CompetitionScore(); $this->assertSame('competition_scores', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(true, $model->timestamps); - $this->assertSame(['competition_id', 'student_id', 'class_section_id', 'score', 'notes', 'created_at', 'updated_at'], $model->getFillable()); + $this->assertSame(["competition_id", "student_id", "class_section_id", "score", "notes", "created_at", "updated_at"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(CompetitionScore::class, new CompetitionScore); + $this->assertInstanceOf(CompetitionScore::class, new CompetitionScore()); } } diff --git a/tests/Unit/Models/CompetitionTest.php b/tests/Unit/Models/CompetitionTest.php index 1aee2371..cde878b3 100644 --- a/tests/Unit/Models/CompetitionTest.php +++ b/tests/Unit/Models/CompetitionTest.php @@ -3,24 +3,24 @@ namespace Tests\Unit\Models; use App\Models\Competition; -use Illuminate\Database\Eloquent\SoftDeletes; use PHPUnit\Framework\TestCase; +use Illuminate\Database\Eloquent\SoftDeletes; class CompetitionTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new Competition; + $model = new Competition(); $this->assertSame('competitions', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(true, $model->timestamps); - $this->assertSame(['title', 'semester', 'school_year', 'class_section_id', 'start_date', 'end_date', 'winners_count', 'prize_per_point', 'is_published', 'published_at', 'created_by', 'created_at', 'updated_at', 'deleted_at', 'is_locked', 'locked_at', 'locked_by'], $model->getFillable()); + $this->assertSame(["title", "semester", "school_year", "class_section_id", "start_date", "end_date", "winners_count", "prize_per_point", "is_published", "published_at", "created_by", "created_at", "updated_at", "deleted_at", "is_locked", "locked_at", "locked_by"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(Competition::class, new Competition); + $this->assertInstanceOf(Competition::class, new Competition()); $this->assertContains(SoftDeletes::class, class_uses_recursive(Competition::class)); } } diff --git a/tests/Unit/Models/CompetitionWinnerTest.php b/tests/Unit/Models/CompetitionWinnerTest.php index 408c419b..b058ec2a 100644 --- a/tests/Unit/Models/CompetitionWinnerTest.php +++ b/tests/Unit/Models/CompetitionWinnerTest.php @@ -9,16 +9,16 @@ class CompetitionWinnerTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new CompetitionWinner; + $model = new CompetitionWinner(); $this->assertSame('competition_winners', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(false, $model->timestamps); - $this->assertSame(['competition_id', 'student_id', 'class_section_id', 'rank', 'score', 'prize_amount', 'created_at'], $model->getFillable()); + $this->assertSame(["competition_id", "student_id", "class_section_id", "rank", "score", "prize_amount", "created_at"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(CompetitionWinner::class, new CompetitionWinner); + $this->assertInstanceOf(CompetitionWinner::class, new CompetitionWinner()); } } diff --git a/tests/Unit/Models/ConfigurationTest.php b/tests/Unit/Models/ConfigurationTest.php index d2f75a8b..78763ce2 100644 --- a/tests/Unit/Models/ConfigurationTest.php +++ b/tests/Unit/Models/ConfigurationTest.php @@ -9,16 +9,16 @@ class ConfigurationTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new Configuration; + $model = new Configuration(); $this->assertSame('configuration', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(false, $model->timestamps); - $this->assertSame(['config_key', 'config_value'], $model->getFillable()); + $this->assertSame(["config_key", "config_value"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(Configuration::class, new Configuration); + $this->assertInstanceOf(Configuration::class, new Configuration()); } } diff --git a/tests/Unit/Models/ContactUsTest.php b/tests/Unit/Models/ContactUsTest.php index e3ba2100..86149a27 100644 --- a/tests/Unit/Models/ContactUsTest.php +++ b/tests/Unit/Models/ContactUsTest.php @@ -9,16 +9,16 @@ class ContactUsTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new ContactUs; + $model = new ContactUs(); $this->assertSame('contactus', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(true, $model->timestamps); - $this->assertSame(['sender_id', 'reciever_id', 'subject', 'message', 'created_at', 'updated_at', 'semester', 'school_year'], $model->getFillable()); + $this->assertSame(["sender_id", "reciever_id", "subject", "message", "created_at", "updated_at", "semester", "school_year"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(ContactUs::class, new ContactUs); + $this->assertInstanceOf(ContactUs::class, new ContactUs()); } } diff --git a/tests/Unit/Models/CurrentFlagTest.php b/tests/Unit/Models/CurrentFlagTest.php index 983f8edf..cde6c2e4 100644 --- a/tests/Unit/Models/CurrentFlagTest.php +++ b/tests/Unit/Models/CurrentFlagTest.php @@ -9,16 +9,16 @@ class CurrentFlagTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new CurrentFlag; + $model = new CurrentFlag(); $this->assertSame('current_flag', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(true, $model->timestamps); - $this->assertSame(['student_id', 'student_name', 'grade', 'flag', 'flag_datetime', 'flag_state', 'updated_by_open', 'open_description', 'updated_by_closed', 'close_description', 'action_taken', 'updated_by_canceled', 'cancel_description', 'semester', 'school_year', 'created_at', 'updated_at'], $model->getFillable()); + $this->assertSame(["student_id", "student_name", "grade", "flag", "flag_datetime", "flag_state", "updated_by_open", "open_description", "updated_by_closed", "close_description", "action_taken", "updated_by_canceled", "cancel_description", "semester", "school_year", "created_at", "updated_at"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(CurrentFlag::class, new CurrentFlag); + $this->assertInstanceOf(CurrentFlag::class, new CurrentFlag()); } } diff --git a/tests/Unit/Models/DiscountUsageTest.php b/tests/Unit/Models/DiscountUsageTest.php index b8753ed2..a5600163 100644 --- a/tests/Unit/Models/DiscountUsageTest.php +++ b/tests/Unit/Models/DiscountUsageTest.php @@ -9,16 +9,16 @@ class DiscountUsageTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new DiscountUsage; + $model = new DiscountUsage(); $this->assertSame('discount_usages', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(true, $model->timestamps); - $this->assertSame(['voucher_id', 'invoice_id', 'parent_id', 'discount_amount', 'description', 'school_year', 'updated_by', 'used_at', 'created_at', 'updated_at', 'semester'], $model->getFillable()); + $this->assertSame(["voucher_id", "invoice_id", "parent_id", "discount_amount", "description", "school_year", "updated_by", "used_at", "created_at", "updated_at", "semester"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(DiscountUsage::class, new DiscountUsage); + $this->assertInstanceOf(DiscountUsage::class, new DiscountUsage()); } } diff --git a/tests/Unit/Models/DiscountVoucherTest.php b/tests/Unit/Models/DiscountVoucherTest.php index 1676d2ae..f9527db9 100644 --- a/tests/Unit/Models/DiscountVoucherTest.php +++ b/tests/Unit/Models/DiscountVoucherTest.php @@ -9,16 +9,16 @@ class DiscountVoucherTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new DiscountVoucher; + $model = new DiscountVoucher(); $this->assertSame('discount_vouchers', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(true, $model->timestamps); - $this->assertSame(['code', 'discount_type', 'description', 'discount_value', 'max_uses', 'times_used', 'valid_from', 'valid_until', 'is_active', 'created_at', 'updated_at', 'school_year', 'semester'], $model->getFillable()); + $this->assertSame(["code", "discount_type", "description", "discount_value", "max_uses", "times_used", "valid_from", "valid_until", "is_active", "created_at", "updated_at", "school_year", "semester"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(DiscountVoucher::class, new DiscountVoucher); + $this->assertInstanceOf(DiscountVoucher::class, new DiscountVoucher()); } } diff --git a/tests/Unit/Models/EarlyDismissalSignatureTest.php b/tests/Unit/Models/EarlyDismissalSignatureTest.php index b65ad4b6..f6fabd94 100644 --- a/tests/Unit/Models/EarlyDismissalSignatureTest.php +++ b/tests/Unit/Models/EarlyDismissalSignatureTest.php @@ -9,16 +9,16 @@ class EarlyDismissalSignatureTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new EarlyDismissalSignature; + $model = new EarlyDismissalSignature(); $this->assertSame('early_dismissal_signatures', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(true, $model->timestamps); - $this->assertSame(['report_date', 'school_year', 'semester', 'filename', 'original_name', 'mime_type', 'file_size', 'uploaded_by', 'created_at', 'updated_at'], $model->getFillable()); + $this->assertSame(["report_date", "school_year", "semester", "filename", "original_name", "mime_type", "file_size", "uploaded_by", "created_at", "updated_at"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(EarlyDismissalSignature::class, new EarlyDismissalSignature); + $this->assertInstanceOf(EarlyDismissalSignature::class, new EarlyDismissalSignature()); } } diff --git a/tests/Unit/Models/EmergencyContactTest.php b/tests/Unit/Models/EmergencyContactTest.php index 8d67861d..d1def549 100644 --- a/tests/Unit/Models/EmergencyContactTest.php +++ b/tests/Unit/Models/EmergencyContactTest.php @@ -9,16 +9,16 @@ class EmergencyContactTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new EmergencyContact; + $model = new EmergencyContact(); $this->assertSame('emergency_contacts', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(true, $model->timestamps); - $this->assertSame(['emergency_contact_name', 'parent_id', 'cellphone', 'email', 'relation', 'semester', 'school_year', 'created_at', 'updated_at'], $model->getFillable()); + $this->assertSame(["emergency_contact_name", "parent_id", "cellphone", "email", "relation", "semester", "school_year", "created_at", "updated_at"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(EmergencyContact::class, new EmergencyContact); + $this->assertInstanceOf(EmergencyContact::class, new EmergencyContact()); } } diff --git a/tests/Unit/Models/EnrollmentTest.php b/tests/Unit/Models/EnrollmentTest.php index 5d4a9d88..f1eb3dc0 100644 --- a/tests/Unit/Models/EnrollmentTest.php +++ b/tests/Unit/Models/EnrollmentTest.php @@ -9,16 +9,16 @@ class EnrollmentTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new Enrollment; + $model = new Enrollment(); $this->assertSame('enrollments', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(true, $model->timestamps); - $this->assertSame(['student_id', 'class_section_id', 'parent_id', 'enrollment_date', 'enrollment_status', 'withdrawal_date', 'is_withdrawn', 'admission_status', 'semester', 'school_year', 'created_at', 'updated_at'], $model->getFillable()); + $this->assertSame(["student_id", "class_section_id", "parent_id", "enrollment_date", "enrollment_status", "withdrawal_date", "is_withdrawn", "admission_status", "semester", "school_year", "created_at", "updated_at"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(Enrollment::class, new Enrollment); + $this->assertInstanceOf(Enrollment::class, new Enrollment()); } } diff --git a/tests/Unit/Models/EventChargesTest.php b/tests/Unit/Models/EventChargesTest.php index f51e21ca..2ca8483e 100644 --- a/tests/Unit/Models/EventChargesTest.php +++ b/tests/Unit/Models/EventChargesTest.php @@ -9,16 +9,16 @@ class EventChargesTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new EventCharges; + $model = new EventCharges(); $this->assertSame('event_charges', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(true, $model->timestamps); - $this->assertSame(['event_id', 'parent_id', 'student_id', 'participation', 'charged', 'semester', 'school_year', 'updated_by', 'created_at', 'updated_at'], $model->getFillable()); + $this->assertSame(["event_id", "parent_id", "student_id", "participation", "charged", "semester", "school_year", "updated_by", "created_at", "updated_at"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(EventCharges::class, new EventCharges); + $this->assertInstanceOf(EventCharges::class, new EventCharges()); } } diff --git a/tests/Unit/Models/EventTest.php b/tests/Unit/Models/EventTest.php index 7e868e8f..68053845 100644 --- a/tests/Unit/Models/EventTest.php +++ b/tests/Unit/Models/EventTest.php @@ -9,16 +9,16 @@ class EventTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new Event; + $model = new Event(); $this->assertSame('events', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(true, $model->timestamps); - $this->assertSame(['event_name', 'event_category', 'description', 'amount', 'flyer', 'expiration_date', 'semester', 'school_year', 'created_by', 'created_at', 'updated_at'], $model->getFillable()); + $this->assertSame(["event_name", "event_category", "description", "amount", "flyer", "expiration_date", "semester", "school_year", "created_by", "created_at", "updated_at"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(Event::class, new Event); + $this->assertInstanceOf(Event::class, new Event()); } } diff --git a/tests/Unit/Models/ExamDraftTest.php b/tests/Unit/Models/ExamDraftTest.php index 68295ff7..14d3add3 100644 --- a/tests/Unit/Models/ExamDraftTest.php +++ b/tests/Unit/Models/ExamDraftTest.php @@ -9,16 +9,16 @@ class ExamDraftTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new ExamDraft; + $model = new ExamDraft(); $this->assertSame('exam_drafts', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(true, $model->timestamps); - $this->assertSame(['teacher_id', 'class_section_id', 'semester', 'school_year', 'exam_type', 'draft_title', 'description', 'teacher_file', 'teacher_filename', 'status', 'admin_id', 'admin_comments', 'reviewed_at', 'final_file', 'final_filename', 'version', 'previous_draft_id', 'is_legacy', 'created_at', 'updated_at'], $model->getFillable()); + $this->assertSame(["teacher_id", "class_section_id", "semester", "school_year", "exam_type", "draft_title", "description", "teacher_file", "teacher_filename", "status", "admin_id", "admin_comments", "reviewed_at", "final_file", "final_filename", "version", "previous_draft_id", "is_legacy", "created_at", "updated_at"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(ExamDraft::class, new ExamDraft); + $this->assertInstanceOf(ExamDraft::class, new ExamDraft()); } } diff --git a/tests/Unit/Models/ExamTest.php b/tests/Unit/Models/ExamTest.php index 5240ca5d..ee45d00b 100644 --- a/tests/Unit/Models/ExamTest.php +++ b/tests/Unit/Models/ExamTest.php @@ -9,16 +9,16 @@ class ExamTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new Exam; + $model = new Exam(); $this->assertSame('exams', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(false, $model->timestamps); - $this->assertSame(['student_id', 'student_school_id', 'class_section_id', 'exam_name', 'created_at'], $model->getFillable()); + $this->assertSame(["student_id", "student_school_id", "class_section_id", "exam_name", "created_at"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(Exam::class, new Exam); + $this->assertInstanceOf(Exam::class, new Exam()); } } diff --git a/tests/Unit/Models/ExpenseTest.php b/tests/Unit/Models/ExpenseTest.php index 117b478e..63cae558 100644 --- a/tests/Unit/Models/ExpenseTest.php +++ b/tests/Unit/Models/ExpenseTest.php @@ -9,16 +9,16 @@ class ExpenseTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new Expense; + $model = new Expense(); $this->assertSame('expenses', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(true, $model->timestamps); - $this->assertSame(['category', 'amount', 'receipt_path', 'description', 'retailor', 'purchased_by', 'date_of_purchase', 'added_by', 'created_at', 'updated_at', 'school_year', 'semester', 'status', 'status_reason', 'updated_by', 'approved_by', 'reimbursement_id'], $model->getFillable()); + $this->assertSame(["category", "amount", "receipt_path", "description", "retailor", "purchased_by", "date_of_purchase", "added_by", "created_at", "updated_at", "school_year", "semester", "status", "status_reason", "updated_by", "approved_by", "reimbursement_id"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(Expense::class, new Expense); + $this->assertInstanceOf(Expense::class, new Expense()); } } diff --git a/tests/Unit/Models/FamilyCommPrefTest.php b/tests/Unit/Models/FamilyCommPrefTest.php index 32e16375..08fc13ff 100644 --- a/tests/Unit/Models/FamilyCommPrefTest.php +++ b/tests/Unit/Models/FamilyCommPrefTest.php @@ -9,16 +9,16 @@ class FamilyCommPrefTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new FamilyCommPref; + $model = new FamilyCommPref(); $this->assertSame('family_comm_prefs', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(true, $model->timestamps); - $this->assertSame(['family_id', 'category', 'via_email', 'via_sms', 'cc_all_guardians', 'created_at', 'updated_at'], $model->getFillable()); + $this->assertSame(["family_id", "category", "via_email", "via_sms", "cc_all_guardians", "created_at", "updated_at"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(FamilyCommPref::class, new FamilyCommPref); + $this->assertInstanceOf(FamilyCommPref::class, new FamilyCommPref()); } } diff --git a/tests/Unit/Models/FamilyGuardianTest.php b/tests/Unit/Models/FamilyGuardianTest.php index 5018bdd0..4f70e606 100644 --- a/tests/Unit/Models/FamilyGuardianTest.php +++ b/tests/Unit/Models/FamilyGuardianTest.php @@ -9,16 +9,16 @@ class FamilyGuardianTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new FamilyGuardian; + $model = new FamilyGuardian(); $this->assertSame('family_guardians', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(true, $model->timestamps); - $this->assertSame(['family_id', 'user_id', 'relation', 'is_primary', 'receive_emails', 'receive_sms', 'custody_notes', 'created_at', 'updated_at'], $model->getFillable()); + $this->assertSame(["family_id", "user_id", "relation", "is_primary", "receive_emails", "receive_sms", "custody_notes", "created_at", "updated_at"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(FamilyGuardian::class, new FamilyGuardian); + $this->assertInstanceOf(FamilyGuardian::class, new FamilyGuardian()); } } diff --git a/tests/Unit/Models/FamilyStudentTest.php b/tests/Unit/Models/FamilyStudentTest.php index 2da4a68d..75bf2d37 100644 --- a/tests/Unit/Models/FamilyStudentTest.php +++ b/tests/Unit/Models/FamilyStudentTest.php @@ -9,16 +9,16 @@ class FamilyStudentTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new FamilyStudent; + $model = new FamilyStudent(); $this->assertSame('family_students', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(true, $model->timestamps); - $this->assertSame(['family_id', 'student_id', 'is_primary_home', 'notes', 'created_at', 'updated_at'], $model->getFillable()); + $this->assertSame(["family_id", "student_id", "is_primary_home", "notes", "created_at", "updated_at"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(FamilyStudent::class, new FamilyStudent); + $this->assertInstanceOf(FamilyStudent::class, new FamilyStudent()); } } diff --git a/tests/Unit/Models/FamilyTest.php b/tests/Unit/Models/FamilyTest.php index 888bece1..23add37b 100644 --- a/tests/Unit/Models/FamilyTest.php +++ b/tests/Unit/Models/FamilyTest.php @@ -9,16 +9,16 @@ class FamilyTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new Family; + $model = new Family(); $this->assertSame('families', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(true, $model->timestamps); - $this->assertSame(['family_code', 'household_name', 'address_line1', 'address_line2', 'city', 'state', 'postal_code', 'country', 'primary_phone', 'preferred_lang', 'preferred_contact_method', 'is_active', 'created_at', 'updated_at'], $model->getFillable()); + $this->assertSame(["family_code", "household_name", "address_line1", "address_line2", "city", "state", "postal_code", "country", "primary_phone", "preferred_lang", "preferred_contact_method", "is_active", "created_at", "updated_at"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(Family::class, new Family); + $this->assertInstanceOf(Family::class, new Family()); } } diff --git a/tests/Unit/Models/FinalExamTest.php b/tests/Unit/Models/FinalExamTest.php index 0e3e5620..4a2bcc68 100644 --- a/tests/Unit/Models/FinalExamTest.php +++ b/tests/Unit/Models/FinalExamTest.php @@ -9,16 +9,16 @@ class FinalExamTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new FinalExam; + $model = new FinalExam(); $this->assertSame('final_exam', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(true, $model->timestamps); - $this->assertSame(['student_id', 'school_id', 'class_section_id', 'updated_by', 'score', 'semester', 'school_year', 'created_at', 'updated_at'], $model->getFillable()); + $this->assertSame(["student_id", "school_id", "class_section_id", "updated_by", "score", "semester", "school_year", "created_at", "updated_at"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(FinalExam::class, new FinalExam); + $this->assertInstanceOf(FinalExam::class, new FinalExam()); } } diff --git a/tests/Unit/Models/FinalScoreTest.php b/tests/Unit/Models/FinalScoreTest.php index 1dc6252a..9fd2b8ae 100644 --- a/tests/Unit/Models/FinalScoreTest.php +++ b/tests/Unit/Models/FinalScoreTest.php @@ -9,16 +9,16 @@ class FinalScoreTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new FinalScore; + $model = new FinalScore(); $this->assertSame('final_score', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(true, $model->timestamps); - $this->assertSame(['student_id', 'school_id', 'class_section_id', 'updated_by', 'score', 'score_letter', 'comment', 'school_year', 'created_at', 'updated_at', 'semester'], $model->getFillable()); + $this->assertSame(["student_id", "school_id", "class_section_id", "updated_by", "score", "score_letter", "comment", "school_year", "created_at", "updated_at", "semester"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(FinalScore::class, new FinalScore); + $this->assertInstanceOf(FinalScore::class, new FinalScore()); } } diff --git a/tests/Unit/Models/GradingLockTest.php b/tests/Unit/Models/GradingLockTest.php index edbe8f3a..11e4585f 100644 --- a/tests/Unit/Models/GradingLockTest.php +++ b/tests/Unit/Models/GradingLockTest.php @@ -9,16 +9,16 @@ class GradingLockTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new GradingLock; + $model = new GradingLock(); $this->assertSame('grading_locks', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(true, $model->timestamps); - $this->assertSame(['class_section_id', 'semester', 'school_year', 'is_locked', 'locked_by', 'locked_at', 'created_at', 'updated_at'], $model->getFillable()); + $this->assertSame(["class_section_id", "semester", "school_year", "is_locked", "locked_by", "locked_at", "created_at", "updated_at"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(GradingLock::class, new GradingLock); + $this->assertInstanceOf(GradingLock::class, new GradingLock()); } } diff --git a/tests/Unit/Models/HomeworkTest.php b/tests/Unit/Models/HomeworkTest.php index 133d9549..7ef76ed0 100644 --- a/tests/Unit/Models/HomeworkTest.php +++ b/tests/Unit/Models/HomeworkTest.php @@ -9,16 +9,16 @@ class HomeworkTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new Homework; + $model = new Homework(); $this->assertSame('homework', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(true, $model->timestamps); - $this->assertSame(['student_id', 'school_id', 'class_section_id', 'updated_by', 'homework_index', 'score', 'comment', 'semester', 'school_year', 'created_at', 'updated_at'], $model->getFillable()); + $this->assertSame(["student_id", "school_id", "class_section_id", "updated_by", "homework_index", "score", "comment", "semester", "school_year", "created_at", "updated_at"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(Homework::class, new Homework); + $this->assertInstanceOf(Homework::class, new Homework()); } } diff --git a/tests/Unit/Models/IncidentTest.php b/tests/Unit/Models/IncidentTest.php index b6bdc323..ab03fc9a 100644 --- a/tests/Unit/Models/IncidentTest.php +++ b/tests/Unit/Models/IncidentTest.php @@ -9,16 +9,16 @@ class IncidentTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new Incident; + $model = new Incident(); $this->assertSame('incident', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(true, $model->timestamps); - $this->assertSame(['student_id', 'student_name', 'grade', 'incident', 'incident_datetime', 'incident_state', 'updated_by_open', 'open_description', 'updated_by_closed', 'close_description', 'action_taken', 'updated_by_canceled', 'cancel_description', 'semester', 'school_year', 'created_at', 'updated_at'], $model->getFillable()); + $this->assertSame(["student_id", "student_name", "grade", "incident", "incident_datetime", "incident_state", "updated_by_open", "open_description", "updated_by_closed", "close_description", "action_taken", "updated_by_canceled", "cancel_description", "semester", "school_year", "created_at", "updated_at"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(Incident::class, new Incident); + $this->assertInstanceOf(Incident::class, new Incident()); } } diff --git a/tests/Unit/Models/InventoryCategoryTest.php b/tests/Unit/Models/InventoryCategoryTest.php index 15d77d94..125d8009 100644 --- a/tests/Unit/Models/InventoryCategoryTest.php +++ b/tests/Unit/Models/InventoryCategoryTest.php @@ -9,16 +9,16 @@ class InventoryCategoryTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new InventoryCategory; + $model = new InventoryCategory(); $this->assertSame('inventory_categories', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(true, $model->timestamps); - $this->assertSame(['type', 'name', 'description', 'grade_min', 'grade_max', 'created_at', 'updated_at'], $model->getFillable()); + $this->assertSame(["type", "name", "description", "grade_min", "grade_max", "created_at", "updated_at"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(InventoryCategory::class, new InventoryCategory); + $this->assertInstanceOf(InventoryCategory::class, new InventoryCategory()); } } diff --git a/tests/Unit/Models/InventoryItemTest.php b/tests/Unit/Models/InventoryItemTest.php index beb36960..7769d136 100644 --- a/tests/Unit/Models/InventoryItemTest.php +++ b/tests/Unit/Models/InventoryItemTest.php @@ -9,16 +9,16 @@ class InventoryItemTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new InventoryItem; + $model = new InventoryItem(); $this->assertSame('inventory_items', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(true, $model->timestamps); - $this->assertSame(['type', 'category_id', 'name', 'description', 'quantity', 'good_qty', 'needs_repair_qty', 'need_replace_qty', 'cannot_find_qty', 'unit', 'condition', 'isbn', 'edition', 'sku', 'notes', 'semester', 'school_year', 'updated_by', 'created_at', 'updated_at'], $model->getFillable()); + $this->assertSame(["type", "category_id", "name", "description", "quantity", "good_qty", "needs_repair_qty", "need_replace_qty", "cannot_find_qty", "unit", "condition", "isbn", "edition", "sku", "notes", "semester", "school_year", "updated_by", "created_at", "updated_at"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(InventoryItem::class, new InventoryItem); + $this->assertInstanceOf(InventoryItem::class, new InventoryItem()); } } diff --git a/tests/Unit/Models/InventoryMovementTest.php b/tests/Unit/Models/InventoryMovementTest.php index a94be57a..25a47591 100644 --- a/tests/Unit/Models/InventoryMovementTest.php +++ b/tests/Unit/Models/InventoryMovementTest.php @@ -9,16 +9,16 @@ class InventoryMovementTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new InventoryMovement; + $model = new InventoryMovement(); $this->assertSame('inventory_movements', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(true, $model->timestamps); - $this->assertSame(['item_id', 'qty_change', 'movement_type', 'reason', 'note', 'semester', 'school_year', 'performed_by', 'teacher_id', 'student_id', 'class_section_id', 'created_at', 'updated_at'], $model->getFillable()); + $this->assertSame(["item_id", "qty_change", "movement_type", "reason", "note", "semester", "school_year", "performed_by", "teacher_id", "student_id", "class_section_id", "created_at", "updated_at"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(InventoryMovement::class, new InventoryMovement); + $this->assertInstanceOf(InventoryMovement::class, new InventoryMovement()); } } diff --git a/tests/Unit/Models/InvoiceEventTest.php b/tests/Unit/Models/InvoiceEventTest.php index 7d479ed9..ed5c2191 100644 --- a/tests/Unit/Models/InvoiceEventTest.php +++ b/tests/Unit/Models/InvoiceEventTest.php @@ -9,16 +9,16 @@ class InvoiceEventTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new InvoiceEvent; + $model = new InvoiceEvent(); $this->assertSame('invoice_event', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(true, $model->timestamps); - $this->assertSame(['invoice_id', 'event_name', 'description', 'amount', 'semester', 'school_year', 'updated_by', 'created_at', 'updated_at'], $model->getFillable()); + $this->assertSame(["invoice_id", "event_name", "description", "amount", "semester", "school_year", "updated_by", "created_at", "updated_at"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(InvoiceEvent::class, new InvoiceEvent); + $this->assertInstanceOf(InvoiceEvent::class, new InvoiceEvent()); } } diff --git a/tests/Unit/Models/InvoiceStudentListTest.php b/tests/Unit/Models/InvoiceStudentListTest.php index 64a949b0..7ced9ffe 100644 --- a/tests/Unit/Models/InvoiceStudentListTest.php +++ b/tests/Unit/Models/InvoiceStudentListTest.php @@ -9,16 +9,16 @@ class InvoiceStudentListTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new InvoiceStudentList; + $model = new InvoiceStudentList(); $this->assertSame('invoice_students_list', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(true, $model->timestamps); - $this->assertSame(['invoice_id', 'student_id', 'student_firstname', 'student_lastname', 'school_id', 'enrolled', 'school_year', 'created_at', 'updated_at', 'semester'], $model->getFillable()); + $this->assertSame(["invoice_id", "student_id", "student_firstname", "student_lastname", "school_id", "enrolled", "school_year", "created_at", "updated_at", "semester"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(InvoiceStudentList::class, new InvoiceStudentList); + $this->assertInstanceOf(InvoiceStudentList::class, new InvoiceStudentList()); } } diff --git a/tests/Unit/Models/InvoiceTest.php b/tests/Unit/Models/InvoiceTest.php index db378512..693e9d2a 100644 --- a/tests/Unit/Models/InvoiceTest.php +++ b/tests/Unit/Models/InvoiceTest.php @@ -9,16 +9,16 @@ class InvoiceTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new Invoice; + $model = new Invoice(); $this->assertSame('invoices', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(true, $model->timestamps); - $this->assertSame(['parent_id', 'invoice_number', 'total_amount', 'balance', 'paid_amount', 'has_discount', 'issue_date', 'due_date', 'status', 'description', 'school_year', 'created_at', 'updated_at', 'updated_by', 'semester'], $model->getFillable()); + $this->assertSame(["parent_id", "invoice_number", "total_amount", "balance", "paid_amount", "has_discount", "issue_date", "due_date", "status", "description", "school_year", "created_at", "updated_at", "updated_by", "semester"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(Invoice::class, new Invoice); + $this->assertInstanceOf(Invoice::class, new Invoice()); } } diff --git a/tests/Unit/Models/IpAttemptTest.php b/tests/Unit/Models/IpAttemptTest.php index 84f591b5..9c3f1f17 100644 --- a/tests/Unit/Models/IpAttemptTest.php +++ b/tests/Unit/Models/IpAttemptTest.php @@ -9,16 +9,16 @@ class IpAttemptTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new IpAttempt; + $model = new IpAttempt(); $this->assertSame('ip_attempts', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(true, $model->timestamps); - $this->assertSame(['ip_address', 'attempts', 'last_attempt_at', 'blocked_until', 'created_at', 'updated_at', 'school_year', 'semester'], $model->getFillable()); + $this->assertSame(["ip_address", "attempts", "last_attempt_at", "blocked_until", "created_at", "updated_at", "school_year", "semester"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(IpAttempt::class, new IpAttempt); + $this->assertInstanceOf(IpAttempt::class, new IpAttempt()); } } diff --git a/tests/Unit/Models/LateSlipLogTest.php b/tests/Unit/Models/LateSlipLogTest.php index 9918b4c3..4b7e34a1 100644 --- a/tests/Unit/Models/LateSlipLogTest.php +++ b/tests/Unit/Models/LateSlipLogTest.php @@ -9,16 +9,16 @@ class LateSlipLogTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new LateSlipLog; + $model = new LateSlipLog(); $this->assertSame('late_slip_logs', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(false, $model->timestamps); - $this->assertSame(['school_year', 'semester', 'student_name', 'slip_date', 'time_in', 'grade', 'reason', 'admin_name', 'printed_by', 'printed_at'], $model->getFillable()); + $this->assertSame(["school_year", "semester", "student_name", "slip_date", "time_in", "grade", "reason", "admin_name", "printed_by", "printed_at"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(LateSlipLog::class, new LateSlipLog); + $this->assertInstanceOf(LateSlipLog::class, new LateSlipLog()); } } diff --git a/tests/Unit/Models/LoginActivityTest.php b/tests/Unit/Models/LoginActivityTest.php index 88d489bb..08f77502 100644 --- a/tests/Unit/Models/LoginActivityTest.php +++ b/tests/Unit/Models/LoginActivityTest.php @@ -9,16 +9,16 @@ class LoginActivityTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new LoginActivity; + $model = new LoginActivity(); $this->assertSame('login_activity', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(true, $model->timestamps); - $this->assertSame(['user_id', 'email', 'login_time', 'logout_time', 'ip_address', 'user_agent', 'created_at', 'updated_at', 'semester', 'school_year'], $model->getFillable()); + $this->assertSame(["user_id", "email", "login_time", "logout_time", "ip_address", "user_agent", "created_at", "updated_at", "semester", "school_year"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(LoginActivity::class, new LoginActivity); + $this->assertInstanceOf(LoginActivity::class, new LoginActivity()); } } diff --git a/tests/Unit/Models/ManualPaymentTest.php b/tests/Unit/Models/ManualPaymentTest.php index f996bebe..abd7e8bf 100644 --- a/tests/Unit/Models/ManualPaymentTest.php +++ b/tests/Unit/Models/ManualPaymentTest.php @@ -9,16 +9,16 @@ class ManualPaymentTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new ManualPayment; + $model = new ManualPayment(); $this->assertSame('manual_payments', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(false, $model->timestamps); - $this->assertSame(['invoice_number', 'amount', 'payment_method', 'reference', 'discount_number', 'proof_path', 'created_at', 'school_year', 'semester'], $model->getFillable()); + $this->assertSame(["invoice_number", "amount", "payment_method", "reference", "discount_number", "proof_path", "created_at", "school_year", "semester"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(ManualPayment::class, new ManualPayment); + $this->assertInstanceOf(ManualPayment::class, new ManualPayment()); } } diff --git a/tests/Unit/Models/MessageTest.php b/tests/Unit/Models/MessageTest.php index 7c25f5ef..9bc8bdab 100644 --- a/tests/Unit/Models/MessageTest.php +++ b/tests/Unit/Models/MessageTest.php @@ -9,16 +9,16 @@ class MessageTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new Message; + $model = new Message(); $this->assertSame('messages', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(false, $model->timestamps); - $this->assertSame(['sender_id', 'recipient_id', 'subject', 'message', 'sent_datetime', 'read_status', 'read_datetime', 'message_number', 'priority', 'attachment', 'status', 'semester', 'school_year'], $model->getFillable()); + $this->assertSame(["sender_id", "recipient_id", "subject", "message", "sent_datetime", "read_status", "read_datetime", "message_number", "priority", "attachment", "status", "semester", "school_year"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(Message::class, new Message); + $this->assertInstanceOf(Message::class, new Message()); } } diff --git a/tests/Unit/Models/MidtermExamTest.php b/tests/Unit/Models/MidtermExamTest.php index beb09763..2d215b09 100644 --- a/tests/Unit/Models/MidtermExamTest.php +++ b/tests/Unit/Models/MidtermExamTest.php @@ -9,16 +9,16 @@ class MidtermExamTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new MidtermExam; + $model = new MidtermExam(); $this->assertSame('midterm_exam', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(true, $model->timestamps); - $this->assertSame(['student_id', 'school_id', 'class_section_id', 'updated_by', 'score', 'comment', 'semester', 'school_year', 'created_at', 'updated_at'], $model->getFillable()); + $this->assertSame(["student_id", "school_id", "class_section_id", "updated_by", "score", "comment", "semester", "school_year", "created_at", "updated_at"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(MidtermExam::class, new MidtermExam); + $this->assertInstanceOf(MidtermExam::class, new MidtermExam()); } } diff --git a/tests/Unit/Models/MissingScoreOverrideTest.php b/tests/Unit/Models/MissingScoreOverrideTest.php index e1a00196..f4eaf535 100644 --- a/tests/Unit/Models/MissingScoreOverrideTest.php +++ b/tests/Unit/Models/MissingScoreOverrideTest.php @@ -9,16 +9,16 @@ class MissingScoreOverrideTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new MissingScoreOverride; + $model = new MissingScoreOverride(); $this->assertSame('missing_score_overrides', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(true, $model->timestamps); - $this->assertSame(['student_id', 'class_section_id', 'semester', 'school_year', 'item_type', 'item_index', 'is_allowed', 'updated_by', 'created_at', 'updated_at'], $model->getFillable()); + $this->assertSame(["student_id", "class_section_id", "semester", "school_year", "item_type", "item_index", "is_allowed", "updated_by", "created_at", "updated_at"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(MissingScoreOverride::class, new MissingScoreOverride); + $this->assertInstanceOf(MissingScoreOverride::class, new MissingScoreOverride()); } } diff --git a/tests/Unit/Models/NavItemTest.php b/tests/Unit/Models/NavItemTest.php index 600ca95a..455f6430 100644 --- a/tests/Unit/Models/NavItemTest.php +++ b/tests/Unit/Models/NavItemTest.php @@ -3,24 +3,24 @@ namespace Tests\Unit\Models; use App\Models\NavItem; -use Illuminate\Database\Eloquent\SoftDeletes; use PHPUnit\Framework\TestCase; +use Illuminate\Database\Eloquent\SoftDeletes; class NavItemTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new NavItem; + $model = new NavItem(); $this->assertSame('nav_items', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(true, $model->timestamps); - $this->assertSame(['menu_parent_id', 'label', 'url', 'icon_class', 'target', 'sort_order', 'is_enabled', 'created_at', 'updated_at', 'deleted_at'], $model->getFillable()); + $this->assertSame(["menu_parent_id", "label", "url", "icon_class", "target", "sort_order", "is_enabled", "created_at", "updated_at", "deleted_at"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(NavItem::class, new NavItem); + $this->assertInstanceOf(NavItem::class, new NavItem()); $this->assertContains(SoftDeletes::class, class_uses_recursive(NavItem::class)); } } diff --git a/tests/Unit/Models/NotificationTest.php b/tests/Unit/Models/NotificationTest.php index 31cdd898..3af06477 100644 --- a/tests/Unit/Models/NotificationTest.php +++ b/tests/Unit/Models/NotificationTest.php @@ -3,24 +3,24 @@ namespace Tests\Unit\Models; use App\Models\Notification; -use Illuminate\Database\Eloquent\SoftDeletes; use PHPUnit\Framework\TestCase; +use Illuminate\Database\Eloquent\SoftDeletes; class NotificationTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new Notification; + $model = new Notification(); $this->assertSame('notifications', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(true, $model->timestamps); - $this->assertSame(['title', 'message', 'target_group', 'delivery_channels', 'priority', 'status', 'action_url', 'attachment_path', 'scheduled_at', 'sent_at', 'expires_at', 'created_at', 'updated_at', 'deleted_at', 'school_year', 'semester'], $model->getFillable()); + $this->assertSame(["title", "message", "target_group", "delivery_channels", "priority", "status", "action_url", "attachment_path", "scheduled_at", "sent_at", "expires_at", "created_at", "updated_at", "deleted_at", "school_year", "semester"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(Notification::class, new Notification); + $this->assertInstanceOf(Notification::class, new Notification()); $this->assertContains(SoftDeletes::class, class_uses_recursive(Notification::class)); } } diff --git a/tests/Unit/Models/ParentAttendanceReportTest.php b/tests/Unit/Models/ParentAttendanceReportTest.php index 420a64c6..41d08542 100644 --- a/tests/Unit/Models/ParentAttendanceReportTest.php +++ b/tests/Unit/Models/ParentAttendanceReportTest.php @@ -9,16 +9,16 @@ class ParentAttendanceReportTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new ParentAttendanceReport; + $model = new ParentAttendanceReport(); $this->assertSame('parent_attendance_reports', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(true, $model->timestamps); - $this->assertSame(['parent_id', 'student_id', 'class_section_id', 'report_date', 'type', 'arrival_time', 'dismiss_time', 'reason', 'semester', 'school_year', 'status', 'created_at', 'updated_at'], $model->getFillable()); + $this->assertSame(["parent_id", "student_id", "class_section_id", "report_date", "type", "arrival_time", "dismiss_time", "reason", "semester", "school_year", "status", "created_at", "updated_at"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(ParentAttendanceReport::class, new ParentAttendanceReport); + $this->assertInstanceOf(ParentAttendanceReport::class, new ParentAttendanceReport()); } } diff --git a/tests/Unit/Models/ParentMeetingScheduleTest.php b/tests/Unit/Models/ParentMeetingScheduleTest.php index 8e025dd5..ccaf2703 100644 --- a/tests/Unit/Models/ParentMeetingScheduleTest.php +++ b/tests/Unit/Models/ParentMeetingScheduleTest.php @@ -9,16 +9,16 @@ class ParentMeetingScheduleTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new ParentMeetingSchedule; + $model = new ParentMeetingSchedule(); $this->assertSame('parent_meeting_schedules', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(true, $model->timestamps); - $this->assertSame(['student_id', 'parent_user_id', 'parent_name', 'student_name', 'class_section_name', 'date', 'time', 'notes', 'semester', 'school_year', 'status', 'created_by', 'created_at', 'updated_at'], $model->getFillable()); + $this->assertSame(["student_id", "parent_user_id", "parent_name", "student_name", "class_section_name", "date", "time", "notes", "semester", "school_year", "status", "created_by", "created_at", "updated_at"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(ParentMeetingSchedule::class, new ParentMeetingSchedule); + $this->assertInstanceOf(ParentMeetingSchedule::class, new ParentMeetingSchedule()); } } diff --git a/tests/Unit/Models/ParentNotificationTest.php b/tests/Unit/Models/ParentNotificationTest.php index 068d986f..6004821c 100644 --- a/tests/Unit/Models/ParentNotificationTest.php +++ b/tests/Unit/Models/ParentNotificationTest.php @@ -9,16 +9,16 @@ class ParentNotificationTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new ParentNotification; + $model = new ParentNotification(); $this->assertSame('parent_notifications', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(true, $model->timestamps); - $this->assertSame(['student_id', 'code', 'incident_date', 'channel', 'to_address', 'subject', 'status', 'response', 'semester', 'school_year', 'created_at', 'updated_at'], $model->getFillable()); + $this->assertSame(["student_id", "code", "incident_date", "channel", "to_address", "subject", "status", "response", "semester", "school_year", "created_at", "updated_at"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(ParentNotification::class, new ParentNotification); + $this->assertInstanceOf(ParentNotification::class, new ParentNotification()); } } diff --git a/tests/Unit/Models/ParentTest.php b/tests/Unit/Models/ParentTest.php index 4d848cca..ea3e3982 100644 --- a/tests/Unit/Models/ParentTest.php +++ b/tests/Unit/Models/ParentTest.php @@ -9,16 +9,16 @@ class ParentTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new ParentModel; + $model = new ParentModel(); $this->assertSame('parents', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(true, $model->timestamps); - $this->assertSame(['secondparent_firstname', 'secondparent_lastname', 'secondparent_gender', 'secondparent_email', 'secondparent_phone', 'firstparent_id', 'secondparent_id', 'semester', 'school_year', 'created_at', 'updated_at'], $model->getFillable()); + $this->assertSame(["secondparent_firstname", "secondparent_lastname", "secondparent_gender", "secondparent_email", "secondparent_phone", "firstparent_id", "secondparent_id", "semester", "school_year", "created_at", "updated_at"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(ParentModel::class, new ParentModel); + $this->assertInstanceOf(ParentModel::class, new ParentModel()); } } diff --git a/tests/Unit/Models/ParticipationTest.php b/tests/Unit/Models/ParticipationTest.php index 1e4b320f..f165b8d0 100644 --- a/tests/Unit/Models/ParticipationTest.php +++ b/tests/Unit/Models/ParticipationTest.php @@ -9,16 +9,16 @@ class ParticipationTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new Participation; + $model = new Participation(); $this->assertSame('participation', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(true, $model->timestamps); - $this->assertSame(['student_id', 'school_id', 'class_section_id', 'updated_by', 'score', 'comment', 'semester', 'school_year', 'created_at', 'updated_at'], $model->getFillable()); + $this->assertSame(["student_id", "school_id", "class_section_id", "updated_by", "score", "comment", "semester", "school_year", "created_at", "updated_at"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(Participation::class, new Participation); + $this->assertInstanceOf(Participation::class, new Participation()); } } diff --git a/tests/Unit/Models/PasswordResetRequestTest.php b/tests/Unit/Models/PasswordResetRequestTest.php index 3e572f1f..64d9d2e7 100644 --- a/tests/Unit/Models/PasswordResetRequestTest.php +++ b/tests/Unit/Models/PasswordResetRequestTest.php @@ -9,16 +9,16 @@ class PasswordResetRequestTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new PasswordResetRequest; + $model = new PasswordResetRequest(); $this->assertSame('password_reset_requests', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(false, $model->timestamps); - $this->assertSame(['ip_address', 'requested_at'], $model->getFillable()); + $this->assertSame(["ip_address", "requested_at"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(PasswordResetRequest::class, new PasswordResetRequest); + $this->assertInstanceOf(PasswordResetRequest::class, new PasswordResetRequest()); } } diff --git a/tests/Unit/Models/PasswordResetTest.php b/tests/Unit/Models/PasswordResetTest.php index ef7440f6..4fda955a 100644 --- a/tests/Unit/Models/PasswordResetTest.php +++ b/tests/Unit/Models/PasswordResetTest.php @@ -9,16 +9,16 @@ class PasswordResetTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new PasswordReset; + $model = new PasswordReset(); $this->assertSame('password_resets', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(false, $model->timestamps); - $this->assertSame(['email', 'token', 'created_at', 'expires_at'], $model->getFillable()); + $this->assertSame(["email", "token", "created_at", "expires_at"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(PasswordReset::class, new PasswordReset); + $this->assertInstanceOf(PasswordReset::class, new PasswordReset()); } } diff --git a/tests/Unit/Models/PayPalPaymentTest.php b/tests/Unit/Models/PayPalPaymentTest.php index f7298574..a2d8894a 100644 --- a/tests/Unit/Models/PayPalPaymentTest.php +++ b/tests/Unit/Models/PayPalPaymentTest.php @@ -9,16 +9,16 @@ class PayPalPaymentTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new PayPalPayment; + $model = new PayPalPayment(); $this->assertSame('paypal_payments', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(false, $model->timestamps); - $this->assertSame(['webhook_id', 'parent_school_id', 'order_id', 'transaction_id', 'status', 'amount', 'currency', 'paypal_fee', 'net_amount', 'payer_email', 'merchant_id', 'event_type', 'summary', 'raw_payload', 'synced', 'sync_attempts', 'created_at'], $model->getFillable()); + $this->assertSame(["webhook_id", "parent_school_id", "order_id", "transaction_id", "status", "amount", "currency", "paypal_fee", "net_amount", "payer_email", "merchant_id", "event_type", "summary", "raw_payload", "synced", "sync_attempts", "created_at"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(PayPalPayment::class, new PayPalPayment); + $this->assertInstanceOf(PayPalPayment::class, new PayPalPayment()); } } diff --git a/tests/Unit/Models/PaymentErrorTest.php b/tests/Unit/Models/PaymentErrorTest.php index 88e3a34e..3507b01d 100644 --- a/tests/Unit/Models/PaymentErrorTest.php +++ b/tests/Unit/Models/PaymentErrorTest.php @@ -9,16 +9,16 @@ class PaymentErrorTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new PaymentError; + $model = new PaymentError(); $this->assertSame('payment_error', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(false, $model->timestamps); - $this->assertSame(['payment_id', 'invoice_id', 'parent_id', 'wrong_paid_amount', 'wrong_payment_method', 'wrong_check_file', 'error_note', 'logged_at', 'logged_by', 'school_year', 'semester'], $model->getFillable()); + $this->assertSame(["payment_id", "invoice_id", "parent_id", "wrong_paid_amount", "wrong_payment_method", "wrong_check_file", "error_note", "logged_at", "logged_by", "school_year", "semester"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(PaymentError::class, new PaymentError); + $this->assertInstanceOf(PaymentError::class, new PaymentError()); } } diff --git a/tests/Unit/Models/PaymentNotificationLogTest.php b/tests/Unit/Models/PaymentNotificationLogTest.php index 35524e6c..e25e969d 100644 --- a/tests/Unit/Models/PaymentNotificationLogTest.php +++ b/tests/Unit/Models/PaymentNotificationLogTest.php @@ -9,16 +9,16 @@ class PaymentNotificationLogTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new PaymentNotificationLog; + $model = new PaymentNotificationLog(); $this->assertSame('payment_notification_logs', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(false, $model->timestamps); - $this->assertSame(['parent_id', 'invoice_id', 'school_year', 'period_year', 'period_month', 'type', 'to_email', 'cc_email', 'head_fa_notified', 'subject', 'body', 'status', 'error_message', 'balance_snapshot', 'created_at', 'sent_at'], $model->getFillable()); + $this->assertSame(["parent_id", "invoice_id", "school_year", "period_year", "period_month", "type", "to_email", "cc_email", "head_fa_notified", "subject", "body", "status", "error_message", "balance_snapshot", "created_at", "sent_at"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(PaymentNotificationLog::class, new PaymentNotificationLog); + $this->assertInstanceOf(PaymentNotificationLog::class, new PaymentNotificationLog()); } } diff --git a/tests/Unit/Models/PaymentTest.php b/tests/Unit/Models/PaymentTest.php index 9e9926df..5c6135da 100644 --- a/tests/Unit/Models/PaymentTest.php +++ b/tests/Unit/Models/PaymentTest.php @@ -9,16 +9,16 @@ class PaymentTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new Payment; + $model = new Payment(); $this->assertSame('payments', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(true, $model->timestamps); - $this->assertSame(['parent_id', 'invoice_id', 'total_amount', 'paid_amount', 'balance', 'number_of_installments', 'transaction_id', 'check_file', 'check_number', 'payment_method', 'payment_date', 'semester', 'school_year', 'status', 'updated_by', 'created_at', 'updated_at'], $model->getFillable()); + $this->assertSame(["parent_id", "invoice_id", "total_amount", "paid_amount", "balance", "number_of_installments", "transaction_id", "check_file", "check_number", "payment_method", "payment_date", "semester", "school_year", "status", "updated_by", "created_at", "updated_at"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(Payment::class, new Payment); + $this->assertInstanceOf(Payment::class, new Payment()); } } diff --git a/tests/Unit/Models/PaymentTransactionTest.php b/tests/Unit/Models/PaymentTransactionTest.php index 06882fd5..6def8968 100644 --- a/tests/Unit/Models/PaymentTransactionTest.php +++ b/tests/Unit/Models/PaymentTransactionTest.php @@ -9,16 +9,16 @@ class PaymentTransactionTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new PaymentTransaction; + $model = new PaymentTransaction(); $this->assertSame('payment_transactions', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(false, $model->timestamps); - $this->assertSame(['transaction_id', 'payment_id', 'transaction_date', 'amount', 'payment_method', 'payment_status', 'transaction_fee', 'school_year', 'semester'], $model->getFillable()); + $this->assertSame(["transaction_id", "payment_id", "transaction_date", "amount", "payment_method", "payment_status", "transaction_fee", "school_year", "semester"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(PaymentTransaction::class, new PaymentTransaction); + $this->assertInstanceOf(PaymentTransaction::class, new PaymentTransaction()); } } diff --git a/tests/Unit/Models/PaypalTransactionTest.php b/tests/Unit/Models/PaypalTransactionTest.php index a391646c..1358da5a 100644 --- a/tests/Unit/Models/PaypalTransactionTest.php +++ b/tests/Unit/Models/PaypalTransactionTest.php @@ -9,16 +9,16 @@ class PaypalTransactionTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new PaypalTransaction; + $model = new PaypalTransaction(); $this->assertSame('paypal_transactions', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(true, $model->timestamps); - $this->assertSame(['transaction_id', 'payment_id', 'firstname', 'lastname', 'event_type', 'payer_email', 'amount', 'currency', 'status', 'payment_method', 'transaction_fee', 'raw_data', 'created_at', 'updated_at', 'school_year', 'semester'], $model->getFillable()); + $this->assertSame(["transaction_id", "payment_id", "firstname", "lastname", "event_type", "payer_email", "amount", "currency", "status", "payment_method", "transaction_fee", "raw_data", "created_at", "updated_at", "school_year", "semester"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(PaypalTransaction::class, new PaypalTransaction); + $this->assertInstanceOf(PaypalTransaction::class, new PaypalTransaction()); } } diff --git a/tests/Unit/Models/PermissionTest.php b/tests/Unit/Models/PermissionTest.php index 21d4a095..4666f185 100644 --- a/tests/Unit/Models/PermissionTest.php +++ b/tests/Unit/Models/PermissionTest.php @@ -9,16 +9,16 @@ class PermissionTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new Permission; + $model = new Permission(); $this->assertSame('permissions', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(true, $model->timestamps); - $this->assertSame(['name', 'description', 'created_at', 'updated_at'], $model->getFillable()); + $this->assertSame(["name", "description", "created_at", "updated_at"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(Permission::class, new Permission); + $this->assertInstanceOf(Permission::class, new Permission()); } } diff --git a/tests/Unit/Models/PlacementBatchTest.php b/tests/Unit/Models/PlacementBatchTest.php index 35845ca4..5f8d2c10 100644 --- a/tests/Unit/Models/PlacementBatchTest.php +++ b/tests/Unit/Models/PlacementBatchTest.php @@ -9,16 +9,16 @@ class PlacementBatchTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new PlacementBatch; + $model = new PlacementBatch(); $this->assertSame('placement_batches', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(true, $model->timestamps); - $this->assertSame(['placement_test', 'school_year', 'created_by', 'updated_by', 'created_at', 'updated_at'], $model->getFillable()); + $this->assertSame(["placement_test", "school_year", "created_by", "updated_by", "created_at", "updated_at"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(PlacementBatch::class, new PlacementBatch); + $this->assertInstanceOf(PlacementBatch::class, new PlacementBatch()); } } diff --git a/tests/Unit/Models/PlacementLevelTest.php b/tests/Unit/Models/PlacementLevelTest.php index 28603373..5a6ba505 100644 --- a/tests/Unit/Models/PlacementLevelTest.php +++ b/tests/Unit/Models/PlacementLevelTest.php @@ -9,16 +9,16 @@ class PlacementLevelTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new PlacementLevel; + $model = new PlacementLevel(); $this->assertSame('placement_levels', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(true, $model->timestamps); - $this->assertSame(['student_id', 'level', 'school_year', 'created_by', 'updated_by', 'created_at', 'updated_at'], $model->getFillable()); + $this->assertSame(["student_id", "level", "school_year", "created_by", "updated_by", "created_at", "updated_at"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(PlacementLevel::class, new PlacementLevel); + $this->assertInstanceOf(PlacementLevel::class, new PlacementLevel()); } } diff --git a/tests/Unit/Models/PlacementScoreTest.php b/tests/Unit/Models/PlacementScoreTest.php index caa9f834..36c66691 100644 --- a/tests/Unit/Models/PlacementScoreTest.php +++ b/tests/Unit/Models/PlacementScoreTest.php @@ -9,16 +9,16 @@ class PlacementScoreTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new PlacementScore; + $model = new PlacementScore(); $this->assertSame('placement_scores', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(true, $model->timestamps); - $this->assertSame(['batch_id', 'student_id', 'score', 'created_by', 'updated_by', 'created_at', 'updated_at'], $model->getFillable()); + $this->assertSame(["batch_id", "student_id", "score", "created_by", "updated_by", "created_at", "updated_at"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(PlacementScore::class, new PlacementScore); + $this->assertInstanceOf(PlacementScore::class, new PlacementScore()); } } diff --git a/tests/Unit/Models/PreferencesTest.php b/tests/Unit/Models/PreferencesTest.php index 1520f59c..4a75f9e7 100644 --- a/tests/Unit/Models/PreferencesTest.php +++ b/tests/Unit/Models/PreferencesTest.php @@ -9,16 +9,16 @@ class PreferencesTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new Preferences; + $model = new Preferences(); $this->assertSame('user_preferences', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(true, $model->timestamps); - $this->assertSame(['user_id', 'notification_email', 'notification_sms', 'theme', 'language', 'style_color', 'menu_color', 'menu_custom_bg', 'menu_custom_text', 'menu_custom_mode', 'created_at', 'updated_at'], $model->getFillable()); + $this->assertSame(["user_id", "notification_email", "notification_sms", "theme", "language", "style_color", "menu_color", "menu_custom_bg", "menu_custom_text", "menu_custom_mode", "created_at", "updated_at"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(Preferences::class, new Preferences); + $this->assertInstanceOf(Preferences::class, new Preferences()); } } diff --git a/tests/Unit/Models/PrintRequestTest.php b/tests/Unit/Models/PrintRequestTest.php index 2bea9da1..6e693e19 100644 --- a/tests/Unit/Models/PrintRequestTest.php +++ b/tests/Unit/Models/PrintRequestTest.php @@ -9,16 +9,16 @@ class PrintRequestTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new PrintRequest; + $model = new PrintRequest(); $this->assertSame('print_requests', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(true, $model->timestamps); - $this->assertSame(['teacher_id', 'admin_id', 'class_id', 'file_path', 'page_selection', 'num_copies', 'required_by', 'pickup_method', 'status', 'created_at', 'updated_at'], $model->getFillable()); + $this->assertSame(["teacher_id", "admin_id", "class_id", "file_path", "page_selection", "num_copies", "required_by", "pickup_method", "status", "created_at", "updated_at"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(PrintRequest::class, new PrintRequest); + $this->assertInstanceOf(PrintRequest::class, new PrintRequest()); } } diff --git a/tests/Unit/Models/ProjectTest.php b/tests/Unit/Models/ProjectTest.php index 15a22620..add070ec 100644 --- a/tests/Unit/Models/ProjectTest.php +++ b/tests/Unit/Models/ProjectTest.php @@ -9,16 +9,16 @@ class ProjectTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new Project; + $model = new Project(); $this->assertSame('project', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(true, $model->timestamps); - $this->assertSame(['student_id', 'school_id', 'class_section_id', 'updated_by', 'project_index', 'score', 'comment', 'semester', 'school_year', 'created_at', 'updated_at'], $model->getFillable()); + $this->assertSame(["student_id", "school_id", "class_section_id", "updated_by", "project_index", "score", "comment", "semester", "school_year", "created_at", "updated_at"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(Project::class, new Project); + $this->assertInstanceOf(Project::class, new Project()); } } diff --git a/tests/Unit/Models/PromotionQueueTest.php b/tests/Unit/Models/PromotionQueueTest.php index 052bad75..134671e9 100644 --- a/tests/Unit/Models/PromotionQueueTest.php +++ b/tests/Unit/Models/PromotionQueueTest.php @@ -9,16 +9,16 @@ class PromotionQueueTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new PromotionQueue; + $model = new PromotionQueue(); $this->assertSame('promotion_queue', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(true, $model->timestamps); - $this->assertSame(['student_id', 'from_class_section_id', 'to_class_id', 'to_class_section_id', 'school_year_from', 'school_year_to', 'status', 'created_at', 'updated_at', 'updated_by'], $model->getFillable()); + $this->assertSame(["student_id", "from_class_section_id", "to_class_id", "to_class_section_id", "school_year_from", "school_year_to", "status", "created_at", "updated_at", "updated_by"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(PromotionQueue::class, new PromotionQueue); + $this->assertInstanceOf(PromotionQueue::class, new PromotionQueue()); } } diff --git a/tests/Unit/Models/PurchaseOrderItemTest.php b/tests/Unit/Models/PurchaseOrderItemTest.php index 55b5ff2e..ff0c0843 100644 --- a/tests/Unit/Models/PurchaseOrderItemTest.php +++ b/tests/Unit/Models/PurchaseOrderItemTest.php @@ -9,16 +9,16 @@ class PurchaseOrderItemTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new PurchaseOrderItem; + $model = new PurchaseOrderItem(); $this->assertSame('purchase_order_items', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(true, $model->timestamps); - $this->assertSame(['purchase_order_id', 'supply_id', 'description', 'quantity', 'received_qty', 'unit_cost'], $model->getFillable()); + $this->assertSame(["purchase_order_id", "supply_id", "description", "quantity", "received_qty", "unit_cost"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(PurchaseOrderItem::class, new PurchaseOrderItem); + $this->assertInstanceOf(PurchaseOrderItem::class, new PurchaseOrderItem()); } } diff --git a/tests/Unit/Models/PurchaseOrderTest.php b/tests/Unit/Models/PurchaseOrderTest.php index 2bf66e81..2f980389 100644 --- a/tests/Unit/Models/PurchaseOrderTest.php +++ b/tests/Unit/Models/PurchaseOrderTest.php @@ -3,24 +3,24 @@ namespace Tests\Unit\Models; use App\Models\PurchaseOrder; -use Illuminate\Database\Eloquent\SoftDeletes; use PHPUnit\Framework\TestCase; +use Illuminate\Database\Eloquent\SoftDeletes; class PurchaseOrderTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new PurchaseOrder; + $model = new PurchaseOrder(); $this->assertSame('purchase_orders', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(true, $model->timestamps); - $this->assertSame(['po_number', 'supplier_id', 'status', 'order_date', 'expected_date', 'subtotal', 'tax', 'total', 'notes'], $model->getFillable()); + $this->assertSame(["po_number", "supplier_id", "status", "order_date", "expected_date", "subtotal", "tax", "total", "notes"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(PurchaseOrder::class, new PurchaseOrder); + $this->assertInstanceOf(PurchaseOrder::class, new PurchaseOrder()); $this->assertContains(SoftDeletes::class, class_uses_recursive(PurchaseOrder::class)); } } diff --git a/tests/Unit/Models/QuizTest.php b/tests/Unit/Models/QuizTest.php index cb0efd59..b413472e 100644 --- a/tests/Unit/Models/QuizTest.php +++ b/tests/Unit/Models/QuizTest.php @@ -9,16 +9,16 @@ class QuizTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new Quiz; + $model = new Quiz(); $this->assertSame('quiz', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(true, $model->timestamps); - $this->assertSame(['student_id', 'school_id', 'class_section_id', 'updated_by', 'quiz_index', 'score', 'comment', 'semester', 'school_year', 'created_at', 'updated_at'], $model->getFillable()); + $this->assertSame(["student_id", "school_id", "class_section_id", "updated_by", "quiz_index", "score", "comment", "semester", "school_year", "created_at", "updated_at"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(Quiz::class, new Quiz); + $this->assertInstanceOf(Quiz::class, new Quiz()); } } diff --git a/tests/Unit/Models/RefundTest.php b/tests/Unit/Models/RefundTest.php index 5e7195a6..5e6dfa9d 100644 --- a/tests/Unit/Models/RefundTest.php +++ b/tests/Unit/Models/RefundTest.php @@ -9,16 +9,16 @@ class RefundTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new Refund; + $model = new Refund(); $this->assertSame('refunds', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(true, $model->timestamps); - $this->assertSame(['parent_id', 'school_year', 'invoice_id', 'refund_amount', 'requested_at', 'approved_at', 'refunded_at', 'status', 'reason', 'refund_paid_amount', 'request', 'note', 'approved_by', 'updated_by', 'refund_method', 'check_nbr', 'check_file', 'created_at', 'updated_at', 'semester'], $model->getFillable()); + $this->assertSame(["parent_id", "school_year", "invoice_id", "refund_amount", "requested_at", "approved_at", "refunded_at", "status", "reason", "refund_paid_amount", "request", "note", "approved_by", "updated_by", "refund_method", "check_nbr", "check_file", "created_at", "updated_at", "semester"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(Refund::class, new Refund); + $this->assertInstanceOf(Refund::class, new Refund()); } } diff --git a/tests/Unit/Models/ReimbursementBatchAdminFileTest.php b/tests/Unit/Models/ReimbursementBatchAdminFileTest.php index 9551777f..342bc734 100644 --- a/tests/Unit/Models/ReimbursementBatchAdminFileTest.php +++ b/tests/Unit/Models/ReimbursementBatchAdminFileTest.php @@ -9,16 +9,16 @@ class ReimbursementBatchAdminFileTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new ReimbursementBatchAdminFile; + $model = new ReimbursementBatchAdminFile(); $this->assertSame('reimbursement_batch_admin_files', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(false, $model->timestamps); - $this->assertSame(['batch_id', 'admin_id', 'filename', 'original_filename', 'uploaded_at', 'uploaded_by'], $model->getFillable()); + $this->assertSame(["batch_id", "admin_id", "filename", "original_filename", "uploaded_at", "uploaded_by"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(ReimbursementBatchAdminFile::class, new ReimbursementBatchAdminFile); + $this->assertInstanceOf(ReimbursementBatchAdminFile::class, new ReimbursementBatchAdminFile()); } } diff --git a/tests/Unit/Models/ReimbursementBatchItemTest.php b/tests/Unit/Models/ReimbursementBatchItemTest.php index d75e628f..20e9f33b 100644 --- a/tests/Unit/Models/ReimbursementBatchItemTest.php +++ b/tests/Unit/Models/ReimbursementBatchItemTest.php @@ -9,16 +9,16 @@ class ReimbursementBatchItemTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new ReimbursementBatchItem; + $model = new ReimbursementBatchItem(); $this->assertSame('reimbursement_batch_items', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(false, $model->timestamps); - $this->assertSame(['batch_id', 'expense_id', 'reimbursement_id', 'admin_id', 'assigned_at', 'unassigned_at', 'notes', 'school_year', 'semester'], $model->getFillable()); + $this->assertSame(["batch_id", "expense_id", "reimbursement_id", "admin_id", "assigned_at", "unassigned_at", "notes", "school_year", "semester"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(ReimbursementBatchItem::class, new ReimbursementBatchItem); + $this->assertInstanceOf(ReimbursementBatchItem::class, new ReimbursementBatchItem()); } } diff --git a/tests/Unit/Models/ReimbursementBatchTest.php b/tests/Unit/Models/ReimbursementBatchTest.php index a7ebcdf9..86c240b5 100644 --- a/tests/Unit/Models/ReimbursementBatchTest.php +++ b/tests/Unit/Models/ReimbursementBatchTest.php @@ -9,16 +9,16 @@ class ReimbursementBatchTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new ReimbursementBatch; + $model = new ReimbursementBatch(); $this->assertSame('reimbursement_batches', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(false, $model->timestamps); - $this->assertSame(['title', 'yearly_batch_number', 'status', 'school_year', 'semester', 'created_by', 'closed_by', 'opened_at', 'closed_at', 'notes'], $model->getFillable()); + $this->assertSame(["title", "yearly_batch_number", "status", "school_year", "semester", "created_by", "closed_by", "opened_at", "closed_at", "notes"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(ReimbursementBatch::class, new ReimbursementBatch); + $this->assertInstanceOf(ReimbursementBatch::class, new ReimbursementBatch()); } } diff --git a/tests/Unit/Models/ReimbursementTest.php b/tests/Unit/Models/ReimbursementTest.php index 1e354e3f..eaa8c6fa 100644 --- a/tests/Unit/Models/ReimbursementTest.php +++ b/tests/Unit/Models/ReimbursementTest.php @@ -9,16 +9,16 @@ class ReimbursementTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new Reimbursement; + $model = new Reimbursement(); $this->assertSame('reimbursements', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(true, $model->timestamps); - $this->assertSame(['expense_id', 'amount', 'reimbursed_to', 'approved_by', 'receipt_path', 'description', 'status', 'added_by', 'created_at', 'updated_at', 'school_year', 'semester', 'check_number', 'reimbursement_method', 'batch_number'], $model->getFillable()); + $this->assertSame(["expense_id", "amount", "reimbursed_to", "approved_by", "receipt_path", "description", "status", "added_by", "created_at", "updated_at", "school_year", "semester", "check_number", "reimbursement_method", "batch_number"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(Reimbursement::class, new Reimbursement); + $this->assertInstanceOf(Reimbursement::class, new Reimbursement()); } } diff --git a/tests/Unit/Models/RoleNavItemTest.php b/tests/Unit/Models/RoleNavItemTest.php index 7292e5b8..fadf1680 100644 --- a/tests/Unit/Models/RoleNavItemTest.php +++ b/tests/Unit/Models/RoleNavItemTest.php @@ -9,16 +9,16 @@ class RoleNavItemTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new RoleNavItem; + $model = new RoleNavItem(); $this->assertSame('role_nav_items', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(true, $model->timestamps); - $this->assertSame(['role_id', 'nav_item_id', 'created_at', 'updated_at'], $model->getFillable()); + $this->assertSame(["role_id", "nav_item_id", "created_at", "updated_at"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(RoleNavItem::class, new RoleNavItem); + $this->assertInstanceOf(RoleNavItem::class, new RoleNavItem()); } } diff --git a/tests/Unit/Models/RolePermissionTest.php b/tests/Unit/Models/RolePermissionTest.php index 5100bfba..55f9ade5 100644 --- a/tests/Unit/Models/RolePermissionTest.php +++ b/tests/Unit/Models/RolePermissionTest.php @@ -9,16 +9,16 @@ class RolePermissionTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new RolePermission; + $model = new RolePermission(); $this->assertSame('role_permissions', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(true, $model->timestamps); - $this->assertSame(['role_id', 'permission_id', 'can_create', 'can_read', 'can_update', 'can_delete', 'can_manage', 'created_at', 'updated_at'], $model->getFillable()); + $this->assertSame(["role_id", "permission_id", "can_create", "can_read", "can_update", "can_delete", "can_manage", "created_at", "updated_at"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(RolePermission::class, new RolePermission); + $this->assertInstanceOf(RolePermission::class, new RolePermission()); } } diff --git a/tests/Unit/Models/RoleTest.php b/tests/Unit/Models/RoleTest.php index 6edcac79..73c0d30f 100644 --- a/tests/Unit/Models/RoleTest.php +++ b/tests/Unit/Models/RoleTest.php @@ -9,16 +9,16 @@ class RoleTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new Role; + $model = new Role(); $this->assertSame('roles', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(true, $model->timestamps); - $this->assertSame(['name', 'slug', 'description', 'dashboard_route', 'priority', 'is_active', 'created_at', 'updated_at'], $model->getFillable()); + $this->assertSame(["name", "slug", "description", "dashboard_route", "priority", "is_active", "created_at", "updated_at"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(Role::class, new Role); + $this->assertInstanceOf(Role::class, new Role()); } } diff --git a/tests/Unit/Models/ScoreCommentTest.php b/tests/Unit/Models/ScoreCommentTest.php index 97012b69..87c730b4 100644 --- a/tests/Unit/Models/ScoreCommentTest.php +++ b/tests/Unit/Models/ScoreCommentTest.php @@ -9,16 +9,16 @@ class ScoreCommentTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new ScoreComment; + $model = new ScoreComment(); $this->assertSame('score_comments', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(false, $model->timestamps); - $this->assertSame(['student_id', 'class_section_id', 'score_type', 'semester', 'school_year', 'comment', 'comment_review', 'reviewed_by', 'commented_by', 'created_at'], $model->getFillable()); + $this->assertSame(["student_id", "class_section_id", "score_type", "semester", "school_year", "comment", "comment_review", "reviewed_by", "commented_by", "created_at"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(ScoreComment::class, new ScoreComment); + $this->assertInstanceOf(ScoreComment::class, new ScoreComment()); } } diff --git a/tests/Unit/Models/SectionTest.php b/tests/Unit/Models/SectionTest.php index 7188e4f8..fbd1602a 100644 --- a/tests/Unit/Models/SectionTest.php +++ b/tests/Unit/Models/SectionTest.php @@ -9,16 +9,16 @@ class SectionTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new Section; + $model = new Section(); $this->assertSame('sections', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(false, $model->timestamps); - $this->assertSame(['section_name', 'description', 'updated_at', 'updated_by'], $model->getFillable()); + $this->assertSame(["section_name", "description", "updated_at", "updated_by"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(Section::class, new Section); + $this->assertInstanceOf(Section::class, new Section()); } } diff --git a/tests/Unit/Models/SemesterScoreTest.php b/tests/Unit/Models/SemesterScoreTest.php index abc7541b..c2fe38f5 100644 --- a/tests/Unit/Models/SemesterScoreTest.php +++ b/tests/Unit/Models/SemesterScoreTest.php @@ -9,16 +9,16 @@ class SemesterScoreTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new SemesterScore; + $model = new SemesterScore(); $this->assertSame('semester_scores', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(true, $model->timestamps); - $this->assertSame(['student_id', 'school_id', 'class_section_id', 'updated_by', 'homework_avg', 'quiz_avg', 'project_avg', 'midterm_exam_score', 'final_exam_score', 'attendance_score', 'participation_score', 'ptap_score', 'test_avg', 'semester_score', 'semester', 'school_year', 'created_at', 'updated_at'], $model->getFillable()); + $this->assertSame(["student_id", "school_id", "class_section_id", "updated_by", "homework_avg", "quiz_avg", "project_avg", "midterm_exam_score", "final_exam_score", "attendance_score", "participation_score", "ptap_score", "test_avg", "semester_score", "semester", "school_year", "created_at", "updated_at"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(SemesterScore::class, new SemesterScore); + $this->assertInstanceOf(SemesterScore::class, new SemesterScore()); } } diff --git a/tests/Unit/Models/SettingsTest.php b/tests/Unit/Models/SettingsTest.php index dbb254da..5e507649 100644 --- a/tests/Unit/Models/SettingsTest.php +++ b/tests/Unit/Models/SettingsTest.php @@ -9,16 +9,16 @@ class SettingsTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new Setting; + $model = new Setting(); $this->assertSame('settings', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(true, $model->timestamps); - $this->assertSame(['name', 'timezone', 'updated_by', 'created_at', 'updated_at'], $model->getFillable()); + $this->assertSame(["name", "timezone", "updated_by", "created_at", "updated_at"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(Setting::class, new Setting); + $this->assertInstanceOf(Setting::class, new Setting()); } } diff --git a/tests/Unit/Models/StaffAttendanceTest.php b/tests/Unit/Models/StaffAttendanceTest.php index 40d2dce9..88a9aeeb 100644 --- a/tests/Unit/Models/StaffAttendanceTest.php +++ b/tests/Unit/Models/StaffAttendanceTest.php @@ -9,16 +9,16 @@ class StaffAttendanceTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new StaffAttendance; + $model = new StaffAttendance(); $this->assertSame('staff_attendance', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(true, $model->timestamps); - $this->assertSame(['user_id', 'role_name', 'date', 'semester', 'school_year', 'status', 'reason', 'created_at', 'updated_at', 'created_by', 'updated_by'], $model->getFillable()); + $this->assertSame(["user_id", "role_name", "date", "semester", "school_year", "status", "reason", "created_at", "updated_at", "created_by", "updated_by"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(StaffAttendance::class, new StaffAttendance); + $this->assertInstanceOf(StaffAttendance::class, new StaffAttendance()); } } diff --git a/tests/Unit/Models/StaffTest.php b/tests/Unit/Models/StaffTest.php index daae2fe3..bb2e8b4e 100644 --- a/tests/Unit/Models/StaffTest.php +++ b/tests/Unit/Models/StaffTest.php @@ -9,16 +9,16 @@ class StaffTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new Staff; + $model = new Staff(); $this->assertSame('staff', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(true, $model->timestamps); - $this->assertSame(['firstname', 'lastname', 'email', 'phone', 'role_name', 'school_year', 'active_role', 'created_at', 'updated_at', 'user_id'], $model->getFillable()); + $this->assertSame(["firstname", "lastname", "email", "phone", "role_name", "school_year", "active_role", "created_at", "updated_at", "user_id"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(Staff::class, new Staff); + $this->assertInstanceOf(Staff::class, new Staff()); } } diff --git a/tests/Unit/Models/StatsTest.php b/tests/Unit/Models/StatsTest.php index 0e0c0a3b..526d7635 100644 --- a/tests/Unit/Models/StatsTest.php +++ b/tests/Unit/Models/StatsTest.php @@ -9,16 +9,16 @@ class StatsTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new Stats; + $model = new Stats(); $this->assertSame('stats', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(true, $model->timestamps); - $this->assertSame(['students', 'teachers', 'admins', 'users'], $model->getFillable()); + $this->assertSame(["students", "teachers", "admins", "users"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(Stats::class, new Stats); + $this->assertInstanceOf(Stats::class, new Stats()); } } diff --git a/tests/Unit/Models/StudentAllergyTest.php b/tests/Unit/Models/StudentAllergyTest.php index ad8a35a0..12a7a694 100644 --- a/tests/Unit/Models/StudentAllergyTest.php +++ b/tests/Unit/Models/StudentAllergyTest.php @@ -9,16 +9,16 @@ class StudentAllergyTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new StudentAllergy; + $model = new StudentAllergy(); $this->assertSame('student_allergies', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(false, $model->timestamps); - $this->assertSame(['student_id', 'allergy'], $model->getFillable()); + $this->assertSame(["student_id", "allergy"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(StudentAllergy::class, new StudentAllergy); + $this->assertInstanceOf(StudentAllergy::class, new StudentAllergy()); } } diff --git a/tests/Unit/Models/StudentClassTest.php b/tests/Unit/Models/StudentClassTest.php index 3fc71298..864c1891 100644 --- a/tests/Unit/Models/StudentClassTest.php +++ b/tests/Unit/Models/StudentClassTest.php @@ -9,16 +9,16 @@ class StudentClassTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new StudentClass; + $model = new StudentClass(); $this->assertSame('student_class', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(true, $model->timestamps); - $this->assertSame(['student_id', 'class_section_id', 'is_event_only', 'semester', 'school_year', 'description', 'created_at', 'updated_at', 'updated_by'], $model->getFillable()); + $this->assertSame(["student_id", "class_section_id", "is_event_only", "semester", "school_year", "description", "created_at", "updated_at", "updated_by"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(StudentClass::class, new StudentClass); + $this->assertInstanceOf(StudentClass::class, new StudentClass()); } } diff --git a/tests/Unit/Models/StudentMedicalConditionTest.php b/tests/Unit/Models/StudentMedicalConditionTest.php index bde4515a..8e217ad0 100644 --- a/tests/Unit/Models/StudentMedicalConditionTest.php +++ b/tests/Unit/Models/StudentMedicalConditionTest.php @@ -9,16 +9,16 @@ class StudentMedicalConditionTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new StudentMedicalCondition; + $model = new StudentMedicalCondition(); $this->assertSame('student_medical_conditions', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(false, $model->timestamps); - $this->assertSame(['student_id', 'condition_name'], $model->getFillable()); + $this->assertSame(["student_id", "condition_name"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(StudentMedicalCondition::class, new StudentMedicalCondition); + $this->assertInstanceOf(StudentMedicalCondition::class, new StudentMedicalCondition()); } } diff --git a/tests/Unit/Models/StudentTest.php b/tests/Unit/Models/StudentTest.php index 5e5a7a8e..b06df4df 100644 --- a/tests/Unit/Models/StudentTest.php +++ b/tests/Unit/Models/StudentTest.php @@ -9,16 +9,16 @@ class StudentTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new Student; + $model = new Student(); $this->assertSame('students', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(false, $model->timestamps); - $this->assertSame(['school_id', 'firstname', 'lastname', 'dob', 'age', 'gender', 'is_active', 'registration_grade', 'is_new', 'photo_consent', 'parent_id', 'registration_date', 'tuition_paid', 'rfid_tag', 'semester', 'year_of_registration', 'school_year'], $model->getFillable()); + $this->assertSame(["school_id", "firstname", "lastname", "dob", "age", "gender", "is_active", "registration_grade", "is_new", "photo_consent", "parent_id", "registration_date", "tuition_paid", "rfid_tag", "semester", "year_of_registration", "school_year"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(Student::class, new Student); + $this->assertInstanceOf(Student::class, new Student()); } } diff --git a/tests/Unit/Models/SubjectCurriculumTest.php b/tests/Unit/Models/SubjectCurriculumTest.php index a9a873f6..f06dbc66 100644 --- a/tests/Unit/Models/SubjectCurriculumTest.php +++ b/tests/Unit/Models/SubjectCurriculumTest.php @@ -9,16 +9,16 @@ class SubjectCurriculumTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new SubjectCurriculum; + $model = new SubjectCurriculum(); $this->assertSame('subject_curriculum_items', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(true, $model->timestamps); - $this->assertSame(['class_id', 'subject', 'unit_number', 'unit_title', 'chapter_name', 'created_at', 'updated_at'], $model->getFillable()); + $this->assertSame(["class_id", "subject", "unit_number", "unit_title", "chapter_name", "created_at", "updated_at"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(SubjectCurriculum::class, new SubjectCurriculum); + $this->assertInstanceOf(SubjectCurriculum::class, new SubjectCurriculum()); } } diff --git a/tests/Unit/Models/TeacherClassTest.php b/tests/Unit/Models/TeacherClassTest.php index 6e123d3b..fe1c936d 100644 --- a/tests/Unit/Models/TeacherClassTest.php +++ b/tests/Unit/Models/TeacherClassTest.php @@ -9,16 +9,16 @@ class TeacherClassTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new TeacherClass; + $model = new TeacherClass(); $this->assertSame('teacher_class', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(true, $model->timestamps); - $this->assertSame(['class_section_id', 'teacher_id', 'position', 'semester', 'school_year', 'description', 'created_at', 'updated_at', 'updated_by'], $model->getFillable()); + $this->assertSame(["class_section_id", "teacher_id", "position", "semester", "school_year", "description", "created_at", "updated_at", "updated_by"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(TeacherClass::class, new TeacherClass); + $this->assertInstanceOf(TeacherClass::class, new TeacherClass()); } } diff --git a/tests/Unit/Models/TeacherSubmissionNotificationHistoryTest.php b/tests/Unit/Models/TeacherSubmissionNotificationHistoryTest.php index edcabcf3..2ab11368 100644 --- a/tests/Unit/Models/TeacherSubmissionNotificationHistoryTest.php +++ b/tests/Unit/Models/TeacherSubmissionNotificationHistoryTest.php @@ -9,16 +9,16 @@ class TeacherSubmissionNotificationHistoryTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new TeacherSubmissionNotificationHistory; + $model = new TeacherSubmissionNotificationHistory(); $this->assertSame('teacher_submission_notification_history', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(false, $model->timestamps); - $this->assertSame(['teacher_id', 'class_section_id', 'admin_id', 'notification_category', 'message', 'status', 'school_year', 'semester', 'sent_at'], $model->getFillable()); + $this->assertSame(["teacher_id", "class_section_id", "admin_id", "notification_category", "message", "status", "school_year", "semester", "sent_at"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(TeacherSubmissionNotificationHistory::class, new TeacherSubmissionNotificationHistory); + $this->assertInstanceOf(TeacherSubmissionNotificationHistory::class, new TeacherSubmissionNotificationHistory()); } } diff --git a/tests/Unit/Models/TeacherTest.php b/tests/Unit/Models/TeacherTest.php index 809c3ee0..1a28e6ca 100644 --- a/tests/Unit/Models/TeacherTest.php +++ b/tests/Unit/Models/TeacherTest.php @@ -9,16 +9,16 @@ class TeacherTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new Teacher; + $model = new Teacher(); $this->assertSame('users', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(true, $model->timestamps); - $this->assertSame(['school_id', 'firstname', 'lastname', 'gender', 'cellphone', 'email', 'address_street', 'apt', 'city', 'state', 'zip', 'accept_school_policy', 'is_verified', 'status', 'is_suspended', 'failed_attempts', 'password', 'created_at', 'updated_at', 'token', 'account_id', 'user_type', 'semester', 'school_year', 'rfid_tag', 'last_failed_at'], $model->getFillable()); + $this->assertSame(["school_id", "firstname", "lastname", "gender", "cellphone", "email", "address_street", "apt", "city", "state", "zip", "accept_school_policy", "is_verified", "status", "is_suspended", "failed_attempts", "password", "created_at", "updated_at", "token", "account_id", "user_type", "semester", "school_year", "rfid_tag", "last_failed_at"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(Teacher::class, new Teacher); + $this->assertInstanceOf(Teacher::class, new Teacher()); } } diff --git a/tests/Unit/Models/UserNotificationTest.php b/tests/Unit/Models/UserNotificationTest.php index de539065..8f5dfa5b 100644 --- a/tests/Unit/Models/UserNotificationTest.php +++ b/tests/Unit/Models/UserNotificationTest.php @@ -9,16 +9,16 @@ class UserNotificationTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new UserNotification; + $model = new UserNotification(); $this->assertSame('user_notifications', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(false, $model->timestamps); - $this->assertSame(['notification_id', 'user_id', 'is_read', 'delivered', 'delivered_at', 'school_year', 'semester'], $model->getFillable()); + $this->assertSame(["notification_id", "user_id", "is_read", "delivered", "delivered_at", "school_year", "semester"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(UserNotification::class, new UserNotification); + $this->assertInstanceOf(UserNotification::class, new UserNotification()); } } diff --git a/tests/Unit/Models/UserRoleTest.php b/tests/Unit/Models/UserRoleTest.php index 36594243..c5b77b72 100644 --- a/tests/Unit/Models/UserRoleTest.php +++ b/tests/Unit/Models/UserRoleTest.php @@ -9,16 +9,16 @@ class UserRoleTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new UserRole; + $model = new UserRole(); $this->assertSame('user_roles', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(true, $model->timestamps); - $this->assertSame(['user_id', 'role_id', 'created_at', 'updated_at', 'deleted_at', 'updated_by'], $model->getFillable()); + $this->assertSame(["user_id", "role_id", "created_at", "updated_at", "deleted_at", "updated_by"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(UserRole::class, new UserRole); + $this->assertInstanceOf(UserRole::class, new UserRole()); } } diff --git a/tests/Unit/Models/UserTest.php b/tests/Unit/Models/UserTest.php index 53c8ab5f..26ac46b3 100644 --- a/tests/Unit/Models/UserTest.php +++ b/tests/Unit/Models/UserTest.php @@ -9,16 +9,16 @@ class UserTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new User; + $model = new User(); $this->assertSame('users', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(true, $model->timestamps); - $this->assertSame(['school_id', 'firstname', 'lastname', 'gender', 'cellphone', 'email', 'address_street', 'apt', 'city', 'state', 'zip', 'accept_school_policy', 'is_verified', 'status', 'is_suspended', 'failed_attempts', 'password', 'created_at', 'updated_at', 'token', 'account_id', 'user_type', 'semester', 'school_year', 'rfid_tag', 'last_failed_at'], $model->getFillable()); + $this->assertSame(["school_id", "firstname", "lastname", "gender", "cellphone", "email", "address_street", "apt", "city", "state", "zip", "accept_school_policy", "is_verified", "status", "is_suspended", "failed_attempts", "password", "created_at", "updated_at", "token", "account_id", "user_type", "semester", "school_year", "rfid_tag", "last_failed_at"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(User::class, new User); + $this->assertInstanceOf(User::class, new User()); } } diff --git a/tests/Unit/Models/WhatsappGroupLinkTest.php b/tests/Unit/Models/WhatsappGroupLinkTest.php index c3a1fb12..5b8f51c1 100644 --- a/tests/Unit/Models/WhatsappGroupLinkTest.php +++ b/tests/Unit/Models/WhatsappGroupLinkTest.php @@ -9,16 +9,16 @@ class WhatsappGroupLinkTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new WhatsappGroupLink; + $model = new WhatsappGroupLink(); $this->assertSame('whatsapp_group_links', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(true, $model->timestamps); - $this->assertSame(['class_section_id', 'class_section_name', 'school_year', 'semester', 'invite_link', 'active', 'created_at', 'updated_at'], $model->getFillable()); + $this->assertSame(["class_section_id", "class_section_name", "school_year", "semester", "invite_link", "active", "created_at", "updated_at"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(WhatsappGroupLink::class, new WhatsappGroupLink); + $this->assertInstanceOf(WhatsappGroupLink::class, new WhatsappGroupLink()); } } diff --git a/tests/Unit/Models/WhatsappGroupMembershipTest.php b/tests/Unit/Models/WhatsappGroupMembershipTest.php index 7d652864..e03d410d 100644 --- a/tests/Unit/Models/WhatsappGroupMembershipTest.php +++ b/tests/Unit/Models/WhatsappGroupMembershipTest.php @@ -9,16 +9,16 @@ class WhatsappGroupMembershipTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new WhatsappGroupMembership; + $model = new WhatsappGroupMembership(); $this->assertSame('whatsapp_group_memberships', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(true, $model->timestamps); - $this->assertSame(['class_section_id', 'school_year', 'semester', 'subject_type', 'subject_id', 'is_member', 'verified_by', 'verified_at', 'created_at', 'updated_at'], $model->getFillable()); + $this->assertSame(["class_section_id", "school_year", "semester", "subject_type", "subject_id", "is_member", "verified_by", "verified_at", "created_at", "updated_at"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(WhatsappGroupMembership::class, new WhatsappGroupMembership); + $this->assertInstanceOf(WhatsappGroupMembership::class, new WhatsappGroupMembership()); } } diff --git a/tests/Unit/Models/WhatsappInviteLogTest.php b/tests/Unit/Models/WhatsappInviteLogTest.php index 69680bb0..8e3b4379 100644 --- a/tests/Unit/Models/WhatsappInviteLogTest.php +++ b/tests/Unit/Models/WhatsappInviteLogTest.php @@ -9,16 +9,16 @@ class WhatsappInviteLogTest extends TestCase { public function test_model_metadata_is_converted(): void { - $model = new WhatsappInviteLog; + $model = new WhatsappInviteLog(); $this->assertSame('whatsapp_invites_log', $model->getTable()); $this->assertSame('id', $model->getKeyName()); $this->assertSame(false, $model->timestamps); - $this->assertSame(['parent_id', 'email', 'class_section_id', 'link_id', 'status', 'error_message', 'sent_at'], $model->getFillable()); + $this->assertSame(["parent_id", "email", "class_section_id", "link_id", "status", "error_message", "sent_at"], $model->getFillable()); } public function test_model_class_loads(): void { - $this->assertInstanceOf(WhatsappInviteLog::class, new WhatsappInviteLog); + $this->assertInstanceOf(WhatsappInviteLog::class, new WhatsappInviteLog()); } } diff --git a/tests/Unit/Requests/AttendanceCommentTemplate/StoreAttendanceCommentTemplateRequestTest.php b/tests/Unit/Requests/AttendanceCommentTemplate/StoreAttendanceCommentTemplateRequestTest.php index 1fb34f3f..0aa4c5a6 100644 --- a/tests/Unit/Requests/AttendanceCommentTemplate/StoreAttendanceCommentTemplateRequestTest.php +++ b/tests/Unit/Requests/AttendanceCommentTemplate/StoreAttendanceCommentTemplateRequestTest.php @@ -10,7 +10,7 @@ class StoreAttendanceCommentTemplateRequestTest extends TestCase { private function makeRequest(): StoreAttendanceCommentTemplateRequest { - return new StoreAttendanceCommentTemplateRequest; + return new StoreAttendanceCommentTemplateRequest(); } public function test_rules_pass_with_valid_data(): void @@ -75,4 +75,4 @@ class StoreAttendanceCommentTemplateRequestTest extends TestCase $this->assertArrayHasKey('min_score', $validator->errors()->toArray()); $this->assertArrayHasKey('max_score', $validator->errors()->toArray()); } -} +} \ No newline at end of file diff --git a/tests/Unit/Requests/AttendanceCommentTemplate/UpdateAttendanceCommentTemplateRequestTest.php b/tests/Unit/Requests/AttendanceCommentTemplate/UpdateAttendanceCommentTemplateRequestTest.php index cf9282b1..0d3a3d3e 100644 --- a/tests/Unit/Requests/AttendanceCommentTemplate/UpdateAttendanceCommentTemplateRequestTest.php +++ b/tests/Unit/Requests/AttendanceCommentTemplate/UpdateAttendanceCommentTemplateRequestTest.php @@ -10,7 +10,7 @@ class UpdateAttendanceCommentTemplateRequestTest extends TestCase { private function makeRequest(): UpdateAttendanceCommentTemplateRequest { - return new UpdateAttendanceCommentTemplateRequest; + return new UpdateAttendanceCommentTemplateRequest(); } public function test_rules_pass_with_partial_valid_data(): void @@ -78,4 +78,4 @@ class UpdateAttendanceCommentTemplateRequestTest extends TestCase $this->assertTrue($validator->fails()); $this->assertArrayHasKey('is_active', $validator->errors()->toArray()); } -} +} \ No newline at end of file diff --git a/tests/Unit/Services/Administrator/AdminNotificationSubjectServiceTest.php b/tests/Unit/Services/Administrator/AdminNotificationSubjectServiceTest.php index fe1a0993..aaf4b504 100644 --- a/tests/Unit/Services/Administrator/AdminNotificationSubjectServiceTest.php +++ b/tests/Unit/Services/Administrator/AdminNotificationSubjectServiceTest.php @@ -3,7 +3,6 @@ namespace Tests\Unit\Services\Administrator; use App\Services\Administrator\AdminNotificationSubjectService; -use App\Services\Administrator\AdminNotificationUserService; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\DB; use Tests\TestCase; @@ -45,7 +44,7 @@ class AdminNotificationSubjectServiceTest extends TestCase 'role_id' => 1, ]); - $userService = new AdminNotificationUserService; + $userService = new \App\Services\Administrator\AdminNotificationUserService(); $service = new AdminNotificationSubjectService($userService); $data = $service->alertsData(); diff --git a/tests/Unit/Services/Administrator/AdminNotificationUserServiceTest.php b/tests/Unit/Services/Administrator/AdminNotificationUserServiceTest.php index 5a9e494f..ee9c0c38 100644 --- a/tests/Unit/Services/Administrator/AdminNotificationUserServiceTest.php +++ b/tests/Unit/Services/Administrator/AdminNotificationUserServiceTest.php @@ -66,7 +66,7 @@ class AdminNotificationUserServiceTest extends TestCase ['id' => 2, 'user_id' => 2, 'role_id' => 2], ]); - $service = new AdminNotificationUserService; + $service = new AdminNotificationUserService(); $admins = $service->fetchAdminNotificationUsers(); $this->assertCount(1, $admins); diff --git a/tests/Unit/Services/Administrator/AdminPrintRecipientServiceTest.php b/tests/Unit/Services/Administrator/AdminPrintRecipientServiceTest.php index 09106e10..72f2abb3 100644 --- a/tests/Unit/Services/Administrator/AdminPrintRecipientServiceTest.php +++ b/tests/Unit/Services/Administrator/AdminPrintRecipientServiceTest.php @@ -2,7 +2,6 @@ namespace Tests\Unit\Services\Administrator; -use App\Services\Administrator\AdminNotificationUserService; use App\Services\Administrator\AdminPrintRecipientService; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\DB; @@ -51,7 +50,7 @@ class AdminPrintRecipientServiceTest extends TestCase 'subject' => 'print_requests', ]); - $service = new AdminPrintRecipientService(new AdminNotificationUserService); + $service = new AdminPrintRecipientService(new \App\Services\Administrator\AdminNotificationUserService()); $data = $service->data(); $this->assertTrue($data['assigned'][1]); diff --git a/tests/Unit/Services/Administrator/AdministratorAbsenceServiceTest.php b/tests/Unit/Services/Administrator/AdministratorAbsenceServiceTest.php index 32586771..4522565f 100644 --- a/tests/Unit/Services/Administrator/AdministratorAbsenceServiceTest.php +++ b/tests/Unit/Services/Administrator/AdministratorAbsenceServiceTest.php @@ -2,13 +2,7 @@ namespace Tests\Unit\Services\Administrator; -use App\Models\StaffAttendance; -use App\Models\User; use App\Services\Administrator\AdministratorAbsenceService; -use App\Services\Administrator\AdministratorSharedService; -use App\Services\SemesterRangeService; -use App\Services\Semesters\SemesterConfigService; -use App\Services\Staff\StaffTimeOffLinkService; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Http\Request; use Mockery; @@ -20,17 +14,17 @@ class AdministratorAbsenceServiceTest extends TestCase public function test_submit_requires_reason(): void { - $shared = Mockery::mock(AdministratorSharedService::class); + $shared = Mockery::mock(\App\Services\Administrator\AdministratorSharedService::class); $shared->shouldReceive('getSemester')->andReturn('Fall'); $shared->shouldReceive('getSchoolYear')->andReturn('2025-2026'); $shared->shouldReceive('allowedAbsenceDates')->andReturn(['2025-01-05']); $service = new AdministratorAbsenceService( $shared, - new User, - new StaffAttendance, - new SemesterRangeService(new SemesterConfigService), - Mockery::mock(StaffTimeOffLinkService::class) + new \App\Models\User(), + new \App\Models\StaffAttendance(), + new \App\Services\SemesterRangeService(new \App\Services\Semesters\SemesterConfigService()), + Mockery::mock(\App\Services\Staff\StaffTimeOffLinkService::class) ); $request = Request::create('/absences', 'POST', ['dates' => ['2025-01-05']]); diff --git a/tests/Unit/Services/Administrator/AdministratorDashboardServiceTest.php b/tests/Unit/Services/Administrator/AdministratorDashboardServiceTest.php index f1cd69ed..003428ea 100644 --- a/tests/Unit/Services/Administrator/AdministratorDashboardServiceTest.php +++ b/tests/Unit/Services/Administrator/AdministratorDashboardServiceTest.php @@ -3,8 +3,6 @@ namespace Tests\Unit\Services\Administrator; use App\Services\Administrator\AdministratorDashboardService; -use App\Services\Administrator\AdministratorMetricsService; -use App\Services\Administrator\AdministratorUserSearchService; use Illuminate\Foundation\Testing\RefreshDatabase; use Mockery; use Tests\TestCase; @@ -15,10 +13,10 @@ class AdministratorDashboardServiceTest extends TestCase public function test_delegates_to_metrics_and_search(): void { - $metrics = Mockery::mock(AdministratorMetricsService::class); + $metrics = Mockery::mock(\App\Services\Administrator\AdministratorMetricsService::class); $metrics->shouldReceive('metrics')->once()->andReturn(['counts' => []]); - $search = Mockery::mock(AdministratorUserSearchService::class); + $search = Mockery::mock(\App\Services\Administrator\AdministratorUserSearchService::class); $search->shouldReceive('search')->once()->with('john')->andReturn(['results' => []]); $service = new AdministratorDashboardService($metrics, $search); diff --git a/tests/Unit/Services/Administrator/AdministratorEnrollmentEventServiceTest.php b/tests/Unit/Services/Administrator/AdministratorEnrollmentEventServiceTest.php index e9f49d05..a2a8f1b4 100644 --- a/tests/Unit/Services/Administrator/AdministratorEnrollmentEventServiceTest.php +++ b/tests/Unit/Services/Administrator/AdministratorEnrollmentEventServiceTest.php @@ -15,7 +15,7 @@ class AdministratorEnrollmentEventServiceTest extends TestCase { Event::fake(); - $service = new AdministratorEnrollmentEventService; + $service = new AdministratorEnrollmentEventService(); $service->dispatchGroupedEvents([], [], [], '2025-2026'); Event::assertNotDispatched('studentEnrolled'); diff --git a/tests/Unit/Services/Administrator/AdministratorEnrollmentQueryServiceTest.php b/tests/Unit/Services/Administrator/AdministratorEnrollmentQueryServiceTest.php index 2cba64eb..ce1b3bb3 100644 --- a/tests/Unit/Services/Administrator/AdministratorEnrollmentQueryServiceTest.php +++ b/tests/Unit/Services/Administrator/AdministratorEnrollmentQueryServiceTest.php @@ -2,12 +2,7 @@ namespace Tests\Unit\Services\Administrator; -use App\Models\ClassSection; -use App\Models\Enrollment; -use App\Models\Student; -use App\Models\StudentClass; use App\Services\Administrator\AdministratorEnrollmentQueryService; -use App\Services\Administrator\AdministratorSharedService; use Illuminate\Foundation\Testing\RefreshDatabase; use Mockery; use Tests\TestCase; @@ -18,17 +13,17 @@ class AdministratorEnrollmentQueryServiceTest extends TestCase public function test_enrollment_withdrawal_data_returns_structure(): void { - $shared = Mockery::mock(AdministratorSharedService::class); + $shared = Mockery::mock(\App\Services\Administrator\AdministratorSharedService::class); $shared->shouldReceive('getSchoolYear')->andReturn('2025-2026'); $shared->shouldReceive('getSemester')->andReturn('Fall'); $shared->shouldReceive('getSchoolYearStartYear')->andReturn(2025); $service = new AdministratorEnrollmentQueryService( $shared, - new Student, - new StudentClass, - new Enrollment, - new ClassSection + new \App\Models\Student(), + new \App\Models\StudentClass(), + new \App\Models\Enrollment(), + new \App\Models\ClassSection() ); $data = $service->enrollmentWithdrawalData('2025-2026'); diff --git a/tests/Unit/Services/Administrator/AdministratorEnrollmentRefundServiceTest.php b/tests/Unit/Services/Administrator/AdministratorEnrollmentRefundServiceTest.php index 4f85f04b..4b9399ee 100644 --- a/tests/Unit/Services/Administrator/AdministratorEnrollmentRefundServiceTest.php +++ b/tests/Unit/Services/Administrator/AdministratorEnrollmentRefundServiceTest.php @@ -3,7 +3,6 @@ namespace Tests\Unit\Services\Administrator; use App\Services\Administrator\AdministratorEnrollmentRefundService; -use App\Services\FeeCalculationService; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\DB; use Mockery; @@ -25,7 +24,7 @@ class AdministratorEnrollmentRefundServiceTest extends TestCase 'school_year' => '2025-2026', ]); - $feeCalc = Mockery::mock(FeeCalculationService::class); + $feeCalc = Mockery::mock(\App\Services\FeeCalculationService::class); $service = new AdministratorEnrollmentRefundService($feeCalc); $result = $service->processRefunds([10], '2025-2026', 1); diff --git a/tests/Unit/Services/Administrator/AdministratorEnrollmentServiceTest.php b/tests/Unit/Services/Administrator/AdministratorEnrollmentServiceTest.php index 480bfd42..a199ce38 100644 --- a/tests/Unit/Services/Administrator/AdministratorEnrollmentServiceTest.php +++ b/tests/Unit/Services/Administrator/AdministratorEnrollmentServiceTest.php @@ -2,9 +2,7 @@ namespace Tests\Unit\Services\Administrator; -use App\Services\Administrator\AdministratorEnrollmentQueryService; use App\Services\Administrator\AdministratorEnrollmentService; -use App\Services\Administrator\AdministratorEnrollmentStatusService; use Illuminate\Foundation\Testing\RefreshDatabase; use Mockery; use Tests\TestCase; @@ -15,11 +13,11 @@ class AdministratorEnrollmentServiceTest extends TestCase public function test_delegates_to_query_and_status_services(): void { - $query = Mockery::mock(AdministratorEnrollmentQueryService::class); + $query = Mockery::mock(\App\Services\Administrator\AdministratorEnrollmentQueryService::class); $query->shouldReceive('enrollmentWithdrawalData')->once()->andReturn(['rows' => []]); $query->shouldReceive('newStudentsData')->once()->andReturn(['students' => []]); - $status = Mockery::mock(AdministratorEnrollmentStatusService::class); + $status = Mockery::mock(\App\Services\Administrator\AdministratorEnrollmentStatusService::class); $status->shouldReceive('updateStatuses')->once()->andReturn(['success' => true]); $service = new AdministratorEnrollmentService($query, $status); diff --git a/tests/Unit/Services/Administrator/AdministratorEnrollmentStatusServiceTest.php b/tests/Unit/Services/Administrator/AdministratorEnrollmentStatusServiceTest.php index 4b9abf7d..a902889e 100644 --- a/tests/Unit/Services/Administrator/AdministratorEnrollmentStatusServiceTest.php +++ b/tests/Unit/Services/Administrator/AdministratorEnrollmentStatusServiceTest.php @@ -2,12 +2,7 @@ namespace Tests\Unit\Services\Administrator; -use App\Models\Student; -use App\Models\User; -use App\Services\Administrator\AdministratorEnrollmentEventService; -use App\Services\Administrator\AdministratorEnrollmentRefundService; use App\Services\Administrator\AdministratorEnrollmentStatusService; -use App\Services\Administrator\AdministratorSharedService; use Illuminate\Foundation\Testing\RefreshDatabase; use Mockery; use Tests\TestCase; @@ -18,17 +13,17 @@ class AdministratorEnrollmentStatusServiceTest extends TestCase public function test_update_statuses_requires_payload(): void { - $shared = Mockery::mock(AdministratorSharedService::class); + $shared = Mockery::mock(\App\Services\Administrator\AdministratorSharedService::class); $shared->shouldReceive('getSchoolYear')->andReturn('2025-2026'); $shared->shouldReceive('getSemester')->andReturn('Fall'); - $refundService = Mockery::mock(AdministratorEnrollmentRefundService::class); - $eventService = Mockery::mock(AdministratorEnrollmentEventService::class); + $refundService = Mockery::mock(\App\Services\Administrator\AdministratorEnrollmentRefundService::class); + $eventService = Mockery::mock(\App\Services\Administrator\AdministratorEnrollmentEventService::class); $service = new AdministratorEnrollmentStatusService( $shared, - new Student, - new User, + new \App\Models\Student(), + new \App\Models\User(), $refundService, $eventService ); diff --git a/tests/Unit/Services/Administrator/AdministratorMetricsServiceTest.php b/tests/Unit/Services/Administrator/AdministratorMetricsServiceTest.php index 4332d3a9..e19d0333 100644 --- a/tests/Unit/Services/Administrator/AdministratorMetricsServiceTest.php +++ b/tests/Unit/Services/Administrator/AdministratorMetricsServiceTest.php @@ -2,11 +2,7 @@ namespace Tests\Unit\Services\Administrator; -use App\Models\Configuration; -use App\Models\LoginActivity; -use App\Models\User; use App\Services\Administrator\AdministratorMetricsService; -use App\Services\Administrator\AdministratorSharedService; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\DB; use Tests\TestCase; @@ -23,9 +19,9 @@ class AdministratorMetricsServiceTest extends TestCase ]); $service = new AdministratorMetricsService( - new AdministratorSharedService(new Configuration), - new User, - new LoginActivity + new \App\Services\Administrator\AdministratorSharedService(new \App\Models\Configuration()), + new \App\Models\User(), + new \App\Models\LoginActivity() ); $metrics = $service->metrics(); diff --git a/tests/Unit/Services/Administrator/AdministratorNotificationServiceTest.php b/tests/Unit/Services/Administrator/AdministratorNotificationServiceTest.php index 7c486c2c..1e3d8927 100644 --- a/tests/Unit/Services/Administrator/AdministratorNotificationServiceTest.php +++ b/tests/Unit/Services/Administrator/AdministratorNotificationServiceTest.php @@ -3,8 +3,6 @@ namespace Tests\Unit\Services\Administrator; use App\Services\Administrator\AdministratorNotificationService; -use App\Services\Administrator\AdminNotificationSubjectService; -use App\Services\Administrator\AdminPrintRecipientService; use Illuminate\Foundation\Testing\RefreshDatabase; use Mockery; use Tests\TestCase; @@ -15,11 +13,11 @@ class AdministratorNotificationServiceTest extends TestCase public function test_delegates_to_subject_and_print_services(): void { - $subject = Mockery::mock(AdminNotificationSubjectService::class); + $subject = Mockery::mock(\App\Services\Administrator\AdminNotificationSubjectService::class); $subject->shouldReceive('alertsData')->once()->andReturn(['admins' => []]); $subject->shouldReceive('save')->once()->andReturn(['success' => true]); - $print = Mockery::mock(AdminPrintRecipientService::class); + $print = Mockery::mock(\App\Services\Administrator\AdminPrintRecipientService::class); $print->shouldReceive('data')->once()->andReturn(['admins' => []]); $print->shouldReceive('save')->once()->andReturn(['success' => true]); diff --git a/tests/Unit/Services/Administrator/AdministratorSharedServiceTest.php b/tests/Unit/Services/Administrator/AdministratorSharedServiceTest.php index d29db504..f8925487 100644 --- a/tests/Unit/Services/Administrator/AdministratorSharedServiceTest.php +++ b/tests/Unit/Services/Administrator/AdministratorSharedServiceTest.php @@ -2,7 +2,6 @@ namespace Tests\Unit\Services\Administrator; -use App\Models\Configuration; use App\Services\Administrator\AdministratorSharedService; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; @@ -13,7 +12,7 @@ class AdministratorSharedServiceTest extends TestCase public function test_get_previous_school_year(): void { - $service = new AdministratorSharedService(new Configuration); + $service = new AdministratorSharedService(new \App\Models\Configuration()); $this->assertSame('2024-2025', $service->getPreviousSchoolYear('2025-2026')); $this->assertSame('2024-25', $service->getPreviousSchoolYear('2025-26')); diff --git a/tests/Unit/Services/Administrator/AdministratorTeacherSubmissionServiceTest.php b/tests/Unit/Services/Administrator/AdministratorTeacherSubmissionServiceTest.php index 76917851..c1f50f38 100644 --- a/tests/Unit/Services/Administrator/AdministratorTeacherSubmissionServiceTest.php +++ b/tests/Unit/Services/Administrator/AdministratorTeacherSubmissionServiceTest.php @@ -3,10 +3,7 @@ namespace Tests\Unit\Services\Administrator; use App\Services\Administrator\AdministratorTeacherSubmissionService; -use App\Services\Administrator\TeacherSubmissionNotificationService; -use App\Services\Administrator\TeacherSubmissionReportService; use Illuminate\Foundation\Testing\RefreshDatabase; -use Illuminate\Http\Request; use Mockery; use Tests\TestCase; @@ -16,15 +13,15 @@ class AdministratorTeacherSubmissionServiceTest extends TestCase public function test_delegates_to_report_and_notification(): void { - $report = Mockery::mock(TeacherSubmissionReportService::class); + $report = Mockery::mock(\App\Services\Administrator\TeacherSubmissionReportService::class); $report->shouldReceive('report')->once()->andReturn(['rows' => []]); - $notify = Mockery::mock(TeacherSubmissionNotificationService::class); + $notify = Mockery::mock(\App\Services\Administrator\TeacherSubmissionNotificationService::class); $notify->shouldReceive('send')->once()->andReturn(['success' => true]); $service = new AdministratorTeacherSubmissionService($report, $notify); $this->assertSame(['rows' => []], $service->report()); - $this->assertSame(['success' => true], $service->sendNotifications(new Request, 1)); + $this->assertSame(['success' => true], $service->sendNotifications(new \Illuminate\Http\Request(), 1)); } } diff --git a/tests/Unit/Services/Administrator/AdministratorUserSearchServiceTest.php b/tests/Unit/Services/Administrator/AdministratorUserSearchServiceTest.php index e5cb5cf5..184e7356 100644 --- a/tests/Unit/Services/Administrator/AdministratorUserSearchServiceTest.php +++ b/tests/Unit/Services/Administrator/AdministratorUserSearchServiceTest.php @@ -12,7 +12,7 @@ class AdministratorUserSearchServiceTest extends TestCase public function test_empty_query_returns_no_results(): void { - $service = new AdministratorUserSearchService; + $service = new AdministratorUserSearchService(); $result = $service->search(''); $this->assertSame('', $result['query']); diff --git a/tests/Unit/Services/Administrator/TeacherSubmissionNotificationServiceTest.php b/tests/Unit/Services/Administrator/TeacherSubmissionNotificationServiceTest.php index 539a53e4..bc1f79d3 100644 --- a/tests/Unit/Services/Administrator/TeacherSubmissionNotificationServiceTest.php +++ b/tests/Unit/Services/Administrator/TeacherSubmissionNotificationServiceTest.php @@ -2,9 +2,7 @@ namespace Tests\Unit\Services\Administrator; -use App\Services\Administrator\AdministratorSharedService; use App\Services\Administrator\TeacherSubmissionNotificationService; -use App\Services\Administrator\TeacherSubmissionSupportService; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Http\Request; use Mockery; @@ -16,8 +14,8 @@ class TeacherSubmissionNotificationServiceTest extends TestCase public function test_send_requires_targets(): void { - $shared = Mockery::mock(AdministratorSharedService::class); - $support = Mockery::mock(TeacherSubmissionSupportService::class); + $shared = Mockery::mock(\App\Services\Administrator\AdministratorSharedService::class); + $support = Mockery::mock(\App\Services\Administrator\TeacherSubmissionSupportService::class); $service = new TeacherSubmissionNotificationService($shared, $support); $request = Request::create('/notify', 'POST', []); diff --git a/tests/Unit/Services/Administrator/TeacherSubmissionReportServiceTest.php b/tests/Unit/Services/Administrator/TeacherSubmissionReportServiceTest.php index 5eb08705..e812f0c0 100644 --- a/tests/Unit/Services/Administrator/TeacherSubmissionReportServiceTest.php +++ b/tests/Unit/Services/Administrator/TeacherSubmissionReportServiceTest.php @@ -2,9 +2,7 @@ namespace Tests\Unit\Services\Administrator; -use App\Services\Administrator\AdministratorSharedService; use App\Services\Administrator\TeacherSubmissionReportService; -use App\Services\Administrator\TeacherSubmissionSupportService; use Illuminate\Foundation\Testing\RefreshDatabase; use Mockery; use Tests\TestCase; @@ -15,11 +13,11 @@ class TeacherSubmissionReportServiceTest extends TestCase public function test_report_returns_summary(): void { - $shared = Mockery::mock(AdministratorSharedService::class); + $shared = Mockery::mock(\App\Services\Administrator\AdministratorSharedService::class); $shared->shouldReceive('getSemester')->andReturn('Fall'); $shared->shouldReceive('getSchoolYear')->andReturn('2025-2026'); - $support = Mockery::mock(TeacherSubmissionSupportService::class); + $support = Mockery::mock(\App\Services\Administrator\TeacherSubmissionSupportService::class); $support->shouldReceive('submissionStatus')->andReturn([ 'label' => 'No students', 'badge' => 'bg-secondary', diff --git a/tests/Unit/Services/Administrator/TeacherSubmissionSupportServiceTest.php b/tests/Unit/Services/Administrator/TeacherSubmissionSupportServiceTest.php index 5d8118c6..5b2f4783 100644 --- a/tests/Unit/Services/Administrator/TeacherSubmissionSupportServiceTest.php +++ b/tests/Unit/Services/Administrator/TeacherSubmissionSupportServiceTest.php @@ -12,7 +12,7 @@ class TeacherSubmissionSupportServiceTest extends TestCase public function test_submission_status_labels(): void { - $service = new TeacherSubmissionSupportService; + $service = new TeacherSubmissionSupportService(); $status = $service->submissionStatus(3, 5); $this->assertSame('Missing', $status['label']); @@ -21,7 +21,7 @@ class TeacherSubmissionSupportServiceTest extends TestCase public function test_build_missing_items(): void { - $service = new TeacherSubmissionSupportService; + $service = new TeacherSubmissionSupportService(); $items = $service->buildMissingItems([ 'attendance_status' => ['completed' => false], 'midterm_score_status' => ['completed' => true], diff --git a/tests/Unit/Services/Assignment/AssignmentContextServiceTest.php b/tests/Unit/Services/Assignment/AssignmentContextServiceTest.php index c68da714..a483177f 100644 --- a/tests/Unit/Services/Assignment/AssignmentContextServiceTest.php +++ b/tests/Unit/Services/Assignment/AssignmentContextServiceTest.php @@ -19,7 +19,7 @@ class AssignmentContextServiceTest extends TestCase ['id' => 2, 'config_key' => 'semester', 'config_value' => 'Fall'], ]); - $service = new AssignmentContextService(new Configuration); + $service = new AssignmentContextService(new Configuration()); $this->assertSame('Fall', $service->getCurrentSemester()); $this->assertSame('2025-2026', $service->getCurrentSchoolYear()); diff --git a/tests/Unit/Services/Assignment/AssignmentDataLoaderServiceTest.php b/tests/Unit/Services/Assignment/AssignmentDataLoaderServiceTest.php index d7452315..bbb477f1 100644 --- a/tests/Unit/Services/Assignment/AssignmentDataLoaderServiceTest.php +++ b/tests/Unit/Services/Assignment/AssignmentDataLoaderServiceTest.php @@ -17,7 +17,7 @@ class AssignmentDataLoaderServiceTest extends TestCase { $this->seedTeacherData(); - $service = new AssignmentDataLoaderService(new TeacherClass, new StudentClass); + $service = new AssignmentDataLoaderService(new TeacherClass(), new StudentClass()); $rows = $service->loadTeacherClasses('2025-2026'); $this->assertCount(1, $rows); @@ -28,7 +28,7 @@ class AssignmentDataLoaderServiceTest extends TestCase { $this->seedStudentData(); - $service = new AssignmentDataLoaderService(new TeacherClass, new StudentClass); + $service = new AssignmentDataLoaderService(new TeacherClass(), new StudentClass()); $rows = $service->loadStudentClasses('2025-2026'); $this->assertCount(1, $rows); diff --git a/tests/Unit/Services/Assignment/AssignmentMetaServiceTest.php b/tests/Unit/Services/Assignment/AssignmentMetaServiceTest.php index 98db24d4..beae96e3 100644 --- a/tests/Unit/Services/Assignment/AssignmentMetaServiceTest.php +++ b/tests/Unit/Services/Assignment/AssignmentMetaServiceTest.php @@ -13,7 +13,7 @@ class AssignmentMetaServiceTest extends TestCase public function test_get_school_years_uses_fallback(): void { - $service = new AssignmentMetaService; + $service = new AssignmentMetaService(); $years = $service->getSchoolYears('2025-2026'); $this->assertSame(['2025-2026'], $years); @@ -27,7 +27,7 @@ class AssignmentMetaServiceTest extends TestCase ['class_section_id' => 103, 'teacher_id' => 3, 'position' => 'main', 'school_year' => '2025-2026', 'semester' => 'Fall'], ]); - $service = new AssignmentMetaService; + $service = new AssignmentMetaService(); $years = $service->getSchoolYears(); $this->assertSame(['2025-2026', '2024-2025'], $years); @@ -35,7 +35,7 @@ class AssignmentMetaServiceTest extends TestCase public function test_first_non_empty_returns_first_value(): void { - $service = new AssignmentMetaService; + $service = new AssignmentMetaService(); $value = $service->firstNonEmpty([null, '', 'Fall', 'Spring']); $this->assertSame('Fall', $value); diff --git a/tests/Unit/Services/Assignment/AssignmentSectionServiceTest.php b/tests/Unit/Services/Assignment/AssignmentSectionServiceTest.php index 69db2fcd..99bcc032 100644 --- a/tests/Unit/Services/Assignment/AssignmentSectionServiceTest.php +++ b/tests/Unit/Services/Assignment/AssignmentSectionServiceTest.php @@ -23,7 +23,7 @@ class AssignmentSectionServiceTest extends TestCase 'school_year' => '2025-2026', ]); - $service = new AssignmentSectionService(new ClassSection); + $service = new AssignmentSectionService(new ClassSection()); $map = $service->getSectionNamesMap([1]); $this->assertSame('1-A', $map[1]); diff --git a/tests/Unit/Services/AssignmentServiceTest.php b/tests/Unit/Services/AssignmentServiceTest.php index e0e1db12..9b8a4725 100644 --- a/tests/Unit/Services/AssignmentServiceTest.php +++ b/tests/Unit/Services/AssignmentServiceTest.php @@ -17,19 +17,12 @@ class AssignmentServiceTest extends TestCase use RefreshDatabase; protected AssignmentService $service; - protected User $teacherMain; - protected User $teacherAssistant; - protected User $updatedByUser; - protected ClassSection $sectionA; - protected ClassSection $sectionB; - protected Student $studentOne; - protected Student $studentTwo; protected function setUp(): void diff --git a/tests/Unit/Services/Attendance/AttendanceAutoPublishJobServiceTest.php b/tests/Unit/Services/Attendance/AttendanceAutoPublishJobServiceTest.php index c26397f5..429b32f6 100644 --- a/tests/Unit/Services/Attendance/AttendanceAutoPublishJobServiceTest.php +++ b/tests/Unit/Services/Attendance/AttendanceAutoPublishJobServiceTest.php @@ -16,7 +16,7 @@ class AttendanceAutoPublishJobServiceTest extends TestCase public function test_run_publishes_due_records(): void { - $autoPublish = new AttendanceAutoPublishService; + $autoPublish = new AttendanceAutoPublishService(); $service = new AttendanceAutoPublishJobService($autoPublish); $now = new DateTimeImmutable('2025-03-01 10:00:00', new DateTimeZone('UTC')); diff --git a/tests/Unit/Services/Attendance/AttendanceAutoPublishServiceTest.php b/tests/Unit/Services/Attendance/AttendanceAutoPublishServiceTest.php index 6620089b..f026e383 100644 --- a/tests/Unit/Services/Attendance/AttendanceAutoPublishServiceTest.php +++ b/tests/Unit/Services/Attendance/AttendanceAutoPublishServiceTest.php @@ -10,7 +10,7 @@ class AttendanceAutoPublishServiceTest extends TestCase { public function test_second_sunday_after_end_of_day(): void { - $service = new AttendanceAutoPublishService; + $service = new AttendanceAutoPublishService(); $result = $service->secondSundayAfterEndOfDay('2025-02-03', 'UTC'); @@ -19,7 +19,7 @@ class AttendanceAutoPublishServiceTest extends TestCase public function test_second_sunday_backward_date(): void { - $service = new AttendanceAutoPublishService; + $service = new AttendanceAutoPublishService(); $now = new DateTimeImmutable('2025-02-16 10:00:00', new \DateTimeZone('UTC')); $result = $service->secondSundayBackwardDate($now, 'UTC'); diff --git a/tests/Unit/Services/Attendance/AttendanceCommentServiceTest.php b/tests/Unit/Services/Attendance/AttendanceCommentServiceTest.php index e9800ab2..fc7e6273 100644 --- a/tests/Unit/Services/Attendance/AttendanceCommentServiceTest.php +++ b/tests/Unit/Services/Attendance/AttendanceCommentServiceTest.php @@ -28,7 +28,7 @@ class AttendanceCommentServiceTest extends TestCase ], ]); - $service = new AttendanceCommentService; + $service = new AttendanceCommentService(); $comment = $service->commentFromScore(95, 'James'); $this->assertSame("James' attendance is excellent.", $comment); @@ -43,7 +43,7 @@ class AttendanceCommentServiceTest extends TestCase 'is_active' => 1, ]); - $service = new AttendanceCommentService; + $service = new AttendanceCommentService(); $comment = $service->commentFromScore(70); $this->assertSame("This student's needs improvement in attendance.", $comment); diff --git a/tests/Unit/Services/Attendance/AttendanceConsequenceServiceTest.php b/tests/Unit/Services/Attendance/AttendanceConsequenceServiceTest.php index de7173ea..75e67763 100644 --- a/tests/Unit/Services/Attendance/AttendanceConsequenceServiceTest.php +++ b/tests/Unit/Services/Attendance/AttendanceConsequenceServiceTest.php @@ -7,7 +7,6 @@ use App\Services\Attendance\AttendanceConsequenceService; use App\Services\EmailService; use App\Services\Notifications\UserNotificationDispatchService; use Illuminate\Foundation\Testing\RefreshDatabase; -use Illuminate\Support\Facades\DB; use Mockery; use Tests\TestCase; @@ -31,7 +30,7 @@ class AttendanceConsequenceServiceTest extends TestCase public function test_send_follow_up_marks_tracking_and_notifies(): void { - DB::table('students')->insert([ + \Illuminate\Support\Facades\DB::table('students')->insert([ 'id' => 1, 'school_id' => 1, 'parent_id' => 1, diff --git a/tests/Unit/Services/Attendance/AttendancePolicyServiceTest.php b/tests/Unit/Services/Attendance/AttendancePolicyServiceTest.php index b8e6caaf..78da45ac 100644 --- a/tests/Unit/Services/Attendance/AttendancePolicyServiceTest.php +++ b/tests/Unit/Services/Attendance/AttendancePolicyServiceTest.php @@ -12,7 +12,7 @@ class AttendancePolicyServiceTest extends TestCase public function test_teacher_can_edit_draft(): void { - $service = new AttendancePolicyService; + $service = new AttendancePolicyService(); $allowed = $service->canEditDay( ['status' => 'draft'], ['roles' => ['Teacher'], 'permissions' => []] @@ -23,7 +23,7 @@ class AttendancePolicyServiceTest extends TestCase public function test_teacher_cannot_edit_finalized(): void { - $service = new AttendancePolicyService; + $service = new AttendancePolicyService(); $allowed = $service->canEditDay( ['status' => 'finalized'], ['roles' => ['Teacher'], 'permissions' => []] @@ -34,7 +34,7 @@ class AttendancePolicyServiceTest extends TestCase public function test_admin_like_by_permission(): void { - $service = new AttendancePolicyService; + $service = new AttendancePolicyService(); $isAdmin = $service->isAdminLike([], ['attendance.manage.daily']); $this->assertTrue($isAdmin); @@ -42,7 +42,7 @@ class AttendancePolicyServiceTest extends TestCase public function test_can_manage_early_dismissals_for_teacher(): void { - $service = new AttendancePolicyService; + $service = new AttendancePolicyService(); $allowed = $service->canManageEarlyDismissals(['Teacher']); $this->assertTrue($allowed); diff --git a/tests/Unit/Services/Attendance/AttendanceQueryServiceTest.php b/tests/Unit/Services/Attendance/AttendanceQueryServiceTest.php index d04c1bfa..1cbac45c 100644 --- a/tests/Unit/Services/Attendance/AttendanceQueryServiceTest.php +++ b/tests/Unit/Services/Attendance/AttendanceQueryServiceTest.php @@ -2,25 +2,7 @@ namespace Tests\Unit\Services\Attendance; -use App\Models\AttendanceData; -use App\Models\AttendanceDay; -use App\Models\AttendanceRecord; -use App\Models\Calendar; -use App\Models\ClassSection; -use App\Models\Configuration; -use App\Models\Student; -use App\Models\StudentClass; -use App\Models\TeacherClass; -use App\Models\User; -use App\Models\UserRole; -use App\Services\Attendance\AttendanceAutoPublishService; -use App\Services\Attendance\AttendancePolicyService; use App\Services\Attendance\AttendanceQueryService; -use App\Services\Attendance\AttendanceRecordSyncService; -use App\Services\Attendance\AttendanceService; -use App\Services\Attendance\SemesterRangeService; -use App\Services\Attendance\StudentAttendanceWriterService; -use App\Services\Attendance\TeacherAttendanceSubmissionService; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\DB; use Tests\TestCase; @@ -80,45 +62,45 @@ class AttendanceQueryServiceTest extends TestCase ]); $service = new AttendanceQueryService( - new Configuration, - new AttendanceData, - new AttendanceDay, - new AttendanceRecord, - new Student, - new StudentClass, - new ClassSection, - new TeacherClass, - new Calendar, - new User, - new UserRole, - new AttendanceService( - new Configuration, - new AttendanceData, - new AttendanceDay, - new ClassSection, - new UserRole, - new AttendancePolicyService, - new StudentAttendanceWriterService( - new AttendanceData, - new Student, - new AttendanceRecordSyncService(new AttendanceRecord) + new \App\Models\Configuration(), + new \App\Models\AttendanceData(), + new \App\Models\AttendanceDay(), + new \App\Models\AttendanceRecord(), + new \App\Models\Student(), + new \App\Models\StudentClass(), + new \App\Models\ClassSection(), + new \App\Models\TeacherClass(), + new \App\Models\Calendar(), + new \App\Models\User(), + new \App\Models\UserRole(), + new \App\Services\Attendance\AttendanceService( + new \App\Models\Configuration(), + new \App\Models\AttendanceData(), + new \App\Models\AttendanceDay(), + new \App\Models\ClassSection(), + new \App\Models\UserRole(), + new \App\Services\Attendance\AttendancePolicyService(), + new \App\Services\Attendance\StudentAttendanceWriterService( + new \App\Models\AttendanceData(), + new \App\Models\Student(), + new \App\Services\Attendance\AttendanceRecordSyncService(new \App\Models\AttendanceRecord()) ), - new TeacherAttendanceSubmissionService( - new AttendanceDay, - new AttendanceData, - new TeacherClass, - new Student, - new AttendancePolicyService, - new StudentAttendanceWriterService( - new AttendanceData, - new Student, - new AttendanceRecordSyncService(new AttendanceRecord) + new \App\Services\Attendance\TeacherAttendanceSubmissionService( + new \App\Models\AttendanceDay(), + new \App\Models\AttendanceData(), + new \App\Models\TeacherClass(), + new \App\Models\Student(), + new \App\Services\Attendance\AttendancePolicyService(), + new \App\Services\Attendance\StudentAttendanceWriterService( + new \App\Models\AttendanceData(), + new \App\Models\Student(), + new \App\Services\Attendance\AttendanceRecordSyncService(new \App\Models\AttendanceRecord()) ), - new AttendanceAutoPublishService + new \App\Services\Attendance\AttendanceAutoPublishService() ), - new AttendanceRecordSyncService(new AttendanceRecord) + new \App\Services\Attendance\AttendanceRecordSyncService(new \App\Models\AttendanceRecord()) ), - new SemesterRangeService + new \App\Services\Attendance\SemesterRangeService() ); $grid = $service->teacherGrid('Fall', '2025-2026', '2025-01-05', 10); diff --git a/tests/Unit/Services/Attendance/AttendanceRecordSyncServiceTest.php b/tests/Unit/Services/Attendance/AttendanceRecordSyncServiceTest.php index 33eb522b..5b6fb40f 100644 --- a/tests/Unit/Services/Attendance/AttendanceRecordSyncServiceTest.php +++ b/tests/Unit/Services/Attendance/AttendanceRecordSyncServiceTest.php @@ -2,7 +2,6 @@ namespace Tests\Unit\Services\Attendance; -use App\Models\AttendanceRecord; use App\Services\Attendance\AttendanceRecordSyncService; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\DB; @@ -26,7 +25,7 @@ class AttendanceRecordSyncServiceTest extends TestCase 'school_year' => '2025-2026', ]); - $service = new AttendanceRecordSyncService(new AttendanceRecord); + $service = new AttendanceRecordSyncService(new \App\Models\AttendanceRecord()); $service->updateAttendanceRecord(10, 'S1', 'absent', 'Fall', '2025-2026', 'present', 1); $row = DB::table('attendance_record')->where('student_id', 10)->first(); @@ -36,7 +35,7 @@ class AttendanceRecordSyncServiceTest extends TestCase public function test_add_new_attendance_record_creates_row(): void { - $service = new AttendanceRecordSyncService(new AttendanceRecord); + $service = new AttendanceRecordSyncService(new \App\Models\AttendanceRecord()); $service->addNewAttendanceRecord(1, 10, 'S1', 'present', 'Fall', '2025-2026', 1); $this->assertDatabaseHas('attendance_record', [ diff --git a/tests/Unit/Services/Attendance/AttendanceSemesterRangeServiceTest.php b/tests/Unit/Services/Attendance/AttendanceSemesterRangeServiceTest.php index 478292cb..7c6d6a15 100644 --- a/tests/Unit/Services/Attendance/AttendanceSemesterRangeServiceTest.php +++ b/tests/Unit/Services/Attendance/AttendanceSemesterRangeServiceTest.php @@ -12,7 +12,7 @@ class AttendanceSemesterRangeServiceTest extends TestCase public function test_get_semester_range(): void { - $service = new SemesterRangeService; + $service = new SemesterRangeService(); $range = $service->getSemesterRange('2025-2026', 'fall'); $this->assertSame(['2025-09-21', '2026-01-18'], $range); @@ -20,7 +20,7 @@ class AttendanceSemesterRangeServiceTest extends TestCase public function test_build_sunday_list_returns_sundays(): void { - $service = new SemesterRangeService; + $service = new SemesterRangeService(); $dates = $service->buildSundayList('2025-09-01', '2025-09-30'); $this->assertContains('2025-09-07', $dates); diff --git a/tests/Unit/Services/Attendance/AttendanceServiceTest.php b/tests/Unit/Services/Attendance/AttendanceServiceTest.php index 19af4587..cb7e7eec 100644 --- a/tests/Unit/Services/Attendance/AttendanceServiceTest.php +++ b/tests/Unit/Services/Attendance/AttendanceServiceTest.php @@ -34,11 +34,11 @@ class AttendanceServiceTest extends TestCase $recordSync = Mockery::mock(AttendanceRecordSyncService::class); $service = new AttendanceService( - new Configuration, - new AttendanceData, - new AttendanceDay, - new ClassSection, - new UserRole, + new Configuration(), + new AttendanceData(), + new AttendanceDay(), + new ClassSection(), + new UserRole(), $policy, $writer, $teacherSubmission, @@ -70,11 +70,11 @@ class AttendanceServiceTest extends TestCase $recordSync = Mockery::mock(AttendanceRecordSyncService::class); $service = new AttendanceService( - new Configuration, - new AttendanceData, - new AttendanceDay, - new ClassSection, - new UserRole, + new Configuration(), + new AttendanceData(), + new AttendanceDay(), + new ClassSection(), + new UserRole(), $policy, $writer, $teacherSubmission, diff --git a/tests/Unit/Services/Attendance/AttendanceSummaryRebuildServiceTest.php b/tests/Unit/Services/Attendance/AttendanceSummaryRebuildServiceTest.php index c28a2e72..4b0dd0af 100644 --- a/tests/Unit/Services/Attendance/AttendanceSummaryRebuildServiceTest.php +++ b/tests/Unit/Services/Attendance/AttendanceSummaryRebuildServiceTest.php @@ -46,7 +46,7 @@ class AttendanceSummaryRebuildServiceTest extends TestCase ], ]); - $service = new AttendanceSummaryRebuildService; + $service = new AttendanceSummaryRebuildService(); $result = $service->rebuild(); $this->assertSame(2, $result['inserted']); diff --git a/tests/Unit/Services/Attendance/AttendanceTrackingService.php b/tests/Unit/Services/Attendance/AttendanceTrackingService.php index 3961de5b..4b7386a2 100644 --- a/tests/Unit/Services/Attendance/AttendanceTrackingService.php +++ b/tests/Unit/Services/Attendance/AttendanceTrackingService.php @@ -2,13 +2,13 @@ namespace App\Services; -use App\Models\AttendanceData; -use App\Models\AttendanceEmailTemplate; use App\Models\AttendanceTracking; -use App\Models\Configuration; -use App\Models\ParentNotification; -use App\Models\Student; use App\Models\StudentClass; +use App\Models\Student; +use App\Models\Configuration; +use App\Models\AttendanceData; +use App\Models\ParentNotification; +use App\Models\AttendanceEmailTemplate; use Carbon\Carbon; use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\Facades\DB; @@ -19,11 +19,8 @@ use Throwable; class AttendanceTrackingService { protected string $semester; - protected string $schoolYear; - protected array $debugCompute = []; - protected ?array $attendanceReportedColumns = null; public function __construct( @@ -43,19 +40,19 @@ class AttendanceTrackingService public function getPendingViolations(?string $schoolYearParam = null, ?string $semesterParam = null): array { $schoolYear = filled($schoolYearParam) ? (string) $schoolYearParam : (string) $this->schoolYear; - $semester = filled($semesterParam) ? (string) $semesterParam : null; // disabled like legacy + $semester = filled($semesterParam) ? (string) $semesterParam : null; // disabled like legacy $debugInfo = [ 'school_year_param' => $schoolYear, - 'semester_param' => $semester, - 'class_students' => 0, - 'student_ids' => 0, - 'attendance_rows' => 0, - 'filtered_rows' => 0, - 'violations' => 0, - 'active_weeks' => 0, - 'abs_last5' => 0, - 'late_last5' => 0, + 'semester_param' => $semester, + 'class_students' => 0, + 'student_ids' => 0, + 'attendance_rows' => 0, + 'filtered_rows' => 0, + 'violations' => 0, + 'active_weeks' => 0, + 'abs_last5' => 0, + 'late_last5' => 0, ]; $classStudentsQuery = $this->studentClassModel->query(); @@ -76,9 +73,9 @@ class AttendanceTrackingService ->orderByDesc('id') ->first(); - if ($latestTerm && ! empty($latestTerm->school_year)) { + if ($latestTerm && !empty($latestTerm->school_year)) { $schoolYear = (string) $latestTerm->school_year; - if (! empty($latestTerm->semester)) { + if (!empty($latestTerm->semester)) { $semester = (string) $latestTerm->semester; } @@ -109,7 +106,7 @@ class AttendanceTrackingService if (empty($studentIds) && empty($studentCodes)) { $attendanceStudents = DB::table('attendance_data') ->selectRaw('DISTINCT student_id') - ->when(! empty($schoolYear), fn ($q) => $q->where('school_year', $schoolYear)) + ->when(!empty($schoolYear), fn ($q) => $q->where('school_year', $schoolYear)) ->orderBy('student_id') ->get(); @@ -126,7 +123,7 @@ class AttendanceTrackingService $studentIds = array_values(array_unique(array_filter($studentIds, fn ($v) => $v > 0))); $studentCodes = array_values(array_unique(array_filter($studentCodes, fn ($v) => $v !== ''))); - if (! empty($studentIds)) { + if (!empty($studentIds)) { $inactiveRows = $this->studentModel->query() ->select('id') ->whereIn('id', $studentIds) @@ -141,11 +138,11 @@ class AttendanceTrackingService $studentIds = array_values(array_filter( $studentIds, - static fn ($id) => ! isset($inactiveIdSet[$id]) + static fn ($id) => !isset($inactiveIdSet[$id]) )); } - if (! empty($studentCodes)) { + if (!empty($studentCodes)) { $inactiveCodeRows = $this->studentModel->query() ->select('school_id') ->whereIn('school_id', $studentCodes) @@ -160,7 +157,7 @@ class AttendanceTrackingService $studentCodes = array_values(array_filter( $studentCodes, - static fn ($code) => $code !== '' && ! isset($inactiveCodeSet[$code]) + static fn ($code) => $code !== '' && !isset($inactiveCodeSet[$code]) )); } @@ -177,7 +174,7 @@ class AttendanceTrackingService } $students = []; - if (! empty($studentIds)) { + if (!empty($studentIds)) { $students = $this->studentModel->query() ->whereIn('id', $studentIds) ->where('is_active', 1) @@ -185,7 +182,7 @@ class AttendanceTrackingService ->toArray(); } - if (! empty($studentCodes)) { + if (!empty($studentCodes)) { $byCode = $this->studentModel->query() ->whereIn('school_id', $studentCodes) ->where('is_active', 1) @@ -195,7 +192,7 @@ class AttendanceTrackingService $seen = array_flip(array_map(fn ($s) => (int) ($s['id'] ?? 0), $students)); foreach ($byCode as $s) { $sid = (int) ($s['id'] ?? 0); - if ($sid > 0 && ! isset($seen[$sid])) { + if ($sid > 0 && !isset($seen[$sid])) { $students[] = $s; $seen[$sid] = true; } @@ -211,7 +208,7 @@ class AttendanceTrackingService } foreach ($studentCodes as $code) { - if (! isset($knownCodes[$code])) { + if (!isset($knownCodes[$code])) { $virtualId = abs(crc32($code)) ?: random_int(1000000, 1999999); $students[] = [ 'id' => $virtualId, @@ -247,7 +244,7 @@ class AttendanceTrackingService foreach (array_merge($studentIds, $studentCodes) as $sidOrCode) { if (is_numeric($sidOrCode)) { $sid = (int) $sidOrCode; - if ($sid > 0 && ! isset($existingIds[$sid])) { + if ($sid > 0 && !isset($existingIds[$sid])) { $students[] = [ 'id' => $sid, 'school_id' => (string) $sid, @@ -260,7 +257,7 @@ class AttendanceTrackingService } } else { $code = (string) $sidOrCode; - if ($code !== '' && ! isset($studentCodeToId[$code])) { + if ($code !== '' && !isset($studentCodeToId[$code])) { $virtualId = abs(crc32($code)) ?: random_int(2000000, 2999999); $students[] = [ 'id' => $virtualId, @@ -298,14 +295,14 @@ class AttendanceTrackingService 'is_reported', 'is_notified', ]) - ->when(! empty($schoolYear), fn ($q) => $q->where('school_year', $schoolYear)) - ->when(! empty($semester), fn ($q) => $q->where('semester', $semester)) + ->when(!empty($schoolYear), fn ($q) => $q->where('school_year', $schoolYear)) + ->when(!empty($semester), fn ($q) => $q->where('semester', $semester)) ->where(function ($q) use ($studentIds, $studentCodes) { - if (! empty($studentIds)) { + if (!empty($studentIds)) { $q->whereIn('student_id', $studentIds); } - if (! empty($studentCodes)) { - if (! empty($studentIds)) { + if (!empty($studentCodes)) { + if (!empty($studentIds)) { $q->orWhereIn('student_id', $studentCodes); } else { $q->whereIn('student_id', $studentCodes); @@ -325,16 +322,16 @@ class AttendanceTrackingService if (empty($termRows)) { $latestAttendanceTerm = DB::table('attendance_data') ->select('school_year', 'semester', 'date') - ->when(! empty($studentIds), fn ($q) => $q->whereIn('student_id', $studentIds)) + ->when(!empty($studentIds), fn ($q) => $q->whereIn('student_id', $studentIds)) ->orderByDesc('date') ->first(); if ($latestAttendanceTerm) { - $schoolYear = ! empty($latestAttendanceTerm->school_year) + $schoolYear = !empty($latestAttendanceTerm->school_year) ? (string) $latestAttendanceTerm->school_year : $schoolYear; - $semester = ! empty($latestAttendanceTerm->semester) + $semester = !empty($latestAttendanceTerm->semester) ? (string) $latestAttendanceTerm->semester : $semester; @@ -357,14 +354,14 @@ class AttendanceTrackingService 'is_reported', 'is_notified', ]) - ->when(! empty($schoolYear), fn ($q) => $q->where('school_year', $schoolYear)) - ->when(! empty($semester), fn ($q) => $q->where('semester', $semester)) + ->when(!empty($schoolYear), fn ($q) => $q->where('school_year', $schoolYear)) + ->when(!empty($semester), fn ($q) => $q->where('semester', $semester)) ->where(function ($q) use ($studentIds, $studentCodes) { - if (! empty($studentIds)) { + if (!empty($studentIds)) { $q->whereIn('student_id', $studentIds); } - if (! empty($studentCodes)) { - if (! empty($studentIds)) { + if (!empty($studentCodes)) { + if (!empty($studentIds)) { $q->orWhereIn('student_id', $studentCodes); } else { $q->whereIn('student_id', $studentCodes); @@ -395,7 +392,7 @@ class AttendanceTrackingService $attendanceRows = []; foreach ($termRows as $row) { $dStr = $row['date'] ?? null; - if (! $dStr) { + if (!$dStr) { continue; } @@ -405,7 +402,7 @@ class AttendanceTrackingService continue; } - if ($d->lt($start) || ! $d->lt($endEx)) { + if ($d->lt($start) || !$d->lt($endEx)) { continue; } @@ -421,7 +418,7 @@ class AttendanceTrackingService if ($sid === null) { $sid = abs(crc32($code)) ?: random_int(3000000, 3999999); $studentCodeToId[$code] = $sid; - if (! isset($existingIds[$sid])) { + if (!isset($existingIds[$sid])) { $students[] = [ 'id' => $sid, 'school_id' => $code, @@ -446,7 +443,7 @@ class AttendanceTrackingService foreach ($attendanceRows as $arow) { $sid = (int) ($arow['student_id'] ?? 0); - if ($sid > 0 && ! isset($existingIds[$sid])) { + if ($sid > 0 && !isset($existingIds[$sid])) { $students[] = [ 'id' => $sid, 'school_id' => (string) ($arow['student_code'] ?? $arow['student_id']), @@ -498,13 +495,13 @@ class AttendanceTrackingService $parentEmail = $data['parent_email'] ?? ''; $parentName = $data['parent_name'] ?? ''; - if (! $parentEmail) { + if (!$parentEmail) { $parent = $this->getPrimaryParentForStudent($sid) ?? []; $parentEmail = $parent['email'] ?? ''; $parentName = $parent['parent_name'] ?? ''; } - if (! $parentEmail) { + if (!$parentEmail) { return [ 'success' => false, 'message' => 'No parent email found for this student.', @@ -555,7 +552,7 @@ class AttendanceTrackingService try { $this->attendanceMailerService->queueAttendanceEvent($payload); - if ($row && ! empty($row['id']) && method_exists($this->attendanceTrackingModel, 'markAsNotified')) { + if ($row && !empty($row['id']) && method_exists($this->attendanceTrackingModel, 'markAsNotified')) { $this->attendanceTrackingModel->markAsNotified((int) $row['id']); } @@ -565,7 +562,7 @@ class AttendanceTrackingService 'data' => $payload, ]; } catch (Throwable $e) { - Log::error('Attendance record queue failed: '.$e->getMessage()); + Log::error('Attendance record queue failed: ' . $e->getMessage()); return [ 'success' => false, @@ -585,7 +582,7 @@ class AttendanceTrackingService ?string $semesterFilter = null ): array { $student = $this->studentModel->query()->find($studentId); - if (! $student) { + if (!$student) { return [ 'success' => false, 'message' => 'Student not found', @@ -594,7 +591,7 @@ class AttendanceTrackingService } $student = $student->toArray(); - $student['name'] = trim(($student['firstname'] ?? '').' '.($student['lastname'] ?? '')); + $student['name'] = trim(($student['firstname'] ?? '') . ' ' . ($student['lastname'] ?? '')); $codeParam = strtoupper((string) ($codeParam ?? '')); $incidentDate = preg_match('/^\d{4}-\d{2}-\d{2}$/', (string) $incidentDate) ? (string) $incidentDate : ''; @@ -659,7 +656,7 @@ class AttendanceTrackingService ]) ->where(function ($q) use ($studentIdValues, $studentCodeValues) { $q->whereIn('student_id', $studentIdValues); - if (! empty($studentCodeValues)) { + if (!empty($studentCodeValues)) { $q->orWhereIn('student_id', $studentCodeValues); } }) @@ -677,7 +674,7 @@ class AttendanceTrackingService if ($pendingOnly) { $this->applyUnreportedAttendanceFilter($qb); - if (! $includeNotified) { + if (!$includeNotified) { $qb->whereRaw("(LOWER(TRIM(COALESCE(is_notified, ''))) NOT IN ('yes','1'))"); } } @@ -703,11 +700,11 @@ class AttendanceTrackingService 'semester' => $semester, ]; - if ($data['violation_date'] === '' && ! empty($data['attendance'][0]['date'])) { + if ($data['violation_date'] === '' && !empty($data['attendance'][0]['date'])) { $data['violation_date'] = substr((string) $data['attendance'][0]['date'], 0, 10); } - if (! empty($data['attendance'][0]['is_notified'])) { + if (!empty($data['attendance'][0]['is_notified'])) { $data['violation_notified'] = $data['attendance'][0]['is_notified']; } @@ -785,17 +782,15 @@ class AttendanceTrackingService $to = trim((string) ($v['parent_email'] ?? '')); $pname = (string) ($v['parent_name'] ?? ''); - if ($sid <= 0 || ! $code || ! $date || ! $to) { + if ($sid <= 0 || !$code || !$date || !$to) { $errors++; $details[] = ['student_id' => $sid, 'code' => $code, 'date' => $date, 'result' => 'missing data']; - continue; } if ($this->notificationAlreadySent($sid, $code, $date, 'email', $to)) { $skipped++; $details[] = ['student_id' => $sid, 'code' => $code, 'date' => $date, 'result' => 'duplicate']; - continue; } @@ -816,7 +811,7 @@ class AttendanceTrackingService $tpl = $this->renderTemplate($code, $variant, $context); - if (! $tpl) { + if (!$tpl) { $subject = match ($code) { 'ABS_1' => 'Attendance Notice: Unreported Absence', 'LATE_2' => 'Attendance Notice: Repeated Lateness', @@ -824,14 +819,14 @@ class AttendanceTrackingService }; $studentName = (string) ($v['name'] ?? ''); - $body = '

Dear '.($pname ?: 'Parent/Guardian').'

' - ."

We'd like to inform you about your student's recent attendance:

" - .'
    ' - ."
  • Student: {$studentName}
  • " - .'
  • Issue: '.(string) ($v['violation'] ?? '').'
  • ' - ."
  • As of: {$date}
  • " - .'
' - .'

If you have any questions, please contact the school office.

'; + $body = "

Dear " . ($pname ?: 'Parent/Guardian') . "

" + . "

We'd like to inform you about your student's recent attendance:

" + . "
    " + . "
  • Student: {$studentName}
  • " + . "
  • Issue: " . (string) ($v['violation'] ?? '') . "
  • " + . "
  • As of: {$date}
  • " + . "
" + . "

If you have any questions, please contact the school office.

"; } else { [$subject, $body] = $tpl; } @@ -854,7 +849,7 @@ class AttendanceTrackingService } catch (Throwable $e) { $errors++; $this->logNotification($sid, $code, $date, 'email', $to, $subject, 'failed', $e->getMessage()); - $details[] = ['student_id' => $sid, 'code' => $code, 'date' => $date, 'result' => 'error: '.$e->getMessage()]; + $details[] = ['student_id' => $sid, 'code' => $code, 'date' => $date, 'result' => 'error: ' . $e->getMessage()]; } try { @@ -880,7 +875,7 @@ class AttendanceTrackingService } } catch (Throwable $e) { $errors++; - $details[] = ['student_id' => $sid, 'code' => $code, 'date' => $date, 'result' => 'cc_error: '.$e->getMessage()]; + $details[] = ['student_id' => $sid, 'code' => $code, 'date' => $date, 'result' => 'cc_error: ' . $e->getMessage()]; } if ($anyOk) { @@ -900,10 +895,10 @@ class AttendanceTrackingService $absDates = array_map(fn ($d) => substr((string) $d, 0, 10), (array) ($v['absences'] ?? [])); $lateDates = array_map(fn ($d) => substr((string) $d, 0, 10), (array) ($v['lates'] ?? [])); - if (! empty($absDates)) { + if (!empty($absDates)) { $allDates = array_merge($allDates, $absDates); } - if (! empty($lateDates)) { + if (!empty($lateDates)) { $allDates = array_merge($allDates, $lateDates); } if (empty($allDates)) { @@ -1039,7 +1034,7 @@ class AttendanceTrackingService foreach ($rows as $row) { $sid = (int) ($row->student_id ?? 0); - if ($sid > 0 && ! isset($classByStudent[$sid])) { + if ($sid > 0 && !isset($classByStudent[$sid])) { $classByStudent[$sid] = [ 'class_section_id' => $row->class_section_id ?? null, 'class_section_name' => (string) ($row->class_section_name ?? ''), @@ -1054,7 +1049,7 @@ class AttendanceTrackingService $mergedData = []; foreach ($trackingData as $row) { $sid = (int) ($row['student_id'] ?? 0); - if (! isset($studentMap[$sid])) { + if (!isset($studentMap[$sid])) { continue; } @@ -1062,7 +1057,7 @@ class AttendanceTrackingService if (isset($classByStudent[$sid])) { $merged['class_section_name'] = (string) ($classByStudent[$sid]['class_section_name'] ?? ''); $merged['class_name'] = (string) ($classByStudent[$sid]['class_name'] ?? ''); - } elseif (! empty($merged['registration_grade'])) { + } elseif (!empty($merged['registration_grade'])) { $merged['grade'] = (string) $merged['registration_grade']; } @@ -1106,7 +1101,7 @@ class AttendanceTrackingService $violation = [ 'id' => $studentId, - 'name' => trim(($student['firstname'] ?? '').' '.($student['lastname'] ?? '')), + 'name' => trim(($student['firstname'] ?? '') . ' ' . ($student['lastname'] ?? '')), 'last_date' => $lastDate, 'violation' => $code, 'violation_code' => $code, @@ -1115,7 +1110,7 @@ class AttendanceTrackingService $ctx = $this->buildTemplateContext($violation, $parent); $rendered = $this->renderTemplate($code, $variant, $ctx); - if (! $rendered) { + if (!$rendered) { return [ 'success' => false, 'message' => "Template not found for {$code} ({$variant}).", @@ -1193,7 +1188,7 @@ class AttendanceTrackingService $sec = $this->getSecondaryParentForStudent($sid); $to2 = trim((string) ($sec['email'] ?? '')); if ($to2 !== '' && strcasecmp($to2, $to) !== 0) { - if (! $this->notificationAlreadySent($sid, $code, $ymd, 'email', $to2)) { + if (!$this->notificationAlreadySent($sid, $code, $ymd, 'email', $to2)) { $ok2 = $this->attendanceMailerService->send($to2, $subject, $wrapped); $this->logNotification( $sid, @@ -1211,7 +1206,7 @@ class AttendanceTrackingService } } - if (! $anyOk) { + if (!$anyOk) { return [ 'success' => false, 'message' => 'Failed to send email.', @@ -1256,7 +1251,7 @@ class AttendanceTrackingService return [ 'success' => false, - 'message' => 'Error: '.$e->getMessage(), + 'message' => 'Error: ' . $e->getMessage(), 'status' => 500, ]; } @@ -1278,7 +1273,7 @@ class AttendanceTrackingService ->where('id', $studentId) ->first(); - if (! $stu) { + if (!$stu) { return [ 'success' => false, 'message' => 'Student not found', @@ -1303,7 +1298,7 @@ class AttendanceTrackingService ] : null; $pb = DB::table('parents')->where('firstparent_id', $primaryId); - if (! empty($schoolYear)) { + if (!empty($schoolYear)) { $pb->where('school_year', $schoolYear); } @@ -1330,7 +1325,7 @@ class AttendanceTrackingService } } - if (! $secondary) { + if (!$secondary) { $fallbackEmail = (string) ($pRow->secondparent_email ?? ''); $fallbackPhone = (string) ($pRow->secondparent_phone ?? ''); $fallbackFirst = (string) ($pRow->secondparent_firstname ?? ''); @@ -1356,7 +1351,7 @@ class AttendanceTrackingService ], ]; } catch (Throwable $e) { - Log::error('parentsInfo() failed: '.$e->getMessage()); + Log::error('parentsInfo() failed: ' . $e->getMessage()); return [ 'success' => false, @@ -1384,11 +1379,11 @@ class AttendanceTrackingService ->where('school_year', $this->schoolYear) ->where('date', '>=', $start) ->where('date', '<', $end) - ->where('reason', 'like', '%'.$code.'%') + ->where('reason', 'like', '%' . $code . '%') ->orderByDesc('date') ->first(); - if (! $row) { + if (!$row) { $row = $this->attendanceTrackingModel->query() ->where('student_id', $sid) ->where('semester', $this->semester) @@ -1399,7 +1394,7 @@ class AttendanceTrackingService ->first(); } - if ($row && ! empty($row->id)) { + if ($row && !empty($row->id)) { DB::table($this->attendanceTrackingModel->getTable()) ->where('id', (int) $row->id) ->update([ @@ -1431,7 +1426,7 @@ class AttendanceTrackingService } catch (Throwable $e) { return [ 'success' => false, - 'message' => 'Failed to save note: '.$e->getMessage(), + 'message' => 'Failed to save note: ' . $e->getMessage(), 'status' => 500, ]; } @@ -1455,14 +1450,13 @@ class AttendanceTrackingService ->where('incident_date', $ymd) ->where('channel', $channel); - if (! empty($to)) { + if (!empty($to)) { $query->where('to_address', $to); } return $query->exists(); } catch (Throwable $e) { - Log::debug('notificationAlreadySent(): '.$e->getMessage()); - + Log::debug('notificationAlreadySent(): ' . $e->getMessage()); return false; } } @@ -1485,7 +1479,7 @@ class AttendanceTrackingService 'channel' => $channel, ]; - if (! empty($to)) { + if (!empty($to)) { $where['to_address'] = $to; } @@ -1494,7 +1488,7 @@ class AttendanceTrackingService ->orderByDesc('id') ->first(); - if ($existing && ! empty($existing->id)) { + if ($existing && !empty($existing->id)) { $existing->update([ 'subject' => $subject, 'status' => $status, @@ -1518,7 +1512,7 @@ class AttendanceTrackingService ]); } } catch (Throwable $e) { - Log::debug('logNotification(): '.$e->getMessage()); + Log::debug('logNotification(): ' . $e->getMessage()); } } @@ -1530,7 +1524,7 @@ class AttendanceTrackingService ->where('s.id', $studentId) ->first(); - if (! $row || empty($row->user_id)) { + if (!$row || empty($row->user_id)) { return null; } @@ -1550,7 +1544,7 @@ class AttendanceTrackingService ->where('id', $studentId) ->first(); - if (! $stu) { + if (!$stu) { return null; } @@ -1563,7 +1557,7 @@ class AttendanceTrackingService } $pRow = $pb->orderByDesc('updated_at')->first(); - if (! $pRow) { + if (!$pRow) { return null; } @@ -1574,11 +1568,11 @@ class AttendanceTrackingService ->where('id', $secondId) ->first(); - if ($u2 && ! empty($u2->email)) { + if ($u2 && !empty($u2->email)) { return [ 'user_id' => (int) $u2->id, 'email' => (string) $u2->email, - 'parent_name' => trim(($u2->firstname ?? '').' '.($u2->lastname ?? '')), + 'parent_name' => trim(($u2->firstname ?? '') . ' ' . ($u2->lastname ?? '')), 'phone' => (string) ($u2->cellphone ?? ''), ]; } @@ -1593,12 +1587,12 @@ class AttendanceTrackingService return [ 'user_id' => $secondId ?: null, 'email' => $fallbackEmail, - 'parent_name' => trim($fallbackFirst.' '.$fallbackLast) ?: null, + 'parent_name' => trim($fallbackFirst . ' ' . $fallbackLast) ?: null, 'phone' => $fallbackPhone, ]; } } catch (Throwable $e) { - Log::error('getSecondaryParentForStudent() failed: '.$e->getMessage()); + Log::error('getSecondaryParentForStudent() failed: ' . $e->getMessage()); } return null; @@ -1637,7 +1631,7 @@ class AttendanceTrackingService $ymd = substr((string) ($r['date'] ?? ''), 0, 10); $stat = strtolower(trim((string) ($r['status'] ?? ''))); - if (! in_array($stat, ['absent', 'late'], true)) { + if (!in_array($stat, ['absent', 'late'], true)) { continue; } @@ -1649,10 +1643,10 @@ class AttendanceTrackingService [$syStart, $syEnd] = $this->deriveSchoolYearBounds($schoolYear); $weekRowsSource = null; - if (! empty($weekRowsFallback)) { + if (!empty($weekRowsFallback)) { foreach ($weekRowsFallback as $r) { $st = strtolower(trim((string) ($r['status'] ?? ''))); - if ($st !== '' && ! in_array($st, ['absent', 'late'], true)) { + if ($st !== '' && !in_array($st, ['absent', 'late'], true)) { $weekRowsSource = $weekRowsFallback; break; } @@ -1685,7 +1679,6 @@ class AttendanceTrackingService } $out = array_values(array_unique($out)); rsort($out); - return $out; }; @@ -1696,7 +1689,7 @@ class AttendanceTrackingService $students ))); - if (! empty($studentIdsForClass)) { + if (!empty($studentIdsForClass)) { $rows = DB::table('student_class as sc') ->select([ 'sc.student_id', @@ -1714,7 +1707,7 @@ class AttendanceTrackingService foreach ($rows as $row) { $sid = (int) ($row->student_id ?? 0); - if ($sid > 0 && ! isset($classByStudent[$sid])) { + if ($sid > 0 && !isset($classByStudent[$sid])) { $classByStudent[$sid] = [ 'class_section_id' => $row->class_section_id ?? null, 'class_section_name' => (string) ($row->class_section_name ?? ''), @@ -1725,7 +1718,7 @@ class AttendanceTrackingService } } } catch (Throwable $e) { - Log::debug('computeViolations(): class prefetch failed: '.$e->getMessage()); + Log::debug('computeViolations(): class prefetch failed: ' . $e->getMessage()); } $out = []; @@ -1734,7 +1727,7 @@ class AttendanceTrackingService foreach ($students as $stu) { $sid = (int) ($stu['id'] ?? 0); - $name = trim(($stu['firstname'] ?? '').' '.($stu['lastname'] ?? '')); + $name = trim(($stu['firstname'] ?? '') . ' ' . ($stu['lastname'] ?? '')); $absDatesAll = array_values(array_unique($byStudent[$sid]['absent'] ?? [])); $lateDatesAll = array_values(array_unique($byStudent[$sid]['late'] ?? [])); @@ -1752,7 +1745,7 @@ class AttendanceTrackingService $lateCountLast5 += count($lateDates); $absenceViolation = null; - if (! empty($absDates)) { + if (!empty($absDates)) { $lastAbsDate = $absDates[0]; $A2row = $this->hasNConsecutiveItems($absDates, 2, 7); $A3row = $this->hasNConsecutiveItems($absDates, 3, 7); @@ -1799,7 +1792,7 @@ class AttendanceTrackingService } $lateViolation = null; - if (! empty($lateDates)) { + if (!empty($lateDates)) { $lastLateDate = $lateDates[0]; $L2row = $this->hasNConsecutiveItems($lateDates, 2, 7); @@ -1814,7 +1807,7 @@ class AttendanceTrackingService $lateWeeksSet = array_flip($lateWeekIdx); $lateCoversAllWindowWeeks = true; for ($i = $windowStartIdx; $i <= $currentWeekIdx; $i++) { - if (! isset($lateWeeksSet[$i])) { + if (!isset($lateWeeksSet[$i])) { $lateCoversAllWindowWeeks = false; break; } @@ -1865,7 +1858,7 @@ class AttendanceTrackingService $chosen = $this->chooseHigherSeverity($absenceViolation, $lateViolation); - if (! $chosen && count($absDates) === 1) { + if (!$chosen && count($absDates) === 1) { $chosen = [ 'type' => 'absence', 'code' => 'ABS_1', @@ -1878,7 +1871,7 @@ class AttendanceTrackingService ]; } - if (! $chosen) { + if (!$chosen) { continue; } @@ -1890,7 +1883,7 @@ class AttendanceTrackingService ->where('school_year', $schoolYear) ->where('date', '>=', $start) ->where('date', '<', $end) - ->where('reason', 'like', '%'.$chosen['code'].'%'); + ->where('reason', 'like', '%' . $chosen['code'] . '%'); if ($semester !== null) { $trackingQ->where('semester', $semester); @@ -1904,7 +1897,7 @@ class AttendanceTrackingService $cls = $classByStudent[$sid] ?? []; $classLabel = (string) ($cls['class_section_name'] ?? ''); - if ($classLabel === '' && ! empty($cls['class_name'])) { + if ($classLabel === '' && !empty($cls['class_name'])) { $classLabel = (string) $cls['class_name']; } @@ -1954,7 +1947,6 @@ class AttendanceTrackingService if ($a && $b) { if ($a['severity'] === $b['severity']) { $prio = ['expel_notify' => 3, 'team_notify' => 2, 'auto_email' => 1]; - return ($prio[$a['action']] ?? 0) >= ($prio[$b['action']] ?? 0) ? $a : $b; } @@ -1984,11 +1976,11 @@ class AttendanceTrackingService { $cols = $this->attendanceReportedColumns(); - if (! empty($cols['is_reported'])) { + if (!empty($cols['is_reported'])) { $qb->whereRaw("(LOWER(TRIM(COALESCE(is_reported, ''))) NOT IN ('yes','1','true','y','on'))"); } - if (! empty($cols['reported'])) { + if (!empty($cols['reported'])) { $qb->whereRaw("(LOWER(TRIM(COALESCE(reported, ''))) NOT IN ('yes','1','true','y','on'))"); } @@ -2010,10 +2002,9 @@ class AttendanceTrackingService protected function dayBounds(string $ymd): array { $d = substr($ymd, 0, 10); - return [ - $d.' 00:00:00', - date('Y-m-d', strtotime($d.' +1 day')).' 00:00:00', + $d . ' 00:00:00', + date('Y-m-d', strtotime($d . ' +1 day')) . ' 00:00:00', ]; } @@ -2033,10 +2024,9 @@ class AttendanceTrackingService protected function deriveSchoolYearBounds(string $schoolYear): array { - if (! preg_match('/^(\d{4})-(\d{4})$/', $schoolYear, $m)) { + if (!preg_match('/^(\d{4})-(\d{4})$/', $schoolYear, $m)) { $y = (int) date('Y'); $startY = (int) (date('n') >= 8 ? $y : $y - 1); - return [sprintf('%d-08-01', $startY), sprintf('%d-07-31', $startY + 1)]; } @@ -2076,7 +2066,7 @@ class AttendanceTrackingService ->where('is_active', 1) ->first(); - if (! $row) { + if (!$row) { $row = $this->attendanceEmailTemplateModel->query() ->where('code', $code) ->where('variant', 'default') @@ -2087,7 +2077,7 @@ class AttendanceTrackingService $row = $row?->toArray(); } - if (! $row) { + if (!$row) { return null; } @@ -2145,10 +2135,10 @@ class AttendanceTrackingService $semester = ($semester !== null && $semester !== '') ? (string) $semester : null; $weeks = []; - if (! empty($weekRowsFallback)) { + if (!empty($weekRowsFallback)) { foreach ($weekRowsFallback as $r) { $d = $r['date'] ?? null; - if (! $d) { + if (!$d) { continue; } $ts = strtotime($d); @@ -2205,38 +2195,25 @@ class AttendanceTrackingService } $idx = array_keys($set); sort($idx); - return $idx; } protected function windowWeeksForViolationCode(string $code): int { $code = strtoupper(trim($code)); - if ($code === '') { - return 5; - } - if (Str::startsWith($code, 'ABS_2') || Str::startsWith($code, 'LATE_2')) { - return 3; - } - if (Str::startsWith($code, 'ABS_3') || Str::startsWith($code, 'LATE_3') || Str::startsWith($code, 'MIX')) { - return 4; - } - if (Str::startsWith($code, 'LATE_4')) { - return 4; - } - if (Str::startsWith($code, 'ABS_4')) { - return 5; - } - + if ($code === '') return 5; + if (Str::startsWith($code, 'ABS_2') || Str::startsWith($code, 'LATE_2')) return 3; + if (Str::startsWith($code, 'ABS_3') || Str::startsWith($code, 'LATE_3') || Str::startsWith($code, 'MIX')) return 4; + if (Str::startsWith($code, 'LATE_4')) return 4; + if (Str::startsWith($code, 'ABS_4')) return 5; return 5; } protected function isoWeekStartYmd(string $weekKey): string { if (preg_match('/^(\d{4})-W(\d{2})$/', $weekKey, $m)) { - return date('Y-m-d', strtotime($m[1].'-W'.$m[2].'-1')); + return date('Y-m-d', strtotime($m[1] . '-W' . $m[2] . '-1')); } - return date('Y-m-d'); } @@ -2257,15 +2234,14 @@ class AttendanceTrackingService if (empty($activeWeekKeys)) { $days = ($windowWeeks * 7) - 1; - - return date('Y-m-d', strtotime($endBound.' -'.$days.' days')); + return date('Y-m-d', strtotime($endBound . ' -' . $days . ' days')); } $activeWeekKeys = array_values($activeWeekKeys); $weekIndex = array_flip($activeWeekKeys); $endWeekKey = date('o-\WW', strtotime($endBound)); - if (! isset($weekIndex[$endWeekKey])) { + if (!isset($weekIndex[$endWeekKey])) { $endWeekKey = null; for ($i = count($activeWeekKeys) - 1; $i >= 0; $i--) { $wkStart = $this->isoWeekStartYmd($activeWeekKeys[$i]); @@ -2289,9 +2265,7 @@ class AttendanceTrackingService protected function hasNConsecutiveItems(array $datesDesc, int $n, int $daysApart): bool { $N = count($datesDesc); - if ($N < $n) { - return false; - } + if ($N < $n) return false; $dates = $datesDesc; sort($dates); @@ -2304,9 +2278,7 @@ class AttendanceTrackingService if ($diff === $daysApart) { $run++; - if ($run >= $n) { - return true; - } + if ($run >= $n) return true; } else { $run = 1; } @@ -2318,9 +2290,7 @@ class AttendanceTrackingService protected function hasNInWActiveWeeks(array $weekIdx, int $n, int $w): bool { $N = count($weekIdx); - if ($N < $n) { - return false; - } + if ($N < $n) return false; $l = 0; for ($r = 0; $r < $N; $r++) { @@ -2337,9 +2307,7 @@ class AttendanceTrackingService protected function twoLatesOneAbsInWWeeks(array $lateIdx, array $absIdx, int $w): bool { - if (count($lateIdx) < 2 || count($absIdx) < 1) { - return false; - } + if (count($lateIdx) < 2 || count($absIdx) < 1) return false; $L = $lateIdx; $A = $absIdx; @@ -2354,7 +2322,6 @@ class AttendanceTrackingService if ($L[$iL2] > $windowEnd) { $iL1++; $iL2 = $iL1 + 1; - continue; } @@ -2375,4 +2342,4 @@ class AttendanceTrackingService return false; } -} +} \ No newline at end of file diff --git a/tests/Unit/Services/Attendance/SemesterRangeServiceTest.php b/tests/Unit/Services/Attendance/SemesterRangeServiceTest.php index 1e4ffa4e..e1975112 100644 --- a/tests/Unit/Services/Attendance/SemesterRangeServiceTest.php +++ b/tests/Unit/Services/Attendance/SemesterRangeServiceTest.php @@ -9,7 +9,7 @@ class SemesterRangeServiceTest extends TestCase { public function test_normalize_semester(): void { - $service = new SemesterRangeService; + $service = new SemesterRangeService(); $this->assertSame('Fall', $service->normalizeSemester('fall')); $this->assertSame('Spring', $service->normalizeSemester('SPRING')); @@ -18,7 +18,7 @@ class SemesterRangeServiceTest extends TestCase public function test_get_school_year_range(): void { - $service = new SemesterRangeService; + $service = new SemesterRangeService(); [$start, $end] = $service->getSchoolYearRange('2025-2026'); @@ -28,7 +28,7 @@ class SemesterRangeServiceTest extends TestCase public function test_build_sunday_list(): void { - $service = new SemesterRangeService; + $service = new SemesterRangeService(); $sundays = $service->buildSundayList('2025-09-01', '2025-09-30'); @@ -37,4 +37,4 @@ class SemesterRangeServiceTest extends TestCase $this->assertContains('2025-09-21', $sundays); $this->assertContains('2025-09-28', $sundays); } -} +} \ No newline at end of file diff --git a/tests/Unit/Services/Attendance/StaffAttendanceServiceTest.php b/tests/Unit/Services/Attendance/StaffAttendanceServiceTest.php index 8a1f1fd2..a0340358 100644 --- a/tests/Unit/Services/Attendance/StaffAttendanceServiceTest.php +++ b/tests/Unit/Services/Attendance/StaffAttendanceServiceTest.php @@ -2,11 +2,6 @@ namespace Tests\Unit\Services\Attendance; -use App\Models\ClassSection; -use App\Models\Configuration; -use App\Models\StaffAttendance; -use App\Models\TeacherClass; -use App\Services\Attendance\SemesterRangeService; use App\Services\Attendance\StaffAttendanceService; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\DB; @@ -80,11 +75,11 @@ class StaffAttendanceServiceTest extends TestCase ]); $service = new StaffAttendanceService( - new Configuration, - new StaffAttendance, - new TeacherClass, - new ClassSection, - new SemesterRangeService + new \App\Models\Configuration(), + new \App\Models\StaffAttendance(), + new \App\Models\TeacherClass(), + new \App\Models\ClassSection(), + new \App\Services\Attendance\SemesterRangeService() ); $payload = $service->monthData('Fall', '2025-2026'); diff --git a/tests/Unit/Services/Attendance/StudentAttendanceWriterServiceTest.php b/tests/Unit/Services/Attendance/StudentAttendanceWriterServiceTest.php index f0f87877..55012fea 100644 --- a/tests/Unit/Services/Attendance/StudentAttendanceWriterServiceTest.php +++ b/tests/Unit/Services/Attendance/StudentAttendanceWriterServiceTest.php @@ -2,8 +2,6 @@ namespace Tests\Unit\Services\Attendance; -use App\Models\AttendanceData; -use App\Models\Student; use App\Services\Attendance\AttendanceRecordSyncService; use App\Services\Attendance\StudentAttendanceWriterService; use Illuminate\Foundation\Testing\RefreshDatabase; @@ -31,7 +29,7 @@ class StudentAttendanceWriterServiceTest extends TestCase ]); $sync = Mockery::mock(AttendanceRecordSyncService::class); - $service = new StudentAttendanceWriterService(new AttendanceData, new Student, $sync); + $service = new StudentAttendanceWriterService(new \App\Models\AttendanceData(), new \App\Models\Student(), $sync); $this->assertSame('S1', $service->resolveStudentSchoolId(1, null)); } @@ -67,7 +65,7 @@ class StudentAttendanceWriterServiceTest extends TestCase $sync = Mockery::mock(AttendanceRecordSyncService::class); $sync->shouldReceive('updateAttendanceRecord')->once(); - $service = new StudentAttendanceWriterService(new AttendanceData, new Student, $sync); + $service = new StudentAttendanceWriterService(new \App\Models\AttendanceData(), new \App\Models\Student(), $sync); $service->saveOrUpdateStudentAttendance([ 'class_section_id' => 1, diff --git a/tests/Unit/Services/Attendance/TeacherAttendanceSubmissionServiceTest.php b/tests/Unit/Services/Attendance/TeacherAttendanceSubmissionServiceTest.php index 954addea..3aa4ece3 100644 --- a/tests/Unit/Services/Attendance/TeacherAttendanceSubmissionServiceTest.php +++ b/tests/Unit/Services/Attendance/TeacherAttendanceSubmissionServiceTest.php @@ -2,16 +2,7 @@ namespace Tests\Unit\Services\Attendance; -use App\Models\AttendanceData; -use App\Models\AttendanceDay; -use App\Models\AttendanceRecord; -use App\Models\Student; -use App\Models\TeacherClass; use App\Models\User; -use App\Services\Attendance\AttendanceAutoPublishService; -use App\Services\Attendance\AttendancePolicyService; -use App\Services\Attendance\AttendanceRecordSyncService; -use App\Services\Attendance\StudentAttendanceWriterService; use App\Services\Attendance\TeacherAttendanceSubmissionService; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\DB; @@ -35,21 +26,21 @@ class TeacherAttendanceSubmissionServiceTest extends TestCase $user = $this->seedUser(); - $policy = Mockery::mock(AttendancePolicyService::class); + $policy = Mockery::mock(\App\Services\Attendance\AttendancePolicyService::class); $policy->shouldReceive('canEditDay')->andReturn(true); $service = new TeacherAttendanceSubmissionService( - new AttendanceDay, - new AttendanceData, - new TeacherClass, - new Student, + new \App\Models\AttendanceDay(), + new \App\Models\AttendanceData(), + new \App\Models\TeacherClass(), + new \App\Models\Student(), $policy, - new StudentAttendanceWriterService( - new AttendanceData, - new Student, - new AttendanceRecordSyncService(new AttendanceRecord) + new \App\Services\Attendance\StudentAttendanceWriterService( + new \App\Models\AttendanceData(), + new \App\Models\Student(), + new \App\Services\Attendance\AttendanceRecordSyncService(new \App\Models\AttendanceRecord()) ), - new AttendanceAutoPublishService + new \App\Services\Attendance\AttendanceAutoPublishService() ); $this->expectException(\RuntimeException::class); diff --git a/tests/Unit/Services/AttendanceManagement/AttendanceManagementServiceTest.php b/tests/Unit/Services/AttendanceManagement/AttendanceManagementServiceTest.php index 7e65e625..5b27c34c 100644 --- a/tests/Unit/Services/AttendanceManagement/AttendanceManagementServiceTest.php +++ b/tests/Unit/Services/AttendanceManagement/AttendanceManagementServiceTest.php @@ -16,7 +16,7 @@ class AttendanceManagementServiceTest extends TestCase $this->seedConfiguration(); $this->seedUserWithBadge('RFID-100'); - $service = new AttendanceManagementService; + $service = new AttendanceManagementService(); $row = $service->badgeScan([ 'badge_scan' => 'RFID-100', @@ -46,7 +46,7 @@ class AttendanceManagementServiceTest extends TestCase 'updated_at' => now(), ]); - $service = new AttendanceManagementService; + $service = new AttendanceManagementService(); $data = $service->dashboard([ 'date' => '2026-02-10', diff --git a/tests/Unit/Services/AttendanceTracking/AttendanceCaseQueryServiceTest.php b/tests/Unit/Services/AttendanceTracking/AttendanceCaseQueryServiceTest.php index d8dfaaa7..ec522757 100644 --- a/tests/Unit/Services/AttendanceTracking/AttendanceCaseQueryServiceTest.php +++ b/tests/Unit/Services/AttendanceTracking/AttendanceCaseQueryServiceTest.php @@ -2,12 +2,7 @@ namespace Tests\Unit\Services\AttendanceTracking; -use App\Models\AttendanceData; -use App\Models\AttendanceTracking; -use App\Models\Student; use App\Services\AttendanceTracking\AttendanceCaseQueryService; -use App\Services\AttendanceTracking\AttendanceParentLookupService; -use App\Services\AttendanceTracking\ViolationRuleEngineService; use Illuminate\Foundation\Testing\RefreshDatabase; use Mockery; use Tests\TestCase; @@ -19,11 +14,11 @@ class AttendanceCaseQueryServiceTest extends TestCase public function test_returns_not_found_when_student_missing(): void { $service = new AttendanceCaseQueryService( - new Student, - new AttendanceData, - new AttendanceTracking, - Mockery::mock(ViolationRuleEngineService::class), - Mockery::mock(AttendanceParentLookupService::class) + new \App\Models\Student(), + new \App\Models\AttendanceData(), + new \App\Models\AttendanceTracking(), + Mockery::mock(\App\Services\AttendanceTracking\ViolationRuleEngineService::class), + Mockery::mock(\App\Services\AttendanceTracking\AttendanceParentLookupService::class) ); $result = $service->getStudentCase(999); diff --git a/tests/Unit/Services/AttendanceTracking/AttendanceCommunicationSupportServiceTest.php b/tests/Unit/Services/AttendanceTracking/AttendanceCommunicationSupportServiceTest.php index 0196b102..bcf20b31 100644 --- a/tests/Unit/Services/AttendanceTracking/AttendanceCommunicationSupportServiceTest.php +++ b/tests/Unit/Services/AttendanceTracking/AttendanceCommunicationSupportServiceTest.php @@ -2,12 +2,7 @@ namespace Tests\Unit\Services\AttendanceTracking; -use App\Models\AttendanceData; -use App\Models\Configuration; -use App\Models\Student; use App\Services\AttendanceTracking\AttendanceCommunicationSupportService; -use App\Services\AttendanceTracking\AttendanceEmailComposerService; -use App\Services\AttendanceTracking\AttendanceParentLookupService; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\DB; use Mockery; @@ -54,13 +49,13 @@ class AttendanceCommunicationSupportServiceTest extends TestCase private function makeService(): AttendanceCommunicationSupportService { - $parentLookup = Mockery::mock(AttendanceParentLookupService::class); - $emailComposer = Mockery::mock(AttendanceEmailComposerService::class); + $parentLookup = Mockery::mock(\App\Services\AttendanceTracking\AttendanceParentLookupService::class); + $emailComposer = Mockery::mock(\App\Services\AttendanceTracking\AttendanceEmailComposerService::class); return new AttendanceCommunicationSupportService( - new Student, - new AttendanceData, - new Configuration, + new \App\Models\Student(), + new \App\Models\AttendanceData(), + new \App\Models\Configuration(), $parentLookup, $emailComposer ); diff --git a/tests/Unit/Services/AttendanceTracking/AttendanceEmailComposerServiceTest.php b/tests/Unit/Services/AttendanceTracking/AttendanceEmailComposerServiceTest.php index 7c32587c..ca5eb145 100644 --- a/tests/Unit/Services/AttendanceTracking/AttendanceEmailComposerServiceTest.php +++ b/tests/Unit/Services/AttendanceTracking/AttendanceEmailComposerServiceTest.php @@ -2,7 +2,6 @@ namespace Tests\Unit\Services\AttendanceTracking; -use App\Models\AttendanceEmailTemplate; use App\Services\AttendanceTracking\AttendanceEmailComposerService; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\DB; @@ -22,7 +21,7 @@ class AttendanceEmailComposerServiceTest extends TestCase 'is_active' => 1, ]); - $service = new AttendanceEmailComposerService(new AttendanceEmailTemplate); + $service = new AttendanceEmailComposerService(new \App\Models\AttendanceEmailTemplate()); $context = ['{{student_name}}' => 'Student A', '{{parent_name}}' => 'Parent A']; $rendered = $service->renderTemplate('ABS_1', 'default', $context); @@ -32,7 +31,7 @@ class AttendanceEmailComposerServiceTest extends TestCase public function test_pick_variant_fallback(): void { - $service = new AttendanceEmailComposerService(new AttendanceEmailTemplate); + $service = new AttendanceEmailComposerService(new \App\Models\AttendanceEmailTemplate()); $this->assertSame('no_answer', $service->pickVariant('ABS_2')); $this->assertSame('default', $service->pickVariant('ABS_1')); } diff --git a/tests/Unit/Services/AttendanceTracking/AttendanceNotificationLogServiceTest.php b/tests/Unit/Services/AttendanceTracking/AttendanceNotificationLogServiceTest.php index f9428cfb..5d9b0047 100644 --- a/tests/Unit/Services/AttendanceTracking/AttendanceNotificationLogServiceTest.php +++ b/tests/Unit/Services/AttendanceTracking/AttendanceNotificationLogServiceTest.php @@ -2,7 +2,6 @@ namespace Tests\Unit\Services\AttendanceTracking; -use App\Models\ParentNotification; use App\Services\AttendanceTracking\AttendanceNotificationLogService; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\DB; @@ -14,7 +13,7 @@ class AttendanceNotificationLogServiceTest extends TestCase public function test_log_notification_creates_and_updates(): void { - $service = new AttendanceNotificationLogService(new ParentNotification); + $service = new AttendanceNotificationLogService(new \App\Models\ParentNotification()); $service->logNotification(1, 'ABS_1', '2025-01-01', 'email', 'parent@example.com', 'Subject', 'sent'); $this->assertDatabaseHas('parent_notifications', [ diff --git a/tests/Unit/Services/AttendanceTracking/AttendanceNotificationWorkflowServiceTest.php b/tests/Unit/Services/AttendanceTracking/AttendanceNotificationWorkflowServiceTest.php index 8630fd50..e7c57ffc 100644 --- a/tests/Unit/Services/AttendanceTracking/AttendanceNotificationWorkflowServiceTest.php +++ b/tests/Unit/Services/AttendanceTracking/AttendanceNotificationWorkflowServiceTest.php @@ -2,17 +2,7 @@ namespace Tests\Unit\Services\AttendanceTracking; -use App\Models\AttendanceData; -use App\Models\AttendanceTracking; -use App\Models\Configuration; -use App\Models\Student; -use App\Models\StudentClass; -use App\Services\AttendanceTracking\AttendanceEmailComposerService; -use App\Services\AttendanceTracking\AttendanceMailerService; -use App\Services\AttendanceTracking\AttendanceNotificationLogService; use App\Services\AttendanceTracking\AttendanceNotificationWorkflowService; -use App\Services\AttendanceTracking\AttendanceParentLookupService; -use App\Services\AttendanceTracking\ViolationRuleEngineService; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\DB; use Mockery; @@ -37,20 +27,20 @@ class AttendanceNotificationWorkflowServiceTest extends TestCase 'school_year' => '2025-2026', ]); - $parentLookup = Mockery::mock(AttendanceParentLookupService::class); + $parentLookup = Mockery::mock(\App\Services\AttendanceTracking\AttendanceParentLookupService::class); $parentLookup->shouldReceive('getPrimaryParentForStudent')->once()->andReturn([]); $service = new AttendanceNotificationWorkflowService( - new Student, - new StudentClass, - new AttendanceData, - new AttendanceTracking, - new Configuration, - Mockery::mock(AttendanceMailerService::class), - Mockery::mock(ViolationRuleEngineService::class), + new \App\Models\Student(), + new \App\Models\StudentClass(), + new \App\Models\AttendanceData(), + new \App\Models\AttendanceTracking(), + new \App\Models\Configuration(), + Mockery::mock(\App\Services\AttendanceTracking\AttendanceMailerService::class), + Mockery::mock(\App\Services\AttendanceTracking\ViolationRuleEngineService::class), $parentLookup, - Mockery::mock(AttendanceEmailComposerService::class), - Mockery::mock(AttendanceNotificationLogService::class) + Mockery::mock(\App\Services\AttendanceTracking\AttendanceEmailComposerService::class), + Mockery::mock(\App\Services\AttendanceTracking\AttendanceNotificationLogService::class) ); $result = $service->record([ diff --git a/tests/Unit/Services/AttendanceTracking/AttendanceParentLookupServiceTest.php b/tests/Unit/Services/AttendanceTracking/AttendanceParentLookupServiceTest.php index cc7db384..30f585d1 100644 --- a/tests/Unit/Services/AttendanceTracking/AttendanceParentLookupServiceTest.php +++ b/tests/Unit/Services/AttendanceTracking/AttendanceParentLookupServiceTest.php @@ -47,7 +47,7 @@ class AttendanceParentLookupServiceTest extends TestCase 'school_year' => '2025-2026', ]); - $service = new AttendanceParentLookupService; + $service = new AttendanceParentLookupService(); $parent = $service->getPrimaryParentForStudent(1); $this->assertSame('parent@example.com', $parent['email']); @@ -80,7 +80,7 @@ class AttendanceParentLookupServiceTest extends TestCase 'school_year' => '2025-2026', ]); - $service = new AttendanceParentLookupService; + $service = new AttendanceParentLookupService(); $parent = $service->getSecondaryParentForStudent(1, '2025-2026'); $this->assertSame('second@example.com', $parent['email']); diff --git a/tests/Unit/Services/AttendanceTracking/AttendancePendingViolationServiceTest.php b/tests/Unit/Services/AttendanceTracking/AttendancePendingViolationServiceTest.php index ca7aefad..48cab1a1 100644 --- a/tests/Unit/Services/AttendanceTracking/AttendancePendingViolationServiceTest.php +++ b/tests/Unit/Services/AttendanceTracking/AttendancePendingViolationServiceTest.php @@ -2,10 +2,7 @@ namespace Tests\Unit\Services\AttendanceTracking; -use App\Models\AttendanceData; use App\Services\AttendanceTracking\AttendancePendingViolationService; -use App\Services\AttendanceTracking\AttendanceViolationStudentResolverService; -use App\Services\AttendanceTracking\ViolationRuleEngineService; use Illuminate\Foundation\Testing\RefreshDatabase; use Mockery; use Tests\TestCase; @@ -16,7 +13,7 @@ class AttendancePendingViolationServiceTest extends TestCase public function test_returns_error_when_no_students(): void { - $resolver = Mockery::mock(AttendanceViolationStudentResolverService::class); + $resolver = Mockery::mock(\App\Services\AttendanceTracking\AttendanceViolationStudentResolverService::class); $resolver->shouldReceive('resolveForSchoolYear')->andReturn([ 'school_year' => '2025-2026', 'semester' => 'Fall', @@ -28,10 +25,10 @@ class AttendancePendingViolationServiceTest extends TestCase 'debug' => [], ]); - $engine = Mockery::mock(ViolationRuleEngineService::class); + $engine = Mockery::mock(\App\Services\AttendanceTracking\ViolationRuleEngineService::class); $service = new AttendancePendingViolationService( - new AttendanceData, + new \App\Models\AttendanceData(), $engine, $resolver ); diff --git a/tests/Unit/Services/AttendanceTracking/AttendanceTrackingServiceTest.php b/tests/Unit/Services/AttendanceTracking/AttendanceTrackingServiceTest.php index fbe39d3f..6341d5ec 100644 --- a/tests/Unit/Services/AttendanceTracking/AttendanceTrackingServiceTest.php +++ b/tests/Unit/Services/AttendanceTracking/AttendanceTrackingServiceTest.php @@ -12,7 +12,7 @@ class AttendanceTrackingServiceTest extends TestCase public function test_service_instantiates(): void { - $service = new AttendanceTrackingService; + $service = new AttendanceTrackingService(); $this->assertInstanceOf(AttendanceTrackingService::class, $service); } } diff --git a/tests/Unit/Services/AttendanceTracking/AttendanceViolationStudentResolverServiceTest.php b/tests/Unit/Services/AttendanceTracking/AttendanceViolationStudentResolverServiceTest.php index d510e452..5a8bd45b 100644 --- a/tests/Unit/Services/AttendanceTracking/AttendanceViolationStudentResolverServiceTest.php +++ b/tests/Unit/Services/AttendanceTracking/AttendanceViolationStudentResolverServiceTest.php @@ -2,9 +2,6 @@ namespace Tests\Unit\Services\AttendanceTracking; -use App\Models\AttendanceData; -use App\Models\Student; -use App\Models\StudentClass; use App\Services\AttendanceTracking\AttendanceViolationStudentResolverService; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\DB; @@ -37,9 +34,9 @@ class AttendanceViolationStudentResolverServiceTest extends TestCase ]); $service = new AttendanceViolationStudentResolverService( - new Student, - new StudentClass, - new AttendanceData + new \App\Models\Student(), + new \App\Models\StudentClass(), + new \App\Models\AttendanceData() ); $result = $service->resolveForSchoolYear('2025-2026', 'Fall'); diff --git a/tests/Unit/Services/AttendanceTracking/ViolationRuleEngineServiceTest.php b/tests/Unit/Services/AttendanceTracking/ViolationRuleEngineServiceTest.php index 7d034c5c..74387c4c 100644 --- a/tests/Unit/Services/AttendanceTracking/ViolationRuleEngineServiceTest.php +++ b/tests/Unit/Services/AttendanceTracking/ViolationRuleEngineServiceTest.php @@ -2,8 +2,6 @@ namespace Tests\Unit\Services\AttendanceTracking; -use App\Models\AttendanceTracking; -use App\Services\AttendanceTracking\AttendanceParentLookupService; use App\Services\AttendanceTracking\ViolationRuleEngineService; use Illuminate\Foundation\Testing\RefreshDatabase; use Mockery; @@ -32,8 +30,8 @@ class ViolationRuleEngineServiceTest extends TestCase private function makeService(): ViolationRuleEngineService { - $tracking = new AttendanceTracking; - $parentLookup = Mockery::mock(AttendanceParentLookupService::class); + $tracking = new \App\Models\AttendanceTracking(); + $parentLookup = Mockery::mock(\App\Services\AttendanceTracking\AttendanceParentLookupService::class); return new ViolationRuleEngineService($tracking, $parentLookup); } diff --git a/tests/Unit/Services/Auth/PasswordResetCleanupServiceTest.php b/tests/Unit/Services/Auth/PasswordResetCleanupServiceTest.php index a20e708e..c9df6b83 100644 --- a/tests/Unit/Services/Auth/PasswordResetCleanupServiceTest.php +++ b/tests/Unit/Services/Auth/PasswordResetCleanupServiceTest.php @@ -18,7 +18,7 @@ class PasswordResetCleanupServiceTest extends TestCase ['ip_address' => '127.0.0.1', 'requested_at' => now()->subDays(5)], ]); - $service = new PasswordResetCleanupService; + $service = new PasswordResetCleanupService(); $deleted = $service->cleanup(30); $this->assertSame(1, $deleted); diff --git a/tests/Unit/Services/Auth/PermissionCheckServiceTest.php b/tests/Unit/Services/Auth/PermissionCheckServiceTest.php index 438bf4ed..00a76049 100644 --- a/tests/Unit/Services/Auth/PermissionCheckServiceTest.php +++ b/tests/Unit/Services/Auth/PermissionCheckServiceTest.php @@ -34,7 +34,7 @@ class PermissionCheckServiceTest extends TestCase 'can_read' => 1, ]); - $service = new PermissionCheckService; + $service = new PermissionCheckService(); $this->assertTrue($service->hasPermission(10, 'edit_students')); $this->assertFalse($service->hasPermission(10, 'missing_perm')); diff --git a/tests/Unit/Services/Auth/RegistrationCaptchaServiceTest.php b/tests/Unit/Services/Auth/RegistrationCaptchaServiceTest.php index 94aa6e07..6292655a 100644 --- a/tests/Unit/Services/Auth/RegistrationCaptchaServiceTest.php +++ b/tests/Unit/Services/Auth/RegistrationCaptchaServiceTest.php @@ -12,7 +12,7 @@ class RegistrationCaptchaServiceTest extends TestCase public function test_generate_sets_cache_value(): void { - $service = new RegistrationCaptchaService; + $service = new RegistrationCaptchaService(); $value = $service->generate(6); $this->assertNotEmpty($value); @@ -21,7 +21,7 @@ class RegistrationCaptchaServiceTest extends TestCase public function test_verify_and_clear(): void { - $service = new RegistrationCaptchaService; + $service = new RegistrationCaptchaService(); $value = $service->generate(6); $this->assertTrue($service->verify($value)); diff --git a/tests/Unit/Services/Auth/RegistrationFormatterServiceTest.php b/tests/Unit/Services/Auth/RegistrationFormatterServiceTest.php index 50b202ff..b26561c8 100644 --- a/tests/Unit/Services/Auth/RegistrationFormatterServiceTest.php +++ b/tests/Unit/Services/Auth/RegistrationFormatterServiceTest.php @@ -13,7 +13,7 @@ class RegistrationFormatterServiceTest extends TestCase public function test_format_normalizes_parent_fields(): void { - $formatter = new RegistrationFormatterService(new PhoneFormatterService); + $formatter = new RegistrationFormatterService(new PhoneFormatterService()); $result = $formatter->format([ 'firstname' => 'john', @@ -41,7 +41,7 @@ class RegistrationFormatterServiceTest extends TestCase public function test_format_includes_second_parent_when_present(): void { - $formatter = new RegistrationFormatterService(new PhoneFormatterService); + $formatter = new RegistrationFormatterService(new PhoneFormatterService()); $result = $formatter->format([ 'firstname' => 'john', diff --git a/tests/Unit/Services/Auth/RegistrationServiceTest.php b/tests/Unit/Services/Auth/RegistrationServiceTest.php index 85dc3ff8..c36ea3b6 100644 --- a/tests/Unit/Services/Auth/RegistrationServiceTest.php +++ b/tests/Unit/Services/Auth/RegistrationServiceTest.php @@ -35,7 +35,7 @@ class RegistrationServiceTest extends TestCase 'is_active' => 1, ]); - $captchaService = new RegistrationCaptchaService; + $captchaService = new RegistrationCaptchaService(); $captcha = $captchaService->generate(4); $emailService = Mockery::mock(EmailService::class); @@ -43,8 +43,8 @@ class RegistrationServiceTest extends TestCase $service = new RegistrationService( $emailService, - new SchoolIdService, - new RegistrationFormatterService(new PhoneFormatterService), + new SchoolIdService(), + new RegistrationFormatterService(new PhoneFormatterService()), $captchaService ); @@ -115,7 +115,7 @@ class RegistrationServiceTest extends TestCase 'token' => 'token', ]); - $captchaService = new RegistrationCaptchaService; + $captchaService = new RegistrationCaptchaService(); $captcha = $captchaService->generate(4); $emailService = Mockery::mock(EmailService::class); @@ -123,8 +123,8 @@ class RegistrationServiceTest extends TestCase $service = new RegistrationService( $emailService, - new SchoolIdService, - new RegistrationFormatterService(new PhoneFormatterService), + new SchoolIdService(), + new RegistrationFormatterService(new PhoneFormatterService()), $captchaService ); diff --git a/tests/Unit/Services/Auth/UserRoleServiceTest.php b/tests/Unit/Services/Auth/UserRoleServiceTest.php index 2e989121..2b756fbd 100644 --- a/tests/Unit/Services/Auth/UserRoleServiceTest.php +++ b/tests/Unit/Services/Auth/UserRoleServiceTest.php @@ -35,7 +35,7 @@ class UserRoleServiceTest extends TestCase 'can_manage' => 0, ]); - $service = new UserRoleService; + $service = new UserRoleService(); $roleIds = $service->getRoleIds(5); $this->assertTrue($service->hasPermissionForCrud($roleIds, 'manage_students', 'read')); diff --git a/tests/Unit/Services/Badges/BadgeFormDataServiceTest.php b/tests/Unit/Services/Badges/BadgeFormDataServiceTest.php index 36d0f65f..26482aae 100644 --- a/tests/Unit/Services/Badges/BadgeFormDataServiceTest.php +++ b/tests/Unit/Services/Badges/BadgeFormDataServiceTest.php @@ -19,7 +19,7 @@ class BadgeFormDataServiceTest extends TestCase public function test_build_formats_users_and_assigns_latest_class_for_teacherish_roles(): void { $lookup = Mockery::mock(BadgeUserLookupService::class); - $formatter = new BadgeTextFormatter; + $formatter = new BadgeTextFormatter(); $lookup->shouldReceive('fetchStaffList') ->once() @@ -75,7 +75,7 @@ class BadgeFormDataServiceTest extends TestCase public function test_build_defaults_invalid_active_role_to_teacher(): void { $lookup = Mockery::mock(BadgeUserLookupService::class); - $formatter = new BadgeTextFormatter; + $formatter = new BadgeTextFormatter(); $lookup->shouldReceive('fetchStaffList') ->once() @@ -91,4 +91,4 @@ class BadgeFormDataServiceTest extends TestCase $this->assertSame('teacher', $result['active_role']); } -} +} \ No newline at end of file diff --git a/tests/Unit/Services/Badges/BadgePdfServiceTest.php b/tests/Unit/Services/Badges/BadgePdfServiceTest.php index 787ae2af..8e1267b6 100644 --- a/tests/Unit/Services/Badges/BadgePdfServiceTest.php +++ b/tests/Unit/Services/Badges/BadgePdfServiceTest.php @@ -22,7 +22,7 @@ class BadgePdfServiceTest extends TestCase { $lookup = Mockery::mock(BadgeUserLookupService::class); $printLog = Mockery::mock(BadgePrintLogService::class); - $formatter = new BadgeTextFormatter; + $formatter = new BadgeTextFormatter(); $lookup->shouldReceive('getUserInfoById') ->once() @@ -65,7 +65,7 @@ class BadgePdfServiceTest extends TestCase { $lookup = Mockery::mock(BadgeUserLookupService::class); $printLog = Mockery::mock(BadgePrintLogService::class); - $formatter = new BadgeTextFormatter; + $formatter = new BadgeTextFormatter(); $lookup->shouldReceive('getUserInfoById') ->once() @@ -94,7 +94,7 @@ class BadgePdfServiceTest extends TestCase { $lookup = Mockery::mock(BadgeUserLookupService::class); $printLog = Mockery::mock(BadgePrintLogService::class); - $formatter = new BadgeTextFormatter; + $formatter = new BadgeTextFormatter(); $userInfo = [ 'user_id' => 10, diff --git a/tests/Unit/Services/Badges/BadgePrintLogServiceTest.php b/tests/Unit/Services/Badges/BadgePrintLogServiceTest.php index 651a0dcf..eea60555 100644 --- a/tests/Unit/Services/Badges/BadgePrintLogServiceTest.php +++ b/tests/Unit/Services/Badges/BadgePrintLogServiceTest.php @@ -20,7 +20,7 @@ class BadgePrintLogServiceTest extends TestCase public function test_get_status_normalizes_ids_before_calling_model(): void { $model = Mockery::mock(BadgePrintLog::class); - $formatter = new BadgeTextFormatter; + $formatter = new BadgeTextFormatter(); $expected = [ 5 => ['count' => 2, 'last_printed_at' => '2026-03-06 10:00:00', 'last_printed_by' => 1], @@ -41,7 +41,7 @@ class BadgePrintLogServiceTest extends TestCase public function test_log_normalizes_ids_and_returns_inserted_count(): void { $model = Mockery::mock(BadgePrintLog::class); - $formatter = new BadgeTextFormatter; + $formatter = new BadgeTextFormatter(); $model->shouldReceive('logPrints') ->once() @@ -74,7 +74,7 @@ class BadgePrintLogServiceTest extends TestCase Log::spy(); $model = Mockery::mock(BadgePrintLog::class); - $formatter = new BadgeTextFormatter; + $formatter = new BadgeTextFormatter(); $model->shouldReceive('logPrints') ->once() @@ -91,4 +91,4 @@ class BadgePrintLogServiceTest extends TestCase Log::shouldHaveReceived('error')->once(); $this->assertTrue(true); } -} +} \ No newline at end of file diff --git a/tests/Unit/Services/Badges/BadgeTextFormatterTest.php b/tests/Unit/Services/Badges/BadgeTextFormatterTest.php index b0f25050..cc581db5 100644 --- a/tests/Unit/Services/Badges/BadgeTextFormatterTest.php +++ b/tests/Unit/Services/Badges/BadgeTextFormatterTest.php @@ -13,7 +13,7 @@ class BadgeTextFormatterTest extends TestCase protected function setUp(): void { parent::setUp(); - $this->formatter = new BadgeTextFormatter; + $this->formatter = new BadgeTextFormatter(); } public function test_normalize_ids_filters_and_deduplicates(): void @@ -86,7 +86,7 @@ class BadgeTextFormatterTest extends TestCase public function test_fit_text_returns_size_within_bounds(): void { - $pdf = new FPDF; + $pdf = new FPDF(); $pdf->AddPage(); $pdf->SetFont('Arial', '', 12); @@ -95,4 +95,4 @@ class BadgeTextFormatterTest extends TestCase $this->assertGreaterThanOrEqual(7, $size); $this->assertLessThanOrEqual(11, $size); } -} +} \ No newline at end of file diff --git a/tests/Unit/Services/Billing/BalanceCalculationServiceTest.php b/tests/Unit/Services/Billing/BalanceCalculationServiceTest.php index a6ac2f18..11422592 100644 --- a/tests/Unit/Services/Billing/BalanceCalculationServiceTest.php +++ b/tests/Unit/Services/Billing/BalanceCalculationServiceTest.php @@ -192,13 +192,13 @@ class BalanceCalculationServiceTest extends TestCase private function makeService(): BalanceCalculationService { - $config = new FeeConfigService; + $config = new FeeConfigService(); $tuition = new TuitionCalculationService( $config, - new FeeStudentFeeService(new FeeGradeService, $config) + new FeeStudentFeeService(new FeeGradeService(), $config) ); - return new BalanceCalculationService($config, $tuition, new BillingTotalsService); + return new BalanceCalculationService($config, $tuition, new BillingTotalsService()); } private function seedFeeConfig(): void @@ -226,7 +226,7 @@ class BalanceCalculationServiceTest extends TestCase { return (int) DB::table('invoices')->insertGetId([ 'parent_id' => $parentId, - 'invoice_number' => 'INV-'.$parentId, + 'invoice_number' => 'INV-' . $parentId, 'total_amount' => $total, 'balance' => 0.00, 'paid_amount' => 0.00, diff --git a/tests/Unit/Services/Billing/BillingTotalsServiceTest.php b/tests/Unit/Services/Billing/BillingTotalsServiceTest.php index 3a110fc6..3fefc0ed 100644 --- a/tests/Unit/Services/Billing/BillingTotalsServiceTest.php +++ b/tests/Unit/Services/Billing/BillingTotalsServiceTest.php @@ -51,7 +51,7 @@ class BillingTotalsServiceTest extends TestCase ], ]); - $service = new BillingTotalsService; + $service = new BillingTotalsService(); $eventSubtotal = $service->eventSubtotal(10, '2025-2026'); $additionalSubtotal = $service->additionalSubtotal(99, '2025-2026'); @@ -95,7 +95,7 @@ class BillingTotalsServiceTest extends TestCase ], ]); - $service = new BillingTotalsService; + $service = new BillingTotalsService(); $this->assertSame(30.0, $service->additionalSubtotalByParent(10, '2025-2026')); } @@ -126,7 +126,7 @@ class BillingTotalsServiceTest extends TestCase ], ]); - $service = new BillingTotalsService; + $service = new BillingTotalsService(); $byStudent = $service->eventSubtotalsByStudent(5, '2025-2026'); $this->assertSame(20.0, $byStudent[1]); diff --git a/tests/Unit/Services/BroadcastEmail/BroadcastEmailComposerServiceTest.php b/tests/Unit/Services/BroadcastEmail/BroadcastEmailComposerServiceTest.php index d42a0292..6566ef87 100644 --- a/tests/Unit/Services/BroadcastEmail/BroadcastEmailComposerServiceTest.php +++ b/tests/Unit/Services/BroadcastEmail/BroadcastEmailComposerServiceTest.php @@ -9,7 +9,7 @@ class BroadcastEmailComposerServiceTest extends TestCase { public function test_sanitize_removes_script_and_events(): void { - $service = new BroadcastEmailComposerService; + $service = new BroadcastEmailComposerService(); $html = '

Hi

'; $clean = $service->sanitizeHtml($html); @@ -20,7 +20,7 @@ class BroadcastEmailComposerServiceTest extends TestCase public function test_compose_replaces_name_when_personalized(): void { - $service = new BroadcastEmailComposerService; + $service = new BroadcastEmailComposerService(); $html = '

Hello {{name}}

'; $rendered = $service->compose(false, 'Subject', $html, 'Parent', '', '', '', true); diff --git a/tests/Unit/Services/BroadcastEmail/BroadcastEmailRecipientServiceTest.php b/tests/Unit/Services/BroadcastEmail/BroadcastEmailRecipientServiceTest.php index b43f8a41..2465c4db 100644 --- a/tests/Unit/Services/BroadcastEmail/BroadcastEmailRecipientServiceTest.php +++ b/tests/Unit/Services/BroadcastEmail/BroadcastEmailRecipientServiceTest.php @@ -15,7 +15,7 @@ class BroadcastEmailRecipientServiceTest extends TestCase { $this->seedParentData(); - $service = new BroadcastEmailRecipientService; + $service = new BroadcastEmailRecipientService(); $parents = $service->parentsWithEmails(); $this->assertCount(1, $parents); @@ -26,7 +26,7 @@ class BroadcastEmailRecipientServiceTest extends TestCase { $this->seedParentData(); - $service = new BroadcastEmailRecipientService; + $service = new BroadcastEmailRecipientService(); $recipients = $service->recipientsByIds([10]); $this->assertCount(1, $recipients); diff --git a/tests/Unit/Services/BroadcastEmail/BroadcastEmailSenderOptionsServiceTest.php b/tests/Unit/Services/BroadcastEmail/BroadcastEmailSenderOptionsServiceTest.php index 75730f67..7627c1ea 100644 --- a/tests/Unit/Services/BroadcastEmail/BroadcastEmailSenderOptionsServiceTest.php +++ b/tests/Unit/Services/BroadcastEmail/BroadcastEmailSenderOptionsServiceTest.php @@ -12,12 +12,12 @@ class BroadcastEmailSenderOptionsServiceTest extends TestCase $old = getenv('MAIL_SENDERS') ?: ''; putenv('MAIL_SENDERS={"general":{"name":"Office","email":"office@example.com"}}'); - $service = new BroadcastEmailSenderOptionsService; + $service = new BroadcastEmailSenderOptionsService(); $options = $service->listOptions(); $this->assertSame('general', $options[0]['key']); $this->assertSame('Office ', $options[0]['label']); - putenv('MAIL_SENDERS='.$old); + putenv('MAIL_SENDERS=' . $old); } } diff --git a/tests/Unit/Services/ClassPrep/ClassRosterServiceTest.php b/tests/Unit/Services/ClassPrep/ClassRosterServiceTest.php index 3940f211..fea21717 100644 --- a/tests/Unit/Services/ClassPrep/ClassRosterServiceTest.php +++ b/tests/Unit/Services/ClassPrep/ClassRosterServiceTest.php @@ -15,7 +15,7 @@ class ClassRosterServiceTest extends TestCase { $this->seedRosterData(); - $service = new ClassRosterService; + $service = new ClassRosterService(); $students = $service->listStudentsByClass(101, '2025-2026'); $this->assertCount(1, $students); diff --git a/tests/Unit/Services/ClassPrep/StickerCountServiceTest.php b/tests/Unit/Services/ClassPrep/StickerCountServiceTest.php index 968f3cd4..e68a646a 100644 --- a/tests/Unit/Services/ClassPrep/StickerCountServiceTest.php +++ b/tests/Unit/Services/ClassPrep/StickerCountServiceTest.php @@ -15,7 +15,7 @@ class StickerCountServiceTest extends TestCase { $this->seedStickerData(); - $service = new StickerCountService; + $service = new StickerCountService(); $payload = $service->listAll('2025-2026', 'Fall'); $this->assertSame(2, $payload['totals']['students']); @@ -27,7 +27,7 @@ class StickerCountServiceTest extends TestCase { $this->seedStickerData(); - $service = new StickerCountService; + $service = new StickerCountService(); $payload = $service->listForClass('2025-2026', 'Fall', 101); $this->assertSame(1, $payload['totals']['students']); diff --git a/tests/Unit/Services/ClassPreparation/ClassPreparationAdjustmentServiceTest.php b/tests/Unit/Services/ClassPreparation/ClassPreparationAdjustmentServiceTest.php index fee74c26..8cc02f54 100644 --- a/tests/Unit/Services/ClassPreparation/ClassPreparationAdjustmentServiceTest.php +++ b/tests/Unit/Services/ClassPreparation/ClassPreparationAdjustmentServiceTest.php @@ -21,7 +21,7 @@ class ClassPreparationAdjustmentServiceTest extends TestCase 'adjustable' => 1, ]); - $service = new ClassPreparationAdjustmentService; + $service = new ClassPreparationAdjustmentService(); [$items, $adjMap] = $service->applyAdjustments(['Small Table' => 1], '101', '2025-2026'); $this->assertSame(3, $items['Small Table']); diff --git a/tests/Unit/Services/ClassPreparation/ClassPreparationCalculatorServiceTest.php b/tests/Unit/Services/ClassPreparation/ClassPreparationCalculatorServiceTest.php index bb4fc220..322fbcba 100644 --- a/tests/Unit/Services/ClassPreparation/ClassPreparationCalculatorServiceTest.php +++ b/tests/Unit/Services/ClassPreparation/ClassPreparationCalculatorServiceTest.php @@ -22,7 +22,7 @@ class ClassPreparationCalculatorServiceTest extends TestCase 'school_year' => '2025-2026', ]); - $service = new ClassPreparationCalculatorService; + $service = new ClassPreparationCalculatorService(); $this->assertSame(2, $service->getClassLevelBySection('101')); $this->assertSame(1, $service->getClassLevelBySection('KG')); @@ -49,7 +49,7 @@ class ClassPreparationCalculatorServiceTest extends TestCase 'school_year' => '2025-2026', ]); - $service = new ClassPreparationCalculatorService; + $service = new ClassPreparationCalculatorService(); $items = $service->calculatePrepItems(6, 1, '101'); $this->assertSame(2, $items['Small Table']); diff --git a/tests/Unit/Services/ClassPreparation/ClassPreparationContextServiceTest.php b/tests/Unit/Services/ClassPreparation/ClassPreparationContextServiceTest.php index 7181521f..6da0be6f 100644 --- a/tests/Unit/Services/ClassPreparation/ClassPreparationContextServiceTest.php +++ b/tests/Unit/Services/ClassPreparation/ClassPreparationContextServiceTest.php @@ -20,7 +20,7 @@ class ClassPreparationContextServiceTest extends TestCase 'school_year' => '2025-2026', ]); - $service = new ClassPreparationContextService; + $service = new ClassPreparationContextService(); $this->assertTrue($service->hasRosterForSemester('2025-2026', 'Fall')); $this->assertFalse($service->hasRosterForSemester('2025-2026', 'Spring')); diff --git a/tests/Unit/Services/ClassPreparation/ClassPreparationInventoryServiceTest.php b/tests/Unit/Services/ClassPreparation/ClassPreparationInventoryServiceTest.php index 80347040..c6b9af3b 100644 --- a/tests/Unit/Services/ClassPreparation/ClassPreparationInventoryServiceTest.php +++ b/tests/Unit/Services/ClassPreparation/ClassPreparationInventoryServiceTest.php @@ -40,7 +40,7 @@ class ClassPreparationInventoryServiceTest extends TestCase ], ]); - $service = new ClassPreparationInventoryService; + $service = new ClassPreparationInventoryService(); $map = $service->buildAvailability('2025-2026', 'Fall', true, ['Small Table']); $this->assertSame(8, $map['Small Table']); diff --git a/tests/Unit/Services/ClassPreparation/ClassPreparationLogServiceTest.php b/tests/Unit/Services/ClassPreparation/ClassPreparationLogServiceTest.php index 592af9ae..158868d5 100644 --- a/tests/Unit/Services/ClassPreparation/ClassPreparationLogServiceTest.php +++ b/tests/Unit/Services/ClassPreparation/ClassPreparationLogServiceTest.php @@ -4,6 +4,7 @@ namespace Tests\Unit\Services\ClassPreparation; use App\Services\ClassPreparation\ClassPreparationLogService; use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Support\Facades\DB; use Tests\TestCase; class ClassPreparationLogServiceTest extends TestCase @@ -12,7 +13,7 @@ class ClassPreparationLogServiceTest extends TestCase public function test_has_prep_changed_detects_diff(): void { - $service = new ClassPreparationLogService; + $service = new ClassPreparationLogService(); $this->assertTrue($service->hasPrepChanged(['a' => 1], ['a' => 2])); $this->assertFalse($service->hasPrepChanged(['a' => 1], ['a' => 1])); @@ -20,7 +21,7 @@ class ClassPreparationLogServiceTest extends TestCase public function test_create_log_and_get_latest(): void { - $service = new ClassPreparationLogService; + $service = new ClassPreparationLogService(); $created = $service->createLog('101', '1-A', '2025-2026', ['Small Table' => 2], '2025-09-01 00:00:00'); $this->assertTrue($created); diff --git a/tests/Unit/Services/ClassPreparation/ClassPreparationRosterServiceTest.php b/tests/Unit/Services/ClassPreparation/ClassPreparationRosterServiceTest.php index 9d3cc445..c3c1db0a 100644 --- a/tests/Unit/Services/ClassPreparation/ClassPreparationRosterServiceTest.php +++ b/tests/Unit/Services/ClassPreparation/ClassPreparationRosterServiceTest.php @@ -15,7 +15,7 @@ class ClassPreparationRosterServiceTest extends TestCase { $this->seedRoster(); - $service = new ClassPreparationRosterService; + $service = new ClassPreparationRosterService(); $rows = $service->getClassSectionStudentCounts('2025-2026', 'Fall', true); $this->assertCount(1, $rows); @@ -27,7 +27,7 @@ class ClassPreparationRosterServiceTest extends TestCase { $this->seedRoster(); - $service = new ClassPreparationRosterService; + $service = new ClassPreparationRosterService(); $count = $service->getStudentCountForSection('2025-2026', 'Fall', true, '101'); $this->assertSame(2, $count); diff --git a/tests/Unit/Services/ClassProgress/ClassProgressAttachmentServiceTest.php b/tests/Unit/Services/ClassProgress/ClassProgressAttachmentServiceTest.php index 896ed149..8e078866 100644 --- a/tests/Unit/Services/ClassProgress/ClassProgressAttachmentServiceTest.php +++ b/tests/Unit/Services/ClassProgress/ClassProgressAttachmentServiceTest.php @@ -18,7 +18,7 @@ class ClassProgressAttachmentServiceTest extends TestCase Storage::fake('public'); $report = ClassProgressReport::factory()->create(); - $service = new ClassProgressAttachmentService; + $service = new ClassProgressAttachmentService(); $files = [UploadedFile::fake()->create('sample.pdf', 12)]; $stored = $service->storeAttachments($report->id, $files); diff --git a/tests/Unit/Services/ClassProgress/ClassProgressMetaServiceTest.php b/tests/Unit/Services/ClassProgress/ClassProgressMetaServiceTest.php index 1fd83b40..eff89d2a 100644 --- a/tests/Unit/Services/ClassProgress/ClassProgressMetaServiceTest.php +++ b/tests/Unit/Services/ClassProgress/ClassProgressMetaServiceTest.php @@ -9,7 +9,7 @@ class ClassProgressMetaServiceTest extends TestCase { public function test_subject_sections_return_configured_values(): void { - $service = new ClassProgressMetaService; + $service = new ClassProgressMetaService(); $sections = $service->subjectSections(); diff --git a/tests/Unit/Services/ClassProgress/ClassProgressMutationServiceTest.php b/tests/Unit/Services/ClassProgress/ClassProgressMutationServiceTest.php index faaa3be4..19469225 100644 --- a/tests/Unit/Services/ClassProgress/ClassProgressMutationServiceTest.php +++ b/tests/Unit/Services/ClassProgress/ClassProgressMutationServiceTest.php @@ -22,8 +22,8 @@ class ClassProgressMutationServiceTest extends TestCase $this->assignTeacher($user->id, 101); $service = new ClassProgressMutationService( - new ClassProgressRuleService, - new ClassProgressAttachmentService + new ClassProgressRuleService(), + new ClassProgressAttachmentService() ); $payload = [ @@ -47,8 +47,8 @@ class ClassProgressMutationServiceTest extends TestCase ]); $service = new ClassProgressMutationService( - new ClassProgressRuleService, - new ClassProgressAttachmentService + new ClassProgressRuleService(), + new ClassProgressAttachmentService() ); $updated = $service->updateReport($report, ['status' => 'behind'], []); @@ -60,8 +60,8 @@ class ClassProgressMutationServiceTest extends TestCase { $report = ClassProgressReport::factory()->create(); $service = new ClassProgressMutationService( - new ClassProgressRuleService, - new ClassProgressAttachmentService + new ClassProgressRuleService(), + new ClassProgressAttachmentService() ); $service->deleteReport($report); diff --git a/tests/Unit/Services/ClassProgress/ClassProgressQueryServiceTest.php b/tests/Unit/Services/ClassProgress/ClassProgressQueryServiceTest.php index 4d11829d..ac10c724 100644 --- a/tests/Unit/Services/ClassProgress/ClassProgressQueryServiceTest.php +++ b/tests/Unit/Services/ClassProgress/ClassProgressQueryServiceTest.php @@ -26,7 +26,7 @@ class ClassProgressQueryServiceTest extends TestCase 'class_section_id' => 202, ]); - $service = new ClassProgressQueryService(new ClassProgressRuleService); + $service = new ClassProgressQueryService(new ClassProgressRuleService()); $results = $service->listReports($user, ['class_section_id' => 101]); $this->assertSame(1, $results->total()); diff --git a/tests/Unit/Services/ClassProgress/ClassProgressRuleServiceTest.php b/tests/Unit/Services/ClassProgress/ClassProgressRuleServiceTest.php index 60189aef..0303c999 100644 --- a/tests/Unit/Services/ClassProgress/ClassProgressRuleServiceTest.php +++ b/tests/Unit/Services/ClassProgress/ClassProgressRuleServiceTest.php @@ -9,7 +9,7 @@ class ClassProgressRuleServiceTest extends TestCase { public function test_build_unit_chapter_summary_limits_length(): void { - $service = new ClassProgressRuleService; + $service = new ClassProgressRuleService(); $summary = $service->buildUnitChapterSummary(['Unit 1'], ['Chapter A']); @@ -18,7 +18,7 @@ class ClassProgressRuleServiceTest extends TestCase public function test_ensure_week_end_defaults_to_six_days(): void { - $service = new ClassProgressRuleService; + $service = new ClassProgressRuleService(); $weekEnd = $service->ensureWeekEnd('2025-09-07', null); diff --git a/tests/Unit/Services/Communication/CommunicationTemplateServiceTest.php b/tests/Unit/Services/Communication/CommunicationTemplateServiceTest.php index ce5d3fca..4c1fab86 100644 --- a/tests/Unit/Services/Communication/CommunicationTemplateServiceTest.php +++ b/tests/Unit/Services/Communication/CommunicationTemplateServiceTest.php @@ -22,7 +22,7 @@ class CommunicationTemplateServiceTest extends TestCase 'is_active' => 1, ]); - $service = new CommunicationTemplateService; + $service = new CommunicationTemplateService(); $templates = $service->listActiveTemplates(); $this->assertSame('welcome', $templates[0]['template_key']); diff --git a/tests/Unit/Services/CompetitionScores/CompetitionScoresSaveServiceTest.php b/tests/Unit/Services/CompetitionScores/CompetitionScoresSaveServiceTest.php index 89f02b89..cf550815 100644 --- a/tests/Unit/Services/CompetitionScores/CompetitionScoresSaveServiceTest.php +++ b/tests/Unit/Services/CompetitionScores/CompetitionScoresSaveServiceTest.php @@ -2,6 +2,7 @@ namespace Tests\Unit\Services\CompetitionScores; +use App\Models\CompetitionScore; use App\Services\CompetitionScores\CompetitionScoresSaveService; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\DB; @@ -13,7 +14,7 @@ class CompetitionScoresSaveServiceTest extends TestCase public function test_filter_scores_rejects_non_integers(): void { - $service = new CompetitionScoresSaveService; + $service = new CompetitionScoresSaveService(); [$clean, $invalid] = $service->filterScores([ 1 => '10', 2 => '4.5', @@ -32,7 +33,7 @@ class CompetitionScoresSaveServiceTest extends TestCase 'score' => 2, ]); - $service = new CompetitionScoresSaveService; + $service = new CompetitionScoresSaveService(); $service->saveScores(1, 101, [1 => 7]); $this->assertDatabaseHas('competition_scores', [ diff --git a/tests/Unit/Services/Discounts/DiscountApplyServiceTest.php b/tests/Unit/Services/Discounts/DiscountApplyServiceTest.php index 21ea1a47..22db9a0c 100644 --- a/tests/Unit/Services/Discounts/DiscountApplyServiceTest.php +++ b/tests/Unit/Services/Discounts/DiscountApplyServiceTest.php @@ -30,7 +30,7 @@ class DiscountApplyServiceTest extends TestCase 'is_active' => 1, ]); - $service = new DiscountApplyService(new DiscountContextService, new DiscountInvoiceService); + $service = new DiscountApplyService(new DiscountContextService(), new DiscountInvoiceService()); $result = $service->applyVoucher(1, [10], true, 1); $this->assertFalse($result['ok']); diff --git a/tests/Unit/Services/Email/EmailExtractorServiceTest.php b/tests/Unit/Services/Email/EmailExtractorServiceTest.php index 29021d4b..0ff9cb3a 100644 --- a/tests/Unit/Services/Email/EmailExtractorServiceTest.php +++ b/tests/Unit/Services/Email/EmailExtractorServiceTest.php @@ -44,7 +44,7 @@ class EmailExtractorServiceTest extends TestCase 'school_year' => '2025-2026', ]); - $service = new EmailExtractorService; + $service = new EmailExtractorService(); $emails = $service->listEmails(); $this->assertContains('user1@example.com', $emails['users']); diff --git a/tests/Unit/Services/Email/EmailProfileServiceTest.php b/tests/Unit/Services/Email/EmailProfileServiceTest.php index fd85f3c6..0306aca8 100644 --- a/tests/Unit/Services/Email/EmailProfileServiceTest.php +++ b/tests/Unit/Services/Email/EmailProfileServiceTest.php @@ -9,7 +9,7 @@ class EmailProfileServiceTest extends TestCase { public function test_resolve_profile_maps_aliases(): void { - $service = new EmailProfileService; + $service = new EmailProfileService(); $this->assertSame('communication', $service->resolveProfile('comm')); $this->assertSame('default', $service->resolveProfile('system')); @@ -18,14 +18,14 @@ class EmailProfileServiceTest extends TestCase public function test_env_key_from_profile(): void { - $service = new EmailProfileService; + $service = new EmailProfileService(); $this->assertSame('PAYMENT_ALERTS', $service->envKeyFromProfile('payment alerts')); } public function test_sanitize_reply_to_name(): void { - $service = new EmailProfileService; + $service = new EmailProfileService(); $this->assertSame('Fallback', $service->sanitizeReplyToName('no-reply', 'Fallback')); $this->assertSame('Support', $service->sanitizeReplyToName('Support', 'Fallback')); diff --git a/tests/Unit/Services/Events/EventChargeQueryServiceTest.php b/tests/Unit/Services/Events/EventChargeQueryServiceTest.php index 19271c4a..e3a802e0 100644 --- a/tests/Unit/Services/Events/EventChargeQueryServiceTest.php +++ b/tests/Unit/Services/Events/EventChargeQueryServiceTest.php @@ -26,7 +26,7 @@ class EventChargeQueryServiceTest extends TestCase 'updated_at' => now(), ]); - $service = new EventChargeQueryService; + $service = new EventChargeQueryService(); $result = $service->list(['per_page' => 10, 'page' => 1]); $this->assertSame(1, $result['pagination']['total']); diff --git a/tests/Unit/Services/Events/EventListServiceTest.php b/tests/Unit/Services/Events/EventListServiceTest.php index 6758c9a3..5248d9cf 100644 --- a/tests/Unit/Services/Events/EventListServiceTest.php +++ b/tests/Unit/Services/Events/EventListServiceTest.php @@ -38,7 +38,7 @@ class EventListServiceTest extends TestCase ], ]); - $service = new EventListService; + $service = new EventListService(); $result = $service->list(['active' => true, 'per_page' => 10]); $this->assertSame(1, $result['pagination']['total']); diff --git a/tests/Unit/Services/Events/EventStudentChargeServiceTest.php b/tests/Unit/Services/Events/EventStudentChargeServiceTest.php index 62403b05..2e6c4d46 100644 --- a/tests/Unit/Services/Events/EventStudentChargeServiceTest.php +++ b/tests/Unit/Services/Events/EventStudentChargeServiceTest.php @@ -31,7 +31,7 @@ class EventStudentChargeServiceTest extends TestCase 'semester' => 'Fall', ]); - $service = new EventStudentChargeService; + $service = new EventStudentChargeService(); $rows = $service->listStudentsWithCharges(10, '2025-2026', 'Fall'); $this->assertTrue($rows[0]['charged']); diff --git a/tests/Unit/Services/Exams/ExamDraftTeacherServiceTest.php b/tests/Unit/Services/Exams/ExamDraftTeacherServiceTest.php index 10155f3e..ff07eded 100644 --- a/tests/Unit/Services/Exams/ExamDraftTeacherServiceTest.php +++ b/tests/Unit/Services/Exams/ExamDraftTeacherServiceTest.php @@ -21,7 +21,7 @@ class ExamDraftTeacherServiceTest extends TestCase $classSectionId = $this->seedClassSection(); $this->seedAssignment($teacherId, $classSectionId); - $service = new ExamDraftTeacherService(new ExamDraftConfigService, new ExamDraftFileService); + $service = new ExamDraftTeacherService(new ExamDraftConfigService(), new ExamDraftFileService()); $file = UploadedFile::fake()->create('draft.docx', 10, 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'); $result = $service->store($teacherId, [ diff --git a/tests/Unit/Services/Expenses/ExpenseReceiptServiceTest.php b/tests/Unit/Services/Expenses/ExpenseReceiptServiceTest.php index 6bdbda04..7a374401 100644 --- a/tests/Unit/Services/Expenses/ExpenseReceiptServiceTest.php +++ b/tests/Unit/Services/Expenses/ExpenseReceiptServiceTest.php @@ -16,11 +16,11 @@ class ExpenseReceiptServiceTest extends TestCase { Storage::fake(); - $service = new ExpenseReceiptService; + $service = new ExpenseReceiptService(); $file = UploadedFile::fake()->create('receipt.pdf', 10, 'application/pdf'); $filename = $service->storeReceipt($file); $this->assertNotEmpty($filename); - $this->assertSame(url('receipts/'.$filename), $service->receiptUrl($filename)); + $this->assertSame(url('receipts/' . $filename), $service->receiptUrl($filename)); } } diff --git a/tests/Unit/Services/Expenses/ExpenseStaffServiceTest.php b/tests/Unit/Services/Expenses/ExpenseStaffServiceTest.php index c548ef4d..32874564 100644 --- a/tests/Unit/Services/Expenses/ExpenseStaffServiceTest.php +++ b/tests/Unit/Services/Expenses/ExpenseStaffServiceTest.php @@ -66,7 +66,7 @@ class ExpenseStaffServiceTest extends TestCase ['user_id' => 11, 'role_id' => 2], ]); - $service = new ExpenseStaffService; + $service = new ExpenseStaffService(); $staff = $service->listStaffUsers(); $this->assertCount(1, $staff); diff --git a/tests/Unit/Services/ExtraCharges/ExtraChargesMetaServiceTest.php b/tests/Unit/Services/ExtraCharges/ExtraChargesMetaServiceTest.php index 85f52721..e4b41d7d 100644 --- a/tests/Unit/Services/ExtraCharges/ExtraChargesMetaServiceTest.php +++ b/tests/Unit/Services/ExtraCharges/ExtraChargesMetaServiceTest.php @@ -13,7 +13,7 @@ class ExtraChargesMetaServiceTest extends TestCase public function test_get_school_years_uses_fallback(): void { - $service = new ExtraChargesMetaService; + $service = new ExtraChargesMetaService(); $years = $service->getSchoolYears('2025-2026'); $this->assertSame(['2025-2026', '2024-2025', '2023-2024', '2022-2023'], $years); @@ -53,7 +53,7 @@ class ExtraChargesMetaServiceTest extends TestCase ], ]); - $service = new ExtraChargesMetaService; + $service = new ExtraChargesMetaService(); $years = $service->getSchoolYears(); $this->assertSame(['2025-2026', '2024-2025', '2023-2024'], $years); diff --git a/tests/Unit/Services/Families/FamilyFinanceServiceTest.php b/tests/Unit/Services/Families/FamilyFinanceServiceTest.php index eb03d91e..a394c682 100644 --- a/tests/Unit/Services/Families/FamilyFinanceServiceTest.php +++ b/tests/Unit/Services/Families/FamilyFinanceServiceTest.php @@ -40,7 +40,7 @@ class FamilyFinanceServiceTest extends TestCase 'updated_at' => now(), ]); - $service = new FamilyFinanceService; + $service = new FamilyFinanceService(); $result = $service->loadFinancialsForParents([1]); $this->assertSame(1, $result['summary']['invoices_count']); diff --git a/tests/Unit/Services/Families/FamilyMutationServiceTest.php b/tests/Unit/Services/Families/FamilyMutationServiceTest.php index cacc81ed..7c23cadf 100644 --- a/tests/Unit/Services/Families/FamilyMutationServiceTest.php +++ b/tests/Unit/Services/Families/FamilyMutationServiceTest.php @@ -16,7 +16,7 @@ class FamilyMutationServiceTest extends TestCase $this->seedUser(1); $this->seedStudent(10, 1); - $service = new FamilyMutationService; + $service = new FamilyMutationService(); $result = $service->bootstrapFamilies(); $this->assertSame(1, $result['families_created']); @@ -36,7 +36,7 @@ class FamilyMutationServiceTest extends TestCase 'updated_at' => now(), ]); - $service = new FamilyMutationService; + $service = new FamilyMutationService(); $result = $service->attachSecondByEmail(10, 'secondary@example.com', 'Second', 'Parent'); $this->assertTrue($result['ok']); @@ -63,7 +63,7 @@ class FamilyMutationServiceTest extends TestCase 'updated_at' => now(), ]); - $service = new FamilyMutationService; + $service = new FamilyMutationService(); $service->setPrimaryHome($familyId, 10, true); $this->assertDatabaseHas('family_students', [ @@ -80,7 +80,7 @@ class FamilyMutationServiceTest extends TestCase 'firstname' => 'Parent', 'lastname' => 'User', 'cellphone' => '9999999999', - 'email' => 'parent'.$id.'@example.com', + 'email' => 'parent' . $id . '@example.com', 'address_street' => '123 Street', 'city' => 'City', 'state' => 'ST', diff --git a/tests/Unit/Services/Families/FamilyQueryServiceTest.php b/tests/Unit/Services/Families/FamilyQueryServiceTest.php index d837efdf..40da91da 100644 --- a/tests/Unit/Services/Families/FamilyQueryServiceTest.php +++ b/tests/Unit/Services/Families/FamilyQueryServiceTest.php @@ -20,7 +20,7 @@ class FamilyQueryServiceTest extends TestCase $this->seedFamilyStudent($familyId, $studentId); $this->seedFamilyGuardian($familyId, 1); - $service = new FamilyQueryService(new FamilyFinanceService); + $service = new FamilyQueryService(new FamilyFinanceService()); $result = $service->adminIndexData($studentId, null, '2025-2026'); $this->assertNotEmpty($result['families']); @@ -31,7 +31,7 @@ class FamilyQueryServiceTest extends TestCase { $this->seedStudent(10, 1); - $service = new FamilyQueryService(new FamilyFinanceService); + $service = new FamilyQueryService(new FamilyFinanceService()); $items = $service->searchSuggestions('Student', 5); $this->assertNotEmpty($items); @@ -46,7 +46,7 @@ class FamilyQueryServiceTest extends TestCase $this->seedFamilyStudent($familyId, $studentId); $this->seedFamilyGuardian($familyId, 1); - $service = new FamilyQueryService(new FamilyFinanceService); + $service = new FamilyQueryService(new FamilyFinanceService()); $family = $service->familyCard(null, null, $familyId, '2025-2026'); $this->assertNotNull($family); @@ -68,7 +68,7 @@ class FamilyQueryServiceTest extends TestCase 'firstname' => 'Parent', 'lastname' => 'User', 'cellphone' => '9999999999', - 'email' => 'parent'.$parentId.'@example.com', + 'email' => 'parent' . $parentId . '@example.com', 'address_street' => '123 Street', 'city' => 'City', 'state' => 'ST', @@ -103,7 +103,7 @@ class FamilyQueryServiceTest extends TestCase { return DB::table('families')->insertGetId([ 'family_code' => $code, - 'household_name' => 'Family '.$code, + 'household_name' => 'Family ' . $code, 'is_active' => 1, 'created_at' => now(), 'updated_at' => now(), diff --git a/tests/Unit/Services/Fees/FeeGradeServiceTest.php b/tests/Unit/Services/Fees/FeeGradeServiceTest.php index ec1b4973..9ba6a0be 100644 --- a/tests/Unit/Services/Fees/FeeGradeServiceTest.php +++ b/tests/Unit/Services/Fees/FeeGradeServiceTest.php @@ -9,7 +9,7 @@ class FeeGradeServiceTest extends TestCase { public function test_grade_level_parsing(): void { - $service = new FeeGradeService; + $service = new FeeGradeService(); $this->assertSame(0, $service->getGradeLevel('K')); $this->assertSame(99, $service->getGradeLevel('Y')); @@ -19,7 +19,7 @@ class FeeGradeServiceTest extends TestCase public function test_compare_grades_orders_numeric_then_suffix(): void { - $service = new FeeGradeService; + $service = new FeeGradeService(); $this->assertSame(-1, $service->compareGrades('2', '3')); $this->assertSame(-1, $service->compareGrades('3-A', '3-B')); diff --git a/tests/Unit/Services/Fees/FeeRefundCalculatorServiceTest.php b/tests/Unit/Services/Fees/FeeRefundCalculatorServiceTest.php index 20cd33fb..3c7469a7 100644 --- a/tests/Unit/Services/Fees/FeeRefundCalculatorServiceTest.php +++ b/tests/Unit/Services/Fees/FeeRefundCalculatorServiceTest.php @@ -225,8 +225,8 @@ class FeeRefundCalculatorServiceTest extends TestCase private function makeService(): FeeRefundCalculatorService { return new FeeRefundCalculatorService( - new FeeConfigService, - new FeeStudentFeeService(new FeeGradeService, new FeeConfigService) + new FeeConfigService(), + new FeeStudentFeeService(new FeeGradeService(), new FeeConfigService()) ); } @@ -258,7 +258,7 @@ class FeeRefundCalculatorServiceTest extends TestCase { $invoiceId = DB::table('invoices')->insertGetId([ 'parent_id' => $parentId, - 'invoice_number' => 'INV-'.$parentId, + 'invoice_number' => 'INV-' . $parentId, 'total_amount' => $amount, 'balance' => 0.00, 'paid_amount' => $amount, diff --git a/tests/Unit/Services/Fees/FeeRefundDetailServiceTest.php b/tests/Unit/Services/Fees/FeeRefundDetailServiceTest.php index 3e89925d..f901e9a9 100644 --- a/tests/Unit/Services/Fees/FeeRefundDetailServiceTest.php +++ b/tests/Unit/Services/Fees/FeeRefundDetailServiceTest.php @@ -55,7 +55,7 @@ class FeeRefundDetailServiceTest extends TestCase 'status' => 'Paid', ]); - $service = new FeeRefundDetailService; + $service = new FeeRefundDetailService(); $result = $service->calculateRefundDetails([ [ 'student_id' => 1, diff --git a/tests/Unit/Services/Fees/FeeStudentFeeServiceTest.php b/tests/Unit/Services/Fees/FeeStudentFeeServiceTest.php index b28d3bd3..ff59b8b6 100644 --- a/tests/Unit/Services/Fees/FeeStudentFeeServiceTest.php +++ b/tests/Unit/Services/Fees/FeeStudentFeeServiceTest.php @@ -138,7 +138,7 @@ class FeeStudentFeeServiceTest extends TestCase private function makeService(): FeeStudentFeeService { - return new FeeStudentFeeService(new FeeGradeService, new FeeConfigService); + return new FeeStudentFeeService(new FeeGradeService(), new FeeConfigService()); } private function seedFeeConfig(): void diff --git a/tests/Unit/Services/Fees/TuitionCalculationServiceTest.php b/tests/Unit/Services/Fees/TuitionCalculationServiceTest.php index 88171fc1..f9ecaf8d 100644 --- a/tests/Unit/Services/Fees/TuitionCalculationServiceTest.php +++ b/tests/Unit/Services/Fees/TuitionCalculationServiceTest.php @@ -148,11 +148,10 @@ class TuitionCalculationServiceTest extends TestCase private function makeService(): TuitionCalculationService { - $config = new FeeConfigService; - + $config = new FeeConfigService(); return new TuitionCalculationService( $config, - new FeeStudentFeeService(new FeeGradeService, $config) + new FeeStudentFeeService(new FeeGradeService(), $config) ); } diff --git a/tests/Unit/Services/Files/ExamDraftDownloadNameServiceTest.php b/tests/Unit/Services/Files/ExamDraftDownloadNameServiceTest.php index a509aa72..3ced4307 100644 --- a/tests/Unit/Services/Files/ExamDraftDownloadNameServiceTest.php +++ b/tests/Unit/Services/Files/ExamDraftDownloadNameServiceTest.php @@ -35,7 +35,7 @@ class ExamDraftDownloadNameServiceTest extends TestCase 'status' => 'draft', ]); - $service = new ExamDraftDownloadNameService; + $service = new ExamDraftDownloadNameService(); $name = $service->build('draft2.pdf', 'drafts'); $this->assertSame('2b_final_exam_v3', $name); diff --git a/tests/Unit/Services/Files/FileServeServiceTest.php b/tests/Unit/Services/Files/FileServeServiceTest.php index 2a7029a1..b28f1297 100644 --- a/tests/Unit/Services/Files/FileServeServiceTest.php +++ b/tests/Unit/Services/Files/FileServeServiceTest.php @@ -15,13 +15,13 @@ class FileServeServiceTest extends TestCase public function test_meta_returns_file_metadata(): void { $dir = storage_path('testing/files'); - if (! is_dir($dir)) { + if (!is_dir($dir)) { mkdir($dir, 0777, true); } - $path = $dir.DIRECTORY_SEPARATOR.'sample.pdf'; + $path = $dir . DIRECTORY_SEPARATOR . 'sample.pdf'; file_put_contents($path, 'PDFDATA'); - $service = new FileServeService; + $service = new FileServeService(); $meta = $service->meta($dir, 'sample.pdf', ['pdf'], 'download'); $this->assertSame('sample.pdf', $meta['name']); @@ -33,13 +33,13 @@ class FileServeServiceTest extends TestCase public function test_serve_inline_returns_304_when_etag_matches(): void { $dir = storage_path('testing/files'); - if (! is_dir($dir)) { + if (!is_dir($dir)) { mkdir($dir, 0777, true); } - $path = $dir.DIRECTORY_SEPARATOR.'cached.pdf'; + $path = $dir . DIRECTORY_SEPARATOR . 'cached.pdf'; file_put_contents($path, 'PDFDATA'); - $service = new FileServeService; + $service = new FileServeService(); $meta = $service->meta($dir, 'cached.pdf', ['pdf']); $request = Request::create('/files/cached.pdf', 'GET', [], [], [], [ @@ -57,13 +57,13 @@ class FileServeServiceTest extends TestCase $this->expectException(HttpException::class); $dir = storage_path('testing/files'); - if (! is_dir($dir)) { + if (!is_dir($dir)) { mkdir($dir, 0777, true); } - $path = $dir.DIRECTORY_SEPARATOR.'sample.exe'; + $path = $dir . DIRECTORY_SEPARATOR . 'sample.exe'; file_put_contents($path, 'DATA'); - $service = new FileServeService; + $service = new FileServeService(); $service->meta($dir, 'sample.exe', ['pdf']); } } diff --git a/tests/Unit/Services/Finance/FinancialChartServiceTest.php b/tests/Unit/Services/Finance/FinancialChartServiceTest.php index 1e89c4b9..e6b863ba 100644 --- a/tests/Unit/Services/Finance/FinancialChartServiceTest.php +++ b/tests/Unit/Services/Finance/FinancialChartServiceTest.php @@ -12,7 +12,7 @@ class FinancialChartServiceTest extends TestCase public function test_generate_charts_returns_null_in_tests(): void { - $service = new FinancialChartService; + $service = new FinancialChartService(); $summary = [ 'totalCharges' => 100, diff --git a/tests/Unit/Services/Finance/FinancialPaymentServiceTest.php b/tests/Unit/Services/Finance/FinancialPaymentServiceTest.php index d33ba803..491c43b2 100644 --- a/tests/Unit/Services/Finance/FinancialPaymentServiceTest.php +++ b/tests/Unit/Services/Finance/FinancialPaymentServiceTest.php @@ -68,7 +68,7 @@ class FinancialPaymentServiceTest extends TestCase ], ]); - $service = new FinancialPaymentService; + $service = new FinancialPaymentService(); $payments = $service->paymentsByInvoice('2025-2026', '2025-01-01', '2025-01-31'); $breakdown = $service->paymentBreakdown('2025-2026', '2025-01-01', '2025-01-31'); diff --git a/tests/Unit/Services/Finance/FinancialPdfReportServiceTest.php b/tests/Unit/Services/Finance/FinancialPdfReportServiceTest.php index 89d40dd2..021c05ad 100644 --- a/tests/Unit/Services/Finance/FinancialPdfReportServiceTest.php +++ b/tests/Unit/Services/Finance/FinancialPdfReportServiceTest.php @@ -13,7 +13,7 @@ class FinancialPdfReportServiceTest extends TestCase public function test_build_pdf_returns_bytes(): void { - $service = new FinancialPdfReportService(new FinancialChartService); + $service = new FinancialPdfReportService(new FinancialChartService()); $summary = [ 'schoolYear' => '2025-2026', diff --git a/tests/Unit/Services/Finance/FinancialReportServiceTest.php b/tests/Unit/Services/Finance/FinancialReportServiceTest.php index 2df5438d..9a289cc9 100644 --- a/tests/Unit/Services/Finance/FinancialReportServiceTest.php +++ b/tests/Unit/Services/Finance/FinancialReportServiceTest.php @@ -112,8 +112,8 @@ class FinancialReportServiceTest extends TestCase ]); $service = new FinancialReportService( - new FinancialPaymentService, - new FinancialSchoolYearService + new FinancialPaymentService(), + new FinancialSchoolYearService() ); $report = $service->getReport('2025-01-01', '2025-12-31', '2025-2026'); diff --git a/tests/Unit/Services/Finance/FinancialSchoolYearServiceTest.php b/tests/Unit/Services/Finance/FinancialSchoolYearServiceTest.php index 55d7fea8..af82841c 100644 --- a/tests/Unit/Services/Finance/FinancialSchoolYearServiceTest.php +++ b/tests/Unit/Services/Finance/FinancialSchoolYearServiceTest.php @@ -42,7 +42,7 @@ class FinancialSchoolYearServiceTest extends TestCase ], ]); - $service = new FinancialSchoolYearService; + $service = new FinancialSchoolYearService(); $years = $service->listYears(); $this->assertSame(['2025-2026', '2024-2025'], $years); diff --git a/tests/Unit/Services/Finance/FinancialSummaryServiceTest.php b/tests/Unit/Services/Finance/FinancialSummaryServiceTest.php index 327d13bd..7851b933 100644 --- a/tests/Unit/Services/Finance/FinancialSummaryServiceTest.php +++ b/tests/Unit/Services/Finance/FinancialSummaryServiceTest.php @@ -163,7 +163,7 @@ class FinancialSummaryServiceTest extends TestCase ], ]); - $service = new FinancialSummaryService(new FinancialDonationRecipientService); + $service = new FinancialSummaryService(new FinancialDonationRecipientService()); $summary = $service->getSummary('2025-01-01', '2025-12-31', '2025-2026'); $this->assertSame(120.0, $summary['totalCharges']); diff --git a/tests/Unit/Services/Finance/FinancialUnpaidParentsServiceTest.php b/tests/Unit/Services/Finance/FinancialUnpaidParentsServiceTest.php index 715d1bdf..1e841527 100644 --- a/tests/Unit/Services/Finance/FinancialUnpaidParentsServiceTest.php +++ b/tests/Unit/Services/Finance/FinancialUnpaidParentsServiceTest.php @@ -99,7 +99,7 @@ class FinancialUnpaidParentsServiceTest extends TestCase 'status' => 'pending', ]); - $service = new FinancialUnpaidParentsService(new FinancialSchoolYearService); + $service = new FinancialUnpaidParentsService(new FinancialSchoolYearService()); $result = $service->getUnpaidParents('2025-2026'); $this->assertSame('2025-2026', $result['schoolYear']); diff --git a/tests/Unit/Services/Frontend/ContactSubmissionServiceTest.php b/tests/Unit/Services/Frontend/ContactSubmissionServiceTest.php index b815531e..28256680 100644 --- a/tests/Unit/Services/Frontend/ContactSubmissionServiceTest.php +++ b/tests/Unit/Services/Frontend/ContactSubmissionServiceTest.php @@ -23,7 +23,7 @@ class ContactSubmissionServiceTest extends TestCase $emailService = $this->createMock(EmailDispatchService::class); $emailService->method('send')->willReturn(false); - $service = new ContactSubmissionService($emailService, new GlobalConfigService); + $service = new ContactSubmissionService($emailService, new GlobalConfigService()); $result = $service->submit([ 'email' => 'contact@example.com', diff --git a/tests/Unit/Services/Frontend/FrontendPageServiceTest.php b/tests/Unit/Services/Frontend/FrontendPageServiceTest.php index bad9ed18..b31d94fe 100644 --- a/tests/Unit/Services/Frontend/FrontendPageServiceTest.php +++ b/tests/Unit/Services/Frontend/FrontendPageServiceTest.php @@ -9,7 +9,7 @@ class FrontendPageServiceTest extends TestCase { public function test_page_returns_payload(): void { - $service = new FrontendPageService; + $service = new FrontendPageService(); $payload = $service->page('index'); $this->assertSame('index', $payload['page']); diff --git a/tests/Unit/Services/Frontend/StaticPageServiceTest.php b/tests/Unit/Services/Frontend/StaticPageServiceTest.php index 2ab401ae..0ac31223 100644 --- a/tests/Unit/Services/Frontend/StaticPageServiceTest.php +++ b/tests/Unit/Services/Frontend/StaticPageServiceTest.php @@ -12,9 +12,9 @@ class StaticPageServiceTest extends TestCase { $dir = public_path('html'); File::ensureDirectoryExists($dir); - File::put($dir.'/terms_of_service.html', '

Terms

'); + File::put($dir . '/terms_of_service.html', '

Terms

'); - $service = new StaticPageService; + $service = new StaticPageService(); $result = $service->load('terms_of_service.html', 'terms_of_service'); $this->assertTrue($result['ok']); @@ -24,7 +24,7 @@ class StaticPageServiceTest extends TestCase public function test_load_missing_returns_error(): void { - $service = new StaticPageService; + $service = new StaticPageService(); $result = $service->load('missing.html', 'missing'); $this->assertFalse($result['ok']); diff --git a/tests/Unit/Services/Grading/GradingBelowSixtyServiceTest.php b/tests/Unit/Services/Grading/GradingBelowSixtyServiceTest.php index 06ebf76d..1981b4f8 100644 --- a/tests/Unit/Services/Grading/GradingBelowSixtyServiceTest.php +++ b/tests/Unit/Services/Grading/GradingBelowSixtyServiceTest.php @@ -45,7 +45,7 @@ class GradingBelowSixtyServiceTest extends TestCase 'semester_score' => 55, ]); - $service = new GradingBelowSixtyService; + $service = new GradingBelowSixtyService(); $rows = $service->listRows('2025-2026', 'Fall'); $this->assertCount(1, $rows); diff --git a/tests/Unit/Services/Grading/GradingLockServiceTest.php b/tests/Unit/Services/Grading/GradingLockServiceTest.php index e7b96b30..2cf59bb0 100644 --- a/tests/Unit/Services/Grading/GradingLockServiceTest.php +++ b/tests/Unit/Services/Grading/GradingLockServiceTest.php @@ -22,7 +22,7 @@ class GradingLockServiceTest extends TestCase 'school_year' => '2025-2026', ]); - $service = new GradingLockService; + $service = new GradingLockService(); $locked = $service->toggle(1, 'Fall', '2025-2026', 99); $this->assertTrue($locked); @@ -55,7 +55,7 @@ class GradingLockServiceTest extends TestCase ], ]); - $service = new GradingLockService; + $service = new GradingLockService(); $count = $service->lockAll('Fall', '2025-2026', 99); $this->assertSame(2, $count); diff --git a/tests/Unit/Services/Grading/HomeworkTrackingServiceTest.php b/tests/Unit/Services/Grading/HomeworkTrackingServiceTest.php index 43a094c0..88ffb099 100644 --- a/tests/Unit/Services/Grading/HomeworkTrackingServiceTest.php +++ b/tests/Unit/Services/Grading/HomeworkTrackingServiceTest.php @@ -72,7 +72,7 @@ class HomeworkTrackingServiceTest extends TestCase 'updated_at' => now(), ]); - $service = new HomeworkTrackingService(new HomeworkTrackingCalendarService); + $service = new HomeworkTrackingService(new HomeworkTrackingCalendarService()); $result = $service->report('Fall', '2025-2026', 1); $this->assertNotEmpty($result['teachers']); diff --git a/tests/Unit/Services/Incidents/IncidentAnalysisServiceTest.php b/tests/Unit/Services/Incidents/IncidentAnalysisServiceTest.php index 44ab9d71..36f98db5 100644 --- a/tests/Unit/Services/Incidents/IncidentAnalysisServiceTest.php +++ b/tests/Unit/Services/Incidents/IncidentAnalysisServiceTest.php @@ -48,7 +48,7 @@ class IncidentAnalysisServiceTest extends TestCase ], ]); - $service = new IncidentAnalysisService(new IncidentLookupService); + $service = new IncidentAnalysisService(new IncidentLookupService()); $result = $service->analyze('2025-2026', 'Fall'); $this->assertCount(1, $result); diff --git a/tests/Unit/Services/Incidents/IncidentHistoryServiceTest.php b/tests/Unit/Services/Incidents/IncidentHistoryServiceTest.php index 6b0f3f40..f4a0e8dd 100644 --- a/tests/Unit/Services/Incidents/IncidentHistoryServiceTest.php +++ b/tests/Unit/Services/Incidents/IncidentHistoryServiceTest.php @@ -57,7 +57,7 @@ class IncidentHistoryServiceTest extends TestCase 'school_year' => '2025-2026', ]); - $service = new IncidentHistoryService(new IncidentLookupService); + $service = new IncidentHistoryService(new IncidentLookupService()); $rows = $service->processed('2025-2026', 'Fall'); $this->assertSame('5A', $rows[0]['grade']); diff --git a/tests/Unit/Services/Incidents/IncidentLookupServiceTest.php b/tests/Unit/Services/Incidents/IncidentLookupServiceTest.php index f76ebfd5..20332c33 100644 --- a/tests/Unit/Services/Incidents/IncidentLookupServiceTest.php +++ b/tests/Unit/Services/Incidents/IncidentLookupServiceTest.php @@ -42,7 +42,7 @@ class IncidentLookupServiceTest extends TestCase 'school_year' => '2025-2026', ]); - $service = new IncidentLookupService; + $service = new IncidentLookupService(); $grades = $service->gradeOptionsWithActiveStudents('2025-2026'); $this->assertSame([['id' => 10, 'name' => '1A']], $grades); @@ -71,7 +71,7 @@ class IncidentLookupServiceTest extends TestCase 'school_year' => '2025-2026', ]); - $service = new IncidentLookupService; + $service = new IncidentLookupService(); $map = $service->updaterNameMap([1]); $this->assertSame('Jane Doe', $map[1]); diff --git a/tests/Unit/Services/Inventory/InventoryCategoryServiceTest.php b/tests/Unit/Services/Inventory/InventoryCategoryServiceTest.php index 9da498da..3c6fad94 100644 --- a/tests/Unit/Services/Inventory/InventoryCategoryServiceTest.php +++ b/tests/Unit/Services/Inventory/InventoryCategoryServiceTest.php @@ -13,7 +13,7 @@ class InventoryCategoryServiceTest extends TestCase public function test_create_category(): void { - $service = new InventoryCategoryService; + $service = new InventoryCategoryService(); $result = $service->create([ 'type' => 'office', 'name' => 'Supplies', @@ -32,7 +32,7 @@ class InventoryCategoryServiceTest extends TestCase 'updated_at' => now(), ]); - $service = new InventoryCategoryService; + $service = new InventoryCategoryService(); $result = $service->update($categoryId, ['name' => 'Updated']); $this->assertTrue($result['ok']); diff --git a/tests/Unit/Services/Inventory/InventoryItemServiceTest.php b/tests/Unit/Services/Inventory/InventoryItemServiceTest.php index 83b45b45..56e063d1 100644 --- a/tests/Unit/Services/Inventory/InventoryItemServiceTest.php +++ b/tests/Unit/Services/Inventory/InventoryItemServiceTest.php @@ -17,7 +17,7 @@ class InventoryItemServiceTest extends TestCase { $this->seedConfig(); $categoryId = $this->seedCategory(); - $service = new InventoryItemService(new InventoryContextService, new InventoryMovementService(new InventoryContextService)); + $service = new InventoryItemService(new InventoryContextService(), new InventoryMovementService(new InventoryContextService())); $result = $service->create([ 'type' => 'classroom', @@ -36,7 +36,7 @@ class InventoryItemServiceTest extends TestCase $this->seedConfig(); $categoryId = $this->seedCategory(); $itemId = $this->seedItem($categoryId); - $service = new InventoryItemService(new InventoryContextService, new InventoryMovementService(new InventoryContextService)); + $service = new InventoryItemService(new InventoryContextService(), new InventoryMovementService(new InventoryContextService())); $result = $service->update($itemId, ['name' => 'Updated'], 1); diff --git a/tests/Unit/Services/Inventory/InventoryMovementServiceTest.php b/tests/Unit/Services/Inventory/InventoryMovementServiceTest.php index 35a9299c..f5b30e49 100644 --- a/tests/Unit/Services/Inventory/InventoryMovementServiceTest.php +++ b/tests/Unit/Services/Inventory/InventoryMovementServiceTest.php @@ -16,7 +16,7 @@ class InventoryMovementServiceTest extends TestCase { $this->seedConfig(); $itemId = $this->seedItem(); - $service = new InventoryMovementService(new InventoryContextService); + $service = new InventoryMovementService(new InventoryContextService()); $result = $service->create([ 'item_id' => $itemId, diff --git a/tests/Unit/Services/Inventory/InventorySummaryServiceTest.php b/tests/Unit/Services/Inventory/InventorySummaryServiceTest.php index b02ef8dd..8b1f7dfe 100644 --- a/tests/Unit/Services/Inventory/InventorySummaryServiceTest.php +++ b/tests/Unit/Services/Inventory/InventorySummaryServiceTest.php @@ -17,7 +17,7 @@ class InventorySummaryServiceTest extends TestCase $this->seedConfig(); $this->seedItem(); - $service = new InventorySummaryService(new InventoryContextService); + $service = new InventorySummaryService(new InventoryContextService()); $result = $service->summary('classroom', '2025-2026'); $this->assertNotEmpty($result['items']); diff --git a/tests/Unit/Services/Inventory/InventoryTeacherDistributionServiceTest.php b/tests/Unit/Services/Inventory/InventoryTeacherDistributionServiceTest.php index dc1187f2..ebe74742 100644 --- a/tests/Unit/Services/Inventory/InventoryTeacherDistributionServiceTest.php +++ b/tests/Unit/Services/Inventory/InventoryTeacherDistributionServiceTest.php @@ -19,8 +19,8 @@ class InventoryTeacherDistributionServiceTest extends TestCase $userId = $this->seedUser(); $service = new InventoryTeacherDistributionService( - new InventoryContextService, - new InventoryMovementService(new InventoryContextService) + new InventoryContextService(), + new InventoryMovementService(new InventoryContextService()) ); $result = $service->formData($userId, null, null); @@ -41,8 +41,8 @@ class InventoryTeacherDistributionServiceTest extends TestCase $bookId = $this->seedItem($categoryId); $service = new InventoryTeacherDistributionService( - new InventoryContextService, - new InventoryMovementService(new InventoryContextService) + new InventoryContextService(), + new InventoryMovementService(new InventoryContextService()) ); $result = $service->distribute($userId, [ diff --git a/tests/Unit/Services/Inventory/SupplierServiceTest.php b/tests/Unit/Services/Inventory/SupplierServiceTest.php index 598617d4..a3192461 100644 --- a/tests/Unit/Services/Inventory/SupplierServiceTest.php +++ b/tests/Unit/Services/Inventory/SupplierServiceTest.php @@ -13,7 +13,7 @@ class SupplierServiceTest extends TestCase public function test_create_supplier(): void { - $service = new SupplierService; + $service = new SupplierService(); $result = $service->create([ 'name' => 'Supplier A', 'email' => 'a@example.com', @@ -31,7 +31,7 @@ class SupplierServiceTest extends TestCase 'updated_at' => now(), ]); - $service = new SupplierService; + $service = new SupplierService(); $result = $service->update($id, ['name' => 'Supplier Updated']); $this->assertTrue($result['ok']); diff --git a/tests/Unit/Services/Inventory/SupplyCategoryServiceTest.php b/tests/Unit/Services/Inventory/SupplyCategoryServiceTest.php index 392d021d..738b810d 100644 --- a/tests/Unit/Services/Inventory/SupplyCategoryServiceTest.php +++ b/tests/Unit/Services/Inventory/SupplyCategoryServiceTest.php @@ -13,7 +13,7 @@ class SupplyCategoryServiceTest extends TestCase public function test_create_category(): void { - $service = new SupplyCategoryService; + $service = new SupplyCategoryService(); $result = $service->create(['name' => 'Paper']); $this->assertTrue($result['ok']); @@ -28,7 +28,7 @@ class SupplyCategoryServiceTest extends TestCase 'updated_at' => now(), ]); - $service = new SupplyCategoryService; + $service = new SupplyCategoryService(); $result = $service->update($id, ['name' => 'Pens Updated']); $this->assertTrue($result['ok']); diff --git a/tests/Unit/Services/Invoices/InvoiceConfigServiceTest.php b/tests/Unit/Services/Invoices/InvoiceConfigServiceTest.php index e1d26dc9..4e5631f8 100644 --- a/tests/Unit/Services/Invoices/InvoiceConfigServiceTest.php +++ b/tests/Unit/Services/Invoices/InvoiceConfigServiceTest.php @@ -24,7 +24,7 @@ class InvoiceConfigServiceTest extends TestCase ['config_key' => 'refund_deadline', 'config_value' => '2025-10-01'], ]); - $service = new InvoiceConfigService; + $service = new InvoiceConfigService(); $this->assertSame('2025-2026', $service->getSchoolYear()); $this->assertSame('Fall', $service->getSemester()); diff --git a/tests/Unit/Services/Invoices/InvoiceGenerationServiceTest.php b/tests/Unit/Services/Invoices/InvoiceGenerationServiceTest.php index f62e1f77..cfc9dad0 100644 --- a/tests/Unit/Services/Invoices/InvoiceGenerationServiceTest.php +++ b/tests/Unit/Services/Invoices/InvoiceGenerationServiceTest.php @@ -77,7 +77,7 @@ class InvoiceGenerationServiceTest extends TestCase 'school_year' => '2025-2026', ]); - $config = new InvoiceConfigService; + $config = new InvoiceConfigService(); $grades = new InvoiceGradeService($config->getGradeFee()); $tuition = new InvoiceTuitionService( $grades, diff --git a/tests/Unit/Services/Invoices/InvoiceManagementServiceTest.php b/tests/Unit/Services/Invoices/InvoiceManagementServiceTest.php index ce37da82..65029f1a 100644 --- a/tests/Unit/Services/Invoices/InvoiceManagementServiceTest.php +++ b/tests/Unit/Services/Invoices/InvoiceManagementServiceTest.php @@ -107,7 +107,7 @@ class InvoiceManagementServiceTest extends TestCase 'refund_paid_amount' => 5, ]); - $service = new InvoiceManagementService(new InvoiceConfigService); + $service = new InvoiceManagementService(new InvoiceConfigService()); $data = $service->getManagementData('2025-2026'); $this->assertSame('2025-2026', $data['schoolYear']); diff --git a/tests/Unit/Services/Invoices/InvoicePaymentServiceTest.php b/tests/Unit/Services/Invoices/InvoicePaymentServiceTest.php index cc678175..801c7160 100644 --- a/tests/Unit/Services/Invoices/InvoicePaymentServiceTest.php +++ b/tests/Unit/Services/Invoices/InvoicePaymentServiceTest.php @@ -53,7 +53,7 @@ class InvoicePaymentServiceTest extends TestCase 'refund_paid_amount' => 5, ]); - $service = new InvoicePaymentService(new InvoiceConfigService); + $service = new InvoicePaymentService(new InvoiceConfigService()); $data = $service->getParentInvoiceSummary(10, '2025-2026'); $this->assertSame('2025-2026', $data['selectedYear']); diff --git a/tests/Unit/Services/Invoices/InvoicePdfServiceTest.php b/tests/Unit/Services/Invoices/InvoicePdfServiceTest.php index 43a55950..2d7f2657 100644 --- a/tests/Unit/Services/Invoices/InvoicePdfServiceTest.php +++ b/tests/Unit/Services/Invoices/InvoicePdfServiceTest.php @@ -89,7 +89,7 @@ class InvoicePdfServiceTest extends TestCase 'school_year' => '2025-2026', ]); - $config = new InvoiceConfigService; + $config = new InvoiceConfigService(); $grades = new InvoiceGradeService($config->getGradeFee()); $service = new InvoicePdfService($config, $grades); diff --git a/tests/Unit/Services/Messaging/MessageCommandServiceTest.php b/tests/Unit/Services/Messaging/MessageCommandServiceTest.php index 15f15c19..85e8f6e1 100644 --- a/tests/Unit/Services/Messaging/MessageCommandServiceTest.php +++ b/tests/Unit/Services/Messaging/MessageCommandServiceTest.php @@ -23,7 +23,7 @@ class MessageCommandServiceTest extends TestCase $sender = $this->createUser('sender@example.com'); $recipient = $this->createUser('recipient@example.com'); - $service = new MessageCommandService(new GlobalConfigService); + $service = new MessageCommandService(new GlobalConfigService()); $message = $service->create($sender->id, [ 'recipient_id' => $recipient->id, 'subject' => 'Subject', @@ -41,7 +41,7 @@ class MessageCommandServiceTest extends TestCase { $sender = $this->createUser('sender2@example.com'); - $service = new MessageCommandService(new GlobalConfigService); + $service = new MessageCommandService(new GlobalConfigService()); $this->expectException(\RuntimeException::class); $service->create($sender->id, [ diff --git a/tests/Unit/Services/Messaging/MessageQueryServiceTest.php b/tests/Unit/Services/Messaging/MessageQueryServiceTest.php index c4d5bc19..33677cef 100644 --- a/tests/Unit/Services/Messaging/MessageQueryServiceTest.php +++ b/tests/Unit/Services/Messaging/MessageQueryServiceTest.php @@ -31,7 +31,7 @@ class MessageQueryServiceTest extends TestCase 'school_year' => '2025-2026', ]); - $service = new MessageQueryService; + $service = new MessageQueryService(); $paginator = $service->inbox($recipient->id, [], 1, 20); $this->assertSame(1, $paginator->total()); diff --git a/tests/Unit/Services/Messaging/MessageRecipientServiceTest.php b/tests/Unit/Services/Messaging/MessageRecipientServiceTest.php index 6b206534..6019804a 100644 --- a/tests/Unit/Services/Messaging/MessageRecipientServiceTest.php +++ b/tests/Unit/Services/Messaging/MessageRecipientServiceTest.php @@ -28,7 +28,7 @@ class MessageRecipientServiceTest extends TestCase 'updated_at' => now(), ]); - $service = new MessageRecipientService; + $service = new MessageRecipientService(); $rows = $service->teachers(); $this->assertSame($teacher->id, $rows[0]['id']); @@ -82,7 +82,7 @@ class MessageRecipientServiceTest extends TestCase 'school_year' => '2025-2026', ]); - $service = new MessageRecipientService; + $service = new MessageRecipientService(); $rows = $service->parents(); $ids = array_column($rows, 'id'); diff --git a/tests/Unit/Services/Navigation/NavBuilderServiceTest.php b/tests/Unit/Services/Navigation/NavBuilderServiceTest.php index 476e4b39..26f6bb7a 100644 --- a/tests/Unit/Services/Navigation/NavBuilderServiceTest.php +++ b/tests/Unit/Services/Navigation/NavBuilderServiceTest.php @@ -2,8 +2,8 @@ namespace Tests\Unit\Services\Navigation; -use App\Services\Navigation\NavbarService; use App\Services\Navigation\NavBuilderService; +use App\Services\Navigation\NavbarService; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\DB; use Tests\TestCase; @@ -19,7 +19,7 @@ class NavBuilderServiceTest extends TestCase 'priority' => 1, ]); - $service = new NavBuilderService(new NavbarService); + $service = new NavBuilderService(new NavbarService()); $result = $service->save([ 'label' => 'Dashboard', 'url' => 'dashboard', @@ -45,7 +45,7 @@ class NavBuilderServiceTest extends TestCase 'is_enabled' => 1, ]); - $service = new NavBuilderService(new NavbarService); + $service = new NavBuilderService(new NavbarService()); $service->reorder([$id => 5]); $this->assertDatabaseHas('nav_items', [ diff --git a/tests/Unit/Services/Navigation/NavbarServiceTest.php b/tests/Unit/Services/Navigation/NavbarServiceTest.php index 8ec5ec1c..525122aa 100644 --- a/tests/Unit/Services/Navigation/NavbarServiceTest.php +++ b/tests/Unit/Services/Navigation/NavbarServiceTest.php @@ -38,7 +38,7 @@ class NavbarServiceTest extends TestCase ['role_id' => $roleId, 'nav_item_id' => $childId], ]); - $service = new NavbarService; + $service = new NavbarService(); $menu = $service->getMenuForRoles(['admin']); $this->assertCount(1, $menu); diff --git a/tests/Unit/Services/Notifications/NotificationActiveServiceTest.php b/tests/Unit/Services/Notifications/NotificationActiveServiceTest.php index 670fbec3..c3ca62e0 100644 --- a/tests/Unit/Services/Notifications/NotificationActiveServiceTest.php +++ b/tests/Unit/Services/Notifications/NotificationActiveServiceTest.php @@ -38,7 +38,7 @@ class NotificationActiveServiceTest extends TestCase ], ]); - $service = new NotificationActiveService; + $service = new NotificationActiveService(); $result = $service->list('parent'); $this->assertCount(1, $result['notifications']); diff --git a/tests/Unit/Services/Notifications/NotificationCleanupServiceTest.php b/tests/Unit/Services/Notifications/NotificationCleanupServiceTest.php index 056670e5..82878abe 100644 --- a/tests/Unit/Services/Notifications/NotificationCleanupServiceTest.php +++ b/tests/Unit/Services/Notifications/NotificationCleanupServiceTest.php @@ -42,7 +42,7 @@ class NotificationCleanupServiceTest extends TestCase ], ]); - $service = new NotificationCleanupService; + $service = new NotificationCleanupService(); $deleted = $service->cleanupExpired(); $this->assertSame(1, $deleted); diff --git a/tests/Unit/Services/Notifications/NotificationDeletedServiceTest.php b/tests/Unit/Services/Notifications/NotificationDeletedServiceTest.php index ecd537db..c54b3481 100644 --- a/tests/Unit/Services/Notifications/NotificationDeletedServiceTest.php +++ b/tests/Unit/Services/Notifications/NotificationDeletedServiceTest.php @@ -24,7 +24,7 @@ class NotificationDeletedServiceTest extends TestCase 'updated_at' => now(), ]); - $service = new NotificationDeletedService; + $service = new NotificationDeletedService(); $result = $service->list(); $this->assertCount(1, $result['notifications']); diff --git a/tests/Unit/Services/Notifications/NotificationDispatchServiceTest.php b/tests/Unit/Services/Notifications/NotificationDispatchServiceTest.php index 4eb945a2..b3dba708 100644 --- a/tests/Unit/Services/Notifications/NotificationDispatchServiceTest.php +++ b/tests/Unit/Services/Notifications/NotificationDispatchServiceTest.php @@ -12,7 +12,7 @@ class NotificationDispatchServiceTest extends TestCase { Log::spy(); - $service = new NotificationDispatchService; + $service = new NotificationDispatchService(); $service->sendEmail('parent@example.com', 'Subject', 'Message'); $service->sendSms('5551112222', 'Message'); diff --git a/tests/Unit/Services/Notifications/NotificationManagementServiceTest.php b/tests/Unit/Services/Notifications/NotificationManagementServiceTest.php index 70abbfb9..f3da6090 100644 --- a/tests/Unit/Services/Notifications/NotificationManagementServiceTest.php +++ b/tests/Unit/Services/Notifications/NotificationManagementServiceTest.php @@ -23,7 +23,7 @@ class NotificationManagementServiceTest extends TestCase 'updated_at' => now(), ]); - $service = new NotificationManagementService; + $service = new NotificationManagementService(); $result = $service->update(1, ['title' => 'New']); $this->assertTrue($result['ok']); @@ -35,7 +35,7 @@ class NotificationManagementServiceTest extends TestCase public function test_update_returns_false_when_missing(): void { - $service = new NotificationManagementService; + $service = new NotificationManagementService(); $result = $service->update(999, ['title' => 'New']); $this->assertFalse($result['ok']); @@ -53,7 +53,7 @@ class NotificationManagementServiceTest extends TestCase 'updated_at' => now(), ]); - $service = new NotificationManagementService; + $service = new NotificationManagementService(); $this->assertTrue($service->delete(1)); $this->assertSoftDeleted('notifications', ['id' => 1]); diff --git a/tests/Unit/Services/Notifications/NotificationReadServiceTest.php b/tests/Unit/Services/Notifications/NotificationReadServiceTest.php index de459ad5..5506cbaf 100644 --- a/tests/Unit/Services/Notifications/NotificationReadServiceTest.php +++ b/tests/Unit/Services/Notifications/NotificationReadServiceTest.php @@ -19,7 +19,7 @@ class NotificationReadServiceTest extends TestCase 'is_read' => 0, ]); - $service = new NotificationReadService; + $service = new NotificationReadService(); $result = $service->markRead(1, 1); $this->assertTrue($result); @@ -32,7 +32,7 @@ class NotificationReadServiceTest extends TestCase public function test_mark_read_returns_false_when_missing(): void { - $service = new NotificationReadService; + $service = new NotificationReadService(); $this->assertFalse($service->markRead(1, 99)); } diff --git a/tests/Unit/Services/Notifications/NotificationRecipientServiceTest.php b/tests/Unit/Services/Notifications/NotificationRecipientServiceTest.php index 7e94c4a0..338d4e2b 100644 --- a/tests/Unit/Services/Notifications/NotificationRecipientServiceTest.php +++ b/tests/Unit/Services/Notifications/NotificationRecipientServiceTest.php @@ -46,7 +46,7 @@ class NotificationRecipientServiceTest extends TestCase 'role_id' => 1, ]); - $service = new NotificationRecipientService; + $service = new NotificationRecipientService(); $rows = $service->getRecipients('parent'); $this->assertCount(1, $rows); diff --git a/tests/Unit/Services/Notifications/NotificationShowServiceTest.php b/tests/Unit/Services/Notifications/NotificationShowServiceTest.php index a28194a2..38650730 100644 --- a/tests/Unit/Services/Notifications/NotificationShowServiceTest.php +++ b/tests/Unit/Services/Notifications/NotificationShowServiceTest.php @@ -30,7 +30,7 @@ class NotificationShowServiceTest extends TestCase 'is_read' => 0, ]); - $service = new NotificationShowService; + $service = new NotificationShowService(); $row = $service->getForUser(1, 1); $this->assertNotNull($row); @@ -39,7 +39,7 @@ class NotificationShowServiceTest extends TestCase public function test_get_for_user_returns_null_when_missing(): void { - $service = new NotificationShowService; + $service = new NotificationShowService(); $row = $service->getForUser(1, 99); $this->assertNull($row); diff --git a/tests/Unit/Services/Notifications/NotificationUserListServiceTest.php b/tests/Unit/Services/Notifications/NotificationUserListServiceTest.php index 14e3491e..b3709f3b 100644 --- a/tests/Unit/Services/Notifications/NotificationUserListServiceTest.php +++ b/tests/Unit/Services/Notifications/NotificationUserListServiceTest.php @@ -49,7 +49,7 @@ class NotificationUserListServiceTest extends TestCase ], ]); - $service = new NotificationUserListService; + $service = new NotificationUserListService(); $result = $service->listForUser(1, [ 'read' => true, 'per_page' => 10, diff --git a/tests/Unit/Services/Notifications/UserNotificationDispatchServiceTest.php b/tests/Unit/Services/Notifications/UserNotificationDispatchServiceTest.php index d59d9b1c..5b024cbb 100644 --- a/tests/Unit/Services/Notifications/UserNotificationDispatchServiceTest.php +++ b/tests/Unit/Services/Notifications/UserNotificationDispatchServiceTest.php @@ -34,7 +34,7 @@ class UserNotificationDispatchServiceTest extends TestCase 'school_year' => '2025-2026', ]); - $service = new UserNotificationDispatchService; + $service = new UserNotificationDispatchService(); $result = $service->notifyUser(1, 'Title', 'Message', ['in_app'], 'registration', 'parent'); $this->assertTrue($result['ok']); @@ -50,7 +50,7 @@ class UserNotificationDispatchServiceTest extends TestCase public function test_notify_user_rejects_invalid_user(): void { - $service = new UserNotificationDispatchService; + $service = new UserNotificationDispatchService(); $result = $service->notifyUser(0, 'Title', 'Message'); $this->assertFalse($result['ok']); diff --git a/tests/Unit/Services/Parents/ParentAttendanceReportServiceTest.php b/tests/Unit/Services/Parents/ParentAttendanceReportServiceTest.php index 326e42d0..3321c784 100644 --- a/tests/Unit/Services/Parents/ParentAttendanceReportServiceTest.php +++ b/tests/Unit/Services/Parents/ParentAttendanceReportServiceTest.php @@ -25,9 +25,9 @@ class ParentAttendanceReportServiceTest extends TestCase $this->seedStudent($parentId); $service = new ParentAttendanceReportService( - new ParentConfigService, - new ParentAttendanceReportCalendarService(new ParentConfigService), - new SemesterRangeService(new SemesterConfigService) + new ParentConfigService(), + new ParentAttendanceReportCalendarService(new ParentConfigService()), + new SemesterRangeService(new SemesterConfigService()) ); $data = $service->formData($parentId); diff --git a/tests/Unit/Services/Parents/ParentAttendanceServiceTest.php b/tests/Unit/Services/Parents/ParentAttendanceServiceTest.php index b9dd229e..7ed2a824 100644 --- a/tests/Unit/Services/Parents/ParentAttendanceServiceTest.php +++ b/tests/Unit/Services/Parents/ParentAttendanceServiceTest.php @@ -2,6 +2,7 @@ namespace Tests\Unit\Services\Parents; +use App\Models\User; use App\Services\Parents\ParentAttendanceService; use App\Services\Parents\ParentConfigService; use Illuminate\Foundation\Testing\RefreshDatabase; @@ -63,7 +64,7 @@ class ParentAttendanceServiceTest extends TestCase 'school_year' => '2025-2026', ]); - $service = new ParentAttendanceService(new ParentConfigService); + $service = new ParentAttendanceService(new ParentConfigService()); $result = $service->listAttendance($parentId, '2025-2026'); $this->assertSame('2025-2026', $result['selectedYear']); diff --git a/tests/Unit/Services/Parents/ParentConfigServiceTest.php b/tests/Unit/Services/Parents/ParentConfigServiceTest.php index 839eeda0..3bf0c70d 100644 --- a/tests/Unit/Services/Parents/ParentConfigServiceTest.php +++ b/tests/Unit/Services/Parents/ParentConfigServiceTest.php @@ -24,7 +24,7 @@ class ParentConfigServiceTest extends TestCase ['config_key' => 'fall_semester_start', 'config_value' => '2025-09-05'], ]); - $service = new ParentConfigService; + $service = new ParentConfigService(); $context = $service->context(); $this->assertSame('2025-2026', $context['school_year']); diff --git a/tests/Unit/Services/Parents/ParentEmergencyContactServiceTest.php b/tests/Unit/Services/Parents/ParentEmergencyContactServiceTest.php index bf662c44..c6bf969c 100644 --- a/tests/Unit/Services/Parents/ParentEmergencyContactServiceTest.php +++ b/tests/Unit/Services/Parents/ParentEmergencyContactServiceTest.php @@ -18,7 +18,7 @@ class ParentEmergencyContactServiceTest extends TestCase $this->seedConfig(); $parentId = $this->seedParent(); - $service = new ParentEmergencyContactService(new ParentConfigService, new PhoneFormatterService); + $service = new ParentEmergencyContactService(new ParentConfigService(), new PhoneFormatterService()); $contact = $service->store($parentId, [ 'name' => 'Sam Contact', 'cellphone' => '1234567890', diff --git a/tests/Unit/Services/Parents/ParentEnrollmentServiceTest.php b/tests/Unit/Services/Parents/ParentEnrollmentServiceTest.php index 07a467f0..83b0aa1c 100644 --- a/tests/Unit/Services/Parents/ParentEnrollmentServiceTest.php +++ b/tests/Unit/Services/Parents/ParentEnrollmentServiceTest.php @@ -42,7 +42,7 @@ class ParentEnrollmentServiceTest extends TestCase 'is_withdrawn' => 0, ]); - $service = new ParentEnrollmentService(new ParentConfigService); + $service = new ParentEnrollmentService(new ParentConfigService()); $result = $service->overview($parentId, '2025-2026'); $this->assertSame('2025-2026', $result['selectedYear']); diff --git a/tests/Unit/Services/Parents/ParentInvoiceServiceTest.php b/tests/Unit/Services/Parents/ParentInvoiceServiceTest.php index 59961f00..510a736c 100644 --- a/tests/Unit/Services/Parents/ParentInvoiceServiceTest.php +++ b/tests/Unit/Services/Parents/ParentInvoiceServiceTest.php @@ -34,7 +34,7 @@ class ParentInvoiceServiceTest extends TestCase 'issue_date' => '2025-09-02', ]); - $service = new ParentInvoiceService; + $service = new ParentInvoiceService(); $rows = $service->listInvoices($parentId); $this->assertCount(1, $rows); diff --git a/tests/Unit/Services/Parents/ParentRegistrationServiceTest.php b/tests/Unit/Services/Parents/ParentRegistrationServiceTest.php index a34842e4..fbb9eecd 100644 --- a/tests/Unit/Services/Parents/ParentRegistrationServiceTest.php +++ b/tests/Unit/Services/Parents/ParentRegistrationServiceTest.php @@ -18,7 +18,7 @@ class ParentRegistrationServiceTest extends TestCase $this->seedConfig(); $parentId = $this->seedParent(); - $service = new ParentRegistrationService(new ParentConfigService, new SchoolIdService); + $service = new ParentRegistrationService(new ParentConfigService(), new SchoolIdService()); $result = $service->register($parentId, [ [ 'firstname' => 'Omar', diff --git a/tests/Unit/Services/Payments/PaymentBalanceServiceTest.php b/tests/Unit/Services/Payments/PaymentBalanceServiceTest.php index 87e71d1b..5fa4e6d7 100644 --- a/tests/Unit/Services/Payments/PaymentBalanceServiceTest.php +++ b/tests/Unit/Services/Payments/PaymentBalanceServiceTest.php @@ -27,7 +27,7 @@ class PaymentBalanceServiceTest extends TestCase 'status' => 'Pending', ]); - $service = new PaymentBalanceService; + $service = new PaymentBalanceService(); $updated = $service->updateBalance(1, 40); $this->assertTrue($updated); diff --git a/tests/Unit/Services/Payments/PaymentEnrollmentEventServiceTest.php b/tests/Unit/Services/Payments/PaymentEnrollmentEventServiceTest.php index 12b0890c..4225103d 100644 --- a/tests/Unit/Services/Payments/PaymentEnrollmentEventServiceTest.php +++ b/tests/Unit/Services/Payments/PaymentEnrollmentEventServiceTest.php @@ -14,7 +14,7 @@ class PaymentEnrollmentEventServiceTest extends TestCase private function makeService(): PaymentEnrollmentEventService { - return new PaymentEnrollmentEventService(new ApplicationUrlService); + return new PaymentEnrollmentEventService(new ApplicationUrlService()); } public function test_build_event_data_requires_parent(): void diff --git a/tests/Unit/Services/Payments/PaymentLookupServiceTest.php b/tests/Unit/Services/Payments/PaymentLookupServiceTest.php index 03afbc4b..a47e0fd1 100644 --- a/tests/Unit/Services/Payments/PaymentLookupServiceTest.php +++ b/tests/Unit/Services/Payments/PaymentLookupServiceTest.php @@ -41,7 +41,7 @@ class PaymentLookupServiceTest extends TestCase 'status' => 'Pending', ]); - $service = new PaymentLookupService; + $service = new PaymentLookupService(); $results = $service->getByParent(10, '2025-2026'); $this->assertCount(1, $results); diff --git a/tests/Unit/Services/Payments/PaymentNotificationDispatchServiceTest.php b/tests/Unit/Services/Payments/PaymentNotificationDispatchServiceTest.php index 07345f48..74b9c2ba 100644 --- a/tests/Unit/Services/Payments/PaymentNotificationDispatchServiceTest.php +++ b/tests/Unit/Services/Payments/PaymentNotificationDispatchServiceTest.php @@ -18,7 +18,7 @@ class PaymentNotificationDispatchServiceTest extends TestCase ['id' => 2002, 'config_key' => 'semester', 'config_value' => 'Fall'], ]); - $service = new PaymentNotificationDispatchService; + $service = new PaymentNotificationDispatchService(); $service->notifyUser(10, 'Payment Reminder', 'Reminder body', ['in_app']); $this->assertDatabaseHas('notifications', [ diff --git a/tests/Unit/Services/Payments/PaymentPlanServiceTest.php b/tests/Unit/Services/Payments/PaymentPlanServiceTest.php index 489dcd70..119ffa30 100644 --- a/tests/Unit/Services/Payments/PaymentPlanServiceTest.php +++ b/tests/Unit/Services/Payments/PaymentPlanServiceTest.php @@ -53,7 +53,7 @@ class PaymentPlanServiceTest extends TestCase 'semester' => 'Fall', ]); - $service = new PaymentPlanService; + $service = new PaymentPlanService(); $payment = $service->createPlan([ 'parent_id' => 10, 'invoice_id' => 1, diff --git a/tests/Unit/Services/Payments/PaymentTransactionServiceTest.php b/tests/Unit/Services/Payments/PaymentTransactionServiceTest.php index 9551e94d..aaf648ea 100644 --- a/tests/Unit/Services/Payments/PaymentTransactionServiceTest.php +++ b/tests/Unit/Services/Payments/PaymentTransactionServiceTest.php @@ -3,8 +3,8 @@ namespace Tests\Unit\Services\Payments; use App\Services\Payments\PaymentTransactionService; -use Illuminate\Database\Schema\Blueprint; use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Schema; use Tests\TestCase; @@ -15,13 +15,13 @@ class PaymentTransactionServiceTest extends TestCase public function test_create_and_update_transaction(): void { - if (! Schema::hasColumn('payment_transactions', 'payment_reference')) { + if (!Schema::hasColumn('payment_transactions', 'payment_reference')) { Schema::table('payment_transactions', function (Blueprint $table) { $table->string('payment_reference')->nullable(); }); } - if (! Schema::hasColumn('payment_transactions', 'is_full_payment')) { + if (!Schema::hasColumn('payment_transactions', 'is_full_payment')) { Schema::table('payment_transactions', function (Blueprint $table) { $table->boolean('is_full_payment')->default(0); }); @@ -41,7 +41,7 @@ class PaymentTransactionServiceTest extends TestCase 'status' => 'Pending', ]); - $service = new PaymentTransactionService; + $service = new PaymentTransactionService(); $transaction = $service->create([ 'transaction_id' => 'TXN-1', 'payment_id' => 1, diff --git a/tests/Unit/Services/Payments/PaypalPaymentServiceTest.php b/tests/Unit/Services/Payments/PaypalPaymentServiceTest.php index a52fd31c..a7a93778 100644 --- a/tests/Unit/Services/Payments/PaypalPaymentServiceTest.php +++ b/tests/Unit/Services/Payments/PaypalPaymentServiceTest.php @@ -14,7 +14,7 @@ class PaypalPaymentServiceTest extends TestCase private function makeService(): PaypalPaymentService { - return new PaypalPaymentService(new ApplicationUrlService); + return new PaypalPaymentService(new ApplicationUrlService()); } public function test_create_payment_falls_back_to_hosted_when_sdk_missing(): void diff --git a/tests/Unit/Services/Payments/PaypalTransactionsServiceTest.php b/tests/Unit/Services/Payments/PaypalTransactionsServiceTest.php index 9457959c..2e2759fb 100644 --- a/tests/Unit/Services/Payments/PaypalTransactionsServiceTest.php +++ b/tests/Unit/Services/Payments/PaypalTransactionsServiceTest.php @@ -37,7 +37,7 @@ class PaypalTransactionsServiceTest extends TestCase 'created_at' => '2025-01-02 00:00:00', ]); - $service = new PaypalTransactionsService; + $service = new PaypalTransactionsService(); $results = $service->listAll('ABC'); $this->assertCount(1, $results); diff --git a/tests/Unit/Services/Policy/PolicyContentServiceTest.php b/tests/Unit/Services/Policy/PolicyContentServiceTest.php index 702c1266..1c12b85a 100644 --- a/tests/Unit/Services/Policy/PolicyContentServiceTest.php +++ b/tests/Unit/Services/Policy/PolicyContentServiceTest.php @@ -12,7 +12,7 @@ class PolicyContentServiceTest extends TestCase public function test_get_policy_returns_content(): void { - $service = new PolicyContentService; + $service = new PolicyContentService(); $result = $service->getPolicy('school'); $this->assertSame('school', $result['type']); diff --git a/tests/Unit/Services/Promotions/LevelProgressionServiceTest.php b/tests/Unit/Services/Promotions/LevelProgressionServiceTest.php index 58e1dc0b..f20daddd 100644 --- a/tests/Unit/Services/Promotions/LevelProgressionServiceTest.php +++ b/tests/Unit/Services/Promotions/LevelProgressionServiceTest.php @@ -14,7 +14,7 @@ class LevelProgressionServiceTest extends TestCase public function test_seed_defaults_creates_baseline_levels(): void { - $service = new LevelProgressionService; + $service = new LevelProgressionService(); $created = $service->seedDefaults(); $this->assertGreaterThan(0, $created); $this->assertSame($created, LevelProgression::query()->count()); @@ -35,7 +35,7 @@ class LevelProgressionServiceTest extends TestCase ['id' => 8, 'class_name' => 'Grade 4', 'school_year' => '2025-2026', 'semester' => 'Fall', 'capacity' => 0], ]); - $service = new LevelProgressionService; + $service = new LevelProgressionService(); $service->seedDefaults(); $resolved = $service->resolveByCurrentClassId(7); @@ -53,7 +53,7 @@ class LevelProgressionServiceTest extends TestCase ['id' => 101, 'class_name' => 'Custom B', 'school_year' => '2025-2026', 'semester' => 'Fall', 'capacity' => 0], ]); - $service = new LevelProgressionService; + $service = new LevelProgressionService(); $resolved = $service->resolveByCurrentClassId(100); $this->assertNotNull($resolved); @@ -64,7 +64,7 @@ class LevelProgressionServiceTest extends TestCase public function test_upsert_mapping_updates_existing_row(): void { - $service = new LevelProgressionService; + $service = new LevelProgressionService(); $service->upsertMapping([ 'current_level_name' => 'Grade 5', 'next_level_name' => 'Grade 6', diff --git a/tests/Unit/Services/Promotions/PromotionEligibilityServiceTest.php b/tests/Unit/Services/Promotions/PromotionEligibilityServiceTest.php index 3f155996..3d34fb9c 100644 --- a/tests/Unit/Services/Promotions/PromotionEligibilityServiceTest.php +++ b/tests/Unit/Services/Promotions/PromotionEligibilityServiceTest.php @@ -2,7 +2,6 @@ namespace Tests\Unit\Services\Promotions; -use App\Models\LevelProgression; use App\Models\StudentPromotionRecord; use App\Services\Promotions\LevelProgressionService; use App\Services\Promotions\PromotionAuditService; @@ -76,7 +75,7 @@ class PromotionEligibilityServiceTest extends TestCase $this->seedDecision($studentId, '2025-2026', 90.0, 'promoted'); // Mark Youth as terminal - LevelProgression::query()->where('current_level_name', 'Youth')->update(['is_terminal' => 1]); + \App\Models\LevelProgression::query()->where('current_level_name', 'Youth')->update(['is_terminal' => 1]); $service = $this->makeService(); $record = $service->evaluateStudent($studentId, '2025-2026', 1); @@ -109,10 +108,9 @@ class PromotionEligibilityServiceTest extends TestCase private function makeService(): PromotionEligibilityService { - $progression = new LevelProgressionService; - $audit = new PromotionAuditService; + $progression = new LevelProgressionService(); + $audit = new PromotionAuditService(); $status = new PromotionStatusService($audit); - return new PromotionEligibilityService($progression, $status, $audit); } @@ -175,4 +173,4 @@ class PromotionEligibilityServiceTest extends TestCase 'updated_at' => now(), ]); } -} +} \ No newline at end of file diff --git a/tests/Unit/Services/Promotions/PromotionEnrollmentServiceTest.php b/tests/Unit/Services/Promotions/PromotionEnrollmentServiceTest.php index 27c627c8..efa98d9c 100644 --- a/tests/Unit/Services/Promotions/PromotionEnrollmentServiceTest.php +++ b/tests/Unit/Services/Promotions/PromotionEnrollmentServiceTest.php @@ -100,7 +100,7 @@ class PromotionEnrollmentServiceTest extends TestCase */ private function bootstrap(): array { - $audit = new PromotionAuditService; + $audit = new PromotionAuditService(); $service = new PromotionEnrollmentService(new PromotionStatusService($audit), $audit); $parentId = (int) DB::table('users')->insertGetId([ diff --git a/tests/Unit/Services/Promotions/PromotionStatusServiceTest.php b/tests/Unit/Services/Promotions/PromotionStatusServiceTest.php index b21db466..c4e157f6 100644 --- a/tests/Unit/Services/Promotions/PromotionStatusServiceTest.php +++ b/tests/Unit/Services/Promotions/PromotionStatusServiceTest.php @@ -82,7 +82,7 @@ class PromotionStatusServiceTest extends TestCase private function makeService(): PromotionStatusService { - return new PromotionStatusService(new PromotionAuditService); + return new PromotionStatusService(new PromotionAuditService()); } private function makeRecord(string $status): StudentPromotionRecord diff --git a/tests/Unit/Services/PurchaseOrders/PurchaseOrderCreateServiceTest.php b/tests/Unit/Services/PurchaseOrders/PurchaseOrderCreateServiceTest.php index adc7a63d..332dd0ab 100644 --- a/tests/Unit/Services/PurchaseOrders/PurchaseOrderCreateServiceTest.php +++ b/tests/Unit/Services/PurchaseOrders/PurchaseOrderCreateServiceTest.php @@ -16,7 +16,7 @@ class PurchaseOrderCreateServiceTest extends TestCase $supplierId = DB::table('suppliers')->insertGetId(['name' => 'Alpha Supplies']); $supplyId = DB::table('supplies')->insertGetId(['name' => 'Paper', 'unit' => 'box', 'qty_on_hand' => 0]); - $service = new PurchaseOrderCreateService; + $service = new PurchaseOrderCreateService(); $po = $service->create([ 'po_number' => 'PO-500', 'supplier_id' => $supplierId, diff --git a/tests/Unit/Services/PurchaseOrders/PurchaseOrderReceiveServiceTest.php b/tests/Unit/Services/PurchaseOrders/PurchaseOrderReceiveServiceTest.php index 8cce2db6..f560dd72 100644 --- a/tests/Unit/Services/PurchaseOrders/PurchaseOrderReceiveServiceTest.php +++ b/tests/Unit/Services/PurchaseOrders/PurchaseOrderReceiveServiceTest.php @@ -39,7 +39,7 @@ class PurchaseOrderReceiveServiceTest extends TestCase 'unit_cost' => 3, ]); - $service = new PurchaseOrderReceiveService; + $service = new PurchaseOrderReceiveService(); $result = $service->receive($po->id, [ ['id' => $item->id, 'quantity' => 3], ], 'tester@example.com'); diff --git a/tests/Unit/Services/PurchaseOrders/PurchaseOrderStatusServiceTest.php b/tests/Unit/Services/PurchaseOrders/PurchaseOrderStatusServiceTest.php index 33e0e63c..37d0532c 100644 --- a/tests/Unit/Services/PurchaseOrders/PurchaseOrderStatusServiceTest.php +++ b/tests/Unit/Services/PurchaseOrders/PurchaseOrderStatusServiceTest.php @@ -22,7 +22,7 @@ class PurchaseOrderStatusServiceTest extends TestCase 'total' => 0, ]); - $service = new PurchaseOrderStatusService; + $service = new PurchaseOrderStatusService(); $updated = $service->cancel($po->id); $this->assertSame(PurchaseOrder::STATUS_CANCELED, $updated->status); diff --git a/tests/Unit/Services/Refunds/RefundDecisionServiceTest.php b/tests/Unit/Services/Refunds/RefundDecisionServiceTest.php index 5842e1ec..298f6054 100644 --- a/tests/Unit/Services/Refunds/RefundDecisionServiceTest.php +++ b/tests/Unit/Services/Refunds/RefundDecisionServiceTest.php @@ -26,7 +26,7 @@ class RefundDecisionServiceTest extends TestCase 'updated_at' => now(), ]); - $service = new RefundDecisionService; + $service = new RefundDecisionService(); $result = $service->approve(1, 2); $this->assertTrue($result['ok']); diff --git a/tests/Unit/Services/Refunds/RefundNotificationServiceTest.php b/tests/Unit/Services/Refunds/RefundNotificationServiceTest.php index ef0a8957..4e687932 100644 --- a/tests/Unit/Services/Refunds/RefundNotificationServiceTest.php +++ b/tests/Unit/Services/Refunds/RefundNotificationServiceTest.php @@ -15,7 +15,7 @@ class RefundNotificationServiceTest extends TestCase { Log::spy(); - $service = new RefundNotificationService; + $service = new RefundNotificationService(); $service->notifyPending(10, 50.25, 'https://example.test/login'); Log::shouldHaveReceived('info')->once(); diff --git a/tests/Unit/Services/Refunds/RefundOverpaymentServiceTest.php b/tests/Unit/Services/Refunds/RefundOverpaymentServiceTest.php index c6ee418f..7b648b67 100644 --- a/tests/Unit/Services/Refunds/RefundOverpaymentServiceTest.php +++ b/tests/Unit/Services/Refunds/RefundOverpaymentServiceTest.php @@ -45,7 +45,7 @@ class RefundOverpaymentServiceTest extends TestCase 'status' => 'Paid', ]); - $service = new RefundOverpaymentService(new RefundNotificationService); + $service = new RefundOverpaymentService(new RefundNotificationService()); $result = $service->recalculate(false, null, 1); $this->assertNotEmpty($result['created_ids']); diff --git a/tests/Unit/Services/Refunds/RefundPayoutServiceTest.php b/tests/Unit/Services/Refunds/RefundPayoutServiceTest.php index 2ad40c0f..5c5cf9cf 100644 --- a/tests/Unit/Services/Refunds/RefundPayoutServiceTest.php +++ b/tests/Unit/Services/Refunds/RefundPayoutServiceTest.php @@ -27,7 +27,7 @@ class RefundPayoutServiceTest extends TestCase 'updated_at' => now(), ]); - $service = new RefundPayoutService; + $service = new RefundPayoutService(); $result = $service->recordPayment(1, [ 'paid_amount' => 40, 'payment_method' => 'Cash', diff --git a/tests/Unit/Services/Refunds/RefundPolicyServiceTest.php b/tests/Unit/Services/Refunds/RefundPolicyServiceTest.php index 6d4ecd1a..593e1c63 100644 --- a/tests/Unit/Services/Refunds/RefundPolicyServiceTest.php +++ b/tests/Unit/Services/Refunds/RefundPolicyServiceTest.php @@ -2,10 +2,10 @@ namespace Tests\Unit\Services\Refunds; -use App\Services\Discounts\DiscountInvoiceService; -use App\Services\Fees\FeeRefundDetailService; use App\Services\Refunds\RefundInvoiceAdjustmentService; use App\Services\Refunds\RefundPolicyService; +use App\Services\Fees\FeeRefundDetailService; +use App\Services\Discounts\DiscountInvoiceService; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\DB; use Tests\TestCase; @@ -84,8 +84,8 @@ class RefundPolicyServiceTest extends TestCase ]); $service = new RefundPolicyService( - new FeeRefundDetailService, - new RefundInvoiceAdjustmentService(new DiscountInvoiceService) + new FeeRefundDetailService(), + new RefundInvoiceAdjustmentService(new DiscountInvoiceService()) ); $result = $service->processWithdrawalRefund(10, 1, '2025-2026', 'Fall', 1); diff --git a/tests/Unit/Services/Refunds/RefundQueryServiceTest.php b/tests/Unit/Services/Refunds/RefundQueryServiceTest.php index 90de4355..3e144468 100644 --- a/tests/Unit/Services/Refunds/RefundQueryServiceTest.php +++ b/tests/Unit/Services/Refunds/RefundQueryServiceTest.php @@ -40,7 +40,7 @@ class RefundQueryServiceTest extends TestCase ], ]); - $service = new RefundQueryService; + $service = new RefundQueryService(); $result = $service->list(['status' => 'Approved', 'per_page' => 10, 'page' => 1]); $this->assertSame(1, $result['pagination']['total']); diff --git a/tests/Unit/Services/Refunds/RefundRequestServiceTest.php b/tests/Unit/Services/Refunds/RefundRequestServiceTest.php index 12f97147..4ec216a9 100644 --- a/tests/Unit/Services/Refunds/RefundRequestServiceTest.php +++ b/tests/Unit/Services/Refunds/RefundRequestServiceTest.php @@ -2,13 +2,13 @@ namespace Tests\Unit\Services\Refunds; -use App\Services\Discounts\DiscountInvoiceService; use App\Services\Fees\FeeRefundDetailService; use App\Services\Refunds\RefundInvoiceAdjustmentService; use App\Services\Refunds\RefundNotificationService; use App\Services\Refunds\RefundPolicyService; use App\Services\Refunds\RefundRequestService; use App\Services\Refunds\RefundSummaryService; +use App\Services\Discounts\DiscountInvoiceService; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\DB; use Tests\TestCase; @@ -60,11 +60,11 @@ class RefundRequestServiceTest extends TestCase $service = new RefundRequestService( new RefundPolicyService( - new FeeRefundDetailService, - new RefundInvoiceAdjustmentService(new DiscountInvoiceService) + new FeeRefundDetailService(), + new RefundInvoiceAdjustmentService(new DiscountInvoiceService()) ), - new RefundSummaryService, - new RefundNotificationService + new RefundSummaryService(), + new RefundNotificationService() ); $result = $service->requestRefund([ diff --git a/tests/Unit/Services/Refunds/RefundSummaryServiceTest.php b/tests/Unit/Services/Refunds/RefundSummaryServiceTest.php index 582c9dd2..04cfb10b 100644 --- a/tests/Unit/Services/Refunds/RefundSummaryServiceTest.php +++ b/tests/Unit/Services/Refunds/RefundSummaryServiceTest.php @@ -53,7 +53,7 @@ class RefundSummaryServiceTest extends TestCase 'updated_at' => now(), ]); - $service = new RefundSummaryService; + $service = new RefundSummaryService(); $summary = $service->getParentFinancialSummary(10, '2025-2026', 'Fall'); $this->assertSame(100.0, $summary['total_invoiced']); diff --git a/tests/Unit/Services/Reimbursements/ReimbursementBatchAssignmentServiceTest.php b/tests/Unit/Services/Reimbursements/ReimbursementBatchAssignmentServiceTest.php index c3bb951f..7d4e2ee5 100644 --- a/tests/Unit/Services/Reimbursements/ReimbursementBatchAssignmentServiceTest.php +++ b/tests/Unit/Services/Reimbursements/ReimbursementBatchAssignmentServiceTest.php @@ -54,7 +54,7 @@ class ReimbursementBatchAssignmentServiceTest extends TestCase 'status' => 'approved', ]); - $service = new ReimbursementBatchAssignmentService(new ReimbursementLookupService); + $service = new ReimbursementBatchAssignmentService(new ReimbursementLookupService()); $result = $service->updateAssignment(100, 10, 1, null, '2025-2026', 'Fall'); $this->assertSame(10, $result['batch_id']); diff --git a/tests/Unit/Services/Reimbursements/ReimbursementBatchServiceTest.php b/tests/Unit/Services/Reimbursements/ReimbursementBatchServiceTest.php index aea494f5..632d2bed 100644 --- a/tests/Unit/Services/Reimbursements/ReimbursementBatchServiceTest.php +++ b/tests/Unit/Services/Reimbursements/ReimbursementBatchServiceTest.php @@ -13,7 +13,7 @@ class ReimbursementBatchServiceTest extends TestCase public function test_create_batch_assigns_sequence_and_title(): void { - $service = new ReimbursementBatchService(new ReimbursementLookupService); + $service = new ReimbursementBatchService(new ReimbursementLookupService()); $batch = $service->createBatch(null, 1, '2025-2026', 'Fall'); diff --git a/tests/Unit/Services/Reimbursements/ReimbursementDonationServiceTest.php b/tests/Unit/Services/Reimbursements/ReimbursementDonationServiceTest.php index 14136f1c..a6a1cbcc 100644 --- a/tests/Unit/Services/Reimbursements/ReimbursementDonationServiceTest.php +++ b/tests/Unit/Services/Reimbursements/ReimbursementDonationServiceTest.php @@ -59,7 +59,7 @@ class ReimbursementDonationServiceTest extends TestCase 'assigned_at' => now(), ]); - $service = new ReimbursementDonationService; + $service = new ReimbursementDonationService(); $service->markDonation(200, 1); $this->assertDatabaseHas('expenses', [ diff --git a/tests/Unit/Services/Reimbursements/ReimbursementRecipientServiceTest.php b/tests/Unit/Services/Reimbursements/ReimbursementRecipientServiceTest.php index b33e3140..a9665b59 100644 --- a/tests/Unit/Services/Reimbursements/ReimbursementRecipientServiceTest.php +++ b/tests/Unit/Services/Reimbursements/ReimbursementRecipientServiceTest.php @@ -66,7 +66,7 @@ class ReimbursementRecipientServiceTest extends TestCase ['user_id' => 2, 'role_id' => 2], ]); - $service = new ReimbursementRecipientService; + $service = new ReimbursementRecipientService(); $recipients = $service->recipientOptions(); $ids = array_map(static fn ($row) => (int) ($row['id'] ?? 0), $recipients); diff --git a/tests/Unit/Services/Reports/ReportCards/ReportCardServiceTest.php b/tests/Unit/Services/Reports/ReportCards/ReportCardServiceTest.php index d87395b4..b5f9d6d1 100644 --- a/tests/Unit/Services/Reports/ReportCards/ReportCardServiceTest.php +++ b/tests/Unit/Services/Reports/ReportCards/ReportCardServiceTest.php @@ -16,7 +16,7 @@ class ReportCardServiceTest extends TestCase $this->seedConfig(); $this->seedReportCardData(); - $service = new ReportCardService; + $service = new ReportCardService(); $result = $service->meta('2025-2026', 'Fall'); $this->assertNotEmpty($result['schoolYears']); @@ -30,7 +30,7 @@ class ReportCardServiceTest extends TestCase $this->seedConfig(); $data = $this->seedReportCardData(); - $service = new ReportCardService; + $service = new ReportCardService(); $result = $service->completeness('2025-2026', 'Fall', $data['class_section_id']); $this->assertTrue($result['ok']); @@ -44,7 +44,7 @@ class ReportCardServiceTest extends TestCase $data = $this->seedReportCardData(); $this->seedAcknowledgement($data['student_id']); - $service = new ReportCardService; + $service = new ReportCardService(); $result = $service->acknowledgement($data['student_id'], '2025-2026', 'Fall'); $this->assertSame('Parent One', $result['signed_name']); @@ -55,7 +55,7 @@ class ReportCardServiceTest extends TestCase $this->seedConfig(); $data = $this->seedReportCardData(); - $service = new ReportCardService; + $service = new ReportCardService(); $result = $service->generateSingleReport($data['student_id'], [ 'school_year' => '2025-2026', 'semester' => 'Fall', @@ -71,7 +71,7 @@ class ReportCardServiceTest extends TestCase $this->seedConfig(); $studentId = $this->seedStudentOnly(); - $service = new ReportCardService; + $service = new ReportCardService(); $result = $service->generateSingleReport($studentId, [ 'school_year' => '2025-2026', 'semester' => 'Fall', diff --git a/tests/Unit/Services/Reports/SlipPrinterFormatterServiceTest.php b/tests/Unit/Services/Reports/SlipPrinterFormatterServiceTest.php index 6690d156..f15deeb5 100644 --- a/tests/Unit/Services/Reports/SlipPrinterFormatterServiceTest.php +++ b/tests/Unit/Services/Reports/SlipPrinterFormatterServiceTest.php @@ -9,13 +9,13 @@ class SlipPrinterFormatterServiceTest extends TestCase { public function test_to_db_date_parses_us_format(): void { - $service = new SlipPrinterFormatterService; + $service = new SlipPrinterFormatterService(); $this->assertSame('2025-09-01', $service->toDbDate('09/01/2025')); } public function test_to_db_time_parses_string(): void { - $service = new SlipPrinterFormatterService; + $service = new SlipPrinterFormatterService(); $this->assertSame('08:15:00', $service->toDbTime('8:15 AM')); } } diff --git a/tests/Unit/Services/Reports/Stickers/StickerPresetServiceTest.php b/tests/Unit/Services/Reports/Stickers/StickerPresetServiceTest.php index 041e5e0b..f1aeaa7c 100644 --- a/tests/Unit/Services/Reports/Stickers/StickerPresetServiceTest.php +++ b/tests/Unit/Services/Reports/Stickers/StickerPresetServiceTest.php @@ -9,7 +9,7 @@ class StickerPresetServiceTest extends TestCase { public function test_presets_include_alias_fields(): void { - $service = new StickerPresetService; + $service = new StickerPresetService(); $presets = $service->presets(); $this->assertNotEmpty($presets); diff --git a/tests/Unit/Services/Reports/Stickers/StickerPrintServiceTest.php b/tests/Unit/Services/Reports/Stickers/StickerPrintServiceTest.php index 1a6b5096..ac3acb0b 100644 --- a/tests/Unit/Services/Reports/Stickers/StickerPrintServiceTest.php +++ b/tests/Unit/Services/Reports/Stickers/StickerPrintServiceTest.php @@ -16,7 +16,7 @@ class StickerPrintServiceTest extends TestCase public function test_generate_returns_pdf_content(): void { $this->seedData(); - $service = new StickerPrintService(new StickerQueryService(new StickerContextService)); + $service = new StickerPrintService(new StickerQueryService(new StickerContextService())); $result = $service->generate([ 'student_id' => 100, @@ -30,7 +30,7 @@ class StickerPrintServiceTest extends TestCase public function test_generate_returns_error_when_no_students(): void { - $service = new StickerPrintService(new StickerQueryService(new StickerContextService)); + $service = new StickerPrintService(new StickerQueryService(new StickerContextService())); $result = $service->generate([ 'class_section_id' => 999, diff --git a/tests/Unit/Services/Reports/Stickers/StickerQueryServiceTest.php b/tests/Unit/Services/Reports/Stickers/StickerQueryServiceTest.php index 446a532c..95c949fb 100644 --- a/tests/Unit/Services/Reports/Stickers/StickerQueryServiceTest.php +++ b/tests/Unit/Services/Reports/Stickers/StickerQueryServiceTest.php @@ -15,7 +15,7 @@ class StickerQueryServiceTest extends TestCase public function test_list_students_by_class_returns_rows(): void { $this->seedData(); - $service = new StickerQueryService(new StickerContextService); + $service = new StickerQueryService(new StickerContextService()); $students = $service->listStudentsByClass(200, '2025-2026'); @@ -28,7 +28,7 @@ class StickerQueryServiceTest extends TestCase $this->seedData(); $this->seedYouthClass(); - $service = new StickerQueryService(new StickerContextService); + $service = new StickerQueryService(new StickerContextService()); $students = $service->listStudentsForPrintAll('2025-2026'); $this->assertCount(1, $students); diff --git a/tests/Unit/Services/Roles/PermissionCrudServiceTest.php b/tests/Unit/Services/Roles/PermissionCrudServiceTest.php index 34ae85a6..d56462a7 100644 --- a/tests/Unit/Services/Roles/PermissionCrudServiceTest.php +++ b/tests/Unit/Services/Roles/PermissionCrudServiceTest.php @@ -13,7 +13,7 @@ class PermissionCrudServiceTest extends TestCase public function test_create_and_update_permission(): void { - $service = new PermissionCrudService; + $service = new PermissionCrudService(); $permission = $service->create([ 'name' => 'manage_roles', 'description' => 'Manage roles', diff --git a/tests/Unit/Services/Roles/RoleAssignmentServiceTest.php b/tests/Unit/Services/Roles/RoleAssignmentServiceTest.php index efc01a41..e826162d 100644 --- a/tests/Unit/Services/Roles/RoleAssignmentServiceTest.php +++ b/tests/Unit/Services/Roles/RoleAssignmentServiceTest.php @@ -2,6 +2,7 @@ namespace Tests\Unit\Services\Roles; +use App\Models\User; use App\Services\Roles\RoleAssignmentService; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\DB; @@ -41,7 +42,7 @@ class RoleAssignmentServiceTest extends TestCase 'updated_at' => now(), ]); - $service = new RoleAssignmentService(new RoleStaffService); + $service = new RoleAssignmentService(new RoleStaffService()); $result = $service->assignRoles($userId, [$roleId], $userId); $this->assertTrue($result['ok']); diff --git a/tests/Unit/Services/Roles/RoleCrudServiceTest.php b/tests/Unit/Services/Roles/RoleCrudServiceTest.php index 551bc851..ad2db6da 100644 --- a/tests/Unit/Services/Roles/RoleCrudServiceTest.php +++ b/tests/Unit/Services/Roles/RoleCrudServiceTest.php @@ -13,7 +13,7 @@ class RoleCrudServiceTest extends TestCase public function test_create_and_update_role(): void { - $service = new RoleCrudService; + $service = new RoleCrudService(); $role = $service->create([ 'name' => 'admin', 'slug' => 'admin', diff --git a/tests/Unit/Services/Roles/RoleDashboardServiceTest.php b/tests/Unit/Services/Roles/RoleDashboardServiceTest.php index c9d0af5d..95933bf9 100644 --- a/tests/Unit/Services/Roles/RoleDashboardServiceTest.php +++ b/tests/Unit/Services/Roles/RoleDashboardServiceTest.php @@ -24,7 +24,7 @@ class RoleDashboardServiceTest extends TestCase 'updated_at' => now(), ]); - $service = new RoleDashboardService; + $service = new RoleDashboardService(); $route = $service->bestDashboardRouteFor(['admin']); $this->assertSame('administrator/administratordashboard', $route); diff --git a/tests/Unit/Services/Roles/RolePermissionServiceTest.php b/tests/Unit/Services/Roles/RolePermissionServiceTest.php index d8a45412..0be8754a 100644 --- a/tests/Unit/Services/Roles/RolePermissionServiceTest.php +++ b/tests/Unit/Services/Roles/RolePermissionServiceTest.php @@ -30,7 +30,7 @@ class RolePermissionServiceTest extends TestCase 'updated_at' => now(), ]); - $service = new RolePermissionService; + $service = new RolePermissionService(); $service->saveRolePermissions($roleId, [ $permissionId => ['create' => true, 'read' => true], ]); diff --git a/tests/Unit/Services/Roles/RoleQueryServiceTest.php b/tests/Unit/Services/Roles/RoleQueryServiceTest.php index ed556dcf..299387a6 100644 --- a/tests/Unit/Services/Roles/RoleQueryServiceTest.php +++ b/tests/Unit/Services/Roles/RoleQueryServiceTest.php @@ -24,7 +24,7 @@ class RoleQueryServiceTest extends TestCase 'updated_at' => now(), ]); - $service = new RoleQueryService; + $service = new RoleQueryService(); $page = $service->listRoles([]); $this->assertCount(1, $page->items()); @@ -65,7 +65,7 @@ class RoleQueryServiceTest extends TestCase 'role_id' => $roleId, ]); - $service = new RoleQueryService; + $service = new RoleQueryService(); $page = $service->listUsersWithRoles([]); $this->assertCount(1, $page->items()); diff --git a/tests/Unit/Services/Roles/RoleSwitchServiceTest.php b/tests/Unit/Services/Roles/RoleSwitchServiceTest.php index 5cef9855..09632972 100644 --- a/tests/Unit/Services/Roles/RoleSwitchServiceTest.php +++ b/tests/Unit/Services/Roles/RoleSwitchServiceTest.php @@ -47,7 +47,7 @@ class RoleSwitchServiceTest extends TestCase 'role_id' => $roleId, ]); - $service = new RoleSwitchService(new RoleDashboardService); + $service = new RoleSwitchService(new RoleDashboardService()); $roles = $service->getUserRoleNames($userId); $this->assertSame(['admin'], $roles); diff --git a/tests/Unit/Services/School/SemesterSelectionServiceTest.php b/tests/Unit/Services/School/SemesterSelectionServiceTest.php index a255f83d..9f997568 100644 --- a/tests/Unit/Services/School/SemesterSelectionServiceTest.php +++ b/tests/Unit/Services/School/SemesterSelectionServiceTest.php @@ -13,7 +13,7 @@ class SemesterSelectionServiceTest extends TestCase public function test_selected_teacher_semester_prefers_session_value(): void { - $service = new SemesterSelectionService; + $service = new SemesterSelectionService(); $this->assertSame('Fall', $service->selectedTeacherSemester()); } @@ -24,7 +24,7 @@ class SemesterSelectionServiceTest extends TestCase ['config_key' => 'semester', 'config_value' => 'Fall'], ]); - $service = new SemesterSelectionService; + $service = new SemesterSelectionService(); $this->assertSame('Fall', $service->selectedTeacherSemester()); } diff --git a/tests/Unit/Services/SchoolIds/SchoolIdGenerationServiceTest.php b/tests/Unit/Services/SchoolIds/SchoolIdGenerationServiceTest.php index 99a21044..4823f673 100644 --- a/tests/Unit/Services/SchoolIds/SchoolIdGenerationServiceTest.php +++ b/tests/Unit/Services/SchoolIds/SchoolIdGenerationServiceTest.php @@ -16,7 +16,7 @@ class SchoolIdGenerationServiceTest extends TestCase $schoolId = $service->generateStudentId(); $year = date('y'); - $this->assertStringStartsWith('STU'.$year, $schoolId); + $this->assertStringStartsWith('STU' . $year, $schoolId); $this->assertSame(10, strlen($schoolId)); } diff --git a/tests/Unit/Services/Scores/AttendanceCalculatorTest.php b/tests/Unit/Services/Scores/AttendanceCalculatorTest.php index f40d1558..9ae89c1e 100644 --- a/tests/Unit/Services/Scores/AttendanceCalculatorTest.php +++ b/tests/Unit/Services/Scores/AttendanceCalculatorTest.php @@ -32,7 +32,7 @@ class AttendanceCalculatorTest extends TestCase 'school_year' => '2024-2025', ]); - $service = new AttendanceCalculator(new AttendanceRecord, new Configuration, new CalendarEvent); + $service = new AttendanceCalculator(new AttendanceRecord(), new Configuration(), new CalendarEvent()); $result = $service->calculate(10, 'Fall', '2024-2025'); $this->assertSame(90.0, $result['attendance_score']); diff --git a/tests/Unit/Services/Scores/ExamScoreServiceTest.php b/tests/Unit/Services/Scores/ExamScoreServiceTest.php index 50f4d27d..991e58a1 100644 --- a/tests/Unit/Services/Scores/ExamScoreServiceTest.php +++ b/tests/Unit/Services/Scores/ExamScoreServiceTest.php @@ -38,7 +38,7 @@ class ExamScoreServiceTest extends TestCase ], ]); - $service = new ExamScoreService(new ScoreTermService); + $service = new ExamScoreService(new ScoreTermService()); $result = $service->list('midterm_exam', 3, 'Fall', '2025-2026'); $this->assertCount(1, $result['students']); @@ -50,7 +50,7 @@ class ExamScoreServiceTest extends TestCase $this->seedStudent(1, 'SCH1', 'Leo', 'Park'); $this->seedStudentClass(1, 3, '2025-2026'); - $service = new ExamScoreService(new ScoreTermService); + $service = new ExamScoreService(new ScoreTermService()); $count = $service->update( 'midterm_exam', diff --git a/tests/Unit/Services/Scores/HomeworkCalculatorTest.php b/tests/Unit/Services/Scores/HomeworkCalculatorTest.php index fef915b5..cb47983f 100644 --- a/tests/Unit/Services/Scores/HomeworkCalculatorTest.php +++ b/tests/Unit/Services/Scores/HomeworkCalculatorTest.php @@ -47,7 +47,7 @@ class HomeworkCalculatorTest extends TestCase ], ]); - $service = new HomeworkCalculator(new Homework); + $service = new HomeworkCalculator(new Homework()); $result = $service->calculate(5, 'Fall', '2024-2025', 1); $this->assertSame(85.0, $result['homework_avg']); diff --git a/tests/Unit/Services/Scores/HomeworkScoreServiceTest.php b/tests/Unit/Services/Scores/HomeworkScoreServiceTest.php index 626ea21d..95bba07d 100644 --- a/tests/Unit/Services/Scores/HomeworkScoreServiceTest.php +++ b/tests/Unit/Services/Scores/HomeworkScoreServiceTest.php @@ -28,7 +28,7 @@ class HomeworkScoreServiceTest extends TestCase 'school_year' => '2025-2026', ]); - $service = new HomeworkScoreService(new ScoreTermService); + $service = new HomeworkScoreService(new ScoreTermService()); $result = $service->list(8, 'Fall', '2025-2026'); $this->assertSame([1], $result['headers']); @@ -40,7 +40,7 @@ class HomeworkScoreServiceTest extends TestCase $this->seedStudent(1, 'SCH1', 'Eli', 'Page'); $this->seedStudentClass(1, 8, '2025-2026'); - $service = new HomeworkScoreService(new ScoreTermService); + $service = new HomeworkScoreService(new ScoreTermService()); $nextIndex = $service->addColumn(8, 'Fall', '2025-2026', 99); diff --git a/tests/Unit/Services/Scores/ParticipationScoreServiceTest.php b/tests/Unit/Services/Scores/ParticipationScoreServiceTest.php index d2b45283..56829a8b 100644 --- a/tests/Unit/Services/Scores/ParticipationScoreServiceTest.php +++ b/tests/Unit/Services/Scores/ParticipationScoreServiceTest.php @@ -28,7 +28,7 @@ class ParticipationScoreServiceTest extends TestCase 'school_year' => '2025-2026', ]); - $service = new ParticipationScoreService(new ScoreTermService); + $service = new ParticipationScoreService(new ScoreTermService()); $result = $service->list(9, 'Fall', '2025-2026'); $this->assertSame(87.0, $result['students'][0]['scores']['score']); @@ -50,7 +50,7 @@ class ParticipationScoreServiceTest extends TestCase 'school_year' => '2025-2026', ]); - $service = new ParticipationScoreService(new ScoreTermService); + $service = new ParticipationScoreService(new ScoreTermService()); $count = $service->update( 9, 'Fall', diff --git a/tests/Unit/Services/Scores/ProjectCalculatorTest.php b/tests/Unit/Services/Scores/ProjectCalculatorTest.php index 1eb2d71d..f1e22a49 100644 --- a/tests/Unit/Services/Scores/ProjectCalculatorTest.php +++ b/tests/Unit/Services/Scores/ProjectCalculatorTest.php @@ -47,7 +47,7 @@ class ProjectCalculatorTest extends TestCase ], ]); - $service = new ProjectCalculator(new Project); + $service = new ProjectCalculator(new Project()); $result = $service->calculate(9, 'Fall', '2024-2025', 1); $this->assertSame(90.0, $result['project_avg']); diff --git a/tests/Unit/Services/Scores/ProjectScoreServiceTest.php b/tests/Unit/Services/Scores/ProjectScoreServiceTest.php index 7c7f11b2..b27c8f7b 100644 --- a/tests/Unit/Services/Scores/ProjectScoreServiceTest.php +++ b/tests/Unit/Services/Scores/ProjectScoreServiceTest.php @@ -28,7 +28,7 @@ class ProjectScoreServiceTest extends TestCase 'school_year' => '2025-2026', ]); - $service = new ProjectScoreService(new ScoreTermService); + $service = new ProjectScoreService(new ScoreTermService()); $result = $service->list(10, 'Fall', '2025-2026'); $this->assertSame([1], $result['headers']); @@ -41,7 +41,7 @@ class ProjectScoreServiceTest extends TestCase $this->seedStudent(1, 'SCH1', 'Maya', 'Stone'); $this->seedStudentClass(1, 10, '2025-2026'); - $service = new ProjectScoreService(new ScoreTermService); + $service = new ProjectScoreService(new ScoreTermService()); $count = $service->update( 10, @@ -73,7 +73,7 @@ class ProjectScoreServiceTest extends TestCase $this->seedStudent(1, 'SCH1', 'Maya', 'Stone'); $this->seedStudentClass(1, 10, '2025-2026'); - $service = new ProjectScoreService(new ScoreTermService); + $service = new ProjectScoreService(new ScoreTermService()); $nextIndex = $service->addColumn(10, 'Fall', '2025-2026', 99); diff --git a/tests/Unit/Services/Scores/QuizCalculatorTest.php b/tests/Unit/Services/Scores/QuizCalculatorTest.php index b53e217d..33dfd0d2 100644 --- a/tests/Unit/Services/Scores/QuizCalculatorTest.php +++ b/tests/Unit/Services/Scores/QuizCalculatorTest.php @@ -56,7 +56,7 @@ class QuizCalculatorTest extends TestCase ], ]); - $service = new QuizCalculator; + $service = new QuizCalculator(); $result = $service->calculate(7, 'Fall', '2024-2025', 2); $this->assertSame(80.0, $result['quiz_avg']); diff --git a/tests/Unit/Services/Scores/QuizScoreServiceTest.php b/tests/Unit/Services/Scores/QuizScoreServiceTest.php index eccdfe42..2be158e8 100644 --- a/tests/Unit/Services/Scores/QuizScoreServiceTest.php +++ b/tests/Unit/Services/Scores/QuizScoreServiceTest.php @@ -28,7 +28,7 @@ class QuizScoreServiceTest extends TestCase 'school_year' => '2025-2026', ]); - $service = new QuizScoreService(new ScoreTermService); + $service = new QuizScoreService(new ScoreTermService()); $result = $service->list(5, 'Fall', '2025-2026'); $this->assertSame([2], $result['headers']); @@ -41,7 +41,7 @@ class QuizScoreServiceTest extends TestCase $this->seedStudent(1, 'SCH1', 'Nina', 'Kim'); $this->seedStudentClass(1, 5, '2025-2026'); - $service = new QuizScoreService(new ScoreTermService); + $service = new QuizScoreService(new ScoreTermService()); $count = $service->update( 5, @@ -73,7 +73,7 @@ class QuizScoreServiceTest extends TestCase $this->seedStudent(1, 'SCH1', 'Nina', 'Kim'); $this->seedStudentClass(1, 5, '2025-2026'); - $service = new QuizScoreService(new ScoreTermService); + $service = new QuizScoreService(new ScoreTermService()); $nextIndex = $service->addColumn(5, 'Fall', '2025-2026', 99); diff --git a/tests/Unit/Services/Scores/ScoreCommentServiceTest.php b/tests/Unit/Services/Scores/ScoreCommentServiceTest.php index 3656e81f..63a13f20 100644 --- a/tests/Unit/Services/Scores/ScoreCommentServiceTest.php +++ b/tests/Unit/Services/Scores/ScoreCommentServiceTest.php @@ -26,7 +26,7 @@ class ScoreCommentServiceTest extends TestCase 'comment' => 'Ava has shown consistent improvement and strong effort across assignments.', ]); - $service = new ScoreCommentService(new ScoreTermService); + $service = new ScoreCommentService(new ScoreTermService()); $result = $service->list('2', 'Fall', '2025-2026'); $this->assertCount(1, $result['students']); @@ -38,7 +38,7 @@ class ScoreCommentServiceTest extends TestCase { $this->seedStudent(1, 'SCH1', 'Ava', 'Lane'); - $service = new ScoreCommentService(new ScoreTermService); + $service = new ScoreCommentService(new ScoreTermService()); $result = $service->save( 2, 'Fall', diff --git a/tests/Unit/Services/Scores/ScoreDashboardServiceTest.php b/tests/Unit/Services/Scores/ScoreDashboardServiceTest.php index 688b098b..bc773bcc 100644 --- a/tests/Unit/Services/Scores/ScoreDashboardServiceTest.php +++ b/tests/Unit/Services/Scores/ScoreDashboardServiceTest.php @@ -39,7 +39,7 @@ class ScoreDashboardServiceTest extends TestCase 'school_year' => '2025-2026', ]); - $service = new ScoreDashboardService(new ScoreTermService); + $service = new ScoreDashboardService(new ScoreTermService()); $result = $service->overview(1, null, 'Fall', '2025-2026'); $this->assertSame(10, $result['class_section_id']); @@ -62,7 +62,7 @@ class ScoreDashboardServiceTest extends TestCase 'school_year' => '2025-2026', ]); - $service = new ScoreDashboardService(new ScoreTermService); + $service = new ScoreDashboardService(new ScoreTermService()); $result = $service->viewStudentScores(7, '2025-2026'); $this->assertArrayHasKey('2025-2026', $result['scores']); diff --git a/tests/Unit/Services/Scores/ScorePredictorServiceTest.php b/tests/Unit/Services/Scores/ScorePredictorServiceTest.php index 7c050d27..fca60523 100644 --- a/tests/Unit/Services/Scores/ScorePredictorServiceTest.php +++ b/tests/Unit/Services/Scores/ScorePredictorServiceTest.php @@ -64,8 +64,8 @@ class ScorePredictorServiceTest extends TestCase ]); $service = new ScorePredictorService( - new ScorePredictorDataService, - new ScorePredictorRiskService + new ScorePredictorDataService(), + new ScorePredictorRiskService() ); $result = $service->report('2025-2026', 1); diff --git a/tests/Unit/Services/Scores/ScoreTermServiceTest.php b/tests/Unit/Services/Scores/ScoreTermServiceTest.php index aa932f92..8c3a6093 100644 --- a/tests/Unit/Services/Scores/ScoreTermServiceTest.php +++ b/tests/Unit/Services/Scores/ScoreTermServiceTest.php @@ -18,7 +18,7 @@ class ScoreTermServiceTest extends TestCase ['config_key' => 'school_year', 'config_value' => '2025-2026'], ]); - $service = new ScoreTermService; + $service = new ScoreTermService(); $this->assertSame('Fall', $service->semesterLabel(null)); $this->assertSame('2025-2026', $service->schoolYear(null)); @@ -26,7 +26,7 @@ class ScoreTermServiceTest extends TestCase public function test_normalize_and_variants(): void { - $service = new ScoreTermService; + $service = new ScoreTermService(); $this->assertSame('fall', $service->normalizeSemester('Semester 1')); $this->assertSame('spring', $service->normalizeSemester('2nd Semester')); diff --git a/tests/Unit/Services/Scores/SemesterScoreServiceTest.php b/tests/Unit/Services/Scores/SemesterScoreServiceTest.php index 39740a91..c699cf76 100644 --- a/tests/Unit/Services/Scores/SemesterScoreServiceTest.php +++ b/tests/Unit/Services/Scores/SemesterScoreServiceTest.php @@ -2,11 +2,11 @@ namespace Tests\Unit\Services\Scores; +use App\Services\Scores\SemesterScoreService; use App\Services\Scores\AttendanceCalculator; use App\Services\Scores\HomeworkCalculator; use App\Services\Scores\ProjectCalculator; use App\Services\Scores\QuizCalculator; -use App\Services\Scores\SemesterScoreService; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\DB; use Tests\TestCase; @@ -28,10 +28,10 @@ class SemesterScoreServiceTest extends TestCase ]); $service = new SemesterScoreService( - new FakeAttendanceCalculator, - new FakeHomeworkCalculator, - new FakeQuizCalculator, - new FakeProjectCalculator + new FakeAttendanceCalculator(), + new FakeHomeworkCalculator(), + new FakeQuizCalculator(), + new FakeProjectCalculator() ); $service->updateStudentScores([ @@ -61,7 +61,9 @@ class SemesterScoreServiceTest extends TestCase class FakeAttendanceCalculator extends AttendanceCalculator { - public function __construct() {} + public function __construct() + { + } public function calculate(int $studentId, string $semester, string $schoolYear, ?int $classSectionId = null): array { @@ -71,7 +73,9 @@ class FakeAttendanceCalculator extends AttendanceCalculator class FakeHomeworkCalculator extends HomeworkCalculator { - public function __construct() {} + public function __construct() + { + } public function calculate(int $studentId, string $semester, string $schoolYear, ?int $classSectionId = null): array { @@ -89,7 +93,9 @@ class FakeQuizCalculator extends QuizCalculator class FakeProjectCalculator extends ProjectCalculator { - public function __construct() {} + public function __construct() + { + } public function calculate(int $studentId, string $semester, string $schoolYear, ?int $classSectionId = null): array { diff --git a/tests/Unit/Services/Security/Pbkdf2HasherTest.php b/tests/Unit/Services/Security/Pbkdf2HasherTest.php index c78f2acb..49b7d223 100644 --- a/tests/Unit/Services/Security/Pbkdf2HasherTest.php +++ b/tests/Unit/Services/Security/Pbkdf2HasherTest.php @@ -9,7 +9,7 @@ class Pbkdf2HasherTest extends TestCase { public function test_hash_and_verify(): void { - $hasher = new Pbkdf2Hasher; + $hasher = new Pbkdf2Hasher(); $hash = $hasher->hash('secret'); $this->assertTrue($hasher->verify('secret', $hash)); diff --git a/tests/Unit/Services/Semesters/SemesterConfigServiceTest.php b/tests/Unit/Services/Semesters/SemesterConfigServiceTest.php index 78283d74..9f1976e5 100644 --- a/tests/Unit/Services/Semesters/SemesterConfigServiceTest.php +++ b/tests/Unit/Services/Semesters/SemesterConfigServiceTest.php @@ -19,7 +19,7 @@ class SemesterConfigServiceTest extends TestCase ['id' => 3, 'config_key' => 'last_school_day', 'config_value' => '2026-06-01'], ]); - $service = new SemesterConfigService; + $service = new SemesterConfigService(); $this->assertSame('2025-09-01', $service->getFallStart()?->format('Y-m-d')); $this->assertSame('2026-01-20', $service->getSpringStart()?->format('Y-m-d')); $this->assertSame('2026-06-01', $service->getLastSchoolDay()?->format('Y-m-d')); diff --git a/tests/Unit/Services/Semesters/SemesterRangeServiceTest.php b/tests/Unit/Services/Semesters/SemesterRangeServiceTest.php index b2713c41..58400bff 100644 --- a/tests/Unit/Services/Semesters/SemesterRangeServiceTest.php +++ b/tests/Unit/Services/Semesters/SemesterRangeServiceTest.php @@ -14,7 +14,7 @@ class SemesterRangeServiceTest extends TestCase public function test_get_school_year_range_falls_back_when_config_missing(): void { - $service = new SemesterRangeService(new SemesterConfigService); + $service = new SemesterRangeService(new SemesterConfigService()); [$start, $end] = $service->getSchoolYearRange('2025-2026'); $this->assertSame('2025-09-01', $start); @@ -29,7 +29,7 @@ class SemesterRangeServiceTest extends TestCase ['id' => 3, 'config_key' => 'last_school_day', 'config_value' => '2026-06-01'], ]); - $service = new SemesterRangeService(new SemesterConfigService); + $service = new SemesterRangeService(new SemesterConfigService()); $semester = $service->getSemesterForDate('2025-10-01'); $this->assertSame('Fall', $semester); @@ -37,7 +37,7 @@ class SemesterRangeServiceTest extends TestCase public function test_normalize_semester(): void { - $service = new SemesterRangeService(new SemesterConfigService); + $service = new SemesterRangeService(new SemesterConfigService()); $this->assertSame('Fall', $service->normalizeSemester('fall')); $this->assertSame('Spring', $service->normalizeSemester('SPRING')); diff --git a/tests/Unit/Services/Settings/ConfigurationServiceTest.php b/tests/Unit/Services/Settings/ConfigurationServiceTest.php index 521cbd18..a69db2eb 100644 --- a/tests/Unit/Services/Settings/ConfigurationServiceTest.php +++ b/tests/Unit/Services/Settings/ConfigurationServiceTest.php @@ -12,7 +12,7 @@ class ConfigurationServiceTest extends TestCase public function test_store_creates_config(): void { - $service = new ConfigurationService; + $service = new ConfigurationService(); $config = $service->store([ 'config_key' => 'school_year', 'config_value' => '2025-2026', diff --git a/tests/Unit/Services/Settings/SchoolCalendar/SchoolCalendarContextServiceTest.php b/tests/Unit/Services/Settings/SchoolCalendar/SchoolCalendarContextServiceTest.php index 0745351c..b6d21722 100644 --- a/tests/Unit/Services/Settings/SchoolCalendar/SchoolCalendarContextServiceTest.php +++ b/tests/Unit/Services/Settings/SchoolCalendar/SchoolCalendarContextServiceTest.php @@ -9,7 +9,7 @@ class SchoolCalendarContextServiceTest extends TestCase { public function test_event_types_returns_list(): void { - $service = new SchoolCalendarContextService; + $service = new SchoolCalendarContextService(); $types = $service->eventTypes(); $this->assertNotEmpty($types); diff --git a/tests/Unit/Services/Settings/SchoolCalendar/SchoolCalendarFormatterServiceTest.php b/tests/Unit/Services/Settings/SchoolCalendar/SchoolCalendarFormatterServiceTest.php index 3d6ded5e..9a07b15a 100644 --- a/tests/Unit/Services/Settings/SchoolCalendar/SchoolCalendarFormatterServiceTest.php +++ b/tests/Unit/Services/Settings/SchoolCalendar/SchoolCalendarFormatterServiceTest.php @@ -9,7 +9,7 @@ class SchoolCalendarFormatterServiceTest extends TestCase { public function test_format_event_sets_background_for_no_school(): void { - $service = new SchoolCalendarFormatterService; + $service = new SchoolCalendarFormatterService(); $formatted = $service->formatEvent([ 'id' => 1, 'title' => 'No School Day', @@ -27,7 +27,7 @@ class SchoolCalendarFormatterServiceTest extends TestCase public function test_format_event_normalizes_iso_dates_to_plain_calendar_date(): void { - $service = new SchoolCalendarFormatterService; + $service = new SchoolCalendarFormatterService(); $formatted = $service->formatEvent([ 'id' => 2, 'title' => 'First Day of School', diff --git a/tests/Unit/Services/Settings/SchoolCalendar/SchoolCalendarMeetingServiceTest.php b/tests/Unit/Services/Settings/SchoolCalendar/SchoolCalendarMeetingServiceTest.php index 60ed77d3..ddcde851 100644 --- a/tests/Unit/Services/Settings/SchoolCalendar/SchoolCalendarMeetingServiceTest.php +++ b/tests/Unit/Services/Settings/SchoolCalendar/SchoolCalendarMeetingServiceTest.php @@ -48,7 +48,7 @@ class SchoolCalendarMeetingServiceTest extends TestCase ], ]); - $service = new SchoolCalendarMeetingService; + $service = new SchoolCalendarMeetingService(); $rows = $service->listMeetings('2025-2026', 'parent', 10); $this->assertCount(1, $rows); diff --git a/tests/Unit/Services/Settings/SchoolCalendar/SchoolCalendarMutationServiceTest.php b/tests/Unit/Services/Settings/SchoolCalendar/SchoolCalendarMutationServiceTest.php index e9281d32..de7d2afa 100644 --- a/tests/Unit/Services/Settings/SchoolCalendar/SchoolCalendarMutationServiceTest.php +++ b/tests/Unit/Services/Settings/SchoolCalendar/SchoolCalendarMutationServiceTest.php @@ -16,7 +16,7 @@ class SchoolCalendarMutationServiceTest extends TestCase public function test_create_persists_event(): void { - $context = new SchoolCalendarContextService; + $context = new SchoolCalendarContextService(); $notifier = Mockery::mock(SchoolCalendarNotificationService::class); $notifier->shouldReceive('notify')->andReturn([]); @@ -47,7 +47,7 @@ class SchoolCalendarMutationServiceTest extends TestCase 'notification_sent' => 0, ]); - $context = new SchoolCalendarContextService; + $context = new SchoolCalendarContextService(); $notifier = Mockery::mock(SchoolCalendarNotificationService::class); $notifier->shouldReceive('notify')->andReturn([]); diff --git a/tests/Unit/Services/Settings/SchoolCalendar/SchoolCalendarQueryServiceTest.php b/tests/Unit/Services/Settings/SchoolCalendar/SchoolCalendarQueryServiceTest.php index 246c3d06..5510dd39 100644 --- a/tests/Unit/Services/Settings/SchoolCalendar/SchoolCalendarQueryServiceTest.php +++ b/tests/Unit/Services/Settings/SchoolCalendar/SchoolCalendarQueryServiceTest.php @@ -44,7 +44,7 @@ class SchoolCalendarQueryServiceTest extends TestCase ], ]); - $service = new SchoolCalendarQueryService; + $service = new SchoolCalendarQueryService(); $events = $service->listEvents(['school_year' => '2025-2026']); $this->assertCount(1, $events); @@ -84,7 +84,7 @@ class SchoolCalendarQueryServiceTest extends TestCase ], ]); - $service = new SchoolCalendarQueryService; + $service = new SchoolCalendarQueryService(); $events = $service->listEvents(['school_year' => '2025-2026']); $filtered = $service->filterEventsForAudience($events, 'parent'); diff --git a/tests/Unit/Services/Settings/SettingsServiceTest.php b/tests/Unit/Services/Settings/SettingsServiceTest.php index d101ba51..70d4c26d 100644 --- a/tests/Unit/Services/Settings/SettingsServiceTest.php +++ b/tests/Unit/Services/Settings/SettingsServiceTest.php @@ -2,8 +2,10 @@ namespace Tests\Unit\Services\Settings; +use App\Models\Setting; use App\Services\Settings\SettingsService; use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Support\Facades\DB; use Tests\TestCase; class SettingsServiceTest extends TestCase @@ -12,7 +14,7 @@ class SettingsServiceTest extends TestCase public function test_update_persists_settings(): void { - $service = new SettingsService; + $service = new SettingsService(); $result = $service->update([ 'site_name' => 'Alrahma', diff --git a/tests/Unit/Services/Staff/StaffCommandServiceTest.php b/tests/Unit/Services/Staff/StaffCommandServiceTest.php index 523c1993..a2c23b2d 100644 --- a/tests/Unit/Services/Staff/StaffCommandServiceTest.php +++ b/tests/Unit/Services/Staff/StaffCommandServiceTest.php @@ -15,7 +15,7 @@ class StaffCommandServiceTest extends TestCase public function test_create_requires_user(): void { - $service = new StaffCommandService(new GlobalConfigService); + $service = new StaffCommandService(new GlobalConfigService()); $this->expectException(\RuntimeException::class); $service->create([ @@ -44,7 +44,7 @@ class StaffCommandServiceTest extends TestCase 'school_year' => '2025-2026', ]); - $service = new StaffCommandService(new GlobalConfigService); + $service = new StaffCommandService(new GlobalConfigService()); $staff = $service->create([ 'user_id' => $user->id, 'firstname' => 'Staff', diff --git a/tests/Unit/Services/Staff/StaffQueryServiceTest.php b/tests/Unit/Services/Staff/StaffQueryServiceTest.php index 9e5fe4f6..75626690 100644 --- a/tests/Unit/Services/Staff/StaffQueryServiceTest.php +++ b/tests/Unit/Services/Staff/StaffQueryServiceTest.php @@ -41,7 +41,7 @@ class StaffQueryServiceTest extends TestCase 'school_year' => '2025-2026', ]); - $service = new StaffQueryService(new GlobalConfigService); + $service = new StaffQueryService(new GlobalConfigService()); $result = $service->paginate([], 1, 10); $this->assertSame(1, $result['paginator']->total()); diff --git a/tests/Unit/Services/Students/StudentAssignmentServiceTest.php b/tests/Unit/Services/Students/StudentAssignmentServiceTest.php index c7b4010c..a0cb4dcc 100644 --- a/tests/Unit/Services/Students/StudentAssignmentServiceTest.php +++ b/tests/Unit/Services/Students/StudentAssignmentServiceTest.php @@ -18,7 +18,7 @@ class StudentAssignmentServiceTest extends TestCase $studentId = $this->seedStudent(); $classSectionId = $this->seedClassSection(); - $service = new StudentAssignmentService(new StudentConfigService); + $service = new StudentAssignmentService(new StudentConfigService()); $result = $service->assignClasses($studentId, [$classSectionId], false); $this->assertTrue($result['ok']); @@ -44,7 +44,7 @@ class StudentAssignmentServiceTest extends TestCase 'updated_at' => now(), ]); - $service = new StudentAssignmentService(new StudentConfigService); + $service = new StudentAssignmentService(new StudentConfigService()); $result = $service->removeClass($studentId, $classSectionId); $this->assertTrue($result['ok']); diff --git a/tests/Unit/Services/Students/StudentProfileServiceTest.php b/tests/Unit/Services/Students/StudentProfileServiceTest.php index fb6d11b1..aabe1091 100644 --- a/tests/Unit/Services/Students/StudentProfileServiceTest.php +++ b/tests/Unit/Services/Students/StudentProfileServiceTest.php @@ -15,7 +15,7 @@ class StudentProfileServiceTest extends TestCase { $studentId = $this->seedStudent(); - $service = new StudentProfileService; + $service = new StudentProfileService(); $result = $service->updateStudent($studentId, [ 'firstname' => 'Lee', 'lastname' => 'Student', diff --git a/tests/Unit/Services/Students/StudentScoreCardServiceTest.php b/tests/Unit/Services/Students/StudentScoreCardServiceTest.php index 2e71ca52..5dd26889 100644 --- a/tests/Unit/Services/Students/StudentScoreCardServiceTest.php +++ b/tests/Unit/Services/Students/StudentScoreCardServiceTest.php @@ -33,7 +33,7 @@ class StudentScoreCardServiceTest extends TestCase 'created_at' => now(), ]); - $service = new StudentScoreCardService; + $service = new StudentScoreCardService(); $result = $service->scoreCard($studentId); $this->assertTrue($result['ok']); diff --git a/tests/Unit/Services/Subjects/SubjectCurriculumServiceTest.php b/tests/Unit/Services/Subjects/SubjectCurriculumServiceTest.php index f5c21017..239a6dcc 100644 --- a/tests/Unit/Services/Subjects/SubjectCurriculumServiceTest.php +++ b/tests/Unit/Services/Subjects/SubjectCurriculumServiceTest.php @@ -14,7 +14,7 @@ class SubjectCurriculumServiceTest extends TestCase public function test_store_creates_entry(): void { $classId = $this->seedClass(); - $service = new SubjectCurriculumService; + $service = new SubjectCurriculumService(); $entry = $service->store([ 'class_id' => $classId, @@ -44,7 +44,7 @@ class SubjectCurriculumServiceTest extends TestCase 'updated_at' => now(), ]); - $service = new SubjectCurriculumService; + $service = new SubjectCurriculumService(); $entry = $service->update($entryId, [ 'class_id' => $classId, 'subject' => 'quran', diff --git a/tests/Unit/Services/Support/SupportRequestServiceTest.php b/tests/Unit/Services/Support/SupportRequestServiceTest.php index 20122dd8..087cab13 100644 --- a/tests/Unit/Services/Support/SupportRequestServiceTest.php +++ b/tests/Unit/Services/Support/SupportRequestServiceTest.php @@ -40,7 +40,7 @@ class SupportRequestServiceTest extends TestCase $mailer = $this->createMock(EmailDispatchService::class); $mailer->method('send')->willReturn(false); - $service = new SupportRequestService(new GlobalConfigService, $mailer); + $service = new SupportRequestService(new GlobalConfigService(), $mailer); $result = $service->create($user->id, [ 'subject' => 'Help', 'message' => 'Need assistance', diff --git a/tests/Unit/Services/System/CleanupSchedulerServiceTest.php b/tests/Unit/Services/System/CleanupSchedulerServiceTest.php index caff5e84..3ee4a583 100644 --- a/tests/Unit/Services/System/CleanupSchedulerServiceTest.php +++ b/tests/Unit/Services/System/CleanupSchedulerServiceTest.php @@ -13,13 +13,13 @@ class CleanupSchedulerServiceTest extends TestCase { Cache::forget('last_cleanup_run'); - $service = new CleanupSchedulerService; + $service = new CleanupSchedulerService(); $this->assertTrue($service->shouldRun('last_cleanup_run', 2)); } public function test_mark_run_sets_cache(): void { - $service = new CleanupSchedulerService; + $service = new CleanupSchedulerService(); $service->markRun('last_cleanup_run'); $this->assertNotNull(Cache::get('last_cleanup_run')); @@ -29,7 +29,7 @@ class CleanupSchedulerServiceTest extends TestCase { Artisan::shouldReceive('call')->once()->with('users:delete-unverified'); - $service = new CleanupSchedulerService; + $service = new CleanupSchedulerService(); $service->runUnverifiedCleanup(); } } diff --git a/tests/Unit/Services/System/ConfigUpdateServiceTest.php b/tests/Unit/Services/System/ConfigUpdateServiceTest.php index 0e025ffe..04c7ddef 100644 --- a/tests/Unit/Services/System/ConfigUpdateServiceTest.php +++ b/tests/Unit/Services/System/ConfigUpdateServiceTest.php @@ -13,7 +13,7 @@ class ConfigUpdateServiceTest extends TestCase public function test_run_task_rejects_unknown_task(): void { - $service = new ConfigUpdateService; + $service = new ConfigUpdateService(); $result = $service->runTask('unknown', false, false, new DateTimeZone('UTC')); $this->assertFalse($result['ok']); @@ -21,7 +21,7 @@ class ConfigUpdateServiceTest extends TestCase public function test_run_task_sets_config_when_forced(): void { - $service = new ConfigUpdateService; + $service = new ConfigUpdateService(); $result = $service->runTask('enable_attendance_on', false, true, new DateTimeZone('UTC')); $this->assertTrue($result['ok']); diff --git a/tests/Unit/Services/System/GlobalConfigServiceTest.php b/tests/Unit/Services/System/GlobalConfigServiceTest.php index 158a00b9..f9895332 100644 --- a/tests/Unit/Services/System/GlobalConfigServiceTest.php +++ b/tests/Unit/Services/System/GlobalConfigServiceTest.php @@ -18,7 +18,7 @@ class GlobalConfigServiceTest extends TestCase ['config_key' => 'school_year', 'config_value' => '2024-2025'], ]); - $service = new GlobalConfigService; + $service = new GlobalConfigService(); $this->assertSame('Fall', $service->getSemester()); $this->assertSame('2024-2025', $service->getSchoolYear()); diff --git a/tests/Unit/Services/System/TimeServiceTest.php b/tests/Unit/Services/System/TimeServiceTest.php index c06529a4..00c24b1c 100644 --- a/tests/Unit/Services/System/TimeServiceTest.php +++ b/tests/Unit/Services/System/TimeServiceTest.php @@ -9,7 +9,7 @@ class TimeServiceTest extends TestCase { public function test_to_utc_and_to_local_round_trip(): void { - $service = new TimeService; + $service = new TimeService(); $utc = $service->toUtc('2025-01-01 12:00:00', 'America/New_York'); $local = $service->toLocal($utc, 'UTC', 'America/New_York'); diff --git a/tests/Unit/Services/Teachers/TeacherAbsenceServiceTest.php b/tests/Unit/Services/Teachers/TeacherAbsenceServiceTest.php index b03e6f63..c626aea5 100644 --- a/tests/Unit/Services/Teachers/TeacherAbsenceServiceTest.php +++ b/tests/Unit/Services/Teachers/TeacherAbsenceServiceTest.php @@ -2,9 +2,9 @@ namespace Tests\Unit\Services\Teachers; +use App\Services\Staff\StaffTimeOffLinkService; use App\Services\SemesterRangeService; use App\Services\Semesters\SemesterConfigService; -use App\Services\Staff\StaffTimeOffLinkService; use App\Services\Teachers\TeacherAbsenceService; use App\Services\Teachers\TeacherConfigService; use Carbon\Carbon; @@ -23,9 +23,9 @@ class TeacherAbsenceServiceTest extends TestCase $teacherId = $this->seedTeacherUser(); $service = new TeacherAbsenceService( - new TeacherConfigService, - new SemesterRangeService(new SemesterConfigService), - new StaffTimeOffLinkService + new TeacherConfigService(), + new SemesterRangeService(new SemesterConfigService()), + new StaffTimeOffLinkService() ); $data = $service->formData($teacherId); @@ -42,9 +42,9 @@ class TeacherAbsenceServiceTest extends TestCase $teacherId = $this->seedTeacherUser(); $service = new TeacherAbsenceService( - new TeacherConfigService, - new SemesterRangeService(new SemesterConfigService), - new StaffTimeOffLinkService + new TeacherConfigService(), + new SemesterRangeService(new SemesterConfigService()), + new StaffTimeOffLinkService() ); $date = $service->formData($teacherId)['available_dates'][0]; diff --git a/tests/Unit/Services/Teachers/TeacherAssignmentServiceTest.php b/tests/Unit/Services/Teachers/TeacherAssignmentServiceTest.php index aadd15d7..73344c38 100644 --- a/tests/Unit/Services/Teachers/TeacherAssignmentServiceTest.php +++ b/tests/Unit/Services/Teachers/TeacherAssignmentServiceTest.php @@ -18,7 +18,7 @@ class TeacherAssignmentServiceTest extends TestCase $teacherId = $this->seedTeacherUser(); $classSectionId = $this->seedClassSection(); - $service = new TeacherAssignmentService(new TeacherConfigService); + $service = new TeacherAssignmentService(new TeacherConfigService()); $result = $service->assign([ 'teacher_id' => $teacherId, 'class_section_id' => $classSectionId, @@ -51,7 +51,7 @@ class TeacherAssignmentServiceTest extends TestCase 'updated_at' => now(), ]); - $service = new TeacherAssignmentService(new TeacherConfigService); + $service = new TeacherAssignmentService(new TeacherConfigService()); $result = $service->delete([ 'teacher_id' => $teacherId, 'class_section_id' => $classSectionId, diff --git a/tests/Unit/Services/Teachers/TeacherDashboardServiceTest.php b/tests/Unit/Services/Teachers/TeacherDashboardServiceTest.php index cb210b59..f8880cb2 100644 --- a/tests/Unit/Services/Teachers/TeacherDashboardServiceTest.php +++ b/tests/Unit/Services/Teachers/TeacherDashboardServiceTest.php @@ -20,7 +20,7 @@ class TeacherDashboardServiceTest extends TestCase $this->seedTeacherAssignment($teacherId, $classSectionId); $this->seedStudent($teacherId, $classSectionId); - $service = new TeacherDashboardService(new TeacherConfigService); + $service = new TeacherDashboardService(new TeacherConfigService()); $data = $service->classView($teacherId, $classSectionId); $this->assertSame($classSectionId, $data['active_class_section_id']); diff --git a/tests/Unit/Services/Ui/UiStyleServiceTest.php b/tests/Unit/Services/Ui/UiStyleServiceTest.php index 44d11bf9..0cea331a 100644 --- a/tests/Unit/Services/Ui/UiStyleServiceTest.php +++ b/tests/Unit/Services/Ui/UiStyleServiceTest.php @@ -29,7 +29,7 @@ class UiStyleServiceTest extends TestCase 'school_year' => '2025-2026', ]); - $service = new UiStyleService; + $service = new UiStyleService(); $prefs = $service->update($user->id, [ 'accent' => 'blue', 'menu' => 'custom', diff --git a/tests/Unit/Services/Users/InactiveUserCleanupServiceTest.php b/tests/Unit/Services/Users/InactiveUserCleanupServiceTest.php index 2b09001a..82fd3d16 100644 --- a/tests/Unit/Services/Users/InactiveUserCleanupServiceTest.php +++ b/tests/Unit/Services/Users/InactiveUserCleanupServiceTest.php @@ -40,7 +40,7 @@ class InactiveUserCleanupServiceTest extends TestCase 'role_id' => 1, ]); - $service = new InactiveUserCleanupService; + $service = new InactiveUserCleanupService(); $result = $service->cleanup(15); $this->assertSame(1, $result['deleted_users']); diff --git a/tests/Unit/Services/Users/UserListServiceTest.php b/tests/Unit/Services/Users/UserListServiceTest.php index 1efd9208..565d7a42 100644 --- a/tests/Unit/Services/Users/UserListServiceTest.php +++ b/tests/Unit/Services/Users/UserListServiceTest.php @@ -2,6 +2,7 @@ namespace Tests\Unit\Services\Users; +use App\Models\User; use App\Services\Users\UserListService; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\DB; diff --git a/tests/Unit/Services/Whatsapp/WhatsappContactServiceTest.php b/tests/Unit/Services/Whatsapp/WhatsappContactServiceTest.php index 8cdabaf6..77152bf1 100644 --- a/tests/Unit/Services/Whatsapp/WhatsappContactServiceTest.php +++ b/tests/Unit/Services/Whatsapp/WhatsappContactServiceTest.php @@ -47,7 +47,7 @@ class WhatsappContactServiceTest extends TestCase 'updated_at' => now(), ]); - $service = new WhatsappContactService(new WhatsappContextService); + $service = new WhatsappContactService(new WhatsappContextService()); $contacts = $service->listParentContacts('2025-2026', 'Fall'); $this->assertCount(2, $contacts); diff --git a/tests/Unit/Services/Whatsapp/WhatsappContextServiceTest.php b/tests/Unit/Services/Whatsapp/WhatsappContextServiceTest.php index 1d05393e..f4b7c0d5 100644 --- a/tests/Unit/Services/Whatsapp/WhatsappContextServiceTest.php +++ b/tests/Unit/Services/Whatsapp/WhatsappContextServiceTest.php @@ -13,7 +13,7 @@ class WhatsappContextServiceTest extends TestCase public function test_school_year_aliases_generate_expected_values(): void { - $service = new WhatsappContextService; + $service = new WhatsappContextService(); [$aliases, $canonical, $startYear] = $service->schoolYearAliases('2025/2026'); $this->assertContains('2025-2026', $aliases); @@ -33,7 +33,7 @@ class WhatsappContextServiceTest extends TestCase 'updated_at' => now(), ]); - $service = new WhatsappContextService; + $service = new WhatsappContextService(); $this->assertTrue($service->hasRosterForYear('2025-2026')); $this->assertFalse($service->hasRosterForYear('2030-2031')); diff --git a/tests/Unit/Services/Whatsapp/WhatsappInviteBundleServiceTest.php b/tests/Unit/Services/Whatsapp/WhatsappInviteBundleServiceTest.php index a8f399a6..903f151c 100644 --- a/tests/Unit/Services/Whatsapp/WhatsappInviteBundleServiceTest.php +++ b/tests/Unit/Services/Whatsapp/WhatsappInviteBundleServiceTest.php @@ -77,7 +77,7 @@ class WhatsappInviteBundleServiceTest extends TestCase ], ]; - $service = new WhatsappInviteBundleService; + $service = new WhatsappInviteBundleService(); $bundle = $service->bundleForParent(10, $linkBySection, '2025-2026', 'Fall'); $this->assertNotNull($bundle); @@ -87,7 +87,7 @@ class WhatsappInviteBundleServiceTest extends TestCase public function test_consolidate_bundles_merges_by_parent(): void { - $service = new WhatsappInviteBundleService; + $service = new WhatsappInviteBundleService(); $merged = $service->consolidateBundles([ [ diff --git a/tests/Unit/Services/Whatsapp/WhatsappInviteNotificationServiceTest.php b/tests/Unit/Services/Whatsapp/WhatsappInviteNotificationServiceTest.php index f486bc75..83eb0deb 100644 --- a/tests/Unit/Services/Whatsapp/WhatsappInviteNotificationServiceTest.php +++ b/tests/Unit/Services/Whatsapp/WhatsappInviteNotificationServiceTest.php @@ -14,7 +14,7 @@ class WhatsappInviteNotificationServiceTest extends TestCase public function test_dispatch_returns_error_when_no_listeners(): void { - $service = new WhatsappInviteNotificationService; + $service = new WhatsappInviteNotificationService(); $result = $service->dispatch([], '2025-2026', 'Fall'); $this->assertFalse($result['ok']); @@ -22,9 +22,10 @@ class WhatsappInviteNotificationServiceTest extends TestCase public function test_dispatch_triggers_event_when_listener_registered(): void { - Event::listen(WhatsappInvitesSend::class, static function () {}); + Event::listen(WhatsappInvitesSend::class, static function () { + }); - $service = new WhatsappInviteNotificationService; + $service = new WhatsappInviteNotificationService(); $result = $service->dispatch([[ 'parent' => ['id' => 10], 'emails' => ['parent1@example.com'], diff --git a/tests/Unit/Services/Whatsapp/WhatsappInviteServiceTest.php b/tests/Unit/Services/Whatsapp/WhatsappInviteServiceTest.php index b942eb0c..375a7a8e 100644 --- a/tests/Unit/Services/Whatsapp/WhatsappInviteServiceTest.php +++ b/tests/Unit/Services/Whatsapp/WhatsappInviteServiceTest.php @@ -19,12 +19,13 @@ class WhatsappInviteServiceTest extends TestCase public function test_send_returns_success_for_parent_mode(): void { $this->seedInviteData(); - Event::listen(WhatsappInvitesSend::class, static function () {}); + Event::listen(WhatsappInvitesSend::class, static function () { + }); $service = new WhatsappInviteService( - new WhatsappContextService, - new WhatsappInviteBundleService, - new WhatsappInviteNotificationService + new WhatsappContextService(), + new WhatsappInviteBundleService(), + new WhatsappInviteNotificationService() ); $result = $service->send([ diff --git a/tests/Unit/Services/Whatsapp/WhatsappLinkServiceTest.php b/tests/Unit/Services/Whatsapp/WhatsappLinkServiceTest.php index 8d0377b7..1ae3e506 100644 --- a/tests/Unit/Services/Whatsapp/WhatsappLinkServiceTest.php +++ b/tests/Unit/Services/Whatsapp/WhatsappLinkServiceTest.php @@ -14,7 +14,7 @@ class WhatsappLinkServiceTest extends TestCase public function test_upsert_creates_link(): void { - $service = new WhatsappLinkService(new WhatsappContextService); + $service = new WhatsappLinkService(new WhatsappContextService()); $link = $service->upsert([ 'class_section_id' => 300, @@ -57,7 +57,7 @@ class WhatsappLinkServiceTest extends TestCase ], ]); - $service = new WhatsappLinkService(new WhatsappContextService); + $service = new WhatsappLinkService(new WhatsappContextService()); $page = $service->paginate(['active' => true]); $this->assertCount(1, $page->items()); diff --git a/tests/Unit/Services/Whatsapp/WhatsappMembershipServiceTest.php b/tests/Unit/Services/Whatsapp/WhatsappMembershipServiceTest.php index 5068c1e0..6b122d10 100644 --- a/tests/Unit/Services/Whatsapp/WhatsappMembershipServiceTest.php +++ b/tests/Unit/Services/Whatsapp/WhatsappMembershipServiceTest.php @@ -13,7 +13,7 @@ class WhatsappMembershipServiceTest extends TestCase public function test_update_membership_saves_primary(): void { - $service = new WhatsappMembershipService; + $service = new WhatsappMembershipService(); $result = $service->updateMembership([ 'class_section_id' => 200, @@ -36,7 +36,7 @@ class WhatsappMembershipServiceTest extends TestCase { Schema::shouldReceive('hasTable')->with('whatsapp_group_memberships')->andReturn(false); - $service = new WhatsappMembershipService; + $service = new WhatsappMembershipService(); $result = $service->updateMembership([ 'class_section_id' => 200, 'primary_id' => 10,